feat(ui): POS UI polish kit — skeletons, toasts, empty states, Cmd+K, tooltips, badges, scrollbars, focus rings, bulk toolbar, breadcrumbs, avatars, connection indicator, sparklines, animations, touch mode, image comparator, ticket preview, resizable columns, sticky headers, density mode

This commit is contained in:
2026-05-26 08:39:32 +00:00
parent 3060dab471
commit 23dbf54f3f
18 changed files with 1060 additions and 44 deletions

View File

@@ -76,38 +76,245 @@
window.print();
};
// ── Toast (simple, non-blocking notification) ──────────────────
// Only creates its own toast if the page doesn't already have one.
window.showToast = function(msg, type) {
// ── Toast (enhanced with icons, progress bar, close button, actions) ──
var _toastIcons = {
ok: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>',
error: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
warn: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
info: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>'
};
var _toastTitles = { ok: 'Éxito', error: 'Error', warn: 'Advertencia', info: 'Información' };
window.showToast = function(msg, type, opts) {
type = type || 'info';
opts = opts || {};
var container = document.getElementById('toast-container');
if (!container) {
container = document.createElement('div');
container.id = 'toast-container';
container.style.cssText = 'position:fixed;top:16px;right:16px;z-index:9999;display:flex;flex-direction:column;gap:8px;pointer-events:none;';
document.body.appendChild(container);
}
var colors = {
ok: 'background:#1a7a3a;color:#fff;',
error: 'background:#c0392b;color:#fff;',
warn: 'background:#d4a017;color:#000;',
info: 'background:var(--color-surface-3,#333);color:var(--color-text-primary,#fff);',
};
var toast = document.createElement('div');
toast.style.cssText = (colors[type] || colors.info) +
'padding:10px 20px;border-radius:8px;font-size:14px;font-weight:500;' +
'box-shadow:0 4px 12px rgba(0,0,0,0.3);pointer-events:auto;' +
'animation:slideInRight 0.3s ease;max-width:400px;';
toast.textContent = msg;
toast.className = 'toast toast--' + type;
var iconHtml = '<div class="toast__icon">' + (_toastIcons[type] || _toastIcons.info) + '</div>';
var titleHtml = opts.title ? '<div class="toast__title">' + opts.title + '</div>' : '';
var actionHtml = '';
if (opts.action && opts.action.text) {
actionHtml = '<div class="toast__action"><button onclick="this.closest(\'.toast\').__toastAction()">' + opts.action.text + '</button></div>';
toast.__toastAction = function() {
if (opts.action.callback) opts.action.callback();
_removeToast(toast);
};
}
var progressHtml = '<div class="toast__progress" style="animation-duration:' + (opts.duration || 4000) + 'ms;"></div>';
toast.innerHTML = iconHtml +
'<div class="toast__content">' + titleHtml + '<div class="toast__msg">' + msg + '</div>' + actionHtml + '</div>' +
'<button class="toast__close" onclick="_removeToast(this.closest(\'.toast\'))">✕</button>' +
progressHtml;
container.appendChild(toast);
setTimeout(function() {
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.3s';
setTimeout(function() { toast.remove(); }, 300);
}, 3000);
var timer = setTimeout(function() { _removeToast(toast); }, opts.duration || 4000);
toast.__toastTimer = timer;
toast.addEventListener('mouseenter', function() { clearTimeout(timer); var p = toast.querySelector('.toast__progress'); if (p) p.style.animationPlayState = 'paused'; });
toast.addEventListener('mouseleave', function() { var p = toast.querySelector('.toast__progress'); if (p) p.style.animationPlayState = 'running'; timer = setTimeout(function() { _removeToast(toast); }, 2000); toast.__toastTimer = timer; });
};
window._removeToast = function(toast) {
if (!toast || toast.__toastRemoved) return;
toast.__toastRemoved = true;
if (toast.__toastTimer) clearTimeout(toast.__toastTimer);
toast.style.animation = 'toastSlideOut 0.25s ease forwards';
setTimeout(function() { toast.remove(); }, 260);
};
// ── Skeleton helpers ──────────────────────────────────────────
window.renderSkeletonRows = function(cols, rows) {
rows = rows || 6;
var html = '';
for (var i = 0; i < rows; i++) {
html += '<tr class="skeleton--table-row">';
for (var j = 0; j < cols; j++) {
html += '<td><div class="skeleton"></div></td>';
}
html += '</tr>';
}
return html;
};
window.showSkeleton = function(containerSelector, cols, rows) {
var el = typeof containerSelector === 'string' ? document.querySelector(containerSelector) : containerSelector;
if (!el) return;
el.dataset.originalContent = el.innerHTML;
el.innerHTML = renderSkeletonRows(cols || 6, rows || 6);
};
window.hideSkeleton = function(containerSelector) {
var el = typeof containerSelector === 'string' ? document.querySelector(containerSelector) : containerSelector;
if (!el || el.dataset.originalContent === undefined) return;
el.innerHTML = el.dataset.originalContent;
delete el.dataset.originalContent;
};
// ── Empty state helper ────────────────────────────────────────
window.renderEmptyState = function(opts) {
opts = opts || {};
var icon = opts.icon || '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="9" y1="9" x2="15" y2="15"/><line x1="15" y1="9" x2="9" y2="15"/></svg>';
var title = opts.title || 'Sin datos';
var subtitle = opts.subtitle || 'No hay información disponible en este momento.';
var action = opts.action ? '<div class="empty-state__action">' + opts.action + '</div>' : '';
return '<div class="empty-state">' +
'<div class="empty-state__icon">' + icon + '</div>' +
'<div class="empty-state__title">' + title + '</div>' +
'<div class="empty-state__subtitle">' + subtitle + '</div>' +
action + '</div>';
};
// ── Cmd+K Global Search ───────────────────────────────────────
(function() {
var cmdkOverlay = null, cmdkInput = null, cmdkResults = null, cmdkSelected = -1;
var cmdkItems = [];
function buildCmdK() {
if (cmdkOverlay) return;
cmdkOverlay = document.createElement('div');
cmdkOverlay.className = 'cmdk-overlay';
cmdkOverlay.innerHTML =
'<div class="cmdk-modal" role="dialog" aria-label="Búsqueda global">' +
' <div class="cmdk-input-wrap">' +
' <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="color:var(--color-text-muted);flex-shrink:0;"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>' +
' <input type="text" class="cmdk-input" placeholder="Buscar módulos, productos, clientes..." autocomplete="off">' +
' <span class="cmdk-shortcut">ESC</span>' +
' </div>' +
' <div class="cmdk-results"></div>' +
' <div class="cmdk-footer"><span>↑↓ navegar · ↵ seleccionar</span><span>' + cmdkItems.length + ' resultados</span></div>' +
'</div>';
document.body.appendChild(cmdkOverlay);
cmdkInput = cmdkOverlay.querySelector('.cmdk-input');
cmdkResults = cmdkOverlay.querySelector('.cmdk-results');
cmdkOverlay.addEventListener('click', function(e) { if (e.target === cmdkOverlay) closeCmdK(); });
cmdkInput.addEventListener('input', function() { filterCmdK(this.value); });
cmdkInput.addEventListener('keydown', function(e) {
if (e.key === 'Escape') { closeCmdK(); return; }
if (e.key === 'ArrowDown') { e.preventDefault(); moveCmdK(1); }
if (e.key === 'ArrowUp') { e.preventDefault(); moveCmdK(-1); }
if (e.key === 'Enter') { e.preventDefault(); activateCmdK(); }
});
}
function openCmdK() {
buildCmdK();
cmdkOverlay.classList.add('is-open');
cmdkInput.value = '';
cmdkInput.focus();
filterCmdK('');
}
function closeCmdK() {
if (cmdkOverlay) cmdkOverlay.classList.remove('is-open');
}
function moveCmdK(dir) {
var items = cmdkResults.querySelectorAll('.cmdk-item');
if (!items.length) return;
cmdkSelected += dir;
if (cmdkSelected < 0) cmdkSelected = items.length - 1;
if (cmdkSelected >= items.length) cmdkSelected = 0;
items.forEach(function(it, i) { it.classList.toggle('is-selected', i === cmdkSelected); });
var sel = items[cmdkSelected];
if (sel) sel.scrollIntoView({ block: 'nearest' });
}
function activateCmdK() {
var items = cmdkResults.querySelectorAll('.cmdk-item');
var sel = items[cmdkSelected];
if (sel && sel.dataset.href) { closeCmdK(); window.location.href = sel.dataset.href; }
}
function filterCmdK(q) {
q = (q || '').toLowerCase().trim();
var groups = {};
cmdkItems.forEach(function(item) {
if (!q || item.label.toLowerCase().indexOf(q) !== -1 || (item.keywords || '').toLowerCase().indexOf(q) !== -1) {
groups[item.group] = groups[item.group] || [];
groups[item.group].push(item);
}
});
var html = '';
var total = 0;
Object.keys(groups).forEach(function(g) {
html += '<div class="cmdk-group"><div class="cmdk-group__label">' + g + '</div>';
groups[g].forEach(function(item) {
total++;
html += '<div class="cmdk-item" data-href="' + (item.href || '') + '">' +
'<div class="cmdk-item__icon">' + (item.icon || '→') + '</div>' +
'<div>' + item.label + '</div>' +
(item.meta ? '<div class="cmdk-item__meta">' + item.meta + '</div>' : '') +
'</div>';
});
html += '</div>';
});
if (!total) html = '<div style="padding:24px;text-align:center;color:var(--color-text-muted);">Sin resultados</div>';
cmdkResults.innerHTML = html;
cmdkSelected = 0;
var first = cmdkResults.querySelector('.cmdk-item');
if (first) first.classList.add('is-selected');
var footer = cmdkOverlay.querySelector('.cmdk-footer span:last-child');
if (footer) footer.textContent = total + ' resultados';
}
document.addEventListener('keydown', function(e) {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); openCmdK(); }
});
window.registerCmdKItem = function(item) {
if (!item || !item.label) return;
cmdkItems.push(item);
};
window.openCmdK = openCmdK;
window.closeCmdK = closeCmdK;
})();
// ── Connection indicator helper ───────────────────────────────
window.ConnectionStatus = {
online: function() { return navigator.onLine; },
render: function(containerId) {
var el = document.getElementById(containerId);
if (!el) return;
function update() {
var isOnline = navigator.onLine;
el.className = 'connection-indicator' + (isOnline ? '' : ' connection-indicator--offline');
el.innerHTML = '<span></span>' + (isOnline ? 'En línea' : 'Sin conexión');
}
update();
window.addEventListener('online', update);
window.addEventListener('offline', update);
}
};
// ── Bulk toolbar helper ───────────────────────────────────────
window.renderBulkToolbar = function(count, actionsHtml) {
return '<div class="bulk-toolbar">' +
'<div class="bulk-toolbar__count">' + count + ' seleccionado' + (count !== 1 ? 's' : '') + '</div>' +
'<div class="bulk-toolbar__actions">' + actionsHtml + '</div>' +
'</div>';
};
// ── Entrance animation helper ─────────────────────────────────
window.animateEntrance = function(selector, animClass, stagger) {
animClass = animClass || 'animate-fade-in-up';
stagger = stagger || 0.05;
var els = document.querySelectorAll(selector);
els.forEach(function(el, i) {
el.style.animationDelay = (i * stagger) + 's';
el.classList.add(animClass);
});
};
// ── "Próximamente" placeholder for features not yet built ──────