UUUUPPPPP work

This commit is contained in:
2026-03-25 02:03:59 +01:00
parent c857900860
commit df8bd891a2
32 changed files with 1126 additions and 233 deletions

142
components_/AgGrid.js Normal file
View File

@@ -0,0 +1,142 @@
import {
createGrid,
ModuleRegistry,
ValidationModule,
ColumnAutoSizeModule,
CellStyleModule,
QuickFilterModule,
RowSelectionModule,
TextEditorModule,
ClientSideRowModelModule,
themeQuartz,
iconSetQuartzLight,
} from "ag-grid-community";
import {
MultiFilterModule,
CellSelectionModule,
PivotModule,
MasterDetailModule,
SideBarModule,
ColumnsToolPanelModule,
ColumnMenuModule,
StatusBarModule,
ExcelExportModule,
ClipboardModule,
} from "ag-grid-enterprise";
import { $ } from "sigpro";
import { isDark } from "../store.js";
// ✅ Registro de módulos (UNA VEZ, fuera del componente)
ModuleRegistry.registerModules([
ValidationModule,
ColumnAutoSizeModule,
CellStyleModule,
QuickFilterModule,
RowSelectionModule,
TextEditorModule,
ClientSideRowModelModule,
MultiFilterModule,
CellSelectionModule,
PivotModule,
MasterDetailModule,
SideBarModule,
ColumnsToolPanelModule,
ColumnMenuModule,
StatusBarModule,
ExcelExportModule,
ClipboardModule,
]);
const getTheme = (isDark) =>
themeQuartz.withPart(iconSetQuartzLight).withParams({
browserColorScheme: isDark ? "dark" : "light",
backgroundColor: isDark ? "#121212" : "#FDFDFD",
foregroundColor: isDark ? "#E0E0E0" : "#181D1F",
accentColor: isDark ? "#4FAAFF" : "#004B9C",
headerBackgroundColor: isDark ? "#2A2A2A" : "#EEB111",
headerTextColor: isDark ? "#4FAAFF" : "#004B9C",
borderRadius: 4,
columnBorder: false,
headerFontSize: 14,
headerFontWeight: 600,
listItemHeight: 20,
iconSize: 14,
spacing: 3,
wrapperBorderRadius: 4,
});
$.component(
"c-grid",
(props, { onUnmount }) => {
let gridApi = null;
// ✅ Crear contenedor específico para este grid
const container = document.createElement("div");
container.style.height = "100%";
const gridContainer = document.createElement("div");
gridContainer.style.height = "100%";
container.appendChild(gridContainer);
// Theme observer
const currentTheme = $(document.querySelector("[data-theme]")?.getAttribute("data-theme") || "light");
const observer = new MutationObserver(() => {
const theme = document.querySelector("[data-theme]")?.getAttribute("data-theme");
if (theme !== currentTheme()) currentTheme(theme);
});
observer.observe(document.documentElement, {
attributes: true,
subtree: true,
attributeFilter: ["data-theme"],
});
// ✅ LIMPIEZA COMPLETA
onUnmount(() => {
observer.disconnect();
// 1. Destruir el grid
if (gridApi) {
gridApi.destroy();
gridApi = null;
}
// 2. Eliminar el contenedor del DOM
if (gridContainer.parentNode) {
gridContainer.parentNode.removeChild(gridContainer);
}
// 3. Limpiar referencias
container.innerHTML = "";
});
// Efecto para tema y creación inicial
$.effect(() => {
const dark = isDark();
const agTheme = getTheme(dark);
if (!gridApi) {
gridApi = createGrid(gridContainer, {
...(props.options?.() || {}),
theme: agTheme,
rowData: props.data?.() || [],
});
} else {
gridApi.setGridOption("theme", agTheme);
}
});
// Efecto para datos
$.effect(() => {
const data = props.data?.();
if (gridApi && Array.isArray(data)) {
gridApi.setGridOption("rowData", data);
}
});
return container;
},
["data", "options"],
);

31
components_/Button.js Normal file
View File

@@ -0,0 +1,31 @@
import { $, html } from "sigpro";
$.component(
"c-button",
(props, { emit, slot }) => {
const spinner = () => html`
<span .class="${() => `loading loading-spinner loading-xs ${props.loading() ? "" : "hidden"}`}"></span>
`;
return html`
<div class="${props.tooltip() ? "tooltip" : ""}" data-tip=${() => props.tooltip() ?? ""}>
<button
class="${() => `btn ${props.cls() ?? ""} ${props.badge() ? "indicator" : ""}`}"
?disabled=${props.loading}
@click=${(e) => {
e.stopPropagation();
if (!props.loading()) emit("click", e);
}}>
${spinner} ${slot()}
${() =>
props.badge()
? html`
<span class="indicator-item badge badge-secondary">${props.badge()}</span>
`
: null}
</button>
</div>
`;
},
["cls", "loading", "badge", "tooltip"],
);

26
components_/Card.js Normal file
View File

@@ -0,0 +1,26 @@
import { $, html } from "sigpro";
$.component(
"c-card",
(props, host) => {
return html`
<div class="${() => `card bg-base-100 shadow-sm ${props.class || ""}`}">
${() =>
props.img
? html`
<figure>
<img src="${props.img}" alt="${props.alt || "Card image"}" />
</figure>
`
: null}
<div class="card-body">
<h2 class="card-title">${host.slot("title")}</h2>
<div class="card-content">${host.slot("body")}</div>
<div class="card-actions justify-end">${host.slot("actions")}</div>
</div>
</div>
`;
},
["img", "alt", "class"],
);

27
components_/Checkbox.js Normal file
View File

@@ -0,0 +1,27 @@
import { $ html } from "sigpro";
$.component(
"c-check",
(props, host) => {
return html`
<label class="label cursor-pointer flex gap-2">
<input
type="checkbox"
@change="${(e) => {
e.stopPropagation();
emit("change", e.target.checked);
}}"
.checked="${() => props.checked()}"
.disabled="${() => props.disabled()}"
class="${cls(props.toggle ? "toggle" : "checkbox")}" />
${() =>
props.label
? html`
<span class="label-text">${props.label}</span>
`
: ""}
</label>
`;
},
["checked", "label", "class", "disabled", "toggle"],
);

View File

@@ -0,0 +1,65 @@
import { $, html } from "sigpro";
const p1 = ["#000", "#1A1A1A", "#333", "#4D4D4D", "#666", "#808080", "#B3B3B3", "#FFF"];
const p2 = ["#450a0a", "#7f1d1d", "#991b1b", "#b91c1c", "#dc2626", "#ef4444", "#f87171", "#fca5a5"];
const p3 = ["#431407", "#7c2d12", "#9a3412", "#c2410c", "#ea580c", "#f97316", "#fb923c", "#ffedd5"];
const p4 = ["#713f12", "#a16207", "#ca8a04", "#eab308", "#facc15", "#fde047", "#fef08a", "#fff9c4"];
const p5 = ["#064e3b", "#065f46", "#059669", "#10b981", "#34d399", "#4ade80", "#84cc16", "#d9f99d"];
const p6 = ["#082f49", "#075985", "#0284c7", "#0ea5e9", "#38bdf8", "#7dd3fc", "#22d3ee", "#cffafe"];
const p7 = ["#1e1b4b", "#312e81", "#4338ca", "#4f46e5", "#6366f1", "#818cf8", "#a5b4fc", "#e0e7ff"];
const p8 = ["#2e1065", "#4c1d95", "#6d28d9", "#7c3aed", "#8b5cf6", "#a855f7", "#d946ef", "#fae8ff"];
const palette = [...p1, ...p2, ...p3, ...p4, ...p5, ...p6, ...p7, ...p8];
$.component(
"c-colorpicker",
(props, { emit }) => {
const handleSelect = (c) => {
if (typeof props.color === "function") props.color(c);
emit("select", c);
};
const getColor = () => props.color() ?? "#000000";
return html`
<div class="card bg-base-200 border-base-300 w-fit border p-2 shadow-sm select-none">
<div class="grid grid-cols-8 gap-0.5">
${() =>
palette.map(
(c) => html`
<button
type="button"
.style=${`background-color: ${c}`}
.class=${() => {
const active = getColor() === c;
return `size-5 rounded-xs cursor-pointer transition-all hover:scale-125 hover:z-10 active:scale-90 outline-none border border-black/5 ${
active ? "ring-2 ring-offset-1 ring-primary z-10 scale-110" : ""
}`;
}}
@click=${() => handleSelect(c)}></button>
`,
)}
</div>
<div class="flex items-center gap-1 mt-2">
<input
type="text"
class="input input-bordered input-xs h-6 px-1 font-mono text-[10px] w-full"
.value=${props.color}
@input=${(e) => handleSelect(e.target.value)} />
<div class="tooltip" data-tip="Copiar">
<button
type="button"
class="btn btn-xs btn-square border border-base-content/20 shadow-inner"
.style=${() => `background-color: ${getColor()}`}
@click=${() => navigator.clipboard.writeText(getColor())}>
<span class="icon-[lucide--copy] text-white mix-blend-difference"></span>
</button>
</div>
</div>
</div>
`;
},
["color"],
);

168
components_/DatePicker.js Normal file
View File

@@ -0,0 +1,168 @@
import { $, html } from "sigpro";
$.component(
"c-datepicker",
(props, { emit }) => {
const viewDate = $(new Date());
const hoveredDate = $(null);
const todayISO = new Date().toLocaleDateString("en-CA");
const toISOLocal = (date) => {
if (!date) return null;
return date.toISOString().split("T")[0];
};
// Función unificada para navegar tiempo
const navigate = (type, offset) => {
hoveredDate(null);
const d = viewDate();
if (type === "month") {
viewDate(new Date(d.getFullYear(), d.getMonth() + offset, 1));
} else if (type === "year") {
viewDate(new Date(d.getFullYear() + offset, d.getMonth(), 1));
}
};
const selectDate = (dateObj) => {
const isoDate = toISOLocal(dateObj);
const isRange = props.range() === "true" || props.range() === true;
const currentVal = typeof props.value === "function" ? props.value() : props.value;
let result;
if (!isRange) {
result = isoDate;
} else {
const s = currentVal?.start || null;
const e = currentVal?.end || null;
if (!s || (s && e)) {
result = { start: isoDate, end: null };
} else {
result = isoDate < s ? { start: isoDate, end: s } : { start: s, end: isoDate };
}
}
if (typeof props.value === "function") {
props.value(isRange ? { ...result } : result);
}
emit("change", result);
};
const handleGridClick = (e) => {
const btn = e.target.closest("button[data-date]");
if (!btn) return;
selectDate(new Date(btn.getAttribute("data-date")));
};
const days = $(() => {
const d = viewDate();
const year = d.getFullYear();
const month = d.getMonth();
const firstDay = new Date(year, month, 1).getDay();
const offset = firstDay === 0 ? 6 : firstDay - 1;
const total = new Date(year, month + 1, 0).getDate();
let grid = Array(offset).fill(null);
for (let i = 1; i <= total; i++) grid.push(new Date(year, month, i));
return grid;
});
const getWeekNumber = (d) => {
const t = new Date(d.valueOf());
t.setDate(t.getDate() - ((d.getDay() + 6) % 7) + 3);
const firstThurs = t.valueOf();
t.setMonth(0, 1);
if (t.getDay() !== 4) t.setMonth(0, 1 + ((4 - t.getDay() + 7) % 7));
return 1 + Math.ceil((firstThurs - t.getTime()) / 604800000);
};
return html`
<div class="card bg-base-100 shadow-xl border border-base-300 w-80 p-4 pb-6 rounded-box select-none">
<div class="flex justify-between items-center mb-4 gap-1">
<div class="flex gap-0.5">
<button type="button" class="btn btn-ghost btn-xs px-1" @click=${() => navigate("year", -1)}>
<span class="icon-[lucide--chevrons-left] w-4 h-4 opacity-50"></span>
</button>
<button type="button" class="btn btn-ghost btn-xs px-1" @click=${() => navigate("month", -1)}>
<span class="icon-[lucide--chevron-left] w-4 h-4"></span>
</button>
</div>
<span class="text-xs font-bold capitalize flex-1 text-center">
${() => viewDate().toLocaleString("es-ES", { month: "long" }).toUpperCase()}
<span class="opacity-50 ml-1">${() => viewDate().getFullYear()}</span>
</span>
<div class="flex gap-0.5">
<button type="button" class="btn btn-ghost btn-xs px-1" @click=${() => navigate("month", 1)}>
<span class="icon-[lucide--chevron-right] w-4 h-4"></span>
</button>
<button type="button" class="btn btn-ghost btn-xs px-1" @click=${() => navigate("year", 1)}>
<span class="icon-[lucide--chevrons-right] w-4 h-4 opacity-50"></span>
</button>
</div>
</div>
<div class="grid grid-cols-8 gap-1 px-1" @click=${handleGridClick}>
<div class="flex items-center justify-center text-[10px] opacity-40 font-bold uppercase"></div>
${() =>
["L", "M", "X", "J", "V", "S", "D"].map(
(l) => html`
<div class="flex items-center justify-center text-[10px] opacity-40 font-bold uppercase">${l}</div>
`,
)}
${() =>
days().map((date, i) => {
const isFirstCol = i % 7 === 0;
const iso = date ? toISOLocal(date) : null;
const btnClass = () => {
if (!date) return "";
const val = typeof props.value === "function" ? props.value() : props.value;
const isR = props.range() === "true" || props.range() === true;
const sDate = isR ? val?.start : typeof val === "string" ? val : val?.start;
const eDate = isR ? val?.end : null;
const hDate = hoveredDate();
const isSel = iso === sDate || iso === eDate;
const tEnd = eDate || hDate;
const inRange = isR && sDate && tEnd && !isSel && ((iso > sDate && iso < tEnd) || (iso < sDate && iso > tEnd));
return `btn btn-xs p-0 aspect-square min-h-0 h-auto font-normal rounded-md relative
${isSel ? "btn-primary !text-primary-content shadow-md" : "btn-ghost"}
${inRange ? "!bg-primary/20 !text-base-content" : ""}`;
};
return html`
${isFirstCol
? html`
<div class="flex items-center justify-center text-[10px] opacity-30 italic bg-base-200/50 rounded-md aspect-square">
${date ? getWeekNumber(date) : days()[i + 6] ? getWeekNumber(days()[i + 6]) : ""}
</div>
`
: ""}
${date
? html`
<button
type="button"
class="${btnClass}"
data-date="${date.toISOString()}"
@mouseenter=${() => hoveredDate(iso)}
@mouseleave=${() => hoveredDate(null)}>
${iso === todayISO
? html`
<span class="absolute -inset-px border-2 border-primary/60 rounded-md pointer-events-none"></span>
`
: ""}
<span class="relative z-10 pointer-events-none">${date.getDate()}</span>
</button>
`
: html`
<div class="aspect-square"></div>
`}
`;
})}
</div>
</div>
`;
},
["range", "value"],
);

37
components_/Dialog.js Normal file
View File

@@ -0,0 +1,37 @@
import { $, html } from "sigpro";
$.component(
"c-dialog",
(props, { slot, emit }) => {
return html`
<dialog
.class=${() => `modal ${props.open() ? "modal-open" : ""}`}
.open=${props.open}
@close=${(e) => {
if (typeof props.open === "function") props.open(false);
emit("close", e);
}}>
<div class="modal-box">
<div class="flex flex-col gap-4">${slot()}</div>
<div class="modal-action">
<form method="dialog" @submit=${() => props.open(false)}>
${slot("buttons")}
${() =>
!slot("buttons").length
? html`
<button class="btn">Cerrar</button>
`
: ""}
</form>
</div>
</div>
<form method="dialog" class="modal-backdrop" @submit=${() => props.open(false)}>
<button>close</button>
</form>
</dialog>
`;
},
["open"],
);

31
components_/Drawer.js Normal file
View File

@@ -0,0 +1,31 @@
import { $, html } from "sigpro";
$.component(
"c-drawer",
(props, { emit, slot }) => {
const id = `drawer-${Math.random().toString(36).substring(2, 9)}`;
return html`
<div class="drawer">
<input
id="${id}"
type="checkbox"
class="drawer-toggle"
.checked=${props.open}
@change=${(e) => {
const isChecked = e.target.checked;
if (typeof props.open === "function") props.open(isChecked);
emit("change", isChecked);
}} />
<div class="drawer-content">${slot("content")}</div>
<div class="drawer-side z-999">
<label for="${id}" aria-label="close sidebar" class="drawer-overlay"></label>
<div class="bg-base-200 min-h-full w-80">${slot()}</div>
</div>
</div>
`;
},
["open"],
);

20
components_/Dropdown.js Normal file
View File

@@ -0,0 +1,20 @@
import { $, html } from "sigpro";
$.component(
"c-dropdown",
(props, { slot }) => {
// Generamos un ID único para el anclaje nativo
const id = props.id ?? `pop-${Math.random().toString(36).slice(2, 7)}`;
return html`
<div class="inline-block">
<button class="btn" popovertarget="${id}" style="anchor-name: --${id}">${slot("trigger")}</button>
<div popover id="${id}" style="position-anchor: --${id}" class="dropdown menu bg-base-100 rounded-box shadow-sm border border-base-300">
${slot()}
</div>
</div>
`;
},
["id"],
);

37
components_/Fab.js Normal file
View File

@@ -0,0 +1,37 @@
import { $, html } from "sigpro";
$.component(
"c-fab",
(props, { emit }) => {
const handleClick = (e, item) => {
if (item.onclick) item.onclick(e);
emit("select", item);
if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
};
return html`
<div class="dropdown dropdown-top dropdown-end fixed bottom-6 right-6 z-100">
<div tabindex="0" role="button" .class=${() => `btn btn-lg btn-circle btn-primary shadow-2xl ${props.cls() ?? ""}`}>
<span class="${() => props["main-icon"]() || "icon-[lucide--plus]"} w-6 h-6"></span>
</div>
<ul tabindex="0" class="dropdown-content menu mb-4 p-0 flex flex-col gap-3 items-center">
${() =>
(props.actions() || []).map(
(item) => html`
<li class="p-0">
<button
.class=${() => `btn btn-circle shadow-lg ${item.cls || "btn-secondary"}`}
@click=${(e) => handleClick(e, item)}
.title=${item.label}>
<span class="${item.icon} w-5 h-5"></span>
</button>
</li>
`,
)}
</ul>
</div>
`;
},
["main-icon", "actions", "cls"],
);

26
components_/Input.js Normal file
View File

@@ -0,0 +1,26 @@
import { $, html } from "sigpro";
$.component(
"c-input",
(props, { slot, emit }) => {
return html`
<div class="${props.tooltip() ? "tooltip" : ""}" data-tip=${() => props.tooltip() ?? ""}>
<label class="floating-label">
<span>${() => props.label() ?? ""}</span>
<label class=${() => `input ${props.cls() ?? ""}`}>
<input
type=${() => props.type() ?? "text"}
class="input"
:value=${props.value}
placeholder=${() => props.place() ?? props.label() ?? ""}
@input=${(e) => emit("input", e.target.value)}
@change=${(e) => emit("change", e.target.value)} />
<span>${slot("icon-action")}</span>
<span class=${() => props.icon() ?? ""}></span>
</label>
</label>
</div>
`;
},
["label", "value", "icon", "tooltip", "cls", "place", "type"],
);

17
components_/InputClear.js Normal file
View File

@@ -0,0 +1,17 @@
import { $, html } from "sigpro";
$.component(
"c-input-clear",
(props) => {
return html`
<button
type="button"
class="btn btn-ghost btn-xs btn-circle opacity-50 hover:opacity-100"
?hidden=${() => !props.value() || props.value().length === 0}
@click=${() => props.value("")}>
<span class="icon-[lucide--x] size-4"></span>
</button>
`;
},
["value"],
);

13
components_/InputView.js Normal file
View File

@@ -0,0 +1,13 @@
import { $, html } from "sigpro";
$.component(
"c-input-view",
(props) => {
return html`
<button type="button" class="btn btn-ghost btn-xs btn-circle opacity-50 hover:opacity-100" @click=${() => props.show(!props.show())}>
<span class="${() => (props.show() ? "icon-[lucide--eye-off]" : "icon-[lucide--eye]")} size-4"></span>
</button>
`;
},
["show"],
);

39
components_/Loading.js Normal file
View File

@@ -0,0 +1,39 @@
export const loading = (show = true, msg = "Cargando...") => {
const body = document.body;
if (!show) {
if (loadingEl) {
loadingEl.classList.replace("opacity-100", "opacity-0");
body.style.removeProperty("overflow"); // Restaurar scroll
const elToRemove = loadingEl; // Captura para el closure
elToRemove.addEventListener("transitionend", () => {
if (elToRemove === loadingEl) { // Solo si sigue siendo el actual
elToRemove.remove();
loadingEl = null;
}
}, { once: true }
);
}
return;
}
if (loadingEl?.isConnected) {
loadingEl.querySelector(".loading-text").textContent = msg;
return;
}
body.style.overflow = "hidden"; // Bloquear scroll
loadingEl = html`
<div class="fixed inset-0 z-9999 flex items-center justify-center bg-base-300/40 backdrop-blur-md transition-opacity duration-300 opacity-0 pointer-events-auto select-none">
<div class="flex flex-col items-center gap-4">
<span class="loading loading-spinner loading-lg text-primary"></span>
<span class="loading-text font-bold text-lg text-base-content">${msg}</span>
</div>
</div>
`.firstElementChild;
body.appendChild(loadingEl);
requestAnimationFrame(() => loadingEl.classList.replace("opacity-0", "opacity-100"));
};

57
components_/Menu.js Normal file
View File

@@ -0,0 +1,57 @@
import { $, html } from "sigpro";
$.component(
"c-menu",
(props, { emit }) => {
const getItems = () => props.items() || [];
const renderItems = (data) => {
return data.map((item) => {
const hasChildren = item.sub && item.sub.length > 0;
const content = html`
${item.icon
? html`
<span class="${item.icon} h-4 w-4"></span>
`
: ""}
<span>${item.label}</span>
`;
if (hasChildren) {
return html`
<li>
<details .open="${!!item.open}">
<summary>${content}</summary>
<ul>
${renderItems(item.sub)}
</ul>
</details>
</li>
`;
}
return html`
<li>
<a
href="${item.href || "#"}"
.class=${item.active ? "active" : ""}
@click="${(e) => {
if (!item.href || item.href === "#") e.preventDefault();
if (item.onClick) item.onClick(item);
emit("select", item);
}}">
${content}
</a>
</li>
`;
});
};
return html`
<ul .class=${() => `menu bg-base-200 rounded-box w-full ${props.cls() ?? ""}`}>
${() => renderItems(getItems())}
</ul>
`;
},
["items", "cls"],
);

28
components_/Radio.js Normal file
View File

@@ -0,0 +1,28 @@
import { $.component, html } from "sigpro";
$.component(
"c-radio",
(props, { emit }) => {
return html`
<label class="label cursor-pointer flex justify-start gap-4">
<input
type="radio"
.name=${props.name}
.value=${props.value}
.class=${() => `radio ${props.cls() ?? ""}`}
.disabled=${props.disabled}
.checked=${props.checked}
@change=${(e) => {
if (e.target.checked) emit("change", props.value());
}} />
${() =>
props.label()
? html`
<span class="label-text">${props.label}</span>
`
: ""}
</label>
`;
},
["checked", "name", "label", "cls", "disabled", "value"],
);

24
components_/Range.js Normal file
View File

@@ -0,0 +1,24 @@
import { $, html } from "sigpro";
$.component(
"c-range",
(props, { emit }) => {
return html`
<input
type="range"
.min=${() => props.min() ?? 0}
.max=${() => props.max() ?? 100}
.step=${() => props.step() ?? 1}
.value=${props.value}
.class=${() => `range ${props.cls() ?? ""}`}
@input=${(e) => {
const val = e.target.value;
if (typeof props.value === "function") props.value(val);
emit("input", val);
emit("change", val);
}} />
`;
},
["cls", "value", "min", "max", "step"],
);

34
components_/Rating.js Normal file
View File

@@ -0,0 +1,34 @@
import { $, html } from "sigpro";
$.component(
"c-rating",
(props, { emit }) => {
const count = () => parseInt(props.count() ?? 5);
const getVal = () => {
const v = props.value();
return v === false || v == null ? 0 : Number(v);
};
return html`
<div .class=${() => `rating ${props.mask() ?? ""}`}>
${() =>
Array.from({ length: count() }).map((_, i) => {
const radioValue = i + 1;
return html`
<input
type="radio"
.name=${props.name}
.class=${() => `mask ${props.mask() ?? "mask-star"}`}
.checked=${() => getVal() === radioValue}
@change=${() => {
if (typeof props.value === "function") props.value(radioValue);
emit("change", radioValue);
}} />
`;
})}
</div>
`;
},
["value", "count", "name", "mask"],
);

38
components_/Tab.js Normal file
View File

@@ -0,0 +1,38 @@
import { $, html } from "sigpro";
$.component(
"c-tab",
(props, { emit, slot }) => {
const groupName = `tab-group-${Math.random().toString(36).substring(2, 9)}`;
const items = () => props.items() || [];
return html`
<div .class=${() => `tabs ${props.class() ?? "tabs-lifted"}`}>
${() =>
items().map(
(item) => html`
<input
type="radio"
name="${groupName}"
class="tab"
aria-label="${item.label}"
.checked=${() => props.value() === item.value}
@change=${() => {
if (typeof props.value === "function") props.value(item.value);
emit("change", item.value);
}} />
<div class="tab-content bg-base-100 border-base-300 p-6">
${item.icon
? html`
<span class="${item.icon} mr-2"></span>
`
: ""}
${slot(item.label)}
</div>
`,
)}
</div>
`;
},
["items", "value", "class"],
);

49
components_/Toast.js Normal file
View File

@@ -0,0 +1,49 @@
import { html } from "sigpro";
let container = null;
export const toast = (msg, type = "alert-success", ms = 3500) => {
if (!container || !container.isConnected) {
container = document.createElement("div");
container.className = "fixed top-0 right-0 z-9999 p-6 flex flex-col gap-4 pointer-events-none items-end";
document.body.appendChild(container);
}
const close = (n) => {
if (!n || n._c) return;
n._c = 1;
Object.assign(n.style, { transform: "translateX(100%)", opacity: 0 });
setTimeout(() => {
Object.assign(n.style, { maxHeight: "0px", marginBottom: "-1rem", marginTop: "0px", padding: "0px" });
}, 100);
n.addEventListener("transitionend", (e) => {
if (["max-height", "opacity"].includes(e.propertyName)) {
n.remove();
if (!container.hasChildNodes()) (container.remove(), (container = null));
}
});
};
const el = html`
<div
class="card bg-base-100 shadow-xl border border-base-200 w-80 sm:w-96 overflow-hidden transition-all duration-500 ease-in-out transform translate-x-full opacity-0 pointer-events-auto"
style="max-height:200px">
<div class="card-body p-1">
<div role="alert" class="${`alert ${type} alert-soft border-none p-2`}">
<div class="flex items-center justify-between w-full gap-2">
<span class="font-medium text-sm">${msg}</span>
<button class="btn btn-ghost btn-xs btn-circle" @click="${(e) => close(e.target.closest(".card"))}">
<span class="icon-[lucide--circle-x] w-5 h-5"></span>
</button>
</div>
</div>
</div>
</div>
`.firstElementChild;
container.appendChild(el);
requestAnimationFrame(() => requestAnimationFrame(() => el.classList.remove("translate-x-full", "opacity-0")));
setTimeout(() => close(el), ms);
};