/** * DocsSurface — builtin documentation viewer (v0.6.2) * * Reads globals: * __SECTION__ — active doc slug (e.g. "GETTING-STARTED") * __BASE__ — base path * * Fetches markdown from GET /api/v1/docs/:name, renders with * a simple markdown-to-HTML converter. Sidebar lists all docs. * * v0.6.2: dark mode fix, topbar navigation, error handling. */ const { html } = window; const { useState, useEffect, useCallback, useMemo } = hooks; const { render } = preact; import { Topbar } from '../../shell/topbar.js'; function DocsSurface() { const [docs, setDocs] = useState([]); const [active, setActive] = useState(window.__SECTION__ || 'GETTING-STARTED'); const [content, setContent] = useState(''); const [title, setTitle] = useState(''); const [loading, setLoading] = useState(true); const [listError, setListError] = useState(false); // Load doc list useEffect(() => { if (!sw?.api?.get) return; sw.api.get('/api/v1/docs').then(r => { setDocs(Array.isArray(r) ? r : r.data || []); setListError(false); }).catch(() => { setListError(true); }); }, []); // Load active doc const loadDoc = useCallback(async (slug) => { setLoading(true); setActive(slug); try { const resp = await sw.api.get(`/api/v1/docs/${slug}`); const d = resp.data || resp; setTitle(d.title || slug); setContent(d.content || 'Document not found.'); } catch (e) { setContent('Failed to load document.'); } setLoading(false); // Update URL without reload const base = window.__BASE__ || ''; history.replaceState(null, '', `${base}/docs/${slug}`); }, []); useEffect(() => { loadDoc(active); }, []); function navigate(slug) { if (slug === active) return; loadDoc(slug); // Scroll to top const el = document.querySelector('.docs-content'); if (el) el.scrollTop = 0; } // Extract document outline from markdown content const outline = useMemo(() => { if (!content) return []; const headings = []; let inCode = false; for (const line of content.split('\n')) { if (line.startsWith('```')) { inCode = !inCode; continue; } if (inCode) continue; const m = line.match(/^(#{1,4})\s+(.+)/); if (m) { const text = m[2].replace(/[*_`\[\]]/g, ''); const id = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); headings.push({ level: m[1].length, text, id }); } } return headings; }, [content]); function scrollToHeading(id) { const el = document.getElementById(id); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); } function retryList() { setListError(false); sw.api.get('/api/v1/docs').then(r => { setDocs(Array.isArray(r) ? r : r.data || []); }).catch(() => { setListError(true); }); } return html` <${Topbar} title="Documentation" />
$1');
return s;
}
function flushTable() {
if (!inTable || tableRows.length === 0) return;
inTable = false;
html += '| ${inline(h.trim())} | `; html += '
|---|
| ${inline(cell.trim())} | `; html += '
${esc(codeLines.join('\n'))}`;
inCode = false;
codeLines = [];
codeLang = '';
} else {
flushList();
flushTable();
inCode = true;
codeLang = line.slice(3).trim();
}
i++;
continue;
}
if (inCode) {
codeLines.push(line);
i++;
continue;
}
// Table rows
if (line.includes('|') && line.trim().startsWith('|')) {
flushList();
const cells = line.split('|').slice(1, -1);
if (!inTable) inTable = true;
// Skip separator row (---|---)
if (cells.every(c => /^[\s:-]+$/.test(c))) {
tableRows.push(cells); // keep for row counting
} else {
tableRows.push(cells);
}
i++;
continue;
} else {
flushTable();
}
// Headings
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
if (headingMatch) {
flushList();
const level = headingMatch[1].length;
const text = headingMatch[2];
const id = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
html += `${inline(line)}
`; i++; } flushList(); flushTable(); return html; } // ── Styles ────────────────────────────────── const style = document.createElement('style'); style.textContent = ` .docs-layout { display: flex; flex: 1; min-height: 0; overflow: hidden; gap: 0; } .docs-sidebar { width: 220px; min-width: 220px; height: 100%; padding: 20px 16px; border-right: 1px solid var(--border); background: var(--bg-secondary); overflow-y: auto; box-sizing: border-box; } .docs-sidebar__heading { margin: 0 0 12px; font-size: 14px; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.5px; } .docs-nav-item { display: block; padding: 6px 10px; margin: 2px 0; border-radius: 6px; color: var(--text); text-decoration: none; font-size: 13px; cursor: pointer; transition: background 0.15s; } .docs-nav-item:hover { background: var(--bg-hover); } .docs-nav-item.active { background: var(--accent-dim); color: var(--accent); font-weight: 600; } .docs-content { flex: 1; height: 100%; padding: 24px 40px; max-width: 800px; overflow-y: auto; box-sizing: border-box; } .docs-loading { padding: 40px; text-align: center; color: var(--text-3); animation: docs-pulse 1.5s ease-in-out infinite; } @keyframes docs-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } .docs-error-inline { padding: 12px; color: var(--text-3); font-size: 13px; text-align: center; } .docs-retry-btn { margin-top: 8px; padding: 4px 12px; background: var(--bg-hover); border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-size: 12px; cursor: pointer; } .docs-retry-btn:hover { background: var(--bg-elevated); } .docs-outline { width: 200px; min-width: 200px; height: 100%; padding: 20px 12px; border-left: 1px solid var(--border); overflow-y: auto; box-sizing: border-box; } .docs-outline__heading { margin: 0 0 10px; font-size: 11px; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; } .docs-outline__item { display: block; padding: 3px 8px; margin: 1px 0; border-radius: 4px; color: var(--text-2); text-decoration: none; font-size: 12px; line-height: 1.4; cursor: pointer; transition: color 0.15s, background 0.15s; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .docs-outline__item:hover { color: var(--text); background: var(--bg-hover); } .docs-outline__item--h1 { padding-left: 8px; font-weight: 600; color: var(--text); } .docs-outline__item--h2 { padding-left: 8px; } .docs-outline__item--h3 { padding-left: 20px; } .docs-outline__item--h4 { padding-left: 32px; font-size: 11px; } .docs-article h1, .docs-article h2, .docs-article h3, .docs-article h4 { scroll-margin-top: 16px; } .docs-article h1 { font-size: 28px; margin: 0 0 16px; border-bottom: 1px solid var(--border); padding-bottom: 8px; color: var(--text); } .docs-article h2 { font-size: 22px; margin: 24px 0 12px; color: var(--text); } .docs-article h3 { font-size: 18px; margin: 20px 0 8px; color: var(--text); } .docs-article p { margin: 8px 0; line-height: 1.6; color: var(--text); } .docs-article pre { background: var(--bg-code); border: 1px solid var(--border); border-radius: 6px; padding: 12px 16px; overflow-x: auto; font-size: 13px; line-height: 1.5; margin: 12px 0; } .docs-article code { background: var(--bg-code); padding: 2px 5px; border-radius: 3px; font-size: 0.9em; color: var(--text); } .docs-article pre code { background: none; padding: 0; } .docs-article ul, .docs-article ol { margin: 8px 0; padding-left: 24px; } .docs-article li { margin: 4px 0; line-height: 1.5; color: var(--text); } .docs-article hr { border: none; border-top: 1px solid var(--border); margin: 24px 0; } .docs-article a { color: var(--accent); } .docs-article .data-table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 13px; } .docs-article .data-table th { text-align: left; padding: 8px 12px; border-bottom: 2px solid var(--border); color: var(--text); font-weight: 600; } .docs-article .data-table td { padding: 6px 12px; border-bottom: 1px solid var(--border); color: var(--text-2); } .docs-article .data-table tr:hover td { background: var(--bg-hover); } .docs-article strong { color: var(--text); } `; document.head.appendChild(style); // ── Mount ─────────────────────────────────── const mount = document.getElementById('docs-mount'); if (mount) { mount.innerHTML = ''; render(preact.h(DocsSurface, null), mount); }