]*)?>([\s\S]*?)<\/code><\/pre>/g, (m, code) => {
const rawCode = code.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, '&');
return `${highlightCode(rawCode)}
`;
});
return { docHtml: h, toc: entries };
}, [doc]);
// copy buttons + anchor links inside the rendered HTML, via event delegation
const onDocClick = useCallback((e) => {
const copy = e.target.closest && e.target.closest('[data-copy]');
if (copy) { const pre = copy.parentElement.querySelector('pre'); if (pre) copyToast(pre.innerText, 'Copied'); return; }
const anc = e.target.closest && e.target.closest('.docs-anchor');
if (anc) { e.preventDefault(); goSection(anc.getAttribute('href').split('/').pop()); }
}, [goSection]);
// after a doc renders, honor a pending #// deep-link scroll
useEffect(() => {
if (!docHtml || !pendingSection.current) return;
const s = pendingSection.current; pendingSection.current = null;
// two frames so code highlighting / reflow settles before we measure the target
requestAnimationFrame(() => requestAnimationFrame(() => {
const el = renderRef.current?.querySelector(`#${CSS.escape(s)}`); if (el) el.scrollIntoView({ block: 'start' });
}));
}, [docHtml]);
// full-text search: fetch all doc bodies once, then match title + body, with snippets
useEffect(() => { bodiesRef.current = null; }, [docs]);
useEffect(() => {
const query = q.trim();
if (!query) { setResults(null); return; }
let live = true;
(async () => {
if (!bodiesRef.current) {
const all = await Promise.all(docs.map((d) => api(`/docs/${d.id}`).then((r) => [d.id, r.doc.body || '']).catch(() => [d.id, ''])));
if (!live) return;
bodiesRef.current = Object.fromEntries(all);
}
const ql = query.toLowerCase();
const res = [];
for (const d of docs) {
const body = bodiesRef.current[d.id] || '';
const inMeta = (d.title + ' ' + (d.folder || '')).toLowerCase().includes(ql);
const idx = body.toLowerCase().indexOf(ql);
if (inMeta || idx >= 0) res.push({ ...d, snippet: idx >= 0 ? makeSnippet(body, idx, query) : '' });
}
if (live) setResults(res);
})();
return () => { live = false; };
}, [q, docs]);
const create = async () => {
const title = window.prompt('Document title', 'New document'); if (!title) return;
try { const r = await api('/docs', { method: 'POST', body: { title, folder: '', body: `# ${title}\n\n` } }); await loadList(); setSel(r.id); setEditing(true); } catch (e) { setErr(e.message); }
};
const save = async () => { try { await api(`/docs/${sel}`, { method: 'PATCH', body: draft }); await loadList(); setDoc({ ...doc, ...draft }); setEditing(false); } catch (e) { setErr(e.message); } };
const del = async () => { if (!window.confirm('Delete document?')) return; try { await api(`/docs/${sel}`, { method: 'DELETE' }); setSel(null); loadList(); } catch (e) { setErr(e.message); } };
const folders = {};
docs.forEach((d) => { (folders[d.folder || ''] = folders[d.folder || ''] || []).push(d); });
return (
{err && {err}
}
{!doc && Select or create a document.}
{doc && (
<>
{editing
? setDraft({ ...draft, title: e.target.value })} />
: {doc.title}
}
{editing && setDraft({ ...draft, folder: e.target.value })} />}
{canEdit && !editing && }
{editing && }
{editing && }
{canEdit && }
updated by {doc.updated_by || '—'}
{!editing && (
// always rendered in reading mode (hidden when too few sections) so it
// keeps a stable child slot — otherwise inserting it would shift the
// index and remount it, wiping the heading/code decorations.
)}
{editing
?
);
}
export default function DocsApp() {
return {(auth) => } ;
}