Restructured
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 4s

This commit is contained in:
2026-04-27 16:48:06 +02:00
parent 7590438780
commit 19279524d7
97 changed files with 288 additions and 4172 deletions

View File

@@ -1,22 +0,0 @@
// components/Accordion.js
import { h } from "sigpro";
export const Accordion = (props, children) => {
const name = props.name || `accordion-${Math.random().toString(36).slice(2, 9)}`;
if (props.items && Array.isArray(props.items)) {
return h("div", { class: `space-y-2 ${props.class ?? ''}` },
props.items.map(item => h("div", { class: `collapse ${item.class ?? ''}` }, [
h("input", { type: "radio", name, checked: item.open }),
h("div", { class: "collapse-title text-xl font-medium" }, item.title),
h("div", { class: "collapse-content" }, children)
]))
);
}
return h("div", { class: `collapse ${props.class ?? ''}` }, [
h("input", { type: "radio", name, checked: props.open }),
h("div", { class: "collapse-title text-xl font-medium" }, props.title),
h("div", { class: "collapse-content" }, children)
]);
};

View File

@@ -1,7 +0,0 @@
// components/Alert.js
import { h } from "sigpro";
export const Alert = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `alert ${props.class ?? ''}` }, children);
};

View File

@@ -1,60 +0,0 @@
import { $, h, when, fx } from 'sigpro';
import { Input } from '../Input.js';
import { filterBy, listKey, getBy } from '../All.js';
export const Autocomplete = ({ items, value, onselect, placeholder = 'Buscar...', ...props }) => {
const query = $(value ? (typeof value === 'function' ? value() : 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 (typeof value === 'function') 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 (typeof value === 'function') 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))
])
),
() => filtered().length === 0
? h('li', { class: 'p-4 opacity-50 text-center' }, 'Sin resultados')
: null
])
)
)
]);
};

View File

@@ -1,7 +0,0 @@
// components/Badge.js
import { h } from "sigpro";
const colors = [""]
export const Badge = (props, children) => {
children === undefined && (children = props, props = {});
return h("span", { ...props, class: `badge ${props.class ?? ''}` }, children);
};

View File

@@ -1,172 +0,0 @@
// components/Calendar.js
import { $, h } from "sigpro";
export const Calendar = (props) => {
const internalDate = $(new Date());
const hoverDate = $(null);
const startHour = $(0);
const endHour = $(0);
const isRangeMode = () => {
const r = typeof props.range === "function" ? props.range() : props.range;
return r === true;
};
const now = new Date();
const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
const formatDate = (d) => {
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
const getCurrentValue = () => {
return typeof props.value === "function" ? props.value() : props.value;
};
const selectDate = (date) => {
const dateStr = formatDate(date);
const current = getCurrentValue();
if (isRangeMode()) {
if (!current?.start || (current.start && current.end)) {
const newValue = {
start: dateStr,
end: null,
...(props.hour && { startHour: startHour() }),
};
props.onChange?.(newValue);
} else {
const start = current.start;
let newValue;
if (dateStr < start) {
newValue = { start: dateStr, end: start };
} else {
newValue = { start, end: dateStr };
}
if (props.hour) {
newValue.startHour = current.startHour !== undefined ? current.startHour : startHour();
newValue.endHour = endHour();
}
props.onChange?.(newValue);
}
} else {
const newValue = props.hour ? `${dateStr}T${String(startHour()).padStart(2, "0")}:00:00` : dateStr;
props.onChange?.(newValue);
}
};
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: onHourChange }) => {
return 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) => onHourChange(parseInt(e.target.value))
}),
h("span", { class: "text-sm font-mono min-w-[48px] text-center" },
() => String(typeof hVal === "function" ? hVal() : hVal).padStart(2, "0") + ":00"
)
])
]);
};
return h("div", { class: `p-4 bg-base-100 border border-base-300 shadow-2xl rounded-box w-80 select-none ${props.class ?? ''}`.trim() }, [
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();
const year = d.getFullYear();
const month = d.getMonth();
const firstDay = new Date(year, month, 1).getDay();
const offset = firstDay === 0 ? 6 : firstDay - 1;
const daysInMonth = new Date(year, month + 1, 0).getDate();
const cells = [];
for (let i = 0; i < offset; i++) cells.push(h("div"));
for (let i = 1; i <= daysInMonth; i++) {
const date = new Date(year, month, i);
const dStr = formatDate(date);
cells.push(
h("button", {
type: "button",
class: () => {
const v = getCurrentValue();
const h = hoverDate();
const isStart = typeof v === "string" ? v.split("T")[0] === dStr : v?.start === dStr;
const isEnd = v?.end === dStr;
let inRange = false;
if (isRangeMode() && v?.start) {
const start = v.start;
if (!v.end && h) {
inRange = (dStr > start && dStr <= h) || (dStr < start && dStr >= h);
} else if (v.end) {
inRange = dStr > start && dStr < v.end;
}
}
const base = "btn btn-xs p-0 aspect-square min-h-0 h-auto font-normal relative";
const state = isStart || isEnd ? "btn-primary z-10" : inRange ? "bg-primary/20 border-none rounded-none" : "btn-ghost";
const today = dStr === todayStr ? "ring-1 ring-primary ring-inset font-black text-primary" : "";
return `${base} ${state} ${today}`.trim();
},
onmouseenter: () => { if (isRangeMode()) hoverDate(dStr); },
onclick: () => selectDate(date)
}, i.toString())
);
}
return cells;
}
]),
props.hour ? h("div", { class: "mt-3 pt-2 border-t border-base-300" }, [
isRangeMode()
? 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
]);
};

View File

@@ -1,83 +0,0 @@
// components/Datepicker.js
import { $, h, when, watch } from "sigpro";
import { Calendar } from "./Calendar.js";
export const Datepicker = (props) => {
const isOpen = $(false);
const isRangeMode = () => {
const r = typeof props.range === "function" ? props.range() : props.range;
return r === true;
};
const displayValue = $("");
watch(() => {
const v = typeof props.value === "function" ? props.value() : props.value;
if (!v) {
displayValue("");
return;
}
let text = "";
if (typeof v === "string") {
text = (props.hour && v.includes("T")) ? v.replace("T", " ") : v;
} else if (v.start && v.end) {
const startStr = props.hour && v.startHour !== undefined
? `${v.start} ${String(v.startHour).padStart(2, "0")}:00`
: v.start;
const endStr = props.hour && v.endHour !== undefined
? `${v.end} ${String(v.endHour).padStart(2, "0")}:00`
: v.end;
text = `${startStr} - ${endStr}`;
} else if (v.start) {
const startStr = props.hour && v.startHour !== undefined
? `${v.start} ${String(v.startHour).padStart(2, "0")}:00`
: v.start;
text = `${startStr}...`;
}
displayValue(text);
});
const handleCalendarChange = (newValue) => {
if (typeof props.value === "function") props.value(newValue);
if (!isRangeMode() || (newValue?.end !== undefined && newValue?.end !== null)) {
isOpen(false);
}
};
const toggleOpen = (e) => {
e.stopPropagation();
isOpen(!isOpen());
};
return h("div", { class: `relative w-full ${props.class ?? ''}` }, [
h("label", { class: "input input-bordered w-full", onclick: toggleOpen }, [
h("span", { class: "icon-[lucide--calendar]" }),
h("input", {
...props,
type: "text",
class: "grow",
value: displayValue,
readonly: true,
placeholder: props.placeholder || (isRangeMode() ? "Seleccionar rango..." : "Seleccionar fecha...")
})
]),
when(isOpen, () =>
h("div", {
class: "absolute left-0 mt-2 z-[100]",
onclick: (e) => e.stopPropagation()
}, [
Calendar({
value: props.value,
range: isRangeMode(),
hour: props.hour,
onChange: handleCalendarChange
})
])
),
when(isOpen, () =>
h("div", { class: "fixed inset-0 z-[90]", onclick: () => isOpen(false) })
)
]);
};

View File

@@ -1,46 +0,0 @@
import { h, $, when, fx } from 'sigpro';
import { get, cls, isFn } from '../All.js';
export const Input = (props) => {
const { label, icon, float, placeholder, value, left, right, rule, hint, content, ...rest } = props;
const showPassword = $(false);
const isFocused = $(false);
const isPassword = props.type === 'password';
const pattern = rule ?? null;
const inputType = () => isPassword
? (get(showPassword) ? 'text' : 'password')
: (props.type || 'text');
return h("div", {
class: "input-container",
onfocusin: () => isFocused(true),
onfocusout: (e) => {
if (!e.currentTarget.contains(e.relatedTarget)) { isFocused(false); }
}
}, [
h('label', { class: "floating-label" }, [
float ? h("span", {}, label) : null,
h("label", { pattern: pattern, class: () => cls('input validator', props.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
])
))
]),
]);
};

View File

@@ -1,29 +0,0 @@
// components/Select.js
import { h, each } from "sigpro";
export const Select = (props) => {
const { items, placeholder, placeholderDisabled = true, keyFn, children, ...rest } = props;
if (children !== undefined) {
return h("select", { ...rest, class: `select ${rest.class ?? ''}` }, children);
}
const placeholderOption = placeholder
? h("option", { disabled: placeholderDisabled, selected: true }, placeholder)
: null;
const dynamicOptions = each(
() => [...(typeof items === "function" ? items() : items || [])],
(item) => {
const value = typeof item === "string" ? item : item.value;
const label = typeof item === "string" ? item : item.label;
return h("option", { value }, label);
}
);
const options = placeholderOption
? [placeholderOption, dynamicOptions]
: dynamicOptions;
return h("select", { ...rest, class: `select ${rest.class ?? ''}` }, options);
};

View File

@@ -1,55 +0,0 @@
// components/Toast.js
import { h, mount } from "sigpro";
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 toastHost = h("div", { style: "display: contents" });
container.appendChild(toastHost);
let timeoutId;
const close = () => {
clearTimeout(timeoutId);
const el = toastHost.firstElementChild;
if (el && !el.classList.contains("opacity-0")) {
el.classList.add("translate-x-full", "opacity-0");
setTimeout(() => {
instance.destroy();
toastHost.remove();
if (!container.hasChildNodes()) container.remove();
}, 300);
} else {
instance.destroy();
toastHost.remove();
}
};
const ToastComponent = () => {
const closeIcon = h("span", { class: "icon-[lucide--x]" });
const closeBtn = h("button", {
class: "btn btn-xs btn-circle btn-ghost",
onclick: close
}, closeIcon);
const alertDiv = h("div", {
class: `alert alert-soft ${type} shadow-lg transition-all duration-300 translate-x-10 opacity-0 pointer-events-auto`
}, [
h("span", {}, typeof message === "function" ? message() : message),
closeBtn
]);
requestAnimationFrame(() => alertDiv.classList.remove("translate-x-10", "opacity-0"));
return alertDiv;
};
const instance = mount(ToastComponent, toastHost);
if (duration > 0) timeoutId = setTimeout(close, duration);
return close;
};

View File

@@ -1,62 +0,0 @@
// _core.js
import { $, watch } from 'sigpro';
export const ui = (base, extras = '') => {
if (!extras) return base;
const extraClasses = extras
.split(' ')
.map(part => part.trim())
.filter(Boolean)
.map(part => part.startsWith(base + '-') ? part : `${base}-${part}`)
.join(' ');
return `${base} ${extraClasses}`.trim();
};
export const get = val => typeof val === "function" ? val() : val;
export const filterBy = (items, query, field = 'label') => {
const q = String(query).toLowerCase();
if (!q) return get(items);
return get(items).filter(item => {
const text = typeof item === 'string' ? item : item[field];
return String(text).toLowerCase().includes(q);
});
};
export const listKey = (items, isOpen) => {
const cursor = $(-1);
watch([items, isOpen], () => {
if (!get(isOpen)) cursor(-1);
});
const onKey = (e, select) => {
const list = get(items);
if (!list.length) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
isOpen(true);
cursor(Math.min(cursor() + 1, list.length - 1));
} else if (e.key === 'ArrowUp') {
e.preventDefault();
cursor(Math.max(cursor() - 1, 0));
} else if (e.key === 'Enter' && cursor() >= 0) {
e.preventDefault();
select(list[cursor()]);
} else if (e.key === 'Escape') {
isOpen(false);
}
};
return { cursor, onKey };
};
export const cls = (...classes) => classes.filter(Boolean).join(' ').trim();
export const getBy = (item, field = 'label') => {
return typeof item === 'string' ? item : item[field];
};
export const isFunc = f => typeof f === "function";

View File

@@ -1,6 +0,0 @@
import { h } from "sigpro";
export const Button = (props, children) => {
children === undefined && (children = props, props = {});
return h("button", { ...props, class: `btn ${props.class ?? ''}` }, children);
};

View File

@@ -1,22 +0,0 @@
// components/Card.js
import { h } from "sigpro";
export const Card = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `card ${props.class ?? ''}` }, children);
};
export const CardTitle = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `card-title ${props.class ?? ''}` }, children);
};
export const CardBody = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `card-body ${props.class ?? ''}` }, children);
};
export const CardActions = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `card-actions ${props.class ?? ''}` }, children);
};

View File

@@ -1,12 +0,0 @@
// components/Carousel.js
import { h } from "sigpro";
export const Carousel = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `carousel ${props.class ?? ''}` }, children);
};
export const CarouselItem = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `carousel-item ${props.class ?? ''}` }, children);
};

View File

@@ -1,42 +0,0 @@
// components/Chat.js
import { h } from "sigpro";
export const Chat = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `chat ${props.class ?? ''}` }, children);
};
export const ChatImage = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `chat-image avatar ${props.class ?? ''}` },
h("div", { class: "w-10 rounded-full" },
typeof children === "string" ? h("img", { src: children, alt: "avatar" }) : children
)
);
};
export const ChatHeader = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `chat-header ${props.class ?? ''}` }, children);
};
export const ChatFooter = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `chat-footer ${props.class ?? ''}` }, children);
};
export const ChatBubble = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `chat-bubble ${props.class ?? ''}` }, children);
};
export const ChatMessage = (props) => {
const { position = "start", avatar, header, message, footer, bubbleClass, ...rest } = props;
return Chat({ ...rest, class: `chat-${position} ${props.class ?? ''}` }, [
avatar && ChatImage(avatar),
header && ChatHeader(header),
ChatBubble({ class: bubbleClass }, message),
footer && ChatFooter(footer)
]);
};

View File

@@ -1,4 +0,0 @@
// components/Checkbox.js
import { h } from "sigpro";
export const Checkbox = (props) => h("input", { ...props, type: "checkbox", class: `checkbox ${props.class ?? ''}` });

View File

@@ -1,68 +0,0 @@
// components/Colorpicker.js
import { $, h, when} from "sigpro";
export const Colorpicker = (props) => {
const isOpen = $(false);
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 getColor = () => {
const v = props.value;
return (typeof v === "function" ? v() : v) || "#000000";
};
return h("div", { class: `relative w-fit ${props.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()); },
...props
}, [
h("div", {
class: "size-5 rounded-sm shadow-inner border border-black/10 shrink-0",
style: () => `background-color: ${getColor()}`
}),
props.label ? h("span", { class: "opacity-80" }, props.label) : null
]),
when(isOpen, () =>
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",
onclick: (e) => e.stopPropagation()
}, [
h("div", { class: "grid grid-cols-8 gap-1" },
palette.map(c =>
h("button", {
type: "button",
style: `background-color: ${c}`,
class: () => {
const active = getColor().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 ${active ? "ring-2 ring-offset-1 ring-primary z-10 scale-110" : ""}`;
},
onclick: () => {
if (typeof props.value === "function") props.value(c);
isOpen(false);
}
})
)
)
])
),
when(isOpen, () =>
h("div", {
class: "fixed inset-0 z-[100]",
onclick: () => isOpen(false)
})
)
]);
};

View File

@@ -1,4 +0,0 @@
// components/Collapse.js
import { h } from "sigpro";
export const Divider = (props) => h("div", { ...props, class: `divider ${props.class ?? ''}` });

View File

@@ -1,31 +0,0 @@
// components/Drawer.js
import { h } from "sigpro";
export const Drawer = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `drawer ${props.class ?? ''}` }, children);
};
export const Sidebar = (props) => {
const id = props.id || `drawer-${Math.random().toString(36).slice(2, 9)}`;
return h("div", { ...props, class: `drawer ${props.class ?? ''}` }, [
h("input", {
id,
type: "checkbox",
class: "drawer-toggle",
checked: () => (typeof props.open === "function" ? props.open() : props.open),
onchange: (e) => typeof props.open === "function" && props.open(e.target.checked)
}),
h("div", { class: "drawer-content" }, props.children),
h("div", { class: "drawer-side" }, [
h("label", {
for: id,
class: "drawer-overlay",
onclick: () => typeof props.open === "function" && props.open(false)
}),
h("div", { class: "min-h-full bg-base-200 w-80 p-4" },
typeof props.content === "function" ? props.content() : props.content
)
])
]);
};

View File

@@ -1,24 +0,0 @@
// components/Dropdown.js
import { h } from "sigpro";
let currentOpen = null;
if (typeof window !== 'undefined' && !window.__dropdownHandlerRegistered) {
window.addEventListener('click', (e) => {
if (currentOpen && !currentOpen.contains(e.target)) {
currentOpen.open = false;
currentOpen = null;
}
});
window.__dropdownHandlerRegistered = true;
}
export const Dropdown = (props) => h("details", {
...props,
class: `dropdown ${props.class ?? ''}`,
onclick: (e) => {
const details = e.currentTarget;
if (currentOpen && currentOpen !== details) currentOpen.open = false;
setTimeout(() => { currentOpen = details.open ? details : null; }, 0);
}
}, props.children);

View File

@@ -1,7 +0,0 @@
// components/Fab.js
import { h } from "sigpro";
export const Fab = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `fab ${props.class ?? ''}` }, children);
};

View File

@@ -1,10 +0,0 @@
// components/Fieldset.js
import { h } from "sigpro";
export const Fieldset = (props, children) => h("fieldset", {
...props,
class: `fieldset ${props.class ?? ''}`
}, [
props.legend ? h("legend", { class: "fieldset-legend" }, props.legend) : null,
children
]);

View File

@@ -1,68 +0,0 @@
// components/Fileinput.js
import { $, h, when, each } from "sigpro";
export const Fileinput = (props) => {
const selectedFiles = $([]);
const isDragging = $(false);
const error = $(null);
const maxBytes = (props.max || 2) * 1024 * 1024;
const handleFiles = (files) => {
const fileList = Array.from(files);
error(null);
if (fileList.find(f => f.size > maxBytes)) {
error(`Máx ${props.max || 2}MB`);
return;
}
selectedFiles([...selectedFiles(), ...fileList]);
props.onselect?.(selectedFiles());
};
const removeFile = (idx) => {
const updated = selectedFiles().filter((_, i) => i !== idx);
selectedFiles(updated);
props.onselect?.(updated);
};
return h("div", { ...props, class: `fieldset w-full p-0 ${props.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 ${isDragging() ? "border-primary bg-primary/10" : "border-base-content/20 bg-base-100 hover:bg-base-200"}`,
ondragover: (e) => { e.preventDefault(); isDragging(true); },
ondragleave: () => isDragging(false),
ondrop: (e) => { e.preventDefault(); isDragging(false); handleFiles(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 ${props.max || 2}MB`)
]),
h("input", {
type: "file",
multiple: true,
accept: props.accept || "*",
class: "hidden",
onchange: (e) => handleFiles(e.target.files)
})
]),
() => error() && h("span", { class: "text-[10px] text-error mt-1 px-1 font-medium" }, error()),
when(() => selectedFiles().length > 0, () =>
h("ul", { class: "mt-2 space-y-1" }, [
each(selectedFiles, (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(); removeFile(idx); }
}, h("span", { class: "icon-[lucide--x]" }))
])
)
])
)
]);
};

View File

@@ -1,14 +0,0 @@
// components/Icon.js
import { h } from "sigpro";
export const Icon = (props, children) => {
if (typeof props === "string") {
if (props.includes("icon-") || props.startsWith("lucide-")) {
return h("span", { class: props }, children);
}
return h("span", { class: "icon" }, props);
}
if (!props) return null;
const { class: className, ...rest } = props;
return h("span", { ...rest, class: className }, children);
};

View File

@@ -1,10 +0,0 @@
// components/Indicator.js
import { h } from "sigpro";
export const Indicator = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `indicator ${props.class ?? ''}` }, [
props.value ? h("span", { class: `indicator-item badge ${props.class ?? ''}` }, props.value) : null,
children
]);
};

View File

@@ -1,7 +0,0 @@
// components/Kbd.js
import { h } from "sigpro";
export const Kbd = (props, children) => {
children === undefined && (children = props, props = {});
return h("kbd", { ...props, class: `kbd ${props.class ?? ''}` }, children);
};

View File

@@ -1,7 +0,0 @@
// components/Loading.js
import { h } from "sigpro";
export const Loading = (props, children) => {
children === undefined && (children = props, props = {});
return h("span", { ...props, class: `loading loading-spinner ${props.class ?? ''}` }, children);
};

View File

@@ -1,32 +0,0 @@
// components/Menu.js
import { h, each } from "sigpro";
export const Menu = (props, children) => {
children === undefined && (children = props, props = {});
return h("ul", { ...props, class: `menu ${props.class ?? ''}` }, children);
};
export const MenuItems = (props) => {
const { items } = props;
const itemsSignal = typeof items === "function" ? items : () => items || [];
const renderItem = (item) => {
if (item.children) {
return h("li", {}, [
h("details", {}, [
h("summary", {}, item.label),
h("ul", {}, MenuItems({ items: item.children }))
])
]);
}
return h("li", {}, h("a", {
href: item.href,
onclick: item.onclick ? (e) => {
if (!item.href) e.preventDefault();
item.onclick(e);
} : null
}, item.label));
};
return each(itemsSignal, renderItem);
};

View File

@@ -1,33 +0,0 @@
// components/Modal.js
import { h, watch } from "sigpro";
export const Modal = (props) => {
let dialogRef = null;
watch(() => {
const isOpen = typeof props.open === "function" ? props.open() : props.open;
if (!dialogRef) return;
isOpen ? dialogRef.showModal() : dialogRef.close();
});
const close = () => typeof props.open === "function" && props.open(false);
return h("dialog", {
...props,
ref: el => dialogRef = el,
class: `modal ${props.class ?? ''}`,
onclose: close,
oncancel: close
}, [
h("div", { class: "modal-box" }, [
props.title && h("h3", { class: "text-lg font-bold" }, props.title),
props.children,
h("div", { class: "modal-action" }, [
props.actions || h("button", { class: "btn", onclick: close }, "Cerrar")
])
]),
h("form", { method: "dialog", class: "modal-backdrop" }, [
h("button", {}, "close")
])
]);
};

View File

@@ -1,7 +0,0 @@
// components/Navbar.js
import { h } from "sigpro";
export const Navbar = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `navbar ${props.class ?? ''}` }, children);
};

View File

@@ -1,18 +0,0 @@
// components/Radial.js
import { h } from "sigpro";
export const Radial = (props, children) => {
children === undefined && (children = props, props = {});
const percentage = props.value != null ? (props.value / (props.max || 100)) * 100 : 0;
const style = `--value: ${percentage}; --max: 100;`;
return h("div", {
...props,
class: `radial-progress ${props.class ?? ''}`,
style: style,
role: "progressbar",
"aria-valuenow": props.value,
"aria-valuemin": 0,
"aria-valuemax": props.max || 100
}, children || `${Math.round(percentage)}%`);
};

View File

@@ -1,4 +0,0 @@
// components/Radio.js
import { h } from "sigpro";
export const Radio = (props) => h("input", { ...props, type: "radio", class: `radio ${props.class ?? ''}` });

View File

@@ -1,4 +0,0 @@
// components/Range.js
import { h } from "sigpro";
export const Range = (props) => h("input", { ...props, type: "range", class: `range ${props.class ?? ''}` });

View File

@@ -1,21 +0,0 @@
// components/Rating.js
import { h } from "sigpro";
export const Rating = (props, children) => {
children === undefined && (children = props, props = {});
const name = `rating-${Math.random().toString(36).slice(2, 7)}`;
return h("div", { ...props, class: `rating ${props.class ?? ''}` }, children || Array.from({ length: props.count || 5 }, (_, i) => {
const starValue = i + 1;
return h("input", {
type: "radio",
name,
class: `mask ${props.mask || "mask-star"}`,
checked: () => typeof props.value === "function" ? props.value() === starValue : props.value === starValue,
onchange: () => {
if (props.onchange) props.onchange(starValue);
else if (typeof props.value === "function") props.value(starValue);
}
});
}));
};

View File

@@ -1,12 +0,0 @@
// components/Skeleton.js
import { h } from "sigpro";
export const Skeleton = (props) => h("div", { ...props, class: `skeleton ${props.class ?? ''}` });
export const SkeletonText = (props) => {
return h("div", { ...props, class: "space-y-2" },
Array.from({ length: props.lines || 3 }, () =>
h("div", { class: `skeleton h-4 w-full ${props.class ?? ''}` })
)
);
};

View File

@@ -1,7 +0,0 @@
// components/Stack.js
import { h } from "sigpro";
export const Stack = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `stack ${props.class ?? ''}` }, children);
};

View File

@@ -1,20 +0,0 @@
// components/Stats.js
import { h } from "sigpro";
export const Stats = (props, children) => {
children === undefined && (children = props, props = {});
const direction = props.vertical ? "stats-vertical" : "stats-horizontal";
return h("div", { ...props, class: `stats ${direction} ${props.class ?? ''}`.trim() }, children);
};
export const Stat = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `stat ${props.class ?? ''}` }, [
props.icon && h("div", { class: "stat-figure" }, props.icon),
props.label && h("div", { class: "stat-title" }, props.label),
props.value && h("div", { class: "stat-value" }, props.value),
props.desc && h("div", { class: "stat-desc" }, props.desc),
props.actions && h("div", { class: "stat-actions" }, props.actions),
children
]);
};

View File

@@ -1,12 +0,0 @@
// components/Steps.js
import { h } from "sigpro";
export const Steps = (props, children) => {
children === undefined && (children = props, props = {});
return h("ul", { ...props, class: `steps ${props.class ?? ''}` }, children);
};
export const Step = (props, children) => {
children === undefined && (children = props, props = {});
return h("li", { ...props, class: `step ${props.class ?? ''}`, "data-content": props.dataContent }, children);
};

View File

@@ -1,14 +0,0 @@
// components/Swap.js
import { h } from "sigpro";
export const Swap = (props) => {
return h("label", { ...props, class: `swap ${props.class ?? ''}` }, [
h("input", {
type: "checkbox",
checked: () => typeof props.value === "function" ? props.value() : props.value,
onchange: (e) => typeof props.value === "function" && props.value(e.target.checked)
}),
h("div", { class: "swap-on" }, props.on),
h("div", { class: "swap-off" }, props.off)
]);
};

View File

@@ -1,30 +0,0 @@
// components/Table.js
import { h, each } from "sigpro";
export const Table = (props, children) => {
children === undefined && (children = props, props = {});
return h("table", { ...props, class: `table ${props.class ?? ''}` }, children);
};
export const TableItems = (props) => {
const itemArray = typeof props.items === "function" ? props.items() : (props.items || []);
const thead = props.header !== false && props.columns?.some(col => col.label) ?
h("thead", {},
h("tr", {},
props.columns.map(col => h("th", { class: col.class }, col.label))
)
) : null;
const tbody = h("tbody", {}, [
each(itemArray, (item, idx) =>
h("tr", {},
props.columns.map(col => {
const content = col.render ? col.render(item, idx) : item[col.key];
return h("td", { class: col.class }, content);
})
))
]);
return [thead, tbody];
};

View File

@@ -1,48 +0,0 @@
// components/Tabs.js
import { h, each } from "sigpro";
export const Tabs = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `tabs ${props.class ?? ''}` }, children);
};
export const Tab = (props, children) => {
children === undefined && (children = props, props = {});
return h("a", { ...props, role: "tab", class: `tab ${props.class ?? ''}` }, children);
};
export const TabContent = (props, children) => {
children === undefined && (children = props, props = {});
return h("div", { ...props, class: `tab-content ${props.class ?? ''}` }, children);
};
export const TabClose = (props) => h("a", { ...props, role: "tab", class: `tab ${props.class ?? ''}` }, [
h("span", { class: "flex items-center" }, [
props.label,
h("span", {
class: "icon-[lucide--x] w-3.5 h-3.5 ml-2 cursor-pointer hover:opacity-70",
onclick: (e) => { e.stopPropagation(); props.onClose?.(e); }
})
])
]);
export const TabItems = (props) => {
const items = typeof props.items === "function" ? props.items : () => props.items || [];
return each(
items,
(item, idx) => {
const TabComp = item.closable ? TabClose : Tab;
return [
TabComp({
...item,
class: () => props.activeIndex() === idx ? `tab-active ${item.class ?? ''}` : item.class,
onclick: (e) => { e.preventDefault(); props.activeIndex(idx); item.onclick?.(e); },
onClose: () => props.onClose?.(idx, item)
}),
TabContent({
style: () => `display: ${props.activeIndex() === idx ? "block" : "none"};`
}, typeof item.content === "function" ? item.content() : item.content)
];
}
);
};

View File

@@ -1,4 +0,0 @@
// components/Textarea.js
import { h } from "sigpro";
export const Textarea = (props) => h("textarea", { ...props, class: `textarea ${props.class ?? ''}` });

View File

@@ -1,12 +0,0 @@
// components/Textrotate.js
import { h } from "sigpro";
export const TextRotate = (props) => {
const wordsArray = Array.isArray(props.words)
? props.words
: (typeof props.words === 'string' ? props.words.split(',') : []);
return h("span", { ...props, class: `text-rotate ${props.class ?? ''}` }, [
h("span", {}, wordsArray.map(word => h("span", {}, word)))
]);
};

View File

@@ -1,12 +0,0 @@
// components/Timeline.js
import { h } from "sigpro";
export const Timeline = (props, children) => {
children === undefined && (children = props, props = {});
const vertical = props.vertical !== false;
const compact = props.compact === true;
return h("ul", {
...props,
class: `timeline ${vertical ? 'timeline-vertical' : 'timeline-horizontal'} ${compact ? 'timeline-compact' : ''} ${props.class ?? ''}`.trim()
}, children);
};

View File

@@ -1,4 +0,0 @@
// components/Toggle.js
import { h } from "sigpro";
export const Toggle = (p) => h("input", { ...p, type: "checkbox", class: `toggle ${p.class ?? ''}` });

View File

@@ -1,4 +0,0 @@
// components/Tooltip.js
import { h } from "sigpro";
export const Tooltip = (p, c) => h("div", { ...p, class: `tooltip ${p.class ?? ''}`, "data-tip": p.tip }, c);

914
dist/sigpro-ui.css vendored

File diff suppressed because it is too large Load Diff

99
dist/sigpro-ui.esm.js vendored
View File

@@ -13,17 +13,9 @@ var __export = (target, all) => {
});
};
// components/All.js
var exports_All = {};
__export(exports_All, {
rand: () => rand,
listKey: () => listKey,
isFn: () => isFn,
getBy: () => getBy,
get: () => get,
filterBy: () => filterBy,
cls: () => cls,
close: () => close,
// sigpro-components.js
var exports_sigpro_components = {};
__export(exports_sigpro_components, {
Tooltip: () => Tooltip,
Toggle: () => Toggle,
Toast: () => Toast,
@@ -106,7 +98,7 @@ var x = false;
var A = 0;
var C = new Set;
var D = new WeakMap;
var $ = Symbol("iter");
var $2 = Symbol("iter");
var B = new WeakMap;
var E = (e) => {
if (!e || e._disposed)
@@ -253,16 +245,16 @@ var I = (e) => {
let u = Reflect.has(c, t), a = Reflect.get(c, t, l), f = Reflect.set(c, t, r, l);
if (f && !Object.is(a, r)) {
if (w(i(t), true), !u)
w(i($), true);
w(i($2), true);
}
return f;
}, deleteProperty(c, t) {
let r = Reflect.deleteProperty(c, t);
if (r)
w(i(t), true), w(i($), true);
w(i(t), true), w(i($2), true);
return r;
}, ownKeys(c) {
return w(i($)), Reflect.ownKeys(c);
return w(i($2)), Reflect.ownKeys(c);
} });
return D.set(e, s), s;
};
@@ -526,7 +518,7 @@ if (typeof window < "u")
window[e] = (o, n) => O(e, o, n);
});
// components/All.js
// sigpro-helpers.js
var get = (val) => typeof val === "function" ? val() : val;
var getBy = (item, field = "label") => item && typeof item === "object" ? item[field] : item;
var cls = (...classes) => classes.filter(Boolean).join(" ").trim();
@@ -535,7 +527,7 @@ var filterBy = (items, query, field = "label", q2 = String(query).toLowerCase())
var rand = (r) => `${r}-${Math.random().toString(36).slice(2, 9)}`;
var close = () => document.activeElement?.blur();
var listKey = (items, isOpen) => {
const cursor = S(-1);
const cursor = $(-1);
const onKey = (e, select) => {
const list = get(items), i = cursor(), len = list.length;
if (!len)
@@ -545,6 +537,37 @@ var listKey = (items, isOpen) => {
};
return { cursor, onKey };
};
var fx = ({ name, duration = 200, scale, slide, rotate, blur }, child) => {
const el = typeof child === "function" ? child() : child;
if (!(el instanceof Node))
return el;
if (name) {
el.style.animation = `${name}-in ${duration}ms`;
return el;
}
const hasTransform = scale || slide || rotate || blur;
const initialTransform = [
scale ? "scale(0.95)" : "",
slide ? "translateY(-10px)" : "",
rotate ? "rotate(-2deg)" : ""
].filter(Boolean).join(" ");
el.style.transition = `all ${duration}ms ease`;
el.style.opacity = "0";
if (hasTransform)
el.style.transform = initialTransform;
if (blur)
el.style.filter = "blur(4px)";
requestAnimationFrame(() => {
el.style.opacity = "1";
if (hasTransform)
el.style.transform = "none";
if (blur)
el.style.filter = "none";
});
return el;
};
// sigpro-components.js
var Accordion = (p2) => {
const name = p2.name || rand("acc");
return K(p2.items, (it) => {
@@ -590,7 +613,7 @@ var Autocomplete = ({ items, value, onselect, placeholder = "Buscar...", ...prop
}, 150),
onkeydown: (e) => onKey(e, pick)
}),
z(isOpen, () => G({ duration: 200, slide: true }, O("ul", {
z(isOpen, () => fx({ duration: 200, slide: true }, O("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"
}, [
K(filtered, (item, idx) => O("li", {}, [
@@ -700,7 +723,14 @@ var Calendar = (p2) => {
return cells;
}
]),
p2.hour ? O("div", { class: "mt-3 pt-2 border-t border-base-300" }, rangeMode() ? O("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
p2.hour ? O("div", { class: "mt-3 pt-2 border-t border-base-300" }, rangeMode() ? O("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,
O("div", { class: cls("flex gap-2 mt-3 pt-2", !p2.hour && "border-t border-base-300") }, [
O("button", {
type: "button",
class: "btn btn-xs flex-1",
onclick: () => close()
}, Icon("icon-[lucide--x]"))
])
]);
};
var Card = (p2, c) => O("div", { ...p2, class: cls("card", p2.class) }, c);
@@ -797,15 +827,22 @@ var ColorPalette = (p2) => {
"#fae8ff"
];
const pick = (c) => isFn(p2.value) ? p2.value(c) : p2.onchange?.(c);
return O("div", { class: "grid grid-cols-8 gap-1" }, palette.map((c) => O("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)
})));
return [
O("div", { class: "grid grid-cols-8 gap-1" }, palette.map((c) => O("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)
}))),
O("button", {
type: "button",
class: "btn btn-xs w-full",
onclick: () => close()
}, Icon("icon-[lucide--x]"))
];
};
var Datepicker = (p2) => {
const isOpen = S(false);
@@ -972,7 +1009,7 @@ var Input = (p2) => {
]) : null
]),
hint ? O("div", { class: "validator-hint" }, hint) : null,
z(isFocused, () => G({ duration: 300, slide: true }, O("div", { class: "input-content", onmousedown: (e) => e.preventDefault() }, [
z(isFocused, () => fx({ duration: 300, slide: true }, O("div", { class: "input-content", onmousedown: (e) => e.preventDefault() }, [
isFn(content) ? content(isFocused) : content
])))
])
@@ -1183,7 +1220,7 @@ var Toast = (message, type = "alert-success", duration = 3500) => {
var Toggle = (p2) => O("input", { ...p2, type: "checkbox", class: cls("toggle", p2.class) });
var Tooltip = (p2, c) => O("div", { ...p2, class: cls("tooltip", p2.class), "data-tip": p2.tip }, c);
// utils.js
// sigpro-locale.js
var i18n = {
es: {
close: "Cerrar",
@@ -1208,7 +1245,7 @@ var tt = (t) => () => i18n[currentLocale()][t] || t;
// index.js
var Components = {
...exports_All
...exports_sigpro_components
};
var Utils = {
Locale,

File diff suppressed because one or more lines are too long

99
dist/sigpro-ui.js vendored
View File

@@ -44,17 +44,9 @@
Components: () => Components
});
// components/All.js
var exports_All = {};
__export(exports_All, {
rand: () => rand,
listKey: () => listKey,
isFn: () => isFn,
getBy: () => getBy,
get: () => get,
filterBy: () => filterBy,
cls: () => cls,
close: () => close,
// sigpro-components.js
var exports_sigpro_components = {};
__export(exports_sigpro_components, {
Tooltip: () => Tooltip,
Toggle: () => Toggle,
Toast: () => Toast,
@@ -137,7 +129,7 @@
var A = 0;
var C = new Set;
var D = new WeakMap;
var $ = Symbol("iter");
var $2 = Symbol("iter");
var B = new WeakMap;
var E = (e) => {
if (!e || e._disposed)
@@ -284,16 +276,16 @@
let u = Reflect.has(c, t), a = Reflect.get(c, t, l), f = Reflect.set(c, t, r, l);
if (f && !Object.is(a, r)) {
if (w(i(t), true), !u)
w(i($), true);
w(i($2), true);
}
return f;
}, deleteProperty(c, t) {
let r = Reflect.deleteProperty(c, t);
if (r)
w(i(t), true), w(i($), true);
w(i(t), true), w(i($2), true);
return r;
}, ownKeys(c) {
return w(i($)), Reflect.ownKeys(c);
return w(i($2)), Reflect.ownKeys(c);
} });
return D.set(e, s), s;
};
@@ -557,7 +549,7 @@
window[e] = (o, n) => O(e, o, n);
});
// components/All.js
// sigpro-helpers.js
var get = (val) => typeof val === "function" ? val() : val;
var getBy = (item, field = "label") => item && typeof item === "object" ? item[field] : item;
var cls = (...classes) => classes.filter(Boolean).join(" ").trim();
@@ -566,7 +558,7 @@
var rand = (r) => `${r}-${Math.random().toString(36).slice(2, 9)}`;
var close = () => document.activeElement?.blur();
var listKey = (items, isOpen) => {
const cursor = S(-1);
const cursor = $(-1);
const onKey = (e, select) => {
const list = get(items), i = cursor(), len = list.length;
if (!len)
@@ -576,6 +568,37 @@
};
return { cursor, onKey };
};
var fx = ({ name, duration = 200, scale, slide, rotate, blur }, child) => {
const el = typeof child === "function" ? child() : child;
if (!(el instanceof Node))
return el;
if (name) {
el.style.animation = `${name}-in ${duration}ms`;
return el;
}
const hasTransform = scale || slide || rotate || blur;
const initialTransform = [
scale ? "scale(0.95)" : "",
slide ? "translateY(-10px)" : "",
rotate ? "rotate(-2deg)" : ""
].filter(Boolean).join(" ");
el.style.transition = `all ${duration}ms ease`;
el.style.opacity = "0";
if (hasTransform)
el.style.transform = initialTransform;
if (blur)
el.style.filter = "blur(4px)";
requestAnimationFrame(() => {
el.style.opacity = "1";
if (hasTransform)
el.style.transform = "none";
if (blur)
el.style.filter = "none";
});
return el;
};
// sigpro-components.js
var Accordion = (p2) => {
const name = p2.name || rand("acc");
return K(p2.items, (it) => {
@@ -621,7 +644,7 @@
}, 150),
onkeydown: (e) => onKey(e, pick)
}),
z(isOpen, () => G({ duration: 200, slide: true }, O("ul", {
z(isOpen, () => fx({ duration: 200, slide: true }, O("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"
}, [
K(filtered, (item, idx) => O("li", {}, [
@@ -731,7 +754,14 @@
return cells;
}
]),
p2.hour ? O("div", { class: "mt-3 pt-2 border-t border-base-300" }, rangeMode() ? O("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
p2.hour ? O("div", { class: "mt-3 pt-2 border-t border-base-300" }, rangeMode() ? O("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,
O("div", { class: cls("flex gap-2 mt-3 pt-2", !p2.hour && "border-t border-base-300") }, [
O("button", {
type: "button",
class: "btn btn-xs flex-1",
onclick: () => close()
}, Icon("icon-[lucide--x]"))
])
]);
};
var Card = (p2, c) => O("div", { ...p2, class: cls("card", p2.class) }, c);
@@ -828,15 +858,22 @@
"#fae8ff"
];
const pick = (c) => isFn(p2.value) ? p2.value(c) : p2.onchange?.(c);
return O("div", { class: "grid grid-cols-8 gap-1" }, palette.map((c) => O("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)
})));
return [
O("div", { class: "grid grid-cols-8 gap-1" }, palette.map((c) => O("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)
}))),
O("button", {
type: "button",
class: "btn btn-xs w-full",
onclick: () => close()
}, Icon("icon-[lucide--x]"))
];
};
var Datepicker = (p2) => {
const isOpen = S(false);
@@ -1003,7 +1040,7 @@
]) : null
]),
hint ? O("div", { class: "validator-hint" }, hint) : null,
z(isFocused, () => G({ duration: 300, slide: true }, O("div", { class: "input-content", onmousedown: (e) => e.preventDefault() }, [
z(isFocused, () => fx({ duration: 300, slide: true }, O("div", { class: "input-content", onmousedown: (e) => e.preventDefault() }, [
isFn(content) ? content(isFocused) : content
])))
])
@@ -1214,7 +1251,7 @@
var Toggle = (p2) => O("input", { ...p2, type: "checkbox", class: cls("toggle", p2.class) });
var Tooltip = (p2, c) => O("div", { ...p2, class: cls("tooltip", p2.class), "data-tip": p2.tip }, c);
// utils.js
// sigpro-locale.js
var i18n = {
es: {
close: "Cerrar",
@@ -1239,7 +1276,7 @@
// index.js
var Components = {
...exports_All
...exports_sigpro_components
};
var Utils = {
Locale,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,96 +1,10 @@
import * as AllModule from './components/All.js';
// import * as EditorModule from './components/Editor.js';
// import * as AccordionModule from './components/accordion.js';
// import * as AlertModule from './components/alert.js';
// import * as AutocompleteModule from './components/discarted/Autocomplete.js';
// import * as BadgeModule from './components/badge.js';
// import * as ButtonModule from './components/button.js';
// import * as CalendarModule from './components/Calendar.js';
// import * as CardModule from './components/card.js';
// import * as CarouselModule from './components/carousel.js';
// import * as ChatModule from './components/chat.js';
// import * as CheckboxModule from './components/checkbox.js';
// import * as ColorpickerModule from './components/Colorpicker.js';
// import * as DatepickerModule from './components/Datepicker.js';
// import * as DrawerModule from './components/drawer.js';
// import * as DropdownModule from './components/dropdown.js';
// import * as FabModule from './components/fab.js';
// import * as FieldsetModule from './components/fieldset.js';
// import * as FileinputModule from './components/fileinput.js';
// import * as IconModule from './components/icon.js';
// import * as IndicatorModule from './components/indicator.js';
// import * as InputModule from './components/Input.js';
// import * as KdbModule from './components/kbd.js';
// import * as ListModule from './components/List.js';
// import * as LoadingModule from './components/loading.js';
// import * as MenuModule from './components/menu.js';
// import * as ModalModule from './components/modal.js';
// import * as NavbarModule from './components/navbar.js';
// import * as RadialModule from './components/radial.js';
// import * as RadioModule from './components/radio.js';
// import * as RangeModule from './components/range.js';
// import * as RatingModule from './components/rating.js';
// import * as SkeletonModule from './components/skeleton.js';
// import * as SelectModule from './components/discarted/Select.js';
// import * as StackModule from './components/stack.js';
// import * as StatModule from './components/stat.js';
// import * as StepsModule from './components/stat.js';
// import * as SwapModule from './components/swap.js';
// import * as TableModule from './components/table.js';
// import * as TabsModule from './components/tabs.js';
// import * as TextareaModule from './components/textarea.js';
// import * as TextrotateModule from './components/textrotate.js';
// import * as TimelineModule from './components/timeline.js';
// import * as ToastModule from './components/toast.js';
// import * as TooltipModule from './components/tooltip.js';
import { Locale, tt } from './utils.js';
import * as All from './sigpro-components.js';
// import * as Editor from './sigpro-editor.js';
import { Locale, tt } from './sigpro-locale.js';
export const Components = {
...AllModule,
// ...EditorModule,
// ...AccordionModule,
// ...AlertModule,
// ...AutocompleteModule,
// ...BadgeModule,
// ...ButtonModule,
// ...CalendarModule,
// ...CardModule,
// ...CarouselModule,
// ...ChatModule,
// ...CheckboxModule,
// ...ColorpickerModule,
// ...DatepickerModule,
// ...DrawerModule,
// ...DropdownModule,
// ...FabModule,
// ...FieldsetModule,
// ...FileinputModule,
// ...IconModule,
// ...IndicatorModule,
// ...InputModule,
// ...KdbModule,
// ...ListModule,
// ...LoadingModule,
// ...MenuModule,
// ...ModalModule,
// ...NavbarModule,
// ...RadialModule,
// ...RadioModule,
// ...RangeModule,
// ...RatingModule,
// ...SkeletonModule,
// ...SelectModule,
// ...StackModule,
// ...StatModule,
// ...StepsModule,
// ...SwapModule,
// ...TableModule,
// ...TabsModule,
// ...TextareaModule,
// ...TextrotateModule,
// ...TimelineModule,
// ...ToastModule,
// ...TooltipModule
...All,
// ...Editor,
};
export const Utils = {

View File

@@ -45,8 +45,8 @@
"scripts": {
"del": "bun pm cache rm && rm -f bun.lockb $$ rm -f bun.lock",
"clean": "rm -rf ./dist ./css/*.css ./docs/*.js ./docs/*.css",
"build:css": "tailwindcss -i ./sigpro.css -o ./dist/sigpro-ui.css --content './src/**/*.js' && du -h ./dist/sigpro-ui.css",
"build:cssmin": "tailwindcss -i ./sigpro.css -o ./dist/sigpro-ui.min.css --content './src/**/*.js' --minify && du -h ./dist/sigpro-ui.css",
"build:css": "tailwindcss -i ./sigpro-css.css -o ./dist/sigpro-ui.css --content './src/**/*.js' && du -h ./dist/sigpro-ui.css",
"build:cssmin": "tailwindcss -i ./sigpro-css.css -o ./dist/sigpro-ui.min.css --content './src/**/*.js' --minify && du -h ./dist/sigpro-ui.css",
"build:js": "bun run build:js:iife && bun run build:js:esm",
"build:js:iife": "bun build ./index.js --bundle --outfile=./dist/sigpro-ui.js --format=iife --global-name=SigProUI",
"build:js:iife:min": "bun build ./index.js --bundle --outfile=./dist/sigpro-ui.min.js --format=iife --global-name=SigProUI --minify",

View File

@@ -1,29 +1,7 @@
// All base components
import { h, each, watch, when, fx, mount, $ } from "sigpro";
import { h, each, watch, when, mount, $ } from "sigpro";
import { get, getBy, cls, isFn, filterBy, rand, close, listKey, fx } from "./sigpro-helpers.js"
// 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', q = String(query).toLowerCase()) => !query ? get(items) : get(items).filter(item => String(item && typeof item === 'object' ? item[field] : item).toLowerCase().includes(q));
export const rand = (r) => `${r}-${Math.random().toString(36).slice(2, 9)}`
export const close = () => document.activeElement?.blur()
export const listKey = (items, isOpen) => {
const 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), cursor(-1)) : null;
};
return { cursor, onKey };
};
// Components
export const Accordion = (p) => {
const name = p.name || rand('acc')
return each(p.items, (it) => {
@@ -175,7 +153,12 @@ 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
) : null,
h('button', {
type: 'button',
class: 'btn btn-xs w-full',
onclick: () => close()
}, Icon('icon-[lucide--x]'))
])
}
export const Card = (p, c) => h('div', { ...p, class: cls('card', p.class) }, c);
@@ -198,7 +181,7 @@ export const Colorpicker = (p) => {
p.label && h('span', {}, p.label)
]),
DropdownContent({ class: 'p-3 bg-base-100 rounded-box shadow-xl w-64' },
ColorPalette({ value: p.value, onchange: (c) => { isFn(p.value) ? p.value(c) : p.onchange?.(c); close(); } })
ColorPalette({ value: p.value, onchange: (c) => { isFn(p.value) ? p.value(c) : p.onchange?.(c); } })
)
])
}
@@ -215,9 +198,9 @@ 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', {
return [
h('div', { class: 'grid grid-cols-8 gap-1' },
palette.map(c => h('button', {
type: 'button',
style: `background-color: ${c}`,
class: () => {
@@ -226,8 +209,13 @@ export const ColorPalette = (p) => {
},
onclick: () => pick(c)
})
)
)
)
),
h('button', {
type: 'button',
class: 'btn btn-xs w-full',
onclick: () => close()
}, Icon('icon-[lucide--x]'))]
}
export const Datepicker = (p) => {
const isOpen = $(false)

View File

@@ -1,5 +1,5 @@
import { h, $ } from "sigpro"
import { get, cls, isFn } from "./All.js"
import { get, cls, isFn } from "./sigpro-helpers.js"
export const Editor = (p) => {
const { value, class: extraClass } = p

87
sigpro-helpers.js Normal file
View File

@@ -0,0 +1,87 @@
// 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', q = String(query).toLowerCase()) => !query ? get(items) : get(items).filter(item => String(item && typeof item === 'object' ? item[field] : item).toLowerCase().includes(q));
export const rand = (r) => `${r}-${Math.random().toString(36).slice(2, 9)}`
export const close = () => document.activeElement?.blur()
export const listKey = (items, isOpen) => {
const 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), cursor(-1)) : null;
};
return { cursor, onKey };
};
export const fx = ({ name, duration = 200, scale, slide, rotate, blur }, child) => {
const el = typeof child === "function" ? child() : child;
if (!(el instanceof Node)) return el;
if (name) {
el.style.animation = `${name}-in ${duration}ms`;
return el;
}
const hasTransform = scale || slide || rotate || blur;
const initialTransform = [
scale ? "scale(0.95)" : "",
slide ? "translateY(-10px)" : "",
rotate ? "rotate(-2deg)" : ""
].filter(Boolean).join(" ");
el.style.transition = `all ${duration}ms ease`;
el.style.opacity = "0";
if (hasTransform) el.style.transform = initialTransform;
if (blur) el.style.filter = "blur(4px)";
requestAnimationFrame(() => {
el.style.opacity = "1";
if (hasTransform) el.style.transform = "none";
if (blur) el.style.filter = "none";
});
return el;
};
export const req = ({ url, method = 'GET', headers = {} }) => {
const loading = $(false);
const error = $(null);
const data = $(null);
let controller = null;
let timeoutId = null;
const run = async (body = null) => {
controller?.abort();
clearTimeout(timeoutId);
controller = new AbortController();
timeoutId = setTimeout(() => controller.abort(), 10000);
loading(true);
error(null);
try {
const isFormData = body instanceof FormData;
const res = await fetch(url, {
method,
headers: isFormData ? headers : { 'Content-Type': 'application/json', ...headers },
body: isFormData ? body : (body ? JSON.stringify(body) : undefined),
signal: controller.signal
});
const text = await res.text();
const json = text ? JSON.parse(text) : null;
if (!res.ok) throw new Error(json?.message || res.statusText);
data(json);
return json;
} catch (e) {
if (e.name !== 'AbortError') error(e.message);
throw e;
} finally {
loading(false);
clearTimeout(timeoutId);
controller = null;
timeoutId = null;
}
};
const abort = () => controller?.abort();
return { run, abort, loading, error, data };
};

View File

@@ -1,29 +0,0 @@
// components/Accordion.js
import { h } from "sigpro";
import { ui, val } from "../utils.js";
/**
* Accordion component
*
* daisyUI classes used:
* - collapse, collapse-arrow, collapse-plus, collapse-title, collapse-content
* - collapse-open, collapse-close
* - bg-base-200, bg-base-100, bg-primary, bg-secondary
* - mb-2, mt-2, rounded-box
*/
export const Accordion = (props, children) => {
const { class: className, title, name, open, ...rest } = props;
return h("div", {
...rest,
class: ui('collapse collapse-arrow bg-base-200 mb-2', className),
}, [
h("input", {
type: name ? "radio" : "checkbox",
name: name,
checked: val(open),
}),
h("div", { class: "collapse-title text-xl font-medium" }, title),
h("div", { class: "collapse-content" }, children),
]);
};

View File

@@ -1,42 +0,0 @@
// components/Alert.js
import { h } from "sigpro";
import { ui, getIcon } from "../utils.js";
/**
* Alert component
*
* daisyUI classes used:
* - alert, alert-info, alert-success, alert-warning, alert-error
* - alert-soft, alert-outline, alert-dash
*/
export const Alert = (props, children) => {
const { class: className, actions, type = 'info', soft = true, ...rest } = props;
const iconMap = {
info: "icon-[lucide--info]",
success: "icon-[lucide--check-circle]",
warning: "icon-[lucide--alert-triangle]",
error: "icon-[lucide--alert-circle]",
};
// Build the complete class string
const typeClass = `alert-${type}`;
const softClass = soft ? 'alert-soft' : '';
const allClasses = [typeClass, softClass, className].filter(Boolean).join(' ');
const content = children || props.message;
return h("div", {
...rest,
role: "alert",
class: ui('alert', allClasses),
}, () => [
getIcon(iconMap[type]),
h("div", { class: "flex-1" }, [
h("span", {}, [typeof content === "function" ? content() : content])
]),
actions ? h("div", { class: "flex-none" }, [
typeof actions === "function" ? actions() : actions
]) : null,
].filter(Boolean));
};

View File

@@ -1,114 +0,0 @@
// components/Autocomplete.js
import { $, h, each } from "sigpro";
import { val } from "../utils.js";
import { Input } from "./Input.js";
/**
* Autocomplete component
*
* daisyUI classes used:
* - input, input-bordered, input-primary, input-secondary
* - menu, menu-dropdown, menu-dropdown-show
* - bg-base-100, rounded-box, shadow-xl, border, border-base-300
* - absolute, left-0, w-full, mt-1, p-2, max-h-60, overflow-y-auto
* - z-50, active, bg-primary, text-primary-content
*/
export const Autocomplete = (props) => {
const { class: className, items = [], value, onselect, label, placeholder, ...rest } = props;
// Inicializamos query con el valor actual de la señal recibida o string vacío
const query = $(val(value) || "");
const isOpen = $(false);
const cursor = $(-1);
// FIX CRÍTICO: En lugar de una computed automática, usamos una señal manual
// y un watch para garantizar que la lista se actualice SÍNCRONAMENTE.
const list = $([]);
watch(() => {
const q = String(query()).toLowerCase();
const data = val(items) || [];
const filtered = q
? data.filter((o) => (typeof o === "string" ? o : o.label).toLowerCase().includes(q))
: data;
list(filtered);
});
const pick = (opt) => {
const valStr = typeof opt === "string" ? opt : opt.value;
const labelStr = typeof opt === "string" ? opt : opt.label;
// Actualizamos ambas señales
query(labelStr);
if (typeof value === "function") value(valStr);
onselect?.(opt);
isOpen(false);
cursor(-1);
};
const nav = (e) => {
const currentItems = list();
if (e.key === "ArrowDown") {
e.preventDefault();
isOpen(true);
cursor(Math.min(cursor() + 1, currentItems.length - 1));
} else if (e.key === "ArrowUp") {
e.preventDefault();
cursor(Math.max(cursor() - 1, 0));
} else if (e.key === "Enter" && cursor() >= 0) {
e.preventDefault();
pick(currentItems[cursor()]);
} else if (e.key === "Escape") {
isOpen(false);
}
};
return h("div", { class: 'relative w-full' }, [
input({
label,
class: className,
placeholder: placeholder,
value: query, // Vinculado a la señal query
onfocus: () => isOpen(true),
onblur: () => setTimeout(() => isOpen(false), 150),
onkeydown: nav,
oninput: (e) => {
const v = e.target.value;
query(v); // Esto dispara el watch de arriba y actualiza 'list'
if (typeof value === "function") value(v);
isOpen(true);
cursor(-1);
},
...rest,
}),
h(
"ul",
{
class: "absolute dropdown-menu 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",
// Usamos una función para que el estilo sea reactivo
style: () => (isOpen() && list().length ? "display:block" : "display:none"),
},
[
each(
list,
(opt, i) =>
h("li", {}, [
h(
"a",
{
class: () => `block w-full ${cursor() === i ? "active bg-primary text-primary-content" : ""}`,
onclick: () => pick(opt),
onmouseenter: () => cursor(i),
},
typeof opt === "string" ? opt : opt.label,
),
]),
(opt, i) => (typeof opt === "string" ? opt : opt.value) + i,
),
// Mensaje de "no hay datos" reactivo
() => (list().length ? null : h("li", { class: "p-2 text-center opacity-50" }, "nodata")),
],
),
]);
};

View File

@@ -1,21 +0,0 @@
// components/Badge.js
import { h } from "sigpro";
import { ui } from "../utils.js";
/**
* Badge component
*
* daisyUI classes used:
* - 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
*/
export const Badge = (props, children) => {
const { class: className, ...rest } = props;
return h("span", {
...rest,
class: ui('badge', className),
}, children);
};

View File

@@ -1,24 +0,0 @@
// components/Button.js
import { h } from "sigpro";
import { ui, val, getIcon } from "../utils.js";
/**
* Button component
*
* daisyUI classes used:
* - 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
*/
export const Button = (props, children) => {
const { class: className, ...rest } = props;
return h("button", {
...rest,
class: ui('btn', className),
disabled: () => val(props.disabled),
}, () => children);
};

View File

@@ -1,181 +0,0 @@
import { $, h, watch } from "sigpro";
import { val, getIcon } from "../utils.js";
export const Calendar = (props) => {
const { value, range = false, hour = false, onChange, class: className = "" } = props;
const internalDate = $(new Date());
const hoverDate = $(null);
const startHour = $(0);
const endHour = $(0);
const isRangeMode = () => range === true;
const now = new Date();
const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
const formatDate = (d) => {
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
const selectDate = (date) => {
const dateStr = formatDate(date);
const current = val(value);
if (isRangeMode()) {
if (!current?.start || (current.start && current.end)) {
// primera selección
const newValue = {
start: dateStr,
end: null,
...(hour && { startHour: startHour() }),
};
onChange?.(newValue);
} else {
// segunda selección
const start = current.start;
let newValue;
if (dateStr < start) {
newValue = { start: dateStr, end: start };
} else {
newValue = { start, end: dateStr };
}
if (hour) {
newValue.startHour = current.startHour !== undefined ? current.startHour : startHour();
newValue.endHour = endHour();
}
onChange?.(newValue);
}
} else {
// modo fecha simple
const newValue = hour ? `${dateStr}T${String(startHour()).padStart(2, "0")}:00:00` : dateStr;
onChange?.(newValue);
}
};
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: onHourChange }) => {
return 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) => {
const newHour = parseInt(e.target.value);
onHourChange(newHour);
},
}),
h("span", { class: "text-sm font-mono min-w-[48px] text-center" },
() => String(val(hVal)).padStart(2, "0") + ":00"
),
]),
]);
};
return h("div", { class: `p-4 bg-base-100 border border-base-300 shadow-2xl rounded-box w-80 select-none ${className}` }, [
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) },
getIcon("icon-[lucide--chevrons-left]")
),
h("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(-1) },
getIcon("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) },
getIcon("icon-[lucide--chevron-right]")
),
h("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(1) },
getIcon("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();
const year = d.getFullYear();
const month = d.getMonth();
const firstDay = new Date(year, month, 1).getDay();
const offset = firstDay === 0 ? 6 : firstDay - 1;
const daysInMonth = new Date(year, month + 1, 0).getDate();
const nodes = [];
for (let i = 0; i < offset; i++) nodes.push(h("div"));
for (let i = 1; i <= daysInMonth; i++) {
const date = new Date(year, month, i);
const dStr = formatDate(date);
nodes.push(
h("button", {
type: "button",
class: () => {
const v = val(value);
const h = hoverDate();
const isStart = typeof v === "string" ? v.split("T")[0] === dStr : v?.start === dStr;
const isEnd = v?.end === dStr;
let inRange = false;
if (isRangeMode() && v?.start) {
const start = v.start;
if (!v.end && h) {
inRange = (dStr > start && dStr <= h) || (dStr < start && dStr >= h);
} else if (v.end) {
inRange = dStr > start && dStr < v.end;
}
}
const base = "btn btn-xs p-0 aspect-square min-h-0 h-auto font-normal relative";
const state = isStart || isEnd ? "btn-primary z-10" : inRange ? "bg-primary/20 border-none rounded-none" : "btn-ghost";
const today = dStr === todayStr ? "ring-1 ring-primary ring-inset font-black text-primary" : "";
return `${base} ${state} ${today}`;
},
onmouseenter: () => { if (isRangeMode()) hoverDate(dStr); },
onclick: () => selectDate(date),
}, [i.toString()])
);
}
return nodes;
},
]),
hour ? h("div", { class: "mt-3 pt-2 border-t border-base-300" }, [
isRangeMode()
? h("div", { class: "flex gap-4" }, [
HourSlider({
value: startHour,
onChange: (newHour) => startHour(newHour),
}),
HourSlider({
value: endHour,
onChange: (newHour) => endHour(newHour),
}),
])
: HourSlider({
value: startHour,
onChange: (newHour) => startHour(newHour),
}),
]) : null,
]);
};

View File

@@ -1,30 +0,0 @@
// components/Checkbox.js
import { h } from "sigpro";
import { val, ui } from "../utils.js";
/**
* Checkbox component
*
* daisyUI classes used:
* - 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
* - label, label-text, cursor-pointer
*/
export const Checkbox = (props) => {
const { class: className, value, toggle, label, ...rest } = props;
const checkEl = h("input", {
...rest,
type: "checkbox",
class: () => ui(val(toggle) ? "toggle" : "checkbox", className),
checked: value
});
return h("label", { class: "label cursor-pointer justify-start gap-3" }, [
checkEl,
label ? h("span", { class: "label-text" }, label) : null,
]);
};

View File

@@ -1,92 +0,0 @@
// components/Colorpicker.js
import { $, h, when} from "sigpro";
import { val, ui } from "../utils.js";
/**
* Colorpicker component
*
* daisyUI classes used:
* - btn, btn-primary, btn-secondary, btn-accent, btn-ghost
* - bg-base-100, border, border-base-300, shadow-sm, shadow-2xl
* - rounded-box, rounded-sm, fixed, inset-0
* - z-50, z-110, absolute, left-0, mt-2, p-3, w-64
* - grid, grid-cols-8, gap-1, ring, ring-offset-1, ring-primary
* - scale-110, transition-all, hover:scale-125, active:scale-95
*/
export const Colorpicker = (props) => {
const { class: className, value, label, ...rest } = props;
const isOpen = $(false);
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 getColor = () => val(value) || "#000000";
return h("div", { class: ui('relative w-fit', className) }, [
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());
},
...rest,
},
[
h("div", {
class: "size-5 rounded-sm shadow-inner border border-black/10 shrink-0",
style: () => `background-color: ${getColor()}`,
}),
label ? h("span", { class: "opacity-80" }, label) : null,
],
),
when(isOpen, () =>
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",
onclick: (e) => e.stopPropagation(),
},
[
h(
"div",
{ class: "grid grid-cols-8 gap-1" },
palette.map((c) =>
h("button", {
type: "button",
style: `background-color: ${c}`,
class: () => {
const active = getColor().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
${active ? "ring-2 ring-offset-1 ring-primary z-10 scale-110" : ""}`;
},
onclick: () => {
if (typeof value === "function") value(c);
isOpen(false);
},
}),
),
),
],
),
),
when(isOpen, () =>
h("div", {
class: "fixed inset-0 z-[100]",
onclick: () => isOpen(false),
}),
),
]);
};

View File

@@ -1,75 +0,0 @@
import { $, h, when, watch } from "sigpro";
import { val, ui, getIcon } from "../utils.js";
import { Input } from "./Input.js";
import { Calendar } from "./Calendar.js";
export const Datepicker = (props) => {
const { class: className, value, range, label, placeholder, hour = false, ...rest } = props;
const isOpen = $(false);
const isRangeMode = () => val(range) === true;
// Formatear el valor para mostrarlo en el input
const displayValue = $("");
watch(() => {
const v = val(value);
if (!v) {
displayValue("");
return;
}
let text = "";
if (typeof v === "string") {
text = (hour && v.includes("T")) ? v.replace("T", " ") : v;
} else if (v.start && v.end) {
const startStr = hour && v.startHour !== undefined ? `${v.start} ${String(v.startHour).padStart(2, "0")}:00` : v.start;
const endStr = hour && v.endHour !== undefined ? `${v.end} ${String(v.endHour).padStart(2, "0")}:00` : v.end;
text = `${startStr} - ${endStr}`;
} else if (v.start) {
const startStr = hour && v.startHour !== undefined ? `${v.start} ${String(v.startHour).padStart(2, "0")}:00` : v.start;
text = `${startStr}...`;
}
displayValue(text);
});
const handleCalendarChange = (newValue) => {
if (typeof value === "function") {
value(newValue);
}
// Cerrar el popover si es modo simple o si el rango está completo (end existe)
if (!isRangeMode() || (newValue?.end !== undefined && newValue?.end !== null)) {
isOpen(false);
}
};
return h("div", { class: ui('relative w-full', className) }, [
input({
label,
placeholder: placeholder || (isRangeMode() ? "Seleccionar rango..." : "Seleccionar fecha..."),
value: displayValue,
readonly: true,
icon: getIcon("icon-[lucide--calendar]"),
onclick: (e) => {
e.stopPropagation();
isOpen(!isOpen());
},
...rest,
}),
when(isOpen, () =>
h("div", {
class: "absolute left-0 mt-2 z-[100]",
onclick: (e) => e.stopPropagation(),
}, [
Calendar({
value,
range: isRangeMode(),
hour,
onChange: handleCalendarChange,
}),
])
),
when(isOpen, () => h("div", { class: "fixed inset-0 z-[90]", onclick: () => isOpen(false) })),
]);
};

View File

@@ -1,46 +0,0 @@
// components/Drawer.js
import { h } from "sigpro";
import { ui } from "../utils.js";
/**
* Drawer component
*
* daisyUI classes used:
* - drawer, drawer-toggle, drawer-content, drawer-side, drawer-overlay
* - bg-base-200, w-80, min-h-full
*/
export const Drawer = (props, children) => {
const { class: className, id, open, side, content, ...rest } = props;
const drawerId = id || `drawer-${Math.random().toString(36).slice(2, 9)}`;
return h("div", {
...rest,
class: ui('drawer', className),
}, [
h("input", {
id: drawerId,
type: "checkbox",
class: "drawer-toggle",
checked: () => typeof open === "function" ? open() : open,
onchange: (e) => {
if (typeof open === "function") open(e.target.checked);
}
}),
h("div", { class: "drawer-content" }, [
typeof content === "function" ? content() : content
]),
h("div", { class: "drawer-side" }, [
h("label", {
for: drawerId,
class: "drawer-overlay",
onclick: () => {
if (typeof open === "function") open(false);
}
}),
h("div", { class: "min-h-full bg-base-200 w-80" }, [
typeof side === "function" ? side() : side
])
])
]);
};

View File

@@ -1,78 +0,0 @@
// components/Dropdown.js
import { h, each, watch } from "sigpro";
import { ui } from "../utils.js";
/**
* Dropdown component - Solo soporta menús (items)
*
* daisyUI classes used:
* - dropdown, dropdown-content, dropdown-end, dropdown-top, dropdown-bottom
* - menu, btn
* - bg-base-100, shadow, rounded-box, border
* - z-50, p-2, w-52
* - m-1, flex, items-center, gap-2
*/
let currentOpen = null;
if (typeof window !== 'undefined' && !window.__dropdownHandlerRegistered) {
window.addEventListener('click', (e) => {
if (currentOpen && !currentOpen.contains(e.target)) {
currentOpen.open = false;
currentOpen = null;
}
});
window.__dropdownHandlerRegistered = true;
}
export const Dropdown = (props) => {
const { class: className, label, icon, items, ...rest } = props;
return h("details", {
...rest,
class: ui('dropdown', className),
}, [
h("summary", {
class: "btn m-1 flex items-center gap-2 list-none cursor-pointer",
style: "display: inline-flex;",
onclick: (e) => {
const details = e.currentTarget.closest('details');
if (currentOpen && currentOpen !== details) {
currentOpen.open = false;
}
setTimeout(() => {
currentOpen = details.open ? details : null;
}, 0);
}
}, [
() => icon ? (typeof icon === "function" ? icon() : icon) : null,
() => label ? (typeof label === "function" ? label() : label) : null
]),
h("ul", {
tabindex: "-1",
class: "dropdown-content z-[50] menu p-2 shadow bg-base-100 rounded-box w-52 border border-base-300"
}, [
() => {
const currentItems = typeof items === "function" ? items() : (items || []);
return currentItems.map(item =>
h("li", {}, [
h("a", {
class: item.class || "",
onclick: (e) => {
if (item.onclick) item.onclick(e);
const details = e.currentTarget.closest('details');
if (details) {
details.open = false;
if (currentOpen === details) currentOpen = null;
}
}
}, [
item.icon ? h("span", {}, item.icon) : null,
h("span", {}, item.label)
])
])
);
}
])
]);
};

View File

@@ -1,58 +0,0 @@
// components/Fab.js
import { h } from "sigpro";
import { val, ui, getIcon } from "../utils.js";
/**
* Fab (Floating Action Button) component
*
* daisyUI classes used:
* - btn, btn-lg, btn-circle, btn-primary
* - shadow-2xl, shadow-lg
* - badge, badge-ghost, shadow-sm, whitespace-nowrap
* - absolute, flex, flex-col-reverse, items-end, gap-3
* - z-100, transition-all, duration-300
* - bottom-6, right-6, top-6, left-6
*/
export const Fab = (props) => {
const { class: className, icon, label, actions = [], position = "bottom-6 right-6", ...rest } = props;
return h(
"div",
{
...rest,
class: ui(`fab absolute ${position} flex flex-col-reverse items-end gap-3 z-[100]`, className),
},
[
h(
"div",
{
tabindex: 0,
role: "button",
class: "btn btn-lg btn-circle btn-primary shadow-2xl",
},
[
icon ? getIcon(icon) : null,
!icon && label ? label : null
],
),
...val(actions).map((act) =>
h("div", { class: "flex items-center gap-3 transition-all duration-300" }, [
act.label ? h("span", { class: "badge badge-ghost shadow-sm whitespace-nowrap" }, act.label) : null,
h(
"button",
{
type: "button",
class: `btn btn-circle shadow-lg ${act.class || ""}`,
onclick: (e) => {
e.stopPropagation();
act.onclick?.(e);
},
},
[act.icon ? getIcon(act.icon) : act.text || ""],
),
]),
),
],
);
};

View File

@@ -1,53 +0,0 @@
import { $$, h, isFunc } from "sigpro";
const _cache = new Map();
const getStore = (key) => {
if (!_cache.has(key)) {
_cache.set(key, $$({ data: null, loading: false, error: null }));
}
return _cache.get(key);
};
export const Request = async (key, url, opts = {}) => {
const store = getStore(key);
const { body, ...rest } = opts;
store.loading = true;
store.error = null;
try {
const config = {
method: rest.method || 'GET',
headers: { 'Content-Type': 'application/json', ...rest.headers },
...rest
};
if (body) config.body = typeof body === 'object' ? JSON.stringify(body) : body;
const res = await fetch(url, config);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const result = await res.json();
store.data = result;
return result;
} catch (err) {
store.error = err.message;
throw err;
} finally {
store.loading = false;
}
};
export const Response = ({ name, loading, error }, { children }) => {
const store = getStore(name);
return h("div", { style: "display:contents" }, [
() => {
if (store.loading) return loading || "Cargando...";
if (store.error) return isFunc(error) ? error(store.error) : h("p", {}, store.error);
if (store.data) return isFunc(children[0]) ? children[0](store.data) : children;
return null;
}
]);
};

View File

@@ -1,30 +0,0 @@
// components/Fieldset.js
import { h } from "sigpro";
import { val, ui } from "../utils.js";
/**
* Fieldset component
*
* daisyUI classes used:
* - fieldset, fieldset-legend
* - bg-base-200, border, border-base-300
* - p-4, rounded-lg, font-bold
*/
export const Fieldset = (props, children) => {
const { class: className, legend, ...rest } = props;
return h(
"fieldset",
{
...rest,
class: ui('fieldset bg-base-200 border border-base-300 p-4 rounded-lg', className),
},
[
() => {
const legendText = val(legend);
return legendText ? h("legend", { class: "fieldset-legend font-bold" }, [legendText]) : null;
},
children,
],
);
};

View File

@@ -1,130 +0,0 @@
// components/Fileinput.js
import { $, h, when, each } from "sigpro";
import { ui, getIcon } from "../utils.js";
/**
* Fileinput component
*
* daisyUI classes used:
* - fieldset, w-full, p-0
* - tooltip, tooltip-top, before:z-50, after:z-50
* - relative, flex, items-center, justify-between, w-full, h-12, px-4
* - border-2, border-dashed, rounded-lg, cursor-pointer, transition-all, duration-200
* - border-primary, bg-primary/10, border-base-content/20, bg-base-100, hover:bg-base-200
* - text-sm, opacity-70, truncate, grow, text-left
* - text-[10px], opacity-40, shrink-0
* - text-error, text-[10px], mt-1, px-1, font-medium
* - mt-2, space-y-1
* - flex, items-center, justify-between, p-1.5, pl-3, text-xs, bg-base-200/50, rounded-md, border, border-base-300
* - gap-2, truncate, opacity-50, font-medium, max-w-[200px]
* - btn, btn-ghost, btn-xs, btn-circle
*/
export const Fileinput = (props) => {
const { class: className, tooltip, max = 2, accept = "*", onselect, ...rest } = props;
const selectedFiles = $([]);
const isDragging = $(false);
const error = $(null);
const MAX_BYTES = max * 1024 * 1024;
const handleFiles = (files) => {
const fileList = Array.from(files);
error(null);
const oversized = fileList.find((f) => f.size > MAX_BYTES);
if (oversized) {
error(`Máx ${max}MB`);
return;
}
selectedFiles([...selectedFiles(), ...fileList]);
onselect?.(selectedFiles());
};
const removeFile = (index) => {
const updated = selectedFiles().filter((_, i) => i !== index);
selectedFiles(updated);
onselect?.(updated);
};
return h("fieldset", { ...rest, class: ui('fieldset w-full p-0', className) }, [
h(
"div",
{
class: () => `w-full ${tooltip ? "tooltip tooltip-top before:z-50 after:z-50" : ""}`,
"data-tip": tooltip,
},
[
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
${isDragging() ? "border-primary bg-primary/10" : "border-base-content/20 bg-base-100 hover:bg-base-200"}
`,
ondragover: (e) => {
e.preventDefault();
isDragging(true);
},
ondragleave: () => isDragging(false),
ondrop: (e) => {
e.preventDefault();
isDragging(false);
handleFiles(e.dataTransfer.files);
},
},
[
h("div", { class: "flex items-center gap-3 w-full" }, [
getIcon("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 ${max}MB`),
]),
h("input", {
type: "file",
multiple: true,
accept: accept,
class: "hidden",
onchange: (e) => handleFiles(e.target.files),
}),
],
),
],
),
() => (error() ? h("span", { class: "text-[10px] text-error mt-1 px-1 font-medium" }, error()) : null),
when(
() => selectedFiles().length > 0,
() =>
h("ul", { class: "mt-2 space-y-1" }, [
each(
selectedFiles,
(file, index) =>
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();
e.stopPropagation();
removeFile(index);
},
},
[getIcon("icon-[lucide--x]")]
),
]),
(file) => file.name + file.lastModified,
),
]),
),
]);
};

View File

@@ -1,7 +0,0 @@
// components/Icon.js
import { h } from "sigpro";
export const Icon = (iconClass) => {
if (!iconClass) return null;
return h("span", { class: iconClass });
};

View File

@@ -1,28 +0,0 @@
// components/Indicator.js
import { h } from "sigpro";
import { ui } from "../utils.js";
/**
* Indicator component
*
* daisyUI classes used:
* - indicator, indicator-item
* - 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
*/
export const Indicator = (props, children) => {
const { value, class: className, ...rest } = props;
return h("div", {
...rest,
class: "indicator"
}, () => [
// El indicador debe ir PRIMERO (antes que el children)
value ? h("span", {
class: ui('indicator-item badge', className)
}, () => typeof value === "function" ? value() : value) : null,
children
].filter(Boolean));
};

View File

@@ -1,114 +0,0 @@
// components/Input.js
import { h } from "sigpro";
import { val, ui, getIcon } from "../utils.js";
/**
* Input component - Input con ícono integrado, toggle password, validación y floating label opcional
*
* daisyUI classes used:
* - 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
* - btn, btn-ghost, btn-xs, btn-sm, btn-md, btn-circle
*/
export const Input = (props) => {
const {
class: className,
value,
type = "text",
icon,
oninput,
placeholder,
disabled,
validate,
label,
...rest
} = props;
const isPassword = type === "password";
const visible = $(false);
const errorMsg = $(null);
const iconMap = {
text: "icon-[lucide--text]",
password: "icon-[lucide--lock]",
date: "icon-[lucide--calendar]",
number: "icon-[lucide--hash]",
email: "icon-[lucide--mail]",
search: "icon-[lucide--search]",
tel: "icon-[lucide--phone]",
url: "icon-[lucide--link]",
};
const leftIcon = icon ? getIcon(icon) : (iconMap[type] ? getIcon(iconMap[type]) : null);
const getPasswordIcon = () => getIcon(visible() ? "icon-[lucide--eye-off]" : "icon-[lucide--eye]");
const paddingLeft = leftIcon ? "pl-10" : "";
const paddingRight = isPassword ? "pr-10" : "";
const buttonSize = () => {
if (className?.includes('input-xs')) return 'btn-xs';
if (className?.includes('input-sm')) return 'btn-sm';
if (className?.includes('input-lg')) return 'btn-lg';
return 'btn-md';
};
const handleInput = (e) => {
const newValue = e.target.value;
if (validate) {
const result = validate(newValue);
errorMsg(result || null);
}
oninput?.(e);
};
const hasError = () => errorMsg() && errorMsg() !== '';
const inputClasses = () => {
let classes = `input w-full ${paddingLeft} ${paddingRight}`;
if (className) classes += ` ${className}`;
if (hasError()) classes += ' input-error';
return classes.trim();
};
const inputElement = h("input", {
...rest,
type: () => (isPassword ? (visible() ? "text" : "password") : type),
placeholder: placeholder || (label ? " " : placeholder),
class: inputClasses,
value: value,
oninput: handleInput,
disabled: () => val(disabled),
"aria-invalid": () => hasError() ? "true" : "false",
});
const inputContent = () => [
inputElement,
leftIcon ? h("div", {
class: "absolute left-3 inset-y-0 flex items-center pointer-events-none text-base-content/60",
}, leftIcon) : null,
isPassword ? h("button", {
type: "button",
class: ui("absolute right-3 inset-y-0 flex items-center", "btn btn-ghost btn-circle opacity-50 hover:opacity-100", buttonSize()),
onclick: (e) => {
e.preventDefault();
visible(!visible());
}
}, () => getPasswordIcon()) : null,
h("div", {
class: "text-error text-xs mt-1 px-3 absolute -bottom-5 left-0",
}, () => hasError() ? errorMsg() : null),
];
// Con floating label - añadir w-full para que ocupe todo el ancho
if (label) {
return h("label", { class: ui("floating-label w-full", className) }, () => [
h("div", { class: "relative w-full" }, inputContent),
h("span", {}, val(label))
]);
}
// Sin label
return h("div", { class: "relative w-full" }, inputContent);
};

View File

@@ -1,44 +0,0 @@
// components/InputPopover.js
export const InputPopover = (props) => {
const {
signal,
format = (v) => String(v ?? ""),
content,
placeholder,
label,
icon = "icon-[lucide--calendar]",
readonly = false,
class: className = "",
...rest
} = props;
const isOpen = $(false);
const displayValue = $(format(val(signal)));
watch(signal, () => {
displayValue(format(val(signal)));
});
const close = () => isOpen(false);
const toggle = () => isOpen(!isOpen());
return h("div", { class: ui("relative w-full", className) }, [
input({
label,
placeholder,
value: displayValue,
readonly,
icon: getIcon(icon),
onclick: (e) => { e.stopPropagation(); toggle(); },
onfocus: () => isOpen(true),
...rest,
}),
when(isOpen, () => h("div", {
class: "absolute left-0 mt-2 z-[100]",
onclick: (e) => e.stopPropagation(),
}, [
typeof content === "function" ? content({ signal, close }) : content
])),
when(isOpen, () => h("div", { class: "fixed inset-0 z-[90]", onclick: close }))
]);
};

View File

@@ -1,19 +0,0 @@
// components/Label.js
import { h } from "sigpro";
import { ui, val } from "../utils.js";
export const Label = (props) => {
const { children, value, floating = false, class: className, ...rest } = props;
if (floating) {
return h("label", { class: ui("floating-label", className), ...rest }, () => [
typeof children === 'function' ? children() : children,
value ? h("span", {}, val(value)) : null
]);
}
return h("label", { class: ui("label", className), ...rest }, () => [
value ? h("span", { class: "label-text" }, val(value)) : null,
typeof children === 'function' ? children() : children
]);
};

View File

@@ -1,33 +0,0 @@
// components/List.js
import { h, when, each } from "sigpro";
import { ui, val } from "../utils.js";
/**
* List component
*
* daisyUI classes used:
* - list, list-row, list-bullet, list-image, list-none
* - bg-base-100, rounded-box, shadow-md
* - p-4, pb-2, text-xs, opacity-60
* - flex, items-center, gap-2
*/
export const List = (props) => {
const { class: className, items, header, render = (item) => item, keyFn = (item, index) => item.id ?? index, ...rest } = props;
const listItems = each(
items,
(item, index) => h("li", {
class: "list-row",
style: "width: 100%; display: block;"
}, [
h("div", { style: "width: 100%;" }, [render(item, index)])
]),
keyFn
);
return h("ul", {
...rest,
style: "display: block; width: 100%;",
class: ui('list bg-base-100 rounded-box shadow-md', className),
}, header ? [when(header, () => h("li", { class: "p-4 pb-2 text-xs opacity-60", style: "width: 100%;" }, [val(header)])), listItems] : listItems);
};

View File

@@ -1,36 +0,0 @@
// components/Menu.js
import { h, each } from "sigpro";
import { val, ui } from "../utils.js";
/**
* Menu component
*
* daisyUI classes used:
* - menu, menu-dropdown, menu-dropdown-show
* - bg-base-200, rounded-box
* - details, summary, ul, li, a
* - mr-2, active
*/
export const Menu = (props) => {
const { class: className, items, ...rest } = props;
const renderItems = (items) =>
each(
() => items || [],
(it) =>
h("li", {}, [
it.children
? h("details", { open: it.open }, [
h("summary", {}, [it.icon && h("span", { class: "mr-2" }, it.icon), it.label]),
h("ul", {}, renderItems(it.children)),
])
: h("a", { class: () => (val(it.active) ? "active" : ""), onclick: it.onclick }, [
it.icon && h("span", { class: "mr-2" }, it.icon),
it.label,
]),
]),
(it, i) => it.label || i,
);
return h("ul", { ...rest, class: ui('menu bg-base-200 rounded-box', className) }, renderItems(items));
};

View File

@@ -1,64 +0,0 @@
// components/Modal.js
import { h, watch } from "sigpro";
import { ui } from "../utils.js";
import { Button } from "./Button.js";
/**
* Modal component
*
* daisyUI classes used:
* - modal, modal-box, modal-action, modal-backdrop
* - modal-open, modal-middle, modal-top, modal-bottom
* - text-lg, font-bold, mb-4, py-2
* - flex, gap-2
*/
export const Modal = (props, children) => {
const { class: className, title, buttons, open, ...rest } = props;
let dialogElement = null;
const handleOpen = () => {
const isOpen = typeof open === "function" ? open() : open;
if (!dialogElement) return;
if (isOpen) {
if (!dialogElement.open) dialogElement.showModal();
} else {
if (dialogElement.open) dialogElement.close();
}
};
watch(() => handleOpen());
const close = () => {
if (typeof open === "function") open(false);
};
return h("dialog", {
...rest,
ref: (el) => {
dialogElement = el;
if (el) handleOpen();
},
class: ui('modal', className),
onclose: close,
oncancel: close
}, [
h("div", { class: "modal-box" }, [
title ? h("h3", { class: "text-lg font-bold mb-4" }, () =>
typeof title === "function" ? title() : title
) : null,
h("div", { class: "py-2" }, [
typeof children === "function" ? children() : children
]),
h("div", { class: "modal-action" }, [
h("form", { method: "dialog", class: "flex gap-2" }, [
...(Array.isArray(buttons) ? buttons : [buttons]).filter(Boolean),
button({ type: "submit" }, "close")
])
])
]),
h("form", { method: "dialog", class: "modal-backdrop" }, [
h("button", {}, "close")
])
]);
};

View File

@@ -1,16 +0,0 @@
// components/Navbar.js
import { h } from "sigpro";
import { ui } from "../utils.js";
/**
* Navbar component
*
* daisyUI classes used:
* - navbar, navbar-start, navbar-center, navbar-end
* - bg-base-100, shadow-sm, px-4
*/
export const Navbar = (props, children) => {
const { class: className, ...rest } = props;
return h("div", { ...rest, class: ui('navbar bg-base-100 shadow-sm px-4', className) }, children);
};

View File

@@ -1,37 +0,0 @@
// components/Radio.js
import { h } from "sigpro";
import { val, ui } from "../utils.js";
/**
* Radio component
*
* daisyUI classes used:
* - radio, radio-primary, radio-secondary, radio-accent
* - radio-info, radio-success, radio-warning, radio-error
* - radio-xs, radio-sm, radio-md, radio-lg
* - label, label-text, cursor-pointer, justify-start, gap-3
* - tooltip, tooltip-top, tooltip-bottom, tooltip-left, tooltip-right
*/
export const Radio = (props) => {
const { class: className, label, tooltip, value, inputValue, name, ...rest } = props;
const radioEl = h("input", {
...rest,
type: "radio",
name: name,
class: ui('radio', className),
checked: () => val(value) === inputValue,
onclick: () => {
if (typeof value === "function") value(inputValue);
},
});
if (!label && !tooltip) return radioEl;
const layout = h("label", { class: "label cursor-pointer justify-start gap-3" }, [
radioEl,
label ? h("span", { class: "label-text" }, label) : null,
]);
return tooltip ? h("div", { class: "tooltip", "data-tip": tooltip }, layout) : layout;
};

View File

@@ -1,34 +0,0 @@
// components/Range.js
import { h } from "sigpro";
import { val, ui } from "../utils.js";
/**
* Range component
*
* daisyUI classes used:
* - range, range-primary, range-secondary, range-accent
* - range-info, range-success, range-warning, range-error
* - range-xs, range-sm, range-md, range-lg
* - label-text, flex, flex-col, gap-2
* - tooltip, tooltip-top, tooltip-bottom, tooltip-left, tooltip-right
*/
export const Range = (props) => {
const {class: className, label, tooltip, value, ...rest } = props;
const rangeEl = h("input", {
...rest,
type: "range",
class: ui('range', className),
value: value,
disabled: () => val(props.disabled)
});
if (!label && !tooltip) return rangeEl;
const layout = h("div", { class: "flex flex-col gap-2" }, [
label ? h("span", { class: "label-text" }, label) : null,
rangeEl
]);
return tooltip ? h("div", { class: "tooltip", "data-tip": tooltip }, layout) : layout;
};

View File

@@ -1,44 +0,0 @@
// components/Rating.js
import { h } from "sigpro";
import { val, ui } from "../utils.js";
/**
* Rating component
*
* daisyUI classes used:
* - rating, rating-half, rating-hidden
* - mask, mask-star, mask-star-2, mask-heart, mask-circle
* - pointer-events-none
*/
export const Rating = (props) => {
const { class: className, value, count = 5, mask = "mask-star", readonly = false, onchange, ...rest } = props;
const ratingGroup = `rating-${Math.random().toString(36).slice(2, 7)}`;
return h(
"div",
{
...rest,
class: () => ui(`rating ${val(readonly) ? "pointer-events-none" : ""}`, className),
},
Array.from({ length: val(count) }, (_, i) => {
const starValue = i + 1;
return h("input", {
type: "radio",
name: ratingGroup,
class: `mask ${mask}`,
checked: () => Math.round(val(value)) === starValue,
onchange: () => {
if (!val(readonly)) {
if (typeof onchange === "function") {
onchange(starValue);
}
else if (typeof value === "function") {
value(starValue);
}
}
},
});
})
);
};

View File

@@ -1,45 +0,0 @@
// components/Select.js
import { h, each } from "sigpro";
import { val, ui } from "../utils.js";
/**
* Select component
*
* daisyUI classes used:
* - 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
* - fieldset-label, flex, flex-col, gap-1, w-full
*/
export const Select = (props) => {
const { class: className, label, items, value, ...rest } = props;
const selectEl = h(
"select",
{
...rest,
class: ui('select select-bordered w-full', className),
value: value
},
each(
() => val(items) || [],
(opt) =>
h(
"option",
{
value: opt.value,
$selected: () => String(val(value)) === String(opt.value),
},
opt.label,
),
(opt) => opt.value,
),
);
if (!label) return selectEl;
return h("label", { class: "fieldset-label flex flex-col gap-1" }, [
h("span", {}, label),
selectEl
]);
};

View File

@@ -1,11 +0,0 @@
// components/Spinner.js
import { h } from "sigpro";
import { val } from "../utils.js";
export const Spinner = (props) => {
const { value, ...rest } = props;
return when(
() => val(value),
() => h("span", { class: "loading loading-spinner", ...rest })
);
};

View File

@@ -1,15 +0,0 @@
// components/Stack.js
import { h } from "sigpro";
import { ui } from "../utils.js";
/**
* Stack component
*
* daisyUI classes used:
* - stack, stack-top, stack-bottom, stack-start, stack-end
*/
export const Stack = (props, children) => {
const { class: className, ...rest } = props;
return h("div", { ...rest, class: ui('stack', className) }, children);
};

View File

@@ -1,21 +0,0 @@
// components/Stat.js
import { h } from "sigpro";
import { val, ui } from "../utils.js";
/**
* Stat component
*
* daisyUI classes used:
* - stat, stat-figure, stat-title, stat-value, stat-desc
* - text-secondary
*/
export const Stat = (props) => {
const { class: className, icon, label, value, desc, ...rest } = props;
return h("div", { ...rest, class: ui('stat', className) }, [
icon && h("div", { class: "stat-figure text-secondary" }, icon),
label && h("div", { class: "stat-title" }, label),
h("div", { class: "stat-value" }, () => val(value) ?? value),
desc && h("div", { class: "stat-desc" }, desc),
]);
};

View File

@@ -1,28 +0,0 @@
// components/Swap.js
import { h } from "sigpro";
import { ui, val } from "../utils.js";
/**
* Swap component
*
* daisyUI classes used:
* - swap, swap-on, swap-off, swap-active
* - swap-rotate, swap-flip, swap-indeterminate
*/
export const Swap = (props) => {
const { class: className, value, on, off, ...rest } = props;
return h("label", { ...rest, class: ui('swap', className) }, [
h("input", {
type: "checkbox",
checked: () => val(value),
onclick: (e) => {
if (typeof value === "function") {
value(e.target.checked);
}
}
}),
h("div", { class: "swap-on" }, on),
h("div", { class: "swap-off" }, off),
]);
};

View File

@@ -1,64 +0,0 @@
// components/Table.js
import { h, each, when} from "sigpro";
import { val, ui } from "../utils.js";
/**
* Table component
*
* daisyUI classes used:
* - table, table-zebra, table-pin-rows, table-pin-cols
* - table-xs, table-sm, table-md, table-lg
* - overflow-x-auto, w-full, bg-base-100, rounded-box, border, border-base-300
* - thead, tbody, tfoot, tr, th, td
* - hover, text-center, p-10, opacity-50
*/
export const Table = (props) => {
const { class: className, items = [], columns = [], keyFn, zebra = false, pinRows = false, empty = "nodata", ...rest } = props;
const tableClass = () => {
const zebraClass = val(zebra) ? "table-zebra" : "";
const pinRowsClass = val(pinRows) ? "table-pin-rows" : "";
return ui('table', className, zebraClass, pinRowsClass);
};
const getInternalKeyFn = keyFn || ((item, idx) => item.id || idx);
return h("div", { class: "overflow-x-auto w-full bg-base-100 rounded-box border border-base-300" }, [
h("table", { ...rest, class: tableClass }, [
h("thead", {}, [
h("tr", {},
columns.map(col => h("th", { class: col.class || "" }, col.label))
)
]),
h("tbody", {}, [
each(items, (item, index) => {
const it = () => {
const currentItems = val(items);
const key = getInternalKeyFn(item, index);
return currentItems.find((u, i) => getInternalKeyFn(u, i) === key) || item;
};
return h("tr", { class: "hover" },
columns.map(col => {
const cellContent = () => {
const latestItem = it();
if (col.render) return col.render(latestItem, index);
return val(latestItem[col.key]);
};
return h("td", { class: col.class || "" }, [cellContent]);
})
);
}, getInternalKeyFn),
when(() => val(items).length === 0, () =>
h("tr", {}, [
h("td", { colspan: columns.length, class: "text-center p-10 opacity-50" }, [
val(empty)
])
])
)
])
])
]);
};

View File

@@ -1,105 +0,0 @@
// components/Tabs.js
import { $, h, watch } from "sigpro";
import { val, ui, getIcon } from "../utils.js";
/**
* Tabs component
*
* daisyUI classes used:
* - tabs, tabs-box, tabs-lift, tabs-border
* - tab, tab-content
* - bg-base-100, border-base-300, p-6
*/
export const Tabs = (props) => {
const { items, class: className, onTabClose, ...rest } = props;
const itemsSignal = typeof items === "function" ? items : () => items || [];
const activeIndex = $(0);
watch(() => {
const list = itemsSignal();
const idx = list.findIndex(it => val(it.active) === true);
if (idx !== -1 && activeIndex() !== idx) {
activeIndex(idx);
}
});
const removeTab = (indexToRemove, item) => {
if (item.onClose) item.onClose(item);
if (onTabClose) onTabClose(item, indexToRemove);
const currentItems = itemsSignal();
const newItems = currentItems.filter((_, idx) => idx !== indexToRemove);
const isWritableSignal = typeof items === "function" && !items._isComputed;
if (!isWritableSignal) {
console.warn("Tabs: items must be a writable signal to support closable tabs");
return;
}
items(newItems);
if (newItems.length === 0) return;
let newActive = activeIndex();
if (indexToRemove < newActive) newActive--;
else if (indexToRemove === newActive) newActive = Math.min(newActive, newItems.length - 1);
activeIndex(newActive);
};
return h("div", { ...rest, class: ui('tabs', className) }, () => {
const list = itemsSignal();
const elements = [];
for (let i = 0; i < list.length; i++) {
const item = list[i];
const label = val(item.label);
const labelNode = label instanceof Node ? label : document.createTextNode(String(label));
const buttonChildren = [];
if (item.closable) {
const closeIcon = getIcon("icon-[lucide--x]");
closeIcon.classList.add("w-3.5", "h-3.5", "ml-2", "cursor-pointer", "hover:opacity-70");
closeIcon.onclick = (e) => {
e.stopPropagation();
removeTab(i, item);
};
const wrapper = h("span", { class: "flex items-center" }, [labelNode, closeIcon]);
buttonChildren.push(wrapper);
} else {
buttonChildren.push(labelNode);
}
const buttonBase = h("button", {
class: () => ui("tab", activeIndex() === i ? "tab-active" : ""),
onclick: (e) => {
e.preventDefault();
if (!val(item.disabled)) {
if (item.onclick) item.onclick();
activeIndex(i);
}
}
}, buttonChildren);
const button = item.tip
? h("div", { class: "tooltip", "data-tip": item.tip }, buttonBase)
: buttonBase;
elements.push(button);
let contentNode;
const rawContent = val(item.content);
if (typeof rawContent === "function") {
contentNode = rawContent();
} else if (rawContent instanceof Node) {
contentNode = rawContent;
} else {
contentNode = document.createTextNode(String(rawContent));
}
const inner = h("div", { class: "tab-content-inner" }, contentNode);
const panel = h("div", {
class: "tab-content bg-base-100 border-base-300 p-6",
style: () => activeIndex() === i ? "display: block" : "display: none"
}, inner);
elements.push(panel);
}
return elements;
});
};

View File

@@ -1,59 +0,0 @@
// components/Timeline.js
import { h } from "sigpro";
import { val, ui, getIcon } from "../utils.js";
/**
* Timeline component
*
* daisyUI classes used:
* - timeline, timeline-vertical, timeline-horizontal, timeline-compact
* - timeline-start, timeline-middle, timeline-end, timeline-box
* - flex-1, shadow-sm
* - hr, bg-primary
*/
export const Timeline = (props) => {
const { class: className, items = [], vertical = true, compact = false, ...rest } = props;
const iconMap = {
info: "icon-[lucide--info]",
success: "icon-[lucide--check-circle]",
warning: "icon-[lucide--alert-triangle]",
error: "icon-[lucide--alert-circle]",
};
return h(
"ul",
{
...rest,
class: () => ui(
`timeline ${val(vertical) ? "timeline-vertical" : "timeline-horizontal"} ${val(compact) ? "timeline-compact" : ""}`,
className
),
},
() => {
const list = (typeof items === "function" ? items() : items) || [];
return list.map((item, i) => {
const isFirst = i === 0;
const isLast = i === list.length - 1;
const itemType = item.type || "success";
const isCompleted = () => val(item.completed);
// Nueva lógica: La línea de entrada se pinta si el ANTERIOR estaba completado
const prevCompleted = () => i > 0 && val(list[i - 1].completed);
const renderSlot = (content) => (typeof content === "function" ? content() : content);
return h("li", { class: "flex-1" }, [
!isFirst ? h("hr", { class: () => prevCompleted() ? "bg-primary" : "" }) : null,
h("div", { class: "timeline-start" }, [() => renderSlot(item.title)]),
h("div", { class: "timeline-middle" }, [
() => item.icon ? getIcon(item.icon) : getIcon(iconMap[itemType] || iconMap.success)
]),
h("div", { class: "timeline-end timeline-box shadow-sm" }, [() => renderSlot(item.detail)]),
!isLast ? h("hr", { class: () => isCompleted() ? "bg-primary" : "" }) : null,
]);
});
}
);
};

View File

@@ -1,76 +0,0 @@
// components/Toast.js
import { h, mount } from "sigpro";
import { getIcon } from "../utils.js";
import { Button } from "./Button.js";
/**
* Toast (Imperative Function)
*
* daisyUI classes used:
* - alert, alert-soft, alert-info, alert-success, alert-warning, alert-error
* - shadow-lg, transition-all, duration-300
* - translate-x-10, opacity-0, pointer-events-auto
* - fixed, top-0, right-0, z-[9999], p-4, flex, flex-col, gap-2
* - btn, btn-xs, btn-circle, btn-ghost
*/
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 toastHost = h("div", { style: "display: contents" });
container.appendChild(toastHost);
let timeoutId;
const close = () => {
clearTimeout(timeoutId);
const el = toastHost.firstElementChild;
if (el && !el.classList.contains("opacity-0")) {
el.classList.add("translate-x-full", "opacity-0");
setTimeout(() => {
instance.destroy();
toastHost.remove();
if (!container.hasChildNodes()) container.remove();
}, 300);
} else {
instance.destroy();
toastHost.remove();
}
};
const ToastComponent = () => {
const closeIcon = getIcon("icon-[lucide--x]");
const el = h(
"div",
{
class: `alert alert-soft ${type} shadow-lg transition-all duration-300 translate-x-10 opacity-0 pointer-events-auto`,
},
[
h("span", {}, [typeof message === "function" ? message() : message]),
button({
class: "btn-xs btn-circle btn-ghost",
onclick: close
}, closeIcon)
],
);
requestAnimationFrame(() => el.classList.remove("translate-x-10", "opacity-0"));
return el;
};
const instance = mount(ToastComponent, toastHost);
if (duration > 0) {
timeoutId = setTimeout(close, duration);
}
return close;
};

View File

@@ -1,19 +0,0 @@
// components/Tooltip.js
import { h } from "sigpro";
import { ui } from "../utils.js";
/**
* Tooltip component
*
* daisyUI classes used:
* - 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
*/
export const Tooltip = (props, children) =>
h("div", {
...props,
class: () => ui('tooltip w-full', props.class),
"data-tip": props.tip,
}, children)