// SummerCMS docs: theme toggle, mobile drawer, TOC scroll spy, active // sidebar item and copy buttons. No framework; every DOM change uses // element APIs and textContent. (function () { "use strict"; var STORAGE_KEY = "summer-docs-theme"; var THEMES = ["system", "light", "dark"]; var LABELS = { system: "System", light: "Light", dark: "Dark" }; var root = document.documentElement; var live = document.getElementById("live-region"); function announce(text) { if (!live) { return; } live.textContent = ""; window.setTimeout(function () { live.textContent = text; }, 50); } // Theme toggle: System -> Light -> Dark -> System. If storage throws the // choice still applies for this page view and no error is shown. function currentTheme() { var t = root.getAttribute("data-theme"); return THEMES.indexOf(t) >= 0 ? t : "system"; } function applyTheme(theme, button) { root.classList.remove("light", "dark"); if (theme !== "system") { root.classList.add(theme); } root.setAttribute("data-theme", theme); if (button) { var next = THEMES[(THEMES.indexOf(theme) + 1) % THEMES.length]; var label = "Color theme: " + LABELS[theme] + ". Switch to " + LABELS[next] + "."; button.setAttribute("data-theme", theme); button.setAttribute("aria-label", label); button.setAttribute("title", label); } } var toggle = document.querySelector(".theme-toggle"); if (toggle) { applyTheme(currentTheme(), toggle); toggle.addEventListener("click", function () { var theme = THEMES[(THEMES.indexOf(currentTheme()) + 1) % THEMES.length]; try { window.localStorage.setItem(STORAGE_KEY, theme); } catch (e) { // Session-only choice. } applyTheme(theme, toggle); }); } // Mobile drawer (below 1024px). var menu = document.querySelector(".menu-button"); var panel = document.getElementById("sidebar"); var scrim = document.querySelector(".scrim"); function drawerOpen() { return panel && panel.classList.contains("open"); } function focusables() { return panel ? Array.prototype.slice.call(panel.querySelectorAll("a[href], button:not([disabled])")) : []; } function openDrawer() { if (!panel || !menu) { return; } panel.classList.add("open"); if (scrim) { scrim.hidden = false; } document.body.classList.add("drawer-open"); menu.setAttribute("aria-expanded", "true"); menu.setAttribute("aria-label", "Close navigation"); var items = focusables(); if (items.length) { items[0].focus(); } } function closeDrawer(returnFocus) { if (!drawerOpen()) { return; } panel.classList.remove("open"); if (scrim) { scrim.hidden = true; } document.body.classList.remove("drawer-open"); menu.setAttribute("aria-expanded", "false"); menu.setAttribute("aria-label", "Open navigation"); if (returnFocus) { menu.focus(); } } if (menu && panel) { menu.addEventListener("click", function () { if (drawerOpen()) { closeDrawer(true); } else { openDrawer(); } }); if (scrim) { scrim.addEventListener("click", function () { closeDrawer(true); }); } panel.addEventListener("click", function (e) { if (e.target.closest && e.target.closest("a")) { closeDrawer(false); } }); document.addEventListener("keydown", function (e) { if (!drawerOpen()) { return; } if (e.key === "Escape") { e.preventDefault(); closeDrawer(true); return; } if (e.key !== "Tab") { return; } // Keep focus inside the drawer and its close button. var items = [menu].concat(focusables()); var first = items[0]; var last = items[items.length - 1]; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } else if (items.indexOf(document.activeElement) < 0) { e.preventDefault(); first.focus(); } }); window.matchMedia("(min-width: 1024px)").addEventListener("change", function (m) { if (m.matches) { closeDrawer(false); } }); } // Scroll the active sidebar item into view, without animation. var active = document.querySelector('.sidebar a[aria-current="page"]'); // The sticky sidebar is fully on screen at 1024px and up, so "nearest" // scrolls only the sidebar, never the page (a #fragment jump is kept). if (active && active.scrollIntoView && window.matchMedia("(min-width: 1024px)").matches) { active.scrollIntoView({ block: "nearest" }); } // TOC scroll spy: the heading nearest the top is current. var tocLinks = Array.prototype.slice.call(document.querySelectorAll(".toc a")); if (tocLinks.length && "IntersectionObserver" in window) { var headings = []; tocLinks.forEach(function (link) { var id = decodeURIComponent(link.getAttribute("href").slice(1)); var h = document.getElementById(id); if (h) { headings.push({ el: h, link: link }); } }); var setCurrent = function () { var current = null; for (var i = 0; i < headings.length; i++) { if (headings[i].el.getBoundingClientRect().top <= 112) { current = headings[i]; } } if (!current && headings.length) { current = headings[0]; } tocLinks.forEach(function (link) { link.removeAttribute("aria-current"); }); if (current) { current.link.setAttribute("aria-current", "location"); } }; var observer = new IntersectionObserver(setCurrent, { rootMargin: "-64px 0px -60% 0px" }); headings.forEach(function (h) { observer.observe(h.el); }); setCurrent(); } // Copy buttons, only when the clipboard API exists. if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") { Array.prototype.forEach.call(document.querySelectorAll("figure.code .copy-button"), function (button) { button.hidden = false; button.addEventListener("click", function () { var code = button.parentNode.querySelector("pre code"); if (!code) { return; } var copy = code.cloneNode(true); Array.prototype.forEach.call(copy.querySelectorAll(".tok-prompt"), function (p) { p.parentNode.removeChild(p); }); navigator.clipboard.writeText(copy.textContent).then( function () { button.classList.add("copied"); announce("Copied"); window.setTimeout(function () { button.classList.remove("copied"); }, 2000); }, function () { announce("Copy failed. Select the code and copy it manually."); } ); }); }); } })();