// Split-pane Markdown editor. // - Left: textarea (raw markdown) // - Right: rendered HTML preview via marked → DOMPurify // Save button calls a caller-supplied save(value) that returns a promise // resolving to the updated row. We surface the last edited_at timestamp. import { marked } from '../vendor/marked.esm.js'; import DOMPurify from '../vendor/purify.es.mjs'; import { el } from '../dom.js'; marked.setOptions({ gfm: true, breaks: true }); export function markdownEditor({ initial = '', save }) { const ta = el('textarea', { style: { width: '100%', minHeight: '420px', resize: 'vertical', fontFamily: 'var(--font-mono)', fontSize: '13px', lineHeight: '1.5' } }); ta.value = initial; const preview = el('div', { class: 'md-preview' }); function rerender() { const html = DOMPurify.sanitize(marked.parse(ta.value)); preview.innerHTML = html; // sanitized — see dom.js note about html: opt-in } ta.addEventListener('input', rerender); rerender(); const stamp = el('span', { class: 'muted', style: { fontSize: '11px' } }, ''); const btn = el('button', { class: 'primary', onclick: async () => { btn.disabled = true; try { const updated = await save(ta.value); if (updated?.updated_at) stamp.textContent = 'Saved ' + new Date(updated.updated_at).toLocaleString(); else stamp.textContent = 'Saved'; } catch (e) { stamp.textContent = 'Save failed: ' + e.message; } finally { btn.disabled = false; } } }, 'Save'); return el('div', {}, el('div', { class: 'row' }, el('div', { class: 'card' }, el('h3', {}, 'Edit'), ta), el('div', { class: 'card' }, el('h3', {}, 'Preview'), preview) ), el('div', { style: { display: 'flex', gap: '12px', alignItems: 'center', marginTop: '8px' } }, btn, stamp ) ); }