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:
@@ -49,6 +49,11 @@
|
||||
|
||||
// --- Dashboard summary badges ---
|
||||
function loadSummary() {
|
||||
var skeletonHtml = '<div class="skeleton skeleton--text" style="width:80%;"></div>';
|
||||
['inv-total-skus','inv-total-value','inv-low-stock','inv-no-movement'].forEach(function(id) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.innerHTML = skeletonHtml;
|
||||
});
|
||||
apiFetch(API + '/summary').then(function(data) {
|
||||
if (!data) return;
|
||||
var totalSkusEl = document.getElementById('inv-total-skus');
|
||||
@@ -112,6 +117,17 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === 'function') {
|
||||
registerCmdKItem({ group: 'Inventario', label: 'Ver stock', href: '/pos/inventory#stock', icon: '📦' });
|
||||
registerCmdKItem({ group: 'Inventario', label: 'Alertas de stock', href: '/pos/inventory#alertas', icon: '⚠️' });
|
||||
registerCmdKItem({ group: 'Inventario', label: 'Entradas de mercancía', href: '/pos/inventory#entradas', icon: '📥' });
|
||||
registerCmdKItem({ group: 'Inventario', label: 'Salidas / Ventas', href: '/pos/inventory#salidas', icon: '📤' });
|
||||
registerCmdKItem({ group: 'Inventario', label: 'Traspasos', href: '/pos/inventory#traspasos', icon: '🚚' });
|
||||
registerCmdKItem({ group: 'Inventario', label: 'Ajustes', href: '/pos/inventory#ajustes', icon: '⚙️' });
|
||||
registerCmdKItem({ group: 'Inventario', label: 'Conteos físicos', href: '/pos/inventory#conteos', icon: '🔢' });
|
||||
}
|
||||
|
||||
// Handle hash-based tab switching (e.g. /pos/inventory#alertas)
|
||||
(function handleHashTab() {
|
||||
var hash = window.location.hash.replace('#', '');
|
||||
@@ -206,13 +222,20 @@
|
||||
var params = new URLSearchParams({ page: currentPage, per_page: 50 });
|
||||
if (currentSearch) params.set('q', currentSearch);
|
||||
|
||||
var tbody = document.getElementById('productTableBody');
|
||||
if (tbody) tbody.innerHTML = renderSkeletonRows(12, 8);
|
||||
|
||||
apiFetch(API + '/items?' + params.toString()).then(function (data) {
|
||||
if (!data) return;
|
||||
|
||||
var tbody = document.getElementById('productTableBody');
|
||||
var items = data.data || [];
|
||||
if (!items.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="12" style="text-align:center;padding:30px;color:var(--color-text-muted);">Sin productos</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="12">' + renderEmptyState({
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>',
|
||||
title: 'Sin productos',
|
||||
subtitle: currentSearch ? 'No se encontraron resultados para "' + esc(currentSearch) + '". Intenta con otro término.' : 'El inventario está vacío. Crea tu primer producto para empezar.',
|
||||
action: currentSearch ? '<button class="btn btn--ghost btn--sm" onclick="document.getElementById(\'productSearch\').value=\'\';loadItems(1,\'\')">Limpiar búsqueda</button>' : '<button class="btn btn--primary btn--sm" onclick="openCreateModal()">Crear producto</button>'
|
||||
}) + '</td></tr>';
|
||||
document.getElementById('productPagination').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
@@ -223,7 +246,7 @@
|
||||
rowHeight: 48,
|
||||
buffer: 3,
|
||||
renderRow: renderInventoryRow,
|
||||
emptyHtml: '<tr><td colspan="12" style="text-align:center;padding:30px;color:var(--color-text-muted);">Sin productos</td></tr>'
|
||||
emptyHtml: '<tr><td colspan="12">' + renderEmptyState({ title: 'Sin productos', subtitle: 'El inventario está vacío.' }) + '</td></tr>'
|
||||
});
|
||||
}
|
||||
inventoryVS.setData(items);
|
||||
@@ -653,14 +676,24 @@
|
||||
// =====================================================================
|
||||
|
||||
function loadAlerts() {
|
||||
var container = document.getElementById('alertsContent');
|
||||
if (container) container.innerHTML = '<div style="padding:var(--space-6);">' + renderEmptyState({
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
||||
title: 'Cargando alertas...',
|
||||
subtitle: 'Revisando el estado del inventario'
|
||||
}) + '</div>';
|
||||
|
||||
apiFetch(API + '/alerts').then(function (data) {
|
||||
if (!data) return;
|
||||
var alerts = data.data || [];
|
||||
var container = document.getElementById('alertsContent');
|
||||
if (!container) return;
|
||||
|
||||
if (!alerts.length) {
|
||||
container.innerHTML = '<p style="padding:var(--space-6);text-align:center;color:var(--color-text-muted);">Sin alertas activas</p>';
|
||||
container.innerHTML = renderEmptyState({
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
|
||||
title: 'Todo en orden',
|
||||
subtitle: 'No hay alertas activas en el inventario. Los niveles de stock están dentro de los límites configurados.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -726,7 +759,11 @@
|
||||
var history = data.data || [];
|
||||
var html = '';
|
||||
if (!history.length) {
|
||||
html = '<p style="color:var(--color-text-muted);text-align:center;padding:var(--space-4);">Sin movimientos</p>';
|
||||
html = renderEmptyState({
|
||||
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="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>',
|
||||
title: 'Sin movimientos',
|
||||
subtitle: 'Este producto aún no tiene historial de entradas, salidas ni ajustes.'
|
||||
});
|
||||
} else {
|
||||
html = '<table class="data-table"><thead><tr><th>Fecha</th><th>Tipo</th><th>Cantidad</th><th>Costo</th><th>Empleado</th><th>Notas</th></tr></thead><tbody>';
|
||||
history.forEach(function (h) {
|
||||
@@ -763,11 +800,11 @@
|
||||
headers: token ? { 'Authorization': 'Bearer ' + token } : {}
|
||||
}).then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.error) { alert('Error: ' + data.error); return; }
|
||||
showToast('Artículo eliminado');
|
||||
if (data.error) { showToast(data.error, 'error', { title: 'Error al eliminar' }); return; }
|
||||
showToast('El artículo fue eliminado correctamente.', 'ok', { title: 'Eliminado' });
|
||||
loadItems(currentPage);
|
||||
if (window.loadInventoryStats) window.loadInventoryStats();
|
||||
}).catch(function() { alert('Error al eliminar artículo'); });
|
||||
}).catch(function() { showToast('No se pudo eliminar el artículo. Intenta de nuevo.', 'error', { title: 'Error' }); });
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
|
||||
@@ -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 ──────
|
||||
|
||||
Reference in New Issue
Block a user