This commit is contained in:
Mateusz Gruszczyński
2026-07-24 23:16:03 +02:00
parent 041f586261
commit 3576877ace
17 changed files with 629 additions and 148 deletions
+65 -32
View File
@@ -2304,14 +2304,10 @@ dialog::backdrop {
color: #b9c2cf;
}
/* Task-list layout. */
/* Task-list layout. Keep the checkbox in the same marker gutter as a bullet. */
.markdown-body .task-list-item {
position: relative;
display: grid;
grid-template-columns: 1em minmax(0, 1fr);
grid-template-rows: auto auto;
column-gap: .45em;
align-items: start;
display: block;
min-height: 1.32em;
margin: 0;
padding: 0;
@@ -2320,12 +2316,11 @@ dialog::backdrop {
}
.markdown-body .task-checkbox {
grid-column: 1;
grid-row: 1;
align-self: start;
position: absolute;
top: 0;
left: -1.45em;
appearance: none;
box-sizing: border-box;
flex: 0 0 1em;
width: 1em;
min-width: 1em;
max-width: 1em;
@@ -2358,9 +2353,7 @@ dialog::backdrop {
box-shadow: 0 0 0 2px rgba(124, 104, 238, .3);
}
.markdown-body .task-list-item > .list-item-content {
grid-column: 2;
grid-row: 1;
.markdown-body .task-list-item>.list-item-content {
display: block;
min-width: 0;
margin: 0;
@@ -2368,10 +2361,8 @@ dialog::backdrop {
line-height: inherit;
}
.markdown-body .task-list-item > ul,
.markdown-body .task-list-item > ol {
grid-column: 2;
grid-row: 2;
.markdown-body .task-list-item>ul,
.markdown-body .task-list-item>ol {
min-width: 0;
}
@@ -3764,6 +3755,7 @@ dialog::backdrop {
border-radius: 0 0 14px 14px;
}
}
/* Markdown alerts. */
.markdown-body .markdown-alert {
margin: 1em 0;
@@ -3772,15 +3764,40 @@ dialog::backdrop {
border-left-width: 4px;
border-radius: 8px;
}
.markdown-body .markdown-alert > :first-child { margin-top: 0; }
.markdown-body .markdown-alert > :last-child { margin-bottom: 0; }
.markdown-body .markdown-alert--success { border-color: #2f855a; background: rgba(47,133,90,.14); }
.markdown-body .markdown-alert--info { border-color: #3182ce; background: rgba(49,130,206,.14); }
.markdown-body .markdown-alert--warning { border-color: #d69e2e; background: rgba(214,158,46,.14); }
.markdown-body .markdown-alert--danger { border-color: #c53030; background: rgba(197,48,48,.14); }
.markdown-body .markdown-alert> :first-child {
margin-top: 0;
}
.markdown-body .markdown-alert> :last-child {
margin-bottom: 0;
}
.markdown-body .markdown-alert--success {
border-color: #2f855a;
background: rgba(47, 133, 90, .14);
}
.markdown-body .markdown-alert--info {
border-color: #3182ce;
background: rgba(49, 130, 206, .14);
}
.markdown-body .markdown-alert--warning {
border-color: #d69e2e;
background: rgba(214, 158, 46, .14);
}
.markdown-body .markdown-alert--danger {
border-color: #c53030;
background: rgba(197, 48, 48, .14);
}
/* Fenced code line numbers for every language: ```lang=, ```lang=101, ```= or ```=101. */
.markdown-body pre.code-with-lines code { counter-reset: none; }
.markdown-body pre.code-with-lines code {
counter-reset: none;
}
.markdown-body pre.code-with-lines .code-line {
display: block;
min-height: 1.35em;
@@ -3788,6 +3805,7 @@ dialog::backdrop {
position: relative;
white-space: pre;
}
.markdown-body pre.code-with-lines .code-line::before {
content: attr(data-line);
position: absolute;
@@ -3808,13 +3826,29 @@ dialog::backdrop {
border-radius: 8px;
background: var(--surface-2);
}
.markdown-body .markdown-toc ol { margin: 0; padding-left: 1.4em; }
.markdown-body .markdown-toc li { margin: .25em 0; }
.markdown-body .markdown-toc .toc-level-2 { margin-left: 1em; }
.markdown-body .markdown-toc .toc-level-3 { margin-left: 2em; }
.markdown-body .markdown-toc ol {
margin: 0;
padding-left: 1.4em;
}
.markdown-body .markdown-toc li {
margin: .25em 0;
}
.markdown-body .markdown-toc .toc-level-2 {
margin-left: 1em;
}
.markdown-body .markdown-toc .toc-level-3 {
margin-left: 2em;
}
.markdown-body .markdown-toc .toc-level-4,
.markdown-body .markdown-toc .toc-level-5,
.markdown-body .markdown-toc .toc-level-6 { margin-left: 3em; }
.markdown-body .markdown-toc .toc-level-6 {
margin-left: 3em;
}
/* Nested Markdown lists keep markers and source-line numbers in separate gutters. */
.markdown-body ul,
@@ -3832,7 +3866,6 @@ dialog::backdrop {
word-break: break-word;
}
.markdown-body .contains-task-items > .task-list-item {
.markdown-body .contains-task-items>.task-list-item {
list-style: none;
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ function inline(value) {
html = html.replace(/`([^`]+)`/g, (_, code) => stash(`<code>${code}</code>`));
html = html.replace(/!\[([^\]]*)\]\(([^\s)]+)(?:\s+["']([^"']*)["'])?\)/g, (_, alt, url, title) => {
const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
return stash(`<img src="${safeUrl(url)}" alt="${alt}" loading="lazy" decoding="async"${titleAttr}>`);
return stash(`<img src="${safeUrl(url)}" alt="${alt}" loading="lazy" decoding="async" draggable="false" contenteditable="false"${titleAttr}>`);
});
html = html.replace(/\[([^\]]+)\]\(([^\s)]+)(?:\s+["']([^"']*)["'])?\)/g, (_, label, url, title) => {
const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
+20 -5
View File
@@ -79,6 +79,12 @@ function markdownFromPreview(node){
if(tag==="sub")return `~${body}~`;
if(tag==="sup"&&!current.classList.contains("footnote-ref"))return `^${body}^`;
if(tag==="a")return `[${body}](${current.getAttribute("href")||"#"})`;
if(tag==="img"){
const src=current.getAttribute("src")||"";
const alt=current.getAttribute("alt")||"";
const title=current.getAttribute("title");
return `![${alt}](${src}${title?` "${title.replace(/"/g,"&quot;")}"`:""})`;
}
if(tag==="br")return " ";
return body;
};
@@ -137,19 +143,20 @@ function applyUi({write=false,replace=false}={}){editorWorkspace.className=`work
function applyRemote(content,ownerMap){if(content===editor.value&&ownerMap==null)return;const start=editor.selectionStart,end=editor.selectionEnd;applyingRemote=true;editor.value=content;try{owners=JSON.parse(ownerMap||"[]");}catch{owners=[];}editor.setSelectionRange(Math.min(start,content.length),Math.min(end,content.length));applyingRemote=false;render();}
function connect(){socket?.stop();socket=new NoteSocket({workspaceSlug,noteSlug,password,accessToken,nickname,color:currentUserColor()||null,sessionToken:getAuthToken(),onStatus:s=>setStatus(s==="online"?"online":s==="offline"?"offline":null,s==="online"?"Connected":s==="offline"?"Reconnecting…":"Connecting…"),onAuthenticated:m=>{if(passwordDialog.open)passwordDialog.close();applyRemote(m.content,m.owner_map);editor.focus();},onDocument:m=>{applyRemote(m.content,m.owner_map);document.querySelector("#save-state").textContent=`${m.author?`${m.author} · `:""}${new Date(m.updated_at).toLocaleTimeString("en-US",{hour:"2-digit",minute:"2-digit"})}`;},onPresence:updatePresence,onLatency:updateLatency,onChat:appendChatMessage,onError:m=>{document.querySelector("#password-error").textContent=m;if(/nickname|session|account/i.test(m)){if(!identityDialog.open)identityDialog.showModal();}else if(info?.protected&&!passwordDialog.open)passwordDialog.showModal();}});socket.connect();}
function formatBytes(bytes){const value=Number(bytes)||0;if(value<1024)return `${value} B`;if(value<1024*1024)return `${(value/1024).toFixed(1)} KB`;return `${(value/1024/1024).toFixed(1)} MB`;}
function formatBytes(bytes){const value=Math.max(0,Number(bytes)||0),units=["B","KB","MB","GB","TB"];let size=value,index=0;while(size>=1024&&index<units.length-1){size/=1024;index++;}return `${index===0?Math.round(size):size.toFixed(size>=10?1:2)} ${units[index]}`;}
async function loadFiles({open=false}={}){
try{
const files=await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/files`,{method:"PUT",body:JSON.stringify({access_token:accessToken||null})});
document.querySelector("#footer-files").textContent=`${files.length} ${files.length===1?"file":"files"}`;
const totalSize=files.reduce((sum,file)=>sum+(Number(file.size_bytes)||0),0);
document.querySelector("#footer-files").textContent=`${files.length} ${files.length===1?"file":"files"} · ${formatBytes(totalSize)}`;
const list=document.querySelector("#files-list");
list.innerHTML=files.length?files.map(file=>`<div class="file-row" data-file-row="${file.id}"><div class="file-row-main"><div class="file-name">${escapeHtml(file.filename)}</div><div class="file-meta">${formatBytes(file.size_bytes)} · ${escapeHtml(file.mime_type)} · <span class="file-flag ${file.is_attached?"":"detached"}">${file.is_attached?"in note":"removed from content"}</span></div></div><div class="file-actions"><button data-show-file-code="link" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Link</button><button data-show-file-code="markdown" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Markdown</button>${info?.protected&&accessToken?`<button class="file-delete" data-delete-file="${file.id}" data-file-name="${escapeHtml(file.filename)}">Delete</button>`:""}</div><div class="file-code" hidden><textarea readonly aria-label="Generated file code"></textarea><button data-copy-generated>Copy</button></div></div>`).join(""):'<p class="empty">No files uploaded.</p>';
list.innerHTML=files.length?files.map(file=>`<div class="file-row" data-file-row="${file.id}"><div class="file-row-main"><div class="file-name">${escapeHtml(file.filename)}</div><div class="file-meta">${formatBytes(file.size_bytes)} · ${escapeHtml(file.mime_type)} · <span class="file-flag ${file.is_attached?"":"detached"}">${file.is_attached?"in note":"removed from content"}</span></div></div><div class="file-actions"><button data-show-file-code="link" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Link</button><button data-show-file-code="markdown" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Markdown</button>${info?.can_delete_files?`<button class="file-delete" data-delete-file="${file.id}" data-file-name="${escapeHtml(file.filename)}">Delete</button>`:""}</div><div class="file-code" hidden><textarea readonly aria-label="Generated file code"></textarea><button data-copy-generated>Copy</button></div></div>`).join(""):'<p class="empty">No files uploaded.</p>';
if(open&&!document.querySelector("#files-dialog").open)document.querySelector("#files-dialog").showModal();
}catch(error){toast(error.message);}
}
bindIdentityDialog({dialog:identityDialog,onIdentity:async value=>{nickname=value;identityDialog.close();updateCurrentUser();if(info.protected&&!accessToken)passwordDialog.showModal();else{loadFiles();connect();}}});
identityDialog.addEventListener("close",()=>{if(!nickname)queueMicrotask(()=>{if(!identityDialog.open)identityDialog.showModal();});});
async function initialize(){try{info=await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}`);document.querySelector("#back-workspace").href=`/w/${encodeURIComponent(workspaceSlug)}`;document.title=`${info.title} · ${info.workspace_title}`;publicTaskUpdates.checked=Boolean(info.allow_public_task_updates);applyUi({write:true,replace:true});if(!nickname){identityDialog.showModal();return;}updateCurrentUser();document.querySelector("#delete-note").hidden=info.note_protected;if(info.protected&&!accessToken)passwordDialog.showModal();else{loadFiles();connect();}}catch(e){document.body.innerHTML=`<main class="error-page"><div><h1>Note not found</h1><p>${escapeHtml(e.message)}</p></div></main>`;}}
async function initialize(){try{info=await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}`,{headers:getAuthToken()?{Authorization:`Bearer ${getAuthToken()}`}:{}});document.title=`${info.title} · ${info.workspace_title}`;publicTaskUpdates.checked=Boolean(info.allow_public_task_updates);applyUi({write:true,replace:true});if(!nickname){identityDialog.showModal();return;}updateCurrentUser();document.querySelector("#delete-note").hidden=info.note_protected;if(info.protected&&!accessToken)passwordDialog.showModal();else{loadFiles();connect();}}catch(e){document.body.innerHTML=`<main class="error-page"><div><h1>Note not found</h1><p>${escapeHtml(e.message)}</p></div></main>`;}}
document.querySelectorAll("[data-view]").forEach(b=>b.addEventListener("click",()=>{uiState={...uiState,view:b.dataset.view};applyUi({write:true});}));modeToggle.addEventListener("click",()=>{uiState={...uiState,mode:uiState.mode==="markdown"?"text":"markdown"};applyUi({write:true});});lineToggle.addEventListener("change",()=>{localStorage.setItem("rustpad:line-numbers",lineToggle.checked?"on":"off");renderGutter();});previewLineToggle.addEventListener("change",()=>{localStorage.setItem("rustpad:preview-line-numbers",previewLineToggle.checked?"on":"off");renderGutter();});compactToggle.addEventListener("change",()=>{localStorage.setItem("rustpad:compact",compactToggle.checked?"on":"off");applyUi();});fontFamily.addEventListener("change",()=>{localStorage.setItem("rustpad:font-family",fontFamily.value);applyUi();});fontSize.addEventListener("change",()=>{localStorage.setItem("rustpad:font-size",fontSize.value);applyUi();});
window.addEventListener("popstate",()=>{uiState=readEditorState();applyUi();});window.addEventListener("rustpad:urlchange",updateAddressLabel);document.querySelector("#copy-link").addEventListener("click",async()=>{try{await copyText(currentShareUrl(uiState));toast("Link copied");}catch(e){toast(e.message);}});document.querySelectorAll("[data-format]").forEach(b=>b.addEventListener("click",()=>{applyFormat(editor,b.dataset.format);b.closest("details")?.removeAttribute("open");}));bindFormatShortcuts(editor);document.querySelector("#shortcuts-button").addEventListener("click",()=>document.querySelector("#shortcuts-dialog").showModal());document.querySelector("#close-shortcuts").addEventListener("click",()=>document.querySelector("#shortcuts-dialog").close());preview.addEventListener("change",event=>{const checkbox=event.target.closest(".task-checkbox");if(!checkbox)return;const lineIndex=Number(checkbox.dataset.sourceLine)-1;const lines=editor.value.split("\n");if(lineIndex<0||lineIndex>=lines.length)return;lines[lineIndex]=lines[lineIndex].replace(/^(\s*[-*+]\s+\[)[ xX](\])/,`$1${checkbox.checked?"x":" "}$2`);editor.value=lines.join("\n");editor.dispatchEvent(new Event("input",{bubbles:true}));});preview.addEventListener("keydown",event=>{const target=event.target.closest(".preview-editable");if(!target)return;if(event.key==="Enter"){event.preventDefault();target.blur();return;}if(event.key==="ArrowUp"||event.key==="ArrowDown"){if(movePreviewCaret(target,event.key==="ArrowUp"?-1:1))event.preventDefault();}});preview.addEventListener("blur",event=>{const target=event.target.closest(".preview-editable");if(!target)return;const lineIndex=Number(target.dataset.sourceLine)-1;if(lineIndex<0)return;const lines=editor.value.split("\n");const value=markdownFromPreview(target);let next;if(target.dataset.tableCell!==undefined)next=replaceTableCell(lines[lineIndex],Number(target.dataset.tableCell),value);else{const prefix=target.dataset.sourcePrefix||"",suffix=target.dataset.sourceSuffix||"";next=prefix+value+suffix;}if(lines[lineIndex]===next)return;lines[lineIndex]=next;editor.value=lines.join("\n");editor.setSelectionRange(editor.value.length,editor.value.length);editor.dispatchEvent(new Event("input",{bubbles:true}));},{capture:true});
@@ -167,6 +174,14 @@ userColorPicker.addEventListener("input",()=>{
socket?.setColor(userColorPicker.value);
if(socket)socket.update(editor.value,JSON.stringify(owners));
});
window.addEventListener("storage",event=>{
if(event.key!==storedColorKey(nickname))return;
const replacement=currentOwner();
owners=owners.map(owner=>ownerName(owner)===nickname?replacement:owner);
updateCurrentUser();render();
socket?.setColor(currentUserColor()||null);
if(socket)socket.update(editor.value,JSON.stringify(owners));
});
editor.addEventListener("keydown",continueIndentation);editor.addEventListener("scroll",()=>{gutter.scrollTop=editor.scrollTop;renderGutter();});editor.addEventListener("input",()=>{const newLines=editor.value.split("\n").length;const cursorLine=editor.value.slice(0,editor.selectionStart).split("\n").length-1;while(owners.length<newLines)owners.push(currentOwner());owners=owners.slice(0,newLines);owners[cursorLine]=currentOwner();render();if(applyingRemote)return;clearTimeout(saveTimer);document.querySelector("#save-state").textContent="Saving…";saveTimer=setTimeout(()=>socket?.update(editor.value,JSON.stringify(owners)),250);});
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:"workspace",slug:workspaceSlug,password})});accessToken=result.access_token;setAccessToken("workspace",workspaceSlug,accessToken);password="";document.querySelector("#open-password").value="";document.querySelector("#password-error").textContent="";loadFiles();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='<p class="empty">Loading…</p>';try{const revisions=await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/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 `<article class="revision"><span class="revision__marker" style="--owner:${colorFor(author)}"></span><div><div class="revision__meta"><strong>${escapeHtml(author)}</strong><time>${formatDate(r.created_at)}</time></div><p class="revision__snippet">${snippet}</p><button data-preview="${r.id}">Preview</button><button data-revision="${r.id}">Restore</button><div class="revision__preview" id="preview-${r.id}" hidden></div></div></article>`;}).join(""):'<p class="empty">No history yet.</p>';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/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/restore`,{method:"POST",body:JSON.stringify({access_token:accessToken||null,revision_id:r.id})});toast("Version restored");});}}catch(e){list.innerHTML=`<p class="error">${escapeHtml(e.message)}</p>`;}});document.querySelector("#close-history").addEventListener("click",()=>{historyPanel.classList.remove("open");historyPanel.setAttribute("aria-hidden","true");document.body.classList.remove("history-open");});
@@ -189,7 +204,7 @@ document.querySelector("#files-list").addEventListener("click",async event=>{
const deleteButton=event.target.closest("[data-delete-file]");
if(deleteButton){
if(!await askConfirm(`Delete file "${deleteButton.dataset.fileName}" permanently?`,{title:"Delete file",confirmText:"Delete",danger:true}))return;
try{await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/files/${encodeURIComponent(deleteButton.dataset.deleteFile)}`,{method:"DELETE",body:JSON.stringify({access_token:accessToken||null})});toast("File deleted");await loadFiles();}catch(error){toast(error.message);}return;
try{await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/files/${encodeURIComponent(deleteButton.dataset.deleteFile)}`,{method:"DELETE",headers:getAuthToken()?{Authorization:`Bearer ${getAuthToken()}`}:{},body:JSON.stringify({access_token:accessToken||null})});toast("File deleted");await loadFiles();}catch(error){toast(error.message);}return;
}
});
document.querySelector("#delete-note").addEventListener("click",async()=>{if(!await askConfirm(`Delete note “${info.title}”? This cannot be undone.`,{title:"Delete note",confirmText:"Delete",danger:true}))return;try{await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}`,{method:"DELETE",body:JSON.stringify({access_token:accessToken||null})});location.assign(`/w/${encodeURIComponent(workspaceSlug)}`);}catch(error){toast(error.message);}});
+16 -2
View File
@@ -75,6 +75,12 @@ function markdownFromPreview(node){
if(tag==="sub")return `~${body}~`;
if(tag==="sup"&&!current.classList.contains("footnote-ref"))return `^${body}^`;
if(tag==="a")return `[${body}](${current.getAttribute("href")||"#"})`;
if(tag==="img"){
const src=current.getAttribute("src")||"";
const alt=current.getAttribute("alt")||"";
const title=current.getAttribute("title");
return `![${alt}](${src}${title?` "${title.replace(/"/g,"&quot;")}"`:""})`;
}
if(tag==="br")return " ";
return body;
};
@@ -122,6 +128,8 @@ function continueIndentation(event){
editor.dispatchEvent(new Event("input",{bubbles:true}));
}
function formatBytes(bytes){const value=Math.max(0,Number(bytes)||0),units=["B","KB","MB","GB","TB"];let size=value,index=0;while(size>=1024&&index<units.length-1){size/=1024;index++;}return `${index===0?Math.round(size):size.toFixed(size>=10?1:2)} ${units[index]}`;}
function replaceTableCell(line,index,value){
const leading=line.trimStart().startsWith("|"),trailing=line.trimEnd().endsWith("|");
let body=line.trim();if(leading)body=body.slice(1);if(trailing)body=body.slice(0,-1);
@@ -135,8 +143,9 @@ function applyRemote(content,ownerMap){if(content===editor.value&&ownerMap==null
async function loadFiles({open=false}={}){
try{
const files=await api(`/api/pads/${encodeURIComponent(slug)}/files`,{method:"PUT",body:JSON.stringify({access_token:accessToken||null})});
document.querySelector("#footer-files").textContent=`${files.length} ${files.length===1?"file":"files"}`;
document.querySelector("#files-list").innerHTML=files.length?files.map(file=>`<div class="file-row" data-file-row="${file.id}"><div class="file-row-main"><div class="file-name">${escapeHtml(file.filename)}</div><div class="file-meta">${escapeHtml(file.mime_type)} · ${Math.max(1,Math.round(file.size_bytes/1024))} KB · ${formatDate(file.created_at)} · <span class="file-flag${file.is_attached?"":" detached"}">${file.is_attached?"in note":"removed from content"}</span></div></div><div class="file-actions"><button data-show-file-code="link" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Link</button><button data-show-file-code="markdown" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Markdown</button><button data-show-file-code="html" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">HTML</button></div><div class="file-code" hidden><textarea readonly aria-label="Generated file code"></textarea><button data-copy-generated>Copy</button></div></div>`).join(""):'<p class="dialog-copy">No files uploaded.</p>';
const totalSize=files.reduce((sum,file)=>sum+(Number(file.size_bytes)||0),0);
document.querySelector("#footer-files").textContent=`${files.length} ${files.length===1?"file":"files"} · ${formatBytes(totalSize)}`;
document.querySelector("#files-list").innerHTML=files.length?files.map(file=>`<div class="file-row" data-file-row="${file.id}"><div class="file-row-main"><div class="file-name">${escapeHtml(file.filename)}</div><div class="file-meta">${formatBytes(file.size_bytes)} · ${escapeHtml(file.mime_type)} · ${formatDate(file.created_at)} · <span class="file-flag${file.is_attached?"":" detached"}">${file.is_attached?"in note":"removed from content"}</span></div></div><div class="file-actions"><button data-show-file-code="link" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Link</button><button data-show-file-code="markdown" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Markdown</button><button data-show-file-code="html" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">HTML</button>${info?.can_delete_files?`<button class="file-delete" data-delete-file="${file.id}" data-file-name="${escapeHtml(file.filename)}">Delete</button>`:""}</div><div class="file-code" hidden><textarea readonly aria-label="Generated file code"></textarea><button data-copy-generated>Copy</button></div></div>`).join(""):'<p class="dialog-copy">No files uploaded.</p>';
if(open)document.querySelector("#files-dialog").showModal();
}catch(error){if(open)toast(error.message);}
}
@@ -181,5 +190,10 @@ document.querySelector("#files-list").addEventListener("click",async event=>{
}
const copyButton=event.target.closest("[data-copy-generated]");
if(copyButton){try{await copyText(copyButton.closest(".file-code").querySelector("textarea").value);toast("Copied");}catch(error){toast(error.message);}return;}
const deleteButton=event.target.closest("[data-delete-file]");
if(deleteButton){
if(!confirm(`Delete file "${deleteButton.dataset.fileName}" permanently?`))return;
try{await api(`/api/pads/${encodeURIComponent(slug)}/files/${encodeURIComponent(deleteButton.dataset.deleteFile)}`,{method:"DELETE",headers:getAuthToken()?{Authorization:`Bearer ${getAuthToken()}`}:{},body:JSON.stringify({access_token:accessToken||null})});toast("File deleted");await loadFiles();}catch(error){toast(error.message);}return;
}
});
initialize();
+9 -7
View File
@@ -55,8 +55,8 @@
code</button><button type="button" data-format="codeblock">Code block</button><button
type="button" data-format="codeblock-lines">Code block with line
numbers</button><button type="button" data-format="mermaid">Mermaid
diagram</button><button type="button" data-format="table">Table</button><button type="button"
data-format="footnote">Footnote</button><button type="button"
diagram</button><button type="button" data-format="table">Table</button><button
type="button" data-format="footnote">Footnote</button><button type="button"
data-format="definition">Definition</button><button type="button"
data-format="highlight">Highlight</button><button type="button"
data-format="subscript">Subscript</button><button type="button"
@@ -78,9 +78,10 @@
<option value="22">22</option>
</select></label></div><button id="upload-button"
class="toolbar-action">Image/file</button><input id="file-input" type="file" hidden><label
class="line-toggle"><input id="line-numbers-toggle" type="checkbox" checked> Editor lines</label><label
class="line-toggle"><input id="preview-line-numbers-toggle" type="checkbox"> Preview lines</label><label
class="line-toggle"><input id="compact-toggle" type="checkbox" checked> Compact</label>
class="line-toggle"><input id="line-numbers-toggle" type="checkbox" checked> Editor
lines</label><label class="line-toggle"><input id="preview-line-numbers-toggle" type="checkbox">
Preview lines</label><label class="line-toggle"><input id="compact-toggle" type="checkbox" checked>
Compact</label>
<div class="toolbar-fill"></div><button id="mode-toggle" class="markdown-toggle active"
aria-pressed="true">Markdown</button>
<div class="view-switch"><button data-view="edit">Edit</button><button data-view="split"
@@ -148,7 +149,8 @@
<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
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 H1H4</span></div>
list</span><kbd>Alt+1…4</kbd><span>Headings H1H4</span>
</div>
</div>
</dialog>
<dialog id="files-dialog" class="image-editor-dialog files-dialog">
@@ -191,7 +193,7 @@
<h2>Protected workspace</h2><input id="open-password" type="password" autocomplete="current-password"
required placeholder="Password">
<p id="password-error" class="form-message error"></p><button class="primary-button">Open</button><a
id="back-workspace" class="dialog-link" href="/">Back</a>
class="dialog-link" href="/">Cancel</a>
</form>
</dialog>
<div id="toast" class="toast"></div>
+5 -4
View File
@@ -54,8 +54,8 @@
code</button><button type="button" data-format="codeblock">Code block</button><button
type="button" data-format="codeblock-lines">Code block with line
numbers</button><button type="button" data-format="mermaid">Mermaid
diagram</button><button type="button" data-format="table">Table</button><button type="button"
data-format="footnote">Footnote</button><button type="button"
diagram</button><button type="button" data-format="table">Table</button><button
type="button" data-format="footnote">Footnote</button><button type="button"
data-format="definition">Definition</button><button type="button"
data-format="highlight">Highlight</button><button type="button"
data-format="subscript">Subscript</button><button type="button"
@@ -146,7 +146,8 @@
<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
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 H1H4</span></div>
list</span><kbd>Alt+1…4</kbd><span>Headings H1H4</span>
</div>
</div>
</dialog>
<dialog id="files-dialog" class="image-editor-dialog files-dialog">
@@ -189,7 +190,7 @@
<h2>Protected note</h2><input id="open-password" type="password" autocomplete="current-password" required
placeholder="Password">
<p id="password-error" class="form-message error"></p><button class="primary-button">Open</button><a
class="dialog-link" href="/">Back</a>
class="dialog-link" href="/">Cancel</a>
</form>
</dialog>
<div id="toast" class="toast"></div>
+1 -1
View File
@@ -48,7 +48,7 @@
<dialog id="note-dialog">
<form id="note-form" class="dialog-panel">
<h2>New note</h2><input id="note-name" maxlength="80" required placeholder="Note name"><label
class="dialog-check"><input id="note-protect" type="checkbox" checked> Protect this note from
class="dialog-check"><input id="note-protect" type="checkbox"> Protect this note from
deletion</label>
<p id="note-error" class="form-message error"></p>
<div class="dialog-actions"><button type="button" id="cancel-note"