diff --git a/Cargo.lock b/Cargo.lock
index ad2bdb3..0385722 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2433,7 +2433,7 @@ dependencies = [
[[package]]
name = "rustpad"
-version = "0.0.25"
+version = "0.0.26"
dependencies = [
"argon2",
"aws-config",
diff --git a/Cargo.toml b/Cargo.toml
index 0a620e4..41952f5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "rustpad"
-version = "0.0.25"
+version = "0.0.26"
edition = "2024"
rust-version = "1.94"
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
diff --git a/static/js/pad.js b/static/js/pad.js
index e6d7f82..7a7a655 100644
--- a/static/js/pad.js
+++ b/static/js/pad.js
@@ -154,7 +154,7 @@ function replaceTableCell(line, index, value) {
const cells = body.split("|").map(cell => cell.trim()); while (cells.length <= index) cells.push(""); cells[index] = value.replace(/\|/g, "|");
return `${leading ? "| " : ""}${cells.join(" | ")}${trailing ? " |" : ""}`;
}
-function render() { if (uiState.mode === "markdown") { preview.classList.remove("preview--raw"); preview.innerHTML = renderMarkdown(editor.value); document.querySelector("#preview-label").textContent = "Markdown + Mermaid preview · text and headings are editable"; renderMermaid(); renderCodeHighlight(); } else { preview.classList.add("preview--raw"); preview.innerHTML = editor.value.split("\n").map((line, index) => `
${escapeHtml(line) || "
"}
`).join(""); document.querySelector("#preview-label").textContent = "Text preview · editable"; } alignPreviewLineNumbers(preview); document.querySelector("#characters").textContent = `${editor.value.length} characters`; document.querySelector("#words").textContent = `${editor.value.trim() ? editor.value.trim().split(/\s+/).length : 0} words`; renderGutter(); }
+function render() { if (uiState.mode === "markdown") { preview.classList.remove("preview--raw"); preview.innerHTML = renderMarkdown(editor.value); document.querySelector("#preview-label").textContent = "Preview (mermaid / markdown)"; renderMermaid(); renderCodeHighlight(); } else { preview.classList.add("preview--raw"); preview.innerHTML = editor.value.split("\n").map((line, index) => `${escapeHtml(line) || "
"}
`).join(""); document.querySelector("#preview-label").textContent = "Text preview · editable"; } alignPreviewLineNumbers(preview); document.querySelector("#characters").textContent = `${editor.value.length} characters`; document.querySelector("#words").textContent = `${editor.value.trim() ? editor.value.trim().split(/\s+/).length : 0} words`; renderGutter(); }
function applyUi({ write = false, replace = false } = {}) { editorWorkspace.className = `workspace view-${uiState.view} editor-workspace-font-${fontFamily.value}`; editorWorkspace.style.setProperty("--editor-font-size", `${fontSize.value}px`); document.body.classList.toggle("compact-editor", compactToggle.checked); document.querySelectorAll("[data-view]").forEach(b => { const a = b.dataset.view === uiState.view; b.classList.toggle("active", a); b.setAttribute("aria-pressed", String(a)); }); const markdown = uiState.mode === "markdown"; modeToggle.classList.toggle("active", markdown); modeToggle.textContent = markdown ? "Markdown" : "Text"; render(); if (write) writeEditorState(uiState, { replace }); updateAddressLabel(); }
function applyRemote(content, ownerMap) { if (content === editor.value && ownerMap == null) return; const previous = editor.value, start = editor.selectionStart, end = editor.selectionEnd, direction = editor.selectionDirection, scrollTop = editor.scrollTop, scrollLeft = editor.scrollLeft; const mapped = mapSelectionThroughEdit(previous, content, start, end); applyingRemote = true; editor.value = content; authorship = parseAuthorship(content, ownerMap); previousContent = content; editor.setSelectionRange(mapped.start, mapped.end, direction); editor.scrollTop = scrollTop; editor.scrollLeft = scrollLeft; applyingRemote = false; render(); editor.scrollTop = scrollTop; editor.scrollLeft = scrollLeft; authorshipLayer.scrollTop = scrollTop; authorshipLayer.scrollLeft = scrollLeft; }
@@ -191,3 +191,14 @@ editor.addEventListener("keydown", continueIndentation); editor.addEventListener
document.querySelector("#password-form").addEventListener("submit", async e => { e.preventDefault(); try { password = document.querySelector("#open-password").value; const result = await api("/api/access-token", { method: "POST", body: JSON.stringify({ kind: "pad", slug, password }) }); accessToken = result.access_token; setAccessToken("pad", slug, accessToken); password = ""; document.querySelector("#open-password").value = ""; document.querySelector("#password-error").textContent = ""; connect(); } catch (error) { document.querySelector("#password-error").textContent = error.message; } });
const historyPanel = document.querySelector("#history-panel"); document.querySelector("#history-button").addEventListener("click", async () => { historyPanel.classList.add("open"); historyPanel.setAttribute("aria-hidden", "false"); document.body.classList.add("history-open"); const list = document.querySelector("#history-list"); list.innerHTML = 'Loading…
'; try { const revisions = await api(`/api/pads/${encodeURIComponent(slug)}/history`, { method: "POST", body: JSON.stringify({ access_token: accessToken || null }) }); list.innerHTML = revisions.length ? revisions.map((r, i) => { const snippet = escapeHtml(r.content.trim().split("\n").slice(0, 3).join(" · ").slice(0, 150) || "Empty note"); const author = r.author || "Unknown author"; return `${escapeHtml(author)}
${snippet}
`; }).join("") : 'No history yet.
'; for (const r of revisions) { list.querySelector(`[data-preview="${r.id}"]`)?.addEventListener("click", () => { const el = list.querySelector(`#preview-${r.id}`); el.hidden = !el.hidden; el.textContent = r.content; }); list.querySelector(`[data-revision="${r.id}"]`)?.addEventListener("click", async () => { await api(`/api/pads/${encodeURIComponent(slug)}/restore`, { method: "POST", body: JSON.stringify({ access_token: accessToken || null, revision_id: r.id }) }); toast("Version restored"); }); } } catch (e) { list.innerHTML = `${escapeHtml(e.message)}
`; } }); document.querySelector("#close-history").addEventListener("click", () => { historyPanel.classList.remove("open"); historyPanel.setAttribute("aria-hidden", "true"); document.body.classList.remove("history-open"); });
+window.addEventListener("storage", event => {
+ if (event.key !== storedColorKey(nickname)) return;
+ const replacement = currentOwner();
+ authorship = replaceAuthorshipOwner(authorship, owner => ownerName(owner) === nickname, replacement, editor.value.length);
+ updateCurrentUser(); render();
+ socket?.setColor(currentUserColor() || null);
+ if (socket) socket.update(editor.value, serializeAuthorship(authorship, editor.value.length));
+});
+window.addEventListener("error", event => { setStatus("offline", "Application error"); console.error(event.error || event.message); });
+window.addEventListener("unhandledrejection", event => { setStatus("offline", "Application error"); console.error(event.reason); });
+initialize();
diff --git a/static/js/workspace.js b/static/js/workspace.js
index 6ec929a..f607c1b 100644
--- a/static/js/workspace.js
+++ b/static/js/workspace.js
@@ -62,7 +62,7 @@ function renderNotes(notes = notesCache) {
| ${escapeHtml(note.title)} |
${escapeHtml(note.created_by || "Unknown")} |
- ${note.protected ? 'Protected' : 'Editable'} |
+ ${note.protected ? 'Protected' : 'Unprotected'} |
${formatDate(note.updated_at)} |
${deleteButton(note, true)} |
`).join("")}`;