]*)?>([\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]);
const create = async () => {
const title = window.prompt('Document title', 'New document'); if (!title) return;
@@ -129,9 +168,20 @@ function DocsWorkspace({ 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
?
diff --git a/src/Scene2D.jsx b/src/Scene2D.jsx
index 912e50b..5c6d3d3 100644
--- a/src/Scene2D.jsx
+++ b/src/Scene2D.jsx
@@ -10,7 +10,7 @@ import { api } from './api.js';
const Menu = ({ size = 20 }) => ();
const X = ({ size = 20 }) => ();
-const RELEASE = '0.87.0-docroute';
+const RELEASE = '0.88.0-docsections';
const NAV = [
{ id: 'home', label: 'Tavern' },
diff --git a/src/styles.css b/src/styles.css
index 79089ff..14c0318 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -1504,3 +1504,14 @@ button {
.hf-an-body b { color: #7fe9ff; font-weight: 600; }
.hf-an-body a { color: #46c8e0; }
@media (max-width: 600px) { .hf-agentnative { flex-direction: column; text-align: center; } }
+
+/* docs: on-this-page TOC + heading anchors (0.88) */
+.docs-toc { margin: 4px 0 24px; padding: 13px 16px; border: 1px solid #2a2140; border-radius: 12px; background: rgba(39, 216, 255, 0.03); display: flex; flex-direction: column; gap: 1px; }
+.docs-toc-h { font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: #6f5b8c; margin-bottom: 6px; }
+.docs-toc-link { text-align: left; background: none; border: none; color: #b9a7e0; font-size: 13.5px; line-height: 1.4; padding: 4px 8px; border-radius: 6px; cursor: pointer; transition: background 0.12s, color 0.12s; }
+.docs-toc-link:hover { background: rgba(39, 216, 255, 0.08); color: #fff; }
+.docs-toc-link.lvl3 { padding-left: 22px; font-size: 12.5px; color: #9a89bd; }
+.docs-render h2, .docs-render h3 { position: relative; scroll-margin-top: 28px; }
+.docs-anchor { opacity: 0; margin-left: 9px; padding: 0 2px; background: none; border: none; color: #46c8e0; cursor: pointer; font-size: 0.8em; font-weight: 600; text-decoration: none; vertical-align: middle; transition: opacity 0.12s; }
+.docs-render h2:hover .docs-anchor, .docs-render h3:hover .docs-anchor, .docs-anchor:focus-visible { opacity: 0.85; }
+.docs-anchor:hover { opacity: 1; }