+ );
+}
diff --git a/apps/docs/src/components/DocHtml.tsx b/apps/docs/src/components/DocHtml.tsx
new file mode 100644
index 0000000..950ee08
--- /dev/null
+++ b/apps/docs/src/components/DocHtml.tsx
@@ -0,0 +1,103 @@
+import { useEffect, useRef, useState, type CSSProperties } from 'react';
+
+/**
+ * Renders an HTML documentation fragment produced by the API (either a raw
+ * markdown render or an AI-beautified page) and post-processes any
+ * `
…
` blocks into rendered SVG diagrams.
+ *
+ * The content comes from our own backend, so it is treated as trusted author
+ * HTML and rendered with dangerouslySetInnerHTML. Mermaid is imported lazily
+ * (and only configured once) so the large diagram library stays out of the
+ * main bundle unless a page actually contains diagrams.
+ */
+
+interface DocHtmlProps {
+ html: string;
+ style?: CSSProperties;
+ className?: string;
+}
+
+let diagCounter = 0;
+type MermaidRunner = { render: (id: string, src: string) => Promise<{ svg: string }> };
+let mermaidPromise: Promise | null = null;
+
+async function loadMermaid(): Promise {
+ if (!mermaidPromise) {
+ mermaidPromise = import('mermaid').then((mod) => {
+ const mermaid = mod.default;
+ mermaid.initialize({
+ startOnLoad: false,
+ theme: 'dark',
+ securityLevel: 'strict',
+ fontFamily: 'var(--font-mono, ui-monospace, monospace)',
+ });
+ return mermaid as unknown as MermaidRunner;
+ });
+ }
+ return mermaidPromise;
+}
+
+export function DocHtml({ html, style, className }: DocHtmlProps) {
+ const containerRef = useRef(null);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const root = containerRef.current;
+ if (!root) return;
+
+ const blocks = Array.from(root.querySelectorAll('div.mermaid'));
+ if (blocks.length === 0) return;
+
+ let cancelled = false;
+ setError(null);
+
+ (async () => {
+ let runner: MermaidRunner;
+ try {
+ runner = await loadMermaid();
+ } catch (err) {
+ if (!cancelled) setError(err instanceof Error ? err.message : 'Failed to load diagram library');
+ return;
+ }
+
+ for (const block of blocks) {
+ const source = block.textContent ?? '';
+ if (!source.trim()) continue;
+ const id = `mmd-${diagCounter++}`;
+ try {
+ const { svg } = await runner.render(id, source);
+ if (cancelled) return;
+ block.innerHTML = svg;
+ } catch (err) {
+ if (cancelled) return;
+ const msg = err instanceof Error ? err.message : String(err);
+ block.innerHTML = `
+ );
+}
diff --git a/apps/docs/src/pages/docs/CustomPagesIndexPage.tsx b/apps/docs/src/pages/docs/CustomPagesIndexPage.tsx
new file mode 100644
index 0000000..24a4f38
--- /dev/null
+++ b/apps/docs/src/pages/docs/CustomPagesIndexPage.tsx
@@ -0,0 +1,77 @@
+import { Link } from 'react-router-dom';
+import { useCustomPages } from '../../lib/customPagesStore';
+
+/**
+ * Landing page for user-created documentation at /docs/custom.
+ *
+ * Lists every custom page with an "Add Page" action. Empty state explains the
+ * feature and links to the creator. Custom pages are persisted in the backend
+ * (SQLite via @void-nav/api) and rendered dynamically; the "Beautify with AI"
+ * action lives inside each page's editor.
+ */
+export function CustomPagesIndexPage() {
+ const { pages, loading, error } = useCustomPages();
+
+ return (
+
+
+
Custom Pages
+
+ DYNAMIC
+
+
+ + Add Page
+
+
+
+
+ Documentation pages you create on the fly. Write rough notes in the editor,
+ then hit ✦ Beautify with AI to
+ regenerate the page as structured HTML with generated diagrams.
+