1/** 2 * Main Application JavaScript 3 * 4 * Handles navigation, mobile menu, and general UI interactions. 5 */ 6 7document.addEventListener('DOMContentLoaded', function () { 8 // ============================================ 9 // SPA Navigation (keeps player playing) 10 // ============================================ 11 initSPANavigation(); 12 13 // ============================================ 14 // Mobile Menu Toggle 15 // ============================================ 16 const mobileMenuBtn = document.getElementById('mobileMenuBtn'); 17 const mainNav = document.getElementById('mainNav'); 18 const menuOverlay = document.getElementById('menuOverlay'); 19 20 if (mobileMenuBtn && mainNav) { 21 mobileMenuBtn.addEventListener('click', function () { 22 mainNav.classList.toggle('active'); 23 if (menuOverlay) menuOverlay.classList.toggle('active'); 24 25 // Update icon 26 const icon = this.querySelector('i'); 27 if (mainNav.classList.contains('active')) { 28 icon.className = 'fas fa-times'; 29 } else { 30 icon.className = 'fas fa-bars'; 31 } 32 }); 33 34 // Close menu when clicking outside or on overlay 35 document.addEventListener('click', function (e) { 36 if (!mainNav.contains(e.target) && !mobileMenuBtn.contains(e.target) && mainNav.classList.contains('active')) { 37 mainNav.classList.remove('active'); 38 if (menuOverlay) menuOverlay.classList.remove('active'); 39 mobileMenuBtn.querySelector('i').className = 'fas fa-bars'; 40 } 41 }); 42 43 // Explicit overlay click 44 if (menuOverlay) { 45 menuOverlay.addEventListener('click', function () { 46 mainNav.classList.remove('active'); 47 this.classList.remove('active'); 48 mobileMenuBtn.querySelector('i').className = 'fas fa-bars'; 49 }); 50 } 51 52 // Close menu on link click 53 mainNav.querySelectorAll('a').forEach(link => { 54 link.addEventListener('click', () => { 55 mainNav.classList.remove('active'); 56 if (menuOverlay) menuOverlay.classList.remove('active'); 57 mobileMenuBtn.querySelector('i').className = 'fas fa-bars'; 58 }); 59 }); 60 } 61 62 // ============================================ 63 // Header Scroll Effect 64 // ============================================ 65 const header = document.getElementById('header'); 66 let lastScroll = 0; 67 68 window.addEventListener('scroll', function () { 69 const currentScroll = window.pageYOffset; 70 71 if (currentScroll > 50) { 72 header.classList.add('header-scrolled'); 73 } else { 74 header.classList.remove('header-scrolled'); 75 } 76 77 lastScroll = currentScroll; 78 }); 79 80 // ============================================ 81 // Espaçamento dinâmico abaixo do header 82 // ============================================ 83 adjustHeaderSpacing(); 84 window.addEventListener('resize', adjustHeaderSpacing); 85 // Aguarda o logo carregar para recalcular a altura real 86 window.addEventListener('load', adjustHeaderSpacing); 87 88 // ============================================ 89 // Hero Slider 90 // ============================================ 91 initHeroSlider(); 92 93 // ============================================ 94 // Forms 95 // ============================================ 96 initForms(); 97 98 // ============================================ 99 // Lazy Loading Images 100 // ============================================ 101 initLazyLoading(); 102}); 103 104/** 105 * Compensa a altura real do header fixo no conteúdo abaixo dele. 106 * Usa paddingTop no #mainContent (sobrescreve o valor CSS estático via inline style) 107 * e zera o spacer fÃsico quando presente (demo01/demo05), evitando duplo espaçamento. 108 */ 109function adjustHeaderSpacing() { 110 const header = document.getElementById('header'); 111 const mainContent = document.getElementById('mainContent'); 112 if (!header || !mainContent) return; 113 114 const h = header.offsetHeight; 115 116 // Zera spacer fÃsico (demo01/demo05) para não acumular com o paddingTop abaixo 117 const spacer = document.getElementById('header-spacer'); 118 if (spacer) spacer.style.height = '0'; 119 120 // Respiro superior: nas páginas internas (não na home) separa o conteúdo do 121 // header fixo, evitando que capa/tÃtulo fiquem colados logo abaixo dele. 122 // A home fica sem respiro â usa sliders full-bleed que passam sob o header. 123 const isHome = document.body.classList.contains('page-home'); 124 const breathing = isHome ? 0 : (window.innerWidth <= 768 ? 16 : 24); 125 126 mainContent.style.paddingTop = (h + breathing) + 'px'; 127} 128
129/** 130 * Execute scripts in dynamically loaded content 131 * Scripts inserted via innerHTML don't execute, so we need to recreate them 132 */ 133function executeScripts(container) { 134 const scripts = container.querySelectorAll('script'); 135 scripts.forEach(oldScript => { 136 const newScript = document.createElement('script'); 137 138 // Copy all attributes 139 Array.from(oldScript.attributes).forEach(attr => { 140 newScript.setAttribute(attr.name, attr.value); 141 }); 142 143 // Copy inline content 144 newScript.textContent = oldScript.textContent; 145 146 // Replace old script with new one (this causes it to execute) 147 oldScript.parentNode.replaceChild(newScript, oldScript); 148 }); 149} 150 151/** 152 * Initialize SPA Navigation 153 * Intercepts link clicks and loads content via AJAX to keep player playing 154 */ 155function initSPANavigation() { 156 const mainContent = document.getElementById('mainContent'); 157 158 // Intercept all internal link clicks 159 document.addEventListener('click', function (e) { 160 const link = e.target.closest('a'); 161 162 if (!link) return; 163 164 let href = link.getAttribute('href'); 165 166 if (!href) return; 167 168 // Skip anchor links, admin links, download links, and links with target 169 if (href.startsWith('#') || 170 href.startsWith('javascript:') || 171 href.includes('/sistema') || 172 link.hasAttribute('download') || 173 link.hasAttribute('target')) { 174 return; 175 } 176 177 // Skip WhatsApp widget elements and Life Search Links 178 if (link.closest('.whatsapp-fab') || link.closest('.whatsapp-panel') || link.closest('.live-search-item') || link.closest('.live-search-footer')) { 179 return; 180 } 181 182 // Skip if it's a file link 183 if (href.match(/\.(pdf|zip|doc|docx|xls|xlsx)$/i)) { 184 return; 185 } 186 187 // Check if it's an internal URL (same site) 188 let isInternal = false; 189 190 // Handle absolute URLs from the same site 191 if (href.startsWith(SITE_URL)) { 192 isInternal = true; 193 // Convert to relative path for consistent handling 194 href = href.replace(SITE_URL, '') || '/'; 195 } 196 // Handle relative URLs 197 else if (!href.startsWith('http') && !href.startsWith('//')) { 198 isInternal = true; 199 } 200 // Skip truly external links 201 else { 202 return; 203 } 204 205 if (!isInternal) return; 206 207 e.preventDefault(); 208 209 // Reconstruct full URL for browser history 210 const fullUrl = href.startsWith('/') || href.startsWith('./') 211 ? SITE_URL + href.replace('./', '/').replace(/^\//, '/') 212 : SITE_URL + '/' + href; 213 214 // Load the page via AJAX 215 loadPage(fullUrl); 216 }); 217 218 // Handle browser back/forward buttons 219 window.addEventListener('popstate', function (e) { 220 if (e.state && e.state.href) { 221 loadPage(e.state.href, false); 222 } else { 223 loadPage(window.location.href, false); 224 } 225 }); 226} 227 228/** 229 * Navega para uma URL do próprio site SEM recarregar o documento. 230 * 231 * Qualquer navegação dura â location.href, window.open(url, '_self'), ou um 232 * link que a SPA se recuse a interceptar â destrói o documento e, junto com 233 * ele, a instância do RadioPlayer: a transmissão ao vivo para. Cliques em <a> 234 * internos já passam por initSPANavigation(); este helper é o mesmo caminho 235 * para cliques que NÃO nascem de um link (ex.: a área clicável do slide do 236 * hero slider), aplicando exatamente as mesmas exclusões. 237 * 238 * @param {string} url URL absoluta ou relativa ao site 239 * @param {string} [target] '_blank' abre em nova aba (documento atual intacto) 240 * @returns {boolean} true se a navegação foi resolvida sem recarregar a página 241 */ 242function spaNavigate(url, target) { 243 if (!url) return false; 244 245 // Nova aba: o documento atual não é tocado, o player segue tocando. 246 if (target === '_blank') { 247 window.open(url, '_blank', 'noopener,noreferrer'); 248 return true; 249 } 250 251 const isExternal = /^(https?:)?\/\//i.test(url) && url.indexOf(SITE_URL) !== 0; 252 const isSpecial = url.charAt(0) === '#' || (/^[a-z][a-z0-9+.-]*:/i.test(url) && !/^https?:/i.test(url)); 253 const isAdmin = url.indexOf('/sistema') !== -1;
254 const isFile = /\.(pdf|zip|doc|docx|xls|xlsx)$/i.test(url.split('?')[0]); 255 256 if (isExternal || isSpecial || isAdmin || isFile || typeof loadPage !== 'function') { 257 window.location.href = url; 258 return false; 259 } 260 261 const relative = url.indexOf(SITE_URL) === 0 ? url.slice(SITE_URL.length) : url; 262 loadPage(SITE_URL.replace(/\/+$/, '') + '/' + relative.replace(/^\/+/, '')); 263 return true; 264} 265window.spaNavigate = spaNavigate; 266 267/** 268 * Load page content via AJAX 269 */ 270async function loadPage(href, pushState = true) { 271 const mainContent = document.getElementById('mainContent'); 272 273 // Parse the URL to get page parameter 274 let apiUrl; 275 276 // Remove base URL if present to get relative path 277 let path = href; 278 if (path.startsWith(SITE_URL)) { 279 path = path.replace(SITE_URL, ''); 280 } 281 282 // Separate path from query string 283 let queryString = ''; 284 if (path.includes('?')) { 285 const parts = path.split('?'); 286 path = parts[0]; 287 queryString = parts[1] || ''; 288 } 289 290 // Remove leading and trailing slashes from path 291 path = path.replace(/^\/+/, '').replace(/\/+$/, ''); 292 293 // Home page detection - but check for query params first! 294 // isHomeNav controla o respiro superior: home não recebe respiro (slider full-bleed). 295 let isHomeNav = false; 296 if (queryString && queryString.includes('page=')) { 297 // Legacy URL format with query string containing page param 298 const urlParams = new URLSearchParams(queryString); 299 const page = urlParams.get('page') || 'home'; 300 const slug = urlParams.get('slug') || ''; 301 const category = urlParams.get('category') || ''; 302 apiUrl = SITE_URL + '/api/page.php?page=' + page; 303 if (slug) apiUrl += '&slug=' + slug; 304 if (category) apiUrl += '&category=' + category; 305 isHomeNav = (page === 'home'); 306 } else if (path === '' || path === './' || path === '/' || path === 'index.php' || path === 'home') { 307 apiUrl = SITE_URL + '/api/page.php?page=home'; 308 // Normalizar href para a raiz, evitando que "/home" apareça na barra do browser 309 href = SITE_URL + '/'; 310 isHomeNav = true; 311 } else if (path.includes('/')) { 312 // Friendly URL format with slug (noticia/slug-here or promocao/slug-here) 313 const parts = path.split('/'); 314 const page = parts[0]; 315 const slug = parts[1]; 316 apiUrl = SITE_URL + '/api/page.php?page=' + page + '&slug=' + slug; 317 } else { 318 // Simple page name (noticias, equipe, eventos, etc.) 319 apiUrl = SITE_URL + '/api/page.php?page=' + path; 320 // Preserve any query params (like category filter) 321 if (queryString) { 322 apiUrl += '&' + queryString; 323 } 324 } 325 326 // CRITICAL FIX: Limpar completamente o estado anterior antes de carregar nova página 327 // 1. Parar todos os players ativos 328 if (window.previewPlayer) { 329 window.previewPlayer.pause(); 330 window.previewPlayer.currentTime = 0; 331 } 332 333 // Stop all chart audio players 334 document.querySelectorAll('audio.chart-audio-player').forEach(audio => { 335 audio.pause(); 336 audio.currentTime = 0; 337 }); 338 339 // 2. Limpar timers/intervals de sliders 340 if (window.heroSliderInterval) { 341 clearInterval(window.heroSliderInterval); 342 window.heroSliderInterval = null; 343 } 344 345 // Show loading state - Keep content but fade it 346 mainContent.style.opacity = '0.4'; 347 mainContent.style.transition = 'opacity 0.3s ease'; 348 349 try { 350 const response = await fetch(apiUrl); 351 const html = await response.text(); 352 353 // === Preservar o player de vÃdeo global entre navegações SPA === 354 // O vÃdeo (.video-mobile-section + #vmpWidget) e seu CSS (#vpgStyles) vivem 355 // dentro de #mainContent, mas NÃO devem ser destruÃdos/reiniciados ao trocar 356 // de página. Detacha os nós vivos (mantém o HLS tocando) antes de limpar e 357 // reinsere depois â tudo sÃncrono (sem await no meio) para o vÃdeo não pausar. 358 // O CSS PRECISA ser preservado também, senão o #vmpWidget perde 359 // position:fixed/width e renderiza gigante inline. 360 const _preservedVideoSection = mainContent.querySelector('.video-mobile-section'); 361 const _preservedVideoWidget = document.getElementById('vmpWidget'); 362 const _preservedVideoStyles = document.getElementById('vpgStyles'); 363 if (_preservedVideoSection) _preservedVideoSection.remove(); 364 if (_preservedVideoWidget) _preservedVideoWidget.remove(); 365 if (_preservedVideoStyles) _preservedVideoStyles.remove(); 366 367 // ONLY clear and update content AFTER data is received 368 // 3. Limpar completamente o conteúdo e classes 369 mainContent.innerHTML = ''; 370 mainContent.className = ''; 371 mainContent.removeAttribute('style'); 372 373 // Update content 374 mainContent.innerHTML = html; 375
376 // Remover qualquer player de vÃdeo vindo no HTML da nova página (evita 377 // duplicar com o preservado) e reinserir o player vivo + CSS no topo do fluxo. 378 mainContent.querySelectorAll('.video-mobile-section, #vmpWidget, #vpgStyles').forEach(function (el) { el.remove(); }); 379 if (_preservedVideoStyles) mainContent.appendChild(_preservedVideoStyles); 380 if (_preservedVideoWidget) mainContent.appendChild(_preservedVideoWidget); 381 if (_preservedVideoSection) mainContent.insertBefore(_preservedVideoSection, mainContent.firstChild); 382 383 // Restore opacity 384 mainContent.style.opacity = '1'; 385 386 // Execute any scripts in the loaded content 387 executeScripts(mainContent); 388 389 // Update URL 390 if (pushState) { 391 history.pushState({ href: href }, '', href); 392 } 393 394 // Update active nav link 395 updateActiveNavLink(href); 396 397 // Scroll to top 398 window.scrollTo({ top: 0, behavior: 'smooth' }); 399 400 // Atualiza a classe de página (home vs interna) e recalcula o respiro superior. 401 // O removeAttribute('style') acima apagou o paddingTop inline â reaplicamos aqui 402 // para manter a altura real do header + o respiro das páginas internas. 403 document.body.classList.toggle('page-home', isHomeNav); 404 adjustHeaderSpacing(); 405 406 // Reinitialize page components 407 initHeroSlider(); 408 initForms(); 409 initLazyLoading(); 410 411 // ââ Analytics: registrar pageview da navegação SPA ââ 412 if (typeof window.__analyticsTrack === 'function') { 413 // Extrair o pathname real da URL navegada 414 var trackedUrl = href; 415 if (trackedUrl.startsWith(SITE_URL)) { 416 trackedUrl = trackedUrl.replace(SITE_URL, '') || '/'; 417 } 418 window.__analyticsTrack(trackedUrl, document.title); 419 } 420 421 } catch (error) { 422 console.error('Navigation error:', error); 423 mainContent.style.opacity = '1'; 424 showNotification('Erro ao carregar página', 'error'); 425 } 426} 427 428/** 429 * Update active state on navigation links 430 */ 431function updateActiveNavLink(href) { 432 const nav = document.getElementById('mainNav'); 433 if (!nav) return; 434 435 // Remove all active classes 436 nav.querySelectorAll('.nav-link').forEach(link => { 437 link.classList.remove('active'); 438 }); 439 440 // Find and activate matching link 441 nav.querySelectorAll('.nav-link').forEach(link => { 442 const linkHref = link.getAttribute('href'); 443 if (linkHref === href || 444 (href === './' && linkHref === './') || 445 (href.includes('page=' + linkHref.split('page=')[1]))) { 446 link.classList.add('active'); 447 } 448 }); 449 450 // Sincroniza estados dos temas (Neon, Wave, Pulse) após navegação SPA 451 // pushState já atualizou window.location.pathname antes desta chamada 452 ['syncNeonNavActive', 'syncWaveNavActive', 'syncPulseNavActive'].forEach(fn => { 453 if (typeof window[fn] === 'function') window[fn](); 454 }); 455} 456 457/** 458 * Initialize Hero Slider 459 * Note: home.php has its own enhanced slider that handles title/text 460 * This function clears any existing interval and sets up a basic slider 461 * for pages that might have slides but no custom init 462 */ 463function initHeroSlider() { 464 // Clear any existing interval to prevent duplicates when navigating 465 if (window.heroSliderInterval) { 466 clearInterval(window.heroSliderInterval); 467 window.heroSliderInterval = null; 468 } 469 470 const slides = document.querySelectorAll('.hero-slide'); 471 if (slides.length <= 1) return; 472 473 // Check if home.php's slider script has already run 474 // (home.php sets window.heroSliderInterval when it starts) 475 // Only initialize if there's no existing interval 476 if (window.heroSliderInterval) return; 477 478 let currentSlide = 0; 479 480 function showSlide(index) { 481 slides.forEach((slide, i) => { 482 slide.classList.toggle('active', i === index); 483 }); 484 } 485 486 function nextSlide() { 487 currentSlide = (currentSlide + 1) % slides.length; 488 showSlide(currentSlide); 489 } 490 491 // Auto-advance every 5 seconds 492 window.heroSliderInterval = setInterval(nextSlide, 5000); 493} 494 495/** 496 * Initialize Forms 497 * Nota: o formulário de promoção (#promotionForm) tem seu próprio listener 498 * registrado diretamente em promotion-single.php â não registrar aqui para
499 * evitar handlers duplicados que causam falsos "Erro de conexão". 500 */ 501function initForms() { 502 // Formulário de promoção gerenciado pelo próprio template (promotion-single.php) 503} 504 505/** 506 * Initialize Lazy Loading 507 */ 508function initLazyLoading() { 509 const images = document.querySelectorAll('img[data-src]'); 510 511 if ('IntersectionObserver' in window) { 512 const observer = new IntersectionObserver((entries) => { 513 entries.forEach(entry => { 514 if (entry.isIntersecting) { 515 const img = entry.target; 516 img.src = img.dataset.src; 517 img.removeAttribute('data-src'); 518 observer.unobserve(img); 519 } 520 }); 521 }); 522 523 images.forEach(img => observer.observe(img)); 524 } else { 525 // Fallback for older browsers 526 images.forEach(img => { 527 img.src = img.dataset.src; 528 }); 529 } 530} 531 532/** 533 * Show Notification 534 */ 535function showNotification(message, type = 'info') { 536 // Create notification element 537 const notification = document.createElement('div'); 538 notification.className = `notification notification-${type}`; 539 notification.innerHTML = ` 540 <i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i> 541 <span>${message}</span> 542 `; 543 544 // Add styles if not exists 545 if (!document.getElementById('notification-styles')) { 546 const styles = document.createElement('style'); 547 styles.id = 'notification-styles'; 548 styles.textContent = ` 549 .notification { 550 position: fixed; 551 top: 90px; 552 right: 20px; 553 padding: 15px 25px; 554 background: var(--bg-card); 555 border: 1px solid var(--border-color); 556 border-radius: var(--radius-md); 557 display: flex; 558 align-items: center; 559 gap: 12px; 560 z-index: 10000; 561 animation: slideInRight 0.3s ease; 562 box-shadow: var(--shadow-lg); 563 } 564 .notification-success { border-left: 4px solid var(--success); } 565 .notification-success i { color: var(--success); } 566 .notification-error { border-left: 4px solid var(--danger); } 567 .notification-error i { color: var(--danger); } 568 @keyframes slideInRight { 569 from { transform: translateX(100%); opacity: 0; } 570 to { transform: translateX(0); opacity: 1; } 571 } 572 `; 573 document.head.appendChild(styles); 574 } 575 576 document.body.appendChild(notification); 577 578 // Remove after 4 seconds 579 setTimeout(() => { 580 notification.style.animation = 'slideInRight 0.3s ease reverse'; 581 setTimeout(() => notification.remove(), 300); 582 }, 4000); 583} 584 585/** 586 * Format date to Brazilian format 587 */ 588function formatDate(dateStr) { 589 const date = new Date(dateStr); 590 return date.toLocaleDateString('pt-BR'); 591} 592 593/** 594 * Format datetime to Brazilian format 595 */ 596function formatDateTime(dateStr) { 597 const date = new Date(dateStr); 598 return date.toLocaleString('pt-BR'); 599} 600 601/** 602 * Live Search AJAX 603 */ 604document.addEventListener("DOMContentLoaded", () => { 605 function setupLiveSearch(inputId, formId, dropdownId, spinnerId) { 606 const input = document.getElementById(inputId); 607 const form = document.getElementById(formId); 608 const dropdown = document.getElementById(dropdownId); 609 let spinner = document.getElementById(spinnerId); 610 if (!spinner && form) { 611 spinner = form.querySelector('.neon-search-spinner'); 612 } 613 614 if (!input || !dropdown) return; 615 616 let debounceTimer; 617 618 input.addEventListener("input", (e) => { 619 const query = e.target.value.trim(); 620 621 clearTimeout(debounceTimer); 622 623 if (query.length < 2) { 624 dropdown.innerHTML = ""; 625 dropdown.classList.remove("is-active"); 626 if (spinner) spinner.style.display = "none"; 627 return; 628 } 629 630 if (spinner) spinner.style.display = "block"; 631 632 debounceTimer = setTimeout(() => { 633 fetch(`${SITE_URL}/api/search.php?q=${encodeURIComponent(query)}`) 634 .then(res => res.json()) 635 .then(data => { 636 if (spinner) spinner.style.display = "none"; 637 638 dropdown.innerHTML = ""; 639 640 if (data.results && data.results.length > 0) {
641 data.results.forEach(item => { 642 const a = document.createElement("a"); 643 a.href = item.url; 644 a.className = "live-search-item"; 645 const thumbHtml = item.image 646 ? `<img src="${item.image}" alt="${item.title}" class="search-result-thumb" onerror="this.parentElement.classList.add('no-thumb'); this.remove();">` 647 : `<div class="search-result-thumb search-result-thumb--placeholder"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg></div>`; 648 a.innerHTML = ` 649 ${thumbHtml} 650 <div class="live-search-text"> 651 <span class="live-search-title">${item.title}</span> 652 <span class="live-search-type">${item.type}</span> 653 </div> 654 `; 655 dropdown.appendChild(a); 656 }); 657 658 const allResults = document.createElement("a"); 659 allResults.href = document.querySelector("#" + formId)?.action + "?page=busca&q=" + encodeURIComponent(query); 660 allResults.className = "live-search-footer"; 661 allResults.innerText = `Ver todos os ${data.count} resultados da busca ...`; 662 dropdown.appendChild(allResults); 663 664 } else { 665 dropdown.innerHTML = `<div class="live-search-empty">Nenhuma rádio, notÃcia ou resultado encontrado.</div>`; 666 } 667 668 dropdown.classList.add("is-active"); 669 }) 670 .catch(e => { 671 if (spinner) spinner.style.display = "none"; 672 console.error("Erro na busca:", e); 673 }); 674 }, 350); 675 }); 676 677 document.addEventListener("click", (e) => { 678 if (form && !form.contains(e.target) && !dropdown.contains(e.target)) { 679 dropdown.classList.remove("is-active"); 680 } 681 }); 682 683 input.addEventListener("focus", () => { 684 if (input.value.trim().length >= 2 && dropdown.innerHTML !== "") { 685 dropdown.classList.add("is-active"); 686 } 687 }); 688 } 689 690 setupLiveSearch("neonDesktopSearchInput", "neonDesktopSearchForm", "neonDesktopSearchDropdown", null); 691 setupLiveSearch("neonMobileSearchInput", "neonMobileSearchForm", "neonMobileSearchDropdown", "neonMobileSearchSpinner"); 692}); 693 694/** 695 * Update Theme Color Meta Tag 696 * Update top nav browser color when switching standard mode/themes 697 */ 698window.updateThemeColor = function() { 699 const themeColorMeta = document.querySelector('meta[name="theme-color"]'); 700 if (!themeColorMeta) return; 701 702 // Ler a cor de fundo do header prioritariamente (agora sendo sólida, idêntica) 703 // ou cair como fallback no bg-primary do root document. 704 let headerBg = getComputedStyle(document.documentElement).getPropertyValue('--bg-primary').trim(); 705 706 // Se conseguirmos uma cor computada e sólida (rgb) do próprio header, preferiremos usá-la. 707 const headerNode = document.querySelector('header.neon-header, header'); 708 if (headerNode) { 709 const computedHeader = getComputedStyle(headerNode).backgroundColor; 710 if (computedHeader && computedHeader !== 'transparent' && computedHeader !== 'rgba(0, 0, 0, 0)') { 711 headerBg = computedHeader; 712 } 713 } 714 715 if (headerBg) {
716 themeColorMeta.setAttribute('content', headerBg); 717 } 718};
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.