Only SigPro

This commit is contained in:
2026-03-20 01:30:12 +01:00
parent 2627e9932f
commit 68a1a1f195
22 changed files with 3 additions and 1255 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "sigpro", "name": "sigpro",
"version": "1.0.11", "version": "1.0.12",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"homepage": "https://natxocc.github.io/sigpro/", "homepage": "https://natxocc.github.io/sigpro/",
@@ -14,13 +14,9 @@
"scripts": { "scripts": {
"docs:dev": "vitepress dev packages/docs", "docs:dev": "vitepress dev packages/docs",
"docs:build": "vitepress build packages/docs", "docs:build": "vitepress build packages/docs",
"docs:preview": "vitepress preview packages/docs", "docs:preview": "vitepress preview packages/docs"
"ui:dev": "vite packages/sigproui",
"ui:build": "vite build packages/sigproui"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.2.2",
"vite": "^8.0.0",
"vitepress": "^1.6.4" "vitepress": "^1.6.4"
}, },
"keywords": [ "keywords": [
@@ -31,8 +27,5 @@
"reactive-programming", "reactive-programming",
"signals-library", "signals-library",
"fine-grained-reactivity" "fine-grained-reactivity"
], ]
"dependencies": {
"daisyui": "^5.5.19"
}
} }

View File

@@ -1,244 +0,0 @@
/**
* UI Demo/Test - Para probar componentes localmente sin hacer release
*
* Ejecutar:
* 1. Crear un archivo index.html que importe este archivo
* 2. O usar con Vite: bun add -d vite && bun vite UI/app.js
*
* Alternativamente, simplemente copiar las partes que necesitas a tu proyecto
*/
import { $, html, effect } from "../../index.js";
import { Button, Input, Card, Drawer, Menu, Dropdown, Fab, Dialog, Loading } from "./index.js";
// Importar la función helper de loading
import { loading } from "./components/Loading.js";
// Estado para la demo
const state = {
inputValue: $(""),
checkboxValue: $(false),
radioValue: $("option1"),
rangeValue: $(50),
showDialog: $(false),
openDrawer: $(false),
};
// Menú de navegación
const menuItems = [
{ label: "Home", icon: "icon-[lucide--home]", href: "#/home" },
{ label: "About", icon: "icon-[lucide--info]", href: "#/about" },
{
label: "Components",
icon: "icon-[lucide--box]",
open: false,
sub: [
{ label: "Button", href: "#/button" },
{ label: "Input", href: "#/input" },
{ label: "Card", href: "#/card" },
{ label: "Forms", href: "#/forms" },
]
},
];
// Demo page principal
export default function App() {
effect(() => {
console.log("Input value:", state.inputValue());
});
return html`
<div class="min-h-screen bg-base-100 text-base-content p-4">
<header class="navbar bg-base-200 shadow-xl px-4 mb-6 rounded-lg">
<div class="flex-1">
<span class="text-xl font-bold">SigProUI Test</span>
</div>
<div class="flex-none">
<c-button @click=${() => state.openDrawer(!state.openDrawer())}>
<span class="icon-[lucide--menu]"></span>
</c-button>
</div>
</header>
<c-drawer .open=${state.openDrawer}>
<c-menu .items=${menuItems}></c-menu>
</c-drawer>
<main class="max-w-4xl mx-auto space-y-8">
<!-- Buttons Section -->
<section class="space-y-4">
<h2 class="text-2xl font-bold">Buttons</h2>
<div class="flex flex-wrap gap-2">
<c-button @click=${() => console.log("Primary clicked")}>Primary</c-button>
<c-button ui="btn-secondary">Secondary</c-button>
<c-button ui="btn-accent">Accent</c-button>
<c-button ui="btn-ghost">Ghost</c-button>
<c-button ui="btn-link">Link</c-button>
<c-button .loading=${true}>Loading</c-button>
<c-button .disabled=${true}>Disabled</c-button>
<c-button .badge=${"5"}>With Badge</c-button>
<c-button .tooltip=${"I'm a tooltip!"}>With Tooltip</c-button>
</div>
</section>
<!-- Input Section -->
<section class="space-y-4">
<h2 class="text-2xl font-bold">Input</h2>
<div class="flex flex-col gap-4 max-w-md">
<c-input
label="Username"
.value=${state.inputValue}
@input=${(v) => state.inputValue(v)}
placeholder="Enter username"
></c-input>
<c-input
label="Email"
type="email"
placeholder="email@example.com"
icon="icon-[lucide--mail]"
></c-input>
<c-input
label="Password"
type="password"
placeholder="••••••••"
icon="icon-[lucide--lock]"
></c-input>
</div>
<p class="text-sm opacity-70">Current input value: "${state.inputValue()}"</p>
</section>
<!-- Card Section -->
<section class="space-y-4">
<h2 class="text-2xl font-bold">Card</h2>
<c-card bordered>
<span slot="title">Card Title</span>
<p>This is a basic card with some content.</p>
<div slot="actions">
<c-button ui="btn-sm btn-primary">Accept</c-button>
<c-button ui="btn-sm">Cancel</c-button>
</div>
</c-card>
<c-card .img=${"https://img.daisyui.com/images/stock/photo-1606107557195-0e29a4b5b4aa.webp"} bordered>
<span slot="title">Card with Image</span>
<p>Beautiful flower image</p>
<div slot="actions">
<c-button ui="btn-primary">Buy Now</c-button>
</div>
</c-card>
</section>
<!-- Forms Section -->
<section class="space-y-4">
<h2 class="text-2xl font-bold">Form Controls</h2>
<div class="flex flex-wrap gap-6">
<c-check
.checked=${state.checkboxValue}
label="Accept terms"
@change=${(v) => state.checkboxValue(v)}
></c-check>
<p class="text-sm">Checkbox value: ${() => state.checkboxValue() ? "checked" : "unchecked"}</p>
<div class="flex flex-col gap-2">
<c-radio
name="radio-demo"
.checked=${() => state.radioValue() === "option1"}
.value=${"option1"}
label="Option 1"
@change=${(v) => state.radioValue(v)}
></c-radio>
<c-radio
name="radio-demo"
.checked=${() => state.radioValue() === "option2"}
.value=${"option2"}
label="Option 2"
@change=${(v) => state.radioValue(v)}
></c-radio>
<c-radio
name="radio-demo"
.checked=${() => state.radioValue() === "option3"}
.value=${"option3"}
label="Option 3"
@change=${(v) => state.radioValue(v)}
></c-radio>
</div>
<p class="text-sm">Radio value: "${state.radioValue()}"</p>
<div class="w-full max-w-xs">
<c-range
.value=${state.rangeValue}
min="0" max="100"
@change=${(v) => state.rangeValue(Number(v))}
></c-range>
<p class="text-sm">Range value: ${state.rangeValue()}</p>
</div>
</div>
</section>
<!-- Loading Section -->
<section class="space-y-4">
<h2 class="text-2xl font-bold">Loading</h2>
<div class="flex gap-4">
<c-button @click=${() => loading(true, "Loading...")}>
Show Loading
</c-button>
<c-button @click=${() => loading(false)}>
Hide Loading
</c-button>
</div>
</section>
<!-- Dialog Section -->
<section class="space-y-4">
<h2 class="text-2xl font-bold">Dialog</h2>
<c-button @click=${() => state.showDialog(true)}>Open Dialog</c-button>
<c-dialog .open=${state.showDialog} @close=${() => state.showDialog(false)}>
<span slot="title" class="font-bold text-lg">Confirm Action</span>
<p>Are you sure you want to proceed?</p>
<div slot="buttons" class="flex gap-2 justify-end">
<c-button ui="btn-ghost" @click=${() => state.showDialog(false)}>Cancel</c-button>
<c-button ui="btn-primary" @click=${() => { console.log("Confirmed!"); state.showDialog(false); }}>Confirm</c-button>
</div>
</c-dialog>
</section>
<!-- Dropdown Section -->
<section class="space-y-4">
<h2 class="text-2xl font-bold">Dropdown</h2>
<c-dropdown>
<c-button slot="trigger">Open Dropdown</c-button>
<li><a>Item 1</a></li>
<li><a>Item 2</a></li>
<li><a>Item 3</a></li>
</c-dropdown>
</section>
<!-- FAB Section -->
<section class="space-y-4">
<h2 class="text-2xl font-bold">FAB (Floating Action Button)</h2>
<c-fab
.actions=${[
{ label: "Add", icon: "icon-[lucide--plus]", ui: "btn-secondary" },
{ label: "Edit", icon: "icon-[lucide--edit]", ui: "btn-accent" },
{ label: "Message", icon: "icon-[lucide--message]", ui: "btn-info" },
]}
></c-fab>
</section>
</main>
</div>
`;
}
// Mount the app
if (typeof document !== "undefined") {
const root = document.getElementById("app");
if (root) {
root.appendChild(App());
}
}

View File

@@ -1,31 +0,0 @@
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.ui() ?? ""} ${props.badge() ? "indicator" : ""}`}"
?disabled=${() => props.disabled()}
@click=${(e) => {
e.stopPropagation();
if (!props.loading() && !props.disabled()) emit("click", e);
}}>
${spinner()} ${slot()}
${() =>
props.badge()
? html`
<span class="indicator-item badge badge-secondary">${props.badge()}</span>
`
: null}
</button>
</div>
`;
},
["ui", "loading", "badge", "tooltip", "disabled"],
);

View File

@@ -1,26 +0,0 @@
import { $, html } from "sigpro";
$.component(
"c-card",
(props, host) => {
return html`
<div class="${() => `card bg-base-100 shadow-sm ${props.ui() ?? ""}`}">
${() =>
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", "ui"],
);

View File

@@ -1,50 +0,0 @@
import { $, html } from "sigpro";
const getVal = (props, key, def) => {
const v = props[key];
if (v === undefined || v === null) return def;
if (typeof v === "function") {
try {
return v();
} catch {
return def;
}
}
return v;
};
const toString = (val) => {
if (val === undefined || val === null) return "";
return String(val);
};
$.component(
"c-check",
(props, { emit }) => {
const label = toString(getVal(props, "label", ""));
const disabled = getVal(props, "disabled", false);
const isToggle = getVal(props, "toggle", false);
return html`
<label class="label cursor-pointer flex gap-2">
<input
type="checkbox"
class="${isToggle ? "toggle" : "checkbox"}"
?disabled="${disabled}"
.checked=${() => getVal(props, "checked", false)}
@change="${(e) => {
if (disabled) return;
const val = e.target.checked;
if (typeof props.checked === "function") props.checked(val);
emit("change", val);
}}" />
${label
? html`
<span class="label-text">${label}</span>
`
: ""}
</label>
`;
},
["label", "checked", "disabled", "toggle"],
);

View File

@@ -1,65 +0,0 @@
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"],
);

View File

@@ -1,168 +0,0 @@
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"],
);

View File

@@ -1,37 +0,0 @@
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"],
);

View File

@@ -1,31 +0,0 @@
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"],
);

View File

@@ -1,20 +0,0 @@
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"],
);

View File

@@ -1,37 +0,0 @@
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.ui() ?? ""}`}>
<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.ui() ?? "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", "ui"],
);

View File

@@ -1,26 +0,0 @@
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.ui() ?? ""}`}>
<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", "ui", "place", "type"],
);

View File

@@ -1,46 +0,0 @@
import { html } from "sigpro";
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"));
};

View File

@@ -1,57 +0,0 @@
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.ui() ?? ""}`}>
${() => renderItems(getItems())}
</ul>
`;
},
["items", "ui"],
);

View File

@@ -1,28 +0,0 @@
import { $, 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.ui() ?? ""}`}
.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", "ui", "disabled", "value"],
);

View File

@@ -1,24 +0,0 @@
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.ui() ?? ""}`}
@input=${(e) => {
const val = e.target.value;
if (typeof props.value === "function") props.value(val);
emit("input", val);
emit("change", val);
}} />
`;
},
["ui", "value", "min", "max", "step"],
);

View File

@@ -1,34 +0,0 @@
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"],
);

View File

@@ -1,31 +0,0 @@
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.ui() ?? "tabs-lifted"}`}>
${() =>
items().map(
(item) => html`
<input
type="radio"
name="${groupName}"
class="tab"
.checked=${() => props.value() === item.value}
@change=${() => {
if (typeof props.value === "function") props.value(item.value);
emit("change", item.value);
}} />
<label class="tab">${item.label}</label>
`,
)}
</div>
<div class="tab-content bg-base-100 border-base-300 p-6">${() => slot(props.value())}</div>
`;
},
["items", "value", "ui"],
);

View File

@@ -1,49 +0,0 @@
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);
};

View File

@@ -1,25 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SigProUI Test</title>
<!-- CSS de la librería UI (se procesa con Vite) -->
<link rel="stylesheet" href="./sigproui.css" />
</head>
<body>
<div id="app"></div>
<script type="module">
import App from './app.js';
import lucide from 'lucide';
// Initialize icons
lucide.createIcons();
// Mount the app
document.getElementById('app').appendChild(App());
</script>
</body>
</html>

View File

@@ -1,70 +0,0 @@
// index.js
import "./sigproui.css";
import "./components/Button.js";
import "./components/Card.js";
import "./components/Checkbox.js";
import "./components/ColorPicker.js";
import "./components/DatePicker.js";
import "./components/Dialog.js";
import "./components/Drawer.js";
import "./components/Dropdown.js";
import "./components/Fab.js";
import "./components/Input.js";
import "./components/Loading.js";
import "./components/Menu.js";
import "./components/Radio.js";
import "./components/Range.js";
import "./components/Rating.js";
import "./components/Tab.js";
import "./components/Toast.js";
export { default as Button } from "./components/Button.js";
export { default as Card } from "./components/Card.js";
export { default as Checkbox } from "./components/Checkbox.js";
export { default as ColorPicker } from "./components/ColorPicker.js";
export { default as DatePicker } from "./components/DatePicker.js";
export { default as Dialog } from "./components/Dialog.js";
export { default as Drawer } from "./components/Drawer.js";
export { default as Dropdown } from "./components/Dropdown.js";
export { default as Fab } from "./components/Fab.js";
export { default as Input } from "./components/Input.js";
export { default as Loading } from "./components/Loading.js";
export { default as Menu } from "./components/Menu.js";
export { default as Radio } from "./components/Radio.js";
export { default as Range } from "./components/Range.js";
export { default as Rating } from "./components/Rating.js";
export { default as Tab } from "./components/Tab.js";
export { default as Toast } from "./components/Toast.js";
export const components = [
"Button",
"Card",
"Checkbox",
"ColorPicker",
"DatePicker",
"Dialog",
"Drawer",
"Dropdown",
"Fab",
"Input",
"Loading",
"Menu",
"Radio",
"Range",
"Rating",
"Tab",
"Toast",
];
// Exportar versión
export const version = "1.0.0";
export const name = "SigProUI";
export default {
version,
name,
description: "Biblioteca de componentes UI basada en SigPro, Tailwind CSS y DaisyUI",
components,
};

View File

@@ -1,146 +0,0 @@
/**
* SigProUI - Estilos de la biblioteca de componentes UI
* Requiere Tailwind CSS y DaisyUI
*/
/* Tailwind + DaisyUI */
@import "tailwindcss";
@plugin "daisyui" {
themes: light --default, dark;
}
/* Utilidades personalizadas de SigProUI */
.btn-ghost {
border-color: transparent !important;
}
.floating-label > span {
font-size: 1.1rem;
}
/* Transiciones para componentes */
.input {
transition: all 0.3s ease-in-out;
outline: none;
appearance: none;
align-items: center;
}
.input:hover {
background-color: var(--color-base-300);
}
/* Indicadores y badges */
.indicator {
position: relative;
}
.indicator-item {
position: absolute;
top: 0;
right: 0;
transform: translate(50%, -50%);
}
/* Tooltips */
.tooltip {
position: relative;
}
.tooltip::after {
content: attr(data-tip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 0.5rem 1rem;
background: oklch(var(--p));
color: oklch(var(--pc));
border-radius: 0.375rem;
font-size: 0.75rem;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
.tooltip:hover::after {
opacity: 1;
}
/* Estados de carga */
.loading {
display: inline-block;
width: 1rem;
height: 1rem;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.75s linear infinite;
}
.loading.loading-xs {
width: 1rem;
height: 1rem;
}
.loading.loading-sm {
width: 1rem;
height: 1rem;
}
.loading.loading-md {
width: 1.5rem;
height: 1.5rem;
}
.loading.loading-lg {
width: 2.5rem;
height: 2.5rem;
}
.hidden {
display: none;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Loading spinner variants */
.loading-spinner {
border-right-color: transparent;
}
.loading-dots::after {
content: "";
animation: dots 1s infinite;
}
@keyframes dots {
0%, 20% { content: "."; }
40% { content: ".."; }
60%, 100% { content: "..."; }
}
.loading-ring {
border-bottom-color: transparent;
}
.loading-facebook::after {
content: "";
animation: facebook 1.5s infinite;
}
@keyframes facebook {
0% { transform: scale(0, 0.035); }
25% { transform: scale(0.035, 0.035); }
50% { transform: scale(0.035, 1); }
75% { transform: scale(1, 1); }
100% { transform: scale(1, 0.035); }
}