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:
340
dashboard/catalog-public.html
Normal file
340
dashboard/catalog-public.html
Normal 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()">×</button>
|
||||
<div id="detailBody"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div>© 2026 Nexus Autoparts</div>
|
||||
</footer>
|
||||
|
||||
<script src="/catalog-public.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
469
dashboard/catalog-public.js
Normal file
469
dashboard/catalog-public.js
Normal 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) + ')"') + '>« 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 »</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, '"');
|
||||
}
|
||||
|
||||
})();
|
||||
502
dashboard/landing.html
Normal file
502
dashboard/landing.html
Normal 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">🔍</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">🚗</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">🔄</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">🌎</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">✉</div>
|
||||
<h4>Email</h4>
|
||||
<a href="mailto:ialcarazsalazar@consultoria-as.com">ialcarazsalazar@consultoria-as.com</a>
|
||||
</div>
|
||||
<div class="contact-item">
|
||||
<div class="icon">📱</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">📍</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>© 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>
|
||||
@@ -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')
|
||||
|
||||
564
dashboard/static/css/tokens.css
Normal file
564
dashboard/static/css/tokens.css
Normal 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 (50–900)
|
||||
------------------------------------------------------------------------ */
|
||||
|
||||
--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
|
||||
========================================================================== */
|
||||
Reference in New Issue
Block a user