Independent sigpro vs sigpro-ui
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s

This commit is contained in:
2026-05-04 16:39:57 +02:00
parent 817de6a0ee
commit e6b172efa1
26 changed files with 1596 additions and 2368 deletions

View File

@@ -14,7 +14,7 @@ Based in SigPro Core
**SigPro UI** is a lightweight, ultra-fast, and reactive component library built for the **SigPro** reactivity core. It provides a set of high-quality, accessible, and themeable UI components with **zero external dependencies** - everything is included. **SigPro UI** is a lightweight, ultra-fast, and reactive component library built for the **SigPro** reactivity core. It provides a set of high-quality, accessible, and themeable UI components with **zero external dependencies** - everything is included.
**SigPro UI** is a complete, full self-contained UI library + reactive core in under **35KB gzip** (< 13KB JS + < 22KB CSS). 🎉 **SigPro UI** is a complete, full self-contained UI library in under **35KB gzip** (JS + CSS). 🎉
Unlike heavy frameworks, SigPro UI focuses on a **"Zero-Build"** philosophy, allowing you to build complex reactive interfaces with a functional, declarative syntax that runs natively in the browser. Unlike heavy frameworks, SigPro UI focuses on a **"Zero-Build"** philosophy, allowing you to build complex reactive interfaces with a functional, declarative syntax that runs natively in the browser.
@@ -36,19 +36,20 @@ Unlike heavy frameworks, SigPro UI focuses on a **"Zero-Build"** philosophy, all
### ESM / Bundler ### ESM / Bundler
```bash ```bash
npm install sigpro-ui npm install sigpro sigpro-ui
# or # or
bun add sigpro-ui bun add sigpro sigpro-ui
``` ```
### CDN (Browser - All-in-One) ### CDN (Browser - All-in-One)
```html ```html
<script src="https://unpkg.com/sigpro-ui@latest/dist/sigpro-ui.min.js"></script>
<link href="https://unpkg.com/sigpro-ui@latest/dist/sigpro-ui.min.css" rel="stylesheet"> <link href="https://unpkg.com/sigpro-ui@latest/dist/sigpro-ui.min.css" rel="stylesheet">
<script src="https://unpkg.com/sigpro-ui@latest/dist/sigpro.min.js"></script>
<script src="https://unpkg.com/sigpro-ui@latest/dist/sigpro-ui.min.js"></script>
``` ```
**That's it!** The CDN version includes SigPro core internally - no additional scripts needed. **That's it!** no additional scripts needed.
--- ---
@@ -57,8 +58,9 @@ bun add sigpro-ui
### ESM / Modular (Tree Shaking) ### ESM / Modular (Tree Shaking)
```javascript ```javascript
import { $, mount, watch, h, Button, Modal, Input, Alert } from "sigpro-ui"; import { $, mount, watch, h } from "sigpro"; // Core functions
import "sigpro-ui/css"; import { Button, Modal, Input, Alert } from "sigpro-ui"; // Components
import "sigpro-ui/css"; // CSS
const App = () => { const App = () => {
const show = $(false); const show = $(false);
@@ -111,6 +113,7 @@ mount(App, "#app");
- `when()` - Conditional rendering - `when()` - Conditional rendering
- `each()` - List rendering - `each()` - List rendering
- `mount()` - Mount components to DOM - `mount()` - Mount components to DOM
- `batch()` - Batch multiple reactive updates into a single flush
> [SigPro Core Docs](https://sigpro.natxocc.com/#/) > [SigPro Core Docs](https://sigpro.natxocc.com/#/)
@@ -124,23 +127,23 @@ mount(App, "#app");
- And 30+ more! - And 30+ more!
### Utilities ### Utilities
- `tt()` - i18n translation function (ES/EN) - `t()` - i18n translation function
- `Locale()` - Set global language - `setLocale()` - Set global language
--- ---
## Language Support ## Language Support
Built-in i18n with Spanish and English: Built-in i18n with custom language:
```javascript ```javascript
import { tt, Locale } from "sigpro-ui"; import { tt, setLocale } from "sigpro-ui";
// Change locale (default is 'es') // Change locale
Locale('en'); setLocale('en');
// Use translations // Use translations
Button({}, tt('close')); Button({}, t('close'));
Input({ placeholder: tt('search') }); Input({ placeholder: tt('search') });
``` ```

250
dist/sigpro-ui.editor.esm.js vendored Normal file
View File

@@ -0,0 +1,250 @@
// src/editor.js
import { $, isFunc, h } from "./sigpro.js";
// src/helpers.js
var val = (val2) => typeof val2 === "function" ? val2() : val2;
var cls = (...classes) => classes.filter(Boolean).join(" ").trim();
// src/editor.js
var Editor = (p) => {
const { value, class: extraClass } = p;
let editorRef = null;
let savedRange = null;
const isSource = $(false);
const source = $("");
const count = $(0);
const refreshTick = $(0);
const showEmojis = $(false);
const emojis = ["\uD83D\uDE00", "\uD83D\uDE0A", "\uD83D\uDE09", "\uD83E\uDDD0", "\uD83D\uDE2E", "\uD83E\uDD14", "\uD83D\uDE05", "\uD83D\uDE02", "\uD83D\uDE0D", "\uD83D\uDE18", "\uD83E\uDD70", "\uD83D\uDC4D", "\uD83D\uDC4E", "\uD83D\uDC4C", "\uD83E\uDD1D", "\uD83E\uDD1E", "\uD83D\uDC4B", "\uD83D\uDC4F", "\uD83D\uDE4C", "\uD83D\uDE4F", "\uD83D\uDCAA", "☝️", "\uD83D\uDC47", "\uD83D\uDC48", "\uD83D\uDC49", "\uD83D\uDD95", "✅", "⚠️", "\uD83D\uDE80", "\uD83D\uDCE2", "✉️", "❤️"];
const saveSelection = () => {
const sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount)
savedRange = sel.getRangeAt(0);
};
const restoreSelection = () => {
if (savedRange) {
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(savedRange);
}
};
const triggerRefresh = () => {
refreshTick(refreshTick() + 1);
if (editorRef)
count(editorRef.innerText.length);
};
const notify = () => {
if (!editorRef)
return;
const html = editorRef.innerHTML;
if (isFunc(value))
value(html);
else
p.onchange?.(html);
triggerRefresh();
};
const exec = (cmd, val2 = null) => {
if (!editorRef)
return;
editorRef.focus();
if (savedRange)
restoreSelection();
document.execCommand(cmd, false, val2);
savedRange = null;
notify();
};
const openLightbox = (src) => {
const overlay = document.createElement("div");
overlay.style = `position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:9999;display:flex;align-items:center;justify-content:center;cursor:zoom-out;`;
const img = document.createElement("img");
img.src = src;
img.style = `max-width:95%;max-height:95%;box-shadow:0 0 30px rgba(0,0,0,0.5);border-radius:4px;`;
overlay.onclick = () => document.body.removeChild(overlay);
overlay.appendChild(img);
document.body.appendChild(overlay);
};
const handleUpload = (file) => {
if (!file)
return;
const reader = new FileReader;
reader.onload = (re) => {
if (file.type.startsWith("image/")) {
const imgHtml = `<div style="display:inline-block; resize:both; overflow:hidden; vertical-align:bottom; line-height:0; width:200px; height:auto; border:1px dashed #ccc; padding:2px; cursor:pointer;" class="resizable-img-container"><img src="${re.target.result}" style="width:100%; height:100%; object-fit:contain; pointer-events:none;"></div>&nbsp;`;
exec("insertHTML", imgHtml);
} else {
const linkHtml = `<a href="${re.target.result}" download="${file.name}" contenteditable="false" style="display:inline-flex; align-items:center; gap:5px; padding:4px 8px; border:1px solid #ccc; border-radius:4px; background:#f9f9f9; text-decoration:none; color:#333; font-size:12px; margin:2px; cursor:pointer;"><span class="icon-[lucide--paperclip] w-3 h-3"></span>${file.name}</a>&nbsp;`;
exec("insertHTML", linkHtml);
}
};
reader.readAsDataURL(file);
};
const queryState = (cmd, val2 = null) => {
refreshTick();
if (!editorRef || isSource())
return false;
try {
if (cmd === "formatBlock") {
let node = window.getSelection().getRangeAt(0).commonAncestorContainer;
while (node && node !== editorRef) {
if (node.nodeType === 1 && node.tagName === val2)
return true;
node = node.parentNode;
}
return false;
}
return document.queryCommandState(cmd);
} catch (e) {
return false;
}
};
const toolbar = h("div", { class: "flex flex-wrap items-center gap-1 p-2 border-b border-base-300 bg-base-200 sticky top-0 z-20" }, [
h("div", { class: "flex flex-wrap gap-1 flex-1 items-center" }, [
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${queryState("bold") ? "btn-active bg-primary/20" : ""}`, onclick: () => exec("bold") }, h("span", { class: "icon-[lucide--bold]" })),
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${queryState("italic") ? "btn-active bg-primary/20" : ""}`, onclick: () => exec("italic") }, h("span", { class: "icon-[lucide--italic]" })),
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${queryState("underline") ? "btn-active bg-primary/20" : ""}`, onclick: () => exec("underline") }, h("span", { class: "icon-[lucide--underline]" })),
h("input", { type: "color", class: "w-5 h-5 p-0 border-0 bg-transparent cursor-pointer", oninput: (e) => exec("foreColor", e.target.value) }),
h("span", { class: "w-px h-5 bg-base-300 mx-1" }),
h("button", {
type: "button",
class: "btn btn-ghost btn-xs",
onclick: () => exec("justifyLeft")
}, h("span", { class: "icon-[lucide--align-left]" })),
h("button", {
type: "button",
class: "btn btn-ghost btn-xs",
onclick: () => exec("justifyCenter")
}, h("span", { class: "icon-[lucide--align-center]" })),
h("button", {
type: "button",
class: "btn btn-ghost btn-xs",
onclick: () => exec("justifyRight")
}, h("span", { class: "icon-[lucide--align-right]" })),
h("span", { class: "w-px h-5 bg-base-300 mx-1" }),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("insertUnorderedList") }, h("span", { class: "icon-[lucide--list]" })),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("insertOrderedList") }, h("span", { class: "icon-[lucide--list-ordered]" })),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("outdent") }, h("span", { class: "icon-[lucide--indent-decrease]" })),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("indent") }, h("span", { class: "icon-[lucide--indent-increase]" })),
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${queryState("formatBlock", "BLOCKQUOTE") ? "btn-active" : ""}`, onclick: () => exec("formatBlock", queryState("formatBlock", "BLOCKQUOTE") ? "P" : "BLOCKQUOTE") }, h("span", { class: "icon-[lucide--quote]" })),
h("span", { class: "w-px h-5 bg-base-300 mx-1" }),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => {
const url = window.prompt("URL:");
if (url)
exec("createLink", url);
} }, h("span", { class: "icon-[lucide--link]" })),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => {
const input = document.createElement("input");
input.type = "file";
input.onchange = (e) => handleUpload(e.target.files[0]);
input.click();
} }, h("span", { class: "icon-[lucide--paperclip]" })),
h("div", { class: "relative" }, [
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: (e) => {
e.stopPropagation();
saveSelection();
showEmojis(!showEmojis());
} }, h("span", { class: "icon-[lucide--smile]" })),
h("div", { class: "absolute top-full left-0 mt-1 p-2 bg-base-100 border border-base-300 shadow-xl rounded-box w-52 z-50 flex flex-wrap gap-1", style: () => showEmojis() ? "display:flex" : "display:none" }, emojis.map((emo) => h("span", { class: "cursor-pointer hover:bg-base-200 p-1 rounded text-lg", onclick: (e) => {
e.stopPropagation();
exec("insertText", emo);
showEmojis(false);
} }, emo)))
]),
h("span", { class: "w-px h-5 bg-base-300 mx-1" }),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("undo") }, h("span", { class: "icon-[lucide--undo-2]" })),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("redo") }, h("span", { class: "icon-[lucide--redo-2]" }))
]),
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${isSource() ? "btn-active" : ""}`, onclick: () => {
if (!isSource())
source(editorRef?.innerHTML || "");
else if (editorRef) {
editorRef.innerHTML = source();
notify();
}
isSource(!isSource());
} }, h("span", { class: "icon-[lucide--code-2]" }))
]);
if (typeof document !== "undefined" && !document.getElementById("editor-styles")) {
const style = document.createElement("style");
style.id = "editor-styles";
style.textContent = `
[contenteditable="true"] div,
[contenteditable="true"] p {
margin: 0;
padding: 0;
}
`;
document.head.appendChild(style);
}
return h("div", { class: cls("border border-base-300 rounded-box bg-base-100 overflow-hidden shadow-sm flex flex-col", extraClass) }, [
toolbar,
h("div", { class: "relative flex-1 flex flex-col", onclick: () => showEmojis(false) }, [
h("div", {
ref: (el) => {
if (!editorRef && el) {
editorRef = el;
el.innerHTML = val(value) || "";
document.execCommand("defaultParagraphSeparator", false, "br");
el.addEventListener("click", (e) => {
const container = e.target.closest(".resizable-img-container");
if (container) {
const img = container.querySelector("img");
if (img)
openLightbox(img.src);
}
});
}
},
style: () => `min-height:22rem;${isSource() ? "display:none" : ""}`,
class: "p-4 outline-none text-base-content leading-relaxed [&>div]:m-0 [&>p]:m-0 [&>div]:min-h-[1em] [&_.resizable-img-container]:hover:border-primary [&_blockquote]:border-l-4 [&_blockquote]:border-base-300 [&_blockquote]:pl-4 [&_blockquote]:italic [&_ul]:list-disc [&_ul]:pl-8 [&_ol]:list-decimal [&_ol]:pl-8",
contenteditable: "true",
oninput: notify,
onkeydown: (e) => {
if (e.key === "Tab") {
e.preventDefault();
exec("indent");
}
},
onkeyup: () => {
triggerRefresh();
saveSelection();
},
onclick: (e) => {
triggerRefresh();
saveSelection();
e.stopPropagation();
},
onmouseup: () => {
notify();
saveSelection();
},
onpaste: (e) => {
e.preventDefault();
const text = e.clipboardData.getData("text/plain");
exec("insertText", text);
},
ondragover: (e) => e.preventDefault(),
ondrop: (e) => {
e.preventDefault();
handleUpload(e.dataTransfer.files[0]);
}
}),
h("textarea", {
class: "w-full flex-1 min-h-[22rem] p-4 outline-none font-mono text-sm bg-base-200 border-0",
style: () => isSource() ? "" : "display:none",
value: source,
oninput: (e) => {
source(e.target.value);
if (editorRef)
editorRef.innerHTML = e.target.value;
p.onchange?.(e.target.value);
}
})
]),
h("div", { class: "px-3 py-1 border-t border-base-300 bg-base-100/50 text-[10px] text-right text-base-content/60 italic" }, [
h("span", () => `${count()}`)
])
]);
};
export {
Editor
};

7
dist/sigpro-ui.editor.esm.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1085
dist/sigpro-ui.esm.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

952
dist/sigpro-ui.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -6,12 +6,12 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" /> <path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" />
</svg> </svg>
</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 (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 reactivity."</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://git.natxocc.com/natxocc/sigpro-ui')" class="btn btn-outline btn-lg border-base-300 hover:bg-base-300 hover:text-base-content">Gitea</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>
</div> </div>
</div> </div>
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full -z-0 opacity-10 pointer-events-none"> <div class="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full -z-0 opacity-10 pointer-events-none">

View File

@@ -632,9 +632,9 @@ mount(
```js ```js
mount( mount(
div({ class: "flex gap-4 text-xl" }, [ div({ class: "flex gap-4 text-xl" }, [
Icon("icon-[lucide--home]"), Icon({},"icon-[lucide--home]"),
Icon("icon-[lucide--settings]"), Icon({},"icon-[lucide--settings]"),
Icon("❤️"), // emoji fallback "❤️", // emoji fallback
]), ]),
"#demo-icon", "#demo-icon",
); );
@@ -649,7 +649,7 @@ mount(
```js ```js
mount( mount(
Indicator({ value: "3" }, [ Indicator({ value: "3" }, [
Button({ class: "btn-circle" }, Icon("icon-[lucide--bell]")), Button({ class: "btn-circle" }, Icon({},"icon-[lucide--bell]")),
]), ]),
"#demo-indicator", "#demo-indicator",
); );
@@ -671,14 +671,14 @@ mount(
label: "Username", label: "Username",
float: true, float: true,
value: username, value: username,
left: Icon("icon-[lucide--user]"), left: Icon({},"icon-[lucide--user]"),
}), }),
Input({ Input({
type: "password", type: "password",
label: "Password", label: "Password",
float: true, float: true,
value: password, value: password,
left: Icon("icon-[lucide--lock]"), left: Icon({},"icon-[lucide--lock]"),
}), }),
]), ]),
"#demo-input", "#demo-input",
@@ -813,7 +813,7 @@ mount(
div({ class: "flex-1" }, a({ class: "btn btn-ghost text-xl" }, "SigPro")), div({ class: "flex-1" }, a({ class: "btn btn-ghost text-xl" }, "SigPro")),
div( div(
{ class: "flex-none" }, { class: "flex-none" },
Button({ class: "btn-square btn-ghost" }, Icon("icon-[lucide--menu]")), Button({ class: "btn-square btn-ghost" }, Icon({},"icon-[lucide--menu]")),
), ),
]), ]),
"#demo-navbar", "#demo-navbar",

View File

@@ -67,6 +67,7 @@
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script> <script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify@4.13.0/lib/docsify.min.js"></script> <script src="//cdn.jsdelivr.net/npm/docsify@4.13.0/lib/docsify.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify-copy-code/dist/docsify-copy-code.min.js"></script> <script src="//cdn.jsdelivr.net/npm/docsify-copy-code/dist/docsify-copy-code.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sigpro@latest/dist/sigpro.min.js"></script>
<script src="./sigpro-ui.min.js"></script> <script src="./sigpro-ui.min.js"></script>
</body> </body>
</html> </html>

View File

@@ -5,7 +5,7 @@ Follow these steps to integrate **SigPro-UI** into your project.
--- ---
!> **📘 Core Concepts** !> **📘 Core Concepts**
SigProUI is built on top of the `SigPro` reactive core. To learn how to create signals, manage reactivity, and structure your application logic, check out the [SigPro documentation](https://sigpro.natxocc.com/#/). SigProUI is based on the `SigPro` reactive core. To learn how to create signals, manage reactivity, and structure your application logic, check out the [SigPro documentation](https://sigpro.natxocc.com/#/).
--- ---
@@ -13,10 +13,10 @@ SigProUI is built on top of the `SigPro` reactive core. To learn how to create s
```bash ```bash
# ESM / Bundler (tree-shaking) # ESM / Bundler (tree-shaking)
npm install sigpro-ui npm install sigpro sigpro-ui
# Or with bun # Or with bun
bun add sigpro-ui bun add sigpro sigpro-ui
``` ```
## 2. Import and use in your app ## 2. Import and use in your app
@@ -24,7 +24,8 @@ bun add sigpro-ui
### ESM / Module usage ### ESM / Module usage
```javascript ```javascript
import { $, Input, Button, Alert } from "sigpro-ui"; import { $ } from "sigpro"; // Core functions
import { Input, Button, Alert } from "sigpro-ui"; // Components
import "sigpro-ui/css"; import "sigpro-ui/css";
const App = () => { const App = () => {
@@ -61,6 +62,7 @@ mount(App, "#app");
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SigProUI Demo</title> <title>SigProUI Demo</title>
<!-- Load CSS -->
<link href="https://unpkg.com/sigpro-ui@latest/dist/sigpro-ui.min.css" rel="stylesheet"> <link href="https://unpkg.com/sigpro-ui@latest/dist/sigpro-ui.min.css" rel="stylesheet">
<style> <style>
body { padding: 2rem; } body { padding: 2rem; }
@@ -68,7 +70,8 @@ mount(App, "#app");
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<!-- Load sigpro and sigpro-ui -->
<script src="https://unpkg.com/sigpro-ui@latest/dist/sigpro.min.js"></script>
<script src="https://unpkg.com/sigpro-ui@latest/dist/sigpro-ui.min.js"></script> <script src="https://unpkg.com/sigpro-ui@latest/dist/sigpro-ui.min.js"></script>
<script> <script>
const App = () => { const App = () => {
@@ -133,10 +136,10 @@ mount(App, "#app");
SigProUI includes built-in i18n with Spanish and English: SigProUI includes built-in i18n with Spanish and English:
```javascript ```javascript
import { t, Locale } from 'sigpro-ui'; import { t, setLocale } from 'sigpro-ui';
// Change locale (default is 'es') // Change locale (default is 'es')
Locale('en'); setLocale('en');
// Use translations // Use translations
Button({}, t('close')); Button({}, t('close'));
@@ -158,6 +161,6 @@ import type { ButtonProps, InputProps } from 'sigpro-ui';
| :--- | :--- | | :--- | :--- |
| Components don't look styled | Make sure you're loading the CSS (`sigpro-ui.min.css`) | | Components don't look styled | Make sure you're loading the CSS (`sigpro-ui.min.css`) |
| CDN functions not working | All core functions (`$`, `h`, `watch`, etc.) are available globally | | CDN functions not working | All core functions (`$`, `h`, `watch`, etc.) are available globally |
| Locale not working | Set locale with `Locale('en')` before using translations | | setLocale not working | Set locale with `setLocale('en')` before using translations |
**Happy coding!** 🎉 **Happy coding!** 🎉

View File

@@ -326,6 +326,9 @@ Typically contains `DropdownButton` and `DropdownContent`.
## Editor (Rich Text) ## Editor (Rich Text)
`Editor(props)` `Editor(props)`
> In ESM tree shaking, editor must be loaded `import { Editor } from "sigpro\editor"`
> In IIFE / UMD, Editor is included.
| Prop | Type | Default | Description | | Prop | Type | Default | Description |
|------------|-------------------|---------|------------------------------------------------------------| |------------|-------------------|---------|------------------------------------------------------------|
@@ -789,6 +792,6 @@ Displays an 8×8 grid of preset colours.
## Utility Functions ## Utility Functions
- **`Locale(lang)`** Signal to read/set the current language (`"en"` or `"es"`). - **`setLocale(lang)`** Signal to read/set the current language (`"en"`, `"es"` ...).
- **`t(key)`** Returns a reactive translation string from the builtin dictionary based on current locale. - **`t(key)`** Returns a reactive translation string from the builtin dictionary based on current locale.
``` ```

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,9 @@
{ {
"name": "sigpro-ui", "name": "sigpro-ui",
"version": "1.2.12", "version": "1.2.15",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"author": { "author": {
"name": "NatxoCC", "name": "NatxoCC",
"email": "sigpro@natxocc.com", "email": "sigpro@natxocc.com",
"url": "https://sigpro.natxocc.com/ui" "url": "https://sigpro.natxocc.com/ui"
@@ -19,6 +19,9 @@
"script": "./dist/sigpro-ui.min.js", "script": "./dist/sigpro-ui.min.js",
"types": "./index.d.ts" "types": "./index.d.ts"
}, },
"./editor": {
"import": "./dist/sigpro-ui.editor.esm.js"
},
"./css": "./dist/sigpro-ui.min.css" "./css": "./dist/sigpro-ui.min.css"
}, },
"files": [ "files": [
@@ -39,15 +42,16 @@
"scripts": { "scripts": {
"del": "bun pm cache rm && rm -f bun.lockb && rm -f bun.lock", "del": "bun pm cache rm && rm -f bun.lockb && rm -f bun.lock",
"clean": "rm -rf ./dist ./css/*.css ./docs/*.js ./docs/*.css", "clean": "rm -rf ./dist ./css/*.css ./docs/*.js ./docs/*.css",
"build:css": "tailwindcss -i ./sigpro-ui.css -o ./dist/sigpro-ui.css --content './src/**/*.js' && du -h ./dist/sigpro-ui.css", "build:css": "tailwindcss -i ./src/sigpro-ui.css -o ./dist/sigpro-ui.css --content './src/**/*.js' && du -h ./dist/sigpro-ui.css",
"build:cssmin": "tailwindcss -i ./sigpro-ui.css -o ./dist/sigpro-ui.min.css --content './src/**/*.js' --minify && du -h ./dist/sigpro-ui.css", "build:cssmin": "tailwindcss -i ./src/sigpro-ui.css -o ./dist/sigpro-ui.min.css --content './src/**/*.js' --minify && du -h ./dist/sigpro-ui.css",
"build:js:iife": "bun build ./sigpro-ui_IIFE.js --bundle --outfile=./dist/sigpro-ui.js --format=iife --global-name=SigProUI", "build:js:iife": "esbuild ./src/build_umd.js --bundle --outfile=./dist/sigpro-ui.js --format=iife --global-name=spui --external:./src/sigpro.js=window",
"build:js:iife:min": "bun build ./sigpro-ui_IIFE.js --bundle --outfile=./dist/sigpro-ui.min.js --format=iife --global-name=SigProUI --minify", "build:js:iife:min": "esbuild ./src/build_umd.js --bundle --outfile=./dist/sigpro-ui.min.js --format=iife --global-name=spui --minify --external:./src/sigpro.js=window",
"build:js:esm": "bun build ./sigpro-ui_ESM.js --bundle --outfile=./dist/sigpro-ui.esm.js --format=esm", "build:js:esm": "bun build ./src/build_esm.js --bundle --outfile=./dist/sigpro-ui.esm.js --format=esm --external ./src/sigpro.js",
"build:js:esm:min": "bun build ./sigpro-ui_ESM.js --bundle --outfile=./dist/sigpro-ui.esm.min.js --format=esm --minify", "build:js:esm:min": "bun build ./src/build_esm.js --bundle --outfile=./dist/sigpro-ui.esm.min.js --format=esm --minify --external ./src/sigpro.js",
"build:js:editor:esm": "bun build ./src/build_editor.js --bundle --outfile=./dist/sigpro-ui.editor.esm.js --format=esm --external ./src/sigpro.js",
"build:js:editor:esm:min": "bun build ./src/build_editor.js --bundle --outfile=./dist/sigpro-ui.editor.esm.min.js --format=esm --minify --external ./src/sigpro.js",
"copy:docs": "cp dist/sigpro-ui.min.css dist/sigpro-ui.min.js docs/", "copy:docs": "cp dist/sigpro-ui.min.css dist/sigpro-ui.min.js docs/",
"build": "bun run clean && bun run build:css && bun run build:cssmin && bun run build:js:iife && bun run build:js:iife:min && bun run build:js:esm && bun run build:js:esm:min && bun run copy:docs", "build": "bun run clean && bun run build:css && bun run build:cssmin && bun run build:js:iife && bun run build:js:iife:min && bun run build:js:esm && bun run build:js:esm:min && bun run build:js:editor:esm && bun run build:js:editor:esm:min && bun run copy:docs",
"prepublishOnly": "bun run build",
"docs": "bun x serve docs" "docs": "bun x serve docs"
}, },
"devDependencies": { "devDependencies": {
@@ -55,6 +59,7 @@
"@iconify/tailwind4": "^1.2.3", "@iconify/tailwind4": "^1.2.3",
"@tailwindcss/cli": "^4.2.4", "@tailwindcss/cli": "^4.2.4",
"daisyui": "^5.5.19", "daisyui": "^5.5.19",
"esbuild": "^0.28.0",
"tailwindcss": "^4.2.4" "tailwindcss": "^4.2.4"
} }
} }

View File

@@ -1,747 +0,0 @@
// All base components
import { $, $$, watch, batch, h, Fragment, mount, when, each, router, onUnmount, isArr, isFunc, isObj } from "./sigpro.js"
if (typeof window !== "undefined") {
"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video"
.split(" ")
.forEach(tag => { window[tag] = (props, children) => h(tag, props, children) })
}
export { $, $$, watch, batch, h, Fragment, mount, when, each, router, onUnmount, isArr, isFunc, isObj }
// Helpers
const val = val => typeof val === "function" ? val() : val;
const getBy = (item, field = 'label') => (item && typeof item === 'object') ? item[field] : item;
const cls = (...classes) => classes.filter(Boolean).join(' ').trim();
const filterBy = (items, query, field = 'label') => {
const q = String(val(query) || '').toLowerCase();
const list = (val(items) || []).map(i => typeof i === 'object' ? i : { label: i, value: i });
return !q ? list : list.filter(item => String(item[field] || '').toLowerCase().includes(q));
};
const rand = (r) => `${r}-${Math.random().toString(36).slice(2, 9)}`
export const hide = () => document.activeElement?.blur()
// Locales
export const lang = {
es: {
uploadFiles: "Arrastrar y soltar o click para seleccionar...",
},
en: {
uploadFiles: "Drag and drop or click to select",
}
};
export const currentLocale = $("en");
export const Locale = t => currentLocale(t);
export const t = t => () => lang[currentLocale()][t] || t;
// Components
export const Accordion = (p) => {
const name = p.name || rand('acc')
return each(p.items, (it) => {
return h('div', { class: cls('collapse', p.class) }, [
h('input', { type: 'radio', name, checked: it.open || undefined }),
it.title ? h('div', { class: cls("collapse-title", `${it.classTitle ?? ' font-semibold'}`) }, it.title) : null,
it.content ? h('div', { class: cls("collapse-content text-sm", `${it.classContent ?? ' font-semibold'}`) }, it.content) : null,
]);
});
}
export const Alert = (p, c) => h("div", { ...p, class: cls("alert", p.class) }, c);
export const Avatar = (p, c) => h("div", { class: "avatar" }, h('div', { class: p.class }, c));
export const AvatarGroup = (p, c) => h("div", { ...p, class: cls("avatar-group -space-x-6", p.class) }, c);
export const Autocomplete = ({ items, value, onselect, placeholder = '...', ...props }) => {
const query = $(val(value) || '')
const filtered = $(() => filterBy(items, query()))
const pick = (item) => {
const display = getBy(item)
const actual = typeof item === 'string' ? item : item.value
query(display)
if (isFunc(value)) value(actual)
onselect?.(item)
hide()
}
return Dropdown({ class: 'w-80' }, [
h('div', { tabindex: '0', role: 'button', class: 'w-full' }, Input({ ...props, placeholder, value: query, left: Icon('icon-[lucide--search]') })),
DropdownContent({ class: 'p-2 bg-base-100 rounded-box shadow-xl w-full max-h-60 overflow-y-auto border border-base-300 z-50' },
h('ul', { class: 'menu flex-col flex-nowrap w-full p-0' }, [
each(filtered, (item) => h('li', {}, [h('a', { onmousedown: (e) => e.preventDefault(), onclick: () => pick(item) }, getBy(item))]), 'value'),
() => filtered().length === 0 ? h('li', { class: 'p-4 opacity-50 text-center' }, 'Sin resultados') : null
])
)
])
}
export const Badge = (p, c) => h("span", { ...p, class: cls("badge", p.class) }, c);
export const Breadcrumbs = (p, c) => h("div", { ...p, class: cls("breadcrumbs", p.class) }, c);
export const Button = (p, c) => h("button", { ...p, class: cls("btn", p.class) }, c);
export const Calendar = (p) => {
const internalDate = $(new Date())
const hoverDate = $(null)
const startHour = $(0)
const endHour = $(0)
const now = new Date()
const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
const fmt = d => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
const rangeMode = () => val(p.range) === true
const current = () => val(p.value)
const selectDate = (date) => {
const s = fmt(date)
const v = current()
if (rangeMode()) {
if (!v?.start || (v.start && v.end)) {
p.onChange?.({ start: s, end: null, ...(p.hour && { startHour: startHour() }) })
} else {
const start = v.start
const nv = s < start ? { start: s, end: start } : { start, end: s }
if (p.hour) { nv.startHour = v.startHour ?? startHour(); nv.endHour = endHour() }
p.onChange?.(nv)
}
} else {
p.onChange?.(p.hour ? `${s}T${String(startHour()).padStart(2, '0')}:00:00` : s)
}
}
const move = (m) => { const d = internalDate(); internalDate(new Date(d.getFullYear(), d.getMonth() + m, 1)) }
const moveYear = (y) => { const d = internalDate(); internalDate(new Date(d.getFullYear() + y, d.getMonth(), 1)) }
const HourSlider = ({ value: hVal, onChange: onH }) => h('div', { class: 'flex-1' }, [
h('div', { class: 'flex gap-2 items-center' }, [
h('input', { type: 'range', min: 0, max: 23, value: hVal, class: 'range range-xs flex-1', oninput: e => onH(+e.target.value) }),
h('span', { class: 'text-sm font-mono min-w-[48px] text-center' }, () => String(val(hVal)).padStart(2, '0') + ':00')
])
])
return h('div', {
class: cls('p-4 bg-base-100 border border-base-300 shadow-2xl rounded-box w-80 select-none', p.class)
}, [
h('div', { class: 'flex justify-between items-center mb-4 gap-1' }, [
h('div', { class: 'flex gap-0.5' }, [
h('button', { type: 'button', class: 'btn btn-ghost btn-xs px-1', onclick: () => moveYear(-1) }, h('span', { class: 'icon-[lucide--chevrons-left]' })),
h('button', { type: 'button', class: 'btn btn-ghost btn-xs px-1', onclick: () => move(-1) }, h('span', { class: 'icon-[lucide--chevron-left]' }))
]),
h('span', { class: 'font-bold uppercase flex-1 text-center' }, () => internalDate().toLocaleString('es-ES', { month: 'short', year: 'numeric' })),
h('div', { class: 'flex gap-0.5' }, [
h('button', { type: 'button', class: 'btn btn-ghost btn-xs px-1', onclick: () => move(1) }, h('span', { class: 'icon-[lucide--chevron-right]' })),
h('button', { type: 'button', class: 'btn btn-ghost btn-xs px-1', onclick: () => moveYear(1) }, h('span', { class: 'icon-[lucide--chevrons-right]' }))
])
]),
h('div', { class: 'grid grid-cols-7 gap-1', onmouseleave: () => hoverDate(null) }, [
...['L', 'M', 'X', 'J', 'V', 'S', 'D'].map(d => h('div', { class: 'text-[10px] opacity-40 font-bold text-center' }, d)),
() => {
const d = internalDate(), y = d.getFullYear(), m = d.getMonth()
const firstDay = new Date(y, m, 1).getDay()
const offset = firstDay === 0 ? 6 : firstDay - 1
const dim = new Date(y, m + 1, 0).getDate()
const cells = []
for (let i = 0; i < offset; i++) cells.push(h('div'))
for (let i = 1; i <= dim; i++) {
const date = new Date(y, m, i), ds = fmt(date)
cells.push(h('button', {
type: 'button',
class: () => {
const v = current(), h = hoverDate()
const isStart = typeof v === 'string' ? v.split('T')[0] === ds : v?.start === ds
const isEnd = v?.end === ds
let inRange = false
if (rangeMode() && v?.start) {
const start = v.start
if (!v.end && h) inRange = (ds > start && ds <= h) || (ds < start && ds >= h)
else if (v.end) inRange = ds > start && ds < v.end
}
const base = 'btn btn-xs p-0 aspect-square min-h-0 h-auto font-normal relative'
const st = isStart || isEnd ? 'btn-primary z-10' : inRange ? 'bg-primary/20 border-none rounded-none' : 'btn-ghost'
const today = ds === todayStr ? 'ring-1 ring-primary ring-inset font-black text-primary' : ''
return cls(base, st, today)
},
onmouseenter: () => rangeMode() && hoverDate(ds),
onclick: () => selectDate(date)
}, i.toString()))
}
return cells
}
]),
p.hour ? h('div', { class: 'mt-3 pt-2 border-t border-base-300' },
rangeMode()
? h('div', { class: 'flex gap-4' }, [HourSlider({ value: startHour, onChange: h => startHour(h) }), HourSlider({ value: endHour, onChange: h => endHour(h) })])
: HourSlider({ value: startHour, onChange: h => startHour(h) })
) : null
])
}
export const Card = (p, c) => h('div', { ...p, class: cls('card', p.class) }, c);
export const CardTitle = (p, c) => h('div', { ...p, class: cls('card-title', p.class) }, c);
export const CardBody = (p, c) => h('div', { ...p, class: cls('card-body', p.class) }, c);
export const CardActions = (p, c) => h('div', { ...p, class: cls('card-actions', p.class) }, c);
export const Carousel = (p, c) => h("div", { ...p, class: cls("carousel", p.class) }, c);
export const CarouselItem = (p, c) => h("div", { ...p, class: cls("carousel-item", p.class) }, c);
export const Chat = (p, c) => h("div", { ...p, class: cls("chat", p.class) }, c);
export const ChatBubble = (p, c) => h("div", { ...p, class: cls("chat-bubble", p.class) }, c);
export const ChatFooter = (p, c) => h("div", { ...p, class: cls("chat-footer", p.class) }, c);
export const ChatHeader = (p, c) => h("div", { ...p, class: cls("chat-header", p.class) }, c);
export const ChatImage = (p, c) => h("div", { ...p, class: cls("chat-image avatar", p.class) }, h("div", { class: "w-10 rounded-full" }, typeof c === "string" ? h("img", { src: c, alt: "avatar" }) : c));
export const Checkbox = (p) => h("input", { ...p, type: "checkbox", class: cls("checkbox", p.class) });
export const Colorpicker = (p) => {
const current = () => val(p.value) || '#000000'
return Dropdown({}, [
DropdownButton({ class: 'btn' }, [
h('div', { class: 'size-5 rounded-sm', style: () => `background-color: ${current()}` }),
p.label && h('span', {}, p.label)
]),
DropdownContent({ class: 'p-0' },
ColorPalette({ value: p.value, onchange: (c) => { isFunc(p.value) ? p.value(c) : p.onchange?.(c) } })
)
])
}
export const ColorPalette = (p) => {
const current = () => val(p.value) || '#000000'
const palette = [
'#000', '#1A1A1A', '#333', '#4D4D4D', '#666', '#808080', '#B3B3B3', '#FFF',
'#450a0a', '#7f1d1d', '#991b1b', '#b91c1c', '#dc2626', '#ef4444', '#f87171', '#fca5a5',
'#431407', '#7c2d12', '#9a3412', '#c2410c', '#ea580c', '#f97316', '#fb923c', '#ffedd5',
'#713f12', '#a16207', '#ca8a04', '#eab308', '#facc15', '#fde047', '#fef08a', '#fff9c4',
'#064e3b', '#065f46', '#059669', '#10b981', '#34d399', '#4ade80', '#84cc16', '#d9f99d',
'#082f49', '#075985', '#0284c7', '#0ea5e9', '#38bdf8', '#7dd3fc', '#22d3ee', '#cffafe',
'#1e1b4b', '#312e81', '#4338ca', '#4f46e5', '#6366f1', '#818cf8', '#a5b4fc', '#e0e7ff',
'#2e1065', '#4c1d95', '#6d28d9', '#7c3aed', '#8b5cf6', '#a855f7', '#d946ef', '#fae8ff'
]
const pick = (c) => {
isFunc(p.value) ? p.value(c) : p.onchange?.(c)
hide()
}
return h('div', {
class: cls('p-3 bg-base-100 rounded-box shadow w-64', p.class)
}, h('div', { class: 'grid grid-cols-8 gap-1' },
palette.map(c => h('button', {
type: 'button',
style: `background-color: ${c}`,
class: () => {
const act = current().toLowerCase() === c.toLowerCase()
return `size-6 rounded-sm cursor-pointer transition-all hover:scale-125 hover:z-10 active:scale-95 outline-none border border-black/5 p-0 min-h-0 ${act ? 'ring-2 ring-offset-1 ring-primary z-10 scale-110' : ''}`
},
onclick: () => { pick(c) }
}))
))
}
export const Datepicker = (p) => {
const displayValue = $("")
const rangeMode = () => val(p.range) === true
watch(() => {
const v = val(p.value)
if (!v) return displayValue("")
let text = ""
if (typeof v === "string") {
text = p.hour && v.includes("T") ? v.replace("T", " ") : v
} else if (v.start && v.end) {
const startStr = p.hour && v.startHour != null ? `${v.start} ${String(v.startHour).padStart(2, "0")}:00` : v.start
const endStr = p.hour && v.endHour != null ? `${v.end} ${String(v.endHour).padStart(2, "0")}:00` : v.end
text = `${startStr} - ${endStr}`
} else if (v.start) {
const startStr = p.hour && v.startHour != null ? `${v.start} ${String(v.startHour).padStart(2, "0")}:00` : v.start
text = `${startStr}...`
}
displayValue(text)
})
const handleChange = (val) => {
if (isFunc(p.value)) p.value(val)
else p.onChange?.(val)
if (!rangeMode() || val?.end != null) hide()
}
return Dropdown({ class: cls('w-full', p.class) }, [
h('label', {
tabindex: '0',
role: 'button',
class: 'input input-bordered flex items-center gap-2 cursor-pointer'
}, [
h('span', { class: 'icon-[lucide--calendar] shrink-0' }),
h('span', {
class: () => `grow text-left truncate ${!displayValue() ? 'opacity-50' : ''}`,
}, () => displayValue() || p.placeholder || (rangeMode() ? 'Seleccionar rango...' : 'Seleccionar fecha...')),
() => displayValue() ? h('button', {
type: 'button',
class: 'btn btn-ghost btn-xs btn-circle -mr-2',
onmousedown: (e) => {
e.preventDefault()
e.stopPropagation()
if (isFunc(p.value)) p.value(null)
else p.onChange?.(null)
displayValue("") // Forzar limpieza visual inmediata
}
}, h('span', { class: 'icon-[lucide--x] opacity-50' })) : null
]),
DropdownContent({ class: 'p-0' },
Calendar({ value: p.value, range: rangeMode(), hour: p.hour, onChange: handleChange })
)
])
}
export const Drawer = (p, c) => div({ ...p, class: cls('drawer', p.class) }, c)
export const DrawerToggle = (p) => input({ ...p, type: 'checkbox', class: 'drawer-toggle', checked: () => val(p.checked), onchange: (e) => isFunc(p.checked) && p.checked(e.target.checked) })
export const DrawerContent = (p, c) => div({ ...p, class: cls('drawer-content', p.class) }, c)
export const DrawerSide = (p, c) => div({ ...p, class: cls('drawer-side', p.class) }, c)
export const DrawerOverlay = (p) => label({ ...p, for: p.for, class: cls('drawer-overlay', p.class) })
export const Divider = (p) => h("div", { ...p, class: cls("divider", p.class) });
export const Dropdown = (p, c) => (h('div', { ...p, class: cls('dropdown', p.class) }, c));
export const DropdownButton = (p, c) => (h('div', { ...p, tabindex: '0', role: 'button', class: cls('btn', p.class) }, c));
export const DropdownContent = (p, c) => (h('div', { ...p, tabindex: '0', class: cls('dropdown-content', p.class) }, c));
export const Fab = (p, c) => h("div", { class: "fab" }, [h('div', { tabindex: "0", role: "button", class: cls('btn', p.class) }, Icon(p.icon)), c]);
export const Fieldset = (p, c) => h("fieldset", { class: cls("fieldset", p.class) }, [h("legend", { class: "fieldset-legend" }, p.label), c])
export const Fileinput = (p) => {
const files = $([])
const drag = $(false)
const error = $(null)
const maxBytes = (p.max || 2) * 1024 * 1024
const process = (fileList) => {
const arr = Array.from(fileList)
error(null)
if (arr.some(f => f.size > maxBytes)) {
error(`Máx ${p.max || 2}MB`)
return
}
const updated = [...files(), ...arr]
files(updated)
if (isFunc(p.onselect)) p.onselect(updated)
else if (isFunc(p.value)) p.value(updated)
}
const remove = (idx) => {
const updated = files().filter((_, i) => i !== idx)
files(updated)
if (isFunc(p.onselect)) p.onselect(updated)
else if (isFunc(p.value)) p.value(updated)
}
return h('div', { class: cls('fieldset w-full p-0', p.class) }, [
h('label', {
class: () => `relative flex items-center justify-between w-full h-12 px-4 border-2 border-dashed rounded-lg cursor-pointer transition-all duration-200 ${drag() ? 'border-primary bg-primary/10' : 'border-base-content/20 bg-base-100 hover:bg-base-200'}`,
ondragover: (e) => { e.preventDefault(); drag(true) },
ondragleave: () => drag(false),
ondrop: (e) => { e.preventDefault(); drag(false); process(e.dataTransfer.files) }
}, [
h('div', { class: 'flex items-center gap-3 w-full' }, [
h('span', { class: 'icon-[lucide--upload]' }),
h('span', { class: 'text-sm opacity-70 truncate grow text-left' }, t("uploadFiles")),
h('span', { class: 'text-[10px] opacity-40 shrink-0' }, `Máx ${p.max || 2}MB`)
]),
h('input', {
type: 'file',
multiple: true,
accept: p.accept || '*',
class: 'hidden',
onchange: (e) => process(e.target.files)
})
]),
() => error() && h('span', { class: 'text-[10px] text-error mt-1 px-1 font-medium' }, error()),
when(() => files().length > 0, () =>
h('ul', { class: 'mt-2 space-y-1' },
each(files, (file, idx) =>
h('li', { class: 'flex items-center justify-between p-1.5 pl-3 text-xs bg-base-200/50 rounded-md border border-base-300' }, [
h('div', { class: 'flex items-center gap-2 truncate' }, [
h('span', { class: 'opacity-50' }, '📄'),
h('span', { class: 'truncate font-medium max-w-[200px]' }, file.name),
h('span', { class: 'text-[9px] opacity-40' }, `(${(file.size / 1024).toFixed(0)} KB)`)
]),
h('button', {
type: 'button',
class: 'btn btn-ghost btn-xs btn-circle',
onclick: (e) => { e.preventDefault(); remove(idx) }
}, h('span', { class: 'icon-[lucide--x]' }))
])
)
)
)
])
}
export const Icon = (p) => h("span", { class: p.startsWith("icon-") ? p : "" }, p.startsWith("icon-") ? null : p);
export const Indicator = (p, c) => h("div", { ...p, class: cls("indicator", p.class) }, [p.value && h("span", { class: cls("indicator-item badge", p.class) }, p.value), c]);
export const Input = (p) => {
const { label, icon, float, placeholder, value, left, right, rule, hint, content, ...rest } = p;
const showPassword = $(false);
const isPassword = p.type === 'password';
const pattern = rule ?? null;
const inputType = () => isPassword ? (val(showPassword) ? 'text' : 'password') : (p.type || 'search');
return h("label", { class: float ? 'floating-label' : '' }, [
float ? h("span", {}, label) : null,
h("label", { pattern: pattern, class: () => cls('input validator', p.class) },
[
label && !float ? h('span', { class: 'label' }, label) : null,
left ?? null,
h('input', { ...rest, type: inputType, class: 'grow', pattern: pattern, placeholder: placeholder || label || ' ', value: value }),
right ?? null,
isPassword ? Swap({ class: 'ml-2' }, [
SwapToggle({ value: showPassword, class: "swap-rotate" }),
SwapOn({}, Icon('icon-[lucide--eye]')),
SwapOff({}, Icon('icon-[lucide--eye-off]')),
]) : null
]),
hint ? h('div', { class: "validator-hint" }, hint) : null,
]);
};
export const Kbd = (p, c) => h("kbd", { ...p, class: cls("kbd", p.class) }, c);
export const List = (p, c) => h('ul', { ...p, class: cls('list', p.class) }, c)
export const ListRows = (p) => () => (val(p.items) || []).map((item, idx) => h('li', { class: cls('list-row', p.class, item?.class) }, typeof p.render === 'function' ? p.render(item, idx) : item))
export const Loading = (p, c) => h("span", { ...p, class: cls("loading loading-spinner", p.class) }, c);
export const Menu = (p) => {
if (p.children !== undefined) return h('ul', { class: cls('menu', p.class), ...p }, p.children)
const { items } = p
const render = (item) => item.children
? h('li', {}, h('details', { open: item.open || undefined }, [
h('summary', {}, getBy(item)),
h('ul', {}, each(() => val(item.children) || [], render))
]))
: h('li', {}, h('a', {
href: item.href,
onclick: item.onclick ? (e) => { if (!item.href) e.preventDefault(); item.onclick(e) } : null
}, getBy(item)))
return h('ul', { class: cls('menu', p.class) },
each(() => val(items) || [], render)
)
}
export const Modal = (p) => {
let dialogRef = null;
watch(() => { const isOpen = val(p.open); if (!dialogRef) return; isOpen ? dialogRef.showModal() : dialogRef.hide(); });
const close = () => isFunc(p.open) && p.open(false);
return h("dialog", { ...p, ref: el => dialogRef = el, class: cls('modal', p.class), onclose: close, oncancel: close }, [
h("div", { class: "modal-box" }, [
p.title && h("h3", { class: "text-lg font-bold" }, p.title),
p.children,
h("div", { class: "modal-action" }, [
p.actions || Button({ class: 'btn', onclick: close }, 'Cerrar')
])
]),
h("form", { method: "dialog", class: "modal-backdrop" }, [
h("button", {}, "close")
])
]);
};
export const Navbar = (p, c) => h("div", { ...p, class: cls("navbar", p.class) }, c);
export const Progress = (p) => h("progress", { ...p, class: cls("progress", p.class) });
export const Radial = (p, c) => h("div", { class: cls("radial-progress", p.class,), style: `--value:${val(p.value) ?? 0};`, role: "progressbar", "aria-valuenow": p.value }, c)
export const Radio = (p) => h("input", { ...p, type: "radio", class: cls("radio", p.class) });
export const Range = (p) => h("input", { ...p, type: "range", class: cls("range", p.class) });
export const Rating = (p, c) => h('div', { ...p, class: "rating" }, c);
export const RatingItems = (p) => [...Array(p.count)].map((_, i) => h('input', { class: cls('mask', p.class), name: p.name, type: 'radio', checked: () => val(p.value) === i, onchange: () => isFunc(p.value) ? p.value(i) : p.onchange?.(i) }))
export const Select = (p, c) => {
if (c !== undefined) return h('select', { class: cls('select', p.class), ...p }, c)
const { label, float, placeholder, placeholderDisabled = true, value, left, right, hint, items, keyFn, ...rest } = p
const opts = () => {
const raw = val(items) || []
const ph = placeholder ? [{ disabled: placeholderDisabled, label: placeholder, value: '' }] : []
return [...ph, ...raw]
}
return h('label', { class: float ? 'floating-label' : '' }, [
float ? h('span', {}, label) : null,
h('label', { class: cls('select', rest.class) }, [
(!float && label) ? h('span', { class: 'label' }, label) : null,
left ?? null,
h('select', {
value: () => val(value),
onchange: (e) => isFunc(value) ? value(e.target.value) : rest.onchange?.(e)
},
each(opts, (item) => {
const val = getBy(item, item.value !== undefined ? 'value' : undefined)
const lab = getBy(item, 'label')
return h('option', { value: val, disabled: item.disabled || undefined }, lab)
})
),
right ?? null
]),
hint ? h('div', { class: 'validator-hint' }, hint) : null
])
}
export const Skeleton = (p) => h("div", { ...p, class: cls("skeleton", p.class) });
export const SkeletonText = (p) => h("span", { ...p, class: cls("skeleton skeleton-text", p.class) });
export const Stack = (p, c) => h("div", { ...p, class: cls("stack", p.class) }, c);
export const Stats = (p, c) => h('div', { ...p, class: cls('stats shadow', p.class) }, c)
export const Stat = (p) => h('div', { ...p, class: cls('stat', p.class) }, [
p.title ? h('div', { class: 'stat-title' }, p.title) : null,
p.value ? h('div', { class: 'stat-value' }, p.value) : null,
p.desc ? h('div', { class: 'stat-desc' }, p.desc) : null
])
export const Steps = (p, c) => h("ul", { ...p, class: cls("steps", p.class) }, c);
export const Step = (p, c) => h("li", { ...p, class: cls("step", p.class), "data-content": p.dataContent }, c);
export const Swap = (p, c) => h('label', { ...p, class: cls('swap', p.class) }, c)
export const SwapToggle = (p) => h('input', { type: 'checkbox', checked: () => val(p.value), onchange: (e) => isFunc(p.value) && p.value(e.target.checked), class: p.class })
export const SwapOn = (p, c) => h('div', { ...p, class: cls('swap-on', p.class) }, c)
export const SwapOff = (p, c) => h('div', { ...p, class: cls('swap-off', p.class) }, c)
export const Table = (p, c) => h('table', { ...p, class: cls('table', p.class) }, c)
export const TableItems = ({ items, columns = [], header = true }) => {
const head = header !== false && columns.some(c => c.label) ? h('thead', {}, h('tr', {}, columns.map(c => h('th', { class: c.class }, c.label)))) : null
const body = h('tbody', {}, () => {
const list = val(items) || []
return list.map((it, idx) => h('tr', {}, columns.map(c => { const v = c.render ? c.render(it, idx) : it[c.key]; return h('td', { class: c.class }, v) })))
})
return [head, body].filter(Boolean)
}
export const Tabs = (p, c) => {
if (!p.items) {
const { class: className, ...rest } = p
return h('div', { ...rest, class: cls('tabs', className) }, c)
}
const { items, activeIndex, onClose, class: className, ...rest } = p
const get = x => (isFunc(x) ? x() : x)
const closeH = onClose || (isFunc(items) ? (idx, item) => {
const arr = val(items)
const newArr = arr.filter((_, i) => i !== idx)
items(newArr)
if (activeIndex() >= newArr.length) activeIndex(Math.max(0, newArr.length - 1))
} : null)
return h('div', { ...rest, class: cls('tabs', className) }, () => {
const list = val(items) || []
return list.flatMap((it, idx) => {
const isActive = () => activeIndex() === idx
const button = h('button', {
class: () => `tab ${isActive() ? 'tab-active' : ''} ${it.class || ''}`,
onclick: (e) => { e.preventDefault(); activeIndex(idx); it.onclick?.(e) }
}, [
getBy(it),
it.closable ? h('span', {
class: 'ml-1 inline-flex items-center justify-center w-4 h-4 rounded-full hover:bg-base-300 text-base-content/60 hover:text-base-content cursor-pointer',
onclick: (e) => { e.stopPropagation(); closeH?.(idx, it) }
}, h('span', { class: 'icon-[lucide--x] w-3 h-3' })) : null
])
const contentDiv = h('div', {
class: 'tab-content bg-base-100 border-base-300 p-6',
style: () => `display: ${isActive() ? 'block' : 'none'};`
}, isFunc(it.content) ? it.content() : it.content)
return [button, contentDiv]
})
})
}
export const Textarea = (p) => h("textarea", { ...p, class: cls("textarea", p.class) });
export const Textrotate = (p, c) => h('span', { ...p, class: cls('text-rotate', p.class) }, h('span', {}, c))
export const Timeline = (p, c) => h("ul", { ...p, class: cls("timeline", p.class) }, c);
export const Toast = (message, type = "alert-success", duration = 3500) => {
let container = document.getElementById("sigpro-toast-container");
if (!container) {
container = h("div", { id: "sigpro-toast-container", class: "fixed top-0 right-0 z-[9999] p-4 flex flex-col items-end gap-2 pointer-events-none" });
document.body.appendChild(container);
}
const host = h("div", { style: "display: contents" });
container.appendChild(host);
let closeFn, timer, enterTimer;
const ToastComponent = () => {
const visible = $(false);
const leaving = $(false);
closeFn = () => {
if (leaving()) return;
clearTimeout(timer);
clearTimeout(enterTimer);
leaving(true);
setTimeout(() => { instance.destroy(); host.remove(); if (!container.hasChildNodes()) container.remove(); }, 300);
};
enterTimer = setTimeout(() => visible(true), 0);
const content = typeof message === 'function' ? val(message) : message;
const msgNode = typeof content === 'string' ? h("span", {}, content) : content;
return h("div", {
class: () => {
const base = `alert alert-soft ${type} shadow-lg transition-all duration-300 inline-flex w-auto whitespace-nowrap pointer-events-auto`;
if (leaving()) return `${base} translate-x-full opacity-0`;
if (visible()) return `${base} translate-x-0 opacity-100`;
return `${base} translate-x-10 opacity-0`;
}
}, [
msgNode,
h("button", {
class: "btn btn-xs btn-circle btn-ghost",
onclick: closeFn
}, h("span", { class: "icon-[lucide--x]" }))
]);
};
const instance = mount(ToastComponent, host);
if (duration > 0) timer = setTimeout(closeFn, duration);
return closeFn;
};
export const Toggle = (p) => h("input", { ...p, type: "checkbox", class: cls("toggle", p.class) });
export const Tooltip = (p, c) => h("div", { ...p, class: cls("tooltip", p.class), "data-tip": p.tip }, c);
export const Editor = (p) => {
const { value, class: extraClass } = p
let editorRef = null
let savedRange = null
const isSource = $(false)
const source = $("")
const count = $(0)
const refreshTick = $(0)
const showEmojis = $(false)
const emojis = ["😀", "😊", "😉", "🧐", "😮", "🤔", "😅", "😂", "😍", "😘", "🥰", "👍", "👎", "👌", "🤝", "🤞", "👋", "👏", "🙌", "🙏", "💪", "☝️", "👇", "👈", "👉", "🖕", "✅", "⚠️", "🚀", "📢", "✉️", "❤️"]
const saveSelection = () => {
const sel = window.getSelection()
if (sel.getRangeAt && sel.rangeCount) savedRange = sel.getRangeAt(0)
}
const restoreSelection = () => {
if (savedRange) {
const sel = window.getSelection()
sel.removeAllRanges()
sel.addRange(savedRange)
}
}
const triggerRefresh = () => {
refreshTick(refreshTick() + 1)
if (editorRef) count(editorRef.innerText.length)
}
const notify = () => {
if (!editorRef) return
const html = editorRef.innerHTML
if (isFunc(value)) value(html)
else p.onchange?.(html)
triggerRefresh()
}
const exec = (cmd, val = null) => {
if (!editorRef) return
editorRef.focus()
if (savedRange) restoreSelection()
document.execCommand(cmd, false, val)
savedRange = null
notify()
}
const openLightbox = (src) => {
const overlay = document.createElement('div')
overlay.style = `position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:9999;display:flex;align-items:center;justify-content:center;cursor:zoom-out;`
const img = document.createElement('img')
img.src = src
img.style = `max-width:95%;max-height:95%;box-shadow:0 0 30px rgba(0,0,0,0.5);border-radius:4px;`
overlay.onclick = () => document.body.removeChild(overlay)
overlay.appendChild(img)
document.body.appendChild(overlay)
}
const handleUpload = (file) => {
if (!file) return
const reader = new FileReader()
reader.onload = (re) => {
if (file.type.startsWith('image/')) {
const imgHtml = `<div style="display:inline-block; resize:both; overflow:hidden; vertical-align:bottom; line-height:0; width:200px; height:auto; border:1px dashed #ccc; padding:2px; cursor:pointer;" class="resizable-img-container"><img src="${re.target.result}" style="width:100%; height:100%; object-fit:contain; pointer-events:none;"></div>&nbsp;`
exec("insertHTML", imgHtml)
} else {
const linkHtml = `<a href="${re.target.result}" download="${file.name}" contenteditable="false" style="display:inline-flex; align-items:center; gap:5px; padding:4px 8px; border:1px solid #ccc; border-radius:4px; background:#f9f9f9; text-decoration:none; color:#333; font-size:12px; margin:2px; cursor:pointer;"><span class="icon-[lucide--paperclip] w-3 h-3"></span>${file.name}</a>&nbsp;`
exec("insertHTML", linkHtml)
}
}
reader.readAsDataURL(file)
}
const queryState = (cmd, val = null) => {
refreshTick(); if (!editorRef || isSource()) return false
try {
if (cmd === 'formatBlock') {
let node = window.getSelection().getRangeAt(0).commonAncestorContainer
while (node && node !== editorRef) {
if (node.nodeType === 1 && node.tagName === val) return true
node = node.parentNode
}
return false
}
return document.queryCommandState(cmd)
} catch (e) { return false }
}
const toolbar = h("div", { class: "flex flex-wrap items-center gap-1 p-2 border-b border-base-300 bg-base-200 sticky top-0 z-20" }, [
h("div", { class: "flex flex-wrap gap-1 flex-1 items-center" }, [
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${queryState('bold') ? 'btn-active bg-primary/20' : ''}`, onclick: () => exec("bold") }, h("span", { class: "icon-[lucide--bold]" })),
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${queryState('italic') ? 'btn-active bg-primary/20' : ''}`, onclick: () => exec("italic") }, h("span", { class: "icon-[lucide--italic]" })),
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${queryState('underline') ? 'btn-active bg-primary/20' : ''}`, onclick: () => exec("underline") }, h("span", { class: "icon-[lucide--underline]" })),
h("input", { type: "color", class: "w-5 h-5 p-0 border-0 bg-transparent cursor-pointer", oninput: (e) => exec("foreColor", e.target.value) }),
h("span", { class: "w-px h-5 bg-base-300 mx-1" }),
h("button", {
type: "button",
class: "btn btn-ghost btn-xs",
onclick: () => exec("justifyLeft")
}, h("span", { class: "icon-[lucide--align-left]" })),
h("button", {
type: "button",
class: "btn btn-ghost btn-xs",
onclick: () => exec("justifyCenter")
}, h("span", { class: "icon-[lucide--align-center]" })),
h("button", {
type: "button",
class: "btn btn-ghost btn-xs",
onclick: () => exec("justifyRight")
}, h("span", { class: "icon-[lucide--align-right]" })),
h("span", { class: "w-px h-5 bg-base-300 mx-1" }),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("insertUnorderedList") }, h("span", { class: "icon-[lucide--list]" })),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("insertOrderedList") }, h("span", { class: "icon-[lucide--list-ordered]" })),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("outdent") }, h("span", { class: "icon-[lucide--indent-decrease]" })),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("indent") }, h("span", { class: "icon-[lucide--indent-increase]" })),
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${queryState('formatBlock', 'BLOCKQUOTE') ? 'btn-active' : ''}`, onclick: () => exec("formatBlock", queryState('formatBlock', 'BLOCKQUOTE') ? 'P' : 'BLOCKQUOTE') }, h("span", { class: "icon-[lucide--quote]" })),
h("span", { class: "w-px h-5 bg-base-300 mx-1" }),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => { const url = window.prompt('URL:'); if (url) exec("createLink", url) } }, h("span", { class: "icon-[lucide--link]" })),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => { const input = document.createElement('input'); input.type = 'file'; input.onchange = (e) => handleUpload(e.target.files[0]); input.click(); } }, h("span", { class: "icon-[lucide--paperclip]" })),
h("div", { class: "relative" }, [
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: (e) => { e.stopPropagation(); saveSelection(); showEmojis(!showEmojis()); } }, h("span", { class: "icon-[lucide--smile]" })),
h("div", { class: "absolute top-full left-0 mt-1 p-2 bg-base-100 border border-base-300 shadow-xl rounded-box w-52 z-50 flex flex-wrap gap-1", style: () => showEmojis() ? "display:flex" : "display:none" }, emojis.map(emo => h("span", { class: "cursor-pointer hover:bg-base-200 p-1 rounded text-lg", onclick: (e) => { e.stopPropagation(); exec("insertText", emo); showEmojis(false); } }, emo)))
]),
h("span", { class: "w-px h-5 bg-base-300 mx-1" }),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("undo") }, h("span", { class: "icon-[lucide--undo-2]" })),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("redo") }, h("span", { class: "icon-[lucide--redo-2]" })),
]),
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${isSource() ? 'btn-active' : ''}`, onclick: () => { if (!isSource()) source(editorRef?.innerHTML || ""); else if (editorRef) { editorRef.innerHTML = source(); notify(); }; isSource(!isSource()) } }, h("span", { class: "icon-[lucide--code-2]" }))
])
if (typeof document !== 'undefined' && !document.getElementById('editor-styles')) {
const style = document.createElement('style')
style.id = 'editor-styles'
style.textContent = `
[contenteditable="true"] div,
[contenteditable="true"] p {
margin: 0;
padding: 0;
}
`
document.head.appendChild(style)
}
return h("div", { class: cls("border border-base-300 rounded-box bg-base-100 overflow-hidden shadow-sm flex flex-col", extraClass) }, [
toolbar,
h("div", { class: "relative flex-1 flex flex-col", onclick: () => showEmojis(false) }, [
h("div", {
ref: el => {
if (!editorRef && el) {
editorRef = el; el.innerHTML = val(value) || "";
document.execCommand("defaultParagraphSeparator", false, "br");
el.addEventListener('click', (e) => {
const container = e.target.closest('.resizable-img-container');
if (container) { const img = container.querySelector('img'); if (img) openLightbox(img.src); }
});
}
},
style: () => `min-height:22rem;${isSource() ? 'display:none' : ''}`,
class: "p-4 outline-none text-base-content leading-relaxed [&>div]:m-0 [&>p]:m-0 [&>div]:min-h-[1em] [&_.resizable-img-container]:hover:border-primary [&_blockquote]:border-l-4 [&_blockquote]:border-base-300 [&_blockquote]:pl-4 [&_blockquote]:italic [&_ul]:list-disc [&_ul]:pl-8 [&_ol]:list-decimal [&_ol]:pl-8",
contenteditable: "true",
oninput: notify,
onkeydown: (e) => { if (e.key === 'Tab') { e.preventDefault(); exec("indent"); } },
onkeyup: () => { triggerRefresh(); saveSelection(); },
onclick: (e) => { triggerRefresh(); saveSelection(); e.stopPropagation(); },
onmouseup: () => { notify(); saveSelection(); },
onpaste: (e) => {
e.preventDefault();
const text = e.clipboardData.getData('text/plain');
exec('insertText', text);
},
ondragover: (e) => e.preventDefault(),
ondrop: (e) => { e.preventDefault(); handleUpload(e.dataTransfer.files[0]) }
}),
h("textarea", {
class: "w-full flex-1 min-h-[22rem] p-4 outline-none font-mono text-sm bg-base-200 border-0",
style: () => isSource() ? '' : 'display:none',
value: source,
oninput: (e) => { source(e.target.value); if (editorRef) editorRef.innerHTML = e.target.value; p.onchange?.(e.target.value); }
})
]),
h("div", { class: "px-3 py-1 border-t border-base-300 bg-base-100/50 text-[10px] text-right text-base-content/60 italic" }, [
h("span", () => `${count()}`)
])
])
}

View File

@@ -1,10 +0,0 @@
export * from './sigpro-ui.js';
import * as All from './sigpro-ui.js';
export const Components = { ...All };
import { $, $$, watch, batch, h, Fragment, mount, when, each, router, onUnmount, isArr, isFunc, isObj } from "./sigpro.js"
if (typeof window !== "undefined") {
"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video"
.split(" ")
.forEach(tag => { window[tag] = (props, children) => h(tag, props, children) })
}
export { $, $$, watch, batch, h, Fragment, mount, when, each, router, onUnmount, isArr, isFunc, isObj }

View File

@@ -1,23 +0,0 @@
import * as All from './sigpro-ui.js';
import { $, $$, watch, batch, h, Fragment, mount, when, each, router, onUnmount, isArr, isFunc, isObj } from "./sigpro.js"
if (typeof window !== "undefined") {
Object.assign(window, { $, $$, watch, h, Fragment, when, each, router, mount, batch, onUnmount, isArr, isFunc, isObj })
"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video"
.split(" ")
.forEach(tag => { window[tag] = (props, children) => h(tag, props, children) })
Object.entries(All).forEach(([name, value]) => {
Object.defineProperty(window, name, {
value,
writable: false,
configurable: true,
enumerable: true
});
});
}
if (typeof window !== 'undefined') {
window.Components = { ...All };
}

1
src/build_editor.js Normal file
View File

@@ -0,0 +1 @@
export * from './editor.js'

2
src/build_esm.js Normal file
View File

@@ -0,0 +1,2 @@
export * from './sigpro-ui.js';
export * from './locale.js';

7
src/build_umd.js Normal file
View File

@@ -0,0 +1,7 @@
import * as All from './sigpro-ui.js';
import * as Edt from './editor.js';
import * as Loc from './locale.js';
if (typeof window !== "undefined") {
Object.assign(window, All, Edt, Loc)
}

189
src/editor.js Normal file
View File

@@ -0,0 +1,189 @@
import { $, isFunc, h } from "./sigpro.js"
import { val, cls } from "./helpers.js"
export const Editor = (p) => {
const { value, class: extraClass } = p
let editorRef = null
let savedRange = null
const isSource = $(false)
const source = $("")
const count = $(0)
const refreshTick = $(0)
const showEmojis = $(false)
const emojis = ["😀", "😊", "😉", "🧐", "😮", "🤔", "😅", "😂", "😍", "😘", "🥰", "👍", "👎", "👌", "🤝", "🤞", "👋", "👏", "🙌", "🙏", "💪", "☝️", "👇", "👈", "👉", "🖕", "✅", "⚠️", "🚀", "📢", "✉️", "❤️"]
const saveSelection = () => {
const sel = window.getSelection()
if (sel.getRangeAt && sel.rangeCount) savedRange = sel.getRangeAt(0)
}
const restoreSelection = () => {
if (savedRange) {
const sel = window.getSelection()
sel.removeAllRanges()
sel.addRange(savedRange)
}
}
const triggerRefresh = () => {
refreshTick(refreshTick() + 1)
if (editorRef) count(editorRef.innerText.length)
}
const notify = () => {
if (!editorRef) return
const html = editorRef.innerHTML
if (isFunc(value)) value(html)
else p.onchange?.(html)
triggerRefresh()
}
const exec = (cmd, val = null) => {
if (!editorRef) return
editorRef.focus()
if (savedRange) restoreSelection()
document.execCommand(cmd, false, val)
savedRange = null
notify()
}
const openLightbox = (src) => {
const overlay = document.createElement('div')
overlay.style = `position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:9999;display:flex;align-items:center;justify-content:center;cursor:zoom-out;`
const img = document.createElement('img')
img.src = src
img.style = `max-width:95%;max-height:95%;box-shadow:0 0 30px rgba(0,0,0,0.5);border-radius:4px;`
overlay.onclick = () => document.body.removeChild(overlay)
overlay.appendChild(img)
document.body.appendChild(overlay)
}
const handleUpload = (file) => {
if (!file) return
const reader = new FileReader()
reader.onload = (re) => {
if (file.type.startsWith('image/')) {
const imgHtml = `<div style="display:inline-block; resize:both; overflow:hidden; vertical-align:bottom; line-height:0; width:200px; height:auto; border:1px dashed #ccc; padding:2px; cursor:pointer;" class="resizable-img-container"><img src="${re.target.result}" style="width:100%; height:100%; object-fit:contain; pointer-events:none;"></div>&nbsp;`
exec("insertHTML", imgHtml)
} else {
const linkHtml = `<a href="${re.target.result}" download="${file.name}" contenteditable="false" style="display:inline-flex; align-items:center; gap:5px; padding:4px 8px; border:1px solid #ccc; border-radius:4px; background:#f9f9f9; text-decoration:none; color:#333; font-size:12px; margin:2px; cursor:pointer;"><span class="icon-[lucide--paperclip] w-3 h-3"></span>${file.name}</a>&nbsp;`
exec("insertHTML", linkHtml)
}
}
reader.readAsDataURL(file)
}
const queryState = (cmd, val = null) => {
refreshTick(); if (!editorRef || isSource()) return false
try {
if (cmd === 'formatBlock') {
let node = window.getSelection().getRangeAt(0).commonAncestorContainer
while (node && node !== editorRef) {
if (node.nodeType === 1 && node.tagName === val) return true
node = node.parentNode
}
return false
}
return document.queryCommandState(cmd)
} catch (e) { return false }
}
const toolbar = h("div", { class: "flex flex-wrap items-center gap-1 p-2 border-b border-base-300 bg-base-200 sticky top-0 z-20" }, [
h("div", { class: "flex flex-wrap gap-1 flex-1 items-center" }, [
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${queryState('bold') ? 'btn-active bg-primary/20' : ''}`, onclick: () => exec("bold") }, h("span", { class: "icon-[lucide--bold]" })),
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${queryState('italic') ? 'btn-active bg-primary/20' : ''}`, onclick: () => exec("italic") }, h("span", { class: "icon-[lucide--italic]" })),
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${queryState('underline') ? 'btn-active bg-primary/20' : ''}`, onclick: () => exec("underline") }, h("span", { class: "icon-[lucide--underline]" })),
h("input", { type: "color", class: "w-5 h-5 p-0 border-0 bg-transparent cursor-pointer", oninput: (e) => exec("foreColor", e.target.value) }),
h("span", { class: "w-px h-5 bg-base-300 mx-1" }),
h("button", {
type: "button",
class: "btn btn-ghost btn-xs",
onclick: () => exec("justifyLeft")
}, h("span", { class: "icon-[lucide--align-left]" })),
h("button", {
type: "button",
class: "btn btn-ghost btn-xs",
onclick: () => exec("justifyCenter")
}, h("span", { class: "icon-[lucide--align-center]" })),
h("button", {
type: "button",
class: "btn btn-ghost btn-xs",
onclick: () => exec("justifyRight")
}, h("span", { class: "icon-[lucide--align-right]" })),
h("span", { class: "w-px h-5 bg-base-300 mx-1" }),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("insertUnorderedList") }, h("span", { class: "icon-[lucide--list]" })),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("insertOrderedList") }, h("span", { class: "icon-[lucide--list-ordered]" })),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("outdent") }, h("span", { class: "icon-[lucide--indent-decrease]" })),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("indent") }, h("span", { class: "icon-[lucide--indent-increase]" })),
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${queryState('formatBlock', 'BLOCKQUOTE') ? 'btn-active' : ''}`, onclick: () => exec("formatBlock", queryState('formatBlock', 'BLOCKQUOTE') ? 'P' : 'BLOCKQUOTE') }, h("span", { class: "icon-[lucide--quote]" })),
h("span", { class: "w-px h-5 bg-base-300 mx-1" }),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => { const url = window.prompt('URL:'); if (url) exec("createLink", url) } }, h("span", { class: "icon-[lucide--link]" })),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => { const input = document.createElement('input'); input.type = 'file'; input.onchange = (e) => handleUpload(e.target.files[0]); input.click(); } }, h("span", { class: "icon-[lucide--paperclip]" })),
h("div", { class: "relative" }, [
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: (e) => { e.stopPropagation(); saveSelection(); showEmojis(!showEmojis()); } }, h("span", { class: "icon-[lucide--smile]" })),
h("div", { class: "absolute top-full left-0 mt-1 p-2 bg-base-100 border border-base-300 shadow-xl rounded-box w-52 z-50 flex flex-wrap gap-1", style: () => showEmojis() ? "display:flex" : "display:none" }, emojis.map(emo => h("span", { class: "cursor-pointer hover:bg-base-200 p-1 rounded text-lg", onclick: (e) => { e.stopPropagation(); exec("insertText", emo); showEmojis(false); } }, emo)))
]),
h("span", { class: "w-px h-5 bg-base-300 mx-1" }),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("undo") }, h("span", { class: "icon-[lucide--undo-2]" })),
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("redo") }, h("span", { class: "icon-[lucide--redo-2]" })),
]),
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${isSource() ? 'btn-active' : ''}`, onclick: () => { if (!isSource()) source(editorRef?.innerHTML || ""); else if (editorRef) { editorRef.innerHTML = source(); notify(); }; isSource(!isSource()) } }, h("span", { class: "icon-[lucide--code-2]" }))
])
if (typeof document !== 'undefined' && !document.getElementById('editor-styles')) {
const style = document.createElement('style')
style.id = 'editor-styles'
style.textContent = `
[contenteditable="true"] div,
[contenteditable="true"] p {
margin: 0;
padding: 0;
}
`
document.head.appendChild(style)
}
return h("div", { class: cls("border border-base-300 rounded-box bg-base-100 overflow-hidden shadow-sm flex flex-col", extraClass) }, [
toolbar,
h("div", { class: "relative flex-1 flex flex-col", onclick: () => showEmojis(false) }, [
h("div", {
ref: el => {
if (!editorRef && el) {
editorRef = el; el.innerHTML = val(value) || "";
document.execCommand("defaultParagraphSeparator", false, "br");
el.addEventListener('click', (e) => {
const container = e.target.closest('.resizable-img-container');
if (container) { const img = container.querySelector('img'); if (img) openLightbox(img.src); }
});
}
},
style: () => `min-height:22rem;${isSource() ? 'display:none' : ''}`,
class: "p-4 outline-none text-base-content leading-relaxed [&>div]:m-0 [&>p]:m-0 [&>div]:min-h-[1em] [&_.resizable-img-container]:hover:border-primary [&_blockquote]:border-l-4 [&_blockquote]:border-base-300 [&_blockquote]:pl-4 [&_blockquote]:italic [&_ul]:list-disc [&_ul]:pl-8 [&_ol]:list-decimal [&_ol]:pl-8",
contenteditable: "true",
oninput: notify,
onkeydown: (e) => { if (e.key === 'Tab') { e.preventDefault(); exec("indent"); } },
onkeyup: () => { triggerRefresh(); saveSelection(); },
onclick: (e) => { triggerRefresh(); saveSelection(); e.stopPropagation(); },
onmouseup: () => { notify(); saveSelection(); },
onpaste: (e) => {
e.preventDefault();
const text = e.clipboardData.getData('text/plain');
exec('insertText', text);
},
ondragover: (e) => e.preventDefault(),
ondrop: (e) => { e.preventDefault(); handleUpload(e.dataTransfer.files[0]) }
}),
h("textarea", {
class: "w-full flex-1 min-h-[22rem] p-4 outline-none font-mono text-sm bg-base-200 border-0",
style: () => isSource() ? '' : 'display:none',
value: source,
oninput: (e) => { source(e.target.value); if (editorRef) editorRef.innerHTML = e.target.value; p.onchange?.(e.target.value); }
})
]),
h("div", { class: "px-3 py-1 border-t border-base-300 bg-base-100/50 text-[10px] text-right text-base-content/60 italic" }, [
h("span", () => `${count()}`)
])
])
};

11
src/helpers.js Normal file
View File

@@ -0,0 +1,11 @@
// Helpers
export const val = val => typeof val === "function" ? val() : val;
export const getBy = (item, field = 'label') => (item && typeof item === 'object') ? item[field] : item;
export const cls = (...classes) => classes.filter(Boolean).join(' ').trim();
export const filterBy = (items, query, field = 'label') => {
const q = String(val(query) || '').toLowerCase();
const list = (val(items) || []).map(i => typeof i === 'object' ? i : { label: i, value: i });
return !q ? list : list.filter(item => String(item[field] || '').toLowerCase().includes(q));
};
export const rand = (r) => `${r}-${Math.random().toString(36).slice(2, 9)}`
export const hide = () => document.activeElement?.blur()

7
src/locale.js Normal file
View File

@@ -0,0 +1,7 @@
const currentLocale = $("en");
const lang = {
es: { uploadFiles: "Arrastrar y soltar o click para seleccionar..." },
en: { uploadFiles: "Drag and drop or click to select" }
};
export const setLocale = (locale) => { if (lang[locale]) currentLocale(locale) }
export const t = t => () => lang[currentLocale()][t] || t;

539
src/sigpro-ui.js Normal file
View File

@@ -0,0 +1,539 @@
import { $, watch, h, mount, when, each, isFunc } from "./sigpro.js"
import { val, getBy, cls, filterBy, rand, hide } from "./helpers.js"
import { t } from "./locale.js"
// CreateFunctions
const c1 = (tag, cls) => (p) => h(tag, { ...p, class: `${cls} ${p?.class || ''}`.trim() })
const c2 = (tag, cls) => (p, c) => h(tag, { ...p, class: `${cls} ${p?.class || ''}`.trim() }, c)
const ct = (tag, cls, type) => (p) => h(tag, { type, ...p, class: `${cls} ${p?.class || ''}`.trim() })
// Components
export const Alert = c2("div", "alert")
export const Avatar = (p, c) => h("div", { class: "avatar" }, h('div', { class: p.class }, c))
export const AvatarGroup = c2("div", "avatar-group -space-x-6")
export const Badge = c2("span", "badge")
export const Breadcrumbs = c2("div", "breadcrumbs")
export const Button = c2("button", "btn")
export const Card = c2("div", "card")
export const CardTitle = c2("div", "card-title")
export const CardBody = c2("div", "card-body")
export const CardActions = c2("div", "card-actions")
export const Carousel = c2("div", "carousel")
export const CarouselItem = c2("div", "carousel-item")
export const Chat = c2("div", "chat")
export const ChatBubble = c2("div", "chat-bubble")
export const ChatFooter = c2("div", "chat-footer")
export const ChatHeader = c2("div", "chat-header")
export const ChatImage = (p, c) => h("div", { ...p, class: cls("chat-image avatar", p.class) }, h("div", { class: "w-10 rounded-full" }, typeof c === "string" ? h("img", { src: c, alt: "avatar" }) : c))
export const Checkbox = ct("input", "checkbox", "checkbox")
export const Drawer = c2("div", "drawer")
export const DrawerToggle = (p) => input({ ...p, type: 'checkbox', class: 'drawer-toggle', checked: () => val(p.checked), onchange: (e) => isFunc(p.checked) && p.checked(e.target.checked) })
export const DrawerContent = c2("div", "drawer-content")
export const DrawerSide = c2("div", "drawer-side")
export const DrawerOverlay = (p) => label({ ...p, for: p.for, class: cls('drawer-overlay', p.class) })
export const Divider = c1("div", "divider")
export const Dropdown = c2("div", "dropdown")
export const DropdownButton = (p, c) => (h('div', { ...p, tabindex: '0', role: 'button', class: cls('btn', p.class) }, c))
export const DropdownContent = (p, c) => (h('div', { ...p, tabindex: '0', class: cls('dropdown-content', p.class) }, c))
export const Fab = (p, c) => h("div", { class: "fab" }, [h('div', { tabindex: "0", role: "button", class: cls('btn', p.class) }, Icon({},p.icon)), c])
export const Fieldset = (p, c) => h("fieldset", { class: cls("fieldset", p.class) }, [h("legend", { class: "fieldset-legend" }, p.label), c])
export const Icon = (p, c) => h("span", { ...p, class: cls(c, p.class) })
export const Indicator = (p, c) => h("div", { ...p, class: cls("indicator", p.class) }, [p.value && h("span", { class: cls("indicator-item badge", p.class) }, p.value), c])
export const Kbd = c2("kbd", "kbd")
export const List = c2("ul", "list")
export const ListRows = (p) => () => (val(p.items) || []).map((item, idx) => h('li', { class: cls('list-row', p.class, item?.class) }, typeof p.render === 'function' ? p.render(item, idx) : item))
export const Loading = c2("span", "loading loading-spinner")
export const Navbar = c2("div", "navbar")
export const Progress = c1("progress", "progress")
export const Radial = (p, c) => h("div", { class: cls("radial-progress", p.class), style: `--value:${val(p.value) ?? 0};`, role: "progressbar", "aria-valuenow": p.value }, c)
export const Radio = ct("input", "radio", "radio")
export const Range = ct("input", "range", "range")
export const Rating = c2("div", "rating")
export const RatingItems = (p) => [...Array(p.count)].map((_, i) => h('input', { class: cls('mask', p.class), name: p.name, type: 'radio', checked: () => val(p.value) === i, onchange: () => isFunc(p.value) ? p.value(i) : p.onchange?.(i) }))
export const Skeleton = c1("div", "skeleton")
export const SkeletonText = c1("span", "skeleton skeleton-text")
export const Stack = c2("div", "stack")
export const Stats = c2("div", "stats shadow")
export const Steps = c2("ul", "steps")
export const Step = (p, c) => h("li", { ...p, class: cls("step", p.class), "data-content": p.dataContent }, c)
export const Swap = c2("label", "swap")
export const SwapToggle = (p) => h('input', { type: 'checkbox', checked: () => val(p.value), onchange: (e) => isFunc(p.value) && p.value(e.target.checked), class: p.class })
export const SwapOn = c2("div", "swap-on")
export const SwapOff = c2("div", "swap-off")
export const Table = c2("table", "table")
export const Textarea = c1("textarea", "textarea")
export const Textrotate = (p, c) => h('span', { ...p, class: cls('text-rotate', p.class) }, h('span', {}, c))
export const Timeline = c2("ul", "timeline")
export const Toggle = ct("input", "toggle", "checkbox")
export const Tooltip = (p, c) => h("div", { ...p, class: cls("tooltip", p.class), "data-tip": p.tip }, c)
export const Accordion = (p) => {
const name = p.name || rand('acc')
return each(p.items, (it) => {
return h('div', { class: cls('collapse', p.class) }, [
h('input', { type: 'radio', name, checked: it.open || undefined }),
it.title ? h('div', { class: cls("collapse-title", `${it.classTitle ?? ' font-semibold'}`) }, it.title) : null,
it.content ? h('div', { class: cls("collapse-content text-sm", `${it.classContent ?? ' font-semibold'}`) }, it.content) : null,
]);
});
};
export const Autocomplete = ({ items, value, onselect, placeholder = '...', ...props }) => {
const query = $(val(value) || '')
const filtered = $(() => filterBy(items, query()))
const pick = (item) => {
const display = getBy(item)
const actual = typeof item === 'string' ? item : item.value
query(display)
if (isFunc(value)) value(actual)
onselect?.(item)
hide()
}
return Dropdown({ class: 'w-80' }, [
h('div', { tabindex: '0', role: 'button', class: 'w-full' }, Input({ ...props, placeholder, value: query, left: Icon({},'icon-[lucide--search]') })),
DropdownContent({ class: 'p-2 bg-base-100 rounded-box shadow-xl w-full max-h-60 overflow-y-auto border border-base-300 z-50' },
h('ul', { class: 'menu flex-col flex-nowrap w-full p-0' }, [
each(filtered, (item) => h('li', {}, [h('a', { onmousedown: (e) => e.preventDefault(), onclick: () => pick(item) }, getBy(item))]), 'value'),
() => filtered().length === 0 ? h('li', { class: 'p-4 opacity-50 text-center' }, 'Sin resultados') : null
])
)
])
};
export const Calendar = (p) => {
const internalDate = $(new Date())
const hoverDate = $(null)
const startHour = $(0)
const endHour = $(0)
const now = new Date()
const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
const fmt = d => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
const rangeMode = () => val(p.range) === true
const current = () => val(p.value)
const selectDate = (date) => {
const s = fmt(date)
const v = current()
if (rangeMode()) {
if (!v?.start || (v.start && v.end)) {
p.onChange?.({ start: s, end: null, ...(p.hour && { startHour: startHour() }) })
} else {
const start = v.start
const nv = s < start ? { start: s, end: start } : { start, end: s }
if (p.hour) { nv.startHour = v.startHour ?? startHour(); nv.endHour = endHour() }
p.onChange?.(nv)
}
} else {
p.onChange?.(p.hour ? `${s}T${String(startHour()).padStart(2, '0')}:00:00` : s)
}
}
const move = (m) => { const d = internalDate(); internalDate(new Date(d.getFullYear(), d.getMonth() + m, 1)) }
const moveYear = (y) => { const d = internalDate(); internalDate(new Date(d.getFullYear() + y, d.getMonth(), 1)) }
const HourSlider = ({ value: hVal, onChange: onH }) => h('div', { class: 'flex-1' }, [
h('div', { class: 'flex gap-2 items-center' }, [
h('input', { type: 'range', min: 0, max: 23, value: hVal, class: 'range range-xs flex-1', oninput: e => onH(+e.target.value) }),
h('span', { class: 'text-sm font-mono min-w-[48px] text-center' }, () => String(val(hVal)).padStart(2, '0') + ':00')
])
])
return h('div', {
class: cls('p-4 bg-base-100 border border-base-300 shadow-2xl rounded-box w-80 select-none', p.class)
}, [
h('div', { class: 'flex justify-between items-center mb-4 gap-1' }, [
h('div', { class: 'flex gap-0.5' }, [
h('button', { type: 'button', class: 'btn btn-ghost btn-xs px-1', onclick: () => moveYear(-1) }, h('span', { class: 'icon-[lucide--chevrons-left]' })),
h('button', { type: 'button', class: 'btn btn-ghost btn-xs px-1', onclick: () => move(-1) }, h('span', { class: 'icon-[lucide--chevron-left]' }))
]),
h('span', { class: 'font-bold uppercase flex-1 text-center' }, () => internalDate().toLocaleString('es-ES', { month: 'short', year: 'numeric' })),
h('div', { class: 'flex gap-0.5' }, [
h('button', { type: 'button', class: 'btn btn-ghost btn-xs px-1', onclick: () => move(1) }, h('span', { class: 'icon-[lucide--chevron-right]' })),
h('button', { type: 'button', class: 'btn btn-ghost btn-xs px-1', onclick: () => moveYear(1) }, h('span', { class: 'icon-[lucide--chevrons-right]' }))
])
]),
h('div', { class: 'grid grid-cols-7 gap-1', onmouseleave: () => hoverDate(null) }, [
...['L', 'M', 'X', 'J', 'V', 'S', 'D'].map(d => h('div', { class: 'text-[10px] opacity-40 font-bold text-center' }, d)),
() => {
const d = internalDate(), y = d.getFullYear(), m = d.getMonth()
const firstDay = new Date(y, m, 1).getDay()
const offset = firstDay === 0 ? 6 : firstDay - 1
const dim = new Date(y, m + 1, 0).getDate()
const cells = []
for (let i = 0; i < offset; i++) cells.push(h('div'))
for (let i = 1; i <= dim; i++) {
const date = new Date(y, m, i), ds = fmt(date)
cells.push(h('button', {
type: 'button',
class: () => {
const v = current(), h = hoverDate()
const isStart = typeof v === 'string' ? v.split('T')[0] === ds : v?.start === ds
const isEnd = v?.end === ds
let inRange = false
if (rangeMode() && v?.start) {
const start = v.start
if (!v.end && h) inRange = (ds > start && ds <= h) || (ds < start && ds >= h)
else if (v.end) inRange = ds > start && ds < v.end
}
const base = 'btn btn-xs p-0 aspect-square min-h-0 h-auto font-normal relative'
const st = isStart || isEnd ? 'btn-primary z-10' : inRange ? 'bg-primary/20 border-none rounded-none' : 'btn-ghost'
const today = ds === todayStr ? 'ring-1 ring-primary ring-inset font-black text-primary' : ''
return cls(base, st, today)
},
onmouseenter: () => rangeMode() && hoverDate(ds),
onclick: () => selectDate(date)
}, i.toString()))
}
return cells
}
]),
p.hour ? h('div', { class: 'mt-3 pt-2 border-t border-base-300' },
rangeMode()
? h('div', { class: 'flex gap-4' }, [HourSlider({ value: startHour, onChange: h => startHour(h) }), HourSlider({ value: endHour, onChange: h => endHour(h) })])
: HourSlider({ value: startHour, onChange: h => startHour(h) })
) : null
])
};
export const Colorpicker = (p) => {
const current = () => val(p.value) || '#000000'
return Dropdown({}, [
DropdownButton({ class: 'btn' }, [
h('div', { class: 'size-5 rounded-sm', style: () => `background-color: ${current()}` }),
p.label && h('span', {}, p.label)
]),
DropdownContent({ class: 'p-0' },
ColorPalette({ value: p.value, onchange: (c) => { isFunc(p.value) ? p.value(c) : p.onchange?.(c) } })
)
])
};
export const ColorPalette = (p) => {
const current = () => val(p.value) || '#000000'
const palette = [
'#000', '#1A1A1A', '#333', '#4D4D4D', '#666', '#808080', '#B3B3B3', '#FFF',
'#450a0a', '#7f1d1d', '#991b1b', '#b91c1c', '#dc2626', '#ef4444', '#f87171', '#fca5a5',
'#431407', '#7c2d12', '#9a3412', '#c2410c', '#ea580c', '#f97316', '#fb923c', '#ffedd5',
'#713f12', '#a16207', '#ca8a04', '#eab308', '#facc15', '#fde047', '#fef08a', '#fff9c4',
'#064e3b', '#065f46', '#059669', '#10b981', '#34d399', '#4ade80', '#84cc16', '#d9f99d',
'#082f49', '#075985', '#0284c7', '#0ea5e9', '#38bdf8', '#7dd3fc', '#22d3ee', '#cffafe',
'#1e1b4b', '#312e81', '#4338ca', '#4f46e5', '#6366f1', '#818cf8', '#a5b4fc', '#e0e7ff',
'#2e1065', '#4c1d95', '#6d28d9', '#7c3aed', '#8b5cf6', '#a855f7', '#d946ef', '#fae8ff'
]
const pick = (c) => {
isFunc(p.value) ? p.value(c) : p.onchange?.(c)
hide()
}
return h('div', {
class: cls('p-3 bg-base-100 rounded-box shadow w-64', p.class)
}, h('div', { class: 'grid grid-cols-8 gap-1' },
palette.map(c => h('button', {
type: 'button',
style: `background-color: ${c}`,
class: () => {
const act = current().toLowerCase() === c.toLowerCase()
return `size-6 rounded-sm cursor-pointer transition-all hover:scale-125 hover:z-10 active:scale-95 outline-none border border-black/5 p-0 min-h-0 ${act ? 'ring-2 ring-offset-1 ring-primary z-10 scale-110' : ''}`
},
onclick: () => { pick(c) }
}))
))
};
export const Datepicker = (p) => {
const displayValue = $("")
const rangeMode = () => val(p.range) === true
watch(() => {
const v = val(p.value)
if (!v) return displayValue("")
let text = ""
if (typeof v === "string") {
text = p.hour && v.includes("T") ? v.replace("T", " ") : v
} else if (v.start && v.end) {
const startStr = p.hour && v.startHour != null ? `${v.start} ${String(v.startHour).padStart(2, "0")}:00` : v.start
const endStr = p.hour && v.endHour != null ? `${v.end} ${String(v.endHour).padStart(2, "0")}:00` : v.end
text = `${startStr} - ${endStr}`
} else if (v.start) {
const startStr = p.hour && v.startHour != null ? `${v.start} ${String(v.startHour).padStart(2, "0")}:00` : v.start
text = `${startStr}...`
}
displayValue(text)
})
const handleChange = (val) => {
if (isFunc(p.value)) p.value(val)
else p.onChange?.(val)
if (!rangeMode() || val?.end != null) hide()
}
return Dropdown({ class: cls('w-full', p.class) }, [
h('label', {
tabindex: '0',
role: 'button',
class: 'input input-bordered flex items-center gap-2 cursor-pointer'
}, [
h('span', { class: 'icon-[lucide--calendar] shrink-0' }),
h('span', {
class: () => `grow text-left truncate ${!displayValue() ? 'opacity-50' : ''}`,
}, () => displayValue() || p.placeholder || (rangeMode() ? 'Seleccionar rango...' : 'Seleccionar fecha...')),
() => displayValue() ? h('button', {
type: 'button',
class: 'btn btn-ghost btn-xs btn-circle -mr-2',
onmousedown: (e) => {
e.preventDefault()
e.stopPropagation()
if (isFunc(p.value)) p.value(null)
else p.onChange?.(null)
displayValue("") // Forzar limpieza visual inmediata
}
}, h('span', { class: 'icon-[lucide--x] opacity-50' })) : null
]),
DropdownContent({ class: 'p-0' },
Calendar({ value: p.value, range: rangeMode(), hour: p.hour, onChange: handleChange })
)
])
};
export const Fileinput = (p) => {
const files = $([])
const drag = $(false)
const error = $(null)
const maxBytes = (p.max || 2) * 1024 * 1024
const process = (fileList) => {
const arr = Array.from(fileList)
error(null)
if (arr.some(f => f.size > maxBytes)) {
error(`Máx ${p.max || 2}MB`)
return
}
const updated = [...files(), ...arr]
files(updated)
if (isFunc(p.onselect)) p.onselect(updated)
else if (isFunc(p.value)) p.value(updated)
}
const remove = (idx) => {
const updated = files().filter((_, i) => i !== idx)
files(updated)
if (isFunc(p.onselect)) p.onselect(updated)
else if (isFunc(p.value)) p.value(updated)
}
return h('div', { class: cls('fieldset w-full p-0', p.class) }, [
h('label', {
class: () => `relative flex items-center justify-between w-full h-12 px-4 border-2 border-dashed rounded-lg cursor-pointer transition-all duration-200 ${drag() ? 'border-primary bg-primary/10' : 'border-base-content/20 bg-base-100 hover:bg-base-200'}`,
ondragover: (e) => { e.preventDefault(); drag(true) },
ondragleave: () => drag(false),
ondrop: (e) => { e.preventDefault(); drag(false); process(e.dataTransfer.files) }
}, [
h('div', { class: 'flex items-center gap-3 w-full' }, [
h('span', { class: 'icon-[lucide--upload]' }),
h('span', { class: 'text-sm opacity-70 truncate grow text-left' }, t("uploadFiles")),
h('span', { class: 'text-[10px] opacity-40 shrink-0' }, `Máx ${p.max || 2}MB`)
]),
h('input', {
type: 'file',
multiple: true,
accept: p.accept || '*',
class: 'hidden',
onchange: (e) => process(e.target.files)
})
]),
() => error() && h('span', { class: 'text-[10px] text-error mt-1 px-1 font-medium' }, error()),
when(() => files().length > 0, () =>
h('ul', { class: 'mt-2 space-y-1' },
each(files, (file, idx) =>
h('li', { class: 'flex items-center justify-between p-1.5 pl-3 text-xs bg-base-200/50 rounded-md border border-base-300' }, [
h('div', { class: 'flex items-center gap-2 truncate' }, [
h('span', { class: 'opacity-50' }, '📄'),
h('span', { class: 'truncate font-medium max-w-[200px]' }, file.name),
h('span', { class: 'text-[9px] opacity-40' }, `(${(file.size / 1024).toFixed(0)} KB)`)
]),
h('button', {
type: 'button',
class: 'btn btn-ghost btn-xs btn-circle',
onclick: (e) => { e.preventDefault(); remove(idx) }
}, h('span', { class: 'icon-[lucide--x]' }))
])
)
)
)
])
};
export const Input = (p) => {
const { label, icon, float, placeholder, value, left, right, rule, hint, content, ...rest } = p;
const showPassword = $(false);
const isPassword = p.type === 'password';
const pattern = rule ?? null;
const inputType = () => isPassword ? (val(showPassword) ? 'text' : 'password') : (p.type || 'search');
return h("label", { class: float ? 'floating-label' : '' }, [
float ? h("span", {}, label) : null,
h("label", { pattern: pattern, class: () => cls('input validator', p.class) },
[
label && !float ? h('span', { class: 'label' }, label) : null,
left ?? null,
h('input', { ...rest, type: inputType, class: 'grow', pattern: pattern, placeholder: placeholder || label || ' ', value: value }),
right ?? null,
isPassword ? Swap({ class: 'ml-2' }, [
SwapToggle({ value: showPassword, class: "swap-rotate" }),
SwapOn({}, Icon({},'icon-[lucide--eye]')),
SwapOff({}, Icon({},'icon-[lucide--eye-off]')),
]) : null
]),
hint ? h('div', { class: "validator-hint" }, hint) : null,
]);
};
export const Menu = (p) => {
if (p.children !== undefined) return h('ul', { class: cls('menu', p.class), ...p }, p.children)
const { items } = p
const render = (item) => item.children
? h('li', {}, h('details', { open: item.open || undefined }, [
h('summary', {}, getBy(item)),
h('ul', {}, each(() => val(item.children) || [], render))
]))
: h('li', {}, h('a', {
href: item.href,
onclick: item.onclick ? (e) => { if (!item.href) e.preventDefault(); item.onclick(e) } : null
}, getBy(item)))
return h('ul', { class: cls('menu', p.class) },
each(() => val(items) || [], render)
)
};
export const Modal = (p) => {
let dialogRef = null;
watch(() => { const isOpen = val(p.open); if (!dialogRef) return; isOpen ? dialogRef.showModal() : dialogRef.hide(); });
const close = () => isFunc(p.open) && p.open(false);
return h("dialog", { ...p, ref: el => dialogRef = el, class: cls('modal', p.class), onclose: close, oncancel: close }, [
h("div", { class: "modal-box" }, [
p.title && h("h3", { class: "text-lg font-bold" }, p.title),
p.children,
h("div", { class: "modal-action" }, [
p.actions || Button({ class: 'btn', onclick: close }, 'Cerrar')
])
]),
h("form", { method: "dialog", class: "modal-backdrop" }, [
h("button", {}, "close")
])
]);
};
export const Select = (p, c) => {
if (c !== undefined) return h('select', { class: cls('select', p.class), ...p }, c)
const { label, float, placeholder, placeholderDisabled = true, value, left, right, hint, items, keyFn, ...rest } = p
const opts = () => {
const raw = val(items) || []
const ph = placeholder ? [{ disabled: placeholderDisabled, label: placeholder, value: '' }] : []
return [...ph, ...raw]
}
return h('label', { class: float ? 'floating-label' : '' }, [
float ? h('span', {}, label) : null,
h('label', { class: cls('select', rest.class) }, [
(!float && label) ? h('span', { class: 'label' }, label) : null,
left ?? null,
h('select', {
value: () => val(value),
onchange: (e) => isFunc(value) ? value(e.target.value) : rest.onchange?.(e)
},
each(opts, (item) => {
const val = getBy(item, item.value !== undefined ? 'value' : undefined)
const lab = getBy(item, 'label')
return h('option', { value: val, disabled: item.disabled || undefined }, lab)
})
),
right ?? null
]),
hint ? h('div', { class: 'validator-hint' }, hint) : null
])
};
export const Stat = (p) => h('div', { ...p, class: cls('stat', p.class) }, [
p.title ? h('div', { class: 'stat-title' }, p.title) : null,
p.value ? h('div', { class: 'stat-value' }, p.value) : null,
p.desc ? h('div', { class: 'stat-desc' }, p.desc) : null
]);
export const TableItems = ({ items, columns = [], header = true }) => {
const head = header !== false && columns.some(c => c.label) ? h('thead', {}, h('tr', {}, columns.map(c => h('th', { class: c.class }, c.label)))) : null
const body = h('tbody', {}, () => {
const list = val(items) || []
return list.map((it, idx) => h('tr', {}, columns.map(c => { const v = c.render ? c.render(it, idx) : it[c.key]; return h('td', { class: c.class }, v) })))
})
return [head, body].filter(Boolean)
};
export const Tabs = (p, c) => {
if (!p.items) {
const { class: className, ...rest } = p
return h('div', { ...rest, class: cls('tabs', className) }, c)
}
const { items, activeIndex, onClose, class: className, ...rest } = p
const get = x => (isFunc(x) ? x() : x)
const closeH = onClose || (isFunc(items) ? (idx, item) => {
const arr = val(items)
const newArr = arr.filter((_, i) => i !== idx)
items(newArr)
if (activeIndex() >= newArr.length) activeIndex(Math.max(0, newArr.length - 1))
} : null)
return h('div', { ...rest, class: cls('tabs', className) }, () => {
const list = val(items) || []
return list.flatMap((it, idx) => {
const isActive = () => activeIndex() === idx
const button = h('button', {
class: () => `tab ${isActive() ? 'tab-active' : ''} ${it.class || ''}`,
onclick: (e) => { e.preventDefault(); activeIndex(idx); it.onclick?.(e) }
}, [
getBy(it),
it.closable ? h('span', {
class: 'ml-1 inline-flex items-center justify-center w-4 h-4 rounded-full hover:bg-base-300 text-base-content/60 hover:text-base-content cursor-pointer',
onclick: (e) => { e.stopPropagation(); closeH?.(idx, it) }
}, h('span', { class: 'icon-[lucide--x] w-3 h-3' })) : null
])
const contentDiv = h('div', {
class: 'tab-content bg-base-100 border-base-300 p-6',
style: () => `display: ${isActive() ? 'block' : 'none'};`
}, isFunc(it.content) ? it.content() : it.content)
return [button, contentDiv]
})
})
};
export const Toast = (message, type = "alert-success", duration = 3500) => {
let container = document.getElementById("sigpro-toast-container");
if (!container) {
container = h("div", { id: "sigpro-toast-container", class: "fixed top-0 right-0 z-[9999] p-4 flex flex-col items-end gap-2 pointer-events-none" });
document.body.appendChild(container);
}
const host = h("div", { style: "display: contents" });
container.appendChild(host);
let closeFn, timer, enterTimer;
const ToastComponent = () => {
const visible = $(false);
const leaving = $(false);
closeFn = () => {
if (leaving()) return;
clearTimeout(timer);
clearTimeout(enterTimer);
leaving(true);
setTimeout(() => { instance.destroy(); host.remove(); if (!container.hasChildNodes()) container.remove(); }, 300);
};
enterTimer = setTimeout(() => visible(true), 0);
const content = typeof message === 'function' ? val(message) : message;
const msgNode = typeof content === 'string' ? h("span", {}, content) : content;
return h("div", {
class: () => {
const base = `alert alert-soft ${type} shadow-lg transition-all duration-300 inline-flex w-auto whitespace-nowrap pointer-events-auto`;
if (leaving()) return `${base} translate-x-full opacity-0`;
if (visible()) return `${base} translate-x-0 opacity-100`;
return `${base} translate-x-10 opacity-0`;
}
}, [
msgNode,
h("button", {
class: "btn btn-xs btn-circle btn-ghost",
onclick: closeFn
}, h("span", { class: "icon-[lucide--x]" }))
]);
};
const instance = mount(ToastComponent, host);
if (duration > 0) timer = setTimeout(closeFn, duration);
return closeFn;
};

View File

@@ -93,9 +93,7 @@ const batch = fn => {
return fn() return fn()
} finally { } finally {
batchDepth-- batchDepth--
if (batchDepth === 0 && effectQueue.size > 0 && !isFlushing) { if (batchDepth === 0 && effectQueue.size > 0 && !isFlushing) flush()
flush()
}
} }
} }
@@ -215,7 +213,7 @@ const watch = (sources, cb) => {
return () => dispose(effect) return () => dispose(effect)
} }
const effect = createEffect(() => { const effect = createEffect(() => {
const vals = Array.isArray(sources) ? sources.map(s => s()) : sources() const vals = isArr(sources) ? sources.map(s => s()) : sources()
untrack(() => cb(vals)) untrack(() => cb(vals))
}) })
effect() effect()
@@ -240,10 +238,7 @@ const validateAttr = (key, val) => {
if (val == null || val === false) return null if (val == null || val === false) return null
if (isDangerousAttr(key)) { if (isDangerousAttr(key)) {
const sVal = String(val) const sVal = String(val)
if (DANGEROUS_PROTOCOL.test(sVal)) { if (DANGEROUS_PROTOCOL.test(sVal)) return '#'
console.warn(`[SigPro] Bloqueado protocolo peligroso en ${key}`)
return '#'
}
} }
return val return val
} }
@@ -509,4 +504,10 @@ const mount = (comp, target) => {
return inst return inst
} }
if (typeof window !== "undefined") {
"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video"
.split(" ")
.forEach(tag => { window[tag] = (props, children) => h(tag, props, children) })
}
export { $, $$, watch, batch, h, Fragment, mount, when, each, router, onUnmount, isArr, isFunc, isObj } export { $, $$, watch, batch, h, Fragment, mount, when, each, router, onUnmount, isArr, isFunc, isObj }