feat(ui): page editor + reference detail
Page view: header + split-pane markdown editor (textarea on left, marked + DOMPurify rendered preview on right) + backlinks card pulling /api/pages/:id/backlinks. Save calls PATCH /api/pages/:id with body_md and surfaces the resulting updated_at as a timestamp. Reference detail: media block (image preview / YouTube embed via youtube-nocookie / link fallback), summary card, metadata table, tags card with attach/detach (creates the tag idempotently then attaches), linked-from card from /api/links/to/ref/:id. marked + DOMPurify vendored to public/vendor as ESM. The markdown editor uses the explicit html: opt-in on dom.js's preview element only — all other text comes from textContent. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,56 @@
|
||||
// T17 stub — full implementation lands in T20.
|
||||
// Page view: header + markdown editor + backlinks panel.
|
||||
import { api } from '../api.js';
|
||||
import { el, mount } from '../dom.js';
|
||||
import { markdownEditor } from '../components/markdown_editor.js';
|
||||
|
||||
export async function render(main, ctx) {
|
||||
mount(main,
|
||||
el('h1', { class: 'view-h1' }, 'Page'),
|
||||
el('p', { class: 'view-sub muted' }, 'id: ' + (ctx.params.id || '—')),
|
||||
el('div', { class: 'card muted' }, 'Page editor ships in T20.')
|
||||
const id = ctx.params.id;
|
||||
mount(main, el('p', { class: 'view-sub muted' }, 'Loading …'));
|
||||
|
||||
let page;
|
||||
try { page = await api.get('/api/pages/' + id); }
|
||||
catch (e) {
|
||||
mount(main,
|
||||
el('h1', { class: 'view-h1' }, 'Page not found'),
|
||||
el('p', { class: 'view-sub muted' }, e.message)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = markdownEditor({
|
||||
initial: page.body_md || '',
|
||||
save: (value) => api.patch('/api/pages/' + id, { body_md: value })
|
||||
});
|
||||
|
||||
const backlinksCard = el('div', { class: 'card' },
|
||||
el('h3', {}, 'Backlinks'),
|
||||
el('div', { id: 'backlinks-list' }, el('span', { class: 'muted' }, 'Loading …'))
|
||||
);
|
||||
|
||||
mount(main,
|
||||
el('h1', { class: 'view-h1' }, page.title),
|
||||
el('p', { class: 'view-sub muted' }, '/' + page.slug),
|
||||
editor,
|
||||
backlinksCard
|
||||
);
|
||||
|
||||
try {
|
||||
const links = await api.get('/api/pages/' + id + '/backlinks');
|
||||
const list = document.getElementById('backlinks-list');
|
||||
if (!links.length) mount(list, el('span', { class: 'muted' }, 'No backlinks yet.'));
|
||||
else mount(list,
|
||||
el('ul', { class: 'plain' }, links.map(l =>
|
||||
el('li', {},
|
||||
el('span', { class: 'status idle' }, l.from_type),
|
||||
' ',
|
||||
l.source_title || el('span', { class: 'muted' }, '(untitled)'),
|
||||
' ',
|
||||
el('span', { class: 'muted', style: { fontSize: '11px' } }, 'rel: ' + l.relation)
|
||||
)
|
||||
))
|
||||
);
|
||||
} catch (e) {
|
||||
const list = document.getElementById('backlinks-list');
|
||||
if (list) mount(list, el('span', { class: 'muted' }, 'Could not load: ' + e.message));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user