1// Theme Toggle - Robust Version 2(function() { 3 var listenerAttached = false; 4 5 // Marketing/landing pages set window.__pbForceDark (see shared/_landing_nav). 6 // Those surfaces (nav + hero + cards) are hardcoded dark-only, so a stale 7 // saved `pb-theme=light` must NOT leak onto them â that renders a dark-styled 8 // nav over a light body (the "not nice in light mode" bug). When forced, we 9 // pin data-theme=dark and DO NOT touch localStorage, so the user's real 10 // preference persists for the light-capable surfaces (member portal, /browse). 11 function isForcedDark() { 12 return window.__pbForceDark === true; 13 } 14 15 function getCurrentTheme() { 16 if (isForcedDark()) return 'dark'; 17 return localStorage.getItem('pb-theme') || 18 document.documentElement.getAttribute('data-theme') || 19 (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); 20 } 21 22 function applyTheme(theme, skipLog) { 23 document.documentElement.setAttribute('data-theme', theme); 24 localStorage.setItem('pb-theme', theme); 25 updateAllIcons(theme); 26 if (!skipLog) console.log('Theme switched to:', theme); 27 } 28 29 function toggleTheme(e) { 30 if (e) { 31 e.preventDefault(); 32 e.stopPropagation(); 33 } 34 var currentTheme = getCurrentTheme(); 35 var newTheme = currentTheme === 'dark' ? 'light' : 'dark'; 36 applyTheme(newTheme); 37 } 38 39 // Icons show what you'll SWITCH TO (opposite of current) 40 // Dark mode = show sun (click to go light) 41 // Light mode = show moon (click to go dark) 42 function updateAllIcons(theme) { 43 document.querySelectorAll('[data-theme-toggle]').forEach(function(btn) { 44 var sunIcon = btn.querySelector('.theme-icon-sun'); 45 var moonIcon = btn.querySelector('.theme-icon-moon'); 46 if (sunIcon && moonIcon) { 47 // In dark mode, show sun (to switch to light) 48 // In light mode, show moon (to switch to dark) 49 if (theme === 'dark') { 50 sunIcon.style.display = 'block'; 51 moonIcon.style.display = 'none'; 52 } else { 53 sunIcon.style.display = 'none'; 54 moonIcon.style.display = 'block'; 55 } 56 } 57 }); 58 } 59 60 function syncTheme() { 61 if (isForcedDark()) {
62 // Pin dark WITHOUT writing localStorage â preserve the real saved pref. 63 document.documentElement.setAttribute('data-theme', 'dark'); 64 updateAllIcons('dark'); 65 return; 66 } 67 var theme = getCurrentTheme(); 68 // Ensure DOM matches localStorage 69 document.documentElement.setAttribute('data-theme', theme); 70 // Update all icons 71 updateAllIcons(theme); 72 } 73 74 function attachListener() { 75 if (listenerAttached) return; 76 listenerAttached = true; 77 78 document.addEventListener('click', function(e) { 79 var toggleBtn = e.target.closest('[data-theme-toggle]'); 80 if (toggleBtn) { 81 toggleTheme(e); 82 } 83 }, { capture: true }); 84 85 // Sync across tabs (but never override a forced-dark surface) 86 window.addEventListener('storage', function(e) { 87 if (e.key === 'pb-theme' && e.newValue) { 88 if (isForcedDark()) { syncTheme(); return; } 89 applyTheme(e.newValue, true); 90 } 91 }); 92 93 // Re-sync on tab focus 94 document.addEventListener('visibilitychange', function() { 95 if (document.visibilityState === 'visible') { 96 syncTheme(); 97 } 98 }); 99 } 100 101 function init() { 102 attachListener(); 103 syncTheme(); 104 } 105 106 // Run immediately 107 if (document.readyState === 'loading') { 108 document.addEventListener('DOMContentLoaded', init); 109 } else { 110 init(); 111 } 112 113 // Turbo navigations - sync immediately and after a delay 114 ['turbo:load', 'turbo:render', 'turbo:frame-load', 'turbolinks:load'].forEach(function(event) { 115 document.addEventListener(event, function() { 116 syncTheme(); 117 // Also sync after short delay to catch race conditions 118 setTimeout(syncTheme, 10); 119 setTimeout(syncTheme, 100); 120 }); 121 }); 122 123 // MutationObserver to catch any dynamic sidebar changes 124 var observer = new MutationObserver(function(mutations) { 125 mutations.forEach(function(mutation) { 126 if (mutation.addedNodes.length) { 127 mutation.addedNodes.forEach(function(node) { 128 if (node.nodeType === 1 && (node.hasAttribute && node.hasAttribute('data-theme-toggle') || node.querySelector && node.querySelector('[data-theme-toggle]'))) { 129 syncTheme(); 130 } 131 }); 132 } 133 }); 134 }); 135 136 observer.observe(document.body, { childList: true, subtree: true }); 137 138 window.PBTheme = { toggle: toggleTheme, set: applyTheme, get: getCurrentTheme, sync: syncTheme }; 139})();
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.