feat(ui): page header actions — Edit (accent) + Revisions menu (view/restore) + Export, top-right

Editor refactored to expose controls; Edit moved into the doc header as the orange
primary action; new Revisions dropdown lists page_revisions with a modal preview + Restore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
root
2026-06-04 23:26:36 +10:00
parent 0a490e4e68
commit 6d01cb34a7
4 changed files with 92 additions and 43 deletions

View File

@@ -0,0 +1,57 @@
// Revisions dropdown for a page: lists page_revisions; clicking one opens a modal
// with the rendered old content + a Restore action (PATCH body_md).
import { el } from '../dom.js';
import { api } from '../api.js';
import { marked } from '../vendor/marked.esm.js';
import DOMPurify from '../vendor/purify.es.mjs';
export function revisionsMenu(pageId, onRestore) {
const wrap = el('div', { class: 'exp-menu' });
const list = el('div', { class: 'exp-list rev-list' });
const btn = el('button', {
class: 'ghost',
onclick: async (e) => { e.stopPropagation(); if (wrap.classList.toggle('open')) await load(); }
}, 'Revisions ▾');
async function load() {
list.replaceChildren(el('div', { class: 'muted', style: { padding: '7px 10px' } }, 'Loading…'));
let revs = [];
try { revs = await api.get('/api/pages/' + pageId + '/revisions'); } catch { /* */ }
list.replaceChildren();
if (!revs.length) { list.appendChild(el('div', { class: 'muted', style: { padding: '7px 10px' } }, 'No earlier revisions.')); return; }
revs.forEach(r => list.appendChild(el('button', { onclick: () => view(r) },
new Date(r.created_at).toLocaleString() + (r.edited_by ? ` · ${r.edited_by}` : ''))));
}
function view(rev) {
wrap.classList.remove('open');
const body = el('div', { class: 'md-preview' });
body.innerHTML = DOMPurify.sanitize(marked.parse(rev.body_md || ''));
const modal = el('div', {
class: 'rev-overlay',
onclick: (e) => { if (e.target === modal) modal.remove(); }
},
el('div', { class: 'rev-modal' },
el('div', { class: 'rev-modal-head' },
el('span', {}, 'Revision · ' + new Date(rev.created_at).toLocaleString()),
el('button', { class: 'ghost', onclick: () => modal.remove() }, 'Close')),
body,
el('div', { class: 'rev-modal-foot' },
el('button', {
class: 'primary',
onclick: async () => {
try {
await api.patch('/api/pages/' + pageId, { body_md: rev.body_md });
if (onRestore) onRestore(rev.body_md);
modal.remove();
} catch (e) { alert('Restore failed: ' + e.message); }
}
}, 'Restore this version'))));
document.body.appendChild(modal);
}
wrap.append(btn, list);
document.addEventListener('click', () => wrap.classList.remove('open'));
return wrap;
}