multi mode

This commit is contained in:
Mateusz Gruszczyński
2026-07-31 10:10:19 +02:00
parent e39a1154e6
commit f7afee886b
25 changed files with 910 additions and 212 deletions
+721 -180
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -4,9 +4,10 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="dark">
<meta name="color-scheme" content="dark light">
<title>__DOCUMENT_TITLE__ · __PARENT_TITLE__</title>
__APP_STYLESHEET__
__APP_THEME_BOOTSTRAP__
__APP_STYLESHEET__
__APP_IMPORT_MAP__
__APP_ENTRYPOINT__
</head>
@@ -136,7 +137,7 @@
disabled>Full</button></div>
</div>
</div>
<div id="participant-badges" class="participant-badges" aria-label="Participants"></div>
<div id="participant-badges" class="participant-badges" aria-label="Participants" hidden></div>
<div class="editor-shell">
<div id="line-gutter" class="line-gutter" aria-hidden="true"></div>
<div id="authorship-layer" class="authorship-layer" aria-hidden="true"></div>
+2 -1
View File
@@ -4,9 +4,10 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="dark">
<meta name="color-scheme" content="dark light">
<meta name="robots" content="noindex">
<title>__ERROR_TITLE__ · RustPad</title>
__APP_THEME_BOOTSTRAP__
__APP_STYLESHEET__
</head>
+19 -3
View File
@@ -4,8 +4,9 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="dark">
<meta name="color-scheme" content="dark light">
<title>RustPad</title>
__APP_THEME_BOOTSTRAP__
__APP_STYLESHEET__
__APP_IMPORT_MAP__
__APP_ENTRYPOINT__
@@ -144,6 +145,19 @@
<label>Nickname<input id="profile-nickname" maxlength="40" required></label>
<label class="profile-color-field">Editor color<input id="profile-color" type="color"
aria-label="Choose your editor color"></label>
<fieldset class="profile-theme-field">
<legend>Interface theme</legend>
<div class="theme-options">
<label class="theme-option">
<input type="radio" name="profile-theme" value="dark" checked>
<span><strong>Dark</strong><small>Current RustPad appearance</small></span>
</label>
<label class="theme-option">
<input type="radio" name="profile-theme" value="light">
<span><strong>Light</strong><small>Warm cream surfaces and dark text</small></span>
</label>
</div>
</fieldset>
<label data-local-profile-field>Current e-mail<input id="profile-current-email" type="email" readonly></label>
<label data-local-profile-field>New e-mail<input id="profile-email" type="email" maxlength="320"
placeholder="Leave empty to keep current"></label>
@@ -152,8 +166,10 @@
<label data-local-profile-field>Current password<input id="profile-password" type="password" minlength="8"
maxlength="128" placeholder="Required for e-mail or password changes"></label>
</div>
<button class="primary-button" type="submit">Save profile</button>
<button id="profile-delete" data-local-profile-field class="danger-button" type="button">Delete account</button>
<div class="profile-actions">
<button class="primary-button" type="submit">Save profile</button>
<button id="profile-delete" data-local-profile-field class="danger-button" type="button">Delete account</button>
</div>
<p id="profile-message" class="form-message" role="status"></p>
</form>
</dialog>
+5 -2
View File
@@ -12,6 +12,7 @@ installGlobalDiagnostics();
import { bindIdentityDialog, handleAccountActionToken, handleAccountConfirmationToken, handleResetToken, handleShareInvitationToken, logoutCurrentSession, validateCurrentSession } from "@rustpad/auth-ui";
import { getAuthToken, getGuestId, setAccessToken } from "@rustpad/session";
import { applyTheme, getTheme } from "@rustpad/theme";
import { api } from "@rustpad/api";
import { copyText } from "@rustpad/clipboard";
import { safeAppUrl } from "@rustpad/security";
@@ -313,6 +314,8 @@ if (identityDialog) {
document.querySelector("#profile-nickname").value = currentSession?.nickname || "";
const profileColor = document.querySelector("#profile-color");
profileColor.value = currentSession?.editor_color || "#7c6cff";
const selectedTheme = currentSession?.theme || getTheme();
profileForm.querySelectorAll(`input[name="profile-theme"]`).forEach(input => { input.checked = input.value === selectedTheme; });
document.querySelector("#profile-current-email").value = currentSession?.email || "";
document.querySelector("#profile-email").value = "";
document.querySelector("#profile-new-password").value = "";
@@ -322,7 +325,7 @@ if (identityDialog) {
profileMessage.classList.remove("success", "error");
const directoryManaged = Boolean(currentSession?.directory_managed);
document.querySelector("#profile-copy").textContent = directoryManaged
? "Directory account details are read-only. You can change only the displayed nickname."
? "Directory account details are read-only. You can change the nickname, editor color, and interface theme."
: "Manage your local RustPad account.";
document.querySelectorAll("[data-local-profile-field]").forEach(element => { element.hidden = directoryManaged; });
document.querySelectorAll("[data-directory-profile-field]").forEach(element => { element.hidden = !directoryManaged; });
@@ -338,7 +341,7 @@ if (identityDialog) {
profileDialog?.addEventListener("click", event => { if (event.target === profileDialog) profileDialog.close(); });
profileForm?.addEventListener("submit", async event => {
event.preventDefault(); const message = document.querySelector("#profile-message"); message.textContent = ""; message.classList.remove("success", "error");
try { const selectedColor = document.querySelector("#profile-color").value; const newEmail = currentSession?.directory_managed ? null : (document.querySelector("#profile-email").value.trim() || null); const newPassword = currentSession?.directory_managed ? null : (document.querySelector("#profile-new-password").value || null); const password = currentSession?.directory_managed ? "" : document.querySelector("#profile-password").value; if ((newEmail || newPassword) && !password) throw new Error("Enter the current password to change e-mail or password."); const result = await api("/api/auth/profile", { method: "POST", headers: authHeaders(), body: JSON.stringify({ nickname: document.querySelector("#profile-nickname").value.trim(), editor_color: selectedColor, new_email: newEmail, new_password: newPassword, password }) }); message.textContent = result.message; message.classList.add("success"); currentSession.nickname = result.nickname; currentSession.editor_color = result.editor_color; renderAccount(currentSession); } catch (e) { message.textContent = e.message; message.classList.add("error"); }
try { const selectedColor = document.querySelector("#profile-color").value; const selectedTheme = profileForm.querySelector(`input[name="profile-theme"]:checked`)?.value || "dark"; const newEmail = currentSession?.directory_managed ? null : (document.querySelector("#profile-email").value.trim() || null); const newPassword = currentSession?.directory_managed ? null : (document.querySelector("#profile-new-password").value || null); const password = currentSession?.directory_managed ? "" : document.querySelector("#profile-password").value; if ((newEmail || newPassword) && !password) throw new Error("Enter the current password to change e-mail or password."); const result = await api("/api/auth/profile", { method: "POST", headers: authHeaders(), body: JSON.stringify({ nickname: document.querySelector("#profile-nickname").value.trim(), editor_color: selectedColor, theme: selectedTheme, new_email: newEmail, new_password: newPassword, password }) }); message.textContent = result.message; message.classList.add("success"); currentSession.nickname = result.nickname; currentSession.editor_color = result.editor_color; currentSession.theme = result.theme; applyTheme(result.theme); renderAccount(currentSession); } catch (e) { message.textContent = e.message; message.classList.add("error"); }
});
document.querySelector("#profile-delete")?.addEventListener("click", async () => {
const message = document.querySelector("#profile-message"); const password = document.querySelector("#profile-password").value;
+2 -1
View File
@@ -21,6 +21,7 @@ import { bindIdentityDialog, validateCurrentSession } from "@rustpad/auth-ui";
import { bindNoteFiles } from "@rustpad/note-files";
import { currentShareUrl, readEditorState, writeEditorState } from "@rustpad/url-state";
import { toast } from "@rustpad/toast";
import { getTheme } from "@rustpad/theme";
export function startNoteEditor(adapter) {
const editor = document.querySelector("#editor"), preview = document.querySelector("#preview"), editorWorkspace = document.querySelector("#editor-workspace"), gutter = document.querySelector("#line-gutter"), ownerLabels = document.querySelector("#owner-labels"), authorshipLayer = document.querySelector("#authorship-layer");
@@ -182,7 +183,7 @@ export function startNoteEditor(adapter) {
setStatus(null, "Connecting…");
}
function updateAddressLabel() { document.querySelector(adapter.addressSelector).textContent = `${location.pathname}${location.search}`; }
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: "dark", securityLevel: "strict" }); await mermaid.run({ nodes: [...nodes] }); } catch { nodes.forEach(n => n.insertAdjacentHTML("beforebegin", '<p class="error">Failed to load Mermaid.</p>')); } }
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", '<p class="error">Failed to load Mermaid.</p>')); } }
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;
+2 -1
View File
@@ -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", '<p class="error">Failed to load Mermaid.</p>')); } }
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", '<p class="error">Failed to load Mermaid.</p>')); } }
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'));
+3
View File
@@ -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);
+46
View File
@@ -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 });
});
+2 -1
View File
@@ -4,8 +4,9 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="dark">
<meta name="color-scheme" content="dark light">
<title>Published note · RustPad</title>
__APP_THEME_BOOTSTRAP__
__APP_STYLESHEET__
__APP_IMPORT_MAP__
__APP_ENTRYPOINT__
+3 -2
View File
@@ -4,9 +4,10 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="dark">
<meta name="color-scheme" content="dark light">
<title>__WORKSPACE_TITLE__ · RustPad</title>
__APP_STYLESHEET__
__APP_THEME_BOOTSTRAP__
__APP_STYLESHEET__
__APP_IMPORT_MAP__
__APP_ENTRYPOINT__
</head>