page style
This commit is contained in:
+78
-9
@@ -4861,7 +4861,7 @@ dialog::backdrop {
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pad-page .header-actions .page-settings-menu {
|
||||
@@ -5453,12 +5453,16 @@ dialog::backdrop {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Compact Page settings dropdown. */
|
||||
/* Published page action and settings in one dropdown. */
|
||||
.page-settings {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.page-settings>summary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 9px;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -5467,6 +5471,35 @@ dialog::backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-settings__label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.page-settings__status {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--muted);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--muted) 18%, transparent);
|
||||
}
|
||||
|
||||
.page-settings.is-enabled .page-settings__status {
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 22%, transparent);
|
||||
}
|
||||
|
||||
.page-settings__chevron {
|
||||
font-size: .72em;
|
||||
line-height: 1;
|
||||
transition: transform .16s ease;
|
||||
}
|
||||
|
||||
.page-settings[open] .page-settings__chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.page-settings-menu {
|
||||
position: absolute;
|
||||
z-index: 30;
|
||||
@@ -5474,7 +5507,7 @@ dialog::backdrop {
|
||||
right: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 220px;
|
||||
min-width: 260px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 9px;
|
||||
@@ -5482,6 +5515,46 @@ dialog::backdrop {
|
||||
box-shadow: 0 12px 30px var(--shadow-28);
|
||||
}
|
||||
|
||||
.page-settings-action {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
width: 100%;
|
||||
min-height: 46px;
|
||||
padding: 7px 9px;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 42%, var(--border));
|
||||
border-radius: 7px;
|
||||
background: var(--note-action-accent-bg);
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-settings-action span {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.page-settings-action small {
|
||||
color: var(--muted);
|
||||
font-size: .72rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.page-settings-action:hover:not(:disabled) {
|
||||
border-color: color-mix(in srgb, var(--accent) 68%, var(--border));
|
||||
background: var(--note-action-hover);
|
||||
}
|
||||
|
||||
.page-settings-action:disabled {
|
||||
opacity: .45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.page-settings-divider {
|
||||
height: 1px;
|
||||
margin: 3px 1px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.page-settings-menu .public-task-toggle,
|
||||
.mobile-editor-options__panel .mobile-option-check {
|
||||
min-height: 30px;
|
||||
@@ -5495,11 +5568,6 @@ dialog::backdrop {
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
#publish-page:disabled {
|
||||
opacity: .45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Keep the Simple/Full switch inside the editor label frame. */
|
||||
.authorship-mode-control {
|
||||
padding: 1px;
|
||||
@@ -6007,7 +6075,7 @@ dialog::backdrop {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.pad-page #publish-page:not(:disabled) {
|
||||
.pad-page .page-settings-action:not(:disabled) {
|
||||
border-color: color-mix(in srgb, var(--accent) 55%, var(--note-action-border));
|
||||
background: var(--note-action-accent-bg);
|
||||
color: var(--accent-text-strong);
|
||||
@@ -6015,6 +6083,7 @@ dialog::backdrop {
|
||||
|
||||
.pad-page .header-actions>.secondary-button:focus-visible,
|
||||
.pad-page .page-settings>summary.secondary-button:focus-visible,
|
||||
.pad-page .page-settings-action:focus-visible,
|
||||
.pad-page .editor-toolbar :is(button, summary, select):focus-visible {
|
||||
outline: 2px solid var(--focus);
|
||||
outline-offset: 2px;
|
||||
|
||||
+15
-7
@@ -7,7 +7,7 @@
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<title>__DOCUMENT_TITLE__ · __PARENT_TITLE__</title>
|
||||
__APP_THEME_BOOTSTRAP__
|
||||
__APP_STYLESHEET__
|
||||
__APP_STYLESHEET__
|
||||
__APP_IMPORT_MAP__
|
||||
__APP_ENTRYPOINT__
|
||||
</head>
|
||||
@@ -34,17 +34,23 @@
|
||||
<span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
|
||||
</button>
|
||||
<div id="header-actions" class="header-actions"><button id="copy-link" class="secondary-button">Copy
|
||||
link</button><button id="publish-page" class="secondary-button">Page</button>
|
||||
link</button>
|
||||
<details class="page-settings">
|
||||
<summary class="secondary-button">Page settings</summary>
|
||||
<div class="page-settings-menu"><label class="public-task-toggle"
|
||||
<summary class="secondary-button" aria-label="Page options"><span class="page-settings__label"><span
|
||||
class="page-settings__status" aria-hidden="true"></span>Page</span><span
|
||||
class="page-settings__chevron" aria-hidden="true">▾</span></summary>
|
||||
<div class="page-settings-menu"><button id="publish-page" class="page-settings-action"
|
||||
type="button"><span>Open page</span><small>Copy its link and open it in a new
|
||||
tab</small></button>
|
||||
<div class="page-settings-divider" role="separator"></div><label class="public-task-toggle"
|
||||
title="Enable or disable the published page"><input id="public-page-enabled"
|
||||
type="checkbox"> Enable Page</label><label class="public-task-toggle"
|
||||
title="Allow visitors to update task checkboxes on the published page"><input
|
||||
id="public-task-updates" type="checkbox"> Editable tasks</label><label
|
||||
class="public-task-toggle"
|
||||
title="Allow the published page to open without the resource password or private access"><input
|
||||
id="unprotect-public-page" type="checkbox"> Unprotect Page</label></div>
|
||||
id="unprotect-public-page" type="checkbox"> Unprotect Page</label>
|
||||
</div>
|
||||
</details><button id="files-button" class="secondary-button">Files</button><button id="delete-note"
|
||||
class="secondary-button danger-button" hidden>Delete</button><button id="history-button"
|
||||
class="secondary-button">History</button>
|
||||
@@ -118,7 +124,8 @@
|
||||
class="active">Split</button><button data-view="preview">Preview</button></div>
|
||||
</div>
|
||||
<div id="connection-notice" class="connection-notice" role="status" aria-live="polite" hidden>
|
||||
<span class="connection-notice__signal" aria-hidden="true"><span></span><span></span><span></span></span>
|
||||
<span class="connection-notice__signal"
|
||||
aria-hidden="true"><span></span><span></span><span></span></span>
|
||||
<span class="connection-notice__content"><strong id="connection-notice-title">Connection
|
||||
interrupted</strong><span id="connection-notice-message">Trying to reconnect
|
||||
automatically.</span></span>
|
||||
@@ -199,7 +206,8 @@
|
||||
</div><button id="close-shortcuts" class="icon-button" type="button">×</button>
|
||||
</div>
|
||||
<div class="shortcut-grid">
|
||||
<kbd>Ctrl/Cmd+Z</kbd><span>Undo</span><kbd>Ctrl/Cmd+B</kbd><span>Bold</span><kbd>Ctrl/Cmd+I</kbd><span>Italic</span><kbd>Ctrl/Cmd+Shift+X</kbd><span>Strikethrough</span><kbd>Ctrl/Cmd+K</kbd><span>Link</span><kbd>Ctrl/Cmd+Shift+7</kbd><span>Numbered
|
||||
<kbd>Ctrl/Cmd+Z</kbd><span>Undo last
|
||||
change</span><kbd>Ctrl/Cmd+Shift+Z</kbd><span>Redo</span><kbd>Ctrl/Cmd+B</kbd><span>Bold</span><kbd>Ctrl/Cmd+I</kbd><span>Italic</span><kbd>Ctrl/Cmd+Shift+X</kbd><span>Strikethrough</span><kbd>Ctrl/Cmd+K</kbd><span>Link</span><kbd>Ctrl/Cmd+Shift+7</kbd><span>Numbered
|
||||
list</span><kbd>Ctrl/Cmd+Shift+8</kbd><span>Bullet list</span><kbd>Ctrl/Cmd+Shift+9</kbd><span>Task
|
||||
list</span><kbd>Alt+1…4</kbd><span>Headings H1–H4</span>__EXTRA_SHORTCUTS__
|
||||
</div>
|
||||
|
||||
+134
-6
@@ -32,8 +32,80 @@ export function startNoteEditor(adapter) {
|
||||
const mobileFontFamily = document.querySelector("#mobile-font-family"), mobileFontSize = document.querySelector("#mobile-font-size"), mobileLineToggle = document.querySelector("#mobile-line-numbers-toggle"), mobilePreviewLineToggle = document.querySelector("#mobile-preview-line-numbers-toggle"), mobileCompactToggle = document.querySelector("#mobile-compact-toggle"), mobileLineLinksToggle = document.querySelector("#mobile-line-links-toggle");
|
||||
const shareToken = new URLSearchParams(location.search).get("share");
|
||||
const notePreferenceKey = name => `rustpad:${name}:${location.pathname}`;
|
||||
let accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key), password = "", nickname = getNickname(), info, socket, saveTimer, applyingRemote = false, resourceUnlocked = false, uiState = readEditorState(), authorship = parseAuthorship("", "[]"), previousContent = "", globalColor = "", noteColor = "", presenceUsers = [], authorshipMode = "simple", authorshipColorsEnabled = true, lastRevealedLineHash = "";
|
||||
let accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key), password = "", nickname = getNickname(), info, socket, saveTimer, applyingRemote = false, applyingHistory = false, resourceUnlocked = false, uiState = readEditorState(), authorship = parseAuthorship("", "[]"), previousContent = "", globalColor = "", noteColor = "", presenceUsers = [], authorshipMode = "simple", authorshipColorsEnabled = true, lastRevealedLineHash = "";
|
||||
let editorSettingsSaveTimer, editorSettingsSaveInFlight = false, pendingPersonalSettingsSave = false, pendingAuthorshipSettingsSave = false, connectionNoticeTimer = 0, connectionWasInterrupted = false;
|
||||
const editHistory = {
|
||||
entries: [],
|
||||
index: -1,
|
||||
lastKind: "",
|
||||
lastRecordedAt: 0,
|
||||
snapshot() {
|
||||
return {
|
||||
content: editor.value,
|
||||
ownerMap: serializeAuthorship(authorship, editor.value.length),
|
||||
selectionStart: editor.selectionStart,
|
||||
selectionEnd: editor.selectionEnd,
|
||||
selectionDirection: editor.selectionDirection,
|
||||
scrollTop: editor.scrollTop,
|
||||
scrollLeft: editor.scrollLeft,
|
||||
};
|
||||
},
|
||||
reset() {
|
||||
this.entries = [this.snapshot()];
|
||||
this.index = 0;
|
||||
this.lastKind = "";
|
||||
this.lastRecordedAt = 0;
|
||||
},
|
||||
syncCurrent() {
|
||||
if (this.index < 0) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
this.entries[this.index] = this.snapshot();
|
||||
},
|
||||
record(inputType = "") {
|
||||
const snapshot = this.snapshot();
|
||||
if (this.index < 0) {
|
||||
this.entries = [snapshot];
|
||||
this.index = 0;
|
||||
return;
|
||||
}
|
||||
if (this.entries[this.index]?.content === snapshot.content) {
|
||||
this.entries[this.index] = snapshot;
|
||||
return;
|
||||
}
|
||||
if (this.index < this.entries.length - 1) this.entries.splice(this.index + 1);
|
||||
const kind = inputType === "insertText" || inputType === "insertCompositionText"
|
||||
? "typing"
|
||||
: inputType === "deleteContentBackward" || inputType === "deleteContentForward"
|
||||
? "deleting"
|
||||
: "action";
|
||||
const now = Date.now();
|
||||
const merge = kind !== "action" && kind === this.lastKind && now - this.lastRecordedAt < 900 && this.index > 0;
|
||||
if (merge) this.entries[this.index] = snapshot;
|
||||
else {
|
||||
this.entries.push(snapshot);
|
||||
this.index += 1;
|
||||
if (this.entries.length > 100) {
|
||||
this.entries.shift();
|
||||
this.index -= 1;
|
||||
}
|
||||
}
|
||||
this.lastKind = kind;
|
||||
this.lastRecordedAt = now;
|
||||
},
|
||||
move(offset) {
|
||||
const nextIndex = this.index + offset;
|
||||
if (nextIndex < 0 || nextIndex >= this.entries.length) return false;
|
||||
this.index = nextIndex;
|
||||
this.lastKind = "";
|
||||
this.lastRecordedAt = 0;
|
||||
restoreHistorySnapshot(this.entries[this.index]);
|
||||
return true;
|
||||
},
|
||||
undo() { return this.move(-1); },
|
||||
redo() { return this.move(1); },
|
||||
};
|
||||
const compactLayoutQuery = window.matchMedia("(max-width: 1499px)");
|
||||
const singlePaneQuery = window.matchMedia("(max-width: 760px)");
|
||||
let compactView = uiState.view === "preview" ? "preview" : "edit";
|
||||
@@ -577,6 +649,29 @@ export function startNoteEditor(adapter) {
|
||||
preview.scrollTop = ratio * previewRange;
|
||||
}
|
||||
function render() { if (uiState.mode === "markdown") { preview.classList.remove("preview--raw"); preview.innerHTML = renderMarkdown(editor.value); scheduleAliasFileRefresh(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) => `<div class="preview-source-line preview-editable" data-source-line="${index + 1}">${escapeHtml(line) || "<br>"}</div>`).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(); requestAnimationFrame(syncPreviewScroll); }
|
||||
function scheduleDocumentSave() {
|
||||
clearTimeout(saveTimer);
|
||||
document.querySelector("#save-state").textContent = "Saving…";
|
||||
saveTimer = setTimeout(() => socket?.update(editor.value, serializeAuthorship(authorship, editor.value.length)), 250);
|
||||
}
|
||||
function restoreHistorySnapshot(snapshot) {
|
||||
if (!snapshot) return;
|
||||
const maxOffset = snapshot.content.length;
|
||||
const selectionStart = Math.min(snapshot.selectionStart ?? maxOffset, maxOffset);
|
||||
const selectionEnd = Math.min(snapshot.selectionEnd ?? selectionStart, maxOffset);
|
||||
applyingHistory = true;
|
||||
editor.value = snapshot.content;
|
||||
authorship = parseAuthorship(snapshot.content, snapshot.ownerMap);
|
||||
previousContent = snapshot.content;
|
||||
editor.setSelectionRange(selectionStart, selectionEnd, snapshot.selectionDirection || "none");
|
||||
render();
|
||||
editor.scrollTop = snapshot.scrollTop || 0;
|
||||
editor.scrollLeft = snapshot.scrollLeft || 0;
|
||||
syncEditorLayers();
|
||||
applyingHistory = false;
|
||||
editor.focus({ preventScroll: true });
|
||||
scheduleDocumentSave();
|
||||
}
|
||||
function activeView() {
|
||||
return singlePaneQuery.matches ? compactView : uiState.view;
|
||||
}
|
||||
@@ -621,6 +716,7 @@ export function startNoteEditor(adapter) {
|
||||
if (content === editor.value) {
|
||||
if (ownerMap != null) authorship = adoptCurrentOwnerAliases(parseAuthorship(content, ownerMap), content.length);
|
||||
previousContent = content;
|
||||
editHistory.syncCurrent();
|
||||
renderGutter();
|
||||
requestAnimationFrame(revealLinkedLine);
|
||||
return;
|
||||
@@ -636,6 +732,7 @@ export function startNoteEditor(adapter) {
|
||||
editor.scrollTop = scrollTop;
|
||||
editor.scrollLeft = scrollLeft;
|
||||
applyingRemote = false;
|
||||
editHistory.reset();
|
||||
render();
|
||||
editor.scrollTop = scrollTop;
|
||||
editor.scrollLeft = scrollLeft;
|
||||
@@ -1062,6 +1159,29 @@ export function startNoteEditor(adapter) {
|
||||
button.closest("details")?.removeAttribute("open");
|
||||
});
|
||||
});
|
||||
function handleHistoryShortcut(event) {
|
||||
const primary = event.ctrlKey || event.metaKey;
|
||||
if (!primary || event.altKey) return;
|
||||
const key = event.key.toLowerCase();
|
||||
const undo = key === "z" && !event.shiftKey;
|
||||
const redo = (key === "z" && event.shiftKey) || (key === "y" && !event.shiftKey);
|
||||
if (!undo && !redo) return;
|
||||
const target = event.target instanceof Element ? event.target : null;
|
||||
if (target?.closest('.preview-editable[contenteditable="true"]')) return;
|
||||
if (target && target !== editor && target.matches("input, textarea, select, [contenteditable='true']")) return;
|
||||
if (document.querySelector("dialog[open]") && target !== editor) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (redo) editHistory.redo();
|
||||
else editHistory.undo();
|
||||
}
|
||||
document.addEventListener("keydown", handleHistoryShortcut, true);
|
||||
editor.addEventListener("beforeinput", event => {
|
||||
if (event.inputType !== "historyUndo" && event.inputType !== "historyRedo") return;
|
||||
event.preventDefault();
|
||||
if (event.inputType === "historyRedo") editHistory.redo();
|
||||
else editHistory.undo();
|
||||
});
|
||||
bindFormatShortcuts(editor);
|
||||
bindEmojiPicker({ editor, details: document.querySelector("#emoji-picker"), search: document.querySelector("#emoji-search"), categories: document.querySelector("#emoji-categories"), grid: document.querySelector("#emoji-grid"), empty: document.querySelector("#emoji-empty") });
|
||||
document.querySelector("#shortcuts-button").addEventListener("click", () => document.querySelector("#shortcuts-dialog").showModal());
|
||||
@@ -1151,12 +1271,21 @@ export function startNoteEditor(adapter) {
|
||||
deletePreviewSelection();
|
||||
});
|
||||
const publishPageButton = document.querySelector("#publish-page");
|
||||
const pageSettings = document.querySelector(".page-settings");
|
||||
function updatePageControls() {
|
||||
const enabled = publicPageEnabled.checked;
|
||||
publishPageButton.disabled = !enabled;
|
||||
publicTaskUpdates.disabled = !enabled;
|
||||
unprotectPublicPage.disabled = !enabled;
|
||||
pageSettings?.classList.toggle("is-enabled", enabled);
|
||||
pageSettings?.querySelector("summary")?.setAttribute("title", enabled ? "Published page enabled" : "Published page disabled");
|
||||
}
|
||||
document.addEventListener("pointerdown", event => {
|
||||
if (pageSettings?.open && !event.target.closest(".page-settings")) pageSettings.open = false;
|
||||
}, { passive: true });
|
||||
document.addEventListener("keydown", event => {
|
||||
if (event.key === "Escape" && pageSettings?.open) pageSettings.open = false;
|
||||
});
|
||||
const savePublicOptions = async () => adapter.publish(accessToken, publicTaskUpdates.checked, unprotectPublicPage.checked, publicPageEnabled.checked);
|
||||
publicPageEnabled.addEventListener("change", async () => {
|
||||
const previous = !publicPageEnabled.checked;
|
||||
@@ -1211,17 +1340,16 @@ export function startNoteEditor(adapter) {
|
||||
syncEditorLayers();
|
||||
syncPreviewScroll();
|
||||
});
|
||||
editor.addEventListener("input", () => {
|
||||
editor.addEventListener("input", event => {
|
||||
const nextContent = editor.value;
|
||||
authorship = adoptCurrentOwnerAliases(authorship, previousContent.length);
|
||||
authorship = replaceAuthorshipOwner(authorship, owner => ownerName(owner) === nickname, currentOwner(), previousContent.length);
|
||||
authorship = applyAuthorshipEdit(authorship, previousContent, nextContent, currentOwner());
|
||||
previousContent = nextContent;
|
||||
render();
|
||||
if (applyingRemote) return;
|
||||
clearTimeout(saveTimer);
|
||||
document.querySelector("#save-state").textContent = "Saving…";
|
||||
saveTimer = setTimeout(() => socket?.update(editor.value, serializeAuthorship(authorship, editor.value.length)), 250);
|
||||
if (applyingRemote || applyingHistory) return;
|
||||
editHistory.record(event.inputType || "");
|
||||
scheduleDocumentSave();
|
||||
});
|
||||
passwordDialog.addEventListener("cancel", event => {
|
||||
if (info?.protected && !resourceUnlocked) {
|
||||
|
||||
Reference in New Issue
Block a user