1/** 2 * Theme toggle â class/CSS-variable based (not mix-blend inversion). 3 * Persists preference in localStorage key `sb-theme` ("light" | "dark"). 4 * Falls back to prefers-color-scheme when unset. 5 */ 6(function () { 7 var KEY = "sb-theme"; 8 var root = document.documentElement; 9 var mq = window.matchMedia("(prefers-color-scheme: dark)"); 10 11 function stored() { 12 try { 13 return localStorage.getItem(KEY); 14 } catch (e) { 15 return null; 16 } 17 } 18 19 function resolve(explicit) { 20 if (explicit === "light" || explicit === "dark") return explicit; 21 return mq.matches ? "dark" : "light"; 22 } 23 24 function apply(theme, persist) { 25 root.setAttribute("data-theme", theme); 26 root.style.colorScheme = theme; 27 var meta = document.querySelector('meta[name="theme-color"]'); 28 if (meta) { 29 meta.setAttribute("content", theme === "dark" ? "#0c0e12" : "#eceff3"); 30 } 31 if (persist) { 32 try { 33 localStorage.setItem(KEY, theme); 34 } catch (e) {} 35 } 36 syncToggle(theme); 37 } 38 39 function current() { 40 return root.getAttribute("data-theme") || resolve(stored()); 41 } 42 43 function syncToggle(theme) { 44 var btn = document.getElementById("theme-toggle"); 45 if (!btn) return; 46 var dark = theme === "dark"; 47 btn.setAttribute("aria-pressed", dark ? "true" : "false"); 48 btn.setAttribute( 49 "aria-label", 50 dark ? "Switch to light mode" : "Switch to dark mode" 51 ); 52 btn.title = dark ? "Light mode" : "Dark mode"; 53 } 54 55 function prefersReducedMotion() { 56 try { 57 return window.matchMedia("(prefers-reduced-motion: reduce)").matches; 58 } catch (e) { 59 return false; 60 } 61 } 62 63 function toggleTheme(event) { 64 var next = current() === "dark" ? "light" : "dark"; 65 if (prefersReducedMotion() || typeof document.startViewTransition !== "function") { 66 apply(next, true); 67 return; 68 } 69 70 var x = event && typeof event.clientX === "number" ? event.clientX : window.innerWidth / 2; 71 var y = event && typeof event.clientY === "number" ? event.clientY : window.innerHeight / 2; 72 var endRadius = Math.hypot( 73 Math.max(x, window.innerWidth - x), 74 Math.max(y, window.innerHeight - y) 75 ); 76 77 root.classList.add("theme-vt"); 78 var transition = document.startViewTransition(function () { 79 apply(next, true); 80 }); 81 82 transition.ready 83 .then(function () { 84 root.animate( 85 { 86 clipPath: [ 87 "circle(0px at " + x + "px " + y + "px)", 88 "circle(" + endRadius + "px at " + x + "px " + y + "px)", 89 ], 90 }, 91 { 92 duration: 480, 93 easing: "cubic-bezier(.22,1,.36,1)", 94 pseudoElement: "::view-transition-new(root)", 95 } 96 ); 97 }) 98 .catch(function () {}); 99 100 transition.finished.finally(function () { 101 root.classList.remove("theme-vt"); 102 }); 103 } 104 105 function ensureToggle() { 106 if (document.getElementById("theme-toggle")) return; 107 var btn = document.createElement("button"); 108 btn.type = "button"; 109 btn.id = "theme-toggle"; 110 btn.className = "theme-toggle"; 111 btn.innerHTML = 112 '<span class="theme-toggle__moon" aria-hidden="true">' + 113 '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">' + 114 '<path d="M21 14.5A8.5 8.5 0 1 1 9.5 3 7 7 0 0 0 21 14.5z"/></svg></span>' + 115 '<span class="theme-toggle__sun" aria-hidden="true">' + 116 '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">' + 117 '<circle cx="12" cy="12" r="4"/>' + 118 '<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>' + 119 "</svg></span>"; 120 btn.addEventListener("click", toggleTheme); 121 var slot = document.querySelector(".site-nav__end"); 122 if (slot) slot.appendChild(btn); 123 else document.body.appendChild(btn); 124 syncToggle(current()); 125 } 126 127 // Migrate legacy darkmode-js preference once (before first paint sync) 128 try { 129 if (localStorage.getItem(KEY) == null && localStorage.getItem("darkmode") === "true") { 130 localStorage.setItem(KEY, "dark"); 131 localStorage.removeItem("darkmode"); 132 } 133 } catch (e) {} 134 135 // Re-apply in case head boot ran before this, and keep OS sync when unset 136 apply(resolve(stored()), false); 137 if (document.readyState === "loading") { 138 document.addEventListener("DOMContentLoaded", ensureToggle); 139 } else { 140 ensureToggle(); 141 } 142 143 if (typeof mq.addEventListener === "function") { 144 mq.addEventListener("change", function () { 145 if (!stored()) apply(resolve(null), false); 146 });
147 } else if (typeof mq.addListener === "function") { 148 mq.addListener(function () { 149 if (!stored()) apply(resolve(null), false); 150 }); 151 } 152})();
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.