feat(design): add 16 new components + update 5 pages (ronda 2)

Bloqueantes POS: modal-pago, ticket-termico, banner-cliente, fkeys-footer, columnas-costo-margen
Componentes nuevos: calculadora-cambio, panel-deslizante, badge-cfdi, arbol-colapsable, tarjeta-metrica, grafica-barras, selector-periodo, etiqueta-codigo-barras
Estados: estado-vacio, banner-offline, modal-confirmacion
Páginas actualizadas: facturación, contabilidad, dashboard, configuración, reportes
This commit is contained in:
Lucy
2026-04-01 07:06:34 +00:00
parent 380698258a
commit ccd3962458
25 changed files with 7153 additions and 129 deletions

View File

@@ -0,0 +1,606 @@
<!DOCTYPE html>
<html lang="es" data-theme="industrial">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus Autoparts — Árbol Colapsable</title>
<link rel="stylesheet" href="../tokens/tokens.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body); background: var(--color-bg-base);
color: var(--color-text-primary); padding: var(--space-8);
transition: var(--transition-normal);
}
.theme-switcher {
position: sticky; top: 0; z-index: calc(var(--z-modal) + 10);
display: flex; gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
margin: calc(-1 * var(--space-8)); margin-bottom: var(--space-8);
}
.theme-switcher button {
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border);
background: var(--color-bg-base); color: var(--color-text-primary);
border-radius: var(--radius-md); cursor: pointer;
font-family: var(--font-body); font-size: var(--text-body-sm);
transition: var(--transition-fast);
}
.theme-switcher button.active {
background: var(--color-primary); color: var(--color-text-inverse);
border-color: var(--color-primary);
}
h1 { font-family: var(--font-heading); font-size: var(--text-h2);
font-weight: var(--heading-weight-primary); margin-bottom: var(--space-2);
color: var(--color-text-accent); }
.subtitle { color: var(--color-text-muted); font-size: var(--text-body); margin-bottom: var(--space-8); }
section { margin-bottom: var(--space-10); }
section > h2 {
font-family: var(--font-heading); font-size: var(--text-h4);
font-weight: var(--heading-weight-secondary); color: var(--color-text-secondary);
margin-bottom: var(--space-4); padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
/* Tree controls */
.tree-controls {
display: flex;
gap: var(--space-2);
margin-bottom: var(--space-4);
}
.tree-controls button {
padding: var(--space-2) var(--space-4);
font-family: var(--font-body);
font-size: var(--text-body-sm);
font-weight: var(--font-weight-semibold);
background: var(--btn-ghost-bg);
color: var(--btn-ghost-text);
border: 1px solid var(--btn-ghost-border);
border-radius: var(--radius-md);
cursor: pointer;
transition: var(--transition-fast);
}
.tree-controls button:hover {
background: var(--color-surface-2);
color: var(--color-text-primary);
}
/* Tree container */
.tree-container {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
}
/* Tree header */
.tree-header {
display: flex;
justify-content: space-between;
padding: var(--space-3) var(--space-5);
background: var(--color-surface-1);
border-bottom: 1px solid var(--color-border);
font-size: var(--text-caption);
font-weight: var(--font-weight-semibold);
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
}
/* Tree node */
.tree-node {
border-bottom: 1px solid var(--color-border);
}
.tree-node:last-child { border-bottom: none; }
.tree-row {
display: flex;
align-items: center;
padding: var(--space-3) var(--space-4);
cursor: pointer;
transition: var(--transition-fast);
gap: var(--space-2);
}
.tree-row:hover {
background: var(--color-primary-muted);
}
.tree-row.selected {
background: var(--color-primary-muted);
border-left: 3px solid var(--color-primary);
}
/* Arrow */
.tree-arrow {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 12px;
color: var(--color-text-muted);
transition: transform var(--duration-fast) var(--ease-out);
border-radius: var(--radius-sm);
}
.tree-arrow:hover {
background: var(--color-surface-3);
color: var(--color-text-primary);
}
.tree-arrow.expanded {
transform: rotate(90deg);
}
.tree-arrow.leaf {
visibility: hidden;
}
/* Account code */
.tree-code {
font-family: var(--font-mono);
font-size: var(--text-body-sm);
font-weight: var(--font-weight-semibold);
color: var(--color-text-accent);
min-width: 50px;
flex-shrink: 0;
}
/* Account name */
.tree-name {
flex: 1;
font-size: var(--text-body-sm);
color: var(--color-text-primary);
}
.tree-row.level-0 .tree-name {
font-weight: var(--font-weight-bold);
font-size: var(--text-body);
}
.tree-row.level-1 .tree-name {
font-weight: var(--font-weight-semibold);
}
/* Balance */
.tree-balance {
font-family: var(--font-mono);
font-size: var(--text-body-sm);
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
text-align: right;
min-width: 120px;
flex-shrink: 0;
}
.tree-row.level-0 .tree-balance {
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
font-size: var(--text-body);
}
/* Children container */
.tree-children {
overflow: hidden;
max-height: 0;
transition: max-height var(--duration-normal) var(--ease-out);
}
.tree-children.expanded {
max-height: 2000px;
}
/* Indentation */
.tree-row.level-0 { padding-left: var(--space-4); }
.tree-row.level-1 { padding-left: calc(var(--space-4) + var(--space-6)); }
.tree-row.level-2 { padding-left: calc(var(--space-4) + var(--space-6) * 2); }
/* Level-specific styling */
.tree-node.level-0 > .tree-row {
background: var(--color-surface-1);
}
.tree-node.level-0 > .tree-row:hover {
background: var(--color-primary-muted);
}
/* Summary row */
.tree-summary {
display: flex;
justify-content: space-between;
padding: var(--space-4) var(--space-5);
background: var(--color-surface-2);
border-top: 2px solid var(--color-border-accent);
font-weight: var(--font-weight-bold);
}
.tree-summary .label {
font-family: var(--font-heading);
font-size: var(--text-body);
color: var(--color-text-primary);
text-transform: uppercase;
letter-spacing: var(--tracking-wide);
}
.tree-summary .total {
font-family: var(--font-mono);
font-size: var(--text-body);
color: var(--color-text-accent);
}
/* Count badge */
.child-count {
font-size: var(--text-caption);
color: var(--color-text-disabled);
font-family: var(--font-mono);
margin-left: var(--space-1);
}
</style>
</head>
<body>
<div class="theme-switcher">
<button class="active" onclick="document.documentElement.dataset.theme='industrial'; this.classList.add('active'); this.nextElementSibling.classList.remove('active')">🔧 Industrial</button>
<button onclick="document.documentElement.dataset.theme='modern'; this.classList.add('active'); this.previousElementSibling.classList.remove('active')">⚡ Moderno</button>
</div>
<h1>Árbol Colapsable</h1>
<p class="subtitle">Catálogo de cuentas SAT con estructura jerárquica colapsable</p>
<section>
<h2>Catálogo de Cuentas</h2>
<div class="tree-controls">
<button onclick="expandAll()">Expandir Todo</button>
<button onclick="collapseAll()">Colapsar Todo</button>
</div>
<div class="tree-container" id="accountTree">
<div class="tree-header">
<span>Cuenta</span>
<span>Saldo</span>
</div>
<!-- 1000 Activo -->
<div class="tree-node level-0" data-code="1000">
<div class="tree-row level-0" onclick="toggleNode(this)">
<span class="tree-arrow">&#9654;</span>
<span class="tree-code">1000</span>
<span class="tree-name">Activo <span class="child-count">(3)</span></span>
<span class="tree-balance">$1,245,800.00</span>
</div>
<div class="tree-children">
<!-- 1100 Bancos -->
<div class="tree-node level-1" data-code="1100">
<div class="tree-row level-1" onclick="toggleNode(this)">
<span class="tree-arrow">&#9654;</span>
<span class="tree-code">1100</span>
<span class="tree-name">Bancos <span class="child-count">(3)</span></span>
<span class="tree-balance">$485,320.00</span>
</div>
<div class="tree-children">
<div class="tree-node level-2">
<div class="tree-row level-2" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">1101</span>
<span class="tree-name">Banamex Cta. 7845</span>
<span class="tree-balance">$198,450.00</span>
</div>
</div>
<div class="tree-node level-2">
<div class="tree-row level-2" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">1102</span>
<span class="tree-name">BBVA Cta. 3021</span>
<span class="tree-balance">$245,870.00</span>
</div>
</div>
<div class="tree-node level-2">
<div class="tree-row level-2" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">1103</span>
<span class="tree-name">Banorte Cta. 5590</span>
<span class="tree-balance">$41,000.00</span>
</div>
</div>
</div>
</div>
<!-- 1200 Inventarios -->
<div class="tree-node level-1" data-code="1200">
<div class="tree-row level-1" onclick="toggleNode(this)">
<span class="tree-arrow">&#9654;</span>
<span class="tree-code">1200</span>
<span class="tree-name">Inventarios <span class="child-count">(2)</span></span>
<span class="tree-balance">$628,480.00</span>
</div>
<div class="tree-children">
<div class="tree-node level-2">
<div class="tree-row level-2" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">1201</span>
<span class="tree-name">Almacén General</span>
<span class="tree-balance">$520,300.00</span>
</div>
</div>
<div class="tree-node level-2">
<div class="tree-row level-2" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">1202</span>
<span class="tree-name">Mercancía en Tránsito</span>
<span class="tree-balance">$108,180.00</span>
</div>
</div>
</div>
</div>
<!-- 1300 Cuentas por Cobrar -->
<div class="tree-node level-1" data-code="1300">
<div class="tree-row level-1" onclick="toggleNode(this)">
<span class="tree-arrow">&#9654;</span>
<span class="tree-code">1300</span>
<span class="tree-name">Cuentas por Cobrar <span class="child-count">(2)</span></span>
<span class="tree-balance">$132,000.00</span>
</div>
<div class="tree-children">
<div class="tree-node level-2">
<div class="tree-row level-2" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">1301</span>
<span class="tree-name">Clientes Nacionales</span>
<span class="tree-balance">$118,500.00</span>
</div>
</div>
<div class="tree-node level-2">
<div class="tree-row level-2" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">1302</span>
<span class="tree-name">Deudores Diversos</span>
<span class="tree-balance">$13,500.00</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 2000 Pasivo -->
<div class="tree-node level-0" data-code="2000">
<div class="tree-row level-0" onclick="toggleNode(this)">
<span class="tree-arrow">&#9654;</span>
<span class="tree-code">2000</span>
<span class="tree-name">Pasivo <span class="child-count">(2)</span></span>
<span class="tree-balance">$389,200.00</span>
</div>
<div class="tree-children">
<div class="tree-node level-1" data-code="2100">
<div class="tree-row level-1" onclick="toggleNode(this)">
<span class="tree-arrow">&#9654;</span>
<span class="tree-code">2100</span>
<span class="tree-name">Proveedores <span class="child-count">(2)</span></span>
<span class="tree-balance">$285,000.00</span>
</div>
<div class="tree-children">
<div class="tree-node level-2">
<div class="tree-row level-2" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">2101</span>
<span class="tree-name">Proveedores Nacionales</span>
<span class="tree-balance">$245,000.00</span>
</div>
</div>
<div class="tree-node level-2">
<div class="tree-row level-2" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">2102</span>
<span class="tree-name">Proveedores Extranjeros</span>
<span class="tree-balance">$40,000.00</span>
</div>
</div>
</div>
</div>
<div class="tree-node level-1" data-code="2200">
<div class="tree-row level-1" onclick="toggleNode(this)">
<span class="tree-arrow">&#9654;</span>
<span class="tree-code">2200</span>
<span class="tree-name">Impuestos por Pagar <span class="child-count">(2)</span></span>
<span class="tree-balance">$104,200.00</span>
</div>
<div class="tree-children">
<div class="tree-node level-2">
<div class="tree-row level-2" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">2201</span>
<span class="tree-name">IVA Trasladado</span>
<span class="tree-balance">$72,200.00</span>
</div>
</div>
<div class="tree-node level-2">
<div class="tree-row level-2" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">2202</span>
<span class="tree-name">ISR por Pagar</span>
<span class="tree-balance">$32,000.00</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 3000 Capital -->
<div class="tree-node level-0" data-code="3000">
<div class="tree-row level-0" onclick="toggleNode(this)">
<span class="tree-arrow">&#9654;</span>
<span class="tree-code">3000</span>
<span class="tree-name">Capital <span class="child-count">(2)</span></span>
<span class="tree-balance">$500,000.00</span>
</div>
<div class="tree-children">
<div class="tree-node level-1">
<div class="tree-row level-1" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">3100</span>
<span class="tree-name">Capital Social</span>
<span class="tree-balance">$350,000.00</span>
</div>
</div>
<div class="tree-node level-1">
<div class="tree-row level-1" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">3200</span>
<span class="tree-name">Utilidades Acumuladas</span>
<span class="tree-balance">$150,000.00</span>
</div>
</div>
</div>
</div>
<!-- 4000 Ingresos -->
<div class="tree-node level-0" data-code="4000">
<div class="tree-row level-0" onclick="toggleNode(this)">
<span class="tree-arrow">&#9654;</span>
<span class="tree-code">4000</span>
<span class="tree-name">Ingresos <span class="child-count">(2)</span></span>
<span class="tree-balance">$892,450.00</span>
</div>
<div class="tree-children">
<div class="tree-node level-1" data-code="4100">
<div class="tree-row level-1" onclick="toggleNode(this)">
<span class="tree-arrow">&#9654;</span>
<span class="tree-code">4100</span>
<span class="tree-name">Ventas <span class="child-count">(2)</span></span>
<span class="tree-balance">$845,200.00</span>
</div>
<div class="tree-children">
<div class="tree-node level-2">
<div class="tree-row level-2" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">4101</span>
<span class="tree-name">Ventas Mostrador</span>
<span class="tree-balance">$562,800.00</span>
</div>
</div>
<div class="tree-node level-2">
<div class="tree-row level-2" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">4102</span>
<span class="tree-name">Ventas a Crédito</span>
<span class="tree-balance">$282,400.00</span>
</div>
</div>
</div>
</div>
<div class="tree-node level-1">
<div class="tree-row level-1" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">4200</span>
<span class="tree-name">Otros Ingresos</span>
<span class="tree-balance">$47,250.00</span>
</div>
</div>
</div>
</div>
<!-- 5000 Gastos -->
<div class="tree-node level-0" data-code="5000">
<div class="tree-row level-0" onclick="toggleNode(this)">
<span class="tree-arrow">&#9654;</span>
<span class="tree-code">5000</span>
<span class="tree-name">Gastos <span class="child-count">(2)</span></span>
<span class="tree-balance">$536,050.00</span>
</div>
<div class="tree-children">
<div class="tree-node level-1" data-code="5100">
<div class="tree-row level-1" onclick="toggleNode(this)">
<span class="tree-arrow">&#9654;</span>
<span class="tree-code">5100</span>
<span class="tree-name">Costo de Ventas <span class="child-count">(1)</span></span>
<span class="tree-balance">$421,500.00</span>
</div>
<div class="tree-children">
<div class="tree-node level-2">
<div class="tree-row level-2" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">5101</span>
<span class="tree-name">Costo Mercancía Vendida</span>
<span class="tree-balance">$421,500.00</span>
</div>
</div>
</div>
</div>
<div class="tree-node level-1" data-code="5200">
<div class="tree-row level-1" onclick="toggleNode(this)">
<span class="tree-arrow">&#9654;</span>
<span class="tree-code">5200</span>
<span class="tree-name">Gastos de Operación <span class="child-count">(3)</span></span>
<span class="tree-balance">$114,550.00</span>
</div>
<div class="tree-children">
<div class="tree-node level-2">
<div class="tree-row level-2" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">5201</span>
<span class="tree-name">Sueldos y Salarios</span>
<span class="tree-balance">$68,000.00</span>
</div>
</div>
<div class="tree-node level-2">
<div class="tree-row level-2" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">5202</span>
<span class="tree-name">Renta Local</span>
<span class="tree-balance">$28,000.00</span>
</div>
</div>
<div class="tree-node level-2">
<div class="tree-row level-2" onclick="selectNode(this)">
<span class="tree-arrow leaf">&#9654;</span>
<span class="tree-code">5203</span>
<span class="tree-name">Servicios (Luz, Tel, Internet)</span>
<span class="tree-balance">$18,550.00</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Summary -->
<div class="tree-summary">
<span class="label">Total Activos - Pasivos</span>
<span class="total">$856,600.00</span>
</div>
</div>
</section>
<script>
function toggleNode(rowEl) {
const node = rowEl.parentElement;
const children = node.querySelector('.tree-children');
const arrow = rowEl.querySelector('.tree-arrow');
if (!children) {
selectNode(rowEl);
return;
}
const isExpanded = children.classList.contains('expanded');
if (isExpanded) {
children.classList.remove('expanded');
arrow.classList.remove('expanded');
} else {
children.classList.add('expanded');
arrow.classList.add('expanded');
}
selectNode(rowEl);
}
function selectNode(rowEl) {
// Remove previous selection
document.querySelectorAll('.tree-row.selected').forEach(el => el.classList.remove('selected'));
rowEl.classList.add('selected');
}
function expandAll() {
document.querySelectorAll('.tree-children').forEach(el => el.classList.add('expanded'));
document.querySelectorAll('.tree-arrow:not(.leaf)').forEach(el => el.classList.add('expanded'));
}
function collapseAll() {
document.querySelectorAll('.tree-children').forEach(el => el.classList.remove('expanded'));
document.querySelectorAll('.tree-arrow').forEach(el => el.classList.remove('expanded'));
}
</script>
</body>
</html>

View File

@@ -0,0 +1,353 @@
<!DOCTYPE html>
<html lang="es" data-theme="industrial">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus Autoparts — Badge CFDI</title>
<link rel="stylesheet" href="../tokens/tokens.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body); background: var(--color-bg-base);
color: var(--color-text-primary); padding: var(--space-8);
transition: var(--transition-normal);
}
.theme-switcher {
position: sticky; top: 0; z-index: calc(var(--z-modal) + 10);
display: flex; gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
margin: calc(-1 * var(--space-8)); margin-bottom: var(--space-8);
}
.theme-switcher button {
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border);
background: var(--color-bg-base); color: var(--color-text-primary);
border-radius: var(--radius-md); cursor: pointer;
font-family: var(--font-body); font-size: var(--text-body-sm);
transition: var(--transition-fast);
}
.theme-switcher button.active {
background: var(--color-primary); color: var(--color-text-inverse);
border-color: var(--color-primary);
}
h1 { font-family: var(--font-heading); font-size: var(--text-h2);
font-weight: var(--heading-weight-primary); margin-bottom: var(--space-2);
color: var(--color-text-accent); }
.subtitle { color: var(--color-text-muted); font-size: var(--text-body); margin-bottom: var(--space-8); }
section { margin-bottom: var(--space-10); }
section > h2 {
font-family: var(--font-heading); font-size: var(--text-h4);
font-weight: var(--heading-weight-secondary); color: var(--color-text-secondary);
margin-bottom: var(--space-4); padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
/* Badge base */
.badge {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-full);
font-size: var(--text-caption);
font-weight: var(--font-weight-semibold);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
white-space: nowrap;
transition: var(--transition-fast);
border: 1px solid transparent;
line-height: 1.6;
}
/* Dot indicator */
.badge .dot {
width: 8px;
height: 8px;
border-radius: var(--radius-full);
flex-shrink: 0;
}
/* Pendiente — Blue / Primary */
.badge-pendiente {
background: var(--color-primary-muted);
color: var(--color-primary);
border-color: var(--color-primary);
}
.badge-pendiente .dot {
background: var(--color-primary);
}
/* Enviando — Yellow / Warning */
.badge-enviando {
background: var(--color-warning-light);
color: var(--color-warning-dark);
border-color: var(--color-warning);
}
.badge-enviando .dot {
background: var(--color-warning);
animation: pulse-dot 1.5s ease-in-out infinite;
}
/* Timbrada — Green / Success */
.badge-timbrada {
background: var(--color-success-light);
color: var(--color-success-dark);
border-color: var(--color-success);
}
.badge-timbrada .dot {
background: var(--color-success);
}
/* Fallida — Red / Error */
.badge-fallida {
background: var(--color-error-light);
color: var(--color-error-dark);
border-color: var(--color-error);
}
.badge-fallida .dot {
background: var(--color-error);
}
/* Cancelada — Gray / Muted */
.badge-cancelada {
background: var(--color-surface-2);
color: var(--color-text-muted);
border-color: var(--color-border-strong);
}
.badge-cancelada .dot {
background: var(--color-text-muted);
}
/* Pulse animation for Enviando */
@keyframes pulse-dot {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.75); }
}
/* Badge gallery */
.badge-gallery {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
align-items: center;
padding: var(--space-5);
background: var(--color-surface-1);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
}
/* Badge sizes */
.badge-lg {
padding: var(--space-2) var(--space-4);
font-size: var(--text-body-sm);
}
.badge-lg .dot {
width: 10px;
height: 10px;
}
/* CFDI Table */
.cfdi-table {
width: 100%;
border-collapse: collapse;
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.cfdi-table thead {
background: var(--color-surface-1);
}
.cfdi-table th {
padding: var(--space-3) var(--space-4);
text-align: left;
font-size: var(--text-caption);
font-weight: var(--font-weight-semibold);
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
border-bottom: 1px solid var(--color-border);
}
.cfdi-table td {
padding: var(--space-3) var(--space-4);
font-size: var(--text-body-sm);
color: var(--color-text-secondary);
border-bottom: 1px solid var(--color-border);
vertical-align: middle;
}
.cfdi-table tbody tr {
transition: var(--transition-fast);
}
.cfdi-table tbody tr:hover {
background: var(--color-primary-muted);
}
.cfdi-table tbody tr:last-child td {
border-bottom: none;
}
.cfdi-table .folio {
font-family: var(--font-mono);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
}
.cfdi-table .uuid {
font-family: var(--font-mono);
font-size: var(--text-caption);
color: var(--color-text-muted);
}
.cfdi-table .amount {
font-family: var(--font-mono);
text-align: right;
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
}
.cfdi-table th.col-amount { text-align: right; }
/* Date column */
.cfdi-table .date {
font-size: var(--text-caption);
color: var(--color-text-muted);
white-space: nowrap;
}
/* Legend */
.legend {
display: flex;
flex-wrap: wrap;
gap: var(--space-4);
padding: var(--space-4) var(--space-5);
background: var(--color-surface-1);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
}
.legend-item {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-caption);
color: var(--color-text-secondary);
}
</style>
</head>
<body>
<div class="theme-switcher">
<button class="active" onclick="document.documentElement.dataset.theme='industrial'; this.classList.add('active'); this.nextElementSibling.classList.remove('active')">🔧 Industrial</button>
<button onclick="document.documentElement.dataset.theme='modern'; this.classList.add('active'); this.previousElementSibling.classList.remove('active')">⚡ Moderno</button>
</div>
<h1>Badge CFDI</h1>
<p class="subtitle">Indicadores de estatus para comprobantes fiscales digitales (CFDI)</p>
<!-- All badges inline -->
<section>
<h2>Variantes de Badge</h2>
<div class="badge-gallery">
<span class="badge badge-pendiente"><span class="dot"></span>Pendiente</span>
<span class="badge badge-enviando"><span class="dot"></span>Enviando</span>
<span class="badge badge-timbrada"><span class="dot"></span>Timbrada</span>
<span class="badge badge-fallida"><span class="dot"></span>Fallida</span>
<span class="badge badge-cancelada"><span class="dot"></span>Cancelada</span>
</div>
</section>
<!-- Large size -->
<section>
<h2>Tamaño Grande</h2>
<div class="badge-gallery">
<span class="badge badge-lg badge-pendiente"><span class="dot"></span>Pendiente</span>
<span class="badge badge-lg badge-enviando"><span class="dot"></span>Enviando</span>
<span class="badge badge-lg badge-timbrada"><span class="dot"></span>Timbrada</span>
<span class="badge badge-lg badge-fallida"><span class="dot"></span>Fallida</span>
<span class="badge badge-lg badge-cancelada"><span class="dot"></span>Cancelada</span>
</div>
</section>
<!-- Legend -->
<section>
<h2>Leyenda</h2>
<div class="legend">
<div class="legend-item"><span class="badge badge-pendiente"><span class="dot"></span>Pendiente</span> Generada, sin enviar al PAC</div>
<div class="legend-item"><span class="badge badge-enviando"><span class="dot"></span>Enviando</span> En proceso de timbrado</div>
<div class="legend-item"><span class="badge badge-timbrada"><span class="dot"></span>Timbrada</span> Timbrada correctamente por el SAT</div>
<div class="legend-item"><span class="badge badge-fallida"><span class="dot"></span>Fallida</span> Error al timbrar</div>
<div class="legend-item"><span class="badge badge-cancelada"><span class="dot"></span>Cancelada</span> Cancelada ante el SAT</div>
</div>
</section>
<!-- Table context -->
<section>
<h2>En Contexto: Lista de CFDI</h2>
<table class="cfdi-table">
<thead>
<tr>
<th>Folio</th>
<th>Cliente</th>
<th>UUID</th>
<th>Fecha</th>
<th class="col-amount">Total</th>
<th>Estatus</th>
</tr>
</thead>
<tbody>
<tr>
<td class="folio">FA-00142</td>
<td>Taller Hermanos López</td>
<td class="uuid">a8d2e1f4-3b7c-...</td>
<td class="date">01/Abr/2026</td>
<td class="amount">$18,450.00</td>
<td><span class="badge badge-timbrada"><span class="dot"></span>Timbrada</span></td>
</tr>
<tr>
<td class="folio">FA-00141</td>
<td>Roberto Méndez G.</td>
<td class="uuid">c3f1a902-7d4e-...</td>
<td class="date">01/Abr/2026</td>
<td class="amount">$4,828.50</td>
<td><span class="badge badge-enviando"><span class="dot"></span>Enviando</span></td>
</tr>
<tr>
<td class="folio">FA-00140</td>
<td>Auto Partes del Norte SA</td>
<td class="uuid">--</td>
<td class="date">31/Mar/2026</td>
<td class="amount">$32,100.00</td>
<td><span class="badge badge-pendiente"><span class="dot"></span>Pendiente</span></td>
</tr>
<tr>
<td class="folio">FA-00139</td>
<td>María Elena Ríos</td>
<td class="uuid">f7e2d301-1a9b-...</td>
<td class="date">30/Mar/2026</td>
<td class="amount">$1,250.00</td>
<td><span class="badge badge-timbrada"><span class="dot"></span>Timbrada</span></td>
</tr>
<tr>
<td class="folio">FA-00138</td>
<td>Servicio Automotriz Reyes</td>
<td class="uuid">b1c4d5e6-8f2a-...</td>
<td class="date">29/Mar/2026</td>
<td class="amount">$7,680.00</td>
<td><span class="badge badge-fallida"><span class="dot"></span>Fallida</span></td>
</tr>
<tr>
<td class="folio">FA-00137</td>
<td>Transportes Garza</td>
<td class="uuid">e9a8b7c6-5d4f-...</td>
<td class="date">28/Mar/2026</td>
<td class="amount">$55,200.00</td>
<td><span class="badge badge-cancelada"><span class="dot"></span>Cancelada</span></td>
</tr>
<tr>
<td class="folio">FA-00136</td>
<td>Refaccionaria Central</td>
<td class="uuid">d2f3e4a5-6b7c-...</td>
<td class="date">27/Mar/2026</td>
<td class="amount">$9,320.00</td>
<td><span class="badge badge-timbrada"><span class="dot"></span>Timbrada</span></td>
</tr>
</tbody>
</table>
</section>
</body>
</html>

View File

@@ -0,0 +1,379 @@
<!DOCTYPE html>
<html lang="es" data-theme="industrial">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus Autoparts — Banner Cliente</title>
<link rel="stylesheet" href="../tokens/tokens.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body); background: var(--color-bg-base);
color: var(--color-text-primary); padding: var(--space-8);
transition: var(--transition-normal);
}
.theme-switcher {
position: sticky; top: 0; z-index: calc(var(--z-modal) + 10);
display: flex; gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
margin: calc(-1 * var(--space-8)); margin-bottom: var(--space-8);
}
.theme-switcher button {
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border);
background: var(--color-bg-base); color: var(--color-text-primary);
border-radius: var(--radius-md); cursor: pointer;
font-family: var(--font-body); font-size: var(--text-body-sm);
transition: var(--transition-fast);
}
.theme-switcher button.active {
background: var(--color-primary); color: var(--color-text-inverse);
border-color: var(--color-primary);
}
h1 { font-family: var(--font-heading); font-size: var(--text-h2);
font-weight: var(--heading-weight-primary); margin-bottom: var(--space-2);
color: var(--color-text-accent); }
.subtitle { color: var(--color-text-muted); font-size: var(--text-body); margin-bottom: var(--space-8); }
section { margin-bottom: var(--space-10); }
section > h2 {
font-family: var(--font-heading); font-size: var(--text-h4);
font-weight: var(--heading-weight-secondary); color: var(--color-text-secondary);
margin-bottom: var(--space-4); padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
.label-text {
font-size: var(--text-caption); color: var(--color-text-muted);
text-transform: uppercase; letter-spacing: var(--tracking-widest);
font-weight: var(--font-weight-semibold); margin-bottom: var(--space-3);
}
/* ====== Simulated POS Panel ====== */
.pos-panel {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
max-width: 420px;
}
.pos-panel-header {
background: var(--color-surface-2);
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border);
font-size: var(--text-caption); color: var(--color-text-muted);
text-transform: uppercase; letter-spacing: var(--tracking-widest);
font-weight: var(--font-weight-semibold);
}
/* ====== Banner Cliente ====== */
.banner-cliente {
padding: var(--space-4);
border-bottom: 2px solid var(--color-primary);
background: var(--color-primary-muted);
}
.banner-top {
display: flex; align-items: center; gap: var(--space-3);
margin-bottom: var(--space-3);
}
.banner-avatar {
width: 44px; height: 44px; border-radius: var(--radius-full);
background: var(--color-primary);
display: flex; align-items: center; justify-content: center;
font-family: var(--font-heading); font-size: var(--text-body-lg);
font-weight: var(--font-weight-bold); color: var(--color-text-inverse);
flex-shrink: 0;
}
.banner-name-block {
flex: 1; min-width: 0;
}
.banner-name {
font-family: var(--font-heading); font-size: var(--text-body-lg);
font-weight: var(--heading-weight-primary); color: var(--color-text-primary);
line-height: 1.2;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.banner-rfc {
font-family: var(--font-mono); font-size: var(--text-caption);
color: var(--color-text-muted); letter-spacing: var(--tracking-wide);
}
.banner-close {
width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
background: transparent; border: 1px solid var(--color-border);
border-radius: var(--radius-md); cursor: pointer;
color: var(--color-text-muted); font-size: 14px;
transition: var(--transition-fast); flex-shrink: 0;
}
.banner-close:hover { background: var(--color-surface-2); color: var(--color-text-primary); }
/* Info Grid */
.banner-info {
display: grid; grid-template-columns: 1fr 1fr;
gap: var(--space-2);
}
.banner-info-item {
padding: var(--space-2) var(--space-3);
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
}
.banner-info-label {
font-size: 10px; color: var(--color-text-muted);
text-transform: uppercase; letter-spacing: var(--tracking-widest);
font-weight: var(--font-weight-semibold);
}
.banner-info-value {
font-size: var(--text-body-sm); font-weight: var(--font-weight-semibold);
color: var(--color-text-primary); margin-top: 1px;
}
.banner-info-value.credit-ok { color: var(--color-success); }
.banner-info-value.credit-low { color: var(--color-warning-dark); }
.banner-info-value.credit-none { color: var(--color-error); }
.banner-info-item.full-width { grid-column: 1 / -1; }
/* Vehicle tag */
.vehicle-tag {
display: inline-flex; align-items: center; gap: var(--space-1);
padding: 2px var(--space-2);
background: var(--color-surface-2);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
font-size: var(--text-caption); color: var(--color-text-secondary);
font-weight: var(--font-weight-semibold);
}
/* ====== Variants Container ====== */
.variants-grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
gap: var(--space-6);
}
/* ====== Compact Banner (for narrow panel) ====== */
.banner-cliente.compact {
padding: var(--space-3);
}
.banner-cliente.compact .banner-top { margin-bottom: var(--space-2); }
.banner-cliente.compact .banner-avatar { width: 36px; height: 36px; font-size: var(--text-body-sm); }
.banner-cliente.compact .banner-name { font-size: var(--text-body); }
.banner-cliente.compact .banner-info { grid-template-columns: 1fr 1fr 1fr; }
/* ====== No Client State ====== */
.banner-empty {
padding: var(--space-4);
border-bottom: 1px solid var(--color-border);
display: flex; align-items: center; gap: var(--space-3);
color: var(--color-text-muted);
}
.banner-empty-icon {
width: 44px; height: 44px; border-radius: var(--radius-full);
background: var(--color-surface-2);
display: flex; align-items: center; justify-content: center;
font-size: 20px; color: var(--color-text-muted);
}
.banner-empty-text {
font-size: var(--text-body-sm);
}
.banner-empty-text strong {
display: block; color: var(--color-text-secondary); margin-bottom: 2px;
}
.banner-empty .add-btn {
margin-left: auto;
padding: var(--space-2) var(--space-3);
background: var(--btn-secondary-bg); color: var(--btn-secondary-text);
border: 1px solid var(--btn-secondary-border); border-radius: var(--radius-md);
font-size: var(--text-caption); font-weight: var(--font-weight-semibold);
cursor: pointer; transition: var(--transition-fast); white-space: nowrap;
}
.banner-empty .add-btn:hover { background: var(--btn-secondary-bg-hover); }
/* ====== Warning banner ====== */
.banner-warning {
display: flex; align-items: center; gap: var(--space-2);
padding: var(--space-2) var(--space-3);
background: var(--color-error-light);
font-size: var(--text-caption); font-weight: var(--font-weight-semibold);
color: var(--color-error-dark);
}
</style>
</head>
<body>
<!-- Theme Switcher -->
<div class="theme-switcher">
<button class="active" onclick="setTheme('industrial')">A — Industrial Robusto</button>
<button onclick="setTheme('modern')">B — Técnico Moderno</button>
</div>
<h1>Banner Cliente</h1>
<p class="subtitle">Información inline del cliente seleccionado en el panel derecho del POS.</p>
<!-- ============================================== -->
<!-- SECTION: Variants -->
<!-- ============================================== -->
<section>
<h2>Variantes del Banner</h2>
<div class="variants-grid">
<!-- Standard: Client with credit -->
<div>
<p class="label-text">Cliente con crédito disponible</p>
<div class="pos-panel">
<div class="pos-panel-header">Panel de Venta</div>
<div class="banner-cliente">
<div class="banner-top">
<div class="banner-avatar">TG</div>
<div class="banner-name-block">
<div class="banner-name">Taller Automotriz García</div>
<div class="banner-rfc">RFC: TAU150301XX1</div>
</div>
<button class="banner-close">&#x2715;</button>
</div>
<div class="banner-info">
<div class="banner-info-item">
<div class="banner-info-label">Crédito disponible</div>
<div class="banner-info-value credit-ok">$15,420.00</div>
</div>
<div class="banner-info-item">
<div class="banner-info-label">Última compra</div>
<div class="banner-info-value">28/Mar/2026</div>
</div>
<div class="banner-info-item full-width">
<div class="banner-info-label">Vehículo registrado</div>
<div class="banner-info-value">
<span class="vehicle-tag">&#x1F697; Nissan Sentra 2019 — NIS-B17-19</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Low credit warning -->
<div>
<p class="label-text">Cliente con crédito bajo</p>
<div class="pos-panel">
<div class="pos-panel-header">Panel de Venta</div>
<div class="banner-cliente">
<div class="banner-top">
<div class="banner-avatar">RM</div>
<div class="banner-name-block">
<div class="banner-name">Refaccionaria Mendoza</div>
<div class="banner-rfc">RFC: RME180512XX3</div>
</div>
<button class="banner-close">&#x2715;</button>
</div>
<div class="banner-info">
<div class="banner-info-item">
<div class="banner-info-label">Crédito disponible</div>
<div class="banner-info-value credit-low">$2,100.00</div>
</div>
<div class="banner-info-item">
<div class="banner-info-label">Última compra</div>
<div class="banner-info-value">15/Mar/2026</div>
</div>
<div class="banner-info-item full-width">
<div class="banner-info-label">Vehículo registrado</div>
<div class="banner-info-value">
<span class="vehicle-tag">&#x1F699; Ford F-150 2021 — FOR-F15-21</span>
</div>
</div>
</div>
</div>
<div class="banner-warning">&#x26A0; Crédito bajo — Límite: $25,000 | Utilizado: $22,900</div>
</div>
</div>
<!-- No credit (public) -->
<div>
<p class="label-text">Cliente sin crédito (público general)</p>
<div class="pos-panel">
<div class="pos-panel-header">Panel de Venta</div>
<div class="banner-cliente">
<div class="banner-top">
<div class="banner-avatar">JL</div>
<div class="banner-name-block">
<div class="banner-name">Juan López Hernández</div>
<div class="banner-rfc">RFC: LOHJ850214XX5</div>
</div>
<button class="banner-close">&#x2715;</button>
</div>
<div class="banner-info">
<div class="banner-info-item">
<div class="banner-info-label">Crédito</div>
<div class="banner-info-value credit-none">No aplica</div>
</div>
<div class="banner-info-item">
<div class="banner-info-label">Última compra</div>
<div class="banner-info-value">Primera visita</div>
</div>
<div class="banner-info-item full-width">
<div class="banner-info-label">Vehículo</div>
<div class="banner-info-value" style="color: var(--color-text-muted);">No registrado</div>
</div>
</div>
</div>
</div>
</div>
<!-- Empty State: No client selected -->
<div>
<p class="label-text">Sin cliente seleccionado</p>
<div class="pos-panel">
<div class="pos-panel-header">Panel de Venta</div>
<div class="banner-empty">
<div class="banner-empty-icon">&#x1F464;</div>
<div class="banner-empty-text">
<strong>Sin cliente</strong>
Venta a público general (F2 para buscar)
</div>
<button class="add-btn">+ Cliente</button>
</div>
</div>
</div>
<!-- Compact Version -->
<div>
<p class="label-text">Versión compacta (panel angosto)</p>
<div class="pos-panel" style="max-width: 360px;">
<div class="pos-panel-header">Panel de Venta — Compacto</div>
<div class="banner-cliente compact">
<div class="banner-top">
<div class="banner-avatar">TG</div>
<div class="banner-name-block">
<div class="banner-name">Taller Automotriz García</div>
<div class="banner-rfc">TAU150301XX1</div>
</div>
<button class="banner-close">&#x2715;</button>
</div>
<div class="banner-info">
<div class="banner-info-item">
<div class="banner-info-label">Crédito</div>
<div class="banner-info-value credit-ok">$15,420</div>
</div>
<div class="banner-info-item">
<div class="banner-info-label">Compra</div>
<div class="banner-info-value">28/Mar</div>
</div>
<div class="banner-info-item">
<div class="banner-info-label">Vehículo</div>
<div class="banner-info-value">Sentra '19</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<script>
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
document.querySelectorAll('.theme-switcher button').forEach(b => b.classList.remove('active'));
event.target.classList.add('active');
}
</script>
</body>
</html>

View File

@@ -0,0 +1,394 @@
<!DOCTYPE html>
<html lang="es" data-theme="industrial">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus Autoparts — Banner Offline</title>
<link rel="stylesheet" href="../tokens/tokens.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body); background: var(--color-bg-base);
color: var(--color-text-primary); padding: var(--space-8);
transition: var(--transition-normal);
}
.theme-switcher {
position: sticky; top: 0; z-index: calc(var(--z-modal) + 10);
display: flex; gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
margin: calc(-1 * var(--space-8)); margin-bottom: var(--space-8);
}
.theme-switcher button {
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border);
background: var(--color-bg-base); color: var(--color-text-primary);
border-radius: var(--radius-md); cursor: pointer;
font-family: var(--font-body); font-size: var(--text-body-sm);
transition: var(--transition-fast);
}
.theme-switcher button.active {
background: var(--color-primary); color: var(--color-text-inverse);
border-color: var(--color-primary);
}
h1 { font-family: var(--font-heading); font-size: var(--text-h2);
font-weight: var(--heading-weight-primary); margin-bottom: var(--space-2);
color: var(--color-text-accent); }
.subtitle { color: var(--color-text-muted); font-size: var(--text-body); margin-bottom: var(--space-8); }
section { margin-bottom: var(--space-10); }
section > h2 {
font-family: var(--font-heading); font-size: var(--text-h4);
font-weight: var(--heading-weight-secondary); color: var(--color-text-secondary);
margin-bottom: var(--space-4); padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
/* === Banner Offline Component === */
@keyframes slideDown {
from { transform: translateY(-100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(0); opacity: 1; }
to { transform: translateY(-100%); opacity: 0; }
}
@keyframes ellipsis {
0% { content: ''; }
25% { content: '.'; }
50% { content: '..'; }
75% { content: '...'; }
100% { content: ''; }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.banner {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-md);
font-size: var(--text-body-sm);
font-weight: 500;
line-height: 1.4;
animation: slideDown 0.35s ease-out forwards;
position: relative;
overflow: hidden;
}
.banner--dismissing {
animation: slideUp 0.3s ease-in forwards;
}
.banner__icon {
font-size: 18px;
flex-shrink: 0;
}
.banner__text {
flex: 1;
}
.banner__text strong {
font-weight: 700;
}
.banner__dismiss {
background: none;
border: none;
cursor: pointer;
font-size: 18px;
line-height: 1;
padding: var(--space-1);
border-radius: var(--radius-sm);
transition: var(--transition-fast);
flex-shrink: 0;
opacity: 0.7;
}
.banner__dismiss:hover {
opacity: 1;
}
/* Warning variant */
.banner--warning {
background: var(--color-warning-light);
color: var(--color-warning-dark);
border: 1px solid var(--color-warning);
}
.banner--warning .banner__dismiss {
color: var(--color-warning-dark);
}
/* Error variant */
.banner--error {
background: var(--color-error-light);
color: var(--color-error-dark);
border: 1px solid var(--color-error);
}
.banner--error .banner__dismiss {
color: var(--color-error-dark);
}
.banner--error .dots {
display: inline-block;
animation: pulse 1.5s ease-in-out infinite;
}
/* Success variant */
.banner--success {
background: var(--color-success-light);
color: var(--color-success-dark);
border: 1px solid var(--color-success);
}
.banner--success .banner__dismiss {
color: var(--color-success-dark);
}
/* Demo controls */
.demo-controls {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
margin-bottom: var(--space-6);
}
.demo-btn {
padding: var(--space-2) var(--space-4);
font-family: var(--font-body);
font-size: var(--text-body-sm);
font-weight: 500;
border-radius: var(--radius-md);
cursor: pointer;
transition: var(--transition-fast);
border: 1px solid var(--color-border);
background: var(--color-bg-elevated);
color: var(--color-text-primary);
}
.demo-btn:hover {
background: var(--color-surface-2);
box-shadow: var(--shadow-sm);
}
.demo-btn--warning { border-color: var(--color-warning); color: var(--color-warning-dark); }
.demo-btn--error { border-color: var(--color-error); color: var(--color-error-dark); }
.demo-btn--success { border-color: var(--color-success); color: var(--color-success-dark); }
.demo-btn--all { background: var(--btn-primary-bg); color: var(--btn-primary-text); border-color: var(--btn-primary-border); }
/* Banners container */
.banners-live {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-bottom: var(--space-8);
min-height: 60px;
}
/* Static examples */
.variant-card {
background: var(--color-surface-1);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-5);
margin-bottom: var(--space-4);
}
.variant-label {
font-family: var(--font-mono);
font-size: var(--text-body-sm);
color: var(--color-text-muted);
background: var(--color-bg-overlay);
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-full);
display: inline-block;
margin-bottom: var(--space-3);
}
/* Full-width fixed preview */
.preview-frame {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
margin-top: var(--space-4);
}
.preview-frame__header {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
background: var(--color-surface-2);
border-bottom: 1px solid var(--color-border);
}
.preview-frame__dot {
width: 10px; height: 10px;
border-radius: var(--radius-full);
background: var(--color-border-strong);
}
.preview-frame__body {
padding: 0;
}
.preview-frame__body .banner {
border-radius: 0;
border-left: none;
border-right: none;
border-top: none;
}
.preview-frame__content {
padding: var(--space-6) var(--space-4);
text-align: center;
color: var(--color-text-muted);
font-size: var(--text-body-sm);
}
</style>
</head>
<body>
<div class="theme-switcher">
<button class="active" onclick="document.documentElement.dataset.theme='industrial'; this.classList.add('active'); this.nextElementSibling.classList.remove('active')">🔧 Industrial</button>
<button onclick="document.documentElement.dataset.theme='modern'; this.classList.add('active'); this.previousElementSibling.classList.remove('active')">⚡ Moderno</button>
</div>
<h1>Banner Offline</h1>
<p class="subtitle">Banners de estado de conexión: advertencia, error y reconexión exitosa.</p>
<!-- ========== Interactive demo ========== -->
<section>
<h2>Demo interactivo</h2>
<p style="color: var(--color-text-muted); font-size: var(--text-body-sm); margin-bottom: var(--space-4);">
Haz clic en los botones para mostrar cada variante. Los banners se pueden cerrar con el botón &times; o simulan auto-dismiss.
</p>
<div class="demo-controls">
<button class="demo-btn demo-btn--warning" onclick="showBanner('warning')">Mostrar Warning</button>
<button class="demo-btn demo-btn--error" onclick="showBanner('error')">Mostrar Error</button>
<button class="demo-btn demo-btn--success" onclick="showBanner('success')">Mostrar Success</button>
<button class="demo-btn demo-btn--all" onclick="showBanner('warning'); setTimeout(()=>showBanner('error'),200); setTimeout(()=>showBanner('success'),400)">Mostrar todos</button>
</div>
<div class="banners-live" id="bannersLive"></div>
</section>
<!-- ========== Static variants ========== -->
<section>
<h2>Variantes estáticas</h2>
<!-- a) Warning -->
<div class="variant-card">
<span class="variant-label">a) Warning — Modo offline</span>
<div class="banner banner--warning" style="animation: none;">
<span class="banner__icon">⚠️</span>
<span class="banner__text"><strong>Modo offline</strong> — Funciones limitadas. Solo consultas en caché disponibles.</span>
<button class="banner__dismiss" onclick="this.closest('.banner').style.display='none'" aria-label="Cerrar">&times;</button>
</div>
</div>
<!-- b) Error -->
<div class="variant-card">
<span class="variant-label">b) Error — Conexión perdida</span>
<div class="banner banner--error" style="animation: none;">
<span class="banner__icon">🔴</span>
<span class="banner__text"><strong>Conexión perdida</strong> — Intentando reconectar<span class="dots">...</span></span>
<button class="banner__dismiss" onclick="this.closest('.banner').style.display='none'" aria-label="Cerrar">&times;</button>
</div>
</div>
<!-- c) Success -->
<div class="variant-card">
<span class="variant-label">c) Success — Conexión restaurada (auto-dismiss)</span>
<div class="banner banner--success" style="animation: none;">
<span class="banner__icon"></span>
<span class="banner__text"><strong>Conexión restaurada</strong> — Sincronizando datos...</span>
</div>
</div>
</section>
<!-- ========== In-context preview ========== -->
<section>
<h2>Uso en contexto: Header de aplicación</h2>
<div class="preview-frame">
<div class="preview-frame__header">
<div class="preview-frame__dot"></div>
<div class="preview-frame__dot"></div>
<div class="preview-frame__dot"></div>
<span style="font-size: var(--text-body-sm); color: var(--color-text-muted); margin-left: var(--space-2);">Nexus Autoparts POS</span>
</div>
<div class="preview-frame__body">
<div class="banner banner--warning" style="animation: none;">
<span class="banner__icon">⚠️</span>
<span class="banner__text"><strong>Modo offline</strong> — Funciones limitadas. Solo consultas en caché disponibles.</span>
<button class="banner__dismiss" onclick="this.closest('.banner').style.display='none'" aria-label="Cerrar">&times;</button>
</div>
<div class="preview-frame__content">
<p>Contenido de la aplicación debajo del banner...</p>
</div>
</div>
</div>
</section>
<script>
let bannerCounter = 0;
const bannerTemplates = {
warning: {
icon: '⚠️',
html: '<strong>Modo offline</strong> — Funciones limitadas. Solo consultas en caché disponibles.',
cls: 'banner--warning',
autoDismiss: false
},
error: {
icon: '🔴',
html: '<strong>Conexión perdida</strong> — Intentando reconectar<span class="dots">...</span>',
cls: 'banner--error',
autoDismiss: false
},
success: {
icon: '✅',
html: '<strong>Conexión restaurada</strong> — Sincronizando datos...',
cls: 'banner--success',
autoDismiss: true
}
};
function showBanner(type) {
const tpl = bannerTemplates[type];
const id = 'banner-' + (++bannerCounter);
const container = document.getElementById('bannersLive');
const banner = document.createElement('div');
banner.className = 'banner ' + tpl.cls;
banner.id = id;
banner.innerHTML = `
<span class="banner__icon">${tpl.icon}</span>
<span class="banner__text">${tpl.html}</span>
<button class="banner__dismiss" onclick="dismissBanner('${id}')" aria-label="Cerrar">&times;</button>
`;
container.appendChild(banner);
if (tpl.autoDismiss) {
setTimeout(() => dismissBanner(id), 3000);
}
}
function dismissBanner(id) {
const banner = document.getElementById(id);
if (!banner || banner.classList.contains('banner--dismissing')) return;
banner.classList.add('banner--dismissing');
banner.addEventListener('animationend', () => banner.remove(), { once: true });
}
</script>
</body>
</html>

View File

@@ -0,0 +1,404 @@
<!DOCTYPE html>
<html lang="es" data-theme="industrial">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus Autoparts — Calculadora de Cambio</title>
<link rel="stylesheet" href="../tokens/tokens.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body); background: var(--color-bg-base);
color: var(--color-text-primary); padding: var(--space-8);
transition: var(--transition-normal);
}
.theme-switcher {
position: sticky; top: 0; z-index: calc(var(--z-modal) + 10);
display: flex; gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
margin: calc(-1 * var(--space-8)); margin-bottom: var(--space-8);
}
.theme-switcher button {
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border);
background: var(--color-bg-base); color: var(--color-text-primary);
border-radius: var(--radius-md); cursor: pointer;
font-family: var(--font-body); font-size: var(--text-body-sm);
transition: var(--transition-fast);
}
.theme-switcher button.active {
background: var(--color-primary); color: var(--color-text-inverse);
border-color: var(--color-primary);
}
h1 { font-family: var(--font-heading); font-size: var(--text-h2);
font-weight: var(--heading-weight-primary); margin-bottom: var(--space-2);
color: var(--color-text-accent); }
.subtitle { color: var(--color-text-muted); font-size: var(--text-body); margin-bottom: var(--space-8); }
section { margin-bottom: var(--space-10); }
section > h2 {
font-family: var(--font-heading); font-size: var(--text-h4);
font-weight: var(--heading-weight-secondary); color: var(--color-text-secondary);
margin-bottom: var(--space-4); padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
/* Calculator Container */
.calc-container {
max-width: 520px;
margin: 0 auto;
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-8);
box-shadow: var(--shadow-lg);
}
/* Total display */
.total-display {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: var(--space-4) var(--space-5);
background: var(--color-surface-1);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
margin-bottom: var(--space-6);
}
.total-display .label {
font-size: var(--text-body);
color: var(--color-text-muted);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
}
.total-display .amount {
font-family: var(--font-mono);
font-size: var(--text-h3);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
}
/* Input area */
.input-group {
margin-bottom: var(--space-5);
}
.input-group label {
display: block;
font-size: var(--text-label);
color: var(--color-text-secondary);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: var(--tracking-wide);
margin-bottom: var(--space-2);
}
.input-group .input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.input-group .currency-sign {
position: absolute;
left: var(--space-4);
font-family: var(--font-mono);
font-size: var(--text-h2);
color: var(--color-text-muted);
pointer-events: none;
font-weight: var(--font-weight-bold);
}
.input-group input {
width: 100%;
padding: var(--space-4) var(--space-4) var(--space-4) var(--space-10);
font-family: var(--font-mono);
font-size: var(--text-h2);
font-weight: var(--font-weight-bold);
background: var(--color-bg-base);
color: var(--color-text-primary);
border: 2px solid var(--color-border-strong);
border-radius: var(--radius-md);
outline: none;
transition: var(--transition-fast);
text-align: right;
}
.input-group input:focus {
border-color: var(--color-border-focus);
box-shadow: var(--shadow-focus);
}
.input-group input::placeholder {
color: var(--color-text-disabled);
font-weight: var(--font-weight-regular);
}
/* Quick buttons */
.quick-buttons {
display: flex;
gap: var(--space-2);
margin-bottom: var(--space-6);
flex-wrap: wrap;
}
.quick-btn {
flex: 1;
min-width: 80px;
padding: var(--space-3) var(--space-3);
font-family: var(--font-mono);
font-size: var(--text-body);
font-weight: var(--font-weight-semibold);
background: var(--color-surface-2);
color: var(--color-text-primary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
cursor: pointer;
transition: var(--transition-fast);
text-align: center;
}
.quick-btn:hover {
background: var(--color-primary-muted);
border-color: var(--color-border-accent);
color: var(--color-text-accent);
}
.quick-btn:active {
transform: scale(0.96);
}
.quick-btn.exact {
font-family: var(--font-body);
background: var(--btn-secondary-bg);
color: var(--btn-secondary-text);
border-color: var(--btn-secondary-border);
}
.quick-btn.exact:hover {
background: var(--btn-secondary-bg-hover);
}
/* Change display */
.change-display {
padding: var(--space-5) var(--space-6);
border-radius: var(--radius-lg);
text-align: center;
transition: var(--transition-normal);
border: 2px solid transparent;
}
.change-display .change-label {
font-size: var(--text-body-sm);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
font-weight: var(--font-weight-semibold);
margin-bottom: var(--space-1);
}
.change-display .change-amount {
font-family: var(--font-mono);
font-size: var(--text-h1);
font-weight: var(--font-weight-bold);
line-height: var(--leading-h1);
}
.change-display.positive {
background: var(--color-success-light);
border-color: var(--color-success);
}
.change-display.positive .change-label {
color: var(--color-success-dark);
}
.change-display.positive .change-amount {
color: var(--color-success-dark);
}
.change-display.negative {
background: var(--color-error-light);
border-color: var(--color-error);
}
.change-display.negative .change-label {
color: var(--color-error-dark);
}
.change-display.negative .change-amount {
color: var(--color-error-dark);
}
.change-display.zero {
background: var(--color-surface-2);
border-color: var(--color-border);
}
.change-display.zero .change-label {
color: var(--color-text-muted);
}
.change-display.zero .change-amount {
color: var(--color-text-muted);
}
.change-display.empty {
background: var(--color-surface-1);
border-color: var(--color-border);
}
.change-display.empty .change-label {
color: var(--color-text-disabled);
}
.change-display.empty .change-amount {
color: var(--color-text-disabled);
font-size: var(--text-h3);
}
/* Denomination breakdown */
.breakdown {
margin-top: var(--space-5);
padding: var(--space-4);
background: var(--color-surface-1);
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
display: none;
}
.breakdown.visible { display: block; }
.breakdown-title {
font-size: var(--text-caption);
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: var(--tracking-widest);
margin-bottom: var(--space-3);
}
.breakdown-row {
display: flex;
justify-content: space-between;
padding: var(--space-1) 0;
font-family: var(--font-mono);
font-size: var(--text-body-sm);
color: var(--color-text-secondary);
}
.breakdown-row .denom { color: var(--color-text-muted); }
.breakdown-row .count { color: var(--color-text-accent); font-weight: var(--font-weight-semibold); }
</style>
</head>
<body>
<div class="theme-switcher">
<button class="active" onclick="document.documentElement.dataset.theme='industrial'; this.classList.add('active'); this.nextElementSibling.classList.remove('active')">🔧 Industrial</button>
<button onclick="document.documentElement.dataset.theme='modern'; this.classList.add('active'); this.previousElementSibling.classList.remove('active')">⚡ Moderno</button>
</div>
<h1>Calculadora de Cambio</h1>
<p class="subtitle">Calcula el cambio a devolver al cliente en tiempo real</p>
<section>
<div class="calc-container">
<!-- Total -->
<div class="total-display">
<span class="label">Total a cobrar</span>
<span class="amount" id="totalAmount">$4,828.50</span>
</div>
<!-- Input -->
<div class="input-group">
<label for="received">Monto Recibido</label>
<div class="input-wrapper">
<span class="currency-sign">$</span>
<input type="text" id="received" placeholder="0.00" autocomplete="off" inputmode="decimal">
</div>
</div>
<!-- Quick buttons -->
<div class="quick-buttons">
<button class="quick-btn" onclick="setAmount(500)">$500</button>
<button class="quick-btn" onclick="setAmount(1000)">$1,000</button>
<button class="quick-btn" onclick="setAmount(2000)">$2,000</button>
<button class="quick-btn" onclick="setAmount(5000)">$5,000</button>
<button class="quick-btn exact" onclick="setExact()">Exacto</button>
</div>
<!-- Change display -->
<div class="change-display empty" id="changeDisplay">
<div class="change-label">Cambio</div>
<div class="change-amount" id="changeAmount">Ingrese monto</div>
</div>
<!-- Breakdown -->
<div class="breakdown" id="breakdown">
<div class="breakdown-title">Desglose sugerido</div>
<div id="breakdownRows"></div>
</div>
</div>
</section>
<script>
const TOTAL = 4828.50;
const input = document.getElementById('received');
const changeDisplay = document.getElementById('changeDisplay');
const changeAmount = document.getElementById('changeAmount');
const breakdown = document.getElementById('breakdown');
const breakdownRows = document.getElementById('breakdownRows');
function formatCurrency(n) {
return '$' + n.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function setAmount(val) {
input.value = val.toLocaleString('es-MX');
calculate();
input.focus();
}
function setExact() {
input.value = TOTAL.toLocaleString('es-MX');
calculate();
input.focus();
}
function parseInput(str) {
if (!str) return NaN;
// Remove commas and spaces
let cleaned = str.replace(/[,\s]/g, '');
return parseFloat(cleaned);
}
function getBreakdown(amount) {
const denoms = [1000, 500, 200, 100, 50, 20, 10, 5, 2, 1, 0.50];
const denomLabels = ['$1,000', '$500', '$200', '$100', '$50', '$20', '$10', '$5', '$2', '$1', '$0.50'];
let remaining = Math.round(amount * 100) / 100;
const result = [];
for (let i = 0; i < denoms.length; i++) {
const count = Math.floor(remaining / denoms[i]);
if (count > 0) {
result.push({ denom: denomLabels[i], count: count });
remaining = Math.round((remaining - count * denoms[i]) * 100) / 100;
}
}
return result;
}
function calculate() {
const received = parseInput(input.value);
if (isNaN(received) || input.value.trim() === '') {
changeDisplay.className = 'change-display empty';
changeAmount.textContent = 'Ingrese monto';
breakdown.classList.remove('visible');
return;
}
const change = Math.round((received - TOTAL) * 100) / 100;
if (change > 0) {
changeDisplay.className = 'change-display positive';
changeAmount.textContent = formatCurrency(change);
// Show breakdown
const rows = getBreakdown(change);
breakdownRows.innerHTML = rows.map(r =>
`<div class="breakdown-row"><span class="denom">${r.denom}</span><span class="count">&times; ${r.count}</span></div>`
).join('');
breakdown.classList.add('visible');
} else if (change < 0) {
changeDisplay.className = 'change-display negative';
changeAmount.textContent = '-' + formatCurrency(Math.abs(change));
breakdown.classList.remove('visible');
} else {
changeDisplay.className = 'change-display zero';
changeAmount.textContent = formatCurrency(0);
breakdown.classList.remove('visible');
}
}
input.addEventListener('input', calculate);
input.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
input.value = '';
calculate();
}
});
// Auto-focus on load
input.focus();
</script>
</body>
</html>

View File

@@ -0,0 +1,507 @@
<!DOCTYPE html>
<html lang="es" data-theme="industrial">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus Autoparts — Columnas Costo y Margen</title>
<link rel="stylesheet" href="../tokens/tokens.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body); background: var(--color-bg-base);
color: var(--color-text-primary); padding: var(--space-8);
transition: var(--transition-normal);
}
.theme-switcher {
position: sticky; top: 0; z-index: calc(var(--z-modal) + 10);
display: flex; gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
margin: calc(-1 * var(--space-8)); margin-bottom: var(--space-8);
}
.theme-switcher button {
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border);
background: var(--color-bg-base); color: var(--color-text-primary);
border-radius: var(--radius-md); cursor: pointer;
font-family: var(--font-body); font-size: var(--text-body-sm);
transition: var(--transition-fast);
}
.theme-switcher button.active {
background: var(--color-primary); color: var(--color-text-inverse);
border-color: var(--color-primary);
}
h1 { font-family: var(--font-heading); font-size: var(--text-h2);
font-weight: var(--heading-weight-primary); margin-bottom: var(--space-2);
color: var(--color-text-accent); }
.subtitle { color: var(--color-text-muted); font-size: var(--text-body); margin-bottom: var(--space-8); }
section { margin-bottom: var(--space-10); }
section > h2 {
font-family: var(--font-heading); font-size: var(--text-h4);
font-weight: var(--heading-weight-secondary); color: var(--color-text-secondary);
margin-bottom: var(--space-4); padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
.label-text {
font-size: var(--text-caption); color: var(--color-text-muted);
text-transform: uppercase; letter-spacing: var(--tracking-widest);
font-weight: var(--font-weight-semibold); margin-bottom: var(--space-3);
}
/* ====== Toggle Permission ====== */
.perm-toggle {
display: flex; align-items: center; gap: var(--space-3);
padding: var(--space-3) var(--space-4);
background: var(--color-surface-1);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
margin-bottom: var(--space-4);
}
.perm-toggle label {
font-size: var(--text-body-sm); font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary); cursor: pointer;
}
.toggle-switch {
position: relative; width: 44px; height: 24px;
}
.toggle-switch input {
opacity: 0; width: 0; height: 0;
}
.toggle-slider {
position: absolute; inset: 0;
background: var(--color-border-strong);
border-radius: var(--radius-full);
cursor: pointer; transition: var(--transition-fast);
}
.toggle-slider::after {
content: ''; position: absolute;
left: 3px; top: 3px;
width: 18px; height: 18px;
background: var(--color-text-primary);
border-radius: var(--radius-full);
transition: var(--transition-fast);
}
.toggle-switch input:checked + .toggle-slider {
background: var(--color-primary);
}
.toggle-switch input:checked + .toggle-slider::after {
transform: translateX(20px);
background: var(--color-text-inverse);
}
.perm-badge {
font-size: var(--text-caption); font-weight: var(--font-weight-semibold);
padding: 2px var(--space-2); border-radius: var(--radius-sm);
text-transform: uppercase; letter-spacing: var(--tracking-wide);
}
.perm-badge.admin { background: var(--color-primary-muted); color: var(--color-text-accent); }
.perm-badge.owner { background: var(--color-success-light); color: var(--color-success-dark); }
/* ====== Table ====== */
.sale-table-container {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow-x: auto;
}
.sale-table {
width: 100%; border-collapse: collapse;
font-size: var(--text-body-sm);
}
.sale-table thead {
background: var(--color-surface-2);
}
.sale-table th {
padding: var(--space-3) var(--space-4);
text-align: left; font-weight: var(--font-weight-semibold);
color: var(--color-text-muted); font-size: var(--text-caption);
text-transform: uppercase; letter-spacing: var(--tracking-widest);
border-bottom: 2px solid var(--color-border);
white-space: nowrap;
}
.sale-table td {
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border);
vertical-align: middle;
}
.sale-table tbody tr:hover {
background: var(--color-primary-muted);
}
.sale-table th.right, .sale-table td.right { text-align: right; }
.sale-table th.center, .sale-table td.center { text-align: center; }
/* Product cell */
.product-cell {
display: flex; flex-direction: column; gap: 2px;
}
.product-name {
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
}
.product-sku {
font-family: var(--font-mono); font-size: 11px;
color: var(--color-text-muted);
}
/* Price cells */
.price {
font-family: var(--font-mono); font-weight: var(--font-weight-semibold);
}
/* ====== Margin Column Styles ====== */
.col-cost, .col-margin {
transition: var(--transition-normal);
}
.col-cost.hidden, .col-margin.hidden {
display: none;
}
.cost-value {
font-family: var(--font-mono); font-size: var(--text-body-sm);
color: var(--color-text-muted);
}
.margin-badge {
display: inline-flex; align-items: center; gap: 4px;
padding: 2px var(--space-2);
border-radius: var(--radius-sm);
font-family: var(--font-mono); font-size: var(--text-caption);
font-weight: var(--font-weight-bold);
white-space: nowrap;
}
.margin-high {
background: var(--color-success-light); color: var(--color-success-dark);
}
.margin-mid {
background: var(--color-warning-light); color: var(--color-warning-dark);
}
.margin-low {
background: var(--color-error-light); color: var(--color-error-dark);
}
/* Margin bar (visual) */
.margin-bar-container {
display: flex; align-items: center; gap: var(--space-2);
}
.margin-bar {
width: 60px; height: 6px;
background: var(--color-surface-3);
border-radius: var(--radius-full);
overflow: hidden;
}
.margin-bar-fill {
height: 100%;
border-radius: var(--radius-full);
transition: var(--transition-normal);
}
.margin-bar-fill.high { background: var(--color-success); }
.margin-bar-fill.mid { background: var(--color-warning); }
.margin-bar-fill.low { background: var(--color-error); }
/* ====== Totals Row ====== */
.sale-table tfoot td {
padding: var(--space-3) var(--space-4);
font-weight: var(--font-weight-bold);
border-top: 2px solid var(--color-border);
background: var(--color-surface-1);
}
.total-margin {
font-family: var(--font-mono); font-size: var(--text-body);
font-weight: var(--font-weight-bold);
}
/* ====== Legend ====== */
.legend {
display: flex; gap: var(--space-6); margin-top: var(--space-4);
padding: var(--space-3) var(--space-4);
background: var(--color-surface-1);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
flex-wrap: wrap;
}
.legend-item {
display: flex; align-items: center; gap: var(--space-2);
font-size: var(--text-caption); color: var(--color-text-secondary);
}
.legend-dot {
width: 10px; height: 10px; border-radius: var(--radius-full);
}
.legend-dot.high { background: var(--color-success); }
.legend-dot.mid { background: var(--color-warning); }
.legend-dot.low { background: var(--color-error); }
/* ====== Qty controls ====== */
.qty-cell {
display: flex; align-items: center; gap: var(--space-1);
}
.qty-btn {
width: 24px; height: 24px;
display: flex; align-items: center; justify-content: center;
background: var(--color-surface-2); border: 1px solid var(--color-border);
border-radius: var(--radius-sm); cursor: pointer;
font-size: 14px; font-weight: bold;
color: var(--color-text-secondary);
transition: var(--transition-fast);
}
.qty-btn:hover { background: var(--color-primary-muted); border-color: var(--color-primary); }
.qty-value {
min-width: 28px; text-align: center;
font-family: var(--font-mono); font-weight: var(--font-weight-bold);
}
/* ====== Delete btn ====== */
.del-btn {
width: 28px; height: 28px;
display: flex; align-items: center; justify-content: center;
background: transparent; border: 1px solid var(--color-border);
border-radius: var(--radius-sm); cursor: pointer;
color: var(--color-text-muted); font-size: 14px;
transition: var(--transition-fast);
}
.del-btn:hover { background: var(--color-error-light); color: var(--color-error); border-color: var(--color-error); }
</style>
</head>
<body>
<!-- Theme Switcher -->
<div class="theme-switcher">
<button class="active" onclick="setTheme('industrial')">A — Industrial Robusto</button>
<button onclick="setTheme('modern')">B — Técnico Moderno</button>
</div>
<h1>Columnas Costo y Margen</h1>
<p class="subtitle">2 columnas extra en la tabla de venta, visibles solo con permiso Admin/Owner. Costo unitario + Margen % con color semántico.</p>
<!-- ============================================== -->
<!-- SECTION: With Columns Visible -->
<!-- ============================================== -->
<section>
<h2>Tabla de Venta — Columnas Visibles (Admin/Owner)</h2>
<div class="perm-toggle">
<label class="toggle-switch">
<input type="checkbox" id="toggle-cols" checked onchange="toggleColumns()">
<span class="toggle-slider"></span>
</label>
<label for="toggle-cols">Mostrar Costo y Margen</label>
<span class="perm-badge admin">Admin</span>
<span class="perm-badge owner">Owner</span>
</div>
<div class="sale-table-container">
<table class="sale-table">
<thead>
<tr>
<th style="width: 32px;">#</th>
<th>Producto</th>
<th class="center" style="width: 90px;">Cantidad</th>
<th class="right">P. Unitario</th>
<th class="right col-cost">Costo Unit.</th>
<th class="center col-margin" style="width: 140px;">Margen</th>
<th class="right">Importe</th>
<th style="width: 40px;"></th>
</tr>
</thead>
<tbody>
<!-- Row 1: High margin -->
<tr>
<td style="color: var(--color-text-muted);">1</td>
<td>
<div class="product-cell">
<span class="product-name">Balatas cerám. del. Brembo P68034</span>
<span class="product-sku">BRM-P68034</span>
</div>
</td>
<td class="center">
<div class="qty-cell" style="justify-content: center;">
<button class="qty-btn">-</button>
<span class="qty-value">2</span>
<button class="qty-btn">+</button>
</div>
</td>
<td class="right"><span class="price">$1,250.00</span></td>
<td class="right col-cost"><span class="cost-value">$720.00</span></td>
<td class="center col-margin">
<div class="margin-bar-container" style="justify-content: center;">
<span class="margin-badge margin-high">42.4%</span>
<div class="margin-bar"><div class="margin-bar-fill high" style="width: 42.4%;"></div></div>
</div>
</td>
<td class="right"><span class="price">$2,500.00</span></td>
<td class="center"><button class="del-btn">&#x2715;</button></td>
</tr>
<!-- Row 2: Mid margin -->
<tr>
<td style="color: var(--color-text-muted);">2</td>
<td>
<div class="product-cell">
<span class="product-name">Filtro aceite Wix WL7200</span>
<span class="product-sku">WIX-7200</span>
</div>
</td>
<td class="center">
<div class="qty-cell" style="justify-content: center;">
<button class="qty-btn">-</button>
<span class="qty-value">1</span>
<button class="qty-btn">+</button>
</div>
</td>
<td class="right"><span class="price">$185.00</span></td>
<td class="right col-cost"><span class="cost-value">$138.00</span></td>
<td class="center col-margin">
<div class="margin-bar-container" style="justify-content: center;">
<span class="margin-badge margin-mid">25.4%</span>
<div class="margin-bar"><div class="margin-bar-fill mid" style="width: 25.4%;"></div></div>
</div>
</td>
<td class="right"><span class="price">$185.00</span></td>
<td class="center"><button class="del-btn">&#x2715;</button></td>
</tr>
<!-- Row 3: Low margin -->
<tr>
<td style="color: var(--color-text-muted);">3</td>
<td>
<div class="product-cell">
<span class="product-name">Amortiguador tras. Monroe OESpec 72364</span>
<span class="product-sku">MON-72364</span>
</div>
</td>
<td class="center">
<div class="qty-cell" style="justify-content: center;">
<button class="qty-btn">-</button>
<span class="qty-value">2</span>
<button class="qty-btn">+</button>
</div>
</td>
<td class="right"><span class="price">$1,071.25</span></td>
<td class="right col-cost"><span class="cost-value">$950.00</span></td>
<td class="center col-margin">
<div class="margin-bar-container" style="justify-content: center;">
<span class="margin-badge margin-low">11.3%</span>
<div class="margin-bar"><div class="margin-bar-fill low" style="width: 11.3%;"></div></div>
</div>
</td>
<td class="right"><span class="price">$2,142.50</span></td>
<td class="center"><button class="del-btn">&#x2715;</button></td>
</tr>
<!-- Row 4: High margin -->
<tr>
<td style="color: var(--color-text-muted);">4</td>
<td>
<div class="product-cell">
<span class="product-name">Aceite Motor Mobil 1 5W-30 Sintético 5L</span>
<span class="product-sku">MOB-5W30-5L</span>
</div>
</td>
<td class="center">
<div class="qty-cell" style="justify-content: center;">
<button class="qty-btn">-</button>
<span class="qty-value">1</span>
<button class="qty-btn">+</button>
</div>
</td>
<td class="right"><span class="price">$890.00</span></td>
<td class="right col-cost"><span class="cost-value">$520.00</span></td>
<td class="center col-margin">
<div class="margin-bar-container" style="justify-content: center;">
<span class="margin-badge margin-high">41.6%</span>
<div class="margin-bar"><div class="margin-bar-fill high" style="width: 41.6%;"></div></div>
</div>
</td>
<td class="right"><span class="price">$890.00</span></td>
<td class="center"><button class="del-btn">&#x2715;</button></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3" style="text-align: right; color: var(--color-text-muted);">5 artículos, 6 piezas</td>
<td class="right">Subtotal:</td>
<td class="right col-cost">
<span class="cost-value">$4,098.00</span>
</td>
<td class="center col-margin">
<span class="total-margin margin-badge margin-high" style="font-size: var(--text-body-sm);">
Promedio: 32.8%
</span>
</td>
<td class="right"><span class="price" style="font-size: var(--text-body);">$5,717.50</span></td>
<td></td>
</tr>
</tfoot>
</table>
</div>
<!-- Legend -->
<div class="legend">
<div class="legend-item">
<span class="legend-dot high"></span>
<span>Verde: margen &gt; 30%</span>
</div>
<div class="legend-item">
<span class="legend-dot mid"></span>
<span>Amarillo: margen 15% 30%</span>
</div>
<div class="legend-item">
<span class="legend-dot low"></span>
<span>Rojo: margen &lt; 15%</span>
</div>
</div>
</section>
<!-- ============================================== -->
<!-- SECTION: Margin Badge Variants -->
<!-- ============================================== -->
<section>
<h2>Variantes del Badge de Margen</h2>
<p class="label-text">Los 3 niveles de color semántico</p>
<div style="display: flex; gap: var(--space-6); flex-wrap: wrap; align-items: center;">
<div style="text-align: center;">
<p class="label-text">Alto (&gt;30%)</p>
<div class="margin-bar-container">
<span class="margin-badge margin-high">42.4%</span>
<div class="margin-bar" style="width: 80px;"><div class="margin-bar-fill high" style="width: 42.4%;"></div></div>
</div>
</div>
<div style="text-align: center;">
<p class="label-text">Medio (15-30%)</p>
<div class="margin-bar-container">
<span class="margin-badge margin-mid">25.4%</span>
<div class="margin-bar" style="width: 80px;"><div class="margin-bar-fill mid" style="width: 25.4%;"></div></div>
</div>
</div>
<div style="text-align: center;">
<p class="label-text">Bajo (&lt;15%)</p>
<div class="margin-bar-container">
<span class="margin-badge margin-low">11.3%</span>
<div class="margin-bar" style="width: 80px;"><div class="margin-bar-fill low" style="width: 11.3%;"></div></div>
</div>
</div>
<div style="text-align: center;">
<p class="label-text">Negativo</p>
<div class="margin-bar-container">
<span class="margin-badge margin-low">-5.2%</span>
<div class="margin-bar" style="width: 80px;"><div class="margin-bar-fill low" style="width: 0%;"></div></div>
</div>
</div>
</div>
</section>
<script>
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
document.querySelectorAll('.theme-switcher button').forEach(b => b.classList.remove('active'));
event.target.classList.add('active');
}
function toggleColumns() {
const checked = document.getElementById('toggle-cols').checked;
document.querySelectorAll('.col-cost, .col-margin').forEach(el => {
el.classList.toggle('hidden', !checked);
});
}
</script>
</body>
</html>

View File

@@ -0,0 +1,297 @@
<!DOCTYPE html>
<html lang="es" data-theme="industrial">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus Autoparts — Estado Vacío</title>
<link rel="stylesheet" href="../tokens/tokens.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body); background: var(--color-bg-base);
color: var(--color-text-primary); padding: var(--space-8);
transition: var(--transition-normal);
}
.theme-switcher {
position: sticky; top: 0; z-index: calc(var(--z-modal) + 10);
display: flex; gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
margin: calc(-1 * var(--space-8)); margin-bottom: var(--space-8);
}
.theme-switcher button {
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border);
background: var(--color-bg-base); color: var(--color-text-primary);
border-radius: var(--radius-md); cursor: pointer;
font-family: var(--font-body); font-size: var(--text-body-sm);
transition: var(--transition-fast);
}
.theme-switcher button.active {
background: var(--color-primary); color: var(--color-text-inverse);
border-color: var(--color-primary);
}
h1 { font-family: var(--font-heading); font-size: var(--text-h2);
font-weight: var(--heading-weight-primary); margin-bottom: var(--space-2);
color: var(--color-text-accent); }
.subtitle { color: var(--color-text-muted); font-size: var(--text-body); margin-bottom: var(--space-8); }
section { margin-bottom: var(--space-10); }
section > h2 {
font-family: var(--font-heading); font-size: var(--text-h4);
font-weight: var(--heading-weight-secondary); color: var(--color-text-secondary);
margin-bottom: var(--space-4); padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
/* === Empty State Component === */
.variants-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: var(--space-6);
}
.variant-card {
background: var(--color-surface-1);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-6);
display: flex;
flex-direction: column;
align-items: center;
}
.variant-label {
font-family: var(--font-mono);
font-size: var(--text-body-sm);
color: var(--color-text-muted);
background: var(--color-bg-overlay);
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-full);
margin-bottom: var(--space-4);
align-self: flex-start;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
max-width: 320px;
padding: var(--space-8) var(--space-4);
width: 100%;
}
.empty-state__icon {
font-size: 48px;
line-height: 1;
margin-bottom: var(--space-4);
opacity: 0.6;
filter: grayscale(30%);
}
.empty-state__title {
font-family: var(--font-heading);
font-size: var(--text-h5);
font-weight: var(--heading-weight-secondary);
color: var(--color-text-primary);
margin-bottom: var(--space-2);
}
.empty-state__subtitle {
font-size: var(--text-body-sm);
color: var(--color-text-muted);
line-height: 1.5;
margin-bottom: var(--space-5);
}
.empty-state__action {
padding: var(--space-2) var(--space-5);
font-family: var(--font-body);
font-size: var(--text-body-sm);
font-weight: 500;
border-radius: var(--radius-md);
cursor: pointer;
transition: var(--transition-fast);
border: 1px solid var(--btn-secondary-border);
background: var(--btn-secondary-bg);
color: var(--btn-secondary-text);
}
.empty-state__action:hover {
background: var(--btn-secondary-bg-hover);
box-shadow: var(--shadow-sm);
}
.empty-state__action--primary {
background: var(--btn-primary-bg);
color: var(--btn-primary-text);
border-color: var(--btn-primary-border);
}
.empty-state__action--primary:hover {
background: var(--btn-primary-bg-hover);
}
/* Context: inside table */
.context-table {
width: 100%;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.context-table__header {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
background: var(--color-surface-2);
padding: var(--space-3) var(--space-4);
font-size: var(--text-body-sm);
font-weight: 600;
color: var(--color-text-muted);
border-bottom: 1px solid var(--color-border);
}
.context-table__body {
display: flex;
justify-content: center;
padding: var(--space-6) var(--space-4);
background: var(--color-bg-base);
min-height: 200px;
align-items: center;
}
/* Context: inside card */
.context-card {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.context-card__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border);
background: var(--color-surface-1);
}
.context-card__header span {
font-family: var(--font-heading);
font-weight: var(--heading-weight-secondary);
font-size: var(--text-body);
color: var(--color-text-secondary);
}
.context-card__body {
display: flex;
justify-content: center;
padding: var(--space-4);
min-height: 180px;
align-items: center;
}
</style>
</head>
<body>
<div class="theme-switcher">
<button class="active" onclick="document.documentElement.dataset.theme='industrial'; this.classList.add('active'); this.nextElementSibling.classList.remove('active')">🔧 Industrial</button>
<button onclick="document.documentElement.dataset.theme='modern'; this.classList.add('active'); this.previousElementSibling.classList.remove('active')">⚡ Moderno</button>
</div>
<h1>Estado Vacío</h1>
<p class="subtitle">Componente reutilizable para estados vacíos en tablas, cards y páginas completas.</p>
<!-- ========== Variants ========== -->
<section>
<h2>Variantes</h2>
<div class="variants-grid">
<!-- a) No hay productos -->
<div class="variant-card">
<span class="variant-label">a) Sin productos</span>
<div class="empty-state">
<div class="empty-state__icon">📦</div>
<div class="empty-state__title">No se encontraron productos</div>
<div class="empty-state__subtitle">Intenta con otro término de búsqueda</div>
<button class="empty-state__action">Limpiar filtros</button>
</div>
</div>
<!-- b) No hay ventas -->
<div class="variant-card">
<span class="variant-label">b) Sin ventas</span>
<div class="empty-state">
<div class="empty-state__icon">🧾</div>
<div class="empty-state__title">No hay ventas registradas hoy</div>
<div class="empty-state__subtitle">Las ventas aparecerán aquí conforme se realicen</div>
</div>
</div>
<!-- c) Sin resultados -->
<div class="variant-card">
<span class="variant-label">c) Sin resultados</span>
<div class="empty-state">
<div class="empty-state__icon">🔍</div>
<div class="empty-state__title">Sin resultados para 'BAL-2847'</div>
<div class="empty-state__subtitle">Verifica el número de parte o nombre</div>
<button class="empty-state__action empty-state__action--primary">Nueva búsqueda</button>
</div>
</div>
<!-- d) Sin conexión -->
<div class="variant-card">
<span class="variant-label">d) Sin conexión</span>
<div class="empty-state">
<div class="empty-state__icon">📡</div>
<div class="empty-state__title">Sin conexión al servidor</div>
<div class="empty-state__subtitle">Verifica tu conexión a internet</div>
<button class="empty-state__action empty-state__action--primary">Reintentar</button>
</div>
</div>
</div>
</section>
<!-- ========== Contextual usage: inside a table ========== -->
<section>
<h2>Uso en contexto: Dentro de tabla</h2>
<div class="context-table">
<div class="context-table__header">
<span>Producto</span>
<span>No. Parte</span>
<span>Stock</span>
<span>Precio</span>
</div>
<div class="context-table__body">
<div class="empty-state">
<div class="empty-state__icon">🔍</div>
<div class="empty-state__title">Sin resultados para 'xyz'</div>
<div class="empty-state__subtitle">Verifica el número de parte o nombre</div>
<button class="empty-state__action">Limpiar filtros</button>
</div>
</div>
</div>
</section>
<!-- ========== Contextual usage: inside a card ========== -->
<section>
<h2>Uso en contexto: Dentro de card</h2>
<div class="context-card">
<div class="context-card__header">
<span>Ventas del día</span>
<span style="font-size: var(--text-body-sm); color: var(--color-text-muted);">01 Abr 2026</span>
</div>
<div class="context-card__body">
<div class="empty-state">
<div class="empty-state__icon">🧾</div>
<div class="empty-state__title">No hay ventas registradas hoy</div>
<div class="empty-state__subtitle">Las ventas aparecerán aquí conforme se realicen</div>
</div>
</div>
</div>
</section>
</body>
</html>

View File

@@ -0,0 +1,341 @@
<!DOCTYPE html>
<html lang="es" data-theme="industrial">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus Autoparts — Etiqueta Código de Barras</title>
<link rel="stylesheet" href="../tokens/tokens.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body); background: var(--color-bg-base);
color: var(--color-text-primary); padding: var(--space-8);
transition: var(--transition-normal);
}
.theme-switcher {
position: sticky; top: 0; z-index: calc(var(--z-modal) + 10);
display: flex; gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
margin: calc(-1 * var(--space-8)); margin-bottom: var(--space-8);
}
.theme-switcher button {
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border);
background: var(--color-bg-base); color: var(--color-text-primary);
border-radius: var(--radius-md); cursor: pointer;
font-family: var(--font-body); font-size: var(--text-body-sm);
transition: var(--transition-fast);
}
.theme-switcher button.active {
background: var(--color-primary); color: var(--color-text-inverse);
border-color: var(--color-primary);
}
h1 { font-family: var(--font-heading); font-size: var(--text-h2);
font-weight: var(--heading-weight-primary); margin-bottom: var(--space-2);
color: var(--color-text-accent); }
.subtitle { color: var(--color-text-muted); font-size: var(--text-body); margin-bottom: var(--space-8); }
section { margin-bottom: var(--space-10); }
section > h2 {
font-family: var(--font-heading); font-size: var(--text-h4);
font-weight: var(--heading-weight-secondary); color: var(--color-text-secondary);
margin-bottom: var(--space-4); padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
/* ── Labels Grid ── */
.labels-grid {
display: flex;
flex-wrap: wrap;
gap: var(--space-6);
}
/* ── Base Label ── */
.label-card {
background: #ffffff;
color: #000000;
border: 1px solid #cccccc;
border-radius: 2px;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: var(--shadow-md);
position: relative;
}
/* Size variants */
.label-50x25 {
width: 189px; /* ~50mm at 96dpi */
height: 95px; /* ~25mm */
padding: 6px 8px 4px;
}
.label-50x30 {
width: 189px;
height: 113px; /* ~30mm */
padding: 6px 8px 4px;
}
/* ── Label inner layout ── */
.label-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 2px;
}
.label-partno {
font-family: 'Courier New', 'Consolas', monospace;
font-size: 11px;
font-weight: 700;
color: #000000;
letter-spacing: 0.03em;
}
.label-location {
font-family: 'Courier New', 'Consolas', monospace;
font-size: 8px;
font-weight: 400;
color: #555555;
background: #f0f0f0;
padding: 1px 4px;
border-radius: 2px;
}
.label-name {
font-family: Arial, sans-serif;
font-size: 8px;
color: #333333;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 3px;
}
/* ── Barcode simulation ── */
.barcode-area {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
min-height: 24px;
}
.barcode {
display: flex;
align-items: stretch;
height: 100%;
max-height: 28px;
gap: 0;
}
.barcode .b {
background: #000000;
}
.barcode .w {
background: #ffffff;
}
.barcode-number {
text-align: center;
font-family: 'Courier New', 'Consolas', monospace;
font-size: 7px;
color: #333333;
letter-spacing: 0.08em;
margin-top: 1px;
}
/* ── Price ── */
.label-bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-top: auto;
}
.label-price {
font-family: 'Courier New', 'Consolas', monospace;
font-size: 16px;
font-weight: 700;
color: #000000;
line-height: 1;
}
.label-50x25 .label-price {
font-size: 14px;
}
/* Size badge on preview */
.size-badge {
font-size: var(--text-caption);
color: var(--color-text-muted);
margin-bottom: var(--space-2);
font-family: var(--font-mono);
}
/* ── Preview wrapper (for the design system page) ── */
.label-preview {
display: flex;
flex-direction: column;
align-items: flex-start;
}
/* ── Print button ── */
.btn-print {
display: inline-flex; align-items: center; gap: var(--space-2);
font-family: var(--font-body); font-size: var(--text-body-sm);
font-weight: var(--font-weight-semibold);
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-md);
border: 1px solid var(--btn-secondary-border);
background: var(--btn-secondary-bg);
color: var(--btn-secondary-text);
cursor: pointer;
transition: var(--transition-fast);
}
.btn-print:hover {
background: var(--btn-secondary-bg-hover);
}
/* Notes */
.note {
font-size: var(--text-caption);
color: var(--color-text-muted);
margin-top: var(--space-4);
padding: var(--space-3) var(--space-4);
background: var(--color-surface-1);
border-radius: var(--radius-md);
border-left: 3px solid var(--color-primary);
}
/* ── Print Styles ── */
@media print {
body {
background: #ffffff !important;
color: #000000 !important;
padding: 0 !important;
margin: 0 !important;
}
.theme-switcher, h1, .subtitle, section > h2,
.btn-print, .note, .size-badge {
display: none !important;
}
section {
margin: 0 !important;
padding: 0 !important;
border: none !important;
}
section > h2 { display: none !important; }
.labels-grid {
gap: 2mm;
justify-content: flex-start;
}
.label-card {
box-shadow: none !important;
border: 0.5px solid #999 !important;
page-break-inside: avoid;
}
.label-preview {
break-inside: avoid;
}
}
</style>
</head>
<body>
<div class="theme-switcher">
<button class="active" onclick="document.documentElement.dataset.theme='industrial'; this.classList.add('active'); this.nextElementSibling.classList.remove('active')">🔧 Industrial</button>
<button onclick="document.documentElement.dataset.theme='modern'; this.classList.add('active'); this.previousElementSibling.classList.remove('active')">⚡ Moderno</button>
</div>
<h1>Etiqueta Código de Barras</h1>
<p class="subtitle">Etiquetas para impresora de etiquetas. Fondo blanco fijo (para impresión). Tamaños estándar 50x25mm y 50x30mm.</p>
<section>
<h2>Etiquetas 50x25mm</h2>
<div class="labels-grid" id="labels25"></div>
</section>
<section>
<h2>Etiquetas 50x30mm (con ubicación)</h2>
<div class="labels-grid" id="labels30"></div>
</section>
<section>
<h2>Acciones</h2>
<button class="btn-print" onclick="window.print()">🖨 Imprimir etiquetas</button>
<div class="note">
Las etiquetas siempre se renderizan con fondo blanco y texto negro independientemente del tema activo.
El CSS @media print oculta la interfaz y deja solo las etiquetas para impresión directa.
</div>
</section>
<script>
const products = [
{ partNo: 'BRK-4521', name: 'Balata delantera cerámica Brembo', barcode: '7501234567890', price: 485.00, location: 'A-03-12' },
{ partNo: 'FLT-0089', name: 'Filtro de aceite Fram PH3614', barcode: '7509876543210', price: 125.50, location: 'B-01-05' },
{ partNo: 'AMR-1200', name: 'Amortiguador trasero Monroe Sensatrac', barcode: '7504561237890', price: 1249.99, location: 'C-07-02' },
];
// Generate pseudo-barcode stripes from a number string
function generateBarcode(code) {
// Simple EAN-like pattern: start/end guards + data bars
const patterns = [];
// Start guard
patterns.push(1,0,1);
for (let i = 0; i < code.length; i++) {
const d = parseInt(code[i]);
// Simple visual pattern per digit (not real EAN encoding, just visually plausible)
const widths = [
[3,2,1,1], [2,2,2,1], [2,1,2,2], [1,4,1,1], [1,1,3,2],
[1,2,3,1], [1,1,1,4], [1,3,1,2], [1,2,1,3], [3,1,1,2]
];
const w = widths[d];
w.forEach((size, j) => {
for (let k = 0; k < size; k++) {
patterns.push(j % 2 === 0 ? 1 : 0);
}
});
if (i === 5) patterns.push(0,1,0,1,0); // center guard
}
// End guard
patterns.push(1,0,1);
return patterns;
}
function renderBarcode(code) {
const bars = generateBarcode(code);
let html = '<div class="barcode">';
bars.forEach(b => {
html += '<div class="' + (b ? 'b' : 'w') + '" style="width:1px;"></div>';
});
html += '</div>';
html += '<div class="barcode-number">' + code + '</div>';
return html;
}
function createLabel(product, sizeClass) {
const showLocation = sizeClass === 'label-50x30';
return `
<div class="label-preview">
<span class="size-badge">${sizeClass === 'label-50x25' ? '50 x 25 mm' : '50 x 30 mm'}</span>
<div class="label-card ${sizeClass}">
<div class="label-top">
<span class="label-partno">${product.partNo}</span>
${showLocation ? '<span class="label-location">' + product.location + '</span>' : ''}
</div>
<div class="label-name" title="${product.name}">${product.name}</div>
<div class="barcode-area">
${renderBarcode(product.barcode)}
</div>
<div class="label-bottom">
<span class="label-price">$${product.price.toFixed(2)}</span>
</div>
</div>
</div>
`;
}
// Render labels
const container25 = document.getElementById('labels25');
const container30 = document.getElementById('labels30');
products.forEach(p => {
container25.innerHTML += createLabel(p, 'label-50x25');
container30.innerHTML += createLabel(p, 'label-50x30');
});
</script>
</body>
</html>

View File

@@ -0,0 +1,338 @@
<!DOCTYPE html>
<html lang="es" data-theme="industrial">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus Autoparts — F-Keys Footer</title>
<link rel="stylesheet" href="../tokens/tokens.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body); background: var(--color-bg-base);
color: var(--color-text-primary); padding: var(--space-8);
padding-bottom: 120px;
transition: var(--transition-normal);
}
.theme-switcher {
position: sticky; top: 0; z-index: calc(var(--z-modal) + 10);
display: flex; gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
margin: calc(-1 * var(--space-8)); margin-bottom: var(--space-8);
}
.theme-switcher button {
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border);
background: var(--color-bg-base); color: var(--color-text-primary);
border-radius: var(--radius-md); cursor: pointer;
font-family: var(--font-body); font-size: var(--text-body-sm);
transition: var(--transition-fast);
}
.theme-switcher button.active {
background: var(--color-primary); color: var(--color-text-inverse);
border-color: var(--color-primary);
}
h1 { font-family: var(--font-heading); font-size: var(--text-h2);
font-weight: var(--heading-weight-primary); margin-bottom: var(--space-2);
color: var(--color-text-accent); }
.subtitle { color: var(--color-text-muted); font-size: var(--text-body); margin-bottom: var(--space-8); }
section { margin-bottom: var(--space-10); }
section > h2 {
font-family: var(--font-heading); font-size: var(--text-h4);
font-weight: var(--heading-weight-secondary); color: var(--color-text-secondary);
margin-bottom: var(--space-4); padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
.label-text {
font-size: var(--text-caption); color: var(--color-text-muted);
text-transform: uppercase; letter-spacing: var(--tracking-widest);
font-weight: var(--font-weight-semibold); margin-bottom: var(--space-3);
}
/* ====== F-Keys Footer Bar ====== */
.fkeys-footer {
position: fixed; bottom: 0; left: 0; right: 0;
background: var(--color-surface-2);
border-top: 2px solid var(--color-primary);
padding: var(--space-2) var(--space-4);
display: flex; align-items: center; justify-content: center;
gap: var(--space-1);
z-index: var(--z-sticky);
box-shadow: var(--shadow-lg);
}
.fkey {
display: flex; align-items: center; gap: var(--space-1);
padding: var(--space-2) var(--space-3);
background: transparent; border: 1px solid var(--color-border);
border-radius: var(--radius-md);
cursor: pointer; transition: var(--transition-fast);
white-space: nowrap; flex-shrink: 0;
}
.fkey:hover {
background: var(--color-primary-muted);
border-color: var(--color-primary);
}
.fkey:active {
background: var(--color-primary);
transform: scale(0.96);
}
.fkey:active .fkey-label { color: var(--color-text-inverse); }
.fkey:active .fkey-key { background: rgba(0,0,0,0.2); color: var(--color-text-inverse); border-color: transparent; }
.fkey-key {
display: inline-flex; align-items: center; justify-content: center;
min-width: 28px; height: 22px;
padding: 0 var(--space-2);
background: var(--color-bg-base);
border: 1px solid var(--color-border-strong);
border-radius: var(--radius-sm);
font-family: var(--font-mono); font-size: 11px;
font-weight: var(--font-weight-bold);
color: var(--color-text-accent);
line-height: 1;
}
.fkey-label {
font-size: var(--text-caption);
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
}
/* Separator */
.fkey-sep {
width: 1px; height: 24px;
background: var(--color-border);
margin: 0 var(--space-1);
flex-shrink: 0;
}
/* Highlight key (F3 = Cobrar) */
.fkey.highlight {
background: var(--color-primary-muted);
border-color: var(--color-primary);
}
.fkey.highlight .fkey-key {
background: var(--color-primary);
color: var(--color-text-inverse);
border-color: var(--color-primary);
}
.fkey.highlight .fkey-label {
color: var(--color-text-accent);
font-weight: var(--font-weight-bold);
}
/* ====== Inline Preview (non-fixed) ====== */
.fkeys-inline {
position: relative;
background: var(--color-surface-2);
border: 2px solid var(--color-primary);
border-radius: var(--radius-lg);
padding: var(--space-2) var(--space-4);
display: flex; align-items: center; justify-content: center;
gap: var(--space-1);
flex-wrap: wrap;
}
/* ====== Responsive: Stack on mobile ====== */
@media (max-width: 768px) {
.fkeys-footer, .fkeys-inline {
flex-wrap: wrap; gap: var(--space-1);
padding: var(--space-2);
}
.fkey {
padding: var(--space-1) var(--space-2);
}
.fkey-label { font-size: 10px; }
.fkey-key { min-width: 24px; height: 20px; font-size: 10px; }
.fkey-sep { display: none; }
}
/* ====== Compact variant ====== */
.fkeys-compact .fkey {
padding: var(--space-1) var(--space-2);
border: none; background: transparent;
}
.fkeys-compact .fkey:hover {
background: var(--color-primary-muted);
border-radius: var(--radius-sm);
}
.fkeys-compact .fkey-key {
min-width: 22px; height: 18px; font-size: 10px;
padding: 0 4px;
}
.fkeys-compact .fkey-label { font-size: 10px; }
</style>
</head>
<body>
<!-- Theme Switcher -->
<div class="theme-switcher">
<button class="active" onclick="setTheme('industrial')">A — Industrial Robusto</button>
<button onclick="setTheme('modern')">B — Técnico Moderno</button>
</div>
<h1>F-Keys Footer</h1>
<p class="subtitle">Barra fija inferior con atajos de teclado del POS: F1-F6, +/-, *, Esc.</p>
<!-- ============================================== -->
<!-- SECTION: Standard Footer -->
<!-- ============================================== -->
<section>
<h2>Barra Estándar (Fixed Bottom)</h2>
<p class="label-text">Preview inline — la barra real se muestra fija abajo de la página</p>
<div class="fkeys-inline">
<div class="fkey" title="Buscar producto">
<span class="fkey-key">F1</span>
<span class="fkey-label">Buscar</span>
</div>
<div class="fkey" title="Seleccionar cliente">
<span class="fkey-key">F2</span>
<span class="fkey-label">Cliente</span>
</div>
<div class="fkey highlight" title="Cobrar venta">
<span class="fkey-key">F3</span>
<span class="fkey-label">Cobrar</span>
</div>
<div class="fkey" title="Generar cotización">
<span class="fkey-key">F4</span>
<span class="fkey-label">Cotización</span>
</div>
<div class="fkey-sep"></div>
<div class="fkey" title="Última venta">
<span class="fkey-key">F5</span>
<span class="fkey-label">Últ.Venta</span>
</div>
<div class="fkey" title="Abrir cajón de dinero">
<span class="fkey-key">F6</span>
<span class="fkey-label">Cajón</span>
</div>
<div class="fkey-sep"></div>
<div class="fkey" title="Aumentar / Disminuir cantidad">
<span class="fkey-key">+/-</span>
<span class="fkey-label">Cantidad</span>
</div>
<div class="fkey" title="Aplicar descuento">
<span class="fkey-key">*</span>
<span class="fkey-label">Descuento</span>
</div>
<div class="fkey-sep"></div>
<div class="fkey" title="Cancelar / Limpiar venta">
<span class="fkey-key">Esc</span>
<span class="fkey-label">Cancelar</span>
</div>
</div>
</section>
<!-- ============================================== -->
<!-- SECTION: Compact Variant -->
<!-- ============================================== -->
<section>
<h2>Variante Compacta</h2>
<p class="label-text">Para pantallas chicas o cuando se necesita más espacio</p>
<div class="fkeys-inline fkeys-compact">
<div class="fkey"><span class="fkey-key">F1</span><span class="fkey-label">Buscar</span></div>
<div class="fkey"><span class="fkey-key">F2</span><span class="fkey-label">Cliente</span></div>
<div class="fkey highlight"><span class="fkey-key">F3</span><span class="fkey-label">Cobrar</span></div>
<div class="fkey"><span class="fkey-key">F4</span><span class="fkey-label">Cotización</span></div>
<div class="fkey"><span class="fkey-key">F5</span><span class="fkey-label">Últ.Venta</span></div>
<div class="fkey"><span class="fkey-key">F6</span><span class="fkey-label">Cajón</span></div>
<div class="fkey"><span class="fkey-key">+/-</span><span class="fkey-label">Cant</span></div>
<div class="fkey"><span class="fkey-key">*</span><span class="fkey-label">Desc</span></div>
<div class="fkey"><span class="fkey-key">Esc</span><span class="fkey-label">Salir</span></div>
</div>
</section>
<!-- ============================================== -->
<!-- SECTION: Extended keys -->
<!-- ============================================== -->
<section>
<h2>Variante Extendida (Admin)</h2>
<p class="label-text">Teclas adicionales para rol Admin/Owner</p>
<div class="fkeys-inline">
<div class="fkey"><span class="fkey-key">F1</span><span class="fkey-label">Buscar</span></div>
<div class="fkey"><span class="fkey-key">F2</span><span class="fkey-label">Cliente</span></div>
<div class="fkey highlight"><span class="fkey-key">F3</span><span class="fkey-label">Cobrar</span></div>
<div class="fkey"><span class="fkey-key">F4</span><span class="fkey-label">Cotización</span></div>
<div class="fkey"><span class="fkey-key">F5</span><span class="fkey-label">Últ.Venta</span></div>
<div class="fkey"><span class="fkey-key">F6</span><span class="fkey-label">Cajón</span></div>
<div class="fkey-sep"></div>
<div class="fkey"><span class="fkey-key">F7</span><span class="fkey-label">Corte</span></div>
<div class="fkey"><span class="fkey-key">F8</span><span class="fkey-label">Devolución</span></div>
<div class="fkey"><span class="fkey-key">F9</span><span class="fkey-label">Inventario</span></div>
<div class="fkey-sep"></div>
<div class="fkey"><span class="fkey-key">+/-</span><span class="fkey-label">Cant</span></div>
<div class="fkey"><span class="fkey-key">*</span><span class="fkey-label">Desc</span></div>
<div class="fkey"><span class="fkey-key">Esc</span><span class="fkey-label">Cancelar</span></div>
</div>
</section>
<!-- ============================================== -->
<!-- SECTION: Individual Key States -->
<!-- ============================================== -->
<section>
<h2>Estados de Tecla</h2>
<p class="label-text">Normal, hover, active, highlight, disabled</p>
<div style="display: flex; gap: var(--space-4); flex-wrap: wrap; align-items: center;">
<div>
<p class="label-text">Normal</p>
<div class="fkey"><span class="fkey-key">F1</span><span class="fkey-label">Buscar</span></div>
</div>
<div>
<p class="label-text">Highlight (primary action)</p>
<div class="fkey highlight"><span class="fkey-key">F3</span><span class="fkey-label">Cobrar</span></div>
</div>
<div>
<p class="label-text">Disabled</p>
<div class="fkey" style="opacity: 0.4; pointer-events: none;">
<span class="fkey-key">F8</span><span class="fkey-label">Devolución</span>
</div>
</div>
</div>
</section>
<!-- ====== FIXED FOOTER (actual position) ====== -->
<div class="fkeys-footer">
<div class="fkey" title="Buscar producto"><span class="fkey-key">F1</span><span class="fkey-label">Buscar</span></div>
<div class="fkey" title="Seleccionar cliente"><span class="fkey-key">F2</span><span class="fkey-label">Cliente</span></div>
<div class="fkey highlight" title="Cobrar venta"><span class="fkey-key">F3</span><span class="fkey-label">Cobrar</span></div>
<div class="fkey" title="Cotización"><span class="fkey-key">F4</span><span class="fkey-label">Cotización</span></div>
<div class="fkey-sep"></div>
<div class="fkey" title="Última venta"><span class="fkey-key">F5</span><span class="fkey-label">Últ.Venta</span></div>
<div class="fkey" title="Abrir cajón"><span class="fkey-key">F6</span><span class="fkey-label">Cajón</span></div>
<div class="fkey-sep"></div>
<div class="fkey" title="Cantidad"><span class="fkey-key">+/-</span><span class="fkey-label">Cantidad</span></div>
<div class="fkey" title="Descuento"><span class="fkey-key">*</span><span class="fkey-label">Descuento</span></div>
<div class="fkey-sep"></div>
<div class="fkey" title="Cancelar"><span class="fkey-key">Esc</span><span class="fkey-label">Cancelar</span></div>
</div>
<script>
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
document.querySelectorAll('.theme-switcher button').forEach(b => b.classList.remove('active'));
event.target.classList.add('active');
}
// Keyboard shortcuts demo
document.addEventListener('keydown', (e) => {
const keyMap = {
'F1': 'Buscar producto',
'F2': 'Seleccionar cliente',
'F3': 'Cobrar venta',
'F4': 'Generar cotización',
'F5': 'Última venta',
'F6': 'Abrir cajón',
'Escape': 'Cancelar'
};
if (keyMap[e.key]) {
e.preventDefault();
console.log('Atajo:', keyMap[e.key]);
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,358 @@
<!DOCTYPE html>
<html lang="es" data-theme="industrial">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus Autoparts — Gráfica de Barras</title>
<link rel="stylesheet" href="../tokens/tokens.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body); background: var(--color-bg-base);
color: var(--color-text-primary); padding: var(--space-8);
transition: var(--transition-normal);
}
.theme-switcher {
position: sticky; top: 0; z-index: calc(var(--z-modal) + 10);
display: flex; gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
margin: calc(-1 * var(--space-8)); margin-bottom: var(--space-8);
}
.theme-switcher button {
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border);
background: var(--color-bg-base); color: var(--color-text-primary);
border-radius: var(--radius-md); cursor: pointer;
font-family: var(--font-body); font-size: var(--text-body-sm);
transition: var(--transition-fast);
}
.theme-switcher button.active {
background: var(--color-primary); color: var(--color-text-inverse);
border-color: var(--color-primary);
}
h1 { font-family: var(--font-heading); font-size: var(--text-h2);
font-weight: var(--heading-weight-primary); margin-bottom: var(--space-2);
color: var(--color-text-accent); }
.subtitle { color: var(--color-text-muted); font-size: var(--text-body); margin-bottom: var(--space-8); }
section { margin-bottom: var(--space-10); }
section > h2 {
font-family: var(--font-heading); font-size: var(--text-h4);
font-weight: var(--heading-weight-secondary); color: var(--color-text-secondary);
margin-bottom: var(--space-4); padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
/* ── Chart Container ── */
.chart-container {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-6);
max-width: 800px;
}
.chart-title {
font-family: var(--font-heading);
font-size: var(--text-h5);
font-weight: var(--heading-weight-secondary);
color: var(--color-text-primary);
margin-bottom: var(--space-1);
}
.chart-subtitle {
font-size: var(--text-caption);
color: var(--color-text-muted);
margin-bottom: var(--space-6);
}
/* ── Chart Layout ── */
.chart-area {
display: flex;
gap: var(--space-2);
height: 320px;
}
/* Y-Axis */
.y-axis {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-end;
padding-bottom: 28px; /* match x-axis label space */
min-width: 52px;
}
.y-axis span {
font-size: var(--text-caption);
color: var(--color-text-muted);
font-family: var(--font-mono);
line-height: 1;
}
/* Bars Area */
.bars-area {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
}
/* Grid lines */
.grid-lines {
position: absolute;
inset: 0;
bottom: 28px;
display: flex;
flex-direction: column;
justify-content: space-between;
pointer-events: none;
}
.grid-line {
width: 100%;
height: 1px;
background: var(--color-border);
}
/* Bars flex container */
.bars-row {
flex: 1;
display: flex;
align-items: flex-end;
justify-content: space-around;
gap: var(--space-2);
position: relative;
z-index: 1;
}
/* Individual bar wrapper */
.bar-wrapper {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
justify-content: flex-end;
position: relative;
}
/* Bar element */
.bar {
width: 70%;
max-width: 56px;
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
background: var(--color-primary);
transition: height 0.8s var(--ease-out), background 0.15s ease;
height: 0;
cursor: pointer;
position: relative;
}
.bar:hover {
background: var(--color-primary-hover);
}
/* Tooltip */
.bar-tooltip {
position: absolute;
top: -36px;
left: 50%;
transform: translateX(-50%);
background: var(--color-bg-overlay);
color: var(--color-text-primary);
font-family: var(--font-mono);
font-size: var(--text-caption);
font-weight: var(--font-weight-semibold);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-sm);
border: 1px solid var(--color-border-strong);
box-shadow: var(--shadow-md);
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease;
}
.bar-tooltip::after {
content: '';
position: absolute;
bottom: -5px;
left: 50%;
transform: translateX(-50%) rotate(45deg);
width: 8px; height: 8px;
background: var(--color-bg-overlay);
border-right: 1px solid var(--color-border-strong);
border-bottom: 1px solid var(--color-border-strong);
}
.bar:hover .bar-tooltip {
opacity: 1;
}
/* X-Axis labels */
.x-labels {
display: flex;
justify-content: space-around;
padding-top: var(--space-2);
border-top: 1px solid var(--color-border);
}
.x-labels span {
flex: 1;
text-align: center;
font-size: var(--text-caption);
color: var(--color-text-muted);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: var(--tracking-wide);
}
/* ── Chart footer / total ── */
.chart-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: var(--space-4);
padding-top: var(--space-3);
border-top: 1px solid var(--color-border);
}
.chart-total {
font-family: var(--font-heading);
font-size: var(--text-h5);
font-weight: var(--heading-weight-primary);
color: var(--color-text-accent);
}
.chart-total-label {
font-size: var(--text-caption);
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
}
.chart-avg {
font-size: var(--text-body-sm);
color: var(--color-text-secondary);
}
.chart-avg strong {
font-family: var(--font-mono);
color: var(--color-text-primary);
}
/* Highlight best day */
.bar.best {
background: var(--color-success);
}
.bar.best:hover {
background: var(--color-success-dark);
}
.bar.worst {
opacity: 0.65;
}
</style>
</head>
<body>
<div class="theme-switcher">
<button class="active" onclick="document.documentElement.dataset.theme='industrial'; this.classList.add('active'); this.nextElementSibling.classList.remove('active')">🔧 Industrial</button>
<button onclick="document.documentElement.dataset.theme='modern'; this.classList.add('active'); this.previousElementSibling.classList.remove('active')">⚡ Moderno</button>
</div>
<h1>Gráfica de Barras</h1>
<p class="subtitle">Gráfica de barras verticales pura CSS/JS. Animación al cargar, tooltips al hover.</p>
<section>
<h2>Ventas Semanales</h2>
<div class="chart-container">
<div class="chart-title">Ventas por Día</div>
<div class="chart-subtitle">Semana del 24 al 30 de marzo 2026</div>
<div class="chart-area">
<div class="y-axis" id="yAxis"></div>
<div class="bars-area">
<div class="grid-lines" id="gridLines"></div>
<div class="bars-row" id="barsRow"></div>
<div class="x-labels" id="xLabels"></div>
</div>
</div>
<div class="chart-footer">
<div>
<div class="chart-total-label">Total semanal</div>
<div class="chart-total" id="totalValue"></div>
</div>
<div class="chart-avg">Promedio diario: <strong id="avgValue"></strong></div>
</div>
</div>
</section>
<script>
const data = [
{ day: 'Lun', value: 12500 },
{ day: 'Mar', value: 18300 },
{ day: 'Mié', value: 15700 },
{ day: 'Jue', value: 22100 },
{ day: 'Vie', value: 28500 },
{ day: 'Sáb', value: 31200 },
{ day: 'Dom', value: 8900 },
];
const maxVal = Math.max(...data.map(d => d.value));
const bestIdx = data.indexOf(data.find(d => d.value === maxVal));
const worstIdx = data.indexOf(data.find(d => d.value === Math.min(...data.map(d => d.value))));
// Round up max to nearest nice number for Y-axis
const yMax = Math.ceil(maxVal / 5000) * 5000;
const ySteps = 5;
// Build Y-axis labels
const yAxis = document.getElementById('yAxis');
for (let i = ySteps; i >= 0; i--) {
const val = (yMax / ySteps) * i;
const span = document.createElement('span');
span.textContent = '$' + (val / 1000).toFixed(0) + 'k';
yAxis.appendChild(span);
}
// Build grid lines
const gridLines = document.getElementById('gridLines');
for (let i = 0; i <= ySteps; i++) {
const line = document.createElement('div');
line.className = 'grid-line';
gridLines.appendChild(line);
}
// Build bars
const barsRow = document.getElementById('barsRow');
const xLabels = document.getElementById('xLabels');
data.forEach((d, i) => {
const wrapper = document.createElement('div');
wrapper.className = 'bar-wrapper';
const bar = document.createElement('div');
bar.className = 'bar';
if (i === bestIdx) bar.classList.add('best');
if (i === worstIdx) bar.classList.add('worst');
bar.dataset.height = ((d.value / yMax) * 100).toFixed(1);
const tooltip = document.createElement('div');
tooltip.className = 'bar-tooltip';
tooltip.textContent = '$' + d.value.toLocaleString('es-MX');
bar.appendChild(tooltip);
wrapper.appendChild(bar);
barsRow.appendChild(wrapper);
const label = document.createElement('span');
label.textContent = d.day;
xLabels.appendChild(label);
});
// Totals
const total = data.reduce((s, d) => s + d.value, 0);
document.getElementById('totalValue').textContent = '$' + total.toLocaleString('es-MX');
document.getElementById('avgValue').textContent = '$' + Math.round(total / data.length).toLocaleString('es-MX');
// Animate bars on load
window.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
document.querySelectorAll('.bar').forEach(bar => {
bar.style.height = bar.dataset.height + '%';
});
}, 150);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,477 @@
<!DOCTYPE html>
<html lang="es" data-theme="industrial">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus Autoparts — Modal de Confirmación</title>
<link rel="stylesheet" href="../tokens/tokens.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body); background: var(--color-bg-base);
color: var(--color-text-primary); padding: var(--space-8);
transition: var(--transition-normal);
}
.theme-switcher {
position: sticky; top: 0; z-index: calc(var(--z-modal) + 10);
display: flex; gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
margin: calc(-1 * var(--space-8)); margin-bottom: var(--space-8);
}
.theme-switcher button {
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border);
background: var(--color-bg-base); color: var(--color-text-primary);
border-radius: var(--radius-md); cursor: pointer;
font-family: var(--font-body); font-size: var(--text-body-sm);
transition: var(--transition-fast);
}
.theme-switcher button.active {
background: var(--color-primary); color: var(--color-text-inverse);
border-color: var(--color-primary);
}
h1 { font-family: var(--font-heading); font-size: var(--text-h2);
font-weight: var(--heading-weight-primary); margin-bottom: var(--space-2);
color: var(--color-text-accent); }
.subtitle { color: var(--color-text-muted); font-size: var(--text-body); margin-bottom: var(--space-8); }
section { margin-bottom: var(--space-10); }
section > h2 {
font-family: var(--font-heading); font-size: var(--text-h4);
font-weight: var(--heading-weight-secondary); color: var(--color-text-secondary);
margin-bottom: var(--space-4); padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
/* === Modal Component === */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scaleIn {
from { opacity: 0; transform: translate(-50%, -50%) scale(0.92); }
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes scaleOut {
from { opacity: 1; transform: translate(-50%, -50%) scale(1); }
to { opacity: 0; transform: translate(-50%, -50%) scale(0.92); }
}
.modal-overlay {
position: fixed;
inset: 0;
background: var(--overlay-backdrop, rgba(0, 0, 0, 0.6));
z-index: var(--z-modal, 1000);
display: none;
animation: fadeIn 0.2s ease-out forwards;
}
.modal-overlay.active {
display: block;
}
.modal-overlay.closing {
animation: fadeOut 0.2s ease-in forwards;
}
.modal-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: calc(var(--z-modal, 1000) + 1);
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-xl);
width: 90%;
max-width: 420px;
padding: var(--space-6);
text-align: center;
display: none;
animation: scaleIn 0.25s ease-out forwards;
}
.modal-dialog.active {
display: block;
}
.modal-dialog.closing {
animation: scaleOut 0.2s ease-in forwards;
}
.modal__icon {
width: 56px;
height: 56px;
border-radius: var(--radius-full);
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
margin: 0 auto var(--space-4);
}
.modal__icon--warning {
background: var(--color-warning-light);
}
.modal__icon--info {
background: var(--color-primary-muted);
}
.modal__icon--danger {
background: var(--color-error-light);
}
.modal__title {
font-family: var(--font-heading);
font-size: var(--text-h4);
font-weight: var(--heading-weight-primary);
color: var(--color-text-primary);
margin-bottom: var(--space-2);
}
.modal__desc {
font-size: var(--text-body-sm);
color: var(--color-text-muted);
line-height: 1.6;
margin-bottom: var(--space-6);
padding: 0 var(--space-2);
}
.modal__actions {
display: flex;
gap: var(--space-3);
justify-content: center;
}
.modal__btn {
flex: 1;
max-width: 180px;
padding: var(--space-3) var(--space-4);
font-family: var(--font-body);
font-size: var(--text-body-sm);
font-weight: 600;
border-radius: var(--radius-md);
cursor: pointer;
transition: var(--transition-fast);
border: 1px solid transparent;
}
.modal__btn--secondary {
background: var(--btn-secondary-bg);
color: var(--btn-secondary-text);
border-color: var(--btn-secondary-border);
}
.modal__btn--secondary:hover {
background: var(--btn-secondary-bg-hover);
}
.modal__btn--primary {
background: var(--btn-primary-bg);
color: var(--btn-primary-text);
border-color: var(--btn-primary-border);
}
.modal__btn--primary:hover {
background: var(--btn-primary-bg-hover);
box-shadow: var(--shadow-sm);
}
.modal__btn--danger {
background: var(--btn-danger-bg);
color: var(--btn-danger-text);
border-color: var(--btn-danger-bg);
}
.modal__btn--danger:hover {
opacity: 0.9;
box-shadow: var(--shadow-sm);
}
/* === Demo trigger buttons === */
.trigger-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: var(--space-4);
}
.trigger-card {
background: var(--color-surface-1);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-5);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-3);
}
.trigger-card__label {
font-family: var(--font-mono);
font-size: var(--text-body-sm);
color: var(--color-text-muted);
background: var(--color-bg-overlay);
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-full);
}
.trigger-card__desc {
font-size: var(--text-body-sm);
color: var(--color-text-muted);
text-align: center;
line-height: 1.5;
}
.trigger-btn {
padding: var(--space-3) var(--space-5);
font-family: var(--font-body);
font-size: var(--text-body-sm);
font-weight: 600;
border-radius: var(--radius-md);
cursor: pointer;
transition: var(--transition-fast);
border: 1px solid var(--color-border);
background: var(--color-bg-elevated);
color: var(--color-text-primary);
}
.trigger-btn:hover {
background: var(--color-surface-2);
box-shadow: var(--shadow-sm);
}
.trigger-btn--warning { border-color: var(--color-warning); }
.trigger-btn--primary { border-color: var(--color-primary); }
.trigger-btn--danger { border-color: var(--color-error); }
/* === Static previews === */
.static-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
gap: var(--space-6);
}
.static-preview {
background: var(--color-surface-1);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-5);
}
.static-preview .variant-label {
font-family: var(--font-mono);
font-size: var(--text-body-sm);
color: var(--color-text-muted);
background: var(--color-bg-overlay);
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-full);
display: inline-block;
margin-bottom: var(--space-4);
}
.static-modal {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-lg);
padding: var(--space-6);
text-align: center;
}
</style>
</head>
<body>
<div class="theme-switcher">
<button class="active" onclick="document.documentElement.dataset.theme='industrial'; this.classList.add('active'); this.nextElementSibling.classList.remove('active')">🔧 Industrial</button>
<button onclick="document.documentElement.dataset.theme='modern'; this.classList.add('active'); this.previousElementSibling.classList.remove('active')">⚡ Moderno</button>
</div>
<h1>Modal de Confirmación</h1>
<p class="subtitle">Diálogos modales para confirmar acciones destructivas o importantes del sistema POS.</p>
<!-- ========== Interactive triggers ========== -->
<section>
<h2>Demo interactivo — Abrir modales</h2>
<div class="trigger-grid">
<div class="trigger-card">
<span class="trigger-card__label">a) Cancelar venta</span>
<p class="trigger-card__desc">Confirma antes de descartar una venta en curso</p>
<button class="trigger-btn trigger-btn--warning" onclick="openModal('cancelar-venta')">Abrir modal</button>
</div>
<div class="trigger-card">
<span class="trigger-card__label">b) Cerrar caja</span>
<p class="trigger-card__desc">Genera corte de caja al final del turno</p>
<button class="trigger-btn trigger-btn--primary" onclick="openModal('cerrar-caja')">Abrir modal</button>
</div>
<div class="trigger-card">
<span class="trigger-card__label">c) Ajustar stock</span>
<p class="trigger-card__desc">Ajusta inventario de múltiples artículos</p>
<button class="trigger-btn trigger-btn--warning" onclick="openModal('ajustar-stock')">Abrir modal</button>
</div>
<div class="trigger-card">
<span class="trigger-card__label">d) Cerrar periodo</span>
<p class="trigger-card__desc">Cierra periodo fiscal contable</p>
<button class="trigger-btn trigger-btn--danger" onclick="openModal('cerrar-periodo')">Abrir modal</button>
</div>
</div>
</section>
<!-- ========== Static previews ========== -->
<section>
<h2>Variantes estáticas</h2>
<div class="static-grid">
<div class="static-preview">
<span class="variant-label">a) Cancelar venta</span>
<div class="static-modal">
<div class="modal__icon modal__icon--warning">⚠️</div>
<div class="modal__title">¿Cancelar venta?</div>
<div class="modal__desc">Se perderán los artículos agregados al ticket actual.</div>
<div class="modal__actions">
<button class="modal__btn modal__btn--secondary">No, continuar</button>
<button class="modal__btn modal__btn--danger">Sí, cancelar</button>
</div>
</div>
</div>
<div class="static-preview">
<span class="variant-label">b) Cerrar caja</span>
<div class="static-modal">
<div class="modal__icon modal__icon--info"></div>
<div class="modal__title">¿Cerrar caja?</div>
<div class="modal__desc">Se generará el corte de caja con total <strong style="color: var(--color-text-primary);">$15,800.00</strong></div>
<div class="modal__actions">
<button class="modal__btn modal__btn--secondary">Cancelar</button>
<button class="modal__btn modal__btn--primary">Cerrar caja</button>
</div>
</div>
</div>
<div class="static-preview">
<span class="variant-label">c) Ajustar stock</span>
<div class="static-modal">
<div class="modal__icon modal__icon--warning">⚠️</div>
<div class="modal__title">¿Ajustar stock?</div>
<div class="modal__desc">Se ajustarán <strong style="color: var(--color-text-primary);">5 artículos</strong> del inventario. Esta acción no se puede deshacer.</div>
<div class="modal__actions">
<button class="modal__btn modal__btn--secondary">Cancelar</button>
<button class="modal__btn modal__btn--primary">Ajustar</button>
</div>
</div>
</div>
<div class="static-preview">
<span class="variant-label">d) Cerrar periodo fiscal</span>
<div class="static-modal">
<div class="modal__icon modal__icon--danger">🛑</div>
<div class="modal__title">¿Cerrar periodo fiscal?</div>
<div class="modal__desc">Se cerrará el periodo <strong style="color: var(--color-text-primary);">Marzo 2026</strong>. No podrás registrar pólizas en este periodo.</div>
<div class="modal__actions">
<button class="modal__btn modal__btn--secondary">Cancelar</button>
<button class="modal__btn modal__btn--danger">Cerrar periodo</button>
</div>
</div>
</div>
</div>
</section>
<!-- ========== Actual Modal Overlays (hidden by default) ========== -->
<!-- a) Cancelar venta -->
<div class="modal-overlay" id="overlay-cancelar-venta" onclick="if(event.target===this) closeModal('cancelar-venta')"></div>
<div class="modal-dialog" id="modal-cancelar-venta">
<div class="modal__icon modal__icon--warning">⚠️</div>
<div class="modal__title">¿Cancelar venta?</div>
<div class="modal__desc">Se perderán los artículos agregados al ticket actual.</div>
<div class="modal__actions">
<button class="modal__btn modal__btn--secondary" onclick="closeModal('cancelar-venta')">No, continuar</button>
<button class="modal__btn modal__btn--danger" onclick="closeModal('cancelar-venta')">Sí, cancelar</button>
</div>
</div>
<!-- b) Cerrar caja -->
<div class="modal-overlay" id="overlay-cerrar-caja" onclick="if(event.target===this) closeModal('cerrar-caja')"></div>
<div class="modal-dialog" id="modal-cerrar-caja">
<div class="modal__icon modal__icon--info"></div>
<div class="modal__title">¿Cerrar caja?</div>
<div class="modal__desc">Se generará el corte de caja con total <strong style="color: var(--color-text-primary);">$15,800.00</strong></div>
<div class="modal__actions">
<button class="modal__btn modal__btn--secondary" onclick="closeModal('cerrar-caja')">Cancelar</button>
<button class="modal__btn modal__btn--primary" onclick="closeModal('cerrar-caja')">Cerrar caja</button>
</div>
</div>
<!-- c) Ajustar stock -->
<div class="modal-overlay" id="overlay-ajustar-stock" onclick="if(event.target===this) closeModal('ajustar-stock')"></div>
<div class="modal-dialog" id="modal-ajustar-stock">
<div class="modal__icon modal__icon--warning">⚠️</div>
<div class="modal__title">¿Ajustar stock?</div>
<div class="modal__desc">Se ajustarán <strong style="color: var(--color-text-primary);">5 artículos</strong> del inventario. Esta acción no se puede deshacer.</div>
<div class="modal__actions">
<button class="modal__btn modal__btn--secondary" onclick="closeModal('ajustar-stock')">Cancelar</button>
<button class="modal__btn modal__btn--primary" onclick="closeModal('ajustar-stock')">Ajustar</button>
</div>
</div>
<!-- d) Cerrar periodo fiscal -->
<div class="modal-overlay" id="overlay-cerrar-periodo" onclick="if(event.target===this) closeModal('cerrar-periodo')"></div>
<div class="modal-dialog" id="modal-cerrar-periodo">
<div class="modal__icon modal__icon--danger">🛑</div>
<div class="modal__title">¿Cerrar periodo fiscal?</div>
<div class="modal__desc">Se cerrará el periodo <strong style="color: var(--color-text-primary);">Marzo 2026</strong>. No podrás registrar pólizas en este periodo.</div>
<div class="modal__actions">
<button class="modal__btn modal__btn--secondary" onclick="closeModal('cerrar-periodo')">Cancelar</button>
<button class="modal__btn modal__btn--danger" onclick="closeModal('cerrar-periodo')">Cerrar periodo</button>
</div>
</div>
<script>
function openModal(id) {
const overlay = document.getElementById('overlay-' + id);
const dialog = document.getElementById('modal-' + id);
overlay.classList.remove('closing');
dialog.classList.remove('closing');
overlay.classList.add('active');
dialog.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeModal(id) {
const overlay = document.getElementById('overlay-' + id);
const dialog = document.getElementById('modal-' + id);
overlay.classList.add('closing');
dialog.classList.add('closing');
setTimeout(() => {
overlay.classList.remove('active', 'closing');
dialog.classList.remove('active', 'closing');
document.body.style.overflow = '';
}, 200);
}
// Close on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
document.querySelectorAll('.modal-overlay.active').forEach(overlay => {
const id = overlay.id.replace('overlay-', '');
closeModal(id);
});
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,555 @@
<!DOCTYPE html>
<html lang="es" data-theme="industrial">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus Autoparts — Modal de Pago</title>
<link rel="stylesheet" href="../tokens/tokens.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body); background: var(--color-bg-base);
color: var(--color-text-primary); padding: var(--space-8);
transition: var(--transition-normal);
}
.theme-switcher {
position: sticky; top: 0; z-index: calc(var(--z-modal) + 10);
display: flex; gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
margin: calc(-1 * var(--space-8)); margin-bottom: var(--space-8);
}
.theme-switcher button {
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border);
background: var(--color-bg-base); color: var(--color-text-primary);
border-radius: var(--radius-md); cursor: pointer;
font-family: var(--font-body); font-size: var(--text-body-sm);
transition: var(--transition-fast);
}
.theme-switcher button.active {
background: var(--color-primary); color: var(--color-text-inverse);
border-color: var(--color-primary);
}
h1 { font-family: var(--font-heading); font-size: var(--text-h2);
font-weight: var(--heading-weight-primary); margin-bottom: var(--space-2);
color: var(--color-text-accent); }
.subtitle { color: var(--color-text-muted); font-size: var(--text-body); margin-bottom: var(--space-8); }
section { margin-bottom: var(--space-10); }
section > h2 {
font-family: var(--font-heading); font-size: var(--text-h4);
font-weight: var(--heading-weight-secondary); color: var(--color-text-secondary);
margin-bottom: var(--space-4); padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
.label-text {
font-size: var(--text-caption); color: var(--color-text-muted);
text-transform: uppercase; letter-spacing: var(--tracking-widest);
font-weight: var(--font-weight-semibold); margin-bottom: var(--space-3);
}
/* ====== Overlay ====== */
.modal-overlay {
position: fixed; inset: 0;
background: var(--overlay-backdrop);
display: flex; align-items: center; justify-content: center;
z-index: var(--z-modal);
opacity: 0; pointer-events: none;
transition: var(--transition-normal);
}
.modal-overlay.open { opacity: 1; pointer-events: auto; }
/* ====== Modal Container ====== */
.modal-pago {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
width: 560px; max-width: 95vw;
max-height: 90vh; overflow-y: auto;
transform: translateY(20px);
transition: var(--transition-normal);
}
.modal-overlay.open .modal-pago { transform: translateY(0); }
/* ====== Header ====== */
.modal-header {
display: flex; align-items: center; justify-content: space-between;
padding: var(--space-5) var(--space-6);
border-bottom: 1px solid var(--color-border);
}
.modal-header h3 {
font-family: var(--font-heading); font-size: var(--text-h4);
font-weight: var(--heading-weight-primary); color: var(--color-text-primary);
}
.modal-close {
width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
background: transparent; border: 1px solid var(--color-border);
border-radius: var(--radius-md); cursor: pointer;
color: var(--color-text-muted); font-size: 18px;
transition: var(--transition-fast);
}
.modal-close:hover { background: var(--color-surface-2); color: var(--color-text-primary); }
/* ====== Total Banner ====== */
.total-banner {
background: var(--color-primary-muted);
border-bottom: 2px solid var(--color-primary);
padding: var(--space-4) var(--space-6);
display: flex; align-items: center; justify-content: space-between;
}
.total-label {
font-size: var(--text-body); font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary); text-transform: uppercase;
letter-spacing: var(--tracking-wide);
}
.total-amount {
font-family: var(--font-mono); font-size: var(--text-h3);
font-weight: var(--font-weight-bold); color: var(--color-text-accent);
}
/* ====== Items Summary ====== */
.items-summary {
padding: var(--space-3) var(--space-6);
border-bottom: 1px solid var(--color-border);
font-size: var(--text-body-sm); color: var(--color-text-muted);
display: flex; gap: var(--space-4);
}
/* ====== Tabs ====== */
.pago-tabs {
display: flex; border-bottom: 2px solid var(--color-border);
padding: 0 var(--space-6);
}
.pago-tab {
padding: var(--space-3) var(--space-5);
font-family: var(--font-body); font-size: var(--text-body-sm);
font-weight: var(--font-weight-semibold);
background: transparent; border: none;
color: var(--color-text-muted); cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -2px; transition: var(--transition-fast);
display: flex; align-items: center; gap: var(--space-2);
}
.pago-tab:hover { color: var(--color-text-primary); }
.pago-tab.active {
color: var(--color-text-accent);
border-bottom-color: var(--color-primary);
}
.pago-tab .tab-icon { font-size: 16px; }
/* ====== Tab Content ====== */
.tab-content { padding: var(--space-6); display: none; }
.tab-content.active { display: block; }
/* ====== Form Elements ====== */
.form-group { margin-bottom: var(--space-4); }
.form-label {
display: block; font-size: var(--text-caption); font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary); margin-bottom: var(--space-2);
text-transform: uppercase; letter-spacing: var(--tracking-wide);
}
.form-input {
width: 100%; padding: var(--space-3) var(--space-4);
font-family: var(--font-body); font-size: var(--text-body);
background: var(--color-bg-base); color: var(--color-text-primary);
border: 1px solid var(--color-border); border-radius: var(--radius-md);
transition: var(--transition-fast);
}
.form-input:focus { outline: none; border-color: var(--color-border-focus); box-shadow: var(--shadow-focus); }
.form-input-lg {
font-family: var(--font-mono); font-size: var(--text-h3);
font-weight: var(--font-weight-bold); text-align: right;
padding: var(--space-4) var(--space-5);
}
.form-hint {
font-size: var(--text-caption); color: var(--color-text-muted); margin-top: var(--space-1);
}
/* ====== Calculadora de Cambio ====== */
.cambio-display {
background: var(--color-surface-2);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-4) var(--space-5);
display: flex; align-items: center; justify-content: space-between;
margin-top: var(--space-4);
}
.cambio-label {
font-size: var(--text-body-sm); color: var(--color-text-secondary);
font-weight: var(--font-weight-semibold);
}
.cambio-amount {
font-family: var(--font-mono); font-size: var(--text-h4);
font-weight: var(--font-weight-bold);
}
.cambio-amount.positive { color: var(--color-success); }
.cambio-amount.negative { color: var(--color-error); }
.quick-amounts {
display: grid; grid-template-columns: repeat(4, 1fr);
gap: var(--space-2); margin-top: var(--space-3);
}
.quick-btn {
padding: var(--space-2) var(--space-3);
font-family: var(--font-mono); font-size: var(--text-body-sm);
font-weight: var(--font-weight-semibold);
background: var(--color-surface-2); color: var(--color-text-primary);
border: 1px solid var(--color-border); border-radius: var(--radius-md);
cursor: pointer; transition: var(--transition-fast); text-align: center;
}
.quick-btn:hover { background: var(--color-primary-muted); border-color: var(--color-primary); }
/* ====== Mixto Split ====== */
.split-row {
display: grid; grid-template-columns: 1fr auto 1fr;
gap: var(--space-3); align-items: end; margin-bottom: var(--space-3);
}
.split-method {
padding: var(--space-2) var(--space-3);
font-size: var(--text-body-sm); font-weight: var(--font-weight-semibold);
background: var(--color-surface-2); color: var(--color-text-secondary);
border-radius: var(--radius-md); text-align: center;
border: 1px solid var(--color-border);
}
.split-remaining {
background: var(--color-surface-2); border-radius: var(--radius-md);
padding: var(--space-3) var(--space-4);
display: flex; align-items: center; justify-content: space-between;
font-size: var(--text-body-sm); margin-top: var(--space-3);
}
.split-remaining .amount {
font-family: var(--font-mono); font-weight: var(--font-weight-bold);
color: var(--color-text-accent);
}
/* ====== Checkbox CFDI ====== */
.cfdi-check {
display: flex; align-items: center; gap: var(--space-3);
padding: var(--space-4) var(--space-6);
border-top: 1px solid var(--color-border);
background: var(--color-surface-1);
}
.cfdi-check input[type="checkbox"] {
width: 20px; height: 20px; accent-color: var(--color-primary);
cursor: pointer;
}
.cfdi-check label {
font-size: var(--text-body-sm); font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary); cursor: pointer;
}
.cfdi-check .cfdi-hint {
font-size: var(--text-caption); color: var(--color-text-muted);
margin-left: auto;
}
/* ====== Footer ====== */
.modal-footer {
padding: var(--space-4) var(--space-6);
border-top: 1px solid var(--color-border);
display: flex; gap: var(--space-3); justify-content: flex-end;
}
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
padding: var(--space-3) var(--space-6);
font-family: var(--font-body); font-size: var(--text-body);
font-weight: var(--font-weight-semibold); line-height: 1;
border: 2px solid transparent; border-radius: var(--radius-md);
cursor: pointer; transition: var(--transition-fast); white-space: nowrap;
}
.btn:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
.btn:active { transform: scale(0.97); }
.btn-primary { background: var(--btn-primary-bg); color: var(--btn-primary-text); }
.btn-primary:hover { background: var(--btn-primary-bg-hover); }
.btn-secondary { background: var(--btn-secondary-bg); color: var(--btn-secondary-text); border-color: var(--btn-secondary-border); }
.btn-secondary:hover { background: var(--btn-secondary-bg-hover); }
.btn-ghost { background: var(--btn-ghost-bg); color: var(--btn-ghost-text); border-color: var(--btn-ghost-border); }
.btn-ghost:hover { background: var(--color-surface-2); }
.btn-lg { padding: var(--space-4) var(--space-8); font-size: var(--text-body-lg); }
/* ====== Inline Preview (for showcase) ====== */
.preview-container {
background: var(--color-bg-base);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
margin-bottom: var(--space-8);
}
/* ====== Transfer / Card fields ====== */
.ref-field {
display: flex; gap: var(--space-3); align-items: end;
}
.ref-field .form-group { flex: 1; }
.status-indicator {
display: flex; align-items: center; gap: var(--space-2);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-md);
font-size: var(--text-body-sm); font-weight: var(--font-weight-semibold);
}
.status-indicator.pending {
background: var(--color-warning-light); color: var(--color-warning-dark);
}
.status-indicator.confirmed {
background: var(--color-success-light); color: var(--color-success-dark);
}
/* ====== Terminal Select ====== */
.terminal-cards {
display: grid; grid-template-columns: repeat(2, 1fr);
gap: var(--space-3);
}
.terminal-card {
padding: var(--space-4);
background: var(--color-surface-2);
border: 2px solid var(--color-border);
border-radius: var(--radius-md);
cursor: pointer; transition: var(--transition-fast);
text-align: center;
}
.terminal-card:hover { border-color: var(--color-primary); }
.terminal-card.selected {
border-color: var(--color-primary);
background: var(--color-primary-muted);
}
.terminal-card .terminal-name {
font-size: var(--text-body-sm); font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
}
.terminal-card .terminal-type {
font-size: var(--text-caption); color: var(--color-text-muted); margin-top: var(--space-1);
}
</style>
</head>
<body>
<!-- Theme Switcher -->
<div class="theme-switcher">
<button class="active" onclick="setTheme('industrial')">A — Industrial Robusto</button>
<button onclick="setTheme('modern')">B — Técnico Moderno</button>
</div>
<h1>Modal de Pago</h1>
<p class="subtitle">Componente de cobro con 4 métodos: Efectivo, Transferencia, Tarjeta, Mixto.</p>
<!-- ============================================== -->
<!-- SECTION: Full Modal Preview -->
<!-- ============================================== -->
<section>
<h2>Vista Completa del Modal</h2>
<p class="label-text">Preview interactivo — haz click en las tabs para cambiar de método</p>
<div class="preview-container">
<!-- Modal Header -->
<div class="modal-header">
<h3>Cobrar Venta</h3>
<button class="modal-close">&#x2715;</button>
</div>
<!-- Total Banner -->
<div class="total-banner">
<span class="total-label">Total a Cobrar</span>
<span class="total-amount">$4,827.50</span>
</div>
<!-- Items Summary -->
<div class="items-summary">
<span>3 productos</span>
<span>&#x2022;</span>
<span>Cliente: Taller Automotriz García</span>
<span>&#x2022;</span>
<span>Folio: V-2026-0847</span>
</div>
<!-- Payment Tabs -->
<div class="pago-tabs">
<button class="pago-tab active" onclick="switchPagoTab('efectivo')">
<span class="tab-icon">&#x1F4B5;</span> Efectivo
</button>
<button class="pago-tab" onclick="switchPagoTab('transferencia')">
<span class="tab-icon">&#x1F3E6;</span> Transferencia
</button>
<button class="pago-tab" onclick="switchPagoTab('tarjeta')">
<span class="tab-icon">&#x1F4B3;</span> Tarjeta
</button>
<button class="pago-tab" onclick="switchPagoTab('mixto')">
<span class="tab-icon">&#x1F504;</span> Mixto
</button>
</div>
<!-- TAB: Efectivo -->
<div class="tab-content active" id="tab-efectivo">
<div class="form-group">
<label class="form-label">Monto recibido</label>
<input type="text" class="form-input form-input-lg" value="$5,000.00" id="monto-recibido" oninput="calcCambio()">
</div>
<div class="quick-amounts">
<button class="quick-btn" onclick="setMonto(4828)">$4,828</button>
<button class="quick-btn" onclick="setMonto(4850)">$4,850</button>
<button class="quick-btn" onclick="setMonto(5000)">$5,000</button>
<button class="quick-btn" onclick="setMonto(5500)">$5,500</button>
</div>
<div class="cambio-display">
<span class="cambio-label">Cambio</span>
<span class="cambio-amount positive" id="cambio-valor">$172.50</span>
</div>
</div>
<!-- TAB: Transferencia -->
<div class="tab-content" id="tab-transferencia">
<div class="form-group">
<label class="form-label">Monto</label>
<input type="text" class="form-input form-input-lg" value="$4,827.50" readonly>
</div>
<div class="form-group">
<label class="form-label">Banco destino</label>
<select class="form-input">
<option>BBVA — **** 4521</option>
<option>Banorte — **** 7832</option>
</select>
</div>
<div class="ref-field">
<div class="form-group">
<label class="form-label">Referencia / No. operación</label>
<input type="text" class="form-input" placeholder="Ej: 2026040100847">
</div>
</div>
<div class="form-hint">Verificar que la transferencia se haya recibido antes de confirmar</div>
<div style="margin-top: var(--space-4);">
<div class="status-indicator pending">&#x23F3; Pendiente de confirmación</div>
</div>
</div>
<!-- TAB: Tarjeta -->
<div class="tab-content" id="tab-tarjeta">
<div class="form-group">
<label class="form-label">Monto</label>
<input type="text" class="form-input form-input-lg" value="$4,827.50" readonly>
</div>
<div class="form-group">
<label class="form-label">Terminal</label>
<div class="terminal-cards">
<div class="terminal-card selected">
<div class="terminal-name">Terminal 1</div>
<div class="terminal-type">Clip Pro — Visa/MC</div>
</div>
<div class="terminal-card">
<div class="terminal-name">Terminal 2</div>
<div class="terminal-type">Banorte TPV — Todas</div>
</div>
</div>
</div>
<div class="form-group">
<label class="form-label">No. de autorización</label>
<input type="text" class="form-input" placeholder="6 dígitos de autorización">
</div>
<div class="form-group">
<label class="form-label">Últimos 4 dígitos de tarjeta</label>
<input type="text" class="form-input" placeholder="Ej: 4521" maxlength="4" style="width: 120px;">
</div>
</div>
<!-- TAB: Mixto -->
<div class="tab-content" id="tab-mixto">
<div class="form-group">
<label class="form-label">Método 1 — Efectivo</label>
<div class="split-row">
<input type="text" class="form-input" value="$3,000.00" style="font-family: var(--font-mono); text-align: right;">
<div class="split-method">+</div>
<div>
<label class="form-label">Método 2 — Tarjeta</label>
<input type="text" class="form-input" value="$1,827.50" style="font-family: var(--font-mono); text-align: right;">
</div>
</div>
</div>
<div class="split-remaining">
<span>Restante por asignar:</span>
<span class="amount">$0.00</span>
</div>
<div class="form-group" style="margin-top: var(--space-4);">
<label class="form-label">No. de autorización (tarjeta)</label>
<input type="text" class="form-input" placeholder="6 dígitos">
</div>
</div>
<!-- CFDI Checkbox -->
<div class="cfdi-check">
<input type="checkbox" id="cfdi-check" checked>
<label for="cfdi-check">Facturar CFDI</label>
<span class="cfdi-hint">RFC: TAU150301XX1 — Régimen: 601</span>
</div>
<!-- Footer -->
<div class="modal-footer">
<button class="btn btn-ghost">Cancelar</button>
<button class="btn btn-primary btn-lg">&#x2714; Confirmar Pago — $4,827.50</button>
</div>
</div>
</section>
<!-- ============================================== -->
<!-- SECTION: States -->
<!-- ============================================== -->
<section>
<h2>Estados del Cambio</h2>
<p class="label-text">Indicador visual de cambio positivo, exacto y faltante</p>
<div style="display: flex; gap: var(--space-4); flex-wrap: wrap;">
<div class="cambio-display" style="flex: 1; min-width: 200px;">
<span class="cambio-label">Cambio</span>
<span class="cambio-amount positive">$172.50</span>
</div>
<div class="cambio-display" style="flex: 1; min-width: 200px;">
<span class="cambio-label">Cambio</span>
<span class="cambio-amount" style="color: var(--color-text-accent);">$0.00</span>
</div>
<div class="cambio-display" style="flex: 1; min-width: 200px;">
<span class="cambio-label">Faltante</span>
<span class="cambio-amount negative">-$327.50</span>
</div>
</div>
</section>
<!-- ============================================== -->
<!-- SECTION: Transfer Status -->
<!-- ============================================== -->
<section>
<h2>Estados de Transferencia</h2>
<div style="display: flex; gap: var(--space-4); flex-wrap: wrap;">
<div class="status-indicator pending">&#x23F3; Pendiente de confirmación</div>
<div class="status-indicator confirmed">&#x2705; Transferencia confirmada</div>
</div>
</section>
<script>
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
document.querySelectorAll('.theme-switcher button').forEach(b => b.classList.remove('active'));
event.target.classList.add('active');
}
function switchPagoTab(tabId) {
document.querySelectorAll('.pago-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
event.target.closest('.pago-tab').classList.add('active');
document.getElementById('tab-' + tabId).classList.add('active');
}
function setMonto(val) {
document.getElementById('monto-recibido').value = '$' + val.toLocaleString('en-US', {minimumFractionDigits: 2});
const cambio = val - 4827.50;
const el = document.getElementById('cambio-valor');
el.textContent = (cambio >= 0 ? '$' : '-$') + Math.abs(cambio).toLocaleString('en-US', {minimumFractionDigits: 2});
el.className = 'cambio-amount ' + (cambio > 0 ? 'positive' : cambio < 0 ? 'negative' : '');
}
function calcCambio() {
const raw = document.getElementById('monto-recibido').value.replace(/[^0-9.]/g, '');
const val = parseFloat(raw) || 0;
const cambio = val - 4827.50;
const el = document.getElementById('cambio-valor');
el.textContent = (cambio >= 0 ? '$' : '-$') + Math.abs(cambio).toLocaleString('en-US', {minimumFractionDigits: 2});
el.className = 'cambio-amount ' + (cambio > 0 ? 'positive' : cambio < 0 ? 'negative' : '');
}
</script>
</body>
</html>

View File

@@ -0,0 +1,560 @@
<!DOCTYPE html>
<html lang="es" data-theme="industrial">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus Autoparts — Panel Deslizante</title>
<link rel="stylesheet" href="../tokens/tokens.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body); background: var(--color-bg-base);
color: var(--color-text-primary); padding: var(--space-8);
transition: var(--transition-normal);
}
.theme-switcher {
position: sticky; top: 0; z-index: calc(var(--z-modal) + 10);
display: flex; gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
margin: calc(-1 * var(--space-8)); margin-bottom: var(--space-8);
}
.theme-switcher button {
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border);
background: var(--color-bg-base); color: var(--color-text-primary);
border-radius: var(--radius-md); cursor: pointer;
font-family: var(--font-body); font-size: var(--text-body-sm);
transition: var(--transition-fast);
}
.theme-switcher button.active {
background: var(--color-primary); color: var(--color-text-inverse);
border-color: var(--color-primary);
}
h1 { font-family: var(--font-heading); font-size: var(--text-h2);
font-weight: var(--heading-weight-primary); margin-bottom: var(--space-2);
color: var(--color-text-accent); }
.subtitle { color: var(--color-text-muted); font-size: var(--text-body); margin-bottom: var(--space-8); }
section { margin-bottom: var(--space-10); }
section > h2 {
font-family: var(--font-heading); font-size: var(--text-h4);
font-weight: var(--heading-weight-secondary); color: var(--color-text-secondary);
margin-bottom: var(--space-4); padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
/* Trigger button */
.btn-open-panel {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-5);
background: var(--btn-primary-bg);
color: var(--btn-primary-text);
border: 1px solid var(--btn-primary-border);
border-radius: var(--radius-md);
font-family: var(--font-body);
font-size: var(--text-body);
font-weight: var(--font-weight-semibold);
cursor: pointer;
transition: var(--transition-fast);
}
.btn-open-panel:hover {
background: var(--btn-primary-bg-hover);
}
.btn-open-panel:active {
background: var(--btn-primary-bg-active);
transform: scale(0.97);
}
/* Demo context: fake table behind */
.demo-table {
width: 100%;
border-collapse: collapse;
margin-top: var(--space-4);
}
.demo-table th, .demo-table td {
padding: var(--space-3) var(--space-4);
text-align: left;
border-bottom: 1px solid var(--color-border);
font-size: var(--text-body-sm);
}
.demo-table th {
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: var(--tracking-wide);
font-size: var(--text-caption);
font-weight: var(--font-weight-semibold);
background: var(--color-surface-1);
}
.demo-table td { color: var(--color-text-secondary); }
.demo-table .name-link {
color: var(--color-text-accent);
cursor: pointer;
text-decoration: none;
font-weight: var(--font-weight-semibold);
}
.demo-table .name-link:hover { text-decoration: underline; }
/* Overlay */
.panel-overlay {
position: fixed;
inset: 0;
background: var(--overlay-backdrop);
z-index: var(--z-modal);
opacity: 0;
visibility: hidden;
transition: opacity var(--duration-normal) var(--ease-out),
visibility var(--duration-normal) var(--ease-out);
}
.panel-overlay.open {
opacity: 1;
visibility: visible;
}
/* Slide-in panel */
.slide-panel {
position: fixed;
top: 0;
right: 0;
width: 380px;
max-width: 90vw;
height: 100vh;
background: var(--color-bg-elevated);
border-left: 1px solid var(--color-border);
box-shadow: var(--shadow-xl);
z-index: calc(var(--z-modal) + 1);
transform: translateX(100%);
transition: transform var(--duration-slow) var(--ease-out);
display: flex;
flex-direction: column;
overflow: hidden;
}
.slide-panel.open {
transform: translateX(0);
}
/* Panel header */
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
.panel-header h3 {
font-family: var(--font-heading);
font-size: var(--text-h5);
font-weight: var(--heading-weight-secondary);
color: var(--color-text-primary);
}
.btn-close {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
color: var(--color-text-muted);
cursor: pointer;
font-size: 18px;
transition: var(--transition-fast);
}
.btn-close:hover {
background: var(--color-surface-2);
color: var(--color-text-primary);
border-color: var(--color-border-strong);
}
/* Panel body */
.panel-body {
flex: 1;
overflow-y: auto;
padding: var(--space-5);
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
/* Client info sections */
.client-avatar-row {
display: flex;
align-items: center;
gap: var(--space-4);
margin-bottom: var(--space-5);
}
.client-avatar {
width: 56px;
height: 56px;
border-radius: var(--radius-full);
background: var(--color-primary-muted);
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-heading);
font-size: var(--text-h4);
font-weight: var(--heading-weight-primary);
color: var(--color-text-accent);
border: 2px solid var(--color-border-accent);
flex-shrink: 0;
}
.client-info-main .client-name {
font-family: var(--font-heading);
font-size: var(--text-h5);
font-weight: var(--heading-weight-primary);
color: var(--color-text-primary);
margin-bottom: 2px;
}
.client-info-main .client-rfc {
font-family: var(--font-mono);
font-size: var(--text-body-sm);
color: var(--color-text-muted);
}
.panel-section {
margin-bottom: var(--space-5);
}
.panel-section-title {
font-size: var(--text-caption);
text-transform: uppercase;
letter-spacing: var(--tracking-widest);
color: var(--color-text-muted);
font-weight: var(--font-weight-semibold);
margin-bottom: var(--space-3);
}
/* Credit bar */
.credit-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: var(--space-3);
margin-bottom: var(--space-3);
}
.credit-item {
text-align: center;
padding: var(--space-3);
background: var(--color-surface-1);
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
}
.credit-item .credit-label {
font-size: var(--text-caption);
color: var(--color-text-muted);
margin-bottom: var(--space-1);
}
.credit-item .credit-value {
font-family: var(--font-mono);
font-size: var(--text-body);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
}
.credit-item .credit-value.success { color: var(--color-success); }
.credit-item .credit-value.warning { color: var(--color-warning); }
.credit-bar-track {
height: 8px;
background: var(--color-surface-2);
border-radius: var(--radius-full);
overflow: hidden;
}
.credit-bar-fill {
height: 100%;
border-radius: var(--radius-full);
background: var(--color-primary);
transition: var(--transition-normal);
}
/* Vehicle list */
.vehicle-item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3);
background: var(--color-surface-1);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
margin-bottom: var(--space-2);
}
.vehicle-icon {
width: 36px;
height: 36px;
border-radius: var(--radius-md);
background: var(--color-surface-3);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.vehicle-details {
flex: 1;
min-width: 0;
}
.vehicle-name {
font-size: var(--text-body-sm);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
}
.vehicle-plate {
font-family: var(--font-mono);
font-size: var(--text-caption);
color: var(--color-text-muted);
}
/* Recent purchases mini-table */
.purchases-table {
width: 100%;
border-collapse: collapse;
}
.purchases-table th, .purchases-table td {
padding: var(--space-2) var(--space-2);
text-align: left;
font-size: var(--text-caption);
border-bottom: 1px solid var(--color-border);
}
.purchases-table th {
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: var(--tracking-wide);
font-weight: var(--font-weight-semibold);
}
.purchases-table td {
color: var(--color-text-secondary);
}
.purchases-table .amount-cell {
font-family: var(--font-mono);
text-align: right;
font-weight: var(--font-weight-semibold);
}
.purchases-table th:last-child { text-align: right; }
/* Panel footer */
.panel-footer {
padding: var(--space-4) var(--space-5);
border-top: 1px solid var(--color-border);
display: flex;
gap: var(--space-2);
flex-shrink: 0;
}
.panel-footer button {
flex: 1;
padding: var(--space-3);
border-radius: var(--radius-md);
font-family: var(--font-body);
font-size: var(--text-body-sm);
font-weight: var(--font-weight-semibold);
cursor: pointer;
transition: var(--transition-fast);
border: 1px solid;
}
.panel-footer .btn-edit {
background: var(--btn-secondary-bg);
color: var(--btn-secondary-text);
border-color: var(--btn-secondary-border);
}
.panel-footer .btn-edit:hover {
background: var(--btn-secondary-bg-hover);
}
.panel-footer .btn-sale {
background: var(--btn-primary-bg);
color: var(--btn-primary-text);
border-color: var(--btn-primary-border);
}
.panel-footer .btn-sale:hover {
background: var(--btn-primary-bg-hover);
}
</style>
</head>
<body>
<div class="theme-switcher">
<button class="active" onclick="document.documentElement.dataset.theme='industrial'; this.classList.add('active'); this.nextElementSibling.classList.remove('active')">🔧 Industrial</button>
<button onclick="document.documentElement.dataset.theme='modern'; this.classList.add('active'); this.previousElementSibling.classList.remove('active')">⚡ Moderno</button>
</div>
<h1>Panel Deslizante</h1>
<p class="subtitle">Panel lateral que se desliza desde la derecha con detalle de cliente</p>
<section>
<h2>Contexto: Lista de Clientes</h2>
<p style="color: var(--color-text-secondary); margin-bottom: var(--space-4); font-size: var(--text-body-sm);">
Haz clic en un nombre para abrir el panel deslizante.
</p>
<table class="demo-table">
<thead>
<tr>
<th>Cliente</th>
<th>RFC</th>
<th>Saldo</th>
<th>Vehículos</th>
</tr>
</thead>
<tbody>
<tr>
<td><a class="name-link" onclick="openPanel()">Roberto Méndez Gutiérrez</a></td>
<td style="font-family: var(--font-mono); font-size: var(--text-caption);">MEGR820415QR3</td>
<td style="font-family: var(--font-mono);">$12,450.00</td>
<td>3</td>
</tr>
<tr>
<td><a class="name-link" onclick="openPanel()">Taller Mecánico Hermanos López</a></td>
<td style="font-family: var(--font-mono); font-size: var(--text-caption);">THL950220MN8</td>
<td style="font-family: var(--font-mono);">$28,900.00</td>
<td>--</td>
</tr>
<tr>
<td><a class="name-link" onclick="openPanel()">María Elena Ríos</a></td>
<td style="font-family: var(--font-mono); font-size: var(--text-caption);">RIEM770903AB1</td>
<td style="font-family: var(--font-mono);">$0.00</td>
<td>1</td>
</tr>
</tbody>
</table>
<button class="btn-open-panel" onclick="openPanel()" style="margin-top: var(--space-6);">
<span>👤</span> Ver Detalle de Cliente
</button>
</section>
<!-- Overlay -->
<div class="panel-overlay" id="panelOverlay" onclick="closePanel()"></div>
<!-- Slide Panel -->
<div class="slide-panel" id="slidePanel">
<div class="panel-header">
<h3>Detalle de Cliente</h3>
<button class="btn-close" onclick="closePanel()" title="Cerrar">&times;</button>
</div>
<div class="panel-body">
<!-- Client avatar + name -->
<div class="client-avatar-row">
<div class="client-avatar">RM</div>
<div class="client-info-main">
<div class="client-name">Roberto Méndez Gutiérrez</div>
<div class="client-rfc">RFC: MEGR820415QR3</div>
</div>
</div>
<!-- Credit info -->
<div class="panel-section">
<div class="panel-section-title">Crédito</div>
<div class="credit-grid">
<div class="credit-item">
<div class="credit-label">Límite</div>
<div class="credit-value">$50,000</div>
</div>
<div class="credit-item">
<div class="credit-label">Utilizado</div>
<div class="credit-value warning">$12,450</div>
</div>
<div class="credit-item">
<div class="credit-label">Disponible</div>
<div class="credit-value success">$37,550</div>
</div>
</div>
<div class="credit-bar-track">
<div class="credit-bar-fill" style="width: 24.9%;"></div>
</div>
</div>
<!-- Vehicles -->
<div class="panel-section">
<div class="panel-section-title">Vehículos Registrados</div>
<div class="vehicle-item">
<div class="vehicle-icon">🚗</div>
<div class="vehicle-details">
<div class="vehicle-name">Nissan Sentra 2019</div>
<div class="vehicle-plate">JKL-4521 / Motor: MR20DE</div>
</div>
</div>
<div class="vehicle-item">
<div class="vehicle-icon">🛻</div>
<div class="vehicle-details">
<div class="vehicle-name">Toyota Hilux 2021</div>
<div class="vehicle-plate">ABC-7890 / Motor: 2GD-FTV</div>
</div>
</div>
<div class="vehicle-item">
<div class="vehicle-icon">🚗</div>
<div class="vehicle-details">
<div class="vehicle-name">VW Jetta 2017</div>
<div class="vehicle-plate">MNO-1234 / Motor: EA211</div>
</div>
</div>
</div>
<!-- Recent purchases -->
<div class="panel-section">
<div class="panel-section-title">Compras Recientes</div>
<table class="purchases-table">
<thead>
<tr>
<th>Fecha</th>
<th>Folio</th>
<th style="text-align:right;">Total</th>
</tr>
</thead>
<tbody>
<tr>
<td>28/Mar/2026</td>
<td>V-04521</td>
<td class="amount-cell">$3,240.00</td>
</tr>
<tr>
<td>15/Mar/2026</td>
<td>V-04389</td>
<td class="amount-cell">$1,890.50</td>
</tr>
<tr>
<td>02/Mar/2026</td>
<td>V-04210</td>
<td class="amount-cell">$5,620.00</td>
</tr>
<tr>
<td>18/Feb/2026</td>
<td>V-04098</td>
<td class="amount-cell">$780.00</td>
</tr>
<tr>
<td>05/Feb/2026</td>
<td>V-03955</td>
<td class="amount-cell">$2,410.00</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="panel-footer">
<button class="btn-edit">Editar Cliente</button>
<button class="btn-sale">Nueva Venta</button>
</div>
</div>
<script>
const overlay = document.getElementById('panelOverlay');
const panel = document.getElementById('slidePanel');
function openPanel() {
overlay.classList.add('open');
panel.classList.add('open');
document.body.style.overflow = 'hidden';
}
function closePanel() {
overlay.classList.remove('open');
panel.classList.remove('open');
document.body.style.overflow = '';
}
// Close on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closePanel();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,414 @@
<!DOCTYPE html>
<html lang="es" data-theme="industrial">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus Autoparts — Selector de Período</title>
<link rel="stylesheet" href="../tokens/tokens.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body); background: var(--color-bg-base);
color: var(--color-text-primary); padding: var(--space-8);
transition: var(--transition-normal);
}
.theme-switcher {
position: sticky; top: 0; z-index: calc(var(--z-modal) + 10);
display: flex; gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
margin: calc(-1 * var(--space-8)); margin-bottom: var(--space-8);
}
.theme-switcher button {
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border);
background: var(--color-bg-base); color: var(--color-text-primary);
border-radius: var(--radius-md); cursor: pointer;
font-family: var(--font-body); font-size: var(--text-body-sm);
transition: var(--transition-fast);
}
.theme-switcher button.active {
background: var(--color-primary); color: var(--color-text-inverse);
border-color: var(--color-primary);
}
h1 { font-family: var(--font-heading); font-size: var(--text-h2);
font-weight: var(--heading-weight-primary); margin-bottom: var(--space-2);
color: var(--color-text-accent); }
.subtitle { color: var(--color-text-muted); font-size: var(--text-body); margin-bottom: var(--space-8); }
section { margin-bottom: var(--space-10); }
section > h2 {
font-family: var(--font-heading); font-size: var(--text-h4);
font-weight: var(--heading-weight-secondary); color: var(--color-text-secondary);
margin-bottom: var(--space-4); padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
/* ── Shared form styles ── */
.form-label {
display: block;
font-size: var(--text-label);
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
margin-bottom: var(--space-1);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
}
select, input[type="date"] {
appearance: none;
-webkit-appearance: none;
font-family: var(--font-body);
font-size: var(--text-body-sm);
color: var(--color-text-primary);
background: var(--color-bg-base);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-3);
padding-right: var(--space-8);
transition: var(--transition-fast);
cursor: pointer;
min-width: 0;
}
select:hover, input[type="date"]:hover {
border-color: var(--color-border-strong);
}
select:focus, input[type="date"]:focus {
outline: none;
border-color: var(--color-border-focus);
box-shadow: var(--shadow-focus);
}
input[type="date"] {
padding-right: var(--space-3);
}
/* Select arrow */
.select-wrap {
position: relative;
display: inline-block;
}
.select-wrap::after {
content: '';
position: absolute;
right: 12px; top: 50%;
transform: translateY(-50%);
width: 0; height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid var(--color-text-muted);
pointer-events: none;
}
/* ── Buttons ── */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
font-family: var(--font-body);
font-size: var(--text-body-sm);
font-weight: var(--font-weight-semibold);
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-md);
border: 1px solid transparent;
cursor: pointer;
transition: var(--transition-fast);
white-space: nowrap;
}
.btn-primary {
background: var(--btn-primary-bg);
color: var(--btn-primary-text);
border-color: var(--btn-primary-border);
}
.btn-primary:hover { background: var(--btn-primary-bg-hover); }
.btn-primary:active { background: var(--btn-primary-bg-active); }
.btn-ghost {
background: var(--btn-ghost-bg);
color: var(--btn-ghost-text);
border-color: var(--btn-ghost-border);
}
.btn-ghost:hover {
background: var(--color-surface-2);
}
/* ── Period Selector: Dropdown variant ── */
.period-selector {
display: flex;
align-items: flex-end;
gap: var(--space-3);
flex-wrap: wrap;
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-4) var(--space-5);
}
.period-selector .field {
display: flex;
flex-direction: column;
}
.period-selector select {
min-width: 140px;
}
/* ── Presets (quick actions) ── */
.presets {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.preset-btn {
display: inline-flex;
align-items: center;
gap: var(--space-1);
font-family: var(--font-body);
font-size: var(--text-caption);
font-weight: var(--font-weight-semibold);
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-full);
border: 1px solid var(--color-border);
background: var(--color-bg-base);
color: var(--color-text-secondary);
cursor: pointer;
transition: var(--transition-fast);
white-space: nowrap;
}
.preset-btn:hover {
border-color: var(--color-primary);
color: var(--color-primary);
background: var(--color-primary-muted);
}
.preset-btn.active {
background: var(--color-primary);
color: var(--color-text-inverse);
border-color: var(--color-primary);
}
/* ── Date Range variant ── */
.date-range-selector {
display: flex;
align-items: flex-end;
gap: var(--space-3);
flex-wrap: wrap;
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-4) var(--space-5);
}
.date-range-selector .field {
display: flex;
flex-direction: column;
}
.separator {
font-size: var(--text-body);
color: var(--color-text-muted);
padding-bottom: var(--space-2);
font-weight: var(--font-weight-semibold);
}
/* ── Selected period display ── */
.selected-display {
margin-top: var(--space-3);
padding: var(--space-3) var(--space-4);
background: var(--color-primary-muted);
border-left: 3px solid var(--color-primary);
border-radius: 0 var(--radius-md) var(--radius-md) 0;
font-size: var(--text-body-sm);
color: var(--color-text-secondary);
}
.selected-display strong {
color: var(--color-text-accent);
font-family: var(--font-mono);
}
/* ── Compact inline variant ── */
.period-inline {
display: inline-flex;
align-items: center;
gap: var(--space-2);
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-2) var(--space-3);
}
.period-inline select {
padding: var(--space-1) var(--space-2);
padding-right: var(--space-6);
font-size: var(--text-caption);
min-width: 100px;
}
.period-inline .btn {
padding: var(--space-1) var(--space-3);
font-size: var(--text-caption);
}
.period-inline .select-wrap::after {
right: 8px;
}
</style>
</head>
<body>
<div class="theme-switcher">
<button class="active" onclick="document.documentElement.dataset.theme='industrial'; this.classList.add('active'); this.nextElementSibling.classList.remove('active')">🔧 Industrial</button>
<button onclick="document.documentElement.dataset.theme='modern'; this.classList.add('active'); this.previousElementSibling.classList.remove('active')">⚡ Moderno</button>
</div>
<h1>Selector de Período</h1>
<p class="subtitle">Controles para seleccionar períodos de reporte: mes/año, presets rápidos y rango de fechas.</p>
<!-- ── Variant A: Dropdowns + button ── -->
<section>
<h2>Selector por Mes y Año</h2>
<div class="period-selector">
<div class="field">
<label class="form-label" for="selYear">Año</label>
<div class="select-wrap">
<select id="selYear">
<option value="2024">2024</option>
<option value="2025">2025</option>
<option value="2026" selected>2026</option>
</select>
</div>
</div>
<div class="field">
<label class="form-label" for="selMonth">Mes</label>
<div class="select-wrap">
<select id="selMonth">
<option value="1">Enero</option>
<option value="2">Febrero</option>
<option value="3">Marzo</option>
<option value="4" selected>Abril</option>
<option value="5">Mayo</option>
<option value="6">Junio</option>
<option value="7">Julio</option>
<option value="8">Agosto</option>
<option value="9">Septiembre</option>
<option value="10">Octubre</option>
<option value="11">Noviembre</option>
<option value="12">Diciembre</option>
</select>
</div>
</div>
<button class="btn btn-primary" onclick="consultarPeriodo()">Consultar</button>
</div>
<div class="selected-display" id="displayPeriod">
Período seleccionado: <strong>Abril 2026</strong>
</div>
</section>
<!-- ── Variant B: Quick presets ── -->
<section>
<h2>Presets Rápidos</h2>
<div class="period-selector" style="flex-direction: column; align-items: flex-start; gap: var(--space-3);">
<div class="presets" id="presetGroup">
<button class="preset-btn active" onclick="selectPreset(this, 'Este mes')">Este mes</button>
<button class="preset-btn" onclick="selectPreset(this, 'Mes anterior')">Mes anterior</button>
<button class="preset-btn" onclick="selectPreset(this, 'Este trimestre')">Este trimestre</button>
<button class="preset-btn" onclick="selectPreset(this, 'Este año')">Este año</button>
</div>
<div class="selected-display" id="displayPreset">
Período: <strong>Abril 2026</strong> (Este mes)
</div>
</div>
</section>
<!-- ── Variant C: Date range ── -->
<section>
<h2>Rango de Fechas</h2>
<div class="date-range-selector">
<div class="field">
<label class="form-label" for="dateFrom">Desde</label>
<input type="date" id="dateFrom" value="2026-04-01">
</div>
<span class="separator"></span>
<div class="field">
<label class="form-label" for="dateTo">Hasta</label>
<input type="date" id="dateTo" value="2026-04-30">
</div>
<button class="btn btn-primary" onclick="consultarRango()">Consultar</button>
<button class="btn btn-ghost" onclick="limpiarRango()">Limpiar</button>
</div>
<div class="selected-display" id="displayRange">
Rango: <strong>01/04/2026</strong><strong>30/04/2026</strong> (30 días)
</div>
</section>
<!-- ── Variant D: Compact inline ── -->
<section>
<h2>Variante Compacta (Inline)</h2>
<p style="color: var(--color-text-muted); font-size: var(--text-body-sm); margin-bottom: var(--space-3);">
Para uso en headers de tablas o secciones con espacio reducido.
</p>
<div class="period-inline">
<div class="select-wrap">
<select>
<option>Abril</option>
<option>Marzo</option>
<option>Febrero</option>
</select>
</div>
<div class="select-wrap">
<select>
<option>2026</option>
<option>2025</option>
<option>2024</option>
</select>
</div>
<button class="btn btn-primary">Ir</button>
</div>
</section>
<script>
const meses = ['Enero','Febrero','Marzo','Abril','Mayo','Junio',
'Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'];
function consultarPeriodo() {
const year = document.getElementById('selYear').value;
const monthIdx = document.getElementById('selMonth').value;
const monthName = meses[monthIdx - 1];
document.getElementById('displayPeriod').innerHTML =
'Período seleccionado: <strong>' + monthName + ' ' + year + '</strong>';
}
function selectPreset(btn, label) {
document.querySelectorAll('#presetGroup .preset-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const now = new Date(2026, 3, 1); // April 2026
let text = '';
switch (label) {
case 'Este mes':
text = 'Período: <strong>Abril 2026</strong> (Este mes)';
break;
case 'Mes anterior':
text = 'Período: <strong>Marzo 2026</strong> (Mes anterior)';
break;
case 'Este trimestre':
text = 'Período: <strong>Enero — Marzo 2026</strong> (Q1 2026)';
break;
case 'Este año':
text = 'Período: <strong>Enero — Diciembre 2026</strong> (Año completo)';
break;
}
document.getElementById('displayPreset').innerHTML = text;
}
function consultarRango() {
const from = document.getElementById('dateFrom').value;
const to = document.getElementById('dateTo').value;
if (!from || !to) return;
const d1 = new Date(from), d2 = new Date(to);
const days = Math.round((d2 - d1) / (1000 * 60 * 60 * 24)) + 1;
const fmt = d => d.toLocaleDateString('es-MX', { day: '2-digit', month: '2-digit', year: 'numeric' });
document.getElementById('displayRange').innerHTML =
'Rango: <strong>' + fmt(d1) + '</strong> — <strong>' + fmt(d2) + '</strong> (' + days + ' días)';
}
function limpiarRango() {
document.getElementById('dateFrom').value = '';
document.getElementById('dateTo').value = '';
document.getElementById('displayRange').innerHTML =
'Rango: <strong>—</strong>';
}
</script>
</body>
</html>

View File

@@ -0,0 +1,283 @@
<!DOCTYPE html>
<html lang="es" data-theme="industrial">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus Autoparts — Tarjeta Métrica</title>
<link rel="stylesheet" href="../tokens/tokens.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body); background: var(--color-bg-base);
color: var(--color-text-primary); padding: var(--space-8);
transition: var(--transition-normal);
}
.theme-switcher {
position: sticky; top: 0; z-index: calc(var(--z-modal) + 10);
display: flex; gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
margin: calc(-1 * var(--space-8)); margin-bottom: var(--space-8);
}
.theme-switcher button {
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border);
background: var(--color-bg-base); color: var(--color-text-primary);
border-radius: var(--radius-md); cursor: pointer;
font-family: var(--font-body); font-size: var(--text-body-sm);
transition: var(--transition-fast);
}
.theme-switcher button.active {
background: var(--color-primary); color: var(--color-text-inverse);
border-color: var(--color-primary);
}
h1 { font-family: var(--font-heading); font-size: var(--text-h2);
font-weight: var(--heading-weight-primary); margin-bottom: var(--space-2);
color: var(--color-text-accent); }
.subtitle { color: var(--color-text-muted); font-size: var(--text-body); margin-bottom: var(--space-8); }
section { margin-bottom: var(--space-10); }
section > h2 {
font-family: var(--font-heading); font-size: var(--text-h4);
font-weight: var(--heading-weight-secondary); color: var(--color-text-secondary);
margin-bottom: var(--space-4); padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
/* ── Metric Card Grid ── */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: var(--space-5);
}
/* ── Metric Card ── */
.metric-card {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-5) var(--space-5) var(--space-4);
transition: var(--transition-normal);
position: relative;
overflow: hidden;
}
.metric-card:hover {
border-color: var(--color-border-accent);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.metric-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 3px;
background: var(--color-primary);
opacity: 0;
transition: var(--transition-fast);
}
.metric-card:hover::before { opacity: 1; }
.metric-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: var(--space-1);
}
.metric-value {
font-family: var(--font-heading);
font-size: var(--text-h2);
font-weight: var(--heading-weight-primary);
line-height: 1.1;
color: var(--color-text-primary);
letter-spacing: var(--tracking-tight);
}
.metric-trend {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-full);
font-size: var(--text-caption);
font-weight: var(--font-weight-semibold);
line-height: 1;
flex-shrink: 0;
margin-top: var(--space-1);
}
.metric-trend.up {
background: var(--color-success-light);
color: var(--color-success-dark);
}
.metric-trend.down {
background: var(--color-error-light);
color: var(--color-error-dark);
}
.metric-trend .arrow {
font-size: 0.875rem;
line-height: 1;
}
.metric-label {
font-size: var(--text-caption);
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: var(--tracking-widest);
font-weight: var(--font-weight-semibold);
margin-bottom: var(--space-4);
}
/* ── Progress Bar ── */
.metric-progress {
margin-top: auto;
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-1);
}
.progress-text {
font-size: var(--text-caption);
color: var(--color-text-muted);
}
.progress-value {
font-size: var(--text-caption);
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
}
.progress-track {
width: 100%;
height: 6px;
background: var(--color-surface-3);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: var(--radius-full);
background: var(--color-primary);
transition: width 0.8s var(--ease-out);
width: 0;
}
.progress-fill.success { background: var(--color-success); }
.progress-fill.warning { background: var(--color-warning); }
.progress-fill.danger { background: var(--color-error); }
/* ── Icon container ── */
.metric-icon {
width: 40px; height: 40px;
display: flex; align-items: center; justify-content: center;
border-radius: var(--radius-md);
background: var(--color-primary-muted);
color: var(--color-primary);
font-size: 1.25rem;
flex-shrink: 0;
margin-bottom: var(--space-3);
}
</style>
</head>
<body>
<div class="theme-switcher">
<button class="active" onclick="document.documentElement.dataset.theme='industrial'; this.classList.add('active'); this.nextElementSibling.classList.remove('active')">🔧 Industrial</button>
<button onclick="document.documentElement.dataset.theme='modern'; this.classList.add('active'); this.previousElementSibling.classList.remove('active')">⚡ Moderno</button>
</div>
<h1>Tarjeta Métrica</h1>
<p class="subtitle">Tarjetas de KPI con valor principal, tendencia y barra de progreso vs meta.</p>
<section>
<h2>Dashboard de Ventas</h2>
<div class="metrics-grid">
<!-- Card 1: Ventas Hoy -->
<div class="metric-card">
<div class="metric-icon">💰</div>
<div class="metric-header">
<span class="metric-value">$48,250</span>
<span class="metric-trend up"><span class="arrow"></span> 12.5%</span>
</div>
<div class="metric-label">Ventas Hoy</div>
<div class="metric-progress">
<div class="progress-header">
<span class="progress-text">Meta diaria</span>
<span class="progress-value">85%</span>
</div>
<div class="progress-track">
<div class="progress-fill" data-width="85"></div>
</div>
</div>
</div>
<!-- Card 2: Ticket Promedio -->
<div class="metric-card">
<div class="metric-icon">🧾</div>
<div class="metric-header">
<span class="metric-value">$1,285</span>
<span class="metric-trend up"><span class="arrow"></span> 3.2%</span>
</div>
<div class="metric-label">Ticket Promedio</div>
<div class="metric-progress">
<div class="progress-header">
<span class="progress-text">Meta $1,500</span>
<span class="progress-value">86%</span>
</div>
<div class="progress-track">
<div class="progress-fill" data-width="86"></div>
</div>
</div>
</div>
<!-- Card 3: Margen Promedio -->
<div class="metric-card">
<div class="metric-icon">📊</div>
<div class="metric-header">
<span class="metric-value">32.4%</span>
<span class="metric-trend down"><span class="arrow"></span> 1.8%</span>
</div>
<div class="metric-label">Margen Promedio</div>
<div class="metric-progress">
<div class="progress-header">
<span class="progress-text">Meta 35%</span>
<span class="progress-value">93%</span>
</div>
<div class="progress-track">
<div class="progress-fill warning" data-width="93"></div>
</div>
</div>
</div>
<!-- Card 4: Efectivo en Caja -->
<div class="metric-card">
<div class="metric-icon">🏦</div>
<div class="metric-header">
<span class="metric-value">$15,800</span>
<span class="metric-trend up"><span class="arrow"></span> 8.7%</span>
</div>
<div class="metric-label">Efectivo en Caja</div>
<div class="metric-progress">
<div class="progress-header">
<span class="progress-text">Corte anterior</span>
<span class="progress-value">105%</span>
</div>
<div class="progress-track">
<div class="progress-fill success" data-width="100"></div>
</div>
</div>
</div>
</div>
</section>
<script>
// Animate progress bars on load
window.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
document.querySelectorAll('.progress-fill').forEach(bar => {
bar.style.width = bar.dataset.width + '%';
});
}, 100);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,544 @@
<!DOCTYPE html>
<html lang="es" data-theme="industrial">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus Autoparts — Ticket Térmico</title>
<link rel="stylesheet" href="../tokens/tokens.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body); background: var(--color-bg-base);
color: var(--color-text-primary); padding: var(--space-8);
transition: var(--transition-normal);
}
.theme-switcher {
position: sticky; top: 0; z-index: calc(var(--z-modal) + 10);
display: flex; gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
margin: calc(-1 * var(--space-8)); margin-bottom: var(--space-8);
}
.theme-switcher button {
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border);
background: var(--color-bg-base); color: var(--color-text-primary);
border-radius: var(--radius-md); cursor: pointer;
font-family: var(--font-body); font-size: var(--text-body-sm);
transition: var(--transition-fast);
}
.theme-switcher button.active {
background: var(--color-primary); color: var(--color-text-inverse);
border-color: var(--color-primary);
}
h1 { font-family: var(--font-heading); font-size: var(--text-h2);
font-weight: var(--heading-weight-primary); margin-bottom: var(--space-2);
color: var(--color-text-accent); }
.subtitle { color: var(--color-text-muted); font-size: var(--text-body); margin-bottom: var(--space-8); }
section { margin-bottom: var(--space-10); }
section > h2 {
font-family: var(--font-heading); font-size: var(--text-h4);
font-weight: var(--heading-weight-secondary); color: var(--color-text-secondary);
margin-bottom: var(--space-4); padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
.label-text {
font-size: var(--text-caption); color: var(--color-text-muted);
text-transform: uppercase; letter-spacing: var(--tracking-widest);
font-weight: var(--font-weight-semibold); margin-bottom: var(--space-3);
}
/* ====== Ticket Sizes ====== */
.tickets-row {
display: flex; gap: var(--space-8); flex-wrap: wrap;
justify-content: center; align-items: flex-start;
}
.ticket-wrapper {
text-align: center;
}
.ticket-label {
font-size: var(--text-body-sm); font-weight: var(--font-weight-semibold);
color: var(--color-text-accent); margin-bottom: var(--space-3);
text-transform: uppercase; letter-spacing: var(--tracking-wide);
}
/* ====== Ticket Base ====== */
.ticket {
background: #ffffff; color: #000000;
font-family: 'Courier New', 'Consolas', monospace;
font-size: 11px; line-height: 1.4;
padding: 12px; text-align: left;
border: 1px dashed #ccc;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.ticket-58 { width: 219px; /* 58mm ≈ 219px at 96dpi */ }
.ticket-80 { width: 302px; /* 80mm ≈ 302px at 96dpi */ }
.ticket .store-name {
font-size: 14px; font-weight: bold; text-align: center;
margin-bottom: 2px;
}
.ticket .store-tagline {
font-size: 9px; text-align: center; color: #555;
margin-bottom: 4px;
}
.ticket .store-info {
font-size: 9px; text-align: center; color: #333;
margin-bottom: 6px; line-height: 1.3;
}
.ticket .divider {
border: none; border-top: 1px dashed #999;
margin: 6px 0;
}
.ticket .divider-double {
border: none; border-top: 2px solid #333;
margin: 6px 0;
}
.ticket .ticket-row {
display: flex; justify-content: space-between;
}
.ticket .ticket-row .left { text-align: left; }
.ticket .ticket-row .right { text-align: right; }
.ticket .folio-line {
font-size: 10px; font-weight: bold;
display: flex; justify-content: space-between;
margin-bottom: 4px;
}
.ticket .item-line {
margin-bottom: 3px;
}
.ticket .item-name {
font-size: 10px; font-weight: bold;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ticket .item-detail {
display: flex; justify-content: space-between;
font-size: 10px; color: #333;
}
.ticket .item-sku {
font-size: 8px; color: #777;
}
.ticket .total-section { margin-top: 4px; }
.ticket .total-line {
display: flex; justify-content: space-between;
font-size: 11px;
}
.ticket .total-line.grand {
font-size: 14px; font-weight: bold;
margin-top: 2px; padding-top: 2px;
border-top: 1px solid #333;
}
.ticket .payment-section {
margin-top: 4px; font-size: 10px;
}
.ticket .footer-section {
text-align: center; margin-top: 8px;
font-size: 9px; color: #555;
}
.ticket .footer-section .thanks {
font-size: 11px; font-weight: bold; color: #000;
margin-bottom: 2px;
}
.ticket .barcode {
text-align: center; margin-top: 6px;
font-size: 24px; letter-spacing: 2px;
font-family: 'Libre Barcode 39', monospace;
}
.ticket .barcode-text {
font-size: 8px; text-align: center; color: #666;
}
/* ====== 80mm extras ====== */
.ticket-80 .item-line-wide {
display: grid; grid-template-columns: auto 1fr auto auto;
gap: 8px; align-items: baseline;
font-size: 10px; margin-bottom: 3px;
}
.ticket-80 .item-line-wide .qty {
font-weight: bold; min-width: 24px; text-align: right;
}
.ticket-80 .item-line-wide .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ticket-80 .item-line-wide .price { text-align: right; min-width: 55px; }
.ticket-80 .item-line-wide .subtotal { text-align: right; font-weight: bold; min-width: 60px; }
/* ====== Print Styles ====== */
@media print {
body { background: #fff; padding: 0; }
.theme-switcher, h1, .subtitle, section > h2,
.label-text, .ticket-wrapper .ticket-label,
.print-btn { display: none !important; }
.tickets-row { justify-content: flex-start; }
.ticket {
border: none; box-shadow: none; padding: 4px;
page-break-inside: avoid;
}
}
/* ====== Print Button ====== */
.print-btn {
display: inline-flex; align-items: center; gap: var(--space-2);
padding: var(--space-3) var(--space-6);
background: var(--btn-primary-bg); color: var(--btn-primary-text);
font-family: var(--font-body); font-size: var(--text-body);
font-weight: var(--font-weight-semibold);
border: none; border-radius: var(--radius-md);
cursor: pointer; transition: var(--transition-fast);
margin-bottom: var(--space-4);
}
.print-btn:hover { background: var(--btn-primary-bg-hover); }
</style>
</head>
<body>
<!-- Theme Switcher -->
<div class="theme-switcher">
<button class="active" onclick="setTheme('industrial')">A — Industrial Robusto</button>
<button onclick="setTheme('modern')">B — Técnico Moderno</button>
</div>
<h1>Ticket Térmico</h1>
<p class="subtitle">Layout de impresión para rollos de 58mm y 80mm. Fuente monoespaciada, CSS @media print.</p>
<button class="print-btn" onclick="window.print()">&#x1F5A8; Imprimir Preview</button>
<!-- ============================================== -->
<!-- SECTION: 58mm vs 80mm Side by Side -->
<!-- ============================================== -->
<section>
<h2>Comparativa: 58mm vs 80mm</h2>
<p class="label-text">Ambos formatos con los mismos datos de venta</p>
<div class="tickets-row">
<!-- ===== 58mm Ticket ===== -->
<div class="ticket-wrapper">
<div class="ticket-label">58mm (Mini Printer)</div>
<div class="ticket ticket-58">
<div class="store-name">NEXUS AUTOPARTS</div>
<div class="store-tagline">Tu conexión con las refacciones</div>
<div class="store-info">
Av. Insurgentes Sur 1234<br>
Col. Del Valle, CDMX 03100<br>
Tel: (55) 1234-5678<br>
RFC: NAU210315XX1
</div>
<hr class="divider-double">
<div class="folio-line">
<span>VENTA: V-0847</span>
<span>01/04/2026</span>
</div>
<div class="ticket-row" style="font-size: 9px; color: #555; margin-bottom: 4px;">
<span>Cajero: Carlos M.</span>
<span>14:32</span>
</div>
<hr class="divider">
<!-- Items -->
<div class="item-line">
<div class="item-name">Balatas cer. del. Brembo</div>
<div class="item-sku">SKU: BRM-P68034</div>
<div class="item-detail">
<span>2 x $1,250.00</span>
<span>$2,500.00</span>
</div>
</div>
<div class="item-line">
<div class="item-name">Filtro aceite Wix WL7200</div>
<div class="item-sku">SKU: WIX-7200</div>
<div class="item-detail">
<span>1 x $185.00</span>
<span>$185.00</span>
</div>
</div>
<div class="item-line">
<div class="item-name">Amort. trasero Monroe OESpec</div>
<div class="item-sku">SKU: MON-72364</div>
<div class="item-detail">
<span>2 x $1,071.25</span>
<span>$2,142.50</span>
</div>
</div>
<hr class="divider-double">
<!-- Totals -->
<div class="total-section">
<div class="total-line">
<span>Subtotal:</span><span>$4,161.64</span>
</div>
<div class="total-line">
<span>IVA 16%:</span><span>$665.86</span>
</div>
<div class="total-line grand">
<span>TOTAL:</span><span>$4,827.50</span>
</div>
</div>
<hr class="divider">
<!-- Payment -->
<div class="payment-section">
<div class="ticket-row">
<span>Forma pago:</span><span>Efectivo</span>
</div>
<div class="ticket-row">
<span>Recibido:</span><span>$5,000.00</span>
</div>
<div class="ticket-row" style="font-weight: bold;">
<span>Cambio:</span><span>$172.50</span>
</div>
</div>
<hr class="divider">
<div class="footer-section">
<div class="thanks">¡Gracias por su compra!</div>
<div>Garantía sujeta a condiciones</div>
<div>del fabricante. Conserve su ticket.</div>
<div style="margin-top: 4px;">www.nexusautoparts.com</div>
</div>
<div class="barcode">||||| |||| |||||</div>
<div class="barcode-text">V-2026-0847</div>
</div>
</div>
<!-- ===== 80mm Ticket ===== -->
<div class="ticket-wrapper">
<div class="ticket-label">80mm (Standard POS)</div>
<div class="ticket ticket-80">
<div class="store-name">NEXUS AUTOPARTS</div>
<div class="store-tagline">Tu conexión con las refacciones</div>
<div class="store-info">
Av. Insurgentes Sur 1234, Col. Del Valle<br>
Ciudad de México, CP 03100 | Tel: (55) 1234-5678<br>
RFC: NAU210315XX1
</div>
<hr class="divider-double">
<div class="folio-line">
<span>VENTA: V-2026-0847</span>
<span>01/Abr/2026 14:32</span>
</div>
<div class="ticket-row" style="font-size: 9px; color: #555; margin-bottom: 4px;">
<span>Cajero: Carlos Martínez</span>
<span>Sucursal: Matriz</span>
</div>
<div class="ticket-row" style="font-size: 9px; color: #555; margin-bottom: 2px;">
<span>Cliente: Taller Automotriz García</span>
<span>RFC: TAU150301XX1</span>
</div>
<hr class="divider">
<!-- Column Headers -->
<div class="item-line-wide" style="font-weight: bold; font-size: 9px; color: #555; text-transform: uppercase;">
<span class="qty">Cant</span>
<span class="name">Descripción</span>
<span class="price">P. Unit</span>
<span class="subtotal">Importe</span>
</div>
<hr class="divider" style="margin: 2px 0;">
<!-- Items -->
<div class="item-line-wide">
<span class="qty">2</span>
<span class="name">Balatas cerám. del. Brembo P68034</span>
<span class="price">$1,250.00</span>
<span class="subtotal">$2,500.00</span>
</div>
<div class="item-line-wide">
<span class="qty">1</span>
<span class="name">Filtro aceite Wix WL7200</span>
<span class="price">$185.00</span>
<span class="subtotal">$185.00</span>
</div>
<div class="item-line-wide">
<span class="qty">2</span>
<span class="name">Amortiguador tras. Monroe OESpec 72364</span>
<span class="price">$1,071.25</span>
<span class="subtotal">$2,142.50</span>
</div>
<hr class="divider-double">
<!-- Totals -->
<div class="total-section">
<div class="total-line">
<span>Subtotal (3 artículos, 5 piezas):</span><span>$4,161.64</span>
</div>
<div class="total-line">
<span>IVA 16%:</span><span>$665.86</span>
</div>
<div class="total-line" style="font-size: 10px; color: #777;">
<span>Descuento:</span><span>$0.00</span>
</div>
<div class="total-line grand">
<span>TOTAL:</span><span>$4,827.50</span>
</div>
</div>
<hr class="divider">
<!-- Payment -->
<div class="payment-section">
<div class="ticket-row">
<span>Forma de pago:</span><span>01 — Efectivo</span>
</div>
<div class="ticket-row">
<span>Monto recibido:</span><span>$5,000.00</span>
</div>
<div class="ticket-row" style="font-weight: bold;">
<span>Cambio:</span><span>$172.50</span>
</div>
</div>
<hr class="divider">
<!-- CFDI -->
<div style="font-size: 9px; color: #555; text-align: center; margin: 4px 0;">
CFDI Timbrado: UUID a1b2c3d4-e5f6-7890-abcd-ef1234567890<br>
Uso CFDI: G03 — Gastos en general<br>
Régimen: 601 — General de Ley
</div>
<hr class="divider">
<div class="footer-section">
<div class="thanks">¡Gracias por su compra!</div>
<div>Garantía sujeta a condiciones del fabricante.</div>
<div>Conserve su ticket como comprobante.</div>
<div>Cambios y devoluciones: 15 días con ticket.</div>
<div style="margin-top: 4px;">www.nexusautoparts.com</div>
</div>
<div class="barcode">||||| |||| ||||| ||| ||||</div>
<div class="barcode-text">V-2026-0847</div>
</div>
</div>
</div>
</section>
<!-- ============================================== -->
<!-- SECTION: Ticket Variants -->
<!-- ============================================== -->
<section>
<h2>Variantes de Ticket</h2>
<p class="label-text">Cotización y Corte de Caja</p>
<div class="tickets-row">
<!-- Cotización -->
<div class="ticket-wrapper">
<div class="ticket-label">Cotización (80mm)</div>
<div class="ticket ticket-80">
<div class="store-name">NEXUS AUTOPARTS</div>
<div class="store-info">Av. Insurgentes Sur 1234 | Tel: (55) 1234-5678</div>
<hr class="divider-double">
<div style="text-align: center; font-weight: bold; font-size: 13px; margin: 4px 0;">
*** COTIZACIÓN ***
</div>
<div class="folio-line">
<span>COT-2026-0312</span>
<span>01/Abr/2026</span>
</div>
<div style="font-size: 9px; color: #555; margin-bottom: 4px;">
Cliente: Taller Automotriz García | Vigencia: 7 días
</div>
<hr class="divider">
<div class="item-line-wide" style="font-weight: bold; font-size: 9px;">
<span class="qty">Cant</span><span class="name">Descripción</span>
<span class="price">P. Unit</span><span class="subtotal">Importe</span>
</div>
<hr class="divider" style="margin: 2px 0;">
<div class="item-line-wide">
<span class="qty">2</span><span class="name">Balatas cerám. del. Brembo</span>
<span class="price">$1,250.00</span><span class="subtotal">$2,500.00</span>
</div>
<div class="item-line-wide">
<span class="qty">1</span><span class="name">Filtro aceite Wix WL7200</span>
<span class="price">$185.00</span><span class="subtotal">$185.00</span>
</div>
<hr class="divider-double">
<div class="total-line grand">
<span>TOTAL:</span><span>$2,685.00</span>
</div>
<div style="font-size: 9px; color: #555; text-align: center; margin-top: 6px;">
Precios sujetos a cambio sin previo aviso.<br>
Esta cotización NO es un comprobante fiscal.
</div>
</div>
</div>
<!-- Corte de Caja -->
<div class="ticket-wrapper">
<div class="ticket-label">Corte de Caja (80mm)</div>
<div class="ticket ticket-80">
<div class="store-name">NEXUS AUTOPARTS</div>
<div class="store-info">Sucursal: Matriz | Caja: 01</div>
<hr class="divider-double">
<div style="text-align: center; font-weight: bold; font-size: 13px; margin: 4px 0;">
*** CORTE DE CAJA ***
</div>
<div class="folio-line">
<span>Cajero: Carlos M.</span>
<span>01/Abr/2026</span>
</div>
<div style="font-size: 9px; color: #555; margin-bottom: 4px;">
Apertura: 08:00 | Cierre: 20:00
</div>
<hr class="divider">
<div style="font-weight: bold; font-size: 10px; margin-bottom: 4px;">RESUMEN DE VENTAS</div>
<div class="ticket-row"><span>Ventas realizadas:</span><span>47</span></div>
<div class="ticket-row"><span>Artículos vendidos:</span><span>128</span></div>
<div class="ticket-row"><span>Cancelaciones:</span><span>2</span></div>
<div class="ticket-row"><span>Devoluciones:</span><span>1</span></div>
<hr class="divider">
<div style="font-weight: bold; font-size: 10px; margin-bottom: 4px;">FORMAS DE PAGO</div>
<div class="ticket-row"><span>Efectivo:</span><span>$35,420.00</span></div>
<div class="ticket-row"><span>Tarjeta crédito:</span><span>$12,850.00</span></div>
<div class="ticket-row"><span>Tarjeta débito:</span><span>$8,200.00</span></div>
<div class="ticket-row"><span>Transferencia:</span><span>$5,670.00</span></div>
<hr class="divider-double">
<div class="total-line grand"><span>TOTAL:</span><span>$62,140.00</span></div>
<hr class="divider">
<div style="font-weight: bold; font-size: 10px; margin-bottom: 4px;">EFECTIVO EN CAJA</div>
<div class="ticket-row"><span>Fondo apertura:</span><span>$3,000.00</span></div>
<div class="ticket-row"><span>+ Cobros efectivo:</span><span>$35,420.00</span></div>
<div class="ticket-row"><span>- Retiros:</span><span>$15,000.00</span></div>
<div class="ticket-row" style="font-weight: bold;"><span>Debe haber:</span><span>$23,420.00</span></div>
<div class="ticket-row"><span>Conteo físico:</span><span>$23,380.00</span></div>
<div class="ticket-row" style="color: #c00; font-weight: bold;"><span>Diferencia:</span><span>-$40.00</span></div>
<hr class="divider">
<div class="footer-section">
<div style="margin-top: 20px; font-size: 9px;">
Firma cajero: _______________<br><br>
Firma supervisor: _______________
</div>
</div>
</div>
</div>
</div>
</section>
<script>
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
document.querySelectorAll('.theme-switcher button').forEach(b => b.classList.remove('active'));
event.target.classList.add('active');
}
</script>
</body>
</html>

View File

@@ -188,7 +188,7 @@
===================================================================== */ ===================================================================== */
.sidebar { .sidebar {
width: 220px; width: 260px;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -169,7 +169,7 @@
========================================================================= */ ========================================================================= */
.sidebar { .sidebar {
width: 220px; width: 260px;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -169,7 +169,7 @@
========================================================================= */ ========================================================================= */
.sidebar { .sidebar {
width: 220px; width: 260px;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -145,7 +145,7 @@
========================================================================== */ ========================================================================== */
.sidebar { .sidebar {
width: 220px; width: 260px;
flex-shrink: 0; flex-shrink: 0;
background-color: var(--color-bg-elevated); background-color: var(--color-bg-elevated);
border-right: 1px solid var(--color-border); border-right: 1px solid var(--color-border);

View File

@@ -169,7 +169,7 @@
========================================================================= */ ========================================================================= */
.sidebar { .sidebar {
width: 220px; width: 260px;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -2573,6 +2573,131 @@
</div><!-- /app-shell --> </div><!-- /app-shell -->
<!-- =========================================================================
MODAL: DETALLE CFDI + XML PREVIEW
========================================================================= -->
<div class="modal-overlay" id="modalDetalleOverlay" style="display:none; position:fixed; inset:0; z-index:var(--z-modal); background:var(--overlay-backdrop); display:none; align-items:center; justify-content:center;">
<div class="modal-card" style="background:var(--color-bg-elevated); border:1px solid var(--color-border); border-radius:var(--radius-lg); width:720px; max-width:95vw; max-height:90vh; overflow:auto; box-shadow:var(--shadow-xl);">
<div style="display:flex; align-items:center; justify-content:space-between; padding:var(--space-5) var(--space-6); border-bottom:1px solid var(--color-border);">
<div>
<div style="font-family:var(--font-heading); font-size:var(--text-h5); font-weight:var(--heading-weight-primary); color:var(--color-text-primary);">Detalle de Factura</div>
<div style="font-size:var(--text-caption); color:var(--color-text-muted); margin-top:2px;">NAP-001289 — Timbrada</div>
</div>
<button onclick="document.getElementById('modalDetalleOverlay').style.display='none'" style="background:none; border:none; color:var(--color-text-muted); font-size:1.4rem; cursor:pointer; padding:var(--space-2);"></button>
</div>
<div style="padding:var(--space-5) var(--space-6);">
<div style="display:grid; grid-template-columns:1fr 1fr; gap:var(--space-4); margin-bottom:var(--space-6);">
<div>
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-1);">Emisor</div>
<div style="font-weight:var(--font-weight-semibold);">Nexus Autoparts SA de CV</div>
<div style="font-family:var(--font-mono); font-size:var(--text-body-sm); color:var(--color-text-secondary);">NAP960714JK3</div>
</div>
<div>
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-1);">Receptor</div>
<div style="font-weight:var(--font-weight-semibold);">Taller Mecánico Rodríguez</div>
<div style="font-family:var(--font-mono); font-size:var(--text-body-sm); color:var(--color-text-secondary);">TMR8402156HJ</div>
</div>
<div>
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-1);">UUID</div>
<div style="font-family:var(--font-mono); font-size:var(--text-caption); color:var(--color-text-accent); word-break:break-all;">6ba7b810-9dad-11d1-80b4-00c04fd430c8</div>
</div>
<div>
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-1);">Total</div>
<div style="font-family:var(--font-mono); font-size:var(--text-h4); font-weight:var(--font-weight-bold); color:var(--color-text-primary);">$4,002.00</div>
</div>
</div>
<!-- XML Preview -->
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-2);">Vista previa XML</div>
<pre style="background:var(--color-surface-3); border:1px solid var(--color-border); border-radius:var(--radius-md); padding:var(--space-4); font-family:var(--font-mono); font-size:11px; color:var(--color-text-secondary); overflow-x:auto; max-height:200px; line-height:1.6;">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/4"
Version="4.0" Serie="A" Folio="001289"
Fecha="2026-03-31T14:22:00" FormaPago="01"
SubTotal="3450.00" Moneda="MXN"
Total="4002.00" TipoDeComprobante="I"
MetodoPago="PUE" LugarExpedicion="64000"&gt;
&lt;cfdi:Emisor Rfc="NAP960714JK3"
Nombre="Nexus Autoparts SA de CV"
RegimenFiscal="601"/&gt;
&lt;cfdi:Receptor Rfc="TMR8402156HJ"
Nombre="Taller Mecánico Rodríguez"
UsoCFDI="G03" DomicilioFiscalReceptor="64000"
RegimenFiscalReceptor="612"/&gt;
&lt;cfdi:Conceptos&gt;
&lt;cfdi:Concepto ClaveProdServ="25174800"
Cantidad="4" ClaveUnidad="H87"
Descripcion="Balatas delanteras TRW"
ValorUnitario="485.00" Importe="1940.00"/&gt;
&lt;/cfdi:Conceptos&gt;
&lt;/cfdi:Comprobante&gt;</pre>
</div>
<div style="display:flex; gap:var(--space-3); justify-content:flex-end; padding:var(--space-4) var(--space-6); border-top:1px solid var(--color-border);">
<button style="padding:var(--space-2) var(--space-5); border:1px solid var(--color-border); border-radius:var(--radius-md); background:transparent; color:var(--color-text-primary); cursor:pointer; font-family:var(--font-body); font-size:var(--text-body-sm);">Descargar XML</button>
<button style="padding:var(--space-2) var(--space-5); border:1px solid var(--color-border); border-radius:var(--radius-md); background:transparent; color:var(--color-text-primary); cursor:pointer; font-family:var(--font-body); font-size:var(--text-body-sm);">Descargar PDF</button>
<button style="padding:var(--space-2) var(--space-5); border:1px solid var(--btn-danger-bg); border-radius:var(--radius-md); background:var(--btn-danger-bg); color:var(--btn-danger-text); cursor:pointer; font-family:var(--font-body); font-size:var(--text-body-sm); font-weight:var(--font-weight-semibold);" onclick="document.getElementById('modalDetalleOverlay').style.display='none'; document.getElementById('modalCancelOverlay').style.display='flex';">Cancelar CFDI</button>
</div>
</div>
</div>
<!-- =========================================================================
MODAL: CANCELACIÓN CFDI — Selector Motivo SAT
========================================================================= -->
<div class="modal-overlay" id="modalCancelOverlay" style="display:none; position:fixed; inset:0; z-index:var(--z-modal); background:var(--overlay-backdrop); display:none; align-items:center; justify-content:center;">
<div class="modal-card" style="background:var(--color-bg-elevated); border:1px solid var(--color-border); border-radius:var(--radius-lg); width:520px; max-width:95vw; box-shadow:var(--shadow-xl);">
<div style="display:flex; align-items:center; gap:var(--space-3); padding:var(--space-5) var(--space-6); border-bottom:1px solid var(--color-border);">
<div style="width:40px; height:40px; border-radius:var(--radius-full); background:rgba(239,68,68,0.12); display:flex; align-items:center; justify-content:center; flex-shrink:0;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--color-error)" stroke-width="2" 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>
</div>
<div>
<div style="font-family:var(--font-heading); font-size:var(--text-h5); font-weight:var(--heading-weight-primary); color:var(--color-text-primary);">Cancelar CFDI</div>
<div style="font-size:var(--text-caption); color:var(--color-text-muted);">NAP-001289 — $4,002.00 MXN</div>
</div>
<button onclick="document.getElementById('modalCancelOverlay').style.display='none'" style="background:none; border:none; color:var(--color-text-muted); font-size:1.4rem; cursor:pointer; padding:var(--space-2); margin-left:auto;"></button>
</div>
<div style="padding:var(--space-5) var(--space-6);">
<div style="font-size:var(--text-body-sm); font-weight:var(--font-weight-semibold); color:var(--color-text-primary); margin-bottom:var(--space-3);">Motivo de cancelación (SAT)</div>
<div style="display:flex; flex-direction:column; gap:var(--space-2);">
<label style="display:flex; align-items:flex-start; gap:var(--space-3); padding:var(--space-3) var(--space-4); border:1px solid var(--color-border); border-radius:var(--radius-md); cursor:pointer; transition:var(--transition-fast);" onmouseover="this.style.borderColor='var(--color-primary)'" onmouseout="this.style.borderColor='var(--color-border)'">
<input type="radio" name="motivo-sat" value="01" style="margin-top:3px; accent-color:var(--color-primary);" checked>
<div>
<div style="font-weight:var(--font-weight-semibold); font-size:var(--text-body-sm);">01 — Comprobante emitido con errores con relación</div>
<div style="font-size:var(--text-caption); color:var(--color-text-muted);">Se sustituirá por otro CFDI. Requiere UUID de reemplazo.</div>
</div>
</label>
<label style="display:flex; align-items:flex-start; gap:var(--space-3); padding:var(--space-3) var(--space-4); border:1px solid var(--color-border); border-radius:var(--radius-md); cursor:pointer; transition:var(--transition-fast);" onmouseover="this.style.borderColor='var(--color-primary)'" onmouseout="this.style.borderColor='var(--color-border)'">
<input type="radio" name="motivo-sat" value="02" style="margin-top:3px; accent-color:var(--color-primary);">
<div>
<div style="font-weight:var(--font-weight-semibold); font-size:var(--text-body-sm);">02 — Comprobante emitido con errores sin relación</div>
<div style="font-size:var(--text-caption); color:var(--color-text-muted);">No se sustituirá por otro CFDI.</div>
</div>
</label>
<label style="display:flex; align-items:flex-start; gap:var(--space-3); padding:var(--space-3) var(--space-4); border:1px solid var(--color-border); border-radius:var(--radius-md); cursor:pointer; transition:var(--transition-fast);" onmouseover="this.style.borderColor='var(--color-primary)'" onmouseout="this.style.borderColor='var(--color-border)'">
<input type="radio" name="motivo-sat" value="03" style="margin-top:3px; accent-color:var(--color-primary);">
<div>
<div style="font-weight:var(--font-weight-semibold); font-size:var(--text-body-sm);">03 — No se llevó a cabo la operación</div>
<div style="font-size:var(--text-caption); color:var(--color-text-muted);">La operación que amparó el CFDI no se realizó.</div>
</div>
</label>
<label style="display:flex; align-items:flex-start; gap:var(--space-3); padding:var(--space-3) var(--space-4); border:1px solid var(--color-border); border-radius:var(--radius-md); cursor:pointer; transition:var(--transition-fast);" onmouseover="this.style.borderColor='var(--color-primary)'" onmouseout="this.style.borderColor='var(--color-border)'">
<input type="radio" name="motivo-sat" value="04" style="margin-top:3px; accent-color:var(--color-primary);">
<div>
<div style="font-weight:var(--font-weight-semibold); font-size:var(--text-body-sm);">04 — Operación nominativa relacionada en una factura global</div>
<div style="font-size:var(--text-caption); color:var(--color-text-muted);">Cancelar cuando la operación se desglosó en una factura global.</div>
</div>
</label>
</div>
<!-- UUID de reemplazo (solo para motivo 01) -->
<div style="margin-top:var(--space-4);">
<label style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); display:block; margin-bottom:var(--space-1);">UUID de CFDI sustituto (motivo 01)</label>
<input type="text" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" style="width:100%; padding:var(--space-2) var(--space-3); border:1px solid var(--color-border); border-radius:var(--radius-md); background:var(--color-surface-2); color:var(--color-text-primary); font-family:var(--font-mono); font-size:var(--text-body-sm);">
</div>
</div>
<div style="display:flex; gap:var(--space-3); justify-content:flex-end; padding:var(--space-4) var(--space-6); border-top:1px solid var(--color-border);">
<button onclick="document.getElementById('modalCancelOverlay').style.display='none'" style="padding:var(--space-2) var(--space-5); border:1px solid var(--color-border); border-radius:var(--radius-md); background:transparent; color:var(--color-text-primary); cursor:pointer; font-family:var(--font-body); font-size:var(--text-body-sm);">Cancelar</button>
<button onclick="document.getElementById('modalCancelOverlay').style.display='none'" style="padding:var(--space-2) var(--space-5); border:1px solid var(--btn-danger-bg); border-radius:var(--radius-md); background:var(--btn-danger-bg); color:var(--btn-danger-text); cursor:pointer; font-family:var(--font-body); font-size:var(--text-body-sm); font-weight:var(--font-weight-semibold);">Solicitar Cancelación SAT</button>
</div>
</div>
</div>
<!-- ========================================================================= <!-- =========================================================================
JAVASCRIPT JAVASCRIPT
========================================================================= --> ========================================================================= -->
@@ -2633,6 +2758,28 @@
updateClock(); updateClock();
setInterval(updateClock, 1000); setInterval(updateClock, 1000);
/* -------------------------------------------------------------------------
MODAL HELPERS
------------------------------------------------------------------------- */
// Wire all XML buttons to open the detail modal
document.querySelectorAll('.btn--ghost').forEach(function(btn) {
if (btn.textContent.trim() === 'XML') {
btn.addEventListener('click', function() {
document.getElementById('modalDetalleOverlay').style.display = 'flex';
});
}
});
// Close modals on overlay click
['modalDetalleOverlay', 'modalCancelOverlay'].forEach(function(id) {
var overlay = document.getElementById(id);
if (overlay) {
overlay.addEventListener('click', function(e) {
if (e.target === overlay) overlay.style.display = 'none';
});
}
});
</script> </script>
</body> </body>

View File

@@ -169,7 +169,7 @@
========================================================================= */ ========================================================================= */
.sidebar { .sidebar {
width: 220px; width: 260px;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -890,92 +890,8 @@
<!-- USER SELECTION --> <!-- USER SELECTION -->
<section class="users-section" aria-labelledby="users-label"> <section class="users-section" aria-labelledby="users-label">
<div class="section-label" id="users-label">Seleccionar usuario</div> <div class="section-label" id="users-label">Seleccionar usuario</div>
<div class="users-grid" role="radiogroup" aria-label="Usuarios disponibles"> <div class="users-grid" id="users-grid" role="radiogroup" aria-label="Usuarios disponibles">
<!-- Empleados cargados dinámicamente desde el tenant -->
<button
class="user-avatar-btn"
data-user="JR"
data-name="J. Ramírez"
data-role="Vendedor"
role="radio"
aria-checked="false"
aria-label="Jorge Ramírez, Vendedor"
>
<div class="user-initials" aria-hidden="true">JR</div>
<div class="user-name">J. Ramírez</div>
<div class="user-role">Vendedor</div>
</button>
<button
class="user-avatar-btn"
data-user="ML"
data-name="M. López"
data-role="Cajero"
role="radio"
aria-checked="false"
aria-label="María López, Cajero"
>
<div class="user-initials" aria-hidden="true">ML</div>
<div class="user-name">M. López</div>
<div class="user-role">Cajero</div>
</button>
<button
class="user-avatar-btn"
data-user="AP"
data-name="A. Peña"
data-role="Almacén"
role="radio"
aria-checked="false"
aria-label="Alejandro Peña, Almacén"
>
<div class="user-initials" aria-hidden="true">AP</div>
<div class="user-name">A. Peña</div>
<div class="user-role">Almacén</div>
</button>
<button
class="user-avatar-btn"
data-user="SC"
data-name="S. Cruz"
data-role="Supervisor"
role="radio"
aria-checked="false"
aria-label="Sara Cruz, Supervisor"
>
<div class="user-initials" aria-hidden="true">SC</div>
<div class="user-name">S. Cruz</div>
<div class="user-role">Supervisor</div>
</button>
<button
class="user-avatar-btn"
data-user="HG"
data-name="H. García"
data-role="Gerente"
role="radio"
aria-checked="false"
aria-label="Hugo García, Gerente"
>
<div class="user-initials" aria-hidden="true">HG</div>
<div class="user-name">H. García</div>
<div class="user-role">Gerente</div>
</button>
<button
class="user-avatar-btn"
data-user="RT"
data-name="R. Torres"
data-role="Vendedor"
role="radio"
aria-checked="false"
aria-label="Roberto Torres, Vendedor"
>
<div class="user-initials" aria-hidden="true">RT</div>
<div class="user-name">R. Torres</div>
<div class="user-role">Vendedor</div>
</button>
</div> </div>
</section> </section>
@@ -1071,12 +987,61 @@
maxPinLength: 6, maxPinLength: 6,
}; };
/* ------------------------------------------------------------------
EMPLEADOS DEL TENANT (dinámico — en producción viene de la API)
------------------------------------------------------------------ */
const tenantEmployees = [
{ id: 'JR', initials: 'JR', name: 'J. Ramírez', fullName: 'Jorge Ramírez', role: 'Vendedor' },
{ id: 'ML', initials: 'ML', name: 'M. López', fullName: 'María López', role: 'Cajero' },
{ id: 'AP', initials: 'AP', name: 'A. Peña', fullName: 'Alejandro Peña', role: 'Almacén' },
{ id: 'SC', initials: 'SC', name: 'S. Cruz', fullName: 'Sara Cruz', role: 'Supervisor' },
{ id: 'HG', initials: 'HG', name: 'H. García', fullName: 'Hugo García', role: 'Gerente' },
{ id: 'RT', initials: 'RT', name: 'R. Torres', fullName: 'Roberto Torres', role: 'Vendedor' },
];
function renderEmployees(employees) {
const grid = document.getElementById('users-grid');
grid.innerHTML = employees.map(emp => `
<button
class="user-avatar-btn"
data-user="${emp.id}"
data-name="${emp.name}"
data-role="${emp.role}"
role="radio"
aria-checked="false"
aria-label="${emp.fullName}, ${emp.role}"
>
<div class="user-initials" aria-hidden="true">${emp.initials}</div>
<div class="user-name">${emp.name}</div>
<div class="user-role">${emp.role}</div>
</button>
`).join('');
bindUserButtons();
}
function bindUserButtons() {
document.querySelectorAll('.user-avatar-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.user-avatar-btn').forEach(b => {
b.classList.remove('selected');
b.setAttribute('aria-checked', 'false');
});
btn.classList.add('selected');
btn.setAttribute('aria-checked', 'true');
state.selectedUser = btn.dataset.user;
state.pin = [];
enablePinPad();
updatePinDisplay();
updateLoginButton();
});
});
}
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------
DOM REFS DOM REFS
------------------------------------------------------------------ */ ------------------------------------------------------------------ */
const html = document.documentElement; const html = document.documentElement;
const themeBtns = document.querySelectorAll('.theme-btn'); const themeBtns = document.querySelectorAll('.theme-btn');
const userBtns = document.querySelectorAll('.user-avatar-btn');
const pinDisplay = document.getElementById('pin-display'); const pinDisplay = document.getElementById('pin-display');
const pinPlaceholder = document.getElementById('pin-placeholder'); const pinPlaceholder = document.getElementById('pin-placeholder');
const pinDots = document.querySelectorAll('.pin-dot'); const pinDots = document.querySelectorAll('.pin-dot');
@@ -1105,28 +1070,9 @@
}); });
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------
USER SELECTION RENDER EMPLOYEES ON INIT
------------------------------------------------------------------ */ ------------------------------------------------------------------ */
userBtns.forEach(btn => { renderEmployees(tenantEmployees);
btn.addEventListener('click', () => {
// Deselect all
userBtns.forEach(b => {
b.classList.remove('selected');
b.setAttribute('aria-checked', 'false');
});
// Select clicked
btn.classList.add('selected');
btn.setAttribute('aria-checked', 'true');
state.selectedUser = btn.dataset.user;
state.pin = [];
// Enable PIN pad
enablePinPad();
updatePinDisplay();
updateLoginButton();
});
});
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------
PIN PAD LOGIC PIN PAD LOGIC

View File

@@ -804,6 +804,83 @@
color: var(--color-error); color: var(--color-error);
} }
/* Columnas Costo/Margen (solo visibles con permiso Admin/Owner) */
.cart-item__cost,
.cart-item__margin {
display: none;
font-family: var(--font-mono);
font-size: var(--text-caption);
text-align: right;
min-width: 56px;
flex-shrink: 0;
}
.cart-item__cost {
color: var(--color-text-muted);
}
.cart-item__margin {
font-weight: var(--font-weight-bold);
}
.cart-item__margin.margin-high { color: var(--color-success); }
.cart-item__margin.margin-mid { color: var(--color-warning); }
.cart-item__margin.margin-low { color: var(--color-error); }
/* When cost columns are visible */
body.show-cost-columns .cart-item__cost,
body.show-cost-columns .cart-item__margin {
display: block;
}
/* Margin footer summary */
.margin-summary {
display: none;
padding: var(--space-2) var(--space-5);
border-top: 1px dashed var(--color-border);
font-size: var(--text-caption);
color: var(--color-text-muted);
}
body.show-cost-columns .margin-summary {
display: flex;
justify-content: space-between;
}
.margin-summary__value {
font-family: var(--font-mono);
font-weight: var(--font-weight-bold);
}
/* Toggle button for cost columns */
.cost-toggle {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-1) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-text-muted);
font-size: 10px;
font-family: var(--font-body);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
cursor: pointer;
transition: var(--transition-fast);
}
.cost-toggle:hover {
border-color: var(--color-primary);
color: var(--color-text-accent);
}
.cost-toggle.active {
background: var(--color-primary-muted);
border-color: var(--color-primary);
color: var(--color-text-accent);
}
/* Cart footer (totals + payment) */ /* Cart footer (totals + payment) */
.cart-footer { .cart-footer {
flex-shrink: 0; flex-shrink: 0;
@@ -1423,6 +1500,7 @@
<div class="cart-header"> <div class="cart-header">
<div class="cart-header__top"> <div class="cart-header__top">
<div class="cart-header__sale-id" id="saleId">Venta #1247</div> <div class="cart-header__sale-id" id="saleId">Venta #1247</div>
<button class="cost-toggle" id="costToggle" title="Mostrar costo/margen (Admin)">💲 C/M</button>
<span class="cart-header__status">Activa</span> <span class="cart-header__status">Activa</span>
</div> </div>
<div class="customer-row"> <div class="customer-row">
@@ -1448,6 +1526,12 @@
<!-- Populated by JS --> <!-- Populated by JS -->
</div> </div>
<!-- Margin Summary (visible when cost columns are on) -->
<div class="margin-summary" id="marginSummary">
<span>Margen promedio ponderado:</span>
<span class="margin-summary__value" id="avgMargin">--</span>
</div>
<!-- Cart Footer --> <!-- Cart Footer -->
<div class="cart-footer"> <div class="cart-footer">
@@ -1608,20 +1692,20 @@
DATA — Product catalogue DATA — Product catalogue
------------------------------------------------------------------ */ ------------------------------------------------------------------ */
const PRODUCTS = [ const PRODUCTS = [
{ id: 1, name: 'Balatas Delanteras', oem: 'TRW-GDB1246', price: 485.00, stock: 12, category: 'frenos', cat_label: 'Frenos' }, { id: 1, name: 'Balatas Delanteras', oem: 'TRW-GDB1246', price: 485.00, cost: 290.00, stock: 12, category: 'frenos', cat_label: 'Frenos' },
{ id: 2, name: 'Disco de Freno Ventilado',oem: 'BREMBO-09A388X',price: 1290.00, stock: 4, category: 'frenos', cat_label: 'Frenos' }, { id: 2, name: 'Disco de Freno Ventilado',oem: 'BREMBO-09A388X',price: 1290.00, cost: 820.00, stock: 4, category: 'frenos', cat_label: 'Frenos' },
{ id: 3, name: 'Cilindro de Rueda', oem: 'LPR-4502', price: 320.00, stock: 7, category: 'frenos', cat_label: 'Frenos' }, { id: 3, name: 'Cilindro de Rueda', oem: 'LPR-4502', price: 320.00, cost: 195.00, stock: 7, category: 'frenos', cat_label: 'Frenos' },
{ id: 4, name: 'Bujía Iridium NGK', oem: 'NGK-ILKAR7L11', price: 185.00, stock: 48, category: 'motor', cat_label: 'Motor' }, { id: 4, name: 'Bujía Iridium NGK', oem: 'NGK-ILKAR7L11', price: 185.00, cost: 98.00, stock: 48, category: 'motor', cat_label: 'Motor' },
{ id: 5, name: 'Empaque de Culata', oem: 'VICTOR-R10154', price: 890.00, stock: 2, category: 'motor', cat_label: 'Motor' }, { id: 5, name: 'Empaque de Culata', oem: 'VICTOR-R10154', price: 890.00, cost: 580.00, stock: 2, category: 'motor', cat_label: 'Motor' },
{ id: 6, name: 'Correa de Distribución', oem: 'GATES-T236', price: 650.00, stock: 9, category: 'motor', cat_label: 'Motor' }, { id: 6, name: 'Correa de Distribución', oem: 'GATES-T236', price: 650.00, cost: 390.00, stock: 9, category: 'motor', cat_label: 'Motor' },
{ id: 7, name: 'Filtro de Aire Fram', oem: 'FRAM-CA10755', price: 195.00, stock: 23, category: 'filtros', cat_label: 'Filtros' }, { id: 7, name: 'Filtro de Aire Fram', oem: 'FRAM-CA10755', price: 195.00, cost: 85.00, stock: 23, category: 'filtros', cat_label: 'Filtros' },
{ id: 8, name: 'Filtro de Aceite Bosch', oem: 'BOSCH-0986AF10',price: 145.00, stock: 31, category: 'filtros', cat_label: 'Filtros' }, { id: 8, name: 'Filtro de Aceite Bosch', oem: 'BOSCH-0986AF10',price: 145.00, cost: 68.00, stock: 31, category: 'filtros', cat_label: 'Filtros' },
{ id: 9, name: 'Aceite Mobil 5W-30 1L', oem: 'MOBIL-5W30-1L', price: 210.00, stock: 64, category: 'aceites', cat_label: 'Aceites' }, { id: 9, name: 'Aceite Mobil 5W-30 1L', oem: 'MOBIL-5W30-1L', price: 210.00, cost: 155.00, stock: 64, category: 'aceites', cat_label: 'Aceites' },
{ id: 10, name: 'Aceite Pennzoil 10W-40', oem: 'PZ-10W40-1L', price: 175.00, stock: 50, category: 'aceites', cat_label: 'Aceites' }, { id: 10, name: 'Aceite Pennzoil 10W-40', oem: 'PZ-10W40-1L', price: 175.00, cost: 120.00, stock: 50, category: 'aceites', cat_label: 'Aceites' },
{ id: 11, name: 'Amortiguador Delantero', oem: 'KYB-339123', price: 1450.00, stock: 3, category: 'suspension', cat_label: 'Suspensión'}, { id: 11, name: 'Amortiguador Delantero', oem: 'KYB-339123', price: 1450.00, cost: 950.00, stock: 3, category: 'suspension', cat_label: 'Suspensión'},
{ id: 12, name: 'Rótula de Dirección', oem: 'MOOG-K9648', price: 560.00, stock: 6, category: 'suspension', cat_label: 'Suspensión'}, { id: 12, name: 'Rótula de Dirección', oem: 'MOOG-K9648', price: 560.00, cost: 320.00, stock: 6, category: 'suspension', cat_label: 'Suspensión'},
{ id: 13, name: 'Bobina de Encendido', oem: 'DELPHI-GN10570',price: 780.00, stock: 5, category: 'electrico', cat_label: 'Eléctrico' }, { id: 13, name: 'Bobina de Encendido', oem: 'DELPHI-GN10570',price: 780.00, cost: 480.00, stock: 5, category: 'electrico', cat_label: 'Eléctrico' },
{ id: 14, name: 'Sensor de Oxígeno Denso', oem: 'DENSO-234-4127',price: 920.00, stock: 4, category: 'electrico', cat_label: 'Eléctrico' }, { id: 14, name: 'Sensor de Oxígeno Denso', oem: 'DENSO-234-4127',price: 920.00, cost: 610.00, stock: 4, category: 'electrico', cat_label: 'Eléctrico' },
]; ];
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------
@@ -1857,6 +1941,8 @@
const p = getProduct(item.productId); const p = getProduct(item.productId);
if (!p) return ''; if (!p) return '';
const lineTotal = p.price * item.qty; const lineTotal = p.price * item.qty;
const margin = p.cost ? ((p.price - p.cost) / p.price * 100) : 0;
const marginClass = margin > 30 ? 'margin-high' : margin > 15 ? 'margin-mid' : 'margin-low';
return ` return `
<div class="cart-item" role="listitem" data-product-id="${p.id}"> <div class="cart-item" role="listitem" data-product-id="${p.id}">
<div class="cart-item__qty-ctrl"> <div class="cart-item__qty-ctrl">
@@ -1868,6 +1954,8 @@
<div class="cart-item__name" title="${p.name}">${p.name}</div> <div class="cart-item__name" title="${p.name}">${p.name}</div>
<div class="cart-item__unit">${fmt(p.price)} c/u</div> <div class="cart-item__unit">${fmt(p.price)} c/u</div>
</div> </div>
<div class="cart-item__cost">${p.cost ? fmt(p.cost) : '—'}</div>
<div class="cart-item__margin ${marginClass}">${margin.toFixed(1)}%</div>
<div class="cart-item__total">${fmt(lineTotal)}</div> <div class="cart-item__total">${fmt(lineTotal)}</div>
<button class="cart-item__remove" data-id="${p.id}" aria-label="Eliminar ${p.name} del carrito">✕</button> <button class="cart-item__remove" data-id="${p.id}" aria-label="Eliminar ${p.name} del carrito">✕</button>
</div>`; </div>`;
@@ -1890,8 +1978,41 @@
// disable cobrar if cart is empty // disable cobrar if cart is empty
$btnCobrar.disabled = state.cart.length === 0; $btnCobrar.disabled = state.cart.length === 0;
// update weighted average margin
updateAvgMargin();
} }
function updateAvgMargin() {
const $avg = document.getElementById('avgMargin');
if (!$avg) return;
let totalRevenue = 0, totalCost = 0;
state.cart.forEach(item => {
const p = getProduct(item.productId);
if (p && p.cost) {
totalRevenue += p.price * item.qty;
totalCost += p.cost * item.qty;
}
});
if (totalRevenue > 0) {
const avg = ((totalRevenue - totalCost) / totalRevenue * 100);
$avg.textContent = avg.toFixed(1) + '%';
$avg.className = 'margin-summary__value ' +
(avg > 30 ? 'margin-high' : avg > 15 ? 'margin-mid' : 'margin-low');
$avg.style.color = avg > 30 ? 'var(--color-success)' : avg > 15 ? 'var(--color-warning)' : 'var(--color-error)';
} else {
$avg.textContent = '--';
}
}
/* ------------------------------------------------------------------
COST/MARGIN TOGGLE (Admin/Owner only)
------------------------------------------------------------------ */
document.getElementById('costToggle').addEventListener('click', function() {
document.body.classList.toggle('show-cost-columns');
this.classList.toggle('active');
});
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------
DISCOUNT INPUT DISCOUNT INPUT
------------------------------------------------------------------ */ ------------------------------------------------------------------ */

View File

@@ -165,7 +165,7 @@
========================================================================= */ ========================================================================= */
.sidebar { .sidebar {
width: 220px; width: 260px;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;