line links in s

This commit is contained in:
Mateusz Gruszczyński
2026-08-07 10:30:11 +02:00
parent 249e5a4fcd
commit d4e64f4e50
5 changed files with 40 additions and 14 deletions
Generated
+1 -1
View File
@@ -2581,7 +2581,7 @@ dependencies = [
[[package]] [[package]]
name = "rustpad" name = "rustpad"
version = "0.2.51" version = "0.2.52"
dependencies = [ dependencies = [
"argon2", "argon2",
"aws-config", "aws-config",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rustpad" name = "rustpad"
version = "0.2.51" version = "0.2.52"
edition = "2024" edition = "2024"
rust-version = "1.94" rust-version = "1.94"
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL" description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
+35 -12
View File
@@ -7350,6 +7350,19 @@ dialog::backdrop {
scroll-margin-top: 88px; scroll-margin-top: 88px;
} }
/*
* Line linking is opt-in on published notes. When regular line numbers are
* hidden, enabling links restores the same gutter so link targets stay inside
* the document instead of protruding past its left edge.
*/
.public-page .public-content {
transition: padding-left .18s ease;
}
.public-page.hide-preview-line-numbers.line-links-enabled .public-content {
padding-left: clamp(24px, 4vw, 52px);
}
/* /*
* The permalink occupies the same gutter slot as the regular source number. * The permalink occupies the same gutter slot as the regular source number.
* It stays invisible until the row is approached, then behaves like the * It stays invisible until the row is approached, then behaves like the
@@ -7360,7 +7373,7 @@ dialog::backdrop {
top: 0; top: 0;
left: var(--preview-line-left, -50px); left: var(--preview-line-left, -50px);
z-index: 3; z-index: 3;
display: block; display: none;
width: 32px; width: 32px;
height: 1.32em; height: 1.32em;
padding: 0 2px 0 0; padding: 0 2px 0 0;
@@ -7378,30 +7391,34 @@ dialog::backdrop {
transition: opacity .14s ease, transform .14s ease, background-color .14s ease, color .14s ease; transition: opacity .14s ease, transform .14s ease, background-color .14s ease, color .14s ease;
} }
.public-content .preview-source-line::before { .public-page.line-links-enabled .public-content .public-permalink {
display: block;
}
.public-page.line-links-enabled .public-content .preview-source-line::before {
transition: opacity .1s ease; transition: opacity .1s ease;
} }
.public-content .preview-source-line:hover::before, .public-page.line-links-enabled .public-content .preview-source-line:hover::before,
.public-content .preview-source-line:focus-within::before { .public-page.line-links-enabled .public-content .preview-source-line:focus-within::before {
opacity: 0; opacity: 0;
} }
.public-content .preview-source-line:hover > .public-permalink, .public-page.line-links-enabled .public-content .preview-source-line:hover > .public-permalink,
.public-content .preview-source-line:focus-within > .public-permalink, .public-page.line-links-enabled .public-content .preview-source-line:focus-within > .public-permalink,
.public-content .public-permalink:focus-visible { .public-page.line-links-enabled .public-content .public-permalink:focus-visible {
opacity: 1; opacity: 1;
transform: translateX(0); transform: translateX(0);
} }
.public-content .public-permalink:hover, .public-page.line-links-enabled .public-content .public-permalink:hover,
.public-content .public-permalink:focus-visible { .public-page.line-links-enabled .public-content .public-permalink:focus-visible {
background: color-mix(in srgb, var(--accent) 14%, transparent); background: color-mix(in srgb, var(--accent) 14%, transparent);
color: var(--text); color: var(--text);
outline: none; outline: none;
} }
.public-content .public-permalink.is-copied { .public-page.line-links-enabled .public-content .public-permalink.is-copied {
background: color-mix(in srgb, var(--success) 18%, transparent); background: color-mix(in srgb, var(--success) 18%, transparent);
color: var(--text); color: var(--text);
opacity: 1; opacity: 1;
@@ -7415,16 +7432,22 @@ dialog::backdrop {
} }
@media (hover: none), (pointer: coarse) { @media (hover: none), (pointer: coarse) {
.public-content .public-permalink { .public-page.line-links-enabled .public-content .public-permalink {
opacity: .62; opacity: .62;
transform: none; transform: none;
} }
.public-content .preview-source-line::before { .public-page.line-links-enabled .public-content .preview-source-line::before {
opacity: 0; opacity: 0;
} }
} }
@media (prefers-reduced-motion: reduce) {
.public-page .public-content {
transition: none;
}
}
/* Per-user, per-note navigation-bar focus mode ---------------------------- */ /* Per-user, per-note navigation-bar focus mode ---------------------------- */
.pad-page .navbar-collapse-toggle { .pad-page .navbar-collapse-toggle {
position: fixed; position: fixed;
+2
View File
@@ -21,6 +21,7 @@ import { loadHighlight, loadMediaPlayer, loadMermaid } from "@rustpad/vendor-lib
const token = location.pathname.split("/").filter(Boolean)[1]; const token = location.pathname.split("/").filter(Boolean)[1];
const content = document.querySelector("#public-content"); const content = document.querySelector("#public-content");
const lineNumbersToggle = document.querySelector("#public-line-numbers-toggle"); const lineNumbersToggle = document.querySelector("#public-line-numbers-toggle");
const lineLinksToggle = document.querySelector("#public-line-links-toggle");
const fullWidthToggle = document.querySelector("#public-full-width-toggle"); const fullWidthToggle = document.querySelector("#public-full-width-toggle");
const fullWidthStorageKey = "rustpad:public-full-width"; const fullWidthStorageKey = "rustpad:public-full-width";
const passwordDialog = document.querySelector("#public-password-dialog"), passwordForm = document.querySelector("#public-password-form"), passwordInput = document.querySelector("#public-password"), passwordError = document.querySelector("#public-password-error"); const passwordDialog = document.querySelector("#public-password-dialog"), passwordForm = document.querySelector("#public-password-form"), passwordInput = document.querySelector("#public-password"), passwordError = document.querySelector("#public-password-error");
@@ -221,6 +222,7 @@ content.addEventListener("click", async event => {
window.addEventListener("hashchange", () => { publicAnchorSettleCleanup?.(); scrollToPublicAnchor(location.hash, "smooth"); }); window.addEventListener("hashchange", () => { publicAnchorSettleCleanup?.(); 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; } }); 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; } });
lineNumbersToggle.addEventListener("change", () => { document.body.classList.toggle("hide-preview-line-numbers", !lineNumbersToggle.checked); }); lineNumbersToggle.addEventListener("change", () => { document.body.classList.toggle("hide-preview-line-numbers", !lineNumbersToggle.checked); });
lineLinksToggle.addEventListener("change", () => { document.body.classList.toggle("line-links-enabled", lineLinksToggle.checked); });
fullWidthToggle.addEventListener("change", () => setFullWidth(fullWidthToggle.checked)); fullWidthToggle.addEventListener("change", () => setFullWidth(fullWidthToggle.checked));
window.addEventListener("resize", () => requestAnimationFrame(() => alignPreviewLineNumbers(content))); window.addEventListener("resize", () => requestAnimationFrame(() => alignPreviewLineNumbers(content)));
passwordForm.addEventListener("submit", async event => { event.preventDefault(); pagePassword = passwordInput.value; await initialize(); }); passwordForm.addEventListener("submit", async event => { event.preventDefault(); pagePassword = passwordInput.value; await initialize(); });
+1
View File
@@ -17,6 +17,7 @@
<a class="brand home-brand" href="/">RustPad</a> <a class="brand home-brand" href="/">RustPad</a>
<div class="public-header__actions"> <div class="public-header__actions">
<label class="line-toggle"><input id="public-line-numbers-toggle" type="checkbox"> Line numbers</label> <label class="line-toggle"><input id="public-line-numbers-toggle" type="checkbox"> Line numbers</label>
<label class="line-toggle"><input id="public-line-links-toggle" type="checkbox"> Line links</label>
<label class="line-toggle"><input id="public-full-width-toggle" type="checkbox"> Full width</label> <label class="line-toggle"><input id="public-full-width-toggle" type="checkbox"> Full width</label>
<button id="copy-public-link" class="secondary-button">Copy link</button> <button id="copy-public-link" class="secondary-button">Copy link</button>
</div> </div>