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

'; + 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

'; + 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

'; + 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 += ''; + 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

'; + 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) + '

'; + 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 += ''; + d.alternatives.forEach(function (a) { + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + }); + html += '
NumeroFabricanteNombreTipo
' + esc(a.part_number || '') + '' + esc(a.manufacturer || '') + '' + esc(a.name || '-') + '' + esc(a.type === 'aftermarket' ? 'Aftermarket' : 'Cross-Ref') + '
'; + } + + // Bodegas + if (d.bodegas && d.bodegas.length) { + html += '
'; + html += '

Disponibilidad en Bodegas (' + d.bodegas.length + ')

'; + html += ''; + d.bodegas.forEach(function (b) { + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + }); + html += '
BodegaStockUbicacion
' + esc(b.business_name || '') + '' + b.stock + '' + esc(b.location || '-') + '
'; + } + + 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 +
+
+
1.5M+
+
Partes
+
+
+
36
+
Marcas
+
+
+
85K+
+
Vehiculos
+
+
+
+
+ + +
+
+

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

+
+ +
+
+
+ + +
+
+

Contacto

+
+ +
+
📱
+

WhatsApp

+ Enviar Mensaje +
+
+
📍
+

Ubicaciones

+

Tijuana, B.C.

+

Guadalajara, Jal.

+
+
+
+
+ + + + + + + + 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 + ========================================================================== */