Advanced editor in React
AdvancedEditor from @skaile/forge-editor/react/advanced is the React counterpart of the Vue AdvancedEditor: an EditorSurface over composeEditor of its content, interaction, ai, add, remove and backend props (defaults { content: 'rich', interaction: 'full', ai: true }, i.e. advancedEditor()), which lists every feature and integration (core-content, core-ui, media, transcript, composites, mentions, authors, comments, ai, collab, block-ui, composite-media, composite-ai, transcript-ai, themes) with its React binding, in the same schema order as the Vue surface. It uses no Vue, Nuxt UI or Nuxt module. The runnable reference is examples/react-advanced (bun run --cwd examples/react-advanced dev, port 3043): the React counterpart of the Nuxt document page. It reuses the Nuxt app's documents, session and collaboration socket through the Vite proxy (/api, /collab), so start the Nuxt app first (bun run dev, port 3056).
Setup is the simple React setup plus three things:
- Styles. Replace the three library imports by one:
@import "@skaile/forge-editor/react/styles/advanced.css";aftertailwindcssandtw-animate-css(it importsui-vars.css,core.css, every feature sheet andthemes.csslast, plus the bridge that lets an Obsidian theme restyle the shadcn tokens). Keep the@sourcefor the library's wholedist(srcin this repository, as the example does) and the shadcn theme block;examples/react-advanced/src/index.csshas a complete copy. - Notices. Create the context with
createEditorContext({ notify: reactNotifier })and render sonner's<Toaster />once inside the provider (both exported by@skaile/forge-editor/react). Features sendNotices to the surface's notifier (import problems, AI rate limits, comment errors, „Dokument gelöscht“ with undo in the tree); without<Toaster />none is visible. - Dependencies. The simple setup's list plus
sonner(theToasterand the notice seam),yjsand@hocuspocus/providerfor collaboration; use the exact version ranges of the example'spackage.json.
main.tsx:
import lucide from '@iconify-json/lucide/icons.json'
import { addCollection } from '@iconify/react'
import { createHttpBackend } from '@skaile/forge-editor/adapters/http'
import { createEditorContext } from '@skaile/forge-editor/core'
import { EditorContextProvider, reactNotifier, Toaster } from '@skaile/forge-editor/react'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './App'
import './index.css'
// Preload the icon set: the page makes no request to the Iconify API.
addCollection(lucide)
// The host owns the context; the provider never disposes it. The backend is the HTTP one (`/api/editor` over the Nuxt host,
// the collaboration websocket `/collab/<id>`); the backend offers `settings`, so they sync with the host's account store,
// notices of app-level models become sonner toasts.
const context = createEditorContext({ backend: createHttpBackend(), notify: reactNotifier })
createRoot(document.getElementById('app')!).render(
<StrictMode>
<EditorContextProvider context={context}>
<App />
<Toaster />
</EditorContextProvider>
</StrictMode>,
)
The example passes createHttpBackend() so settings sync with the host's /api/editor/settings; pass createMemoryBackend() for local-only settings.
The page. The chrome components beside the editor take the editor from onReady and find its preset through it. AdvancedEditor takes the surface's props except preset, plus the composition props (content, interaction, ai, add, remove, backend, blockTypes, blockUi, coreContent, read once at mount); <AdvancedEditor remove={['tables']} ai={false} /> is the advanced editor without tables and AI:
import { AiSidebar } from '@skaile/forge-editor/ai/react'
import { CommentsMargin } from '@skaile/forge-editor/comments/react'
import { CompositeInspector } from '@skaile/forge-editor/composites/react'
import type { CollaborationSession } from '@skaile/forge-editor/core'
import { EditorSettingsModal, EditorToolbar } from '@skaile/forge-editor/react'
import { AdvancedEditor } from '@skaile/forge-editor/react/advanced'
import type { Editor } from '@tiptap/core'
import { useState } from 'react'
export function Page({ id, session }: { id: string, session: CollaborationSession }) {
const [editor, setEditor] = useState<Editor>()
const [scrollEl, setScrollEl] = useState<HTMLElement | null>(null)
const [settingsOpen, setSettingsOpen] = useState(false)
const [sidebarOpen, setSidebarOpen] = useState(false)
const [commentsOpen, setCommentsOpen] = useState(false)
return (
<>
<EditorToolbar editor={editor} />
<main ref={setScrollEl} className="relative flex min-h-0 flex-1 overflow-auto">
<AdvancedEditor
key={id}
collaboration={session}
documentId={id}
settingsUi="external"
sidebarOpen={sidebarOpen}
onSidebarOpenChange={setSidebarOpen}
commentsOpen={commentsOpen}
onCommentsOpenChange={setCommentsOpen}
onReady={setEditor}
onSettingsOpen={() => setSettingsOpen(true)}
/>
<CommentsMargin editor={editor} container={scrollEl} open={commentsOpen} />
</main>
<CompositeInspector editor={editor} />
<AiSidebar editor={editor} open={sidebarOpen} onOpenChange={setSidebarOpen} />
<EditorSettingsModal editor={editor} open={settingsOpen} onOpenChange={setSettingsOpen} />
</>
)
}
Notes:
- Collaboration. The
sessionis{ ydoc, provider, user }(CollaborationSessionfrom@skaile/forge-editor/core): create theY.Docand theHocuspocusProviderin an effect (so StrictMode's double mount leaves exactly one provider), destroy the provider in its cleanup, and key the editor by document so a new document is a new Y.Doc (Collaboration on the client). Withoutcollaborationpassvalue/onChangefor a standalone editor (the collaboration and comments features then contribute nothing). - Beside the editor.
EditorToolbar,EditorSettingsModal/EditorSettingsPanel,CommentsMargin,CompositeTable,CompositeInspector,AiSidebar,EditorOutlineandDocumentTreeare components you place; each takes theeditorfromonReady.useEditorUi(editor)returns the chrome of a ready editor (openSettings(tab?),toggleSidebar(),toggleComments(),commentsOpen,togglePalette(),paletteOpen) for your own buttons. - Own features. Pass them as
add={[myFeature()]}(appended after the catalog), or build the preset for anEditorSurfaceyourself:advancedEditor({ add: [myFeature()] }), as in the Nuxt app's kitchen sink (app/pages/kitchen-sink/surface.vue). - The file browser is
DocumentTreefrom@skaile/forge-editor/tree/reactover aDocumentStore; the example wires it to the host's/api. - Backend. The same as for the Vue advanced editor:
/collab/:id, the AI endpoints, assets, users/tags and themes of the Nuxt module and a host adapter (Nuxt module and EditorPorts). A React app with its own server brings the same routes.
Markdown and code
Edit Markdown files as blocks with frontmatter and unknown fences preserved, edit source files in CodeMirror, and pick a surface per file type.
Kitchen sink: learning path
Fifteen pages, basics first: structure, content blocks, JSON and Markdown, block actions and settings, then the building blocks, then the use cases, each with the code that builds it