Mejorados componentes Datepicker, Autocomplete y eliminado fx
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,6 +1,6 @@
|
||||
// All base components
|
||||
import { h, each, watch, when, mount, $ } from "sigpro";
|
||||
import { get, getBy, cls, isFn, filterBy, rand, close, listKey, fx } from "./sigpro-helpers.js"
|
||||
import { get, getBy, cls, isFn, filterBy, rand, close } from "./sigpro-helpers.js"
|
||||
|
||||
export const Accordion = (p) => {
|
||||
const name = p.name || rand('acc')
|
||||
@@ -13,59 +13,50 @@ export const Accordion = (p) => {
|
||||
});
|
||||
}
|
||||
export const Alert = (p, c) => h("div", { ...p, class: cls("alert", p.class) }, c);
|
||||
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);
|
||||
export const Autocomplete = ({ items, value, onselect, placeholder = '...', ...props }) => {
|
||||
const query = $(get(value) || '')
|
||||
const filtered = $(() => filterBy(items, query()))
|
||||
|
||||
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); cursor(-1); }, 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
|
||||
])
|
||||
)
|
||||
const display = getBy(item)
|
||||
const actual = typeof item === 'string' ? item : item.value
|
||||
query(display)
|
||||
if (isFn(value)) value(actual)
|
||||
onselect?.(item)
|
||||
close()
|
||||
}
|
||||
|
||||
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 (isFn(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 Button = (p, c) => h("button", { ...p, class: cls("btn", p.class) }, c);
|
||||
export const Calendar = (p) => {
|
||||
@@ -102,7 +93,9 @@ export const Calendar = (p) => {
|
||||
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) }, [
|
||||
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]' })),
|
||||
@@ -153,12 +146,7 @@ export const Calendar = (p) => {
|
||||
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,
|
||||
h('button', {
|
||||
type: 'button',
|
||||
class: 'btn btn-xs w-full',
|
||||
onclick: () => close()
|
||||
}, Icon('icon-[lucide--x]'))
|
||||
) : null
|
||||
])
|
||||
}
|
||||
export const Card = (p, c) => h('div', { ...p, class: cls('card', p.class) }, c);
|
||||
@@ -198,29 +186,23 @@ export const ColorPalette = (p) => {
|
||||
'#2e1065', '#4c1d95', '#6d28d9', '#7c3aed', '#8b5cf6', '#a855f7', '#d946ef', '#fae8ff'
|
||||
]
|
||||
const pick = (c) => isFn(p.value) ? p.value(c) : p.onchange?.(c)
|
||||
return [
|
||||
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)
|
||||
})
|
||||
)
|
||||
),
|
||||
h('button', {
|
||||
return h('div', { class: 'grid grid-cols-8 gap-1' },
|
||||
palette.map(c => h('button', {
|
||||
type: 'button',
|
||||
class: 'btn btn-xs w-full',
|
||||
onclick: () => close()
|
||||
}, Icon('icon-[lucide--x]'))]
|
||||
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); close() }
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
export const Datepicker = (p) => {
|
||||
const displayValue = $("")
|
||||
const rangeMode = () => get(p.range) === true
|
||||
|
||||
|
||||
watch(() => {
|
||||
const v = get(p.value)
|
||||
if (!v) return displayValue("")
|
||||
@@ -245,30 +227,33 @@ export const Datepicker = (p) => {
|
||||
}
|
||||
|
||||
return Dropdown({ class: cls('w-full', p.class) }, [
|
||||
h('label', {
|
||||
tabindex: '0', // ← Necesario para que funcione el dropdown
|
||||
role: 'button', // ← Necesario para que funcione el dropdown
|
||||
class: 'input input-bordered w-full flex items-center gap-2 cursor-pointer'
|
||||
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', {
|
||||
h('span', {
|
||||
class: () => `grow text-left truncate ${!displayValue() ? 'opacity-50' : ''}`,
|
||||
}, () => displayValue() || p.placeholder || (rangeMode() ? 'Seleccionar rango...' : 'Seleccionar fecha...'))
|
||||
}, () => 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 (isFn(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 bg-base-100 rounded-box shadow-xl' },
|
||||
Calendar({
|
||||
value: p.value,
|
||||
range: rangeMode(),
|
||||
hour: p.hour,
|
||||
onChange: handleChange,
|
||||
onAccept: () => {
|
||||
p.onAccept?.()
|
||||
close()
|
||||
},
|
||||
onCancel: () => {
|
||||
p.onCancel?.()
|
||||
close()
|
||||
}
|
||||
Calendar({
|
||||
value: p.value,
|
||||
range: rangeMode(),
|
||||
hour: p.hour,
|
||||
onChange: handleChange
|
||||
})
|
||||
)
|
||||
])
|
||||
@@ -355,42 +340,28 @@ 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 ? 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,
|
||||
: (p.type || 'search');
|
||||
|
||||
when(isFocused, () => fx({ duration: 300, slide: true },
|
||||
h('div', { class: 'input-content', onmousedown: e => e.preventDefault() },
|
||||
[
|
||||
isFn(content) ? content(isFocused) : content
|
||||
])
|
||||
))
|
||||
]),
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user