From c24a6106bfb9159639d2e9287937dc5fa65bab2e Mon Sep 17 00:00:00 2001 From: francy51 Date: Tue, 16 Jun 2026 15:44:16 -0400 Subject: [PATCH] feat(docs): add custom documentation pages with AI beautify User-created dynamic doc pages live at /docs/custom/:slug, persisted in the new backend. The editor offers "Beautify with AI", which regenerates the page as structured HTML with Mermaid diagrams and replaces the raw markdown source (the beautified version becomes the page's canonical content and survives edits). Adds a DocHtml renderer that lazily renders Mermaid blocks, a purple design token, sidebar/topbar entries for custom pages, and routing. --- apps/docs/package.json | 1 + apps/docs/src/App.tsx | 90 ++++--- apps/docs/src/components/CustomPageEditor.tsx | 245 ++++++++++++++++++ apps/docs/src/components/DocHtml.tsx | 103 ++++++++ apps/docs/src/components/Sidebar.tsx | 54 ++++ apps/docs/src/components/TopBar.tsx | 9 +- apps/docs/src/lib/customPagesStore.tsx | 102 ++++++++ apps/docs/src/lib/pagesApi.ts | 74 ++++++ apps/docs/src/pages/docs/CustomPageView.tsx | 116 +++++++++ .../src/pages/docs/CustomPagesIndexPage.tsx | 77 ++++++ apps/docs/src/styles/tailwind.css | 157 +++++++++++ 11 files changed, 987 insertions(+), 41 deletions(-) create mode 100644 apps/docs/src/components/CustomPageEditor.tsx create mode 100644 apps/docs/src/components/DocHtml.tsx create mode 100644 apps/docs/src/lib/customPagesStore.tsx create mode 100644 apps/docs/src/lib/pagesApi.ts create mode 100644 apps/docs/src/pages/docs/CustomPageView.tsx create mode 100644 apps/docs/src/pages/docs/CustomPagesIndexPage.tsx diff --git a/apps/docs/package.json b/apps/docs/package.json index 755f279..d8197c7 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -13,6 +13,7 @@ "@react-three/drei": "^9.122.0", "@react-three/fiber": "^8.17.10", "@void-nav/ui": "workspace:*", + "mermaid": "^11.15.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.30.1", diff --git a/apps/docs/src/App.tsx b/apps/docs/src/App.tsx index 8884ed5..eff937f 100644 --- a/apps/docs/src/App.tsx +++ b/apps/docs/src/App.tsx @@ -1,6 +1,7 @@ import { Navigate, Route, Routes } from "react-router-dom"; import { DocsLayout } from "./layouts/DocsLayout"; import { NotFound } from "./components/NotFound"; +import { CustomPagesProvider } from "./lib/customPagesStore"; import { OverviewPage } from "./pages/docs/OverviewPage"; import { ArchitecturePage } from "./pages/docs/ArchitecturePage"; import { TechStackPage } from "./pages/docs/TechStackPage"; @@ -18,6 +19,8 @@ import { GapAnalysisPage } from "./pages/docs/GapAnalysisPage"; import { VerticalSliceEvaluationPage } from "./pages/docs/VerticalSliceEvaluationPage"; import { DesignDocPage } from "./pages/docs/DesignDocPage"; import { KanbanBoardPage } from "./pages/docs/KanbanBoardPage"; +import { CustomPagesIndexPage } from "./pages/docs/CustomPagesIndexPage"; +import { CustomPageView } from "./pages/docs/CustomPageView"; import { StarMapDemo } from "./prototypes/existing-demos/StarMapDemo"; import { ShipMovementDemo } from "./prototypes/existing-demos/ShipMovementDemo"; import { WarpTravelDemo } from "./prototypes/existing-demos/WarpTravelDemo"; @@ -36,49 +39,56 @@ import { GameHudPrototype } from "./prototypes/standalone-huds/GameHudPrototype" export function App() { return ( - - } /> + + + } /> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + {/* User-created dynamic documentation pages */} + } /> + } /> + } /> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + - } /> } /> - - - } /> - + + ); } diff --git a/apps/docs/src/components/CustomPageEditor.tsx b/apps/docs/src/components/CustomPageEditor.tsx new file mode 100644 index 0000000..8248fab --- /dev/null +++ b/apps/docs/src/components/CustomPageEditor.tsx @@ -0,0 +1,245 @@ +import { useState } from 'react'; +import { useCustomPages } from '../lib/customPagesStore'; +import type { CustomPage } from '../lib/pagesApi'; + +/** + * Modal editor for a custom documentation page. + * + * Lets the author set title / icon / blurb and edit the raw content (markdown- + * ish). Two save actions: + * - **Save**: persists the raw content; the backend renders it to plain HTML. + * - **Beautify with AI**: sends the content to the backend's AI endpoint, + * which returns polished HTML with Mermaid diagrams. The beautified HTML + * replaces the page's source content, discarding the old markdown. + * + * Works for both create (`page === null`) and edit modes. + */ + +interface CustomPageEditorProps { + page: CustomPage | null; + onClose: () => void; + onSaved?: (page: CustomPage) => void; + onDeleted?: () => void; +} + +export function CustomPageEditor({ page, onClose, onSaved, onDeleted }: CustomPageEditorProps) { + const { create, update, remove, beautify } = useCustomPages(); + + const [title, setTitle] = useState(page?.title ?? ''); + const [icon, setIcon] = useState(page?.icon ?? '◈'); + const [blurb, setBlurb] = useState(page?.blurb ?? ''); + const [content, setContent] = useState(page?.content ?? ''); + const [isBeautified, setIsBeautified] = useState(page?.beautified ?? false); + + const [saving, setSaving] = useState(false); + const [beautifying, setBeautifying] = useState(false); + const [err, setErr] = useState(null); + + const overlayStyle = { + position: 'fixed' as const, + inset: 0, + background: 'rgba(0,0,0,0.6)', + backdropFilter: 'blur(2px)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + zIndex: 50, + padding: 'var(--sp-4)', + }; + + const persist = async (): Promise => { + setErr(null); + if (!title.trim()) { + setErr('Title is required.'); + return null; + } + setSaving(true); + try { + const input = { title: title.trim(), icon: icon.trim() || '◈', blurb: blurb.trim(), content }; + const saved = page ? await update(page.id, input) : await create(input); + return saved; + } catch (e) { + setErr(e instanceof Error ? e.message : 'Save failed'); + return null; + } finally { + setSaving(false); + } + }; + + const handleSave = async () => { + const saved = await persist(); + if (saved) onSaved?.(saved); + }; + + const handleBeautify = async () => { + setErr(null); + if (!content.trim()) { + setErr('Add some content before beautifying.'); + return; + } + setBeautifying(true); + try { + // Ensure the page exists and has the latest content before beautifying. + let target = page; + if (!target) { + const created = await persist(); + if (!created) return; + target = created; + } else { + const updated = await update(target.id, { title: title.trim(), icon: icon.trim() || '◈', blurb: blurb.trim(), content }); + target = updated; + } + const beautified = await beautify(target.id); + // The beautified HTML now lives in `content`; sync the editor so it shows + // the replacement of the old markdown. + setTitle(beautified.title); + setIcon(beautified.icon); + setBlurb(beautified.blurb); + setContent(beautified.content); + setIsBeautified(beautified.beautified); + onSaved?.(beautified); + } catch (e) { + setErr(e instanceof Error ? e.message : 'Beautify failed'); + } finally { + setBeautifying(false); + } + }; + + const handleDelete = async () => { + if (!page) return; + if (!confirm(`Delete "${page.title}"? This cannot be undone.`)) return; + setSaving(true); + try { + await remove(page.id); + onDeleted?.(); + } catch (e) { + setErr(e instanceof Error ? e.message : 'Delete failed'); + } finally { + setSaving(false); + } + }; + + const inputCls = + 'w-full rounded-lg border border-border bg-bg px-3 py-2 text-[0.9rem] text-fg outline-none transition-colors focus:border-accent'; + + return ( +
+
e.stopPropagation()} + > +
+

{page ? 'Edit Page' : 'Create Custom Page'}

+ +
+ +
+ + +
+ + + +