All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
837 lines
39 KiB
JavaScript
837 lines
39 KiB
JavaScript
// All base components
|
|
import { h, each, watch, when, mount, $, isFunc } from "sigpro";
|
|
|
|
// 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', q = String(query).toLowerCase()) => !query ? val(items) : val(items).filter(item => String(item && typeof item === 'object' ? item[field] : item).toLowerCase().includes(q));
|
|
const rand = (r) => `${r}-${Math.random().toString(36).slice(2, 9)}`
|
|
export const hide = () => document.activeElement?.blur()
|
|
|
|
// Locales
|
|
export const i18n = {
|
|
es: {
|
|
close: "Cerrar",
|
|
confirm: "Confirmar",
|
|
cancel: "Cancelar",
|
|
search: "Buscar...",
|
|
loading: "Cargando...",
|
|
nodata: "Sin datos"
|
|
},
|
|
en: {
|
|
close: "Close",
|
|
confirm: "Confirm",
|
|
cancel: "Cancel",
|
|
search: "Search...",
|
|
loading: "Loading...",
|
|
nodata: "No data"
|
|
}
|
|
};
|
|
|
|
export const currentLocale = $("en");
|
|
export const Locale = t => currentLocale(t);
|
|
export const tt = t => () => i18n[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-full' }, [
|
|
h('div', { tabindex: '0', role: 'button', class: 'w-full' },
|
|
Input({
|
|
...props,
|
|
placeholder,
|
|
value: query,
|
|
left: h('span', { class: 'icon-[lucide--search]' }),
|
|
oninput: (e) => {
|
|
query(e.target.value)
|
|
if (isFunc(value)) value(e.target.value)
|
|
}
|
|
})
|
|
),
|
|
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))
|
|
]),
|
|
(item) => getBy(item)
|
|
),
|
|
() => 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' }, 'Drag and drop...'),
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Editor
|
|
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()}`)
|
|
])
|
|
])
|
|
} |