/* Harbor's theme picker, shared by the chat and the board. A theme is a palette family (themes.css; Harbor's own look in harbor.css is the default) and a mode: system, light or dark. Both are kept in localStorage (harbor.palette, harbor.theme), set on before the first paint by each page's head script, and kept in step across open tabs. Any element with data-theme-open opens the Appearance dialog; radios named "theme" set the mode; elements with data-theme-name show the current theme's name. */ (function () { var THEMES = [ { id: "harbor", name: "Harbor", note: "Default" }, { id: "dmg", name: "DMG" }, { id: "graphite", name: "Duke" }, { id: "atlas", name: "FLUDD" }, { id: "rosepine", name: "Flowey" }, { id: "gruvbox", name: "Gerudo" }, { id: "contrast", name: "Kill Screen" }, { id: "snes", name: "Mode 7" }, { id: "terra", name: "Shovel Knight" }, { id: "nord", name: "Snowdin" }, { id: "solarized", name: "Solaire" }, { id: "tokyo", name: "Tokyo-to" }, { id: "dracula", name: "Vampire Killer" } ]; var MODES = [["auto", "System", "monitor"], ["light", "Light", "sun"], ["dark", "Dark", "moon"]]; var root = document.documentElement, dark = matchMedia("(prefers-color-scheme: dark)"), dialog = null; function get(k, d) { try { var v = localStorage.getItem("harbor." + k); return v === null ? d : JSON.parse(v); } catch (e) { return d; } } function put(k, v) { try { localStorage.setItem("harbor." + k, JSON.stringify(v)); } catch (e) {} } function mode() { var m = get("theme", "auto"); return m === "light" || m === "dark" ? m : "auto"; } function palette() { var p = get("palette", "harbor"); return THEMES.some(function (t) { return t.id === p; }) ? p : "harbor"; } function side() { var m = mode(); return m === "auto" ? (dark.matches ? "dark" : "light") : m; } function icon(n) { return ''; } function name() { var p = palette(); return THEMES.filter(function (t) { return t.id === p; })[0].name; } function apply() { var m = mode(), p = palette(); if (m === "auto") delete root.dataset.theme; else root.dataset.theme = m; if (p === "harbor") delete root.dataset.palette; else root.dataset.palette = p; paint(); } function paint() { var m = mode(), p = palette(), s = side(); document.querySelectorAll('input[name="theme"], input[name="themeMode"]').forEach(function (r) { r.checked = r.value === m; }); document.querySelectorAll("[data-theme-name]").forEach(function (e) { e.textContent = name(); }); if (!dialog) return; dialog.querySelectorAll("[data-pick]").forEach(function (b) { b.setAttribute("aria-pressed", String(b.dataset.pick === p)); b.querySelector(".tprev").dataset.p = b.dataset.pick + "-" + s; }); } function set(key, value) { put(key, value); apply(); } function build() { dialog = document.createElement("dialog"); dialog.className = "theme-dialog"; dialog.setAttribute("aria-labelledby", "themeDialogTitle"); dialog.innerHTML = '

Appearance

' + '
Mode
' + MODES.map(function (x) { return '"; }).join("") + '
' + THEMES.map(function (t) { return '"; }).join("") + '
'; document.body.appendChild(dialog); dialog.addEventListener("click", function (e) { if (e.target === dialog || e.target.closest("[data-theme-close]")) return dialog.close(); var card = e.target.closest("[data-pick]"); if (card) set("palette", card.dataset.pick); }); } function open() { if (!dialog) build(); paint(); dialog.showModal(); var on = dialog.querySelector('[aria-pressed="true"]'); if (on) on.focus(); } document.addEventListener("change", function (e) { var r = e.target; if (r.matches && r.matches('input[name="theme"], input[name="themeMode"]')) set("theme", r.value); }); document.addEventListener("click", function (e) { if (e.target.closest && e.target.closest("[data-theme-open]")) open(); }); window.addEventListener("storage", function (e) { if (e.key === "harbor.theme" || e.key === "harbor.palette") apply(); }); if (dark.addEventListener) dark.addEventListener("change", paint); window.HarborTheme = { themes: THEMES, apply: apply, open: open, mode: mode, palette: palette, side: side }; apply(); })();