diff --git a/dashboard/catalog-public.html b/dashboard/catalog-public.html
new file mode 100644
index 0000000..811f947
--- /dev/null
+++ b/dashboard/catalog-public.html
@@ -0,0 +1,340 @@
+
+
+
+
+
+ Catalogo — Nexus Autoparts
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cargando catalogo...
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dashboard/catalog-public.js b/dashboard/catalog-public.js
new file mode 100644
index 0000000..7cde793
--- /dev/null
+++ b/dashboard/catalog-public.js
@@ -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 = 'Buscando...
';
+ fetch(API + '/search?q=' + encodeURIComponent(q))
+ .then(function (r) { return r.json(); })
+ .then(function (data) { renderSearchResults(data); })
+ .catch(function () { content.innerHTML = 'Error en la busqueda.
'; });
+ };
+
+ // ── Detail modal (global) ──
+ window.openDetail = function (partId) {
+ var modal = document.getElementById('detailModal');
+ var body = document.getElementById('detailBody');
+ body.innerHTML = 'Cargando detalle...
';
+ modal.classList.add('open');
+ fetch(API + '/part/' + partId)
+ .then(function (r) { return r.json(); })
+ .then(function (d) { renderDetail(d, body); })
+ .catch(function () { body.innerHTML = 'Error cargando detalle.
'; });
+ };
+
+ 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('Catalogo');
+
+ if (state.brand) {
+ parts.push('/');
+ parts.push('' + esc(state.brand.name) + '');
+ }
+ if (state.model) {
+ parts.push('/');
+ parts.push('' + esc(state.model.name) + '');
+ }
+ if (state.year) {
+ parts.push('/');
+ parts.push('' + esc(String(state.year.value)) + '');
+ }
+ if (state.engine) {
+ parts.push('/');
+ var engineLabel = state.engine.name + (state.engine.trim ? ' (' + state.engine.trim + ')' : '');
+ parts.push('' + esc(engineLabel) + '');
+ }
+ if (state.category) {
+ parts.push('/');
+ parts.push('' + esc(state.category.name) + '');
+ }
+ if (state.group) {
+ parts.push('/');
+ parts.push('' + esc(state.group.name) + '');
+ }
+ if (state.level === 'search') {
+ parts.push('/');
+ parts.push('Busqueda');
+ }
+
+ 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 = 'Cargando marcas...
';
+ fetch(API + '/brands')
+ .then(function (r) { return r.json(); })
+ .then(function (brands) {
+ var html = 'Selecciona una Marca
';
+ brands.forEach(function (b) {
+ html += '
';
+ html += '' + esc(b.name_brand) + '';
+ html += '
';
+ });
+ html += '
';
+ content.innerHTML = html;
+ })
+ .catch(function () { content.innerHTML = 'Error cargando marcas.
'; });
+ }
+
+ window.selectBrand = function (id, name) {
+ state.brand = { id: id, name: name };
+ state.level = 'models';
+ loadModels();
+ };
+
+ function loadModels() {
+ renderBreadcrumb();
+ content.innerHTML = 'Cargando modelos...
';
+ fetch(API + '/models?brand_id=' + state.brand.id)
+ .then(function (r) { return r.json(); })
+ .then(function (models) {
+ var html = '' + esc(state.brand.name) + ' — Modelos
';
+ models.forEach(function (m) {
+ html += '
';
+ html += '' + esc(m.name_model) + '';
+ html += '
';
+ });
+ html += '
';
+ content.innerHTML = html;
+ })
+ .catch(function () { content.innerHTML = 'Error cargando modelos.
'; });
+ }
+
+ window.selectModel = function (id, name) {
+ state.model = { id: id, name: name };
+ state.level = 'years';
+ loadYears();
+ };
+
+ function loadYears() {
+ renderBreadcrumb();
+ content.innerHTML = 'Cargando anos...
';
+ fetch(API + '/years?model_id=' + state.model.id)
+ .then(function (r) { return r.json(); })
+ .then(function (years) {
+ var html = '' + esc(state.brand.name) + ' ' + esc(state.model.name) + ' — Anos
';
+ years.forEach(function (y) {
+ html += '
';
+ html += '' + y.year_car + '';
+ html += '
';
+ });
+ html += '
';
+ content.innerHTML = html;
+ })
+ .catch(function () { content.innerHTML = 'Error cargando anos.
'; });
+ }
+
+ window.selectYear = function (id, value) {
+ state.year = { id: id, value: value };
+ state.level = 'engines';
+ loadEngines();
+ };
+
+ function loadEngines() {
+ renderBreadcrumb();
+ content.innerHTML = 'Cargando motores...
';
+ fetch(API + '/engines?model_id=' + state.model.id + '&year_id=' + state.year.id)
+ .then(function (r) { return r.json(); })
+ .then(function (engines) {
+ var html = '' + esc(state.brand.name) + ' ' + esc(state.model.name) + ' ' + state.year.value + ' — Motor
';
+ html += '';
+ engines.forEach(function (e) {
+ var label = e.name_engine + (e.trim_level ? ' (' + e.trim_level + ')' : '');
+ html += '
';
+ html += '' + esc(label) + '';
+ html += '
';
+ });
+ html += '
';
+ content.innerHTML = html;
+ })
+ .catch(function () { content.innerHTML = 'Error cargando motores.
'; });
+ }
+
+ 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 = 'Cargando categorias...
';
+ fetch(API + '/categories?mye_id=' + state.engine.id_mye)
+ .then(function (r) { return r.json(); })
+ .then(function (cats) {
+ if (!cats.length) {
+ content.innerHTML = 'Categorias
No se encontraron categorias con partes para este vehiculo.
';
+ return;
+ }
+ var html = 'Categorias
';
+ cats.forEach(function (c) {
+ html += '
';
+ html += '' + esc(c.name) + '';
+ html += '' + c.part_count + '';
+ html += '
';
+ });
+ html += '
';
+ content.innerHTML = html;
+ })
+ .catch(function () { content.innerHTML = 'Error cargando categorias.
'; });
+ }
+
+ window.selectCategory = function (id, name) {
+ state.category = { id: id, name: name };
+ state.level = 'groups';
+ loadGroups();
+ };
+
+ function loadGroups() {
+ renderBreadcrumb();
+ content.innerHTML = 'Cargando grupos...
';
+ 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 = '' + esc(state.category.name) + '
No se encontraron sub-grupos.
';
+ return;
+ }
+ var html = '' + esc(state.category.name) + '
';
+ groups.forEach(function (g) {
+ html += '
';
+ html += '' + esc(g.name) + '';
+ html += '' + g.part_count + '';
+ html += '
';
+ });
+ html += '
';
+ content.innerHTML = html;
+ })
+ .catch(function () { content.innerHTML = 'Error cargando grupos.
'; });
+ }
+
+ window.selectGroup = function (id, name) {
+ state.group = { id: id, name: name };
+ state.level = 'parts';
+ state.page = 1;
+ loadParts();
+ };
+
+ function loadParts() {
+ renderBreadcrumb();
+ content.innerHTML = 'Cargando partes...
';
+ 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 = '' + esc(state.group.name) + '
No se encontraron partes.
';
+ return;
+ }
+
+ var html = '' + esc(state.group.name) + ' (' + pag.total + ' partes)
';
+ html += '';
+ parts.forEach(function (p) {
+ html += '
';
+ html += '
';
+ html += '
' + esc(p.oem_part_number) + '
';
+ html += '
' + esc(p.name || '') + '
';
+ if (p.description) html += '
' + esc(p.description) + '
';
+ html += '
';
+ html += '
';
+ if (p.image_url) {
+ html += '
 + ')
';
+ }
+ html += '
';
+ });
+ html += '
';
+
+ // Pagination
+ if (pag.total_pages > 1) {
+ html += '';
+ }
+
+ content.innerHTML = html;
+ })
+ .catch(function () { content.innerHTML = 'Error cargando partes.
'; });
+ }
+
+ 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 = 'Busqueda
No se encontraron resultados.
';
+ return;
+ }
+ var html = 'Resultados (' + results.length + ')
';
+ results.forEach(function (p) {
+ html += '
';
+ html += '
';
+ html += '
' + esc(p.oem_part_number) + '
';
+ html += '
' + esc(p.name || '') + '
';
+ if (p.vehicle_info) html += '
' + esc(p.vehicle_info) + '
';
+ html += '
';
+ html += '
';
+ if (p.image_url) {
+ html += '
 + ')
';
+ }
+ html += '
';
+ });
+ html += '
';
+ content.innerHTML = html;
+ }
+
+ // ── Part detail ──
+ function renderDetail(d, body) {
+ if (!d || !d.part) {
+ body.innerHTML = 'Parte no encontrada.
';
+ return;
+ }
+ var p = d.part;
+ var html = '';
+ html += '' + esc(p.oem_part_number) + '
';
+ html += '' + esc(p.name || '') + '
';
+ if (p.category_name) html += '' + esc(p.category_name) + (p.group_name ? ' / ' + esc(p.group_name) : '') + '
';
+ if (p.description) html += '' + esc(p.description) + '
';
+ if (p.image_url) {
+ html += '';
+ html += '
 + ')
';
+ html += '
';
+ }
+
+ // Alternatives
+ if (d.alternatives && d.alternatives.length) {
+ html += '';
+ html += '
Alternativas y Cross-References (' + d.alternatives.length + ')
';
+ html += '
| Numero | Fabricante | Nombre | Tipo |
';
+ d.alternatives.forEach(function (a) {
+ html += '';
+ html += '| ' + esc(a.part_number || '') + ' | ';
+ html += '' + esc(a.manufacturer || '') + ' | ';
+ html += '' + esc(a.name || '-') + ' | ';
+ html += '' + esc(a.type === 'aftermarket' ? 'Aftermarket' : 'Cross-Ref') + ' | ';
+ html += '
';
+ });
+ html += '
';
+ }
+
+ // Bodegas
+ if (d.bodegas && d.bodegas.length) {
+ html += '';
+ html += '
Disponibilidad en Bodegas (' + d.bodegas.length + ')
';
+ html += '
| Bodega | Stock | Ubicacion |
';
+ d.bodegas.forEach(function (b) {
+ html += '';
+ html += '| ' + esc(b.business_name || '') + ' | ';
+ html += '' + b.stock + ' | ';
+ html += '' + esc(b.location || '-') + ' | ';
+ html += '
';
+ });
+ html += '
';
+ }
+
+ 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, '"');
+ }
+
+})();
diff --git a/dashboard/landing.html b/dashboard/landing.html
new file mode 100644
index 0000000..f353e1f
--- /dev/null
+++ b/dashboard/landing.html
@@ -0,0 +1,502 @@
+
+
+
+
+
+ Nexus Autoparts — Tu conexion directa con las partes que necesitas
+
+
+
+
+
+
+
+
+
+
+
+
+
Nexus Autoparts
+
Tu conexion directa con las partes que necesitas
+
Ver Catalogo
+
+
+
+
+
+
+
+
Por que Nexus
+
+
+
🔍
+
Catalogo 1.5M+ Partes
+
Base de datos TecDoc completa con partes OEM y aftermarket para vehiculos vendidos en Mexico, USA y Canada.
+
+
+
🚗
+
Navegacion por Vehiculo
+
Encuentra la parte exacta navegando por Marca, Modelo, Ano, Motor y Categoria. Sin adivinar numeros de parte.
+
+
+
🔄
+
Cross-References OEM / Aftermarket
+
Ve las equivalencias entre partes originales y alternativas de fabricantes como Bosch, Denso, Monroe, Gates y mas.
+
+
+
🌎
+
Multi-Marca
+
Toyota, Nissan, Ford, VW, Honda, Chevrolet, Hyundai, Kia, Mazda, BMW, Mercedes-Benz, Renault y mas.
+
+
+
+
+
+
+
+
+
Como Funciona
+
+
+
1
+
Selecciona tu Vehiculo
+
Elige marca, modelo, ano y motor para filtrar las partes compatibles.
+
+
+
2
+
Encuentra la Parte
+
Navega por categorias o busca directamente por numero de parte OEM.
+
+
+
3
+
Contacta un Distribuidor
+
Consulta disponibilidad y precios con distribuidores de la red Nexus.
+
+
+
+
+
+
+
+
+
Marcas Disponibles
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dashboard/server.py b/dashboard/server.py
index 22c32e8..6bce8fe 100644
--- a/dashboard/server.py
+++ b/dashboard/server.py
@@ -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/')
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/')
+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')
diff --git a/dashboard/static/css/tokens.css b/dashboard/static/css/tokens.css
new file mode 100644
index 0000000..cad8bfb
--- /dev/null
+++ b/dashboard/static/css/tokens.css
@@ -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: or
+ 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: or
+ 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
+ ========================================================================== */