1/** 2 * <logiks-mapa> â Web Component para o mapa interativo 3 * "Nossa Presença Nacional 2026" da Logiks Tecnologia. 4 * 5 * Uso: 6 * <script src="logiks-mapa.js"></script> 7 * <logiks-mapa></logiks-mapa> 8 * 9 * Atributos opcionais: 10 * image="caminho/para/imagem.jpg" â sobrescreve a imagem de fundo 11 * (padrão: "mapa-logiks-bg.jpg") 12 * 13 * Para customizar dados (cidades/posições), edite os arrays MARKERS e LEGEND 14 * logo no inÃcio desta classe. 15 */ 16 17class LogiksMapa extends HTMLElement { 18 19 // ============= CONFIGURAÃÃO (edite aqui se quiser mudar) ============= 20 static MARKERS = [[33.15, 34.89, "Amazonas"], [60.26, 33.53, "Maranhão"], [66.74, 35.96, "Ceará"], [71.51, 37.69, "Rio Grande do Norte"], [71.99, 44.62, "Pernambuco"], [35.96, 49.06, "Rondônia"], [65.34, 52.53, "Bahia"], [70.2, 49.6, "Sergipe"], [56.1, 58.4, "BrasÃlia"], [51.95, 61.86, "Goiás"], [60.25, 64.21, "Minas Gerais"], [54.4, 72.1, "São Paulo"], [63.27, 75.56, "Rio de Janeiro"], [50.25, 77.29, "Paraná"]]; 21 static LEGEND = ["BrasÃlia", "Goiás", "Ceará", "Minas Gerais", "Bahia", "Rio Grande do Norte", "Maranhão", "Pernambuco", "Sergipe", "Rondônia", "Amazonas", "Paraná", "São Paulo", "Rio de Janeiro"]; 22 static PILL_CITIES = []; 23 // ===================================================================== 24 25 constructor() { 26 super(); 27 this.attachShadow({ mode: 'open' }); 28 } 29 30 connectedCallback() { 31 const image = this.getAttribute('image') || 'mapa-logiks-bg.jpg'; 32 this.shadowRoot.innerHTML = this.#render(image); 33 this.#setupInteractions(); 34 } 35 36 #render(imageSrc) { 37 const C = LogiksMapa; 38 const markersHTML = C.MARKERS.map(([x, y, city]) => ` 39 <div class="marker" tabindex="0" data-city="${city}" style="left:${x}%;top:${y}%"> 40 <div class="hit"></div> 41 <div class="ring"></div> 42 <div class="ring ring-2"></div> 43 <div class="glow"></div> 44 <div class="tip">${city}</div> 45 </div>`).join(''); 46 47 const pillSet = new Set(C.PILL_CITIES); 48 const legendHTML = C.LEGEND.map(city => ` 49 <div class="item${pillSet.has(city) ? ' pill' : ''}" data-city="${city}"> 50 <span class="bullet">â¢</span><span>${city}</span> 51 </div>`).join(''); 52 53 return ` 54 <link rel="preconnect" href="https://fonts.googleapis.com"> 55 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 56 <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700;800&display=swap" rel="stylesheet"> 57 <style> 58 :host { 59 display: block; 60 width: 100%; 61 margin: 0 auto; 62 font-family: system-ui, -apple-system, sans-serif; 63 } 64 .stage { 65 position: relative; 66 width: 100%; 67 aspect-ratio: 1600 / 960; 68 background: #f5f4f1 url('${imageSrc}') center/cover no-repeat; 69 overflow: hidden; 70 box-shadow: 0 30px 80px -20px rgba(200, 110, 0, 0.25); 71 animation: stagein 1s ease-out; 72 } 73 @keyframes stagein { 74 from { opacity: 0; transform: scale(0.99); } 75 to { opacity: 1; transform: scale(1); } 76 } 77 78 .shine { 79 position: absolute; top: 4%; left: 27%; 80 width: 47%; height: 9%; overflow: hidden; pointer-events: none; 81 border-radius: 12px; 82 } 83 .shine::after { 84 content: ''; position: absolute; top: 0; left: -50%; 85 width: 50%; height: 100%; 86 background: linear-gradient(90deg, transparent, rgba(255,255,255,0.55), transparent); 87 transform: skewX(-20deg); 88 animation: shine 4.5s ease-in-out infinite; 89 } 90 @keyframes shine { 91 0%, 35% { left: -50%; } 92 65%, 100% { left: 110%; } 93 } 94 95 .circuit { 96 position: absolute; left: 0; top: 0; 97 width: 28%; height: 100%; pointer-events: none; 98 background: 99 radial-gradient(circle at 15% 30%, rgba(232,136,30,0.5) 0%, transparent 1.5%), 100 radial-gradient(circle at 8% 55%, rgba(232,136,30,0.5) 0%, transparent 1.2%), 101 radial-gradient(circle at 22% 75%, rgba(232,136,30,0.5) 0%, transparent 1.4%), 102 radial-gradient(circle at 5% 85%, rgba(232,136,30,0.5) 0%, transparent 1%); 103 animation: flicker 3s ease-in-out infinite; 104 mix-blend-mode: multiply; 105 } 106 @keyframes flicker { 107 0%, 100% { opacity: 0; } 108 50% { opacity: 0.85; } 109 } 110 111 .legend { 112 position: absolute; left: 78%; top: 28%; right: 1%; bottom: 3.5%; 113 display: flex; flex-direction: column; justify-content: space-between; 114 z-index: 3; padding: 0.4% 0; 115 } 116 .item { 117 display: flex; align-items: center; gap: 0.45em; 118 padding: 0.2em 0.7em; 119 font-family: 'Orbitron', system-ui, sans-serif; 120 font-size: clamp(10px, 1.25vw, 19px); 121 font-weight: 500; 122 letter-spacing: 0.02em; 123 color: #E89504; 124 border-radius: 999px; 125 transition: background 0.25s, color 0.2s, transform 0.25s, letter-spacing 0.25s; 126 cursor: pointer; 127 line-height: 1.1; 128 } 129 .bullet { font-size: 1.1em; line-height: 0;
129 margin-top: -0.05em; } 130 .pill { background: rgba(232, 149, 4, 0.18); } 131 .item:hover, .item.is-active { 132 font-weight: 800; 133 color: #a06600; 134 background: rgba(232, 149, 4, 0.32); 135 transform: translateX(4px); 136 letter-spacing: 0.2px; 137 } 138 139 .marker { 140 position: absolute; aspect-ratio: 1; width: 3.6%; 141 transform: translate(-50%, -50%); 142 cursor: pointer; outline: none; z-index: 4; 143 } 144 .hit { position: absolute; inset: -80%; border-radius: 50%; z-index: 1; } 145 146 .ring { 147 position: absolute; inset: 0; border-radius: 50%; 148 border: 2.5px solid #e8881e; opacity: 0; 149 } 150 .marker:hover .ring, 151 .marker:focus .ring, 152 .marker.is-active .ring { animation: pulse 2.2s ease-out infinite; } 153 .marker:hover .ring-2, 154 .marker:focus .ring-2, 155 .marker.is-active .ring-2 { animation-delay: 1.1s; } 156 @keyframes pulse { 157 0% { transform: scale(1); opacity: 1; border-width: 3px; } 158 70% { opacity: 0.15; border-width: 1px; } 159 100% { transform: scale(2.8); opacity: 0; border-width: 1px; } 160 } 161 162 .glow { 163 position: absolute; inset: -25%; border-radius: 50%; 164 background: radial-gradient(circle, rgba(232,136,30,0.45) 0%, transparent 60%); 165 opacity: 0; transition: opacity 0.25s ease; 166 } 167 .marker:hover .glow, 168 .marker:focus .glow, 169 .marker.is-active .glow { 170 opacity: 1; animation: glow 1.8s ease-in-out infinite; 171 } 172 @keyframes glow { 173 0%, 100% { opacity: 0.55; transform: scale(0.95); } 174 50% { opacity: 1; transform: scale(1.15); } 175 } 176 177 .tip { 178 position: absolute; bottom: calc(100% + 14px); left: 50%; 179 transform: translateX(-50%) translateY(8px); 180 background: linear-gradient(135deg, #f29a3a 0%, #d96c00 100%); 181 color: white; 182 padding: 7px 16px; border-radius: 999px; 183 font-family: 'Orbitron', system-ui, sans-serif; 184 font-size: clamp(10px, 0.95vw, 15px); 185 font-weight: 700; white-space: nowrap; letter-spacing: 0.3px; 186 opacity: 0; pointer-events: none; 187 transition: opacity 0.22s ease, transform 0.22s ease; 188 box-shadow: 0 6px 18px rgba(232,136,30,0.45), 0 2px 4px rgba(0,0,0,0.1); 189 z-index: 10; 190 } 191 .tip::after { 192 content: ''; position: absolute; top: 100%; left: 50%; 193 transform: translateX(-50%); 194 border: 7px solid transparent; border-top-color: #d96c00; 195 } 196 .marker:hover .tip, 197 .marker:focus .tip, 198 .marker.is-active .tip { 199 opacity: 1; transform: translateX(-50%) translateY(0); 200 } 201 </style> 202 203 <div class="stage" part="stage"> 204 <div class="shine"></div> 205 <div class="circuit"></div> 206 <div class="legend">${legendHTML}</div> 207 ${markersHTML} 208 </div> 209 `; 210 } 211 212 #setupInteractions() { 213 const root = this.shadowRoot; 214 const markers = root.querySelectorAll('.marker'); 215 const items = root.querySelectorAll('.item'); 216 217 const findItem = c => root.querySelector('.item[data-city="' + c + '"]'); 218 const findMarker = c => root.querySelector('.marker[data-city="' + c + '"]'); 219 220 markers.forEach(marker => { 221 const city = marker.dataset.city; 222 marker.addEventListener('mouseenter', () => findItem(city)?.classList.add('is-active')); 223 marker.addEventListener('mouseleave', () => findItem(city)?.classList.remove('is-active')); 224 marker.addEventListener('focus', () => findItem(city)?.classList.add('is-active')); 225 marker.addEventListener('blur', () => findItem(city)?.classList.remove('is-active')); 226 227 marker.addEventListener('click', e => { 228 e.stopPropagation(); 229 const wasActive = marker.classList.contains('is-active');
230 markers.forEach(m => m.classList.remove('is-active')); 231 items.forEach(i => i.classList.remove('is-active')); 232 if (!wasActive) { 233 marker.classList.add('is-active'); 234 findItem(city)?.classList.add('is-active'); 235 } 236 // Dispara um evento personalizado pra quem usar o componente conseguir reagir 237 this.dispatchEvent(new CustomEvent('city-selected', { 238 detail: { city }, bubbles: true, composed: true 239 })); 240 }); 241 242 marker.addEventListener('keydown', e => { 243 if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); marker.click(); } 244 }); 245 }); 246 247 items.forEach(item => { 248 const city = item.dataset.city; 249 item.addEventListener('mouseenter', () => findMarker(city)?.classList.add('is-active')); 250 item.addEventListener('mouseleave', () => findMarker(city)?.classList.remove('is-active')); 251 }); 252 253 // Clique fora do componente desativa qualquer marcador ativo 254 document.addEventListener('click', () => { 255 markers.forEach(m => m.classList.remove('is-active')); 256 items.forEach(i => i.classList.remove('is-active')); 257 }); 258 } 259} 260 261customElements.define('logiks-mapa', LogiksMapa);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.