Independent sigpro vs sigpro-ui
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
This commit is contained in:
1
src/build_editor.js
Normal file
1
src/build_editor.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './editor.js'
|
||||
2
src/build_esm.js
Normal file
2
src/build_esm.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './sigpro-ui.js';
|
||||
export * from './locale.js';
|
||||
7
src/build_umd.js
Normal file
7
src/build_umd.js
Normal 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
189
src/editor.js
Normal 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> `
|
||||
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> `
|
||||
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
11
src/helpers.js
Normal 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
7
src/locale.js
Normal 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;
|
||||
220
src/sigpro-ui.css
Normal file
220
src/sigpro-ui.css
Normal file
@@ -0,0 +1,220 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "daisyui";
|
||||
@plugin "@iconify/tailwind4";
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: "light";
|
||||
default: true;
|
||||
prefersdark: false;
|
||||
color-scheme: "light";
|
||||
--color-base-100: oklch(100% 0 0);
|
||||
--color-base-200: oklch(98% 0 0);
|
||||
--color-base-300: oklch(92% 0 0);
|
||||
--color-base-content: oklch(25% 0.006 285);
|
||||
--color-primary: oklch(25% 0.006 285);
|
||||
--color-primary-content: oklch(98% 0 0);
|
||||
--color-secondary: oklch(55% 0.046 257.417);
|
||||
--color-secondary-content: oklch(98% 0 0);
|
||||
--color-accent: oklch(96% 0 0);
|
||||
--color-accent-content: oklch(25% 0.006 285);
|
||||
--color-neutral: oklch(14% 0.005 285.823);
|
||||
--color-neutral-content: oklch(92% 0.004 286.32);
|
||||
--color-info: oklch(74% 0.16 232);
|
||||
--color-success: oklch(62% 0.17 163);
|
||||
--color-warning: oklch(82% 0.18 84);
|
||||
--color-error: oklch(60% 0.25 27);
|
||||
--radius-selector: 0.5rem;
|
||||
--radius-field: 0.4rem;
|
||||
--radius-box: 0.5rem;
|
||||
--border: 1px;
|
||||
}
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: "dark";
|
||||
default: false;
|
||||
prefersdark: true;
|
||||
color-scheme: "dark";
|
||||
--color-base-100: oklch(15% 0.005 285.823);
|
||||
--color-base-200: oklch(20% 0.005 285.823);
|
||||
--color-base-300: oklch(30% 0.005 285.823);
|
||||
--color-base-content: oklch(92% 0.004 286.32);
|
||||
--color-primary: oklch(98% 0 0);
|
||||
--color-primary-content: oklch(15% 0 0);
|
||||
--color-secondary: oklch(65% 0.046 257.417);
|
||||
--color-secondary-content: oklch(15% 0.005 285.823);
|
||||
--color-accent: oklch(25% 0 0);
|
||||
--color-accent-content: oklch(98% 0 0);
|
||||
--color-neutral: oklch(92% 0.004 286.32);
|
||||
--color-neutral-content: oklch(14% 0.005 285.823);
|
||||
--color-info: oklch(70% 0.1 230);
|
||||
--color-success: oklch(65% 0.15 160);
|
||||
--color-warning: oklch(85% 0.15 90);
|
||||
--color-error: oklch(55% 0.2 27);
|
||||
--radius-selector: 0.5rem;
|
||||
--radius-field: 0.4rem;
|
||||
--radius-box: 0.5rem;
|
||||
--border: 1px;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input,
|
||||
.label,
|
||||
.select,
|
||||
.textarea {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 4px 0px;
|
||||
}
|
||||
|
||||
&:hover:not(:focus) {
|
||||
background-color: oklch(from var(--color-base-100) calc(l - 0.03) c h);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.floating-label span {
|
||||
color: oklch(30% 0.01 260);
|
||||
font-size: 1.1rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.floating-label:focus-within span {
|
||||
color: oklch(25% 0.02 260);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.floating-label:has(input:not(:placeholder-shown)) span {
|
||||
color: oklch(28% 0.01 260);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
|
||||
/* SOLO PARA LA DEMO */
|
||||
/* Forzar DaisyUI .progress sobre Docsify */
|
||||
/* progress.progress {
|
||||
appearance: auto !important;
|
||||
-webkit-appearance: auto !important;
|
||||
-moz-appearance: auto !important;
|
||||
} */
|
||||
.markdown-section progress.progress {
|
||||
all: revert-layer;
|
||||
}
|
||||
|
||||
/* join join-vertical lg:join-horizontal divider divider-horizontal validator validator-hint glass */
|
||||
|
||||
/* Icons */
|
||||
/* .icon-[lucide--calendar] .icon-[lucide--chevrons-left] .icon-[lucide--chevron-left] .icon-[lucide--chevron-right] .icon-[lucide--chevrons-right] .icon-[lucide--info] .icon-[lucide--check-circle] .icon-[lucide--alert-triangle] .icon-[lucide--alert-circle] .icon-[lucide--heart] */
|
||||
/* .icon-[lucide--upload], .icon-[lucide--x], .icon-[lucide--text], .icon-[lucide--lock], .icon-[lucide--calendar], .icon-[lucide--hash], .icon-[lucide--mail], .icon-[lucide--search], .icon-[lucide--phone], .icon-[lucide--link], .icon-[lucide--eye-off], .icon-[lucide--eye] */
|
||||
|
||||
/* Accordion */
|
||||
/* .input, .input-bordered, .input-ghost, .input-primary, .input-secondary, .input-accent, .input-info, .input-success, .input-warning, .input-error, .input-xs, .input-sm, .input-md, .input-lg, .floating-label, */
|
||||
|
||||
/* Alert */
|
||||
/* .alert, .alert-info, .alert-success, .alert-warning, .alert-error, .alert-soft, .alert-outline, .alert-dash, */
|
||||
|
||||
/* Avatar */
|
||||
/* .avatar .avatar-group .avatar-online .avatar-offline .avatar-placeholder */
|
||||
|
||||
/* Breadcrumbs */
|
||||
/* .breadcrumbs */
|
||||
|
||||
/* Autocomplete */
|
||||
/* .menu, .menu-dropdown, .menu-dropdown-show, */
|
||||
|
||||
/* Badge */
|
||||
/* .badge, .badge-primary, .badge-secondary, .badge-accent, .badge-info, .badge-success, .badge-warning, .badge-error, .badge-outline, .badge-soft, .badge-dash, .badge-xs, .badge-sm, .badge-md, .badge-lg, */
|
||||
|
||||
/* Button */
|
||||
/* .btn, .btn-primary, .btn-secondary, .btn-accent, .btn-ghost, .btn-info, .btn-success, .btn-warning, .btn-error, .btn-neutral, .btn-xs, .btn-sm, .btn-md, .btn-lg, .btn-xl, .btn-outline, .btn-soft, .btn-dash, .btn-link, .btn-circle, .btn-square, .btn-wide, .btn-block, .btn-active, .btn-disabled, */
|
||||
|
||||
/* Checkbox & Toggle */
|
||||
/* .checkbox, .checkbox-primary, .checkbox-secondary, .checkbox-accent, .checkbox-info, .checkbox-success, .checkbox-warning, .checkbox-error, .checkbox-xs, .checkbox-sm, .checkbox-md, .checkbox-lg, .toggle, .toggle-primary, .toggle-secondary, .toggle-accent, .toggle-xs, .toggle-sm, .toggle-md, .toggle-lg, */
|
||||
|
||||
/* Chat */
|
||||
/* .chat .chat-end .chat-start .chat.image .chat-header .chat-footer . chat-bubble */
|
||||
|
||||
/* Colorpicker */
|
||||
|
||||
/* Drawer */
|
||||
/* .drawer, .drawer-end .drawer-toggle, .drawer-content, .drawer-side, .drawer-overlay, */
|
||||
|
||||
/* Dropdown */
|
||||
/* .dropdown, .dropdown-content, .dropdown-end, .dropdown-top, .dropdown-bottom , .dropdown-left, .dropdown-right*/
|
||||
|
||||
/* Fab */
|
||||
/* .fab, */
|
||||
|
||||
/* Fieldset */
|
||||
/* .fieldset, .fieldset-legend, */
|
||||
|
||||
/* Indicator */
|
||||
/* .indicator, .indicator-item, */
|
||||
|
||||
/* Input */
|
||||
/* .input, .input-bordered, .input-ghost, .input-primary, .input-secondary, .input-accent, .input-info, .input-success, .input-warning, .input-error, .input-xs, .input-sm, .input-md, .input-lg, .floating-label, */
|
||||
|
||||
/* Kbd */
|
||||
/* .kbd .kbd-xs .kbd-sm .kbd-md .kbd-lg .kbd-xl */
|
||||
|
||||
/* List */
|
||||
/* .list, .list-row, .list-bullet, .list-image, .list-none, */
|
||||
|
||||
/* Mask */
|
||||
/* .mask, .mask-star, .mask-star-2, .mask-heart, .mask-circle, */
|
||||
|
||||
/* Menu */
|
||||
/* .menu, .menu-dropdown, .menu-dropdown-show, */
|
||||
|
||||
/* Modal */
|
||||
/* .modal, .modal-box, .modal-action, .modal-backdrop, .modal-open, .modal-middle, .modal-top, .modal-bottom, */
|
||||
|
||||
/* Navbar */
|
||||
/* .navbar, .navbar-start, .navbar-center, .navbar-end, */
|
||||
|
||||
/* Progress */
|
||||
/* .progress .progress-neutral .progress-primary .progress-secondary .progress-accent .progress-info .progress-success .progress-warning .progress-error */
|
||||
|
||||
/* Radial */
|
||||
/* .radial-progress */
|
||||
|
||||
/* Radio */
|
||||
/* .radio, .radio-primary, .radio-secondary, .radio-accent, .radio-info, .radio-success, .radio-warning, .radio-error, .radio-xs, .radio-sm, .radio-md, .radio-lg, */
|
||||
|
||||
/* Range */
|
||||
/* .range, .range-primary, .range-secondary, .range-accent, .range-info, .range-success, .range-warning, .range-error, .range-xs, .range-sm, .range-md, .range-lg, */
|
||||
|
||||
/* Rating */
|
||||
/* .rating, .rating-half, .rating-hidden, */
|
||||
|
||||
/* Select */
|
||||
/* .select, .select-bordered, .select-primary, .select-secondary, .select-accent, .select-info, .select-success, .select-warning, .select-error, .select-xs, .select-sm, .select-md, .select-lg, */
|
||||
|
||||
/* Stack */
|
||||
/* .stack, .stack-top, .stack-bottom, .stack-start, .stack-end, */
|
||||
|
||||
/* Stat */
|
||||
/* .stat, .stat-figure, .stat-title, .stat-value, .stat-desc, */
|
||||
|
||||
/* Swap */
|
||||
/* .swap, .swap-on, .swap-off, .swap-active, .swap-rotate, .swap-flip, .swap-indeterminate, */
|
||||
|
||||
/* Table */
|
||||
/* .table, .table-zebra, .table-pin-rows, .table-pin-cols, .table-xs, .table-sm, .table-md, .table-lg, */
|
||||
|
||||
/* Tabs */
|
||||
/* .tabs, .tabs-box, .tabs-lift, .tabs-border, .tab, .tab-content, */
|
||||
|
||||
/* Timeline */
|
||||
/* .timeline, .timeline-vertical, .timeline-horizontal, .timeline-compact, .timeline-start, .timeline-middle, .timeline-end, .timeline-box, */
|
||||
|
||||
/* Toast */
|
||||
/* .icon-[lucide--x], */
|
||||
|
||||
/* Tooltip */
|
||||
/* .tooltip, .tooltip-top, .tooltip-bottom, .tooltip-left, .tooltip-right, .tooltip-primary, .tooltip-secondary, .tooltip-accent, .tooltip-info, .tooltip-success, .tooltip-warning, .tooltip-error, .tooltip-open, */
|
||||
539
src/sigpro-ui.js
Normal file
539
src/sigpro-ui.js
Normal 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;
|
||||
};
|
||||
513
src/sigpro.js
Normal file
513
src/sigpro.js
Normal file
@@ -0,0 +1,513 @@
|
||||
const isFunc = f => typeof f === "function"
|
||||
const isObj = o => o && typeof o === "object"
|
||||
const isArr = Array.isArray
|
||||
const doc = typeof document !== "undefined" ? document : null
|
||||
const ensureNode = n => n?._isRuntime ? n.container : (n instanceof Node ? n : doc.createTextNode(n == null ? "" : String(n)))
|
||||
|
||||
let activeEffect = null
|
||||
let activeOwner = null
|
||||
let isFlushing = false
|
||||
let batchDepth = 0
|
||||
const effectQueue = new Set()
|
||||
const proxyCache = new WeakMap()
|
||||
const ITER = Symbol('iter')
|
||||
const MOUNTED_NODES = new WeakMap()
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg"
|
||||
const XLINK_NS = "http://www.w3.org/1999/xlink"
|
||||
const SVG_TAGS = new Set("svg,path,circle,rect,line,polyline,polygon,g,defs,text,textPath,tspan,use,symbol,image,marker,ellipse".split(","))
|
||||
|
||||
const dispose = eff => {
|
||||
if (!eff || eff._disposed) return
|
||||
eff._disposed = true
|
||||
const stack = [eff]
|
||||
while (stack.length) {
|
||||
const e = stack.pop()
|
||||
if (e._cleanups) {
|
||||
e._cleanups.forEach(fn => fn())
|
||||
e._cleanups.clear()
|
||||
}
|
||||
if (e._children) {
|
||||
e._children.forEach(child => stack.push(child))
|
||||
e._children.clear()
|
||||
}
|
||||
if (e._deps) {
|
||||
e._deps.forEach(depSet => depSet.delete(e))
|
||||
e._deps.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onUnmount = fn => {
|
||||
if (activeOwner) (activeOwner._cleanups ||= new Set()).add(fn)
|
||||
}
|
||||
|
||||
const untrack = fn => {
|
||||
const p = activeEffect
|
||||
activeEffect = null
|
||||
try { return fn() } finally { activeEffect = p }
|
||||
}
|
||||
|
||||
const createEffect = (fn, isComputed = false) => {
|
||||
const effect = () => {
|
||||
if (effect._disposed) return
|
||||
if (effect._deps) effect._deps.forEach(s => s.delete(effect))
|
||||
if (effect._cleanups) {
|
||||
effect._cleanups.forEach(c => c())
|
||||
effect._cleanups.clear()
|
||||
}
|
||||
const prevEffect = activeEffect
|
||||
const prevOwner = activeOwner
|
||||
activeEffect = activeOwner = effect
|
||||
try {
|
||||
return effect._result = fn()
|
||||
} catch (e) {
|
||||
console.error("[SigPro]", e)
|
||||
} finally {
|
||||
activeEffect = prevEffect
|
||||
activeOwner = prevOwner
|
||||
}
|
||||
}
|
||||
effect._deps = effect._cleanups = effect._children = null
|
||||
effect._disposed = false
|
||||
effect._isComputed = isComputed
|
||||
effect._depth = activeEffect ? activeEffect._depth + 1 : 0
|
||||
effect._mounts = []
|
||||
effect._parent = activeOwner
|
||||
if (activeOwner) (activeOwner._children ||= new Set()).add(effect)
|
||||
return effect
|
||||
}
|
||||
|
||||
const flush = () => {
|
||||
if (isFlushing) return
|
||||
isFlushing = true
|
||||
const sorted = Array.from(effectQueue).sort((a, b) => a._depth - b._depth)
|
||||
effectQueue.clear()
|
||||
for (const e of sorted) if (!e._disposed) e()
|
||||
isFlushing = false
|
||||
}
|
||||
|
||||
const batch = fn => {
|
||||
batchDepth++
|
||||
try {
|
||||
return fn()
|
||||
} finally {
|
||||
batchDepth--
|
||||
if (batchDepth === 0 && effectQueue.size > 0 && !isFlushing) flush()
|
||||
}
|
||||
}
|
||||
|
||||
const trackUpdate = (subs, trigger = false) => {
|
||||
if (!trigger && activeEffect && !activeEffect._disposed) {
|
||||
subs.add(activeEffect);
|
||||
(activeEffect._deps ||= new Set()).add(subs)
|
||||
} else if (trigger && subs.size > 0) {
|
||||
let hasQueue = false
|
||||
for (const e of subs) {
|
||||
if (e === activeEffect || e._disposed) continue
|
||||
if (e._isComputed) {
|
||||
e._dirty = true
|
||||
if (e._subs) trackUpdate(e._subs, true)
|
||||
} else {
|
||||
effectQueue.add(e)
|
||||
hasQueue = true
|
||||
}
|
||||
}
|
||||
if (hasQueue && !isFlushing && batchDepth === 0) queueMicrotask(flush)
|
||||
}
|
||||
}
|
||||
|
||||
const $ = (val, key = null) => {
|
||||
const subs = new Set()
|
||||
if (isFunc(val)) {
|
||||
let cache
|
||||
const computed = () => {
|
||||
if (computed._dirty) {
|
||||
const prev = activeEffect
|
||||
activeEffect = computed
|
||||
try {
|
||||
const next = val()
|
||||
if (!Object.is(cache, next)) {
|
||||
cache = next
|
||||
trackUpdate(subs, true)
|
||||
}
|
||||
} finally {
|
||||
activeEffect = prev
|
||||
}
|
||||
computed._dirty = false
|
||||
}
|
||||
trackUpdate(subs)
|
||||
return cache
|
||||
}
|
||||
computed._isComputed = true
|
||||
computed._subs = subs
|
||||
computed._dirty = true
|
||||
computed._deps = null
|
||||
computed._disposed = false
|
||||
return computed
|
||||
}
|
||||
if (key) try { val = JSON.parse(localStorage.getItem(key)) ?? val } catch (e) { }
|
||||
return (...args) => {
|
||||
if (args.length) {
|
||||
const next = isFunc(args[0]) ? args[0](val) : args[0]
|
||||
if (!Object.is(val, next)) {
|
||||
val = next
|
||||
if (key) localStorage.setItem(key, JSON.stringify(val))
|
||||
trackUpdate(subs, true)
|
||||
}
|
||||
}
|
||||
trackUpdate(subs)
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
const $$ = (target) => {
|
||||
if (!isObj(target)) return target
|
||||
const cached = proxyCache.get(target)
|
||||
if (cached) return cached
|
||||
|
||||
const subs = new Map()
|
||||
const getSubs = (key) => {
|
||||
let set = subs.get(key)
|
||||
if (!set) subs.set(key, set = new Set())
|
||||
return set
|
||||
}
|
||||
|
||||
const proxy = new Proxy(target, {
|
||||
get(target, key, receiver) {
|
||||
if (typeof key !== 'symbol') trackUpdate(getSubs(key))
|
||||
return $$(Reflect.get(target, key, receiver))
|
||||
},
|
||||
set(target, key, value, receiver) {
|
||||
const hadKey = Reflect.has(target, key)
|
||||
const oldValue = Reflect.get(target, key, receiver)
|
||||
const result = Reflect.set(target, key, value, receiver)
|
||||
if (result && !Object.is(oldValue, value)) {
|
||||
trackUpdate(getSubs(key), true)
|
||||
if (!hadKey) trackUpdate(getSubs(ITER), true)
|
||||
}
|
||||
return result
|
||||
},
|
||||
deleteProperty(target, key) {
|
||||
const result = Reflect.deleteProperty(target, key)
|
||||
if (result) {
|
||||
trackUpdate(getSubs(key), true)
|
||||
trackUpdate(getSubs(ITER), true)
|
||||
}
|
||||
return result
|
||||
},
|
||||
ownKeys(target) {
|
||||
trackUpdate(getSubs(ITER))
|
||||
return Reflect.ownKeys(target)
|
||||
}
|
||||
})
|
||||
|
||||
proxyCache.set(target, proxy)
|
||||
return proxy
|
||||
}
|
||||
|
||||
const watch = (sources, cb) => {
|
||||
if (cb === undefined) {
|
||||
const effect = createEffect(sources)
|
||||
effect()
|
||||
return () => dispose(effect)
|
||||
}
|
||||
const effect = createEffect(() => {
|
||||
const vals = isArr(sources) ? sources.map(s => s()) : sources()
|
||||
untrack(() => cb(vals))
|
||||
})
|
||||
effect()
|
||||
return () => dispose(effect)
|
||||
}
|
||||
|
||||
const cleanupNode = (node) => {
|
||||
if (!node) return;
|
||||
if (node._cleanups) {
|
||||
node._cleanups.forEach(fn => fn());
|
||||
node._cleanups.clear();
|
||||
}
|
||||
if (node._ownerEffect) dispose(node._ownerEffect);
|
||||
if (node.childNodes) node.childNodes.forEach(n => cleanupNode(n));
|
||||
};
|
||||
|
||||
var DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i;
|
||||
var DANGEROUS_URI_ATTRS = new Set(["src", "href", "formaction", "action", "background", "code", "archive"]);
|
||||
var isDangerousAttr = (key) => DANGEROUS_URI_ATTRS.has(key) || key.startsWith("on");
|
||||
|
||||
const validateAttr = (key, val) => {
|
||||
if (val == null || val === false) return null
|
||||
if (isDangerousAttr(key)) {
|
||||
const sVal = String(val)
|
||||
if (DANGEROUS_PROTOCOL.test(sVal)) return '#'
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
const h = (tag, props = {}, children = []) => {
|
||||
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
||||
children = props
|
||||
props = {}
|
||||
}
|
||||
|
||||
if (isFunc(tag)) {
|
||||
const effect = createEffect(() => {
|
||||
const result = tag(props, {
|
||||
children,
|
||||
emit: (ev, ...args) => props[`on${ev[0].toUpperCase()}${ev.slice(1)}`]?.(...args)
|
||||
})
|
||||
effect._result = result
|
||||
return result
|
||||
})
|
||||
effect()
|
||||
|
||||
const result = effect._result
|
||||
if (result == null) return null
|
||||
|
||||
const node = (result instanceof Node || (isArr(result) && result.every(n => n instanceof Node)))
|
||||
? result
|
||||
: doc.createTextNode(String(result))
|
||||
|
||||
const attach = n => {
|
||||
if (isObj(n) && !n._isRuntime) {
|
||||
n._mounts = effect._mounts || []
|
||||
n._cleanups = effect._cleanups || new Set()
|
||||
n._ownerEffect = effect
|
||||
}
|
||||
}
|
||||
|
||||
isArr(node) ? node.forEach(attach) : attach(node)
|
||||
return node
|
||||
}
|
||||
|
||||
const isSVG = SVG_TAGS.has(tag)
|
||||
const el = isSVG ? doc.createElementNS(SVG_NS, tag) : doc.createElement(tag)
|
||||
el._cleanups = new Set()
|
||||
|
||||
for (const k of Object.keys(props)) {
|
||||
let v = props[k]
|
||||
if (k === "ref") {
|
||||
isFunc(v) ? v(el) : (v.current = el)
|
||||
continue
|
||||
}
|
||||
if (isSVG && k.startsWith("xlink:")) {
|
||||
const cleanVal = validateAttr(k.slice(6), v)
|
||||
cleanVal == null
|
||||
? el.removeAttributeNS(XLINK_NS, k.slice(6))
|
||||
: el.setAttributeNS(XLINK_NS, k.slice(6), cleanVal)
|
||||
continue
|
||||
}
|
||||
if (k.startsWith("on")) {
|
||||
const ev = k.slice(2).toLowerCase()
|
||||
el.addEventListener(ev, v)
|
||||
const off = () => el.removeEventListener(ev, v)
|
||||
el._cleanups.add(off)
|
||||
onUnmount(off)
|
||||
} else if (isFunc(v)) {
|
||||
const effect = createEffect(() => {
|
||||
const val = validateAttr(k, v())
|
||||
if (k === "class") el.className = val || ""
|
||||
else if (val == null) el.removeAttribute(k)
|
||||
else if (k === "style" && typeof val === "string") el.setAttribute("style", val)
|
||||
else if (k in el && !isSVG) el[k] = val
|
||||
else el.setAttribute(k, val === true ? "" : val)
|
||||
})
|
||||
effect()
|
||||
el._cleanups.add(() => dispose(effect))
|
||||
onUnmount(() => dispose(effect))
|
||||
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
|
||||
const evType = k === "checked" ? "change" : "input"
|
||||
el.addEventListener(evType, ev => v(ev.target[k]))
|
||||
}
|
||||
} else {
|
||||
const val = validateAttr(k, v)
|
||||
if (val != null) {
|
||||
if (k === "style" && typeof val === "string") el.setAttribute("style", val)
|
||||
else if (k in el && !isSVG) el[k] = val
|
||||
else el.setAttribute(k, val === true ? "" : val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const append = c => {
|
||||
if (isArr(c)) return c.forEach(append)
|
||||
if (isFunc(c)) {
|
||||
const anchor = doc.createTextNode("")
|
||||
el.appendChild(anchor)
|
||||
let currentNodes = []
|
||||
const effect = createEffect(() => {
|
||||
const res = c()
|
||||
const next = (isArr(res) ? res : [res]).map(ensureNode)
|
||||
currentNodes.forEach(n => {
|
||||
if (n._isRuntime) n.destroy()
|
||||
else cleanupNode(n)
|
||||
if (n.parentNode) n.remove()
|
||||
})
|
||||
let ref = anchor
|
||||
for (let i = next.length - 1; i >= 0; i--) {
|
||||
const node = next[i]
|
||||
if (node.parentNode !== ref.parentNode) ref.parentNode?.insertBefore(node, ref)
|
||||
if (node._mounts) node._mounts.forEach(fn => fn())
|
||||
ref = node
|
||||
}
|
||||
currentNodes = next
|
||||
})
|
||||
effect()
|
||||
el._cleanups.add(() => dispose(effect))
|
||||
onUnmount(() => dispose(effect))
|
||||
} else {
|
||||
const node = ensureNode(c)
|
||||
el.appendChild(node)
|
||||
if (node._mounts) node._mounts.forEach(fn => fn())
|
||||
}
|
||||
}
|
||||
append(children)
|
||||
return el
|
||||
}
|
||||
|
||||
const render = renderFn => {
|
||||
const cleanups = new Set()
|
||||
const previousOwner = activeOwner
|
||||
const previousEffect = activeEffect
|
||||
const container = doc.createElement("div")
|
||||
container.style.display = "contents"
|
||||
container.setAttribute("role", "presentation")
|
||||
activeOwner = { _cleanups: cleanups }
|
||||
activeEffect = null
|
||||
|
||||
const processResult = result => {
|
||||
if (!result) return
|
||||
if (result._isRuntime) {
|
||||
cleanups.add(result.destroy)
|
||||
container.appendChild(result.container)
|
||||
} else if (isArr(result)) {
|
||||
result.forEach(processResult)
|
||||
} else {
|
||||
container.appendChild(result instanceof Node ? result : doc.createTextNode(String(result == null ? "" : result)))
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
processResult(renderFn({ onCleanup: fn => cleanups.add(fn) }))
|
||||
} finally {
|
||||
activeOwner = previousOwner
|
||||
activeEffect = previousEffect
|
||||
}
|
||||
|
||||
return {
|
||||
_isRuntime: true,
|
||||
container,
|
||||
destroy: () => {
|
||||
cleanups.forEach(fn => fn())
|
||||
cleanupNode(container)
|
||||
container.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const when = (cond, SIP, NOP = null) => {
|
||||
const anchor = doc.createTextNode("")
|
||||
const root = h("div", { style: "display:contents" }, [anchor])
|
||||
let currentView = null
|
||||
|
||||
watch(
|
||||
() => !!(isFunc(cond) ? cond() : cond),
|
||||
show => {
|
||||
if (currentView) {
|
||||
currentView.destroy()
|
||||
currentView = null
|
||||
}
|
||||
|
||||
const content = show ? SIP : NOP
|
||||
if (content) {
|
||||
currentView = render(() => isFunc(content) ? content() : content)
|
||||
root.insertBefore(currentView.container, anchor)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onUnmount(() => currentView?.destroy())
|
||||
return root
|
||||
}
|
||||
|
||||
const each = (src, itemFn, keyField) => {
|
||||
const anchor = doc.createTextNode("")
|
||||
const root = h("div", { style: "display:contents" }, [anchor])
|
||||
let cache = new Map()
|
||||
watch(() => (isFunc(src) ? src() : src) || [], items => {
|
||||
const nextCache = new Map()
|
||||
const nextOrder = []
|
||||
const newItems = items || []
|
||||
for (let i = 0; i < newItems.length; i++) {
|
||||
const item = newItems[i]
|
||||
const key = keyField ? (item?.[keyField] ?? i) : (item?.id ?? i)
|
||||
let view = cache.get(key)
|
||||
if (!view) view = render(() => itemFn(item, i))
|
||||
else cache.delete(key)
|
||||
nextCache.set(key, view)
|
||||
nextOrder.push(view)
|
||||
}
|
||||
cache.forEach(view => view.destroy())
|
||||
let lastRef = anchor
|
||||
for (let i = nextOrder.length - 1; i >= 0; i--) {
|
||||
const view = nextOrder[i]
|
||||
const node = view.container
|
||||
if (node.nextSibling !== lastRef) root.insertBefore(node, lastRef)
|
||||
lastRef = node
|
||||
}
|
||||
cache = nextCache
|
||||
})
|
||||
return root
|
||||
}
|
||||
|
||||
const router = routes => {
|
||||
const getHash = () => window.location.hash.slice(1) || "/"
|
||||
const path = $(getHash())
|
||||
const handler = () => path(getHash())
|
||||
window.addEventListener("hashchange", handler)
|
||||
onUnmount(() => window.removeEventListener("hashchange", handler))
|
||||
const hook = h("div", { class: "router-hook" })
|
||||
let currentView = null
|
||||
watch([path], () => {
|
||||
const cur = path()
|
||||
const route = routes.find(r => {
|
||||
const p1 = r.path.split("/").filter(Boolean)
|
||||
const p2 = cur.split("/").filter(Boolean)
|
||||
return p1.length === p2.length && p1.every((p, i) => p[0] === ":" || p === p2[i])
|
||||
}) || routes.find(r => r.path === "*")
|
||||
if (route) {
|
||||
currentView?.destroy()
|
||||
const params = {}
|
||||
route.path.split("/").filter(Boolean).forEach((p, i) => {
|
||||
if (p[0] === ":") params[p.slice(1)] = cur.split("/").filter(Boolean)[i]
|
||||
})
|
||||
router.params(params)
|
||||
currentView = render(() => isFunc(route.component) ? route.component(params) : route.component)
|
||||
hook.replaceChildren(currentView.container)
|
||||
}
|
||||
})
|
||||
return hook
|
||||
}
|
||||
router.params = $({})
|
||||
router.to = p => window.location.hash = p.replace(/^#?\/?/, "#/")
|
||||
router.back = () => window.history.back()
|
||||
router.path = () => window.location.hash.replace(/^#/, "") || "/"
|
||||
|
||||
const Fragment = (props) => props.children;
|
||||
|
||||
const mount = (comp, target) => {
|
||||
const t = typeof target === "string" ? doc.querySelector(target) : target
|
||||
if (!t) return
|
||||
if (MOUNTED_NODES.has(t)) MOUNTED_NODES.get(t).destroy()
|
||||
const inst = render(isFunc(comp) ? comp : () => comp)
|
||||
t.replaceChildren(inst.container)
|
||||
MOUNTED_NODES.set(t, 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 }
|
||||
Reference in New Issue
Block a user