Files
botino/public/lib/modal.js
Lucas Tettamanti 0bf26f8eb5 Restyling light pastel: tema blanco/azul/verde + Inter self-hosted
- theme.css reescrito: paleta light (sky/emerald accents, slate neutrals),
  tokens de spacing/typography/radii/shadows, @font-face Inter +
  JetBrains Mono variable woff2 self-hosted.
- ops-shell: header blanco con nav active=accent-soft + status pill pastel.
- run-timeline: bubbles emerald-100 (user) / blue-100 (bot) sobre blanco.
- home-dashboard: helpers cssVar + withAlpha, 6 charts coordinados a
  paleta pastel (--chart-blue/green/purple/orange/pink/gray).
- 8 CRUDs (users, products, orders, conversations, aliases,
  recommendations, quantities, takeovers, settings, debug) migrados
  de hex hardcoded oscuros a var(--*).
- modal.js + toast.js refactor a vars con fallbacks; modal blanco
  con shadow-lg y soft icon backgrounds.
- test-panel: aliases :host apuntan a globals en vez de override dark.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:56:48 -03:00

200 lines
6.3 KiB
JavaScript

/**
* Sistema de modales centralizado para reemplazar alert() nativos
* Uso:
* import { modal } from './lib/modal.js';
* modal.success("Guardado correctamente");
* modal.error("Error: " + e.message);
* modal.info("Información importante");
* modal.warn("Advertencia");
* const ok = await modal.confirm("¿Estás seguro?");
*/
const STYLES = `
.modal-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(15, 23, 42, 0.45);
backdrop-filter: blur(2px);
display: flex; align-items: center; justify-content: center;
z-index: 10000;
animation: fadeIn 0.15s ease-out;
font-family: var(--font-sans, system-ui);
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideIn {
from { transform: translateY(-12px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-box {
background: var(--panel, #ffffff);
border-radius: var(--r-lg, 12px);
padding: 24px;
min-width: 320px;
max-width: 480px;
box-shadow: var(--shadow-lg, 0 12px 28px rgba(15,23,42,.10));
border: 1px solid var(--border, #e2e8f0);
animation: slideIn 0.2s ease-out;
}
.modal-header {
display: flex; align-items: center; gap: 12px;
margin-bottom: 14px;
}
.modal-icon {
width: 32px; height: 32px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 16px; font-weight: 700;
flex-shrink: 0;
}
.modal-icon.success { background: var(--ok-soft, #d1fae5); color: var(--ok, #10b981); }
.modal-icon.error { background: var(--err-soft, #fee2e2); color: var(--err, #ef4444); }
.modal-icon.warn { background: var(--warn-soft, #fef3c7); color: var(--warn, #f59e0b); }
.modal-icon.info { background: var(--accent-soft, #e0f2fe); color: var(--accent, #0ea5e9); }
.modal-icon.confirm { background: var(--accent-soft, #e0f2fe); color: var(--accent-hover, #0284c7); }
.modal-title {
font-size: 16px; font-weight: 600;
color: var(--text, #0f172a);
margin: 0;
letter-spacing: -0.01em;
}
.modal-message {
color: var(--text-dim, #475569);
font-size: 14px; line-height: 1.5;
margin-bottom: 20px;
word-break: break-word;
}
.modal-buttons {
display: flex; gap: 8px; justify-content: flex-end;
}
.modal-btn {
padding: 8px 16px;
border-radius: var(--r-md, 10px);
font-size: 13px; font-weight: 500;
cursor: pointer; border: 1px solid transparent;
transition: all 0.15s;
font-family: inherit;
}
.modal-btn:focus-visible { outline: none; box-shadow: var(--focus-ring, 0 0 0 3px rgba(14,165,233,.3)); }
.modal-btn.primary {
background: var(--accent, #0ea5e9); color: var(--text-on-acc, #fff);
}
.modal-btn.primary:hover { background: var(--accent-hover, #0284c7); }
.modal-btn.secondary {
background: var(--panel, #fff); color: var(--text, #0f172a);
border-color: var(--border-hi, #cbd5e1);
}
.modal-btn.secondary:hover { border-color: var(--accent, #0ea5e9); color: var(--accent-hover, #0284c7); }
.modal-btn.danger {
background: var(--err, #ef4444); color: #fff;
}
.modal-btn.danger:hover { filter: brightness(0.95); }
`;
// Inyectar estilos una sola vez
let stylesInjected = false;
function injectStyles() {
if (stylesInjected) return;
const style = document.createElement("style");
style.textContent = STYLES;
document.head.appendChild(style);
stylesInjected = true;
}
const ICONS = {
success: "✓",
error: "✕",
warn: "!",
info: "i",
confirm: "?",
};
const TITLES = {
success: "Éxito",
error: "Error",
warn: "Advertencia",
info: "Información",
confirm: "Confirmar",
};
function createModal({ type, message, showCancel = false, confirmText = "Aceptar", cancelText = "Cancelar" }) {
injectStyles();
return new Promise((resolve) => {
const overlay = document.createElement("div");
overlay.className = "modal-overlay";
const box = document.createElement("div");
box.className = "modal-box";
box.innerHTML = `
<div class="modal-header">
<div class="modal-icon ${type}">${ICONS[type]}</div>
<h3 class="modal-title">${TITLES[type]}</h3>
</div>
<div class="modal-message">${escapeHtml(message)}</div>
<div class="modal-buttons">
${showCancel ? `<button class="modal-btn secondary" data-action="cancel">${cancelText}</button>` : ""}
<button class="modal-btn ${type === "error" ? "danger" : "primary"}" data-action="confirm">${confirmText}</button>
</div>
`;
overlay.appendChild(box);
document.body.appendChild(overlay);
// Focus en el botón principal
const confirmBtn = box.querySelector('[data-action="confirm"]');
confirmBtn?.focus();
const close = (result) => {
overlay.style.animation = "fadeIn 0.15s ease-out reverse";
setTimeout(() => {
overlay.remove();
resolve(result);
}, 140);
};
// Click en botones
box.addEventListener("click", (e) => {
const action = e.target.dataset?.action;
if (action === "confirm") close(true);
if (action === "cancel") close(false);
});
// Click fuera cierra (solo para mensajes, no confirms)
overlay.addEventListener("click", (e) => {
if (e.target === overlay && !showCancel) {
close(true);
}
});
// Escape cierra
const handleKeydown = (e) => {
if (e.key === "Escape") {
close(showCancel ? false : true);
document.removeEventListener("keydown", handleKeydown);
}
if (e.key === "Enter") {
close(true);
document.removeEventListener("keydown", handleKeydown);
}
};
document.addEventListener("keydown", handleKeydown);
});
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
export const modal = {
success: (message) => createModal({ type: "success", message }),
error: (message) => createModal({ type: "error", message }),
warn: (message) => createModal({ type: "warn", message }),
info: (message) => createModal({ type: "info", message }),
confirm: (message, { confirmText = "Confirmar", cancelText = "Cancelar" } = {}) =>
createModal({ type: "confirm", message, showCancel: true, confirmText, cancelText }),
};
// También exportar como default para conveniencia
export default modal;