| 1 | /** |
| 2 | * MAPS Connect - Personalización del color de la página |
| 3 | * Permite elegir un color de acento (preestablecido o a medida) que se |
| 4 | * aplica en toda la aplicación (sidebar, topbar, botones, enlaces, |
| 5 | * tarjetas, chat...). Guarda la preferencia en localStorage y la aplica |
| 6 | * como atributo "data-color" + variables --accent-* sobre <html>; |
| 7 | * frontend/css/colores.css se encarga de mapearlas a las variables |
| 8 | * existentes del tema (claro y oscuro). |
| 9 | */ |
| 10 | const Colores = { |
| 11 | KEY: 'color-maps', |
| 12 | |
| 13 | PRESETS: [ |
| 14 | { id: 'verde', hex: '#087527', nombre: 'Verde' }, |
| 15 | { id: 'rosa', hex: '#e1367f', nombre: 'Rosa' }, |
| 16 | { id: 'azul', hex: '#0a6ab3', nombre: 'Azul' }, |
| 17 | { id: 'morado', hex: '#7c3aed', nombre: 'Morado' }, |
| 18 | { id: 'naranja', hex: '#ea580c', nombre: 'Naranja' }, |
| 19 | { id: 'teal', hex: '#0d9488', nombre: 'Teal' } |
| 20 | ], |
| 21 | |
| 22 | guardado() { |
| 23 | try { return localStorage.getItem(this.KEY); } catch (e) { return null; } |
| 24 | }, |
| 25 | |
| 26 | limpiar() { |
| 27 | const r = document.documentElement; |
| 28 | r.removeAttribute('data-color'); |
| 29 | const vars = ['--accent', '--accent-dark', '--accent-mid', '--accent-bright', |
| 30 | '--accent-deep', '--accent-soft', '--accent-soft-border', '--accent-contrast']; |
| 31 | for (const k of vars) r.style.removeProperty(k); |
| 32 | try { localStorage.removeItem(this.KEY); } catch (e) { /* almacenamiento no disponible */ } |
| 33 | }, |
| 34 | |
| 35 | /* --- Utilidades de color (HSL) --- */ |
| 36 | _alHsl(hex) { |
| 37 | hex = hex.replace('#', ''); |
| 38 | const r = parseInt(hex.slice(0, 2), 16) / 255; |
| 39 | const g = parseInt(hex.slice(2, 4), 16) / 255; |
| 40 | const b = parseInt(hex.slice(4, 6), 16) / 255; |
| 41 | const max = Math.max(r, g, b), min = Math.min(r, g, b); |
| 42 | let h = 0, s = 0; |
| 43 | const l = (max + min) / 2; |
| 44 | if (max !== min) { |
| 45 | const d = max - min; |
| 46 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min); |
| 47 | switch (max) { |
| 48 | case r: h = (g - b) / d + (g < b ? 6 : 0); break; |
| 49 | case g: h = (b - r) / d + 2; break; |
| 50 | default: h = (r - g) / d + 4; |
| 51 | } |
| 52 | h *= 60; |
| 53 | } |
| 54 | return { h, s, l }; |
| 55 | }, |
| 56 | |
| 57 | _aHex({ h, s, l }) { |
| 58 | h = ((h % 360) + 360) % 360; |
| 59 | const c = (1 - Math.abs(2 * l - 1)) * s; |
| 60 | const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); |
| 61 | const m = l - c / 2; |
| 62 | let r = 0, g = 0, b = 0; |
| 63 | if (h < 60) { r = c; g = x; } |
| 64 | else if (h < 120) { r = x; g = c; } |
| 65 | else if (h < 180) { g = c; b = x; } |
| 66 | else if (h < 240) { g = x; b = c; } |
| 67 | else if (h < 300) { r = x; b = c; } |
| 68 | else { r = c; b = x; } |
| 69 | const to = (v) => Math.round((v + m) * 255).toString(16).padStart(2, '0'); |
| 70 | return '#' + to(r) + to(g) + to(b); |
| 71 | }, |
| 72 | |
| 73 | _conLuz({ h, s, l }, nl) { return this._aHex({ h, s, l: nl }); }, |
| 74 | |
| 75 | /* --- Aplicación --- */ |
| 76 | |
| 77 | /** Aplica el color guardado; si no hay, restaura el color por defecto. */ |
| 78 | aplicar() { |
| 79 | const hex = this.guardado(); |
| 80 | if (!hex) { this.limpiar(); return; } |
| 81 | this.set(hex, false); |
| 82 | }, |
| 83 | |
| 84 | /** Aplica un color de acento y (por defecto) lo guarda como preferencia. |
| 85 | * Calcula toda la paleta derivada (oscuros, claros, tintes) y la aplica |
| 86 | * como variables CSS, de modo que toda la página se "sintoniza". */ |
| 87 | set(hex, guardar = true) { |
| 88 | if (!/^#[0-9a-f]{6}$/i.test(hex)) return; |
| 89 | const base = this._alHsl(hex); |
| 90 | const clampL = (l) => Math.min(Math.max(l, 0), 1); |
| 91 | const accentDark = clampL(Math.max(base.l * 0.62, 0.3)); |
| 92 | const midL = clampL(Math.max(Math.min(base.l + 0.14, 0.52), 0.38)); |
| 93 | const brightL = clampL(Math.max(Math.min(base.l + 0.3, 0.68), 0.5)); |
| 94 | const deepL = clampL(Math.max(Math.min(base.l * 0.55, 0.32), 0.16)); |
| 95 | const vars = { |
| 96 | '--accent': hex, |
| 97 | '--accent-soft': this._conLuz(base, 0.93), |
| 98 | '--accent-soft-border': this._conLuz(base, 0.8), |
| 99 | '--accent-dark': this._conLuz(base, accentDark), |
| 100 | '--accent-mid': this._conLuz(base, midL), |
| 101 | '--accent-bright': this._conLuz(base, brightL), |
| 102 | '--accent-deep': this._conLuz(base, deepL), |
| 103 | '--accent-contrast': base.l > 0.62 ? '#1f2937' : '#ffffff' |
| 104 | }; |
| 105 | const r = document.documentElement; |
| 106 | r.setAttribute('data-color', 'personalizado'); |
| 107 | for (const k in vars) r.style.setProperty(k, vars[k]); |
| 108 | if (guardar) { |
| 109 | try { localStorage.setItem(this.KEY, hex); } catch (e) { /* almacenamiento no disponible */ } |
| 110 | } |
| 111 | } |
| 112 | }; |
| 113 | |
| 114 | /* Se aplica en cuanto se carga el script, igual que Tema (evita parpadeo). */ |
| 115 | Colores.aplicar(); |