Before repair nav components
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 4s

This commit is contained in:
2026-04-25 11:24:39 +02:00
parent e842ed8041
commit 910c6ab3c7
71 changed files with 4260 additions and 2819 deletions

735
components/All.js Normal file
View File

@@ -0,0 +1,735 @@
// All base components
import { h, each, watch, when, fx, mount, $ } from "sigpro";
// Helpers
export const get = 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 isFn = f => typeof f === "function";
export const filterBy = (items, query, field = 'label') => {
const searchTerm = String(query).toLowerCase();
const list = get(items);
return !searchTerm ? list : list.filter(item => {
const text = (item && typeof item === 'object') ? item[field] : item;
return String(text).toLowerCase().includes(searchTerm);
});
};
export const listKey = (items, isOpen) => {
const cursor = $(-1);
watch(() => { if (!get(isOpen)) cursor(-1) });
const onKey = (e, select) => {
const list = get(items), i = cursor(), len = list.length;
if (!len) return;
const k = e.key;
k === 'ArrowDown' ? (e.preventDefault(), isOpen(true), cursor(Math.min(i + 1, len - 1))) :
k === 'ArrowUp' ? (e.preventDefault(), cursor(Math.max(i - 1, 0))) :
k === 'Enter' ? (i >= 0 && (e.preventDefault(), select(list[i]))) :
k === 'Escape' ? isOpen(false) : null;
};
return { cursor, onKey };
};
export const Alert = (p, c) => h("div", { ...p, class: cls("alert", p.class) }, c);
export const Badge = (p, c) => h("span", { ...p, class: cls("badge", p.class) }, c);
export const Button = (p, c) => h("button", { ...p, class: cls("btn", p.class) }, c);
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 Divider = (p) => h("div", { ...p, class: cls("divider", p.class) });
export const Fab = (p, c) => h("div", { ...p, class: cls("fab", p.class) }, c);
export const Fieldset = (p, c) => h("fieldset", { ...p, class: cls("fieldset", p.class) }, [p.legend && h("legend", { class: "fieldset-legend" }, p.legend), c]);
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 Kbd = (p, c) => h("kbd", { ...p, class: cls("kbd", p.class) }, c);
export const Loading = (p, c) => h("span", { ...p, class: cls("loading loading-spinner", p.class) }, c);
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", { ...p, class: cls("radial-progress", p.class), style: `--value:${p.value ?? 0};${p.style ?? ''}`, role: "progressbar", "aria-valuenow": p.value ?? 0 }, c ?? `${p.value ?? 0}%`)
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 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 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) => h("label", { ...p, class: cls("swap", p.class) }, [
h("input", { type: "checkbox", checked: () => get(p.value), onchange: (e) => isFn(p.value) && p.value(e.target.checked) }),
h("div", { class: "swap-on" }, p.on),
h("div", { class: "swap-off" }, p.off)
]);
export const Textarea = (p) => h("textarea", { ...p, class: cls("textarea", p.class) });
export const TextRotate = (p) => {
const words = Array.isArray(p.words) ? p.words : (typeof p.words === 'string' ? p.words.split(',') : []);
return h("span", { ...p, class: cls("text-rotate", p.class) }, h("span", {}, words.map(w => h("span", {}, w))));
};
export const Timeline = (p, c) => h("ul", {
...p,
class: cls("timeline",
p.vertical !== false ? 'timeline-vertical' : 'timeline-horizontal',
p.compact ? 'timeline-compact' : '',
p.class
)
}, c);
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);
// Complex Components
// Accordion
export const Accordion = (p) => {
const name = p.name || `acc-${Math.random().toString(36).slice(2)}`
const base = cls('collapse', p.variant && `collapse-${p.variant}`, p.class)
const itemFn = (it) => {
const t = getBy(it, 'title')
const c = it.content
return p.type === 'details'
? h('details', { class: base, name, open: it.open || undefined },
h('summary', { class: 'collapse-title font-semibold' }, t),
c ? h('div', { class: 'collapse-content text-sm' }, c) : null)
: h('div', { class: base },
h('input', { type: 'radio', name, checked: it.open || undefined }),
h('div', { class: 'collapse-title font-semibold' }, t),
c ? h('div', { class: 'collapse-content text-sm' }, c) : null)
}
return isFn(p.items) ? each(p.items, itemFn, (it, i) => it?.id ?? i) : (p.items || []).map(itemFn)
}
// Table
export const Table = (p) => {
if (p.children !== undefined) return h('table', { class: cls('table', p.class), ...p }, p.children)
const { items, columns = [], header = true, keyFn, ...rest } = p
const hd = header !== false && columns.some(c => c.label) ? h('thead', {}, h('tr', {}, columns.map(c => h('th', { class: c.class }, c.label)))) : null
const bd = h('tbody', {}, each(
() => get(items) || [],
(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)
})),
keyFn || ((it, idx) => it?.id ?? idx)
))
return h('table', { class: cls('table', rest.class), ...rest }, [hd, bd])
}
// Tabs
export const Tabs = (p) => {
if (p.children !== undefined) return h('div', { class: cls('tabs', p.class), ...p }, p.children)
const { items, activeIndex, onClose, ...rest } = p
const closeHandler = onClose || (isFn(items) ? (idx) => {
const arr = get(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', { class: cls('tabs', p.class), ...rest },
each(
() => get(items) || [],
(it, idx) => {
const act = () => activeIndex() === idx
return [
h('a', {
role: 'tab',
class: () => `tab ${act() ? 'tab-active' : ''} ${it.class || ''}`,
onclick: (e) => { e.preventDefault(); activeIndex(idx); it.onclick?.(e) }
}, getBy(it), it.closable ? h('span', {
class: 'icon-[lucide--x] w-3.5 h-3.5 ml-2 cursor-pointer hover:opacity-70',
onclick: (e) => { e.stopPropagation(); closeHandler?.(idx) }
}) : null),
h('div', {
class: `tab-content ${it.contentClass || ''}`,
style: () => `display: ${act() ? 'block' : 'none'};`
}, isFn(it.content) ? it.content() : it.content)
]
},
(it, idx) => it.id ?? idx
)
)
}
// Rating
export const Rating = (p) => {
const name = `rating-${Math.random().toString(36).slice(2, 7)}`
const stars = p.children ?? Array.from({ length: p.count || 5 }, (_, i) => {
const v = i + 1
return h('input', {
type: 'radio',
name,
class: cls('mask', p.mask || 'mask-star'),
checked: () => get(p.value) === v,
onchange: () => isFn(p.value) ? p.value(v) : p.onchange?.(v)
})
})
return h('div', { class: cls('rating', p.class), ...p }, stars)
}
// Menu
export const Menu = (p) => {
if (p.children !== undefined) return h('ul', { class: cls('menu', p.class), ...p }, p.children)
const { items, keyFn = (it, idx) => it?.id ?? idx, ...rest } = p
const render = (item) => item.children
? h('li', {}, h('details', {}, h('summary', {}, getBy(item)), h('ul', {}, Menu({ items: item.children }))))
: 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', rest.class), ...rest },
each(() => get(items) || [], render, keyFn)
)
}
//Drawer
export const Drawer = (p, c) => {
const id = p.id || `drawer-${Math.random().toString(36).slice(2, 9)}`
return h('div', { class: cls('drawer', p.class) }, [
h('input', {
id,
type: 'checkbox',
class: 'drawer-toggle',
checked: () => get(p.open),
onchange: (e) => isFn(p.open) && p.open(e.target.checked)
}),
h('div', { class: 'drawer-content' }, c),
h('div', { class: 'drawer-side' }, [
h('label', {
for: id,
class: 'drawer-overlay',
onclick: () => isFn(p.open) && p.open(false)
}),
h('div', { class: 'min-h-full bg-base-200 w-80 p-4' }, () => get(p.side))
])
])
}
//Dropdown
export const Dropdown = (p, c) => {
const { trigger, items, ...rest } = p
const content = c || (items ? h('ul', {
class: 'menu dropdown-content bg-base-100 rounded-box z-[1] w-52 p-2 shadow'
}, each(
() => get(items) || [],
(item) => h('li', {},
h('a', {
onclick: (e) => {
item.onclick?.(e)
e.currentTarget.closest('details').open = false
}
}, getBy(item))
),
(it, idx) => it.id ?? idx
)) : null)
return h('details', { class: cls('dropdown', rest.class), ...rest }, [
h('summary', { class: 'btn m-1' }, trigger || 'Dropdown'),
content
])
}
//Select
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 = get(items) || []
const ph = placeholder ? [{ disabled: placeholderDisabled, label: placeholder, value: '' }] : []
return [...ph, ...raw]
}
return h('label', { class: float ? 'floating-label' : '' }, [
float && h('span', {}, label),
h('label', { class: cls('select', rest.class) }, [
label && !float && h('span', { class: 'label' }, label),
left ?? null,
h('select', {
value: () => get(value),
onchange: (e) => isFn(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)
}, (it, i) => it?.id ?? (typeof it === 'string' ? it : it.value) ?? i)
),
right ?? null
]),
hint && h('div', { class: 'validator-hint' }, hint)
])
}
// Autocomplete
export const Autocomplete = ({ items, value, onselect, placeholder = 'Buscar...', ...props }) => {
const query = $(get(value) || '');
const isOpen = $(false);
const filtered = $(() => filterBy(items, query()));
const { cursor, onKey } = listKey(filtered, isOpen);
const pick = (item) => {
const display = getBy(item);
const actual = typeof item === 'string' ? item : item.value;
query(display);
if (isFn(value)) value(actual);
onselect?.(item);
isOpen(false);
};
return h('div', { class: 'relative w-full' }, [
Input({
...props,
type: 'text',
placeholder,
value: query,
left: h('span', { class: 'icon-[lucide--search]' }),
oninput: (e) => {
query(e.target.value);
if (isFn(value)) value(e.target.value);
isOpen(true);
},
onfocus: () => isOpen(true),
onblur: () => setTimeout(() => isOpen(false), 150),
onkeydown: (e) => onKey(e, pick)
}),
when(isOpen, () =>
fx({ duration: 200, slide: true },
h('ul', {
class: 'absolute left-0 w-full menu bg-base-100 rounded-box mt-1 p-2 shadow-xl max-h-60 overflow-y-auto border border-base-300 z-50 flex-col flex-nowrap'
}, [
each(filtered, (item, idx) =>
h('li', {}, [
h('a', {
class: () => cursor() === idx ? 'active bg-primary text-primary-content' : '',
onmousedown: (e) => e.preventDefault(), // evita que el blur cierre antes del click
onclick: () => pick(item),
onmouseenter: () => cursor(idx)
}, getBy(item))
]),
(item, idx) => getBy(item) + idx
),
() => filtered().length === 0
? h('li', { class: 'p-4 opacity-50 text-center' }, 'Sin resultados')
: null
])
)
)
]);
};
//Input
export const Input = (p) => {
const { label, icon, float, placeholder, value, left, right, rule, hint, content, ...rest } = p;
const showPassword = $(false);
const isFocused = $(false);
const isPassword = p.type === 'password';
const pattern = rule ?? null;
const inputType = () => isPassword
? (get(showPassword) ? 'text' : 'password')
: (p.type || 'text');
return h("div", {
class: "input-container",
onfocusin: () => isFocused(true),
onfocusout: (e) => {
if (!e.currentTarget.contains(e.relatedTarget)) { isFocused(false); }
}
}, [
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 ? h('label', { class: 'swap swap-rotate ml-2' }, [
h('input', { type: 'checkbox', onchange: (e) => showPassword(e.target.checked) }),
h('span', { class: 'swap-on icon-[lucide--eye]' }),
h('span', { class: 'swap-off icon-[lucide--eye-off]' })
]) : null
]),
hint ? h('div', { class: "validator-hint" }, hint) : null,
when(isFocused, () => fx({ duration: 300, slide: true },
h('div', { class: 'input-content', onmousedown: e => e.preventDefault() },
[
isFn(content) ? content(isFocused) : content
])
))
]),
]);
};
//Colorpicker
export const Colorpicker = (p) => {
const isOpen = $(false)
const current = () => get(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) => { isFn(p.value) ? p.value(c) : p.onchange?.(c); isOpen(false) }
return h('div', { class: cls('relative w-fit', p.class) }, [
h('button', {
type: 'button',
class: 'btn px-3 bg-base-100 border-base-300 hover:border-primary/50 flex items-center gap-2 shadow-sm font-normal normal-case',
onclick: (e) => { e.stopPropagation(); isOpen(!isOpen()) }
}, [
h('div', { class: 'size-5 rounded-sm shadow-inner border border-black/10 shrink-0', style: () => `background-color: ${current()}` }),
p.label && h('span', { class: 'opacity-80' }, p.label)
]),
when(isOpen, () => [
h('div', {
class: 'fixed inset-0 z-[100]',
onclick: () => isOpen(false)
}),
h('div', { class: 'absolute left-0 mt-2 p-3 bg-base-100 border border-base-300 shadow-2xl rounded-box z-[110] w-64 select-none' },
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)
})
)
)
)
])
])
}
// Calendar
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 = () => get(p.range) === true
const current = () => get(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(get(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
])
}
//Datepicker.js
export const Datepicker = (p) => {
const isOpen = $(false)
const displayValue = $("")
const rangeMode = () => get(p.range) === true
watch(() => {
const v = get(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 (isFn(p.value)) p.value(val)
else p.onChange?.(val)
if (!rangeMode() || val?.end != null) isOpen(false)
}
return h('div', { class: cls('relative w-full', p.class) }, [
h('label', { class: 'input input-bordered w-full', onclick: (e) => { e.stopPropagation(); isOpen(!isOpen()) } }, [
h('span', { class: 'icon-[lucide--calendar]' }),
h('input', {
...p,
type: 'text',
class: 'grow',
value: displayValue,
readonly: true,
placeholder: p.placeholder || (rangeMode() ? 'Seleccionar rango...' : 'Seleccionar fecha...')
})
]),
when(isOpen, () => [
h('div', { class: 'fixed inset-0 z-[90]', onclick: () => isOpen(false) }),
h('div', { class: 'absolute left-0 mt-2 z-[100]', onclick: (e) => e.stopPropagation() },
Calendar({ value: p.value, range: rangeMode(), hour: p.hour, onChange: handleChange })
)
])
])
}
//Fileinput
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 (isFn(p.onselect)) p.onselect(updated)
else if (isFn(p.value)) p.value(updated)
}
const remove = (idx) => {
const updated = files().filter((_, i) => i !== idx)
files(updated)
if (isFn(p.onselect)) p.onselect(updated)
else if (isFn(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' }, 'Arrastra o selecciona archivos...'),
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]' }))
]),
(file) => file.name + file.lastModified
)
)
)
])
}
//Toast
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 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' ? get(message) : message;
const msgNode = typeof content === 'string' ? h("span", {}, content) : content;
return h("div", {
class: () => {
if (leaving()) return `alert alert-soft ${type} shadow-lg transition-all duration-300 translate-x-full opacity-0 pointer-events-auto`;
if (visible()) return `alert alert-soft ${type} shadow-lg transition-all duration-300 translate-x-0 opacity-100 pointer-events-auto`;
return `alert alert-soft ${type} shadow-lg transition-all duration-300 translate-x-10 opacity-0 pointer-events-auto`;
}
}, [
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;
};
//Modal
export const Modal = (p) => {
let dialogRef = null;
watch(() => {
const isOpen = get(p.open);
if (!dialogRef) return;
isOpen ? dialogRef.showModal() : dialogRef.close();
});
const close = () => isFn(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")
])
]);
};