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

@@ -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;
});
};