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'}

+ +
+ +
+ + +
+ + + +