hidden navbar

This commit is contained in:
Mateusz Gruszczyński
2026-08-06 13:49:50 +02:00
parent ce368cf3fd
commit 9880b64659
15 changed files with 247 additions and 22 deletions
+15 -1
View File
@@ -32,7 +32,7 @@ import { loadHighlight, loadMediaPlayer, loadMermaid } from "@rustpad/vendor-lib
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");
const modeToggle = document.querySelector("#mode-toggle"), toolbarCollapseToggle = document.querySelector("#toolbar-collapse-toggle"), passwordDialog = document.querySelector("#password-dialog"), identityDialog = document.querySelector("#identity-dialog");
const modeToggle = document.querySelector("#mode-toggle"), toolbarCollapseToggle = document.querySelector("#toolbar-collapse-toggle"), navbarCollapseToggle = document.querySelector("#navbar-collapse-toggle"), passwordDialog = document.querySelector("#password-dialog"), identityDialog = document.querySelector("#identity-dialog");
const accessLevel = document.querySelector("#access-level"), roomDetails = document.querySelector("#room-details"), roomUsers = document.querySelector("#room-users"), roomCount = document.querySelector("#room-count"), socketLatency = document.querySelector("#socket-latency"), mobileConnectionDetails = document.querySelector("#mobile-connection-details"), chatMessages = document.querySelector("#chat-messages"), chatForm = document.querySelector("#chat-form"), chatInput = document.querySelector("#chat-input"), chatUnread = document.querySelector("#chat-unread"), mobileChatUnread = document.querySelector("#mobile-chat-unread"), connectionNotice = document.querySelector("#connection-notice"), connectionNoticeTitle = document.querySelector("#connection-notice-title"), connectionNoticeMessage = document.querySelector("#connection-notice-message");
const saveState = document.querySelector("#save-state");
editor.readOnly = true;
@@ -42,6 +42,7 @@ export function startNoteEditor(adapter) {
const shareToken = new URLSearchParams(location.search).get("share");
const notePreferenceKey = name => `rustpad:${name}:${location.pathname}`;
let toolbarCollapsed = localStorage.getItem(notePreferenceKey("toolbar-collapsed")) === "on";
let navbarCollapsed = localStorage.getItem(notePreferenceKey("navbar-collapsed")) === "on";
const collaborationClientId = typeof crypto.randomUUID === "function"
? crypto.randomUUID().replaceAll("-", "")
: `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
@@ -234,6 +235,7 @@ export function startNoteEditor(adapter) {
previewLineToggle.checked = info.preview_line_numbers === true;
lineLinksToggle.checked = info.line_links === true;
toolbarCollapsed = info.toolbar_collapsed === true;
navbarCollapsed = info.navbar_collapsed === true;
if (["mono", "system", "serif", "arial", "georgia"].includes(info.font_family)) fontFamily.value = info.font_family;
if (["14", "16", "18", "20", "22"].includes(String(info.font_size))) fontSize.value = String(info.font_size);
}
@@ -1172,6 +1174,15 @@ export function startNoteEditor(adapter) {
toolbarCollapseToggle.querySelector(".toolbar-collapse-toggle__icon").textContent = toolbarCollapsed ? "⌄" : "⌃";
}
function applyNavbarCollapsed() {
document.body.classList.toggle("navbar-collapsed", navbarCollapsed);
if (!navbarCollapseToggle) return;
const label = navbarCollapsed ? "Show navigation bar" : "Hide navigation bar";
navbarCollapseToggle.setAttribute("aria-pressed", String(navbarCollapsed));
navbarCollapseToggle.setAttribute("aria-label", label);
navbarCollapseToggle.title = label;
}
function applyUi({ write = false, replace = false } = {}) {
const view = activeView();
renderedView = view;
@@ -1180,6 +1191,7 @@ export function startNoteEditor(adapter) {
document.body.classList.toggle("compact-editor", compactToggle.checked);
document.body.classList.toggle("compact-note-layout", compactLayoutQuery.matches);
applyToolbarCollapsed();
applyNavbarCollapsed();
document.querySelectorAll("[data-view]").forEach(button => {
const active = button.dataset.view === view;
button.classList.toggle("active", active);
@@ -1602,6 +1614,7 @@ export function startNoteEditor(adapter) {
});
modeToggle.addEventListener("click", () => { uiState = { ...uiState, mode: uiState.mode === "markdown" ? "text" : "markdown" }; applyUiPreservingScroll({ write: true }); });
toolbarCollapseToggle?.addEventListener("click", () => { toolbarCollapsed = !toolbarCollapsed; localStorage.setItem(notePreferenceKey("toolbar-collapsed"), toolbarCollapsed ? "on" : "off"); applyUiPreservingScroll(); scheduleEditorSettingsSave({ personal: true }); });
navbarCollapseToggle?.addEventListener("click", () => { navbarCollapsed = !navbarCollapsed; localStorage.setItem(notePreferenceKey("navbar-collapsed"), navbarCollapsed ? "on" : "off"); if (navbarCollapsed) setHeaderMenuOpen(false); applyUiPreservingScroll(); scheduleEditorSettingsSave({ personal: true }); });
lineToggle.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("line-numbers"), lineToggle.checked ? "on" : "off"); syncMobileEditorControls(); renderGutter(); scheduleEditorSettingsSave({ personal: true }); });
previewLineToggle.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("preview-line-numbers"), previewLineToggle.checked ? "on" : "off"); syncMobileEditorControls(); renderGutter(); alignPreviewLineNumbers(preview); scheduleEditorSettingsSave({ personal: true }); });
compactToggle.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("compact"), compactToggle.checked ? "on" : "off"); syncMobileEditorControls(); applyUiPreservingScroll(); scheduleEditorSettingsSave({ personal: true }); });
@@ -1800,6 +1813,7 @@ export function startNoteEditor(adapter) {
preview_line_numbers: previewLineToggle.checked,
line_links: lineLinksToggle.checked,
toolbar_collapsed: toolbarCollapsed,
navbar_collapsed: navbarCollapsed,
font_family: fontFamily.value,
font_size: Number(fontSize.value),
};
+42 -5
View File
@@ -12,6 +12,7 @@ installGlobalDiagnostics();
import { api } from "@rustpad/api";
import { copyText } from "@rustpad/clipboard";
import { lineFromHash, lineLink } from "@rustpad/line-links";
import { alignPreviewLineNumbers, renderMarkdown, setMarkdownFiles } from "@rustpad/markdown";
import { toast } from "@rustpad/toast";
import { getTheme } from "@rustpad/theme";
@@ -52,17 +53,45 @@ function lockPublicContent(allowTaskUpdates) {
content.querySelectorAll('.preview-editable').forEach(node => node.classList.remove('preview-editable'));
content.querySelectorAll('.task-checkbox').forEach(box => { box.disabled = !allowTaskUpdates; box.title = allowTaskUpdates ? 'Update this task' : 'Task updates are disabled by the owner'; });
}
function scrollToPublicAnchor(hash, behavior = "auto") {
function publicAnchorTarget(hash) {
const rawId = String(hash || "").replace(/^#/, "");
if (!rawId) return false;
if (!rawId) return null;
let id;
try { id = decodeURIComponent(rawId); } catch { id = rawId; }
const target = document.getElementById(id);
if (!target || !content.contains(target)) return false;
if (target && content.contains(target)) return target;
const line = lineFromHash(hash);
return line ? content.querySelector(`.preview-source-line[data-source-line="${line}"]`) : null;
}
function markPublicAnchor(target) {
content.querySelectorAll(".is-public-link-target").forEach(node => node.classList.remove("is-public-link-target"));
target?.classList.add("is-public-link-target");
}
function scrollToPublicAnchor(hash, behavior = "auto") {
const target = publicAnchorTarget(hash);
if (!target) return false;
for (let details = target.closest("details"); details; details = details.parentElement?.closest("details")) details.open = true;
markPublicAnchor(target);
target.scrollIntoView({ behavior, block: "start" });
return true;
}
function installPublicPermalinks() {
content.querySelectorAll(".preview-source-line[data-source-line]").forEach(target => {
if (target.matches("hr") || target.querySelector(":scope > .public-permalink")) return;
const line = Number(target.dataset.sourceLine);
if (!Number.isSafeInteger(line) || line < 1) return;
const heading = target.matches("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]");
const anchor = document.createElement("a");
anchor.className = "public-permalink";
anchor.href = heading ? `#${encodeURIComponent(target.id)}` : `#L${line}`;
anchor.dataset.line = String(line);
anchor.dataset.linkKind = heading ? "heading" : "line";
anchor.textContent = heading ? "¶" : "#";
anchor.setAttribute("aria-label", heading ? `Copy link to heading on line ${line}` : `Copy link to line ${line}`);
anchor.title = heading ? "Copy link to this heading" : `Copy link to line ${line}`;
target.append(anchor);
});
}
function setFullWidth(enabled, { persist = true } = {}) {
const active = Boolean(enabled);
document.body.classList.toggle("public-full-width", active);
@@ -77,14 +106,22 @@ function restoreFullWidth() {
try { enabled = localStorage.getItem(fullWidthStorageKey) === "1"; } catch { }
setFullWidth(enabled, { persist: false });
}
async function initialize() { try { const page = await api(`/api/public/${encodeURIComponent(token)}`, { headers: pageHeaders() }); if (passwordDialog.open) passwordDialog.close(); passwordError.textContent = ""; document.querySelector("#public-title").textContent = page.title; document.querySelector("#public-meta").textContent = `Updated: ${new Date(page.updated_at).toLocaleString("en-US")}${page.allow_task_updates ? " · tasks can be updated" : ""}`; document.title = `${page.title} · RustPad`; setMarkdownFiles(page.files || []); content.innerHTML = renderMarkdown(page.content); alignPreviewLineNumbers(content); lockPublicContent(page.allow_task_updates); await Promise.all([renderMermaid(), renderCodeHighlight(), renderMediaPlayers()]); requestAnimationFrame(() => scrollToPublicAnchor(location.hash)); } catch (error) { if (error.status === 401 || error.status === 403) { passwordError.textContent = error.status === 403 ? "Sign in with an authorized account or enter the resource password." : "Enter the correct password."; if (!passwordDialog.open) passwordDialog.showModal(); passwordInput.focus(); return; } content.replaceChildren(); const message = document.createElement("p"); message.className = "error"; message.textContent = String(error.message); content.append(message); } }
content.addEventListener("click", event => {
async function initialize() { try { const page = await api(`/api/public/${encodeURIComponent(token)}`, { headers: pageHeaders() }); if (passwordDialog.open) passwordDialog.close(); passwordError.textContent = ""; document.querySelector("#public-title").textContent = page.title; document.querySelector("#public-meta").textContent = `Updated: ${new Date(page.updated_at).toLocaleString("en-US")}${page.allow_task_updates ? " · tasks can be updated" : ""}`; document.title = `${page.title} · RustPad`; setMarkdownFiles(page.files || []); content.innerHTML = renderMarkdown(page.content); lockPublicContent(page.allow_task_updates); await Promise.all([renderMermaid(), renderCodeHighlight(), renderMediaPlayers()]); installPublicPermalinks(); alignPreviewLineNumbers(content); requestAnimationFrame(() => scrollToPublicAnchor(location.hash)); } catch (error) { if (error.status === 401 || error.status === 403) { passwordError.textContent = error.status === 403 ? "Sign in with an authorized account or enter the resource password." : "Enter the correct password."; if (!passwordDialog.open) passwordDialog.showModal(); passwordInput.focus(); return; } content.replaceChildren(); const message = document.createElement("p"); message.className = "error"; message.textContent = String(error.message); content.append(message); } }
content.addEventListener("click", async event => {
const link = event.target.closest('a[href^="#"]');
if (!link || !content.contains(link)) return;
const hash = link.getAttribute("href");
event.preventDefault();
if (!hash || hash === "#" || !scrollToPublicAnchor(hash, "smooth")) return;
history.replaceState(null, "", `${location.pathname}${location.search}${hash}`);
if (!link.classList.contains("public-permalink")) return;
try {
const href = link.dataset.linkKind === "heading" ? new URL(hash, location.href).href : lineLink(location.href, Number(link.dataset.line));
await copyText(href);
link.classList.add("is-copied");
setTimeout(() => link.classList.remove("is-copied"), 900);
toast(link.dataset.linkKind === "heading" ? "Heading link copied" : `Link to line ${link.dataset.line} copied`);
} catch (error) { toast(error.message); }
});
window.addEventListener("hashchange", () => scrollToPublicAnchor(location.hash, "smooth"));
content.addEventListener("change", async event => { const box = event.target.closest(".task-checkbox"); if (!box || box.disabled) return; const previous = !box.checked; box.disabled = true; try { const page = await api(`/api/public/${encodeURIComponent(token)}/tasks`, { method: "POST", headers: pageHeaders(), body: JSON.stringify({ source_line: Number(box.dataset.sourceLine), checked: box.checked }) }); document.querySelector("#public-meta").textContent = `Updated: ${new Date(page.updated_at).toLocaleString("en-US")} · tasks can be updated`; toast("Task saved"); } catch (error) { box.checked = previous; toast(error.message); } finally { box.disabled = false; } });