// Professional minimalist UI components — Tormentas
const { useState: useStP, useEffect: useEffP, useRef: useRefP } = React;
const T = {
// Core
bg: "#f8fafc",
card: "#ffffff",
sidebar: "#0f172a",
sidebarHover: "#1e293b",
sidebarActive: "#1e293b",
// Text
text: "#0f172a",
muted: "#64748b",
subtle: "#94a3b8",
inverse: "#ffffff",
inverseMuted: "#94a3b8",
// Borders
border: "#e2e8f0",
borderMd: "#cbd5e1",
// Accent (single blue)
accent: "#2563eb",
accentLt: "#eff6ff",
accentMd: "#bfdbfe",
// Semantic
green: "#16a34a",
greenLt:"#f0fdf4",
red: "#dc2626",
redLt: "#fef2f2",
yellow: "#d97706",
yellowLt:"#fffbeb",
// Stripe
stripe: "#f8fafc",
};
// ── Avatar ───────────────────────────────────────────────
function PAvatar({ initials, color, size = 36, style = {} }) {
// Desaturate colors for professional look
return (
{initials}
);
}
// ── Card ────────────────────────────────────────────────
function PCard({ children, style = {}, onClick, padding = "24px" }) {
return (
{ if (onClick) e.currentTarget.style.borderColor = T.accent + "60"; }}
onMouseLeave={e => { if (onClick) e.currentTarget.style.borderColor = T.border; }}
>{children}
);
}
// ── KPI Card ────────────────────────────────────────────
function PKPICard({ label, value, delta, color = T.text, sub }) {
return (
{label}
{value}
{sub && {sub}
}
);
}
// ── Section Label ────────────────────────────────────────
function PLabel({ children }) {
return (
{children}
);
}
// ── Section Header ───────────────────────────────────────
function PSectionHeader({ title, action, onAction }) {
return (
{title}
{action && (
)}
);
}
// ── Badge ────────────────────────────────────────────────
function PBadge({ text, color = T.accent, variant = "default" }) {
if (variant === "pill") return (
{text}
);
return (
{text}
);
}
// ── Table ────────────────────────────────────────────────
function PTable({ columns, rows, onRowClick }) {
const [hovered, setHovered] = React.useState(null);
return (
{columns.map(col => (
| {col.label} |
))}
{rows.map((row, ri) => (
onRowClick && onRowClick(row)}
onMouseEnter={() => setHovered(ri)}
onMouseLeave={() => setHovered(null)}
style={{
background: hovered === ri && onRowClick ? "#f1f5f9" : ri % 2 === 1 ? T.stripe : "#fff",
cursor: onRowClick ? "pointer" : "default",
transition: "background 0.1s"
}}
>
{columns.map(col => (
|
{row[col.key]}
|
))}
))}
);
}
// ── Progress ─────────────────────────────────────────────
function PProgress({ value, max = 100, color = T.accent, height = 4 }) {
const pct = Math.min(100, Math.round((value / max) * 100));
return (
);
}
// ── Back Button ──────────────────────────────────────────
function PBackBtn({ onClick, label = "Atrás" }) {
return (
);
}
// ── Divider ──────────────────────────────────────────────
function PDivider() {
return ;
}
// ── Tab Bar ──────────────────────────────────────────────
function PTabBar({ tabs, active, onChange }) {
return (
{tabs.map(([id, label]) => (
))}
);
}
// ── Line Chart ───────────────────────────────────────────
function PLineChart({ data, labels, color = T.accent, height = 120 }) {
const ref = useRefP(null);
useEffP(() => {
const canvas = ref.current; if (!canvas) return;
const dpr = window.devicePixelRatio || 1;
const W = canvas.offsetWidth * dpr, H = height * dpr;
canvas.width = W; canvas.height = H;
const ctx = canvas.getContext("2d");
ctx.scale(dpr, dpr);
const cW = canvas.offsetWidth, cH = height;
const pad = { top: 20, right: 16, bottom: 32, left: 36 };
const iW = cW - pad.left - pad.right, iH = cH - pad.top - pad.bottom;
const max = Math.max(...data, 1), n = data.length;
const xOf = i => pad.left + (i / (n - 1)) * iW;
const yOf = v => pad.top + iH - (v / max) * iH;
// Grid
[0, Math.ceil(max / 2), max].forEach(v => {
const y = yOf(v);
ctx.strokeStyle = T.border; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(cW - pad.right, y); ctx.stroke();
ctx.fillStyle = T.muted; ctx.font = `11px DM Sans, sans-serif`;
ctx.textAlign = "right"; ctx.fillText(v, pad.left - 6, y + 4);
});
// Fill
const g = ctx.createLinearGradient(0, pad.top, 0, cH - pad.bottom);
g.addColorStop(0, color + "28"); g.addColorStop(1, color + "00");
ctx.beginPath();
data.forEach((v, i) => i === 0 ? ctx.moveTo(xOf(i), yOf(v)) : ctx.lineTo(xOf(i), yOf(v)));
ctx.lineTo(xOf(n - 1), yOf(0)); ctx.lineTo(xOf(0), yOf(0));
ctx.closePath(); ctx.fillStyle = g; ctx.fill();
// Line
ctx.beginPath();
data.forEach((v, i) => i === 0 ? ctx.moveTo(xOf(i), yOf(v)) : ctx.lineTo(xOf(i), yOf(v)));
ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.lineJoin = "round"; ctx.stroke();
// Dots
data.forEach((v, i) => {
ctx.beginPath(); ctx.arc(xOf(i), yOf(v), 3.5, 0, Math.PI * 2);
ctx.fillStyle = "#fff"; ctx.fill();
ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.stroke();
ctx.fillStyle = T.muted; ctx.font = `10px DM Sans, sans-serif`;
ctx.textAlign = "center"; ctx.fillText(labels[i], xOf(i), cH - 8);
});
}, [data, color, height]);
return ;
}
// ── Bar Chart ────────────────────────────────────────────
function PBarChart({ data, labels, color = T.accent, height = 100 }) {
const ref = useRefP(null);
useEffP(() => {
const canvas = ref.current; if (!canvas) return;
const dpr = window.devicePixelRatio || 1;
canvas.width = canvas.offsetWidth * dpr;
canvas.height = height * dpr;
const ctx = canvas.getContext("2d");
ctx.scale(dpr, dpr);
const cW = canvas.offsetWidth, cH = height;
const pad = { top: 16, right: 8, bottom: 28, left: 8 };
const iW = cW - pad.left - pad.right, iH = cH - pad.top - pad.bottom;
const max = Math.max(...data, 1), n = data.length;
const gap = iW / n, bw = gap * 0.5;
data.forEach((v, i) => {
const x = pad.left + gap * i + (gap - bw) / 2;
const bh = (v / max) * iH, y = pad.top + iH - bh;
const col = Array.isArray(color) ? color[i % color.length] : color;
ctx.fillStyle = col + "cc";
ctx.beginPath(); ctx.roundRect(x, y, bw, bh, [3, 3, 0, 0]); ctx.fill();
ctx.fillStyle = T.muted; ctx.font = `10px DM Sans, sans-serif`; ctx.textAlign = "center";
ctx.fillText(labels[i], x + bw / 2, cH - 8);
ctx.fillStyle = col; ctx.font = `bold 10px DM Sans, sans-serif`;
if (v > 0) ctx.fillText(v, x + bw / 2, y - 4);
});
}, [data, color, height]);
return ;
}
// Pos colors — muted, professional
const posColorsP = {
"Base": "#2563eb",
"Escolta": "#7c3aed",
"Alero": "#0891b2",
"Ala-Pívot": "#059669",
"Pívot": "#d97706",
};
Object.assign(window, {
T, posColorsP,
PAvatar, PCard, PKPICard, PLabel, PSectionHeader,
PBadge, PTable, PProgress, PBackBtn, PDivider, PTabBar,
PLineChart, PBarChart
});