- Failed to load Mermaid.
')); } }
+ async function renderMermaid() { const nodes = preview.querySelectorAll(".mermaid"); if (!nodes.length) return; try { const { default: mermaid } = await import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs"); mermaid.initialize({ startOnLoad: false, theme: getTheme() === "dark" ? "dark" : "default", securityLevel: "strict" }); await mermaid.run({ nodes: [...nodes] }); } catch { nodes.forEach(n => n.insertAdjacentHTML("beforebegin", 'Failed to load Mermaid.
')); } }
async function renderCodeHighlight() { const nodes = preview.querySelectorAll('pre code[class^="language-"]:not(.language-mermaid)'); if (!nodes.length) return; try { const hljs = await import("https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/+esm"); nodes.forEach(node => { const lines = node.querySelectorAll(".code-line"); if (!lines.length) { hljs.default.highlightElement(node); return; } const language = [...node.classList].find(name => name.startsWith("language-"))?.slice(9); lines.forEach(line => { try { line.innerHTML = hljs.default.highlight(line.textContent, { language, ignoreIllegals: true }).value; } catch { line.innerHTML = hljs.default.highlightAuto(line.textContent).value; } }); node.classList.add("hljs"); }); } catch { } }
function renderParticipantBadges(owners) {
if (!participantBadges) return;
diff --git a/static/js/public.js b/static/js/public.js
index c1a87c2..ddc0d2d 100644
--- a/static/js/public.js
+++ b/static/js/public.js
@@ -14,6 +14,7 @@ import { api } from "@rustpad/api";
import { copyText } from "@rustpad/clipboard";
import { alignPreviewLineNumbers, renderMarkdown, setMarkdownFiles } from "@rustpad/markdown";
import { toast } from "@rustpad/toast";
+import { getTheme } from "@rustpad/theme";
const token = location.pathname.split("/").filter(Boolean)[1];
const content = document.querySelector("#public-content");
@@ -21,7 +22,7 @@ const lineNumbersToggle = document.querySelector("#public-line-numbers-toggle");
const passwordDialog = document.querySelector("#public-password-dialog"), passwordForm = document.querySelector("#public-password-form"), passwordInput = document.querySelector("#public-password"), passwordError = document.querySelector("#public-password-error");
let pagePassword = "";
function pageHeaders() { return pagePassword ? { "X-RustPad-Page-Password": pagePassword } : {}; }
-async function renderMermaid() { const nodes = content.querySelectorAll(".mermaid"); if (!nodes.length) return; try { const { default: mermaid } = await import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs"); mermaid.initialize({ startOnLoad: false, theme: "dark", securityLevel: "strict" }); await mermaid.run({ nodes: [...nodes] }); } catch { nodes.forEach(n => n.insertAdjacentHTML("beforebegin", 'Failed to load Mermaid.
')); } }
+async function renderMermaid() { const nodes = content.querySelectorAll(".mermaid"); if (!nodes.length) return; try { const { default: mermaid } = await import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs"); mermaid.initialize({ startOnLoad: false, theme: getTheme() === "dark" ? "dark" : "default", securityLevel: "strict" }); await mermaid.run({ nodes: [...nodes] }); } catch { nodes.forEach(n => n.insertAdjacentHTML("beforebegin", 'Failed to load Mermaid.
')); } }
async function renderCodeHighlight() { const blocks = content.querySelectorAll('pre code[class^="language-"]'); if (!blocks.length) return; try { const hljs = await import("https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/+esm"); blocks.forEach(block => { const lines = block.querySelectorAll(".code-line"); if (!lines.length) { hljs.default.highlightElement(block); return; } const language = [...block.classList].find(name => name.startsWith("language-"))?.slice(9); lines.forEach(line => { try { line.innerHTML = hljs.default.highlight(line.textContent, { language, ignoreIllegals: true }).value; } catch { line.innerHTML = hljs.default.highlightAuto(line.textContent).value; } }); block.classList.add("hljs"); }); } catch { } }
function lockPublicContent(allowTaskUpdates) {
content.querySelectorAll('[contenteditable]').forEach(node => node.removeAttribute('contenteditable'));
diff --git a/static/js/session.js b/static/js/session.js
index d6fd69a..a263857 100644
--- a/static/js/session.js
+++ b/static/js/session.js
@@ -7,6 +7,8 @@
* See LICENSE file in repository root for details.
*/
+import { applySessionTheme } from "@rustpad/theme";
+
const ACCESS_STORAGE_VERSION_KEY = "rustpad:access-storage-version";
const ACCESS_STORAGE_VERSION = "http-only-cookie-v1";
@@ -77,6 +79,7 @@ export function getAuthToken() { return localStorage.getItem(AUTH_STATE_KEY) ? "
export function setAuthSession(session) {
localStorage.setItem(AUTH_STATE_KEY, "1");
setNickname(session.nickname);
+ applySessionTheme(session);
}
export function clearAuthSession() {
localStorage.removeItem(AUTH_STATE_KEY);
diff --git a/static/js/theme.js b/static/js/theme.js
new file mode 100644
index 0000000..45f7c06
--- /dev/null
+++ b/static/js/theme.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
+ * Source-Available Code / Dual-Licensed.
+ *
+ * Free for non-commercial and evaluation use under terms of BSL/GPLv3.
+ * Commercial or production use requires a valid paid license.
+ * See LICENSE file in repository root for details.
+ */
+const STORAGE_KEY = "rustpad:theme";
+const DEFAULT_THEME = "dark";
+const THEMES = new Set(["dark", "light"]);
+
+function normalizeTheme(value) {
+ return THEMES.has(value) ? value : DEFAULT_THEME;
+}
+
+function updateBrowserChrome(theme) {
+ document.documentElement.dataset.theme = theme;
+ document.documentElement.style.colorScheme = theme;
+ document.querySelector('meta[name="color-scheme"]')?.setAttribute("content", theme);
+}
+
+export function getTheme() {
+ return normalizeTheme(document.documentElement.dataset.theme);
+}
+
+export function applyTheme(value, { persist = true } = {}) {
+ const theme = normalizeTheme(value);
+ const previousTheme = getTheme();
+ updateBrowserChrome(theme);
+ if (persist) {
+ try { localStorage.setItem(STORAGE_KEY, theme); } catch { }
+ }
+ if (theme !== previousTheme) {
+ window.dispatchEvent(new CustomEvent("rustpad:theme-change", { detail: { theme } }));
+ }
+ return theme;
+}
+
+export function applySessionTheme(session) {
+ if (session?.theme) applyTheme(session.theme);
+}
+
+window.addEventListener("storage", event => {
+ if (event.key === STORAGE_KEY && event.newValue) applyTheme(event.newValue, { persist: false });
+});
diff --git a/static/public.html b/static/public.html
index 1748d91..36936b1 100644
--- a/static/public.html
+++ b/static/public.html
@@ -4,8 +4,9 @@