PageSourceSearch

https://saxobroko.com/assets/js/darkmode.js?v=2.8.0

js saxobroko.com collected 2026-10-05 22:53:01 UTC 4,975 bytes, 152 lines download raw bytes

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.