feat: rebuild web — landing page + catalogo publico con navegacion por vehiculo

Removidas paginas viejas (demo, tienda, admin, pos, captura, cuentas).
Nueva landing page con estilo del design system (tokens.css, 2 temas).
Catalogo publico sin auth con navegacion Marca>Modelo>Ano>Motor>Categoria>Partes.
Endpoints /api/catalog/* publicos con filtro NORTH_AMERICA_BRANDS (36 marcas).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 23:21:45 +00:00
parent e7376ddaed
commit 989a178143
5 changed files with 2221 additions and 105 deletions

View File

@@ -0,0 +1,340 @@
<!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>Catalogo — Nexus Autoparts</title>
<script>
(function(){
var t = localStorage.getItem('nexus-theme') || 'industrial';
document.documentElement.setAttribute('data-theme', t);
})();
</script>
<link rel="stylesheet" href="/static/css/tokens.css">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: var(--font-body);
background: var(--color-bg-base);
color: var(--color-text-primary);
line-height: var(--leading-body);
min-height: 100vh;
}
a { color: var(--color-text-accent); text-decoration: none; }
a:hover { text-decoration: underline; }
/* ── Header ── */
.site-header {
position: sticky; top: 0; z-index: var(--z-sticky);
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
}
.site-header .inner {
max-width: var(--content-xl); margin: 0 auto;
padding: 0 var(--space-6);
display: flex; align-items: center; justify-content: space-between;
height: 56px;
}
.logo {
font-family: var(--font-heading); font-size: var(--text-h5);
font-weight: var(--heading-weight-primary);
color: var(--color-text-accent);
text-transform: uppercase; letter-spacing: var(--tracking-wide);
}
.header-right { display: flex; gap: var(--space-3); align-items: center; }
.theme-toggle {
background: var(--btn-ghost-bg); border: 1px solid var(--btn-ghost-border);
color: var(--btn-ghost-text); padding: var(--space-1) var(--space-3);
border-radius: var(--radius-md); cursor: pointer; font-size: var(--text-caption);
font-family: var(--font-body); transition: var(--transition-fast);
}
.theme-toggle:hover { background: var(--color-primary-muted); color: var(--color-text-accent); }
/* ── Search bar ── */
.search-bar {
max-width: var(--content-xl); margin: 0 auto;
padding: var(--space-4) var(--space-6);
}
.search-wrapper {
display: flex; gap: var(--space-2);
}
.search-wrapper input {
flex: 1;
padding: var(--space-3) var(--space-4);
background: var(--color-surface-1);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
color: var(--color-text-primary);
font-family: var(--font-body);
font-size: var(--text-body);
outline: none;
transition: var(--transition-fast);
}
.search-wrapper input:focus {
border-color: var(--color-border-focus);
box-shadow: var(--shadow-focus);
}
.search-wrapper input::placeholder { color: var(--color-text-muted); }
.search-wrapper button {
padding: var(--space-3) var(--space-5);
background: var(--btn-primary-bg); color: var(--btn-primary-text);
border: none; 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);
}
.search-wrapper button:hover { background: var(--btn-primary-bg-hover); }
/* ── Breadcrumb ── */
.breadcrumb {
max-width: var(--content-xl); margin: 0 auto;
padding: var(--space-2) var(--space-6);
display: flex; flex-wrap: wrap; gap: var(--space-1);
font-size: var(--text-body-sm);
color: var(--color-text-muted);
}
.breadcrumb a { color: var(--color-text-accent); }
.breadcrumb .sep { margin: 0 var(--space-1); color: var(--color-text-muted); }
/* ── Main content ── */
.main {
max-width: var(--content-xl); margin: 0 auto;
padding: var(--space-4) var(--space-6) var(--space-16);
}
.main h2 {
font-family: var(--font-heading); font-size: var(--text-h3);
font-weight: var(--heading-weight-primary);
margin-bottom: var(--space-6);
letter-spacing: var(--heading-tracking-h3);
}
/* ── Grid for brands / models / years / engines / categories / groups ── */
.nav-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: var(--space-3);
}
.nav-card {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-4) var(--space-5);
cursor: pointer;
transition: var(--transition-fast);
display: flex; align-items: center; justify-content: space-between;
}
.nav-card:hover {
border-color: var(--color-border-accent);
box-shadow: var(--shadow-sm);
transform: translateY(-1px);
}
.nav-card .name {
font-weight: var(--font-weight-semibold);
font-size: var(--text-body);
}
.nav-card .count {
font-size: var(--text-caption);
color: var(--color-text-muted);
font-family: var(--font-mono);
}
/* ── Parts list ── */
.parts-list { display: flex; flex-direction: column; gap: var(--space-3); }
.part-row {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-4) var(--space-5);
display: grid;
grid-template-columns: 1fr auto;
gap: var(--space-3);
transition: var(--transition-fast);
}
.part-row:hover { border-color: var(--color-border-accent); }
.part-name {
font-weight: var(--font-weight-semibold);
font-size: var(--text-body);
}
.part-oem {
font-family: var(--font-mono);
font-size: var(--text-body-sm);
color: var(--color-text-accent);
}
.part-desc {
font-size: var(--text-body-sm);
color: var(--color-text-secondary);
margin-top: var(--space-1);
}
.part-alts {
font-size: var(--text-caption);
color: var(--color-text-muted);
margin-top: var(--space-2);
}
.part-alts span {
display: inline-block;
background: var(--color-surface-2);
padding: 2px var(--space-2);
border-radius: var(--radius-sm);
margin-right: var(--space-1);
margin-bottom: var(--space-1);
font-family: var(--font-mono);
}
.part-img {
width: 80px; height: 80px;
object-fit: contain;
border-radius: var(--radius-sm);
background: var(--color-surface-2);
}
.part-detail-btn {
font-size: var(--text-caption);
color: var(--color-text-accent);
cursor: pointer;
border: none; background: none; font-family: var(--font-body);
padding: var(--space-1) 0;
}
.part-detail-btn:hover { text-decoration: underline; }
/* ── Pagination ── */
.pagination {
display: flex; justify-content: center; gap: var(--space-2);
margin-top: var(--space-6);
}
.pagination button {
padding: var(--space-2) var(--space-4);
background: var(--color-surface-2);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
color: var(--color-text-primary);
cursor: pointer; font-family: var(--font-body);
transition: var(--transition-fast);
}
.pagination button:hover { border-color: var(--color-border-accent); }
.pagination button.active {
background: var(--color-primary);
color: var(--color-text-inverse);
border-color: var(--color-primary);
}
.pagination button:disabled { opacity: .4; cursor: default; }
/* ── Search results ── */
.search-results { display: flex; flex-direction: column; gap: var(--space-3); }
/* ── Part detail modal ── */
.modal-overlay {
display: none;
position: fixed; inset: 0;
background: var(--overlay-backdrop);
z-index: var(--z-modal);
justify-content: center; align-items: flex-start;
padding: var(--space-10) var(--space-4);
overflow-y: auto;
}
.modal-overlay.open { display: flex; }
.modal-content {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
max-width: 700px; width: 100%;
padding: var(--space-8);
position: relative;
}
.modal-close {
position: absolute; top: var(--space-3); right: var(--space-3);
background: none; border: none; color: var(--color-text-muted);
font-size: 1.5rem; cursor: pointer; line-height: 1;
}
.modal-close:hover { color: var(--color-text-primary); }
.detail-section { margin-top: var(--space-6); }
.detail-section h4 {
font-family: var(--font-heading);
font-size: var(--text-h6);
font-weight: var(--heading-weight-secondary);
margin-bottom: var(--space-3);
color: var(--color-text-accent);
}
.alt-table {
width: 100%;
border-collapse: collapse;
font-size: var(--text-body-sm);
}
.alt-table th, .alt-table td {
padding: var(--space-2) var(--space-3);
text-align: left;
border-bottom: 1px solid var(--color-border);
}
.alt-table th {
color: var(--color-text-muted);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
font-size: var(--text-caption);
letter-spacing: var(--tracking-wider);
}
/* ── Loading ── */
.loading {
text-align: center; padding: var(--space-10);
color: var(--color-text-muted);
font-size: var(--text-body);
}
/* ── Empty state ── */
.empty {
text-align: center; padding: var(--space-10);
color: var(--color-text-muted);
}
/* ── Footer ── */
.site-footer {
padding: var(--space-4) 0; text-align: center;
border-top: 1px solid var(--color-border);
font-size: var(--text-caption); color: var(--color-text-muted);
}
@media (max-width: 640px) {
.nav-grid { grid-template-columns: 1fr; }
.part-row { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<header class="site-header">
<div class="inner">
<a href="/" class="logo">Nexus Autoparts</a>
<div class="header-right">
<button class="theme-toggle" onclick="toggleTheme()">Tema</button>
</div>
</div>
</header>
<div class="search-bar">
<div class="search-wrapper">
<input type="text" id="searchInput" placeholder="Buscar por numero de parte o nombre..." autocomplete="off">
<button onclick="doSearch()">Buscar</button>
</div>
</div>
<nav class="breadcrumb" id="breadcrumb"></nav>
<main class="main" id="content">
<div class="loading">Cargando catalogo...</div>
</main>
<!-- Part detail modal -->
<div class="modal-overlay" id="detailModal">
<div class="modal-content" id="detailContent">
<button class="modal-close" onclick="closeDetail()">&times;</button>
<div id="detailBody"></div>
</div>
</div>
<footer class="site-footer">
<div>&copy; 2026 Nexus Autoparts</div>
</footer>
<script src="/catalog-public.js"></script>
</body>
</html>

469
dashboard/catalog-public.js Normal file
View File

@@ -0,0 +1,469 @@
/* =========================================================================
Nexus Autoparts — Public Catalog (catalog-public.js)
Vehicle hierarchy navigation: Brand > Model > Year > Engine > Category > Group > Parts
No auth, no cart, no prices — public browsing only.
========================================================================= */
(function () {
'use strict';
// ── State ──
var state = {
level: 'brands', // brands | models | years | engines | categories | groups | parts | search
brand: null, // {id, name}
model: null, // {id, name}
year: null, // {id, value}
engine: null, // {id_mye, name, trim}
category: null, // {id, name}
group: null, // {id, name}
page: 1,
totalPages: 1,
};
var API = '/api/catalog';
var content = document.getElementById('content');
var breadcrumbEl = document.getElementById('breadcrumb');
var searchInput = document.getElementById('searchInput');
// Check URL for brand param
var urlParams = new URLSearchParams(window.location.search);
var initBrandId = urlParams.get('brand');
// ── Init ──
if (initBrandId) {
// Load brands, find the one matching, then navigate
fetch(API + '/brands')
.then(function (r) { return r.json(); })
.then(function (brands) {
var found = brands.find(function (b) { return b.id_brand == initBrandId; });
if (found) {
state.brand = { id: found.id_brand, name: found.name_brand };
state.level = 'models';
loadModels();
} else {
loadBrands();
}
})
.catch(function () { loadBrands(); });
} else {
loadBrands();
}
// Enter on search
searchInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter') doSearch();
});
// ── Theme toggle (global) ──
window.toggleTheme = function () {
var html = document.documentElement;
var cur = html.getAttribute('data-theme');
var next = cur === 'industrial' ? 'modern' : 'industrial';
html.setAttribute('data-theme', next);
localStorage.setItem('nexus-theme', next);
};
// ── Search (global) ──
window.doSearch = function () {
var q = searchInput.value.trim();
if (!q || q.length < 2) return;
state.level = 'search';
renderBreadcrumb();
content.innerHTML = '<div class="loading">Buscando...</div>';
fetch(API + '/search?q=' + encodeURIComponent(q))
.then(function (r) { return r.json(); })
.then(function (data) { renderSearchResults(data); })
.catch(function () { content.innerHTML = '<div class="empty">Error en la busqueda.</div>'; });
};
// ── Detail modal (global) ──
window.openDetail = function (partId) {
var modal = document.getElementById('detailModal');
var body = document.getElementById('detailBody');
body.innerHTML = '<div class="loading">Cargando detalle...</div>';
modal.classList.add('open');
fetch(API + '/part/' + partId)
.then(function (r) { return r.json(); })
.then(function (d) { renderDetail(d, body); })
.catch(function () { body.innerHTML = '<div class="empty">Error cargando detalle.</div>'; });
};
window.closeDetail = function () {
document.getElementById('detailModal').classList.remove('open');
};
// Close modal on backdrop click
document.getElementById('detailModal').addEventListener('click', function (e) {
if (e.target === this) closeDetail();
});
// ── Breadcrumb ──
function renderBreadcrumb() {
var parts = [];
parts.push('<a href="javascript:void(0)" onclick="catalogNav(\'brands\')">Catalogo</a>');
if (state.brand) {
parts.push('<span class="sep">/</span>');
parts.push('<a href="javascript:void(0)" onclick="catalogNav(\'models\')">' + esc(state.brand.name) + '</a>');
}
if (state.model) {
parts.push('<span class="sep">/</span>');
parts.push('<a href="javascript:void(0)" onclick="catalogNav(\'years\')">' + esc(state.model.name) + '</a>');
}
if (state.year) {
parts.push('<span class="sep">/</span>');
parts.push('<a href="javascript:void(0)" onclick="catalogNav(\'engines\')">' + esc(String(state.year.value)) + '</a>');
}
if (state.engine) {
parts.push('<span class="sep">/</span>');
var engineLabel = state.engine.name + (state.engine.trim ? ' (' + state.engine.trim + ')' : '');
parts.push('<a href="javascript:void(0)" onclick="catalogNav(\'categories\')">' + esc(engineLabel) + '</a>');
}
if (state.category) {
parts.push('<span class="sep">/</span>');
parts.push('<a href="javascript:void(0)" onclick="catalogNav(\'groups\')">' + esc(state.category.name) + '</a>');
}
if (state.group) {
parts.push('<span class="sep">/</span>');
parts.push('<span>' + esc(state.group.name) + '</span>');
}
if (state.level === 'search') {
parts.push('<span class="sep">/</span>');
parts.push('<span>Busqueda</span>');
}
breadcrumbEl.innerHTML = parts.join('');
}
// Global nav
window.catalogNav = function (level) {
if (level === 'brands') {
state.brand = state.model = state.year = state.engine = state.category = state.group = null;
state.level = 'brands';
loadBrands();
} else if (level === 'models') {
state.model = state.year = state.engine = state.category = state.group = null;
state.level = 'models';
loadModels();
} else if (level === 'years') {
state.year = state.engine = state.category = state.group = null;
state.level = 'years';
loadYears();
} else if (level === 'engines') {
state.engine = state.category = state.group = null;
state.level = 'engines';
loadEngines();
} else if (level === 'categories') {
state.category = state.group = null;
state.level = 'categories';
loadCategories();
} else if (level === 'groups') {
state.group = null;
state.level = 'groups';
loadGroups();
}
};
// ── Data loaders ──
function loadBrands() {
state.level = 'brands';
renderBreadcrumb();
content.innerHTML = '<div class="loading">Cargando marcas...</div>';
fetch(API + '/brands')
.then(function (r) { return r.json(); })
.then(function (brands) {
var html = '<h2>Selecciona una Marca</h2><div class="nav-grid">';
brands.forEach(function (b) {
html += '<div class="nav-card" onclick="selectBrand(' + b.id_brand + ',\'' + escAttr(b.name_brand) + '\')">';
html += '<span class="name">' + esc(b.name_brand) + '</span>';
html += '</div>';
});
html += '</div>';
content.innerHTML = html;
})
.catch(function () { content.innerHTML = '<div class="empty">Error cargando marcas.</div>'; });
}
window.selectBrand = function (id, name) {
state.brand = { id: id, name: name };
state.level = 'models';
loadModels();
};
function loadModels() {
renderBreadcrumb();
content.innerHTML = '<div class="loading">Cargando modelos...</div>';
fetch(API + '/models?brand_id=' + state.brand.id)
.then(function (r) { return r.json(); })
.then(function (models) {
var html = '<h2>' + esc(state.brand.name) + ' — Modelos</h2><div class="nav-grid">';
models.forEach(function (m) {
html += '<div class="nav-card" onclick="selectModel(' + m.id_model + ',\'' + escAttr(m.name_model) + '\')">';
html += '<span class="name">' + esc(m.name_model) + '</span>';
html += '</div>';
});
html += '</div>';
content.innerHTML = html;
})
.catch(function () { content.innerHTML = '<div class="empty">Error cargando modelos.</div>'; });
}
window.selectModel = function (id, name) {
state.model = { id: id, name: name };
state.level = 'years';
loadYears();
};
function loadYears() {
renderBreadcrumb();
content.innerHTML = '<div class="loading">Cargando anos...</div>';
fetch(API + '/years?model_id=' + state.model.id)
.then(function (r) { return r.json(); })
.then(function (years) {
var html = '<h2>' + esc(state.brand.name) + ' ' + esc(state.model.name) + ' — Anos</h2><div class="nav-grid">';
years.forEach(function (y) {
html += '<div class="nav-card" onclick="selectYear(' + y.id_year + ',' + y.year_car + ')">';
html += '<span class="name">' + y.year_car + '</span>';
html += '</div>';
});
html += '</div>';
content.innerHTML = html;
})
.catch(function () { content.innerHTML = '<div class="empty">Error cargando anos.</div>'; });
}
window.selectYear = function (id, value) {
state.year = { id: id, value: value };
state.level = 'engines';
loadEngines();
};
function loadEngines() {
renderBreadcrumb();
content.innerHTML = '<div class="loading">Cargando motores...</div>';
fetch(API + '/engines?model_id=' + state.model.id + '&year_id=' + state.year.id)
.then(function (r) { return r.json(); })
.then(function (engines) {
var html = '<h2>' + esc(state.brand.name) + ' ' + esc(state.model.name) + ' ' + state.year.value + ' — Motor</h2>';
html += '<div class="nav-grid">';
engines.forEach(function (e) {
var label = e.name_engine + (e.trim_level ? ' (' + e.trim_level + ')' : '');
html += '<div class="nav-card" onclick="selectEngine(' + e.id_mye + ',\'' + escAttr(e.name_engine) + '\',\'' + escAttr(e.trim_level || '') + '\')">';
html += '<span class="name">' + esc(label) + '</span>';
html += '</div>';
});
html += '</div>';
content.innerHTML = html;
})
.catch(function () { content.innerHTML = '<div class="empty">Error cargando motores.</div>'; });
}
window.selectEngine = function (id_mye, name, trim) {
state.engine = { id_mye: id_mye, name: name, trim: trim };
state.level = 'categories';
loadCategories();
};
function loadCategories() {
renderBreadcrumb();
content.innerHTML = '<div class="loading">Cargando categorias...</div>';
fetch(API + '/categories?mye_id=' + state.engine.id_mye)
.then(function (r) { return r.json(); })
.then(function (cats) {
if (!cats.length) {
content.innerHTML = '<h2>Categorias</h2><div class="empty">No se encontraron categorias con partes para este vehiculo.</div>';
return;
}
var html = '<h2>Categorias</h2><div class="nav-grid">';
cats.forEach(function (c) {
html += '<div class="nav-card" onclick="selectCategory(' + c.id_part_category + ',\'' + escAttr(c.name) + '\')">';
html += '<span class="name">' + esc(c.name) + '</span>';
html += '<span class="count">' + c.part_count + '</span>';
html += '</div>';
});
html += '</div>';
content.innerHTML = html;
})
.catch(function () { content.innerHTML = '<div class="empty">Error cargando categorias.</div>'; });
}
window.selectCategory = function (id, name) {
state.category = { id: id, name: name };
state.level = 'groups';
loadGroups();
};
function loadGroups() {
renderBreadcrumb();
content.innerHTML = '<div class="loading">Cargando grupos...</div>';
fetch(API + '/groups?mye_id=' + state.engine.id_mye + '&category_id=' + state.category.id)
.then(function (r) { return r.json(); })
.then(function (groups) {
if (!groups.length) {
content.innerHTML = '<h2>' + esc(state.category.name) + '</h2><div class="empty">No se encontraron sub-grupos.</div>';
return;
}
var html = '<h2>' + esc(state.category.name) + '</h2><div class="nav-grid">';
groups.forEach(function (g) {
html += '<div class="nav-card" onclick="selectGroup(' + g.id_part_group + ',\'' + escAttr(g.name) + '\')">';
html += '<span class="name">' + esc(g.name) + '</span>';
html += '<span class="count">' + g.part_count + '</span>';
html += '</div>';
});
html += '</div>';
content.innerHTML = html;
})
.catch(function () { content.innerHTML = '<div class="empty">Error cargando grupos.</div>'; });
}
window.selectGroup = function (id, name) {
state.group = { id: id, name: name };
state.level = 'parts';
state.page = 1;
loadParts();
};
function loadParts() {
renderBreadcrumb();
content.innerHTML = '<div class="loading">Cargando partes...</div>';
var url = API + '/parts?mye_id=' + state.engine.id_mye + '&group_id=' + state.group.id + '&page=' + state.page;
fetch(url)
.then(function (r) { return r.json(); })
.then(function (resp) {
var parts = resp.data;
var pag = resp.pagination;
state.totalPages = pag.total_pages;
if (!parts.length) {
content.innerHTML = '<h2>' + esc(state.group.name) + '</h2><div class="empty">No se encontraron partes.</div>';
return;
}
var html = '<h2>' + esc(state.group.name) + ' <span style="font-size:var(--text-body-sm);color:var(--color-text-muted);">(' + pag.total + ' partes)</span></h2>';
html += '<div class="parts-list">';
parts.forEach(function (p) {
html += '<div class="part-row">';
html += '<div>';
html += '<div class="part-oem">' + esc(p.oem_part_number) + '</div>';
html += '<div class="part-name">' + esc(p.name || '') + '</div>';
if (p.description) html += '<div class="part-desc">' + esc(p.description) + '</div>';
html += '<button class="part-detail-btn" onclick="openDetail(' + p.id_part + ')">Ver detalle y alternativas</button>';
html += '</div>';
if (p.image_url) {
html += '<img class="part-img" src="' + esc(p.image_url) + '" alt="" loading="lazy" onerror="this.style.display=\'none\'">';
}
html += '</div>';
});
html += '</div>';
// Pagination
if (pag.total_pages > 1) {
html += '<div class="pagination">';
html += '<button ' + (state.page <= 1 ? 'disabled' : 'onclick="partsPage(' + (state.page - 1) + ')"') + '>&laquo; Anterior</button>';
html += '<button disabled>Pagina ' + state.page + ' de ' + pag.total_pages + '</button>';
html += '<button ' + (state.page >= pag.total_pages ? 'disabled' : 'onclick="partsPage(' + (state.page + 1) + ')"') + '>Siguiente &raquo;</button>';
html += '</div>';
}
content.innerHTML = html;
})
.catch(function () { content.innerHTML = '<div class="empty">Error cargando partes.</div>'; });
}
window.partsPage = function (p) {
state.page = p;
loadParts();
window.scrollTo({ top: 0, behavior: 'smooth' });
};
// ── Search results ──
function renderSearchResults(results) {
renderBreadcrumb();
if (!results.length) {
content.innerHTML = '<h2>Busqueda</h2><div class="empty">No se encontraron resultados.</div>';
return;
}
var html = '<h2>Resultados (' + results.length + ')</h2><div class="parts-list">';
results.forEach(function (p) {
html += '<div class="part-row">';
html += '<div>';
html += '<div class="part-oem">' + esc(p.oem_part_number) + '</div>';
html += '<div class="part-name">' + esc(p.name || '') + '</div>';
if (p.vehicle_info) html += '<div class="part-desc">' + esc(p.vehicle_info) + '</div>';
html += '<button class="part-detail-btn" onclick="openDetail(' + p.id_part + ')">Ver detalle y alternativas</button>';
html += '</div>';
if (p.image_url) {
html += '<img class="part-img" src="' + esc(p.image_url) + '" alt="" loading="lazy" onerror="this.style.display=\'none\'">';
}
html += '</div>';
});
html += '</div>';
content.innerHTML = html;
}
// ── Part detail ──
function renderDetail(d, body) {
if (!d || !d.part) {
body.innerHTML = '<div class="empty">Parte no encontrada.</div>';
return;
}
var p = d.part;
var html = '';
html += '<div class="part-oem" style="font-size:var(--text-h5)">' + esc(p.oem_part_number) + '</div>';
html += '<div class="part-name" style="font-size:var(--text-h4);margin-top:var(--space-2)">' + esc(p.name || '') + '</div>';
if (p.category_name) html += '<div class="part-desc">' + esc(p.category_name) + (p.group_name ? ' / ' + esc(p.group_name) : '') + '</div>';
if (p.description) html += '<div class="part-desc" style="margin-top:var(--space-3)">' + esc(p.description) + '</div>';
if (p.image_url) {
html += '<div style="margin-top:var(--space-4);text-align:center">';
html += '<img src="' + esc(p.image_url) + '" alt="" style="max-width:300px;border-radius:var(--radius-md)" onerror="this.style.display=\'none\'">';
html += '</div>';
}
// Alternatives
if (d.alternatives && d.alternatives.length) {
html += '<div class="detail-section">';
html += '<h4>Alternativas y Cross-References (' + d.alternatives.length + ')</h4>';
html += '<table class="alt-table"><thead><tr><th>Numero</th><th>Fabricante</th><th>Nombre</th><th>Tipo</th></tr></thead><tbody>';
d.alternatives.forEach(function (a) {
html += '<tr>';
html += '<td style="font-family:var(--font-mono)">' + esc(a.part_number || '') + '</td>';
html += '<td>' + esc(a.manufacturer || '') + '</td>';
html += '<td>' + esc(a.name || '-') + '</td>';
html += '<td>' + esc(a.type === 'aftermarket' ? 'Aftermarket' : 'Cross-Ref') + '</td>';
html += '</tr>';
});
html += '</tbody></table></div>';
}
// Bodegas
if (d.bodegas && d.bodegas.length) {
html += '<div class="detail-section">';
html += '<h4>Disponibilidad en Bodegas (' + d.bodegas.length + ')</h4>';
html += '<table class="alt-table"><thead><tr><th>Bodega</th><th>Stock</th><th>Ubicacion</th></tr></thead><tbody>';
d.bodegas.forEach(function (b) {
html += '<tr>';
html += '<td>' + esc(b.business_name || '') + '</td>';
html += '<td>' + b.stock + '</td>';
html += '<td>' + esc(b.location || '-') + '</td>';
html += '</tr>';
});
html += '</tbody></table></div>';
}
body.innerHTML = html;
}
// ── Helpers ──
function esc(s) {
if (!s) return '';
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function escAttr(s) {
return esc(s).replace(/'/g, "\\'").replace(/"/g, '&quot;');
}
})();

502
dashboard/landing.html Normal file
View File

@@ -0,0 +1,502 @@
<!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 — Tu conexion directa con las partes que necesitas</title>
<script>
(function(){
var t = localStorage.getItem('nexus-theme') || 'industrial';
document.documentElement.setAttribute('data-theme', t);
})();
</script>
<link rel="stylesheet" href="/static/css/tokens.css">
<style>
/* ── Reset ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: var(--font-body);
background: var(--color-bg-base);
color: var(--color-text-primary);
line-height: var(--leading-body);
min-height: 100vh;
}
a { color: var(--color-text-accent); text-decoration: none; }
a:hover { text-decoration: underline; }
/* ── Layout ── */
.container {
max-width: var(--content-xl);
margin: 0 auto;
padding: 0 var(--space-6);
}
/* ── Header ── */
.site-header {
position: sticky;
top: 0;
z-index: var(--z-sticky);
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
backdrop-filter: blur(12px);
}
.site-header .container {
display: flex;
align-items: center;
justify-content: space-between;
height: 64px;
}
.logo {
font-family: var(--font-heading);
font-size: var(--text-h4);
font-weight: var(--heading-weight-primary);
color: var(--color-text-accent);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
}
.header-actions { display: flex; gap: var(--space-3); align-items: center; }
.theme-toggle {
background: var(--btn-ghost-bg);
border: 1px solid var(--btn-ghost-border);
color: var(--btn-ghost-text);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
cursor: pointer;
font-size: var(--text-body-sm);
font-family: var(--font-body);
transition: var(--transition-fast);
}
.theme-toggle:hover { background: var(--color-primary-muted); color: var(--color-text-accent); }
/* ── Buttons ── */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-6);
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);
border: 2px solid transparent;
text-decoration: none;
}
.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); text-decoration: none; }
.btn-primary:active { background: var(--btn-primary-bg-active); }
.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); text-decoration: none; }
.btn-lg { padding: var(--space-4) var(--space-8); font-size: var(--text-body-lg); }
/* ── Hero ── */
.hero {
padding: var(--space-16) 0;
text-align: center;
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse at 50% 0%, var(--color-primary-muted) 0%, transparent 70%);
pointer-events: none;
}
.hero-content { position: relative; z-index: 1; }
.hero h1 {
font-family: var(--font-heading);
font-size: var(--text-h1);
font-weight: var(--heading-weight-primary);
line-height: var(--leading-h1);
letter-spacing: var(--heading-tracking-h1);
color: var(--color-text-primary);
margin-bottom: var(--space-4);
}
.hero h1 span { color: var(--color-text-accent); }
.hero .tagline {
font-size: var(--text-body-lg);
color: var(--color-text-secondary);
margin-bottom: var(--space-8);
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.hero-stats {
display: flex;
justify-content: center;
gap: var(--space-10);
margin-top: var(--space-10);
flex-wrap: wrap;
}
.hero-stat {
text-align: center;
}
.hero-stat .number {
font-family: var(--font-heading);
font-size: var(--text-h2);
font-weight: var(--heading-weight-primary);
color: var(--color-text-accent);
line-height: 1;
}
.hero-stat .label {
font-size: var(--text-body-sm);
color: var(--color-text-muted);
margin-top: var(--space-1);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
}
/* ── Features ── */
.features {
padding: var(--space-16) 0;
}
.section-title {
font-family: var(--font-heading);
font-size: var(--text-h2);
font-weight: var(--heading-weight-primary);
text-align: center;
margin-bottom: var(--space-10);
letter-spacing: var(--heading-tracking-h2);
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: var(--space-6);
}
.feature-card {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-8);
transition: var(--transition-normal);
}
.feature-card:hover {
border-color: var(--color-border-accent);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.feature-icon {
font-size: 2.5rem;
margin-bottom: var(--space-4);
}
.feature-card h3 {
font-family: var(--font-heading);
font-size: var(--text-h5);
font-weight: var(--heading-weight-secondary);
margin-bottom: var(--space-2);
color: var(--color-text-primary);
}
.feature-card p {
font-size: var(--text-body-sm);
color: var(--color-text-secondary);
line-height: var(--leading-body);
}
/* ── How it works ── */
.how-it-works {
padding: var(--space-16) 0;
background: var(--color-surface-1);
border-top: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
}
.steps {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: var(--space-8);
max-width: 900px;
margin: 0 auto;
}
.step {
text-align: center;
position: relative;
}
.step-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
border-radius: var(--radius-full);
background: var(--color-primary);
color: var(--color-text-inverse);
font-family: var(--font-heading);
font-size: var(--text-h3);
font-weight: var(--heading-weight-primary);
margin-bottom: var(--space-4);
}
.step h3 {
font-family: var(--font-heading);
font-size: var(--text-h5);
font-weight: var(--heading-weight-secondary);
margin-bottom: var(--space-2);
}
.step p {
font-size: var(--text-body-sm);
color: var(--color-text-secondary);
}
/* ── Brands ── */
.brands-section {
padding: var(--space-16) 0;
text-align: center;
}
.brands-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--space-3);
max-width: 800px;
margin: 0 auto;
}
.brand-tag {
display: inline-block;
padding: var(--space-2) var(--space-4);
background: var(--color-surface-2);
border: 1px solid var(--color-border);
border-radius: var(--radius-full);
font-size: var(--text-body-sm);
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
letter-spacing: var(--tracking-wide);
transition: var(--transition-fast);
}
.brand-tag:hover {
border-color: var(--color-border-accent);
color: var(--color-text-accent);
}
/* ── Contact ── */
.contact {
padding: var(--space-16) 0;
background: var(--color-surface-1);
border-top: 1px solid var(--color-border);
}
.contact-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: var(--space-8);
max-width: 800px;
margin: 0 auto;
}
.contact-item {
text-align: center;
}
.contact-item .icon {
font-size: 2rem;
margin-bottom: var(--space-3);
}
.contact-item h4 {
font-family: var(--font-heading);
font-size: var(--text-h6);
font-weight: var(--heading-weight-secondary);
margin-bottom: var(--space-2);
}
.contact-item p, .contact-item a {
font-size: var(--text-body-sm);
color: var(--color-text-secondary);
}
.btn-whatsapp {
background: #25D366;
color: #fff;
border-color: #25D366;
}
.btn-whatsapp:hover { background: #1EB954; }
/* ── Footer ── */
.site-footer {
padding: var(--space-6) 0;
text-align: center;
border-top: 1px solid var(--color-border);
font-size: var(--text-caption);
color: var(--color-text-muted);
}
/* ── Responsive ── */
@media (max-width: 768px) {
.hero { padding: var(--space-10) 0; }
.hero-stats { gap: var(--space-6); }
.features, .how-it-works, .brands-section, .contact { padding: var(--space-10) 0; }
}
</style>
</head>
<body>
<!-- Header -->
<header class="site-header">
<div class="container">
<a href="/" class="logo">Nexus Autoparts</a>
<div class="header-actions">
<button class="theme-toggle" onclick="toggleTheme()" title="Cambiar tema">Tema</button>
<a href="/catalog" class="btn btn-primary">Ver Catalogo</a>
</div>
</div>
</header>
<!-- Hero -->
<section class="hero">
<div class="container hero-content">
<h1><span>Nexus</span> Autoparts</h1>
<p class="tagline">Tu conexion directa con las partes que necesitas</p>
<a href="/catalog" class="btn btn-primary btn-lg">Ver Catalogo</a>
<div class="hero-stats" id="heroStats">
<div class="hero-stat">
<div class="number" id="statParts">1.5M+</div>
<div class="label">Partes</div>
</div>
<div class="hero-stat">
<div class="number" id="statBrands">36</div>
<div class="label">Marcas</div>
</div>
<div class="hero-stat">
<div class="number" id="statVehicles">85K+</div>
<div class="label">Vehiculos</div>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="features">
<div class="container">
<h2 class="section-title">Por que Nexus</h2>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">&#128269;</div>
<h3>Catalogo 1.5M+ Partes</h3>
<p>Base de datos TecDoc completa con partes OEM y aftermarket para vehiculos vendidos en Mexico, USA y Canada.</p>
</div>
<div class="feature-card">
<div class="feature-icon">&#128663;</div>
<h3>Navegacion por Vehiculo</h3>
<p>Encuentra la parte exacta navegando por Marca, Modelo, Ano, Motor y Categoria. Sin adivinar numeros de parte.</p>
</div>
<div class="feature-card">
<div class="feature-icon">&#128260;</div>
<h3>Cross-References OEM / Aftermarket</h3>
<p>Ve las equivalencias entre partes originales y alternativas de fabricantes como Bosch, Denso, Monroe, Gates y mas.</p>
</div>
<div class="feature-card">
<div class="feature-icon">&#127758;</div>
<h3>Multi-Marca</h3>
<p>Toyota, Nissan, Ford, VW, Honda, Chevrolet, Hyundai, Kia, Mazda, BMW, Mercedes-Benz, Renault y mas.</p>
</div>
</div>
</div>
</section>
<!-- How it works -->
<section class="how-it-works">
<div class="container">
<h2 class="section-title">Como Funciona</h2>
<div class="steps">
<div class="step">
<div class="step-number">1</div>
<h3>Selecciona tu Vehiculo</h3>
<p>Elige marca, modelo, ano y motor para filtrar las partes compatibles.</p>
</div>
<div class="step">
<div class="step-number">2</div>
<h3>Encuentra la Parte</h3>
<p>Navega por categorias o busca directamente por numero de parte OEM.</p>
</div>
<div class="step">
<div class="step-number">3</div>
<h3>Contacta un Distribuidor</h3>
<p>Consulta disponibilidad y precios con distribuidores de la red Nexus.</p>
</div>
</div>
</div>
</section>
<!-- Brands -->
<section class="brands-section">
<div class="container">
<h2 class="section-title">Marcas Disponibles</h2>
<div class="brands-list" id="brandsList">
<!-- Filled by JS -->
</div>
</div>
</section>
<!-- Contact -->
<section class="contact">
<div class="container">
<h2 class="section-title">Contacto</h2>
<div class="contact-grid">
<div class="contact-item">
<div class="icon">&#9993;</div>
<h4>Email</h4>
<a href="mailto:ialcarazsalazar@consultoria-as.com">ialcarazsalazar@consultoria-as.com</a>
</div>
<div class="contact-item">
<div class="icon">&#128241;</div>
<h4>WhatsApp</h4>
<a href="https://wa.me/526641234567" class="btn btn-whatsapp" target="_blank" rel="noopener">Enviar Mensaje</a>
</div>
<div class="contact-item">
<div class="icon">&#128205;</div>
<h4>Ubicaciones</h4>
<p>Tijuana, B.C.</p>
<p>Guadalajara, Jal.</p>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer class="site-footer">
<div class="container">
<p>&copy; 2026 Nexus Autoparts. Todos los derechos reservados.</p>
</div>
</footer>
<script>
function toggleTheme() {
var html = document.documentElement;
var current = html.getAttribute('data-theme');
var next = current === 'industrial' ? 'modern' : 'industrial';
html.setAttribute('data-theme', next);
localStorage.setItem('nexus-theme', next);
}
// Load live stats
fetch('/api/catalog/stats')
.then(r => r.json())
.then(d => {
if (d.parts) document.getElementById('statParts').textContent = (d.parts / 1e6).toFixed(1) + 'M+';
if (d.brands) document.getElementById('statBrands').textContent = d.brands;
if (d.vehicles) document.getElementById('statVehicles').textContent = (d.vehicles / 1e3).toFixed(0) + 'K+';
})
.catch(function(){});
// Load brands list
fetch('/api/catalog/brands')
.then(r => r.json())
.then(brands => {
var el = document.getElementById('brandsList');
el.innerHTML = brands.map(function(b) {
return '<a href="/catalog?brand=' + b.id_brand + '" class="brand-tag">' + b.name_brand + '</a>';
}).join('');
})
.catch(function(){});
</script>
</body>
</html>

View File

@@ -181,35 +181,15 @@ def search_vehicles(brand=None, model=None, year=None, engine_name=None, with_pa
@app.route('/')
def index():
return redirect('/login.html')
return send_from_directory('.', 'landing.html')
@app.route('/admin')
def admin_page():
return send_from_directory('.', 'admin.html')
@app.route('/catalog')
def public_catalog():
return send_from_directory('.', 'catalog-public.html')
@app.route('/landing')
def landing_page():
return send_from_directory('.', 'customer-landing.html')
@app.route('/diagramas')
def diagrams_page():
return send_from_directory('.', 'diagrams.html')
@app.route('/index.html')
def index_html():
return send_from_directory('.', 'index.html')
@app.route('/admin.html')
def admin_html():
return send_from_directory('.', 'admin.html')
@app.route('/customer-landing.html')
def customer_landing_html():
return send_from_directory('.', 'customer-landing.html')
@app.route('/diagrams.html')
def diagrams_html():
return send_from_directory('.', 'diagrams.html')
@app.route('/catalog-public.js')
def catalog_public_js():
return send_from_directory('.', 'catalog-public.js')
@app.route('/static/<path:path>')
def static_files(path):
@@ -236,6 +216,341 @@ def enhanced_search_js():
return send_from_directory('.', 'enhanced-search.js')
# ============================================================================
# Public Catalog API — No auth required
# ============================================================================
NORTH_AMERICA_BRANDS = (
'ACURA', 'AUDI', 'BMW', 'BUICK', 'CADILLAC', 'CHEVROLET', 'CHRYSLER',
'DODGE', 'FIAT', 'FORD', 'GMC', 'HONDA', 'HYUNDAI', 'INFINITI',
'JAGUAR', 'JEEP', 'KIA', 'LAND ROVER', 'LEXUS', 'LINCOLN', 'MAZDA',
'MERCEDES-BENZ', 'MINI', 'MITSUBISHI', 'NISSAN', 'PEUGEOT', 'PORSCHE',
'RAM', 'RENAULT', 'SEAT', 'SUBARU', 'SUZUKI', 'TESLA', 'TOYOTA',
'VOLVO', 'VW',
)
@app.route('/api/catalog/brands')
def api_catalog_brands():
session = Session()
try:
rows = session.execute(text("""
SELECT DISTINCT b.id_brand, b.name_brand
FROM brands b
JOIN models m ON m.brand_id = b.id_brand
JOIN model_year_engine mye ON mye.model_id = m.id_model
WHERE b.name_brand = ANY(:brands)
ORDER BY b.name_brand
"""), {'brands': list(NORTH_AMERICA_BRANDS)}).mappings().all()
return jsonify([{'id_brand': r['id_brand'], 'name_brand': r['name_brand']} for r in rows])
finally:
session.close()
@app.route('/api/catalog/models')
def api_catalog_models():
brand_id = request.args.get('brand_id', type=int)
if not brand_id:
return jsonify({'error': 'brand_id required'}), 400
session = Session()
try:
rows = session.execute(text("""
SELECT DISTINCT m.id_model, m.name_model
FROM models m
JOIN model_year_engine mye ON mye.model_id = m.id_model
WHERE m.brand_id = :brand_id
ORDER BY m.name_model
"""), {'brand_id': brand_id}).mappings().all()
return jsonify([{'id_model': r['id_model'], 'name_model': r['name_model']} for r in rows])
finally:
session.close()
@app.route('/api/catalog/years')
def api_catalog_years():
model_id = request.args.get('model_id', type=int)
if not model_id:
return jsonify({'error': 'model_id required'}), 400
session = Session()
try:
rows = session.execute(text("""
SELECT DISTINCT y.id_year, y.year_car
FROM years y
JOIN model_year_engine mye ON mye.year_id = y.id_year
WHERE mye.model_id = :model_id
ORDER BY y.year_car DESC
"""), {'model_id': model_id}).mappings().all()
return jsonify([{'id_year': r['id_year'], 'year_car': r['year_car']} for r in rows])
finally:
session.close()
@app.route('/api/catalog/engines')
def api_catalog_engines():
model_id = request.args.get('model_id', type=int)
year_id = request.args.get('year_id', type=int)
if not model_id or not year_id:
return jsonify({'error': 'model_id and year_id required'}), 400
session = Session()
try:
rows = session.execute(text("""
SELECT mye.id_mye, e.name_engine, mye.trim_level
FROM model_year_engine mye
JOIN engines e ON e.id_engine = mye.engine_id
WHERE mye.model_id = :model_id AND mye.year_id = :year_id
ORDER BY e.name_engine, mye.trim_level
"""), {'model_id': model_id, 'year_id': year_id}).mappings().all()
return jsonify([{'id_mye': r['id_mye'], 'name_engine': r['name_engine'],
'trim_level': r['trim_level'] or ''} for r in rows])
finally:
session.close()
@app.route('/api/catalog/categories')
def api_catalog_categories():
mye_id = request.args.get('mye_id', type=int)
if not mye_id:
return jsonify({'error': 'mye_id required'}), 400
session = Session()
try:
rows = session.execute(text("""
SELECT pc.id_part_category,
COALESCE(pc.name_es, pc.name_part_category) AS name,
sub.cnt AS part_count
FROM (
SELECT pg.category_id, COUNT(*) AS cnt
FROM vehicle_parts vp
JOIN parts p ON p.id_part = vp.part_id
JOIN part_groups pg ON pg.id_part_group = p.group_id
WHERE vp.model_year_engine_id = :mye_id
GROUP BY pg.category_id
) sub
JOIN part_categories pc ON pc.id_part_category = sub.category_id
ORDER BY name
"""), {'mye_id': mye_id}).mappings().all()
return jsonify([{'id_part_category': r['id_part_category'],
'name': r['name'], 'part_count': r['part_count']} for r in rows])
finally:
session.close()
@app.route('/api/catalog/groups')
def api_catalog_groups():
mye_id = request.args.get('mye_id', type=int)
category_id = request.args.get('category_id', type=int)
if not mye_id or not category_id:
return jsonify({'error': 'mye_id and category_id required'}), 400
session = Session()
try:
rows = session.execute(text("""
SELECT pg.id_part_group,
COALESCE(pg.name_es, pg.name_part_group) AS name,
COUNT(*) AS part_count
FROM vehicle_parts vp
JOIN parts p ON p.id_part = vp.part_id
JOIN part_groups pg ON pg.id_part_group = p.group_id
WHERE vp.model_year_engine_id = :mye_id
AND pg.category_id = :category_id
GROUP BY pg.id_part_group, name
ORDER BY name
"""), {'mye_id': mye_id, 'category_id': category_id}).mappings().all()
return jsonify([{'id_part_group': r['id_part_group'],
'name': r['name'], 'part_count': r['part_count']} for r in rows])
finally:
session.close()
@app.route('/api/catalog/parts')
def api_catalog_parts():
mye_id = request.args.get('mye_id', type=int)
group_id = request.args.get('group_id', type=int)
if not mye_id or not group_id:
return jsonify({'error': 'mye_id and group_id required'}), 400
page = max(1, request.args.get('page', 1, type=int))
per_page = min(request.args.get('per_page', 30, type=int), 100)
offset = (page - 1) * per_page
session = Session()
try:
total = session.execute(text("""
SELECT COUNT(*)
FROM vehicle_parts vp
JOIN parts p ON p.id_part = vp.part_id
WHERE vp.model_year_engine_id = :mye_id AND p.group_id = :group_id
"""), {'mye_id': mye_id, 'group_id': group_id}).scalar()
rows = session.execute(text("""
SELECT p.id_part, p.oem_part_number, p.name_part, p.name_es,
p.description, p.description_es, p.image_url
FROM vehicle_parts vp
JOIN parts p ON p.id_part = vp.part_id
WHERE vp.model_year_engine_id = :mye_id AND p.group_id = :group_id
ORDER BY p.name_part
LIMIT :limit OFFSET :offset
"""), {'mye_id': mye_id, 'group_id': group_id, 'limit': per_page, 'offset': offset}).mappings().all()
items = [{
'id_part': r['id_part'],
'oem_part_number': r['oem_part_number'],
'name': r['name_es'] or r['name_part'],
'description': r['description_es'] or r['description'],
'image_url': r['image_url'],
} for r in rows]
total_pages = max(1, (total + per_page - 1) // per_page)
return jsonify({'data': items, 'pagination': {
'page': page, 'per_page': per_page, 'total': total, 'total_pages': total_pages
}})
finally:
session.close()
@app.route('/api/catalog/part/<int:part_id>')
def api_catalog_part_detail(part_id):
session = Session()
try:
row = session.execute(text("""
SELECT p.id_part, p.oem_part_number, p.name_part, p.name_es,
p.description, p.description_es, p.image_url,
COALESCE(pg.name_es, pg.name_part_group) AS group_name,
COALESCE(pc.name_es, pc.name_part_category) AS category_name
FROM parts p
LEFT JOIN part_groups pg ON pg.id_part_group = p.group_id
LEFT JOIN part_categories pc ON pc.id_part_category = pg.category_id
WHERE p.id_part = :part_id
"""), {'part_id': part_id}).mappings().first()
if not row:
return jsonify({'error': 'Part not found'}), 404
part = {
'id_part': row['id_part'],
'oem_part_number': row['oem_part_number'],
'name': row['name_es'] or row['name_part'],
'description': row['description_es'] or row['description'],
'image_url': row['image_url'],
'group_name': row['group_name'],
'category_name': row['category_name'],
}
# Cross-references
xrefs = session.execute(text("""
SELECT pcr.cross_reference_number, pcr.source_ref
FROM part_cross_references pcr
WHERE pcr.part_id = :pid
LIMIT 50
"""), {'pid': part_id}).mappings().all()
# Aftermarket alternatives
afters = session.execute(text("""
SELECT ap.part_number, m.name_manufacture,
COALESCE(ap.name_es, ap.name_aftermarket_parts) AS name
FROM aftermarket_parts ap
JOIN manufacturers m ON m.id_manufacture = ap.manufacturer_id
WHERE ap.oem_part_id = :pid
LIMIT 50
"""), {'pid': part_id}).mappings().all()
alternatives = []
for x in xrefs:
alternatives.append({
'part_number': x['cross_reference_number'],
'manufacturer': x['source_ref'] or 'OEM Cross-Ref',
'name': None,
'type': 'cross_reference',
})
for a in afters:
alternatives.append({
'part_number': a['part_number'],
'manufacturer': a['name_manufacture'],
'name': a['name'],
'type': 'aftermarket',
})
# Bodegas
bodegas_rows = session.execute(text("""
SELECT u.business_name, wi.stock_quantity, wi.warehouse_location
FROM warehouse_inventory wi
JOIN users u ON u.id_user = wi.user_id
WHERE wi.part_id = :pid AND wi.stock_quantity > 0
ORDER BY wi.stock_quantity DESC
LIMIT 20
"""), {'pid': part_id}).mappings().all()
bodegas = [{'business_name': b['business_name'], 'stock': b['stock_quantity'],
'location': b['warehouse_location']} for b in bodegas_rows]
return jsonify({'part': part, 'alternatives': alternatives, 'bodegas': bodegas})
finally:
session.close()
@app.route('/api/catalog/search')
def api_catalog_search():
q = request.args.get('q', '').strip()
if not q or len(q) < 2:
return jsonify([])
limit = min(request.args.get('limit', 50, type=int), 100)
session = Session()
try:
is_part_number = bool(re.search(r'\d.*[-/]|[-/].*\d|\d{3,}', q))
if is_part_number:
clean_q = q.replace(' ', '').upper()
rows = session.execute(text("""
SELECT p.id_part, p.oem_part_number, p.name_part, p.name_es,
p.image_url
FROM parts p
WHERE REPLACE(UPPER(p.oem_part_number), ' ', '') LIKE :q
ORDER BY p.oem_part_number
LIMIT :limit
"""), {'q': f'%{clean_q}%', 'limit': limit}).mappings().all()
else:
tsquery = ' & '.join(q.split())
rows = session.execute(text("""
SELECT p.id_part, p.oem_part_number, p.name_part, p.name_es,
p.image_url
FROM parts p
WHERE p.search_vector @@ to_tsquery('spanish', :tsq)
OR p.name_part ILIKE :like
OR p.name_es ILIKE :like
ORDER BY
CASE WHEN p.search_vector @@ to_tsquery('spanish', :tsq)
THEN 0 ELSE 1 END,
p.name_part
LIMIT :limit
"""), {'tsq': tsquery, 'like': f'%{q}%', 'limit': limit}).mappings().all()
if not rows:
return jsonify([])
part_ids = [r['id_part'] for r in rows]
# Get one vehicle per part for context
vrows = session.execute(text("""
SELECT DISTINCT ON (vp.part_id)
vp.part_id, b.name_brand, m.name_model, y.year_car
FROM vehicle_parts vp
JOIN model_year_engine mye ON mye.id_mye = vp.model_year_engine_id
JOIN models m ON m.id_model = mye.model_id
JOIN brands b ON b.id_brand = m.brand_id
JOIN years y ON y.id_year = mye.year_id
WHERE vp.part_id = ANY(:pids)
ORDER BY vp.part_id, y.year_car DESC
"""), {'pids': part_ids}).mappings().all()
vmap = {v['part_id']: f"{v['name_brand']} {v['name_model']} {v['year_car']}" for v in vrows}
results = []
for r in rows:
results.append({
'id_part': r['id_part'],
'oem_part_number': r['oem_part_number'],
'name': r['name_es'] or r['name_part'],
'image_url': r['image_url'],
'vehicle_info': vmap.get(r['id_part'], ''),
})
return jsonify(results)
finally:
session.close()
# ============================================================================
# Core API Endpoints
# ============================================================================
@@ -2364,17 +2679,7 @@ def api_admin_delete_hotspot(hotspot_id):
# Captura (Data Entry) Endpoints
# ============================================================================
@app.route('/captura')
def captura_page():
return send_from_directory('.', 'captura.html')
@app.route('/captura.js')
def captura_js():
return send_from_directory('.', 'captura.js')
@app.route('/captura.css')
def captura_css():
return send_from_directory('.', 'captura.css')
# Captura page routes removed — APIs below kept for compatibility
@app.route('/api/captura/vehicles/pending')
@@ -2715,29 +3020,7 @@ def api_captura_part_aftermarket(part_id):
# POS (Point of Sale) Endpoints
# ============================================================================
@app.route('/pos')
def pos_page():
return send_from_directory('.', 'pos.html')
@app.route('/pos.js')
def pos_js():
return send_from_directory('.', 'pos.js')
@app.route('/pos.css')
def pos_css():
return send_from_directory('.', 'pos.css')
@app.route('/cuentas')
def cuentas_page():
return send_from_directory('.', 'cuentas.html')
@app.route('/cuentas.js')
def cuentas_js():
return send_from_directory('.', 'cuentas.js')
@app.route('/cuentas.css')
def cuentas_css():
return send_from_directory('.', 'cuentas.css')
# POS/cuentas page routes removed — served by POS app on its own port
# ---- Customers ----
@@ -3132,50 +3415,8 @@ def api_pos_search_parts():
# Store Dashboard Endpoints
# ============================================================================
@app.route('/demo')
def demo_page():
return send_from_directory('.', 'demo.html')
@app.route('/bodega')
def bodega_page():
return send_from_directory('.', 'bodega.html')
@app.route('/bodega.js')
def bodega_js():
return send_from_directory('.', 'bodega.js')
@app.route('/bodega.css')
def bodega_css():
return send_from_directory('.', 'bodega.css')
@app.route('/pitch')
def pitch_deck():
return send_from_directory('../pitch', 'deck.html')
@app.route('/login.html')
def login_page():
return send_from_directory('.', 'login.html')
@app.route('/login.js')
def login_js():
return send_from_directory('.', 'login.js')
@app.route('/login.css')
def login_css():
return send_from_directory('.', 'login.css')
@app.route('/tienda')
def tienda_page():
return send_from_directory('.', 'tienda.html')
@app.route('/tienda.js')
def tienda_js():
return send_from_directory('.', 'tienda.js')
@app.route('/tienda.css')
def tienda_css():
return send_from_directory('.', 'tienda.css')
# Old page routes removed (demo, bodega, pitch, login, tienda)
# APIs below are kept for backward compatibility
@app.route('/api/tienda/stats')

View File

@@ -0,0 +1,564 @@
/* ==========================================================================
NEXUS AUTOPARTS — Design Tokens
POS System for Auto Parts Stores
Version: 1.0.0
==========================================================================
Themes:
- [data-theme="industrial"] — Industrial Robusto (Dark)
- [data-theme="modern"] — Técnico Moderno (Light)
========================================================================== */
/* --------------------------------------------------------------------------
GOOGLE FONTS IMPORTS
-------------------------------------------------------------------------- */
@import url('https://fonts.googleapis.com/css2?family=Barlow:wght@400;700&family=Barlow+Condensed:wght@600;800&family=Poppins:wght@300;400;600;700&display=swap');
/* ==========================================================================
GLOBAL TOKENS — Theme-independent, shared across both themes
========================================================================== */
:root {
/* ------------------------------------------------------------------------
SEMANTIC COLORS — Status / Feedback (shared)
------------------------------------------------------------------------ */
--color-success: #22c55e;
--color-success-light: #bbf7d0;
--color-success-dark: #15803d;
--color-warning: #eab308;
--color-warning-light: #fef08a;
--color-warning-dark: #a16207;
--color-error: #ef4444;
--color-error-light: #fecaca;
--color-error-dark: #b91c1c;
/* ------------------------------------------------------------------------
NEUTRAL SCALE — Grey ramp (50900)
------------------------------------------------------------------------ */
--color-neutral-50: #fafafa;
--color-neutral-100: #f5f5f5;
--color-neutral-200: #e5e5e5;
--color-neutral-300: #d4d4d4;
--color-neutral-400: #a3a3a3;
--color-neutral-500: #737373;
--color-neutral-600: #525252;
--color-neutral-700: #404040;
--color-neutral-800: #262626;
--color-neutral-900: #171717;
/* ------------------------------------------------------------------------
SPACING — 4px base grid
------------------------------------------------------------------------ */
/* --space-N = N × 4px */
--space-1: 4px; /* 4px */
--space-2: 8px; /* 8px */
--space-3: 12px; /* 12px */
--space-4: 16px; /* 16px */
--space-5: 20px; /* 20px */
--space-6: 24px; /* 24px */
--space-7: 28px; /* 28px */
--space-8: 32px; /* 32px */
--space-9: 36px; /* 36px */
--space-10: 40px; /* 40px */
--space-11: 44px; /* 44px */
--space-12: 48px; /* 48px */
--space-14: 56px; /* 56px */
--space-16: 64px; /* 64px */
/* ------------------------------------------------------------------------
BORDER RADIUS
------------------------------------------------------------------------ */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 20px;
--radius-full: 9999px;
/* ------------------------------------------------------------------------
TRANSITIONS
------------------------------------------------------------------------ */
--transition-fast: all 0.10s ease;
--transition-normal: all 0.20s ease;
--transition-slow: all 0.40s ease;
/* Easing functions for fine-grained control */
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--duration-fast: 100ms;
--duration-normal: 200ms;
--duration-slow: 400ms;
/* ------------------------------------------------------------------------
Z-INDEX SCALE
------------------------------------------------------------------------ */
--z-dropdown: 1000;
--z-sticky: 1020;
--z-modal: 1050;
--z-toast: 1080;
/* ------------------------------------------------------------------------
BREAKPOINTS — Reference only (use in media queries, not calc())
sm: 640px
md: 768px
lg: 1024px
xl: 1280px
------------------------------------------------------------------------ */
}
/* ==========================================================================
THEME A — Industrial Robusto (Dark)
Usage: <html data-theme="industrial"> or <body data-theme="industrial">
Style: Industrial, robust, high-contrast amber accents, clip-path diagonals
========================================================================== */
[data-theme="industrial"] {
/* ------------------------------------------------------------------------
PRIMITIVE COLORS
------------------------------------------------------------------------ */
--color-primary: #F5A623; /* Amber gold — main brand accent */
--color-primary-hover: #e8951a; /* Darker amber on hover */
--color-primary-active: #d4850f; /* Pressed state */
--color-primary-muted: rgba(245, 166, 35, 0.15); /* Subtle tint */
--color-secondary: #333333; /* Mid-dark border / secondary bg */
--color-secondary-hover: #444444;
--color-accent: #F5A623; /* Same as primary in this theme */
/* ------------------------------------------------------------------------
BACKGROUNDS
------------------------------------------------------------------------ */
--color-bg-base: #0d0d0d; /* Page / app shell background */
--color-bg-elevated: #1a1a1a; /* Cards, panels, sidebars */
--color-bg-overlay: #252525; /* Modals, dropdowns, tooltips */
/* Surface levels (for layered UI) */
--color-surface-1: #1a1a1a; /* Lowest raised surface */
--color-surface-2: #252525; /* Mid-level surface */
--color-surface-3: #303030; /* Highest raised surface */
/* ------------------------------------------------------------------------
TEXT
------------------------------------------------------------------------ */
--color-text-primary: #FFFFFF;
--color-text-secondary: #CCCCCC;
--color-text-muted: #888888;
--color-text-disabled: #555555;
--color-text-inverse: #000000; /* Text on amber background */
--color-text-accent: #F5A623;
/* ------------------------------------------------------------------------
BORDERS
------------------------------------------------------------------------ */
--color-border: #333333;
--color-border-strong: #555555;
--color-border-accent: #F5A623;
--color-border-focus: #F5A623;
/* ------------------------------------------------------------------------
BUTTONS
------------------------------------------------------------------------ */
/* Primary button */
--btn-primary-bg: #F5A623;
--btn-primary-bg-hover: #e8951a;
--btn-primary-bg-active: #d4850f;
--btn-primary-text: #000000;
--btn-primary-border: transparent;
/* Secondary button */
--btn-secondary-bg: transparent;
--btn-secondary-bg-hover: rgba(245, 166, 35, 0.10);
--btn-secondary-text: #F5A623;
--btn-secondary-border: #F5A623;
/* Ghost / Danger */
--btn-ghost-bg: transparent;
--btn-ghost-text: #CCCCCC;
--btn-ghost-border: #333333;
--btn-danger-bg: #ef4444;
--btn-danger-text: #FFFFFF;
/* ------------------------------------------------------------------------
TYPOGRAPHY
------------------------------------------------------------------------ */
/* Font families */
--font-heading: 'Barlow Condensed', 'Arial Narrow', sans-serif;
--font-body: 'Barlow', 'Arial', sans-serif;
--font-mono: 'Courier New', 'Consolas', monospace; /* prices / SKUs */
/* Font weights */
--font-weight-light: 300; /* n/a in Barlow — falls to 400 */
--font-weight-regular: 400;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-extrabold: 800;
/* Heading weights (Barlow Condensed) */
--heading-weight-primary: 800;
--heading-weight-secondary: 600;
/* ------------------------------------------------------------------------
SHADOWS / ELEVATION
Tinted with amber to feel cohesive with the theme
------------------------------------------------------------------------ */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.60),
0 1px 2px rgba(0, 0, 0, 0.40);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.60),
0 2px 4px rgba(0, 0, 0, 0.40);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.70),
0 4px 6px rgba(0, 0, 0, 0.50);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.80),
0 10px 10px rgba(0, 0, 0, 0.50);
/* Accent glow — use on focused/highlighted elements */
--shadow-accent: 0 0 0 3px rgba(245, 166, 35, 0.40);
--shadow-focus: 0 0 0 3px rgba(245, 166, 35, 0.50);
/* ------------------------------------------------------------------------
MISC UI
------------------------------------------------------------------------ */
--scrollbar-track: #1a1a1a;
--scrollbar-thumb: #444444;
--scrollbar-thumb-hover: #F5A623;
--overlay-backdrop: rgba(0, 0, 0, 0.75);
/* Industrial clip-path angle (use in clip-path: polygon(...) utilities) */
--clip-diagonal-angle: 6deg;
}
/* ==========================================================================
THEME B — Técnico Moderno (Light)
Usage: <html data-theme="modern"> or <body data-theme="modern">
Style: Clean, modern, Poppins typography, subtle dot-grid background
========================================================================== */
[data-theme="modern"] {
/* ------------------------------------------------------------------------
PRIMITIVE COLORS
------------------------------------------------------------------------ */
--color-primary: #FF6B35; /* Orange — main brand accent */
--color-primary-hover: #f05a22; /* Darker on hover */
--color-primary-active: #dc4a12; /* Pressed state */
--color-primary-muted: rgba(255, 107, 53, 0.10); /* Subtle tint */
--color-secondary: #1a1a2e; /* Deep navy — used for strong text */
--color-secondary-hover: #252545;
--color-accent: #FF6B35; /* Same as primary in this theme */
/* ------------------------------------------------------------------------
BACKGROUNDS
------------------------------------------------------------------------ */
--color-bg-base: #FFFFFF; /* Page / app shell background */
--color-bg-elevated: #F8F9FF; /* Cards, panels — very subtle blue */
--color-bg-overlay: #FFFFFF; /* Modals, dropdowns */
/* Surface levels */
--color-surface-1: #F8F9FF;
--color-surface-2: #F0F2FF;
--color-surface-3: #E8EBFF;
/* Dot-grid background pattern (apply via background-image on body/shell) */
/* background-image: radial-gradient(circle, var(--dot-grid-color) 1px, transparent 1px); */
/* background-size: var(--dot-grid-size) var(--dot-grid-size); */
--dot-grid-color: rgba(26, 26, 46, 0.07);
--dot-grid-size: 24px;
/* ------------------------------------------------------------------------
TEXT
------------------------------------------------------------------------ */
--color-text-primary: #1a1a2e;
--color-text-secondary: #4a4a6a;
--color-text-muted: #8080a0;
--color-text-disabled: #b0b0c8;
--color-text-inverse: #FFFFFF; /* Text on orange background */
--color-text-accent: #FF6B35;
/* ------------------------------------------------------------------------
BORDERS
------------------------------------------------------------------------ */
--color-border: #e2e4f0;
--color-border-strong: #c8cadc;
--color-border-accent: #FF6B35;
--color-border-focus: #FF6B35;
/* ------------------------------------------------------------------------
BUTTONS
------------------------------------------------------------------------ */
/* Primary button */
--btn-primary-bg: #FF6B35;
--btn-primary-bg-hover: #f05a22;
--btn-primary-bg-active: #dc4a12;
--btn-primary-text: #FFFFFF;
--btn-primary-border: transparent;
/* Secondary button */
--btn-secondary-bg: transparent;
--btn-secondary-bg-hover: rgba(255, 107, 53, 0.08);
--btn-secondary-text: #FF6B35;
--btn-secondary-border: #FF6B35;
/* Ghost / Danger */
--btn-ghost-bg: transparent;
--btn-ghost-text: #4a4a6a;
--btn-ghost-border: #e2e4f0;
--btn-danger-bg: #ef4444;
--btn-danger-text: #FFFFFF;
/* ------------------------------------------------------------------------
TYPOGRAPHY
------------------------------------------------------------------------ */
/* Font families */
--font-heading: 'Poppins', 'Segoe UI', sans-serif;
--font-body: 'Poppins', 'Segoe UI', sans-serif;
--font-mono: 'Courier New', 'Consolas', monospace; /* prices / SKUs */
/* Font weights */
--font-weight-light: 300;
--font-weight-regular: 400;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-extrabold: 800; /* falls to 700 in Poppins */
/* Heading weights (Poppins) */
--heading-weight-primary: 700;
--heading-weight-secondary: 600;
/* ------------------------------------------------------------------------
SHADOWS / ELEVATION
Softer, cooler tints for the light theme
------------------------------------------------------------------------ */
--shadow-sm: 0 1px 3px rgba(26, 26, 46, 0.08),
0 1px 2px rgba(26, 26, 46, 0.05);
--shadow-md: 0 4px 6px rgba(26, 26, 46, 0.08),
0 2px 4px rgba(26, 26, 46, 0.05);
--shadow-lg: 0 10px 15px rgba(26, 26, 46, 0.10),
0 4px 6px rgba(26, 26, 46, 0.06);
--shadow-xl: 0 20px 25px rgba(26, 26, 46, 0.12),
0 10px 10px rgba(26, 26, 46, 0.06);
/* Accent glow — use on focused/highlighted elements */
--shadow-accent: 0 0 0 3px rgba(255, 107, 53, 0.25);
--shadow-focus: 0 0 0 3px rgba(255, 107, 53, 0.30);
/* ------------------------------------------------------------------------
MISC UI
------------------------------------------------------------------------ */
--scrollbar-track: #F8F9FF;
--scrollbar-thumb: #c8cadc;
--scrollbar-thumb-hover: #FF6B35;
--overlay-backdrop: rgba(26, 26, 46, 0.50);
/* No diagonal clip in modern theme — set to 0 for override-safe utilities */
--clip-diagonal-angle: 0deg;
}
/* ==========================================================================
TYPOGRAPHY SCALE — Token definitions
Resolved at theme level because font families differ between themes.
These tokens map to semantic roles and should be consumed directly.
========================================================================== */
/* Shared scale values (dimensionless, theme-independent) */
:root {
/* --- Type scale (font-size) --- */
--text-h1: clamp(2.25rem, 5vw, 3.5rem); /* 36px → 56px */
--text-h2: clamp(1.875rem, 4vw, 2.75rem); /* 30px → 44px */
--text-h3: clamp(1.5rem, 3vw, 2.125rem); /* 24px → 34px */
--text-h4: clamp(1.25rem, 2vw, 1.625rem); /* 20px → 26px */
--text-h5: 1.125rem; /* 18px */
--text-h6: 1rem; /* 16px */
--text-body-lg: 1.125rem; /* 18px */
--text-body: 1rem; /* 16px */
--text-body-sm: 0.875rem; /* 14px */
--text-caption: 0.75rem; /* 12px */
--text-label: 0.8125rem; /* 13px */
--text-mono: 1rem; /* 16px — prices, SKUs */
/* --- Line heights --- */
--leading-h1: 1.10;
--leading-h2: 1.12;
--leading-h3: 1.15;
--leading-h4: 1.20;
--leading-h5: 1.25;
--leading-h6: 1.30;
--leading-body-lg: 1.65;
--leading-body: 1.60;
--leading-body-sm: 1.55;
--leading-caption: 1.45;
--leading-label: 1.40;
--leading-mono: 1.50;
/* --- Letter spacing --- */
--tracking-tight: -0.03em;
--tracking-snug: -0.01em;
--tracking-normal: 0em;
--tracking-wide: 0.03em;
--tracking-wider: 0.06em;
--tracking-widest: 0.12em; /* Use for ALL-CAPS labels / badges */
}
/* Heading letter-spacing per theme */
[data-theme="industrial"] {
--heading-tracking-h1: -0.02em;
--heading-tracking-h2: -0.02em;
--heading-tracking-h3: -0.01em;
--heading-tracking-h4: 0em;
--heading-tracking-h5: 0.02em;
--heading-tracking-h6: 0.04em;
}
[data-theme="modern"] {
--heading-tracking-h1: -0.03em;
--heading-tracking-h2: -0.02em;
--heading-tracking-h3: -0.01em;
--heading-tracking-h4: 0em;
--heading-tracking-h5: 0em;
--heading-tracking-h6: 0.01em;
}
/* ==========================================================================
COMPONENT SHORTHAND TOKENS
Convenience aliases that combine multiple primitives. Components should
reference these rather than the primitives above.
========================================================================== */
:root {
/* --- Input / form fields --- */
/* These are intentionally left as CSS variable references so they resolve
correctly within whichever theme is active at runtime. */
/* (No :root overrides needed — components consume --color-* directly.) */
/* --- Focus ring --- */
--focus-ring: 0 0 0 3px var(--shadow-focus, rgba(245,166,35,0.40));
/* --- Content max widths --- */
--content-xs: 480px;
--content-sm: 640px;
--content-md: 768px;
--content-lg: 1024px;
--content-xl: 1280px;
--content-full: 100%;
}
/* ==========================================================================
UTILITY — Scrollbar styles (opt-in via class)
========================================================================== */
.themed-scrollbar {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
.themed-scrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.themed-scrollbar::-webkit-scrollbar-track {
background: var(--scrollbar-track);
}
.themed-scrollbar::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-thumb);
border-radius: var(--radius-full);
border: 2px solid var(--scrollbar-track);
}
.themed-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: var(--scrollbar-thumb-hover);
}
/* ==========================================================================
UTILITY — Dot-grid background (Theme B helper)
Apply class .bg-dot-grid to body or layout shell when using modern theme.
========================================================================== */
[data-theme="modern"] .bg-dot-grid {
background-color: var(--color-bg-base);
background-image: radial-gradient(
circle,
var(--dot-grid-color) 1px,
transparent 1px
);
background-size: var(--dot-grid-size) var(--dot-grid-size);
}
/* ==========================================================================
UTILITY — Industrial diagonal clip helpers (Theme A)
========================================================================== */
[data-theme="industrial"] .clip-top-right {
clip-path: polygon(0 0, calc(100% - 24px) 0, 100% 24px, 100% 100%, 0 100%);
}
[data-theme="industrial"] .clip-bottom-left {
clip-path: polygon(0 0, 100% 0, 100% 100%, 24px 100%, 0 calc(100% - 24px));
}
[data-theme="industrial"] .clip-corner {
clip-path: polygon(0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 0 100%);
}
/* ==========================================================================
END OF TOKENS FILE
nexus-autoparts-design/tokens/tokens.css
========================================================================== */