Tabs Con pestañas cerrables

This commit is contained in:
2026-04-13 16:21:42 +02:00
parent 0697b4b4b7
commit 3c3938b354
12 changed files with 362 additions and 140 deletions

View File

@@ -4544,6 +4544,9 @@
width: calc(var(--spacing) * 6);
height: calc(var(--spacing) * 6);
}
.h-3\.5 {
height: calc(var(--spacing) * 3.5);
}
.h-8 {
height: calc(var(--spacing) * 8);
}
@@ -4618,6 +4621,9 @@
width: 100%;
}
}
.w-3\.5 {
width: calc(var(--spacing) * 3.5);
}
.w-8 {
width: calc(var(--spacing) * 8);
}
@@ -6769,6 +6775,13 @@
}
}
}
.hover\:opacity-70 {
&:hover {
@media (hover: hover) {
opacity: 70%;
}
}
}
.hover\:opacity-100 {
&:hover {
@media (hover: hover) {
@@ -6857,6 +6870,9 @@
}
}
}
:root {
font-size: 14px;
}
.input, .select, .textarea {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
&:focus, &:focus-within {

2
css/sigpro.min.css vendored

File diff suppressed because one or more lines are too long

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

@@ -579,11 +579,11 @@ var exports_utils = {};
__export(exports_utils, {
val: () => val,
ui: () => ui,
getIcon: () => getIcon
getIcon: () => getIcon2
});
var val = (t) => typeof t === "function" ? t() : t;
var ui = (baseClass, additionalClassOrFn) => typeof additionalClassOrFn === "function" ? () => `${baseClass} ${additionalClassOrFn() || ""}`.trim() : `${baseClass} ${additionalClassOrFn || ""}`.trim();
var getIcon = (icon) => {
var getIcon2 = (icon) => {
if (!icon)
return null;
if (typeof icon === "function") {
@@ -644,7 +644,7 @@ var Alert = (props, children) => {
role: "alert",
class: ui("alert", allClasses)
}, () => [
getIcon(iconMap[type]),
getIcon2(iconMap[type]),
Tag("div", { class: "flex-1" }, [
Tag("span", {}, [typeof content === "function" ? content() : content])
]),
@@ -713,8 +713,8 @@ var Input = (props) => {
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 leftIcon = icon ? getIcon2(icon) : iconMap[type] ? getIcon2(iconMap[type]) : null;
const getPasswordIcon = () => getIcon2(visible() ? "icon-[lucide--eye-off]" : "icon-[lucide--eye]");
const paddingLeft = leftIcon ? "pl-10" : "";
const paddingRight = isPassword ? "pr-10" : "";
const buttonSize = () => {
@@ -871,7 +871,7 @@ __export(exports_Button, {
});
var Button = (props, children) => {
const { class: className, loading, icon, ...rest } = props;
const iconEl = getIcon(icon);
const iconEl = getIcon2(icon);
return Tag("button", {
...rest,
class: ui("btn", className),
@@ -1067,7 +1067,7 @@ var Datepicker = (props) => {
placeholder: placeholder || (isRangeMode() ? "Seleccionar rango..." : "Seleccionar fecha..."),
value: displayValue,
readonly: true,
icon: getIcon("icon-[lucide--calendar]"),
icon: getIcon2("icon-[lucide--calendar]"),
onclick: (e) => {
e.stopPropagation();
isOpen(!isOpen());
@@ -1080,15 +1080,15 @@ var Datepicker = (props) => {
}, [
Tag("div", { class: "flex justify-between items-center mb-4 gap-1" }, [
Tag("div", { class: "flex gap-0.5" }, [
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(-1) }, getIcon("icon-[lucide--chevrons-left]")),
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(-1) }, getIcon("icon-[lucide--chevron-left]"))
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(-1) }, getIcon2("icon-[lucide--chevrons-left]")),
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(-1) }, getIcon2("icon-[lucide--chevron-left]"))
]),
Tag("span", { class: "font-bold uppercase flex-1 text-center" }, [
() => internalDate().toLocaleString("es-ES", { month: "short", year: "numeric" })
]),
Tag("div", { class: "flex gap-0.5" }, [
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(1) }, getIcon("icon-[lucide--chevron-right]")),
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(1) }, getIcon("icon-[lucide--chevrons-right]"))
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(1) }, getIcon2("icon-[lucide--chevron-right]")),
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(1) }, getIcon2("icon-[lucide--chevrons-right]"))
])
]),
Tag("div", { class: "grid grid-cols-7 gap-1", onmouseleave: () => hoverDate(null) }, [
@@ -1296,7 +1296,7 @@ var Fab = (props) => {
role: "button",
class: "btn btn-lg btn-circle btn-primary shadow-2xl"
}, [
icon ? getIcon(icon) : null,
icon ? getIcon2(icon) : null,
!icon && label ? label : null
]),
...val(actions).map((act) => Tag("div", { class: "flex items-center gap-3 transition-all duration-300" }, [
@@ -1308,7 +1308,7 @@ var Fab = (props) => {
e.stopPropagation();
act.onclick?.(e);
}
}, [act.icon ? getIcon(act.icon) : act.text || ""])
}, [act.icon ? getIcon2(act.icon) : act.text || ""])
]))
]);
};
@@ -1383,7 +1383,7 @@ var Fileinput = (props) => {
}
}, [
Tag("div", { class: "flex items-center gap-3 w-full" }, [
getIcon("icon-[lucide--upload]"),
getIcon2("icon-[lucide--upload]"),
Tag("span", { class: "text-sm opacity-70 truncate grow text-left" }, "Arrastra o selecciona archivos..."),
Tag("span", { class: "text-[10px] opacity-40 shrink-0" }, `Máx ${max}MB`)
]),
@@ -1412,7 +1412,7 @@ var Fileinput = (props) => {
e.stopPropagation();
removeFile(index);
}
}, [getIcon("icon-[lucide--x]")])
}, [getIcon2("icon-[lucide--x]")])
]), (file) => file.name + file.lastModified)
]))
]);
@@ -1764,18 +1764,57 @@ var Tabs = (props) => {
const itemsSignal = typeof items === "function" ? items : () => items || [];
const activeIndex = $(0);
Watch(() => {
const idx = itemsSignal().findIndex((it) => val(it.active) === true);
if (idx !== -1 && idx !== activeIndex())
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();
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 Tag("div", { ...rest, class: ui("tabs", className) }, () => {
const list = itemsSignal();
const elements = [];
for (let i = 0;i < list.length; i++) {
const item = list[i];
const isActive = () => activeIndex() === 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 = Tag("span", { class: "flex items-center" }, [labelNode, closeIcon]);
buttonChildren.push(wrapper);
} else {
buttonChildren.push(labelNode);
}
const button = Tag("button", {
class: () => ui("tab", isActive() ? "tab-active" : ""),
class: () => {
const isActive = activeIndex() === i;
return ui("tab", isActive ? "tab-active" : "");
},
onclick: (e) => {
e.preventDefault();
if (!val(item.disabled)) {
@@ -1783,21 +1822,24 @@ var Tabs = (props) => {
item.onclick();
activeIndex(i);
}
}
});
const label = val(item.label);
if (label instanceof Node) {
button.replaceChildren(label);
} else {
button.textContent = String(label);
}
},
title: item.tip || ""
}, buttonChildren);
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 = Tag("div", { class: "tab-content-inner" }, contentNode);
const panel = Tag("div", {
class: "tab-content bg-base-100 border-base-300 p-6",
style: () => isActive() ? "display: block" : "display: none"
}, [
Tag("div", { class: "tab-content-inner" }, () => val(item.content))
]);
style: () => activeIndex() === i ? "display: block" : "display: none"
}, inner);
elements.push(panel);
}
return elements;
@@ -1833,7 +1875,7 @@ var Timeline = (props) => {
!isFirst ? Tag("hr", { class: () => prevCompleted() ? "bg-primary" : "" }) : null,
Tag("div", { class: "timeline-start" }, [() => renderSlot(item.title)]),
Tag("div", { class: "timeline-middle" }, [
() => item.icon ? getIcon(item.icon) : getIcon(iconMap[itemType] || iconMap.success)
() => item.icon ? getIcon2(item.icon) : getIcon2(iconMap[itemType] || iconMap.success)
]),
Tag("div", { class: "timeline-end timeline-box shadow-sm" }, [() => renderSlot(item.detail)]),
!isLast ? Tag("hr", { class: () => isCompleted() ? "bg-primary" : "" }) : null
@@ -1876,7 +1918,7 @@ var Toast = (message, type = "alert-success", duration = 3500) => {
}
};
const ToastComponent = () => {
const closeIcon = getIcon("icon-[lucide--x]");
const closeIcon = getIcon2("icon-[lucide--x]");
const el = Tag("div", {
class: `alert alert-soft ${type} shadow-lg transition-all duration-300 translate-x-10 opacity-0 pointer-events-auto`
}, [
@@ -1987,7 +2029,7 @@ export {
val,
ui,
tt,
getIcon,
getIcon2 as getIcon,
Watch2 as Watch,
Tooltip,
Toast,

File diff suppressed because one or more lines are too long

110
dist/sigpro-ui.js vendored
View File

@@ -33,7 +33,7 @@
val: () => val,
ui: () => ui,
tt: () => tt,
getIcon: () => getIcon,
getIcon: () => getIcon2,
Watch: () => Watch2,
Tooltip: () => Tooltip,
Toast: () => Toast,
@@ -645,11 +645,11 @@
__export(exports_utils, {
val: () => val,
ui: () => ui,
getIcon: () => getIcon
getIcon: () => getIcon2
});
var val = (t) => typeof t === "function" ? t() : t;
var ui = (baseClass, additionalClassOrFn) => typeof additionalClassOrFn === "function" ? () => `${baseClass} ${additionalClassOrFn() || ""}`.trim() : `${baseClass} ${additionalClassOrFn || ""}`.trim();
var getIcon = (icon) => {
var getIcon2 = (icon) => {
if (!icon)
return null;
if (typeof icon === "function") {
@@ -710,7 +710,7 @@
role: "alert",
class: ui("alert", allClasses)
}, () => [
getIcon(iconMap[type]),
getIcon2(iconMap[type]),
Tag("div", { class: "flex-1" }, [
Tag("span", {}, [typeof content === "function" ? content() : content])
]),
@@ -779,8 +779,8 @@
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 leftIcon = icon ? getIcon2(icon) : iconMap[type] ? getIcon2(iconMap[type]) : null;
const getPasswordIcon = () => getIcon2(visible() ? "icon-[lucide--eye-off]" : "icon-[lucide--eye]");
const paddingLeft = leftIcon ? "pl-10" : "";
const paddingRight = isPassword ? "pr-10" : "";
const buttonSize = () => {
@@ -937,7 +937,7 @@
});
var Button = (props, children) => {
const { class: className, loading, icon, ...rest } = props;
const iconEl = getIcon(icon);
const iconEl = getIcon2(icon);
return Tag("button", {
...rest,
class: ui("btn", className),
@@ -1133,7 +1133,7 @@
placeholder: placeholder || (isRangeMode() ? "Seleccionar rango..." : "Seleccionar fecha..."),
value: displayValue,
readonly: true,
icon: getIcon("icon-[lucide--calendar]"),
icon: getIcon2("icon-[lucide--calendar]"),
onclick: (e) => {
e.stopPropagation();
isOpen(!isOpen());
@@ -1146,15 +1146,15 @@
}, [
Tag("div", { class: "flex justify-between items-center mb-4 gap-1" }, [
Tag("div", { class: "flex gap-0.5" }, [
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(-1) }, getIcon("icon-[lucide--chevrons-left]")),
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(-1) }, getIcon("icon-[lucide--chevron-left]"))
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(-1) }, getIcon2("icon-[lucide--chevrons-left]")),
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(-1) }, getIcon2("icon-[lucide--chevron-left]"))
]),
Tag("span", { class: "font-bold uppercase flex-1 text-center" }, [
() => internalDate().toLocaleString("es-ES", { month: "short", year: "numeric" })
]),
Tag("div", { class: "flex gap-0.5" }, [
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(1) }, getIcon("icon-[lucide--chevron-right]")),
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(1) }, getIcon("icon-[lucide--chevrons-right]"))
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(1) }, getIcon2("icon-[lucide--chevron-right]")),
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(1) }, getIcon2("icon-[lucide--chevrons-right]"))
])
]),
Tag("div", { class: "grid grid-cols-7 gap-1", onmouseleave: () => hoverDate(null) }, [
@@ -1362,7 +1362,7 @@
role: "button",
class: "btn btn-lg btn-circle btn-primary shadow-2xl"
}, [
icon ? getIcon(icon) : null,
icon ? getIcon2(icon) : null,
!icon && label ? label : null
]),
...val(actions).map((act) => Tag("div", { class: "flex items-center gap-3 transition-all duration-300" }, [
@@ -1374,7 +1374,7 @@
e.stopPropagation();
act.onclick?.(e);
}
}, [act.icon ? getIcon(act.icon) : act.text || ""])
}, [act.icon ? getIcon2(act.icon) : act.text || ""])
]))
]);
};
@@ -1449,7 +1449,7 @@
}
}, [
Tag("div", { class: "flex items-center gap-3 w-full" }, [
getIcon("icon-[lucide--upload]"),
getIcon2("icon-[lucide--upload]"),
Tag("span", { class: "text-sm opacity-70 truncate grow text-left" }, "Arrastra o selecciona archivos..."),
Tag("span", { class: "text-[10px] opacity-40 shrink-0" }, `Máx ${max}MB`)
]),
@@ -1478,7 +1478,7 @@
e.stopPropagation();
removeFile(index);
}
}, [getIcon("icon-[lucide--x]")])
}, [getIcon2("icon-[lucide--x]")])
]), (file) => file.name + file.lastModified)
]))
]);
@@ -1830,18 +1830,57 @@
const itemsSignal = typeof items === "function" ? items : () => items || [];
const activeIndex = $(0);
Watch(() => {
const idx = itemsSignal().findIndex((it) => val(it.active) === true);
if (idx !== -1 && idx !== activeIndex())
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();
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 Tag("div", { ...rest, class: ui("tabs", className) }, () => {
const list = itemsSignal();
const elements = [];
for (let i = 0;i < list.length; i++) {
const item = list[i];
const isActive = () => activeIndex() === 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 = Tag("span", { class: "flex items-center" }, [labelNode, closeIcon]);
buttonChildren.push(wrapper);
} else {
buttonChildren.push(labelNode);
}
const button = Tag("button", {
class: () => ui("tab", isActive() ? "tab-active" : ""),
class: () => {
const isActive = activeIndex() === i;
return ui("tab", isActive ? "tab-active" : "");
},
onclick: (e) => {
e.preventDefault();
if (!val(item.disabled)) {
@@ -1849,21 +1888,24 @@
item.onclick();
activeIndex(i);
}
}
});
const label = val(item.label);
if (label instanceof Node) {
button.replaceChildren(label);
} else {
button.textContent = String(label);
}
},
title: item.tip || ""
}, buttonChildren);
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 = Tag("div", { class: "tab-content-inner" }, contentNode);
const panel = Tag("div", {
class: "tab-content bg-base-100 border-base-300 p-6",
style: () => isActive() ? "display: block" : "display: none"
}, [
Tag("div", { class: "tab-content-inner" }, () => val(item.content))
]);
style: () => activeIndex() === i ? "display: block" : "display: none"
}, inner);
elements.push(panel);
}
return elements;
@@ -1899,7 +1941,7 @@
!isFirst ? Tag("hr", { class: () => prevCompleted() ? "bg-primary" : "" }) : null,
Tag("div", { class: "timeline-start" }, [() => renderSlot(item.title)]),
Tag("div", { class: "timeline-middle" }, [
() => item.icon ? getIcon(item.icon) : getIcon(iconMap[itemType] || iconMap.success)
() => item.icon ? getIcon2(item.icon) : getIcon2(iconMap[itemType] || iconMap.success)
]),
Tag("div", { class: "timeline-end timeline-box shadow-sm" }, [() => renderSlot(item.detail)]),
!isLast ? Tag("hr", { class: () => isCompleted() ? "bg-primary" : "" }) : null
@@ -1942,7 +1984,7 @@
}
};
const ToastComponent = () => {
const closeIcon = getIcon("icon-[lucide--x]");
const closeIcon = getIcon2("icon-[lucide--x]");
const el = Tag("div", {
class: `alert alert-soft ${type} shadow-lg transition-all duration-300 translate-x-10 opacity-0 pointer-events-auto`
}, [

File diff suppressed because one or more lines are too long

View File

@@ -275,7 +275,7 @@ const CartDrawer = () => {
Button({
class: 'btn btn-ghost btn-circle btn-sm',
onclick: () => isOpen(false)
}, '✕')
}, Span('✕'))
])
]),
Div({ class: 'flex-1 overflow-y-auto p-4' }, cart().length === 0
@@ -290,12 +290,12 @@ const CartDrawer = () => {
Button({
class: 'btn btn-xs btn-circle',
onclick: () => updateQuantity(item.id, -1)
}, '-'),
}, Span('-')),
Span({ class: 'w-8 text-center' }, item.quantity),
Button({
class: 'btn btn-xs btn-circle',
onclick: () => updateQuantity(item.id, 1)
}, '+')
}, Span('+'))
]),
Span({ class: 'font-bold w-16 text-right' }, `$${item.price * item.quantity}`)
])
@@ -313,7 +313,7 @@ const CartDrawer = () => {
Toast('Checkout initiated!', 'alert-success', 2000);
},
disabled: () => cart().length === 0
}, 'Checkout')
}, Span('Checkout'))
])
]),
content: Div({ class: 'p-4' }, [
@@ -322,7 +322,7 @@ const CartDrawer = () => {
Button({
class: 'btn btn-primary',
onclick: () => isOpen(true)
}, () => `🛒 Cart (${cart().length})`)
}, Span({ class: 'flex items-center gap-2' }, [Span('🛒'), Span(`Cart (${cart().length})`)]))
]),
Div({ class: 'mt-4 grid grid-cols-2 gap-4' }, [
Button({
@@ -331,7 +331,7 @@ const CartDrawer = () => {
cart([...cart(), { id: Date.now(), name: 'New Product', price: 39, quantity: 1 }]);
Toast('Added to cart!', 'alert-success', 1500);
}
}, ['📦', 'Add to Cart'])
}, Span({ class: 'flex flex-col items-center gap-1' }, [Span('📦'), Span('Add to Cart')]))
])
])
});

View File

@@ -416,4 +416,40 @@ const VariantsDemo = () => {
]);
};
Mount(VariantsDemo, '#demo-variants');
```
### Closable Tabs
<div class="card bg-base-200 border border-base-300 shadow-sm my-6"> <div class="card-body"> <h3 class="card-title text-sm uppercase opacity-50 mb-4">Live Demo</h3> <div id="demo-closable" class="bg-base-100 p-6 rounded-xl border border-base-300"></div> </div> </div>
```javascript
let nextTabId = 4;
const ClosableTabsDemo = () => {
const tabs = $([
{ label: 'Tab 1', content: Div('Content 1') }, // ❌ quita active: true
{ label: 'Tab 2', content: Div('Content 2'), closable: true },
{ label: 'Tab 3', content: Div('Content 3'), closable: true }
]);
// Opcional: si quieres que la primera pestaña esté activa al inicio,
// puedes hacerlo mediante una señal externa o simplemente confiar en que
// activeIndex empieza en 0 (que es el comportamiento por defecto).
const addTab = () => {
const newId = nextTabId++;
tabs([...tabs(), {
label: `Tab ${newId}`,
content: Div(`Content ${newId}`),
closable: true
}]);
};
return Div({ class: 'flex flex-col gap-4' }, [
Button({ class: 'btn btn-sm btn-outline mb-2', onclick: addTab }, 'Add Tab'),
Tabs({ items: tabs })
]);
};
Mount(ClosableTabsDemo, '#demo-closable');
```

110
docs/sigpro-ui.min.js vendored
View File

@@ -33,7 +33,7 @@
val: () => val,
ui: () => ui,
tt: () => tt,
getIcon: () => getIcon,
getIcon: () => getIcon2,
Watch: () => Watch2,
Tooltip: () => Tooltip,
Toast: () => Toast,
@@ -645,11 +645,11 @@
__export(exports_utils, {
val: () => val,
ui: () => ui,
getIcon: () => getIcon
getIcon: () => getIcon2
});
var val = (t) => typeof t === "function" ? t() : t;
var ui = (baseClass, additionalClassOrFn) => typeof additionalClassOrFn === "function" ? () => `${baseClass} ${additionalClassOrFn() || ""}`.trim() : `${baseClass} ${additionalClassOrFn || ""}`.trim();
var getIcon = (icon) => {
var getIcon2 = (icon) => {
if (!icon)
return null;
if (typeof icon === "function") {
@@ -710,7 +710,7 @@
role: "alert",
class: ui("alert", allClasses)
}, () => [
getIcon(iconMap[type]),
getIcon2(iconMap[type]),
Tag("div", { class: "flex-1" }, [
Tag("span", {}, [typeof content === "function" ? content() : content])
]),
@@ -779,8 +779,8 @@
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 leftIcon = icon ? getIcon2(icon) : iconMap[type] ? getIcon2(iconMap[type]) : null;
const getPasswordIcon = () => getIcon2(visible() ? "icon-[lucide--eye-off]" : "icon-[lucide--eye]");
const paddingLeft = leftIcon ? "pl-10" : "";
const paddingRight = isPassword ? "pr-10" : "";
const buttonSize = () => {
@@ -937,7 +937,7 @@
});
var Button = (props, children) => {
const { class: className, loading, icon, ...rest } = props;
const iconEl = getIcon(icon);
const iconEl = getIcon2(icon);
return Tag("button", {
...rest,
class: ui("btn", className),
@@ -1133,7 +1133,7 @@
placeholder: placeholder || (isRangeMode() ? "Seleccionar rango..." : "Seleccionar fecha..."),
value: displayValue,
readonly: true,
icon: getIcon("icon-[lucide--calendar]"),
icon: getIcon2("icon-[lucide--calendar]"),
onclick: (e) => {
e.stopPropagation();
isOpen(!isOpen());
@@ -1146,15 +1146,15 @@
}, [
Tag("div", { class: "flex justify-between items-center mb-4 gap-1" }, [
Tag("div", { class: "flex gap-0.5" }, [
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(-1) }, getIcon("icon-[lucide--chevrons-left]")),
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(-1) }, getIcon("icon-[lucide--chevron-left]"))
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(-1) }, getIcon2("icon-[lucide--chevrons-left]")),
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(-1) }, getIcon2("icon-[lucide--chevron-left]"))
]),
Tag("span", { class: "font-bold uppercase flex-1 text-center" }, [
() => internalDate().toLocaleString("es-ES", { month: "short", year: "numeric" })
]),
Tag("div", { class: "flex gap-0.5" }, [
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(1) }, getIcon("icon-[lucide--chevron-right]")),
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(1) }, getIcon("icon-[lucide--chevrons-right]"))
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(1) }, getIcon2("icon-[lucide--chevron-right]")),
Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(1) }, getIcon2("icon-[lucide--chevrons-right]"))
])
]),
Tag("div", { class: "grid grid-cols-7 gap-1", onmouseleave: () => hoverDate(null) }, [
@@ -1362,7 +1362,7 @@
role: "button",
class: "btn btn-lg btn-circle btn-primary shadow-2xl"
}, [
icon ? getIcon(icon) : null,
icon ? getIcon2(icon) : null,
!icon && label ? label : null
]),
...val(actions).map((act) => Tag("div", { class: "flex items-center gap-3 transition-all duration-300" }, [
@@ -1374,7 +1374,7 @@
e.stopPropagation();
act.onclick?.(e);
}
}, [act.icon ? getIcon(act.icon) : act.text || ""])
}, [act.icon ? getIcon2(act.icon) : act.text || ""])
]))
]);
};
@@ -1449,7 +1449,7 @@
}
}, [
Tag("div", { class: "flex items-center gap-3 w-full" }, [
getIcon("icon-[lucide--upload]"),
getIcon2("icon-[lucide--upload]"),
Tag("span", { class: "text-sm opacity-70 truncate grow text-left" }, "Arrastra o selecciona archivos..."),
Tag("span", { class: "text-[10px] opacity-40 shrink-0" }, `Máx ${max}MB`)
]),
@@ -1478,7 +1478,7 @@
e.stopPropagation();
removeFile(index);
}
}, [getIcon("icon-[lucide--x]")])
}, [getIcon2("icon-[lucide--x]")])
]), (file) => file.name + file.lastModified)
]))
]);
@@ -1830,18 +1830,57 @@
const itemsSignal = typeof items === "function" ? items : () => items || [];
const activeIndex = $(0);
Watch(() => {
const idx = itemsSignal().findIndex((it) => val(it.active) === true);
if (idx !== -1 && idx !== activeIndex())
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();
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 Tag("div", { ...rest, class: ui("tabs", className) }, () => {
const list = itemsSignal();
const elements = [];
for (let i = 0;i < list.length; i++) {
const item = list[i];
const isActive = () => activeIndex() === 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 = Tag("span", { class: "flex items-center" }, [labelNode, closeIcon]);
buttonChildren.push(wrapper);
} else {
buttonChildren.push(labelNode);
}
const button = Tag("button", {
class: () => ui("tab", isActive() ? "tab-active" : ""),
class: () => {
const isActive = activeIndex() === i;
return ui("tab", isActive ? "tab-active" : "");
},
onclick: (e) => {
e.preventDefault();
if (!val(item.disabled)) {
@@ -1849,21 +1888,24 @@
item.onclick();
activeIndex(i);
}
}
});
const label = val(item.label);
if (label instanceof Node) {
button.replaceChildren(label);
} else {
button.textContent = String(label);
}
},
title: item.tip || ""
}, buttonChildren);
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 = Tag("div", { class: "tab-content-inner" }, contentNode);
const panel = Tag("div", {
class: "tab-content bg-base-100 border-base-300 p-6",
style: () => isActive() ? "display: block" : "display: none"
}, [
Tag("div", { class: "tab-content-inner" }, () => val(item.content))
]);
style: () => activeIndex() === i ? "display: block" : "display: none"
}, inner);
elements.push(panel);
}
return elements;
@@ -1899,7 +1941,7 @@
!isFirst ? Tag("hr", { class: () => prevCompleted() ? "bg-primary" : "" }) : null,
Tag("div", { class: "timeline-start" }, [() => renderSlot(item.title)]),
Tag("div", { class: "timeline-middle" }, [
() => item.icon ? getIcon(item.icon) : getIcon(iconMap[itemType] || iconMap.success)
() => item.icon ? getIcon2(item.icon) : getIcon2(iconMap[itemType] || iconMap.success)
]),
Tag("div", { class: "timeline-end timeline-box shadow-sm" }, [() => renderSlot(item.detail)]),
!isLast ? Tag("hr", { class: () => isCompleted() ? "bg-primary" : "" }) : null
@@ -1942,7 +1984,7 @@
}
};
const ToastComponent = () => {
const closeIcon = getIcon("icon-[lucide--x]");
const closeIcon = getIcon2("icon-[lucide--x]");
const el = Tag("div", {
class: `alert alert-soft ${type} shadow-lg transition-all duration-300 translate-x-10 opacity-0 pointer-events-auto`
}, [

File diff suppressed because one or more lines are too long

View File

@@ -15,51 +15,91 @@ export const Tabs = (props) => {
const itemsSignal = typeof items === "function" ? items : () => items || [];
const activeIndex = $(0);
// Sincroniza con active:true solo cuando cambia la lista de items
Watch(() => {
const idx = itemsSignal().findIndex(it => val(it.active) === true);
if (idx !== -1 && idx !== activeIndex()) activeIndex(idx);
const list = itemsSignal();
const idx = list.findIndex(it => val(it.active) === true);
if (idx !== -1 && activeIndex() !== idx) {
activeIndex(idx);
}
});
// Contenedor principal con las clases de DaisyUI
const removeTab = (indexToRemove, item) => {
if (item.onClose) item.onClose();
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 Tag("div", { ...rest, class: ui('tabs', className) }, () => {
const list = itemsSignal();
const elements = [];
for (let i = 0; i < list.length; i++) {
const item = list[i];
const isActive = () => activeIndex() === i;
// Botón (tab)
// --- Botón ---
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 = Tag("span", { class: "flex items-center" }, [labelNode, closeIcon]);
buttonChildren.push(wrapper);
} else {
buttonChildren.push(labelNode);
}
const button = Tag("button", {
class: () => ui("tab", isActive() ? "tab-active" : ""),
class: () => {
const isActive = activeIndex() === i;
return ui("tab", isActive ? "tab-active" : "");
},
onclick: (e) => {
e.preventDefault();
if (!val(item.disabled)) {
if (item.onclick) item.onclick();
activeIndex(i);
}
}
});
// Asignar etiqueta (puede ser texto o nodo)
const label = val(item.label);
if (label instanceof Node) {
button.replaceChildren(label);
} else {
button.textContent = String(label);
}
},
title: item.tip || ""
}, buttonChildren);
elements.push(button);
// Contenido (tab-content) - borde exterior estático
// --- Panel ---
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 = Tag("div", { class: "tab-content-inner" }, contentNode);
const panel = Tag("div", {
class: "tab-content bg-base-100 border-base-300 p-6",
style: () => isActive() ? "display: block" : "display: none"
}, [
// Contenedor interno con animación
Tag("div", { class: "tab-content-inner" }, () => val(item.content))
]);
style: () => activeIndex() === i ? "display: block" : "display: none"
}, inner);
elements.push(panel);
}
return elements;
});
};

View File

@@ -58,6 +58,10 @@
--border: 1px;
}
:root {
font-size: 14px;
}
/* Agrupamos los selectores normales de CSS */
.input, .select, .textarea {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);