# Reading these docs as an agent > Machine entry points of this site (llms.txt, markdown twins, sitemap, installable Agent Skill) and where an agent starts. This site is written for people and for coding agents. Every page has a plain Markdown twin, the whole site is available as one file, and an Agent Skill packages the workflow for tools that support skills. ## Machine entry points
What URL
Index of every page with a one-line description /llms.txt
Every page in one Markdown file /llms-full.txt
Markdown twin of a page append .md to the page URL, or GET /raw/.md , for example /raw/ai-agents/invariants.md
Sitemap as Markdown /sitemap.md
Agent Skill index /.well-known/skills/index.json
The skill itself /.well-known/skills/ecsplico-editor/SKILL.md
Install the skill into a skills-aware agent: ```sh npx skills add https://skaile-ai.github.io/forge-editor ``` The site is static, so it has no MCP endpoint. The npm-side entry point is the [`README.md` of the package](https://github.com/skaile-ai/forge-editor/blob/main/packages/editor/README.md); it links back here. ## Where to start Open only the page that matches the task. Every detail lives in the guides and the reference; these pages state the shortest correct path and the invariants.
Task Page
Add the editor to an app: pick the entry, the preset, the surface Integrate the editor
Add an own block or an own feature Add a block or feature
Check what must stay true (CSS order, mandatory context, SSR, bundle isolation, entries) Pitfalls and invariants
Prove the change works Verify
Look up an exported name Public exports
Look up a feature, its requires and its backend Presets and features
Contribute a custom feature ( EditorFeature members) Feature contract
# Add a block or feature > Agent playbook for adding an own block or feature to @skaile/forge-editor: an ordered checklist where each step links to the tutorial chapter that shows it. Two ways to add a block: **Path A** passes `extensions` and a `commands` transformer to `SimpleEditor` (no feature object); **Path B** ships an `EditorFeature` that composes with any preset. The tutorial builds the same „Hinweis" callout both ways; follow the chapters in order and use this list as the checklist. 1. **Node.** `Node.create` in a Vue-free `node.ts`; `group`, `content`, attributes with `parseHTML`/`renderHTML`, commands and a free shortcut. [Tutorial: own block, path A](https://skaile-ai.github.io/forge-editor/tutorial/own-block-path-a) 2. **Block IDs.** `editorBlock: true` if AI operations, flash/reveal, paste de-duplication or the server ID backfill should reach it. [Tutorial: own block, path A](https://skaile-ai.github.io/forge-editor/tutorial/own-block-path-a) 3. **Abilities.** `abilities` on the node for what is allowed inside; for a custom ability id, grant it through the `abilities` prop (`root`). [Tutorial: own feature, path B](https://skaile-ai.github.io/forge-editor/tutorial/own-feature-path-b) 4. **Markdown.** `renderMarkdown`, `markdownTokenName`, `markdownTokenizer`, `parseMarkdown` with syntax no other stage claims; test the round trip (`getMarkdown()` → `parseBlocks`), including multi-block bodies. [Tutorial: own block, path A](https://skaile-ai.github.io/forge-editor/tutorial/own-block-path-a) 5. **View and CSS.** Node view in a separate `index.ts` (`Node.extend`), stylesheet imported after `core.css`. [Tutorial: own block, path A](https://skaile-ai.github.io/forge-editor/tutorial/own-block-path-a), CSS order: [CSS and UI-provider requirements](https://skaile-ai.github.io/forge-editor/reference/css-and-ui) 6. **Path A.** `extensions` (constant) and the `commands` transformer prop (a command with `slash: {}`), nothing else needed. [Tutorial: own block, path A](https://skaile-ai.github.io/forge-editor/tutorial/own-block-path-a) 7. **Path B.** An `EditorFeature`: `extensions`, then `commands` (one `CommandDef` per command with `toolbar` and/or `slash` placement, `creates`, `ability`) and `toolbarGroups` if you need a new group, `blockActions` with your `ability`, `conversions` with a `custom` target; `requires` for features you depend on. [Tutorial: own feature, path B](https://skaile-ai.github.io/forge-editor/tutorial/own-feature-path-b), every member: [Feature contract](https://skaile-ai.github.io/forge-editor/reference/feature-contract) 8. **Compose.** `defineEditor({ features: [...advancedEditor().features, yourFeature()] })` in setup (`advancedEditor` from `/vue/advanced` or `/react/advanced`); mount with `EditorSurface` and pass `abilities` for custom ability ids; add `blockUiFeature()` to a smaller list when you use hover actions. [Tutorial: compose features](https://skaile-ai.github.io/forge-editor/tutorial/compose-features), [Tutorial: own feature, path B](https://skaile-ai.github.io/forge-editor/tutorial/own-feature-path-b) 9. **Server.** When documents with your block are collaborative, pass `YourNode` in the server's `schema` option (`provideEditorServer({ ports, schema: () => [...advancedSchemaExtensions(), YourNode] })`; the collab server uses it instead of `advancedSchemaExtensions()`), or build `getSchema([...advancedSchemaExtensions(), YourNode])` in your own Hocuspocus server. [Tutorial: backend and collaboration](https://skaile-ai.github.io/forge-editor/tutorial/backend-and-collaboration), [Vue-free schema for servers](https://skaile-ai.github.io/forge-editor/reference/advanced-schema) 10. **AI.** Decide what the model should see (`renderMarkdown`) and write; describe the block in `aiContext.blocks` (`id`, `label`, `description`, `syntax`, `example`, matching your Markdown); `aiContext` is additive, a block id may be described only once; add a `custom` conversion provider if the block should be a `convert` target. [Tutorial: AI](https://skaile-ai.github.io/forge-editor/tutorial/ai), [AI guide](https://skaile-ai.github.io/forge-editor/guides/ai) 11. **Bundle proof.** If you ship the block in a package, add a fixture and a `FIXTURES` entry for `bun run check:editor-entries` ([Dependency rules](https://skaile-ai.github.io/forge-editor/reference/dependency-rules)); `bun run check:editor-composition` covers `defineEditor` validation. [Verify](https://skaile-ai.github.io/forge-editor/ai-agents/verify) Then check the [invariants](https://skaile-ai.github.io/forge-editor/ai-agents/invariants). # Integrate the editor > Agent playbook for adding @skaile/forge-editor to an app: choose the surface, then the entry to import, then follow the matching setup page. ## Choose the surface Decide first which host the editor lands in. Each row links to the page that holds the ordered setup steps and the code; do not retype them from memory.
Host Surface and entry Setup
Nuxt app (editor plus backend) AdvancedEditor from @skaile/forge-editor/vue/advanced , module @skaile/forge-editor/nuxt , EditorPorts in a Nitro plugin Quick start: Nuxt
Plain Vue 3 app (Vite), no backend SimpleEditor from @skaile/forge-editor/vue/simple , createEditor() Quick start: Vue
React 19 app SimpleEditor from @skaile/forge-editor/react/simple inside EditorContextProvider ; AdvancedEditor from @skaile/forge-editor/react/advanced Quick start: React , Advanced editor in React
Framework-free (headless, server, validator) /core , the entries without /vue or /react , schema/* , /server Public exports
The Nuxt path in order: install the package and its peers ([Installation](https://skaile-ai.github.io/forge-editor/getting-started/installation)); register the module **after** `@nuxt/ui`; register `EditorPorts` in a Nitro plugin ([EditorPorts guide](https://skaile-ai.github.io/forge-editor/guides/editor-ports)); mount the editor inside `` with the toolbar as a sibling that receives the editor from `ready`. The Nuxt module installs the client plugin with `createEditor`; plain Vue and React hosts create the context themselves. `AdvancedEditor` pulls in every feature. For a smaller bundle use `SimpleEditor` or compose a list yourself. ## Choose a preset or features Which preset or feature list fits the goal, and what each feature needs from the backend: [Composing an editor](https://skaile-ai.github.io/forge-editor/guides/composition) (decision table) and [Presets and features](https://skaile-ai.github.io/forge-editor/reference/presets-and-features) (every feature with `requires` and backend). Own features: [Add a block or feature](https://skaile-ai.github.io/forge-editor/ai-agents/extend). ## Which entry to import Every entry is an explicit subpath of `package.json#exports`; a path that is not listed there does not resolve. Full inventory with every exported name: [Public exports](https://skaile-ai.github.io/forge-editor/reference/exports).
You need Import Side
A ready simple editor component SimpleEditor from @skaile/forge-editor/vue/simple browser
A ready advanced editor component AdvancedEditor from @skaile/forge-editor/vue/advanced browser
A preset to pass to EditorSurface , EditorToolbar , EditorSettingsPanel simpleEditor from .../presets/compose (framework-free, no ⌘K palette), advancedEditor / composeEditor from .../vue/advanced or .../react/advanced (the framework-free assembly behind them is .../presets/compose : composeEditor(ui, options) , CONTENT , INTERACTION , AI ) browser
Vue chrome: createEditor (mandatory Vue app context), EditorSurface , EditorToolbar , EditorSettingsPanel , EditorSettingsModal , EditorCommandPalette (⌘K palette, the core-ui overlay), SettingsSection , useEditorSettings , useEditorUi @skaile/forge-editor/vue browser (Vue)
React chrome: EditorContextProvider (mandatory React context from createEditorContext() ), EditorSurface , EditorToolbar , EditorSettingsPanel , EditorSettingsModal , EditorCommandPalette (⌘K palette, the core-ui overlay), SettingsSection , useEditorSettings , useEditorUi , Toaster , reactNotifier @skaile/forge-editor/react browser (React)
A ready simple editor component for React SimpleEditor from @skaile/forge-editor/react/simple browser (React)
A ready advanced editor component for React AdvancedEditor , advancedEditor from @skaile/forge-editor/react/advanced (needs EditorContextProvider and Toaster ) browser (React)
shadcn token mapping for the feature CSS (React hosts) @skaile/forge-editor/react/styles/ui-vars.css ; for the advanced editor @skaile/forge-editor/react/styles/advanced.css (token mapping, core.css , every feature sheet, themes.css and the theme bridge in one import) browser (React)
Framework-free core: defineEditor , coreContentFeature , coreUiFeature , createEditorContext , command , commandState , parseBlocks , createStore , Notifier , extension-point types ( MenuItem , EditorFeature ) @skaile/forge-editor/core browser (no UI framework)
One feature FeatureCore(ui) , models and types from .../ ; the Vue binding Feature() and components from ...//vue ; the React binding and components from ...//react ; is media , mentions (framework-free only), transcript , authors , comments , composites , ai , collab , block-ui , themes browser
Markdown string as the document splitFrontmatter , joinFrontmatter , markdownToContent(editor, body, { policy }) → { content, diagnostics } (raw HTML, unsafe URLs, unknown fences reported; OpaqueFence keeps unknown ::: kind fences byte for byte), contentToMarkdown(editor) , MarkdownClipboard (paste/copy as Markdown), createSourceMode({ getEditor }) ( state , enter , edit , commit → false and editor untouched on a fatal diagnostic, cancel ) from .../markdown ; MarkdownSurface ( v-model string, YAML frontmatter kept verbatim, props policy and sourceMode ) from .../markdown/vue (React: value / onChange / onDiagnostics , prop policy , no source view, from .../markdown/react ); GFM tables and task lists: tablesFeature() from .../tables (schema .../tables/schema , CSS .../styles/tables.css ) browser ( .../markdown , .../tables also headless)
Edit a code, YAML, JSON or text file CodeSurface from .../code/vue ( v-model , editable , language , wordWrap , emits save , ready ) or .../code/react ( value , onChange , onSave , onReady ); framework-free createCodeEditor , setReadOnly , setWordWrap , setLanguage , loadLanguage , setDocument , formatJsonInEditor , kindOf(path, mime?) , languageOf(path) from .../code . Language packs are yours: registerLanguage('yaml', async () => (await import('@codemirror/lang-yaml')).yaml()) once at startup, id from languageOf(path) . Theme = --sk-* tokens ( .../styles/tokens.css ). Needs @codemirror/{state,view,language,commands,search} and @lezer/highlight installed (optional peers); no ProseMirror/TipTap in the bundle browser
One surface for Markdown and code files DocumentSurface from .../document/vue ( path , v-model , markdown = props of MarkdownSurface incl. preset , binary slot) or .../document/react ( path , value , onChange , markdown , binary(path) ); chooses by kindOf(path, mime) ; remount ( :key / key = path) when the path changes. Reaches ProseMirror and CodeMirror both browser
An integration (framework-free, works on both surfaces) compositeMediaFeature , compositeAiFeature , transcriptAiFeature from .../integrations/composite-media , /composite-ai , /transcript-ai browser
Heading outline, Document tree EditorOutline from .../outline/vue or .../outline/react ( headingOutline , revealBlockInEditor from .../outline ); DocumentTree , useTreeView from .../tree/vue or .../tree/react ; DocumentStore , createTreeView from .../tree browser
Settings type, defaults, validation EditorSettings , EDITOR_SETTINGS_DEFAULTS , parseEditorSettings , editorSettingsSchema from .../settings anywhere
ProseMirror schema for a server, validator or migration simpleSchemaExtensions from .../schema/simple ; advancedSchemaExtensions from .../schema/advanced ; per-feature ...//schema ; base coreSchemaExtensions , ROOT_ABILITIES , BlockId helpers from .../core/schema framework-free, Node-safe
Nitro side: register the ports, forget a deleted document provideEditorServer , useEditorServer ( .forgetDocument , .liveContent , .enforceRateLimit , .resolveModel ) from .../server/nitro , EditorPorts from .../server Nitro only
Server without Nitro: collab server, AI models, rate limiter, route handlers, websocket listener createCollabServer , createAiModels , createRateLimiter from .../server ; chatPost , usersGet , … and their body schemas from .../server/handlers ; listenCollab from .../server/node any Node process
Store documents as JSON files createJsonAdapter({ dir, authenticate?, authorize?, users? }) (an EditorPorts bundle) from @skaile/forge-editor/adapters/json Node
Store documents in SQLite via drizzle (optional peer drizzle-orm ) bootstrapEditorTables(db) , createDrizzleAdapter({ db, tables: editorTables() }) from @skaile/forge-editor/adapters/drizzle Node
Edit a host's Markdown files createForgeAdapter({ content, state, schema, authenticate, … }) from @skaile/forge-editor/adapters/forge Node
Mount the editor backend on Nitro (no Nuxt module) installEditorServer(nitroApp, { ports, collab: {} | false, ai?, rateLimit? }) from @skaile/forge-editor/server/nitro (comments: ports.comments ), called from one Nitro plugin; needs experimental: { websocket: true } for collab ; peer h3 Nitro only
Bind an editor surface to a document useEditorDocument(id, { backend?, collaboration?: boolean | { token? }, user?, canComment? }) → Vue: { modelValue, ready, dirty, save, reload, error, collaboration, comments, editor, bind, dispose } refs from @skaile/forge-editor/adapters/vue ; React: the same as plain values plus onChange from @skaile/forge-editor/adapters/react ; backend defaults to the editor context's (needs createHttpBackend() for collaboration and comments) browser
Comment threads that survive edits captureAnchor / resolveAnchor / mapAnchor from .../anchors ; createJsonCommentStore({ file }) from .../comments/json ; createDrizzleCommentStore({ db, tables: commentTables() }) from .../comments/drizzle ; handleCommentsRequest(store, { method, documentId, body }) from .../comments/server ; the browser side is backend.comments(documentId, …) of createHttpBackend() from .../adapters/http anchors : browser (no UI framework); the others: Node
Nuxt module (config key tiptapEditor ) @skaile/forge-editor/nuxt in modules build
One config for module, server and client defineEditorConfig(input) , serverConfig(config) , publicConfig(config) from .../config (defaults once; installEditorServer(nitroApp, { ports, ...serverConfig(config) }) , createEditor({ ...publicConfig(config) }) ; the Nuxt module options are its input) anywhere
CSS .../styles/core.css (required) plus /media.css , /mentions.css , /tables.css , /authors.css , /comments.css , /transcript.css , /composites.css , /ai.css , /collab.css , /block-ui.css , /themes.css for the features you use browser
Rules that follow from this table: - Import values from the subpath that owns them. The root entry `@skaile/forge-editor` exports types only (`export type`), so `import { createEditor } from '@skaile/forge-editor'` does not compile; use `@skaile/forge-editor/vue` (Vue) or `@skaile/forge-editor/react` (React). `/core` contains no `vue`, `@tiptap/vue-3`, `@nuxt/ui` or React module and no longer exports `createEditor`, `EditorSurface`, `EditorToolbar`, `EditorSettingsPanel`, `EditorSettingsModal`, `SettingsSection`, `useEditorSettings`, `useEditorUi`, `useEditorContext`: those come from `/vue` or `/react`. Feature entries follow the same rule: `/` is framework-free, the components and the `Feature()` binding live in `//vue` and `//react`. A React preset is `defineEditor({ features: [coreContentFeature(), coreUiFeature(), mentionsFeature(), ...] })` with factories from `//react`; features with Vue components are not assignable to it. - Never import a browser entry from server code, and never import `/server` or `/nuxt` from browser code. Servers use the framework-free `schema`/`protocol`/`types` entries (`/ai/protocol`, `/integrations/transcript-ai/protocol`, `/tree/types`, `/settings`). ## After the change Check the [invariants](https://skaile-ai.github.io/forge-editor/ai-agents/invariants) and run the [verification commands](https://skaile-ai.github.io/forge-editor/ai-agents/verify). # Pitfalls and invariants > Rules that must stay true when integrating or changing @skaile/forge-editor: CSS order, mandatory context, SSR, mount-time props, bundle isolation, entries. Check this list before and after every change to an integration. Each bullet is a failure that does not announce itself at compile time. - **CSS order.** `core.css` after Tailwind and Nuxt UI, feature sheets after it, `themes.css` last, and `@source` so Tailwind sees the library. The exact order per host is in [CSS and UI-provider requirements](https://skaile-ai.github.io/forge-editor/reference/css-and-ui). - **Editor context is mandatory.** `EditorSurface`, `SimpleEditor`, `AdvancedEditor` and `EditorToolbar` throw without `app.use(createEditor())` (the Nuxt module does this in its client plugin); the React components throw without ``, and the React host owns (disposes) that context. - **Notices.** Features send user-facing messages through `ctx.notify` / the context's `notify` (`Notifier`, `silentNotifier` drops them). Vue: the queued notifier hands them to Nuxt UI toasts once a surface mounted (needs ``). React: pass `createEditorContext({ notify: reactNotifier })` and mount sonner's `` (exported by `/react`) once; without it no notice is visible. - **UI provider.** Wrap the page in ``; plain Vue also needs `app.use(ui)` (`@nuxt/ui/vue-plugin`) and the `ui({ router: false })` Vite plugin. - **SSR.** Editors are client-only. Nuxt apps in this repository run `ssr: false`; with SSR enabled wrap editors in ``. - **Mount-time props.** The preset (schema, features), `collaboration`, `abilities`, `extensions`, `commands`, `blockActions`, `blockTypes`, `blockUi`, `coreContent`, `documentId` are read once at setup. Change them by remounting with a new `key`. `editable`, `metadata` and the model stay reactive. Build presets in ` ``` ## Register `EditorPorts` The module's API routes call the host's `EditorPorts` (identity first, then one port per capability: `documents`, `collabState`, `directory`, `assets`, `settings`, `themes`, `models`, `comments`). Hand them to the module with `provideEditorServer({ ports })` in a Nitro plugin; the module installs the editor server at the first request, and without provided ports the client logs „Einstellungen konnten nicht geladen werden“ and every route answers 500. A route whose port you leave out answers 501. These minimal ports keep everything in memory and let any visitor in (replace `authenticate` and the storage with yours; [Server ports](https://skaile-ai.github.io/forge-editor/guides/editor-ports) lists every member, the routes that call it and the required results): ```ts [docs/snippets/minimal-ports.ts] // server/plugins/editor-adapter.ts import type { JSONContent } from '@tiptap/core' import { type AssetStore } from '@skaile/forge-editor/server' import { provideEditorServer } from '@skaile/forge-editor/server/nitro' export default defineNitroPlugin(() => { const contents = new Map() const states = new Map() const assets = new Map[1]>() provideEditorServer({ ports: { // `null` = 401 on API routes; on /collab the socket is closed with 4401 (HTTP 401 at the upgrade with `collab.authenticateUpgrade`) identity: { authenticate: async () => ({ id: 'demo', username: 'demo' }) }, documents: { loadContent: async id => contents.get(id) ?? null, storeContent: async (id, content) => { contents.set(id, content) }, listContents: async () => [...contents].map(([id, content]) => ({ id, content })), }, collabState: { loadState: async id => states.get(id) ?? null, storeState: async (id, state) => { states.set(id, state) }, removeState: async (id) => { states.delete(id) }, }, directory: { listUsers: async () => [] }, assets: { storeAsset: async (id, asset) => { assets.set(`${id}/${asset.name}`, asset) }, loadAsset: async (id, name) => { const asset = assets.get(`${id}/${name}`) if (!asset) return null const { data, ...record } = asset return { record, data } }, }, // Omit `settings`: settings stay in the browser (GET answers 204). // Omit `themes`: the themes routes answer 501 and the standard look stays. }, }) }) ``` ## Mount the advanced editor `app.vue` must wrap pages in `` (overlays, toasts, tooltips). The toolbar is a sibling that receives the editor from `ready`: ```vue ``` Without a `collaboration` prop the editor is standalone (JSON `v-model`, own undo history). Collaboration is a mode fixed at mount: pass `:collaboration="{ ydoc, provider, user }"` and connect the provider to `/collab/:id` ([Collaboration](https://skaile-ai.github.io/forge-editor/guides/collaboration)). `AdvancedEditor` pulls in every feature. For a smaller bundle use `SimpleEditor` or compose your own list ([Feature composition](https://skaile-ai.github.io/forge-editor/guides/composition)). For the simple editor in plain Vue (no Nuxt, no backend) follow the [plain Vue quick start](https://skaile-ai.github.io/forge-editor/getting-started/quick-start-vue). ## Next steps The advanced editor, every adapter method, collaboration and AI are covered in the guides: [Server ports](https://skaile-ai.github.io/forge-editor/guides/editor-ports), [Routes and authentication](https://skaile-ai.github.io/forge-editor/guides/routes-and-auth), [Collaboration](https://skaile-ai.github.io/forge-editor/guides/collaboration) and [AI configuration](https://skaile-ai.github.io/forge-editor/guides/ai-configuration). The [tutorial](https://skaile-ai.github.io/forge-editor/tutorial/overview) builds an editor from the simple editor up. # Quick start: React > Set up the simple editor in a Vite and React app: peers, Tailwind and shadcn tokens, EditorContextProvider, EditorSurface and SimpleEditor. The React layer (`@skaile/forge-editor/react`, shadcn/ui on Radix and Tailwind 4) renders the same framework-free core as the Vue layer: the simple editor, `EditorToolbar`, the settings panel and modal, and the `@`, `#` and `/` suggestion popups. It uses no Vue, Nuxt UI or Nuxt module. The runnable reference is [`examples/react-simple`](https://github.com/skaile-ai/forge-editor/tree/main/examples/react-simple); start it with `bun run --cwd examples/react-simple dev` (port 3041). It has the same sample picker, read-only toggle, host `/datum` command and JSON and Markdown panes as the Vue example, plus `@` mentions. **More than the simple editor:** every advanced feature has a React binding as well (`.//react`: media, transcript, Typed blocks, authors, comments, AI, collaboration, block UI, themes, the Block outline and the Document tree), and `composeEditor(options)` / `advancedEditor()` / `AdvancedEditor` from `@skaile/forge-editor/react/advanced` compose them all; see [Advanced editor in React](https://skaile-ai.github.io/forge-editor/guides/advanced-react). `coreContentFeature()`, `coreUiFeature()`, `mentionsFeature()` and the three integrations have no components and work in both layers; a React preset is `defineEditor({ features })` over features from `.//react` and such component-free features. ## Setup - `package.json` dependencies: `@skaile/forge-editor` (`workspace:*` inside this repository), `react`, `react-dom`, `@tiptap/core`, `@tiptap/pm`, `@tiptap/react`, `@iconify/react`, `@iconify-json/lucide`, `tailwindcss`, `tw-animate-css`, `yjs`, `@hocuspocus/provider` (the last two as in the [README](https://skaile-ai.github.io/forge-editor/getting-started/installation): source-shipped shared types reference them); dev dependencies `vite`, `@tailwindcss/vite`, `@vitejs/plugin-react`, `typescript`, `@types/react`, `@types/react-dom`. Use the exact version ranges of the example's [`package.json`](https://github.com/skaile-ai/forge-editor/blob/main/examples/react-simple/package.json). - `index.html` with `
` and ``. - `tsconfig.json` with `jsx: "react-jsx"`, `moduleResolution: "Bundler"`, `strict` and `include: ["src"]` (see the example's [`tsconfig.json`](https://github.com/skaile-ai/forge-editor/blob/main/examples/react-simple/tsconfig.json)). `vite.config.ts`: ```ts import tailwindcss from '@tailwindcss/vite' import react from '@vitejs/plugin-react' import { defineConfig } from 'vite' export default defineConfig({ plugins: [react(), tailwindcss()] }) ``` `index.css` — Tailwind, the animation utilities the shadcn dialog, dropdown and tooltip use, the library sheets, and a `@source` so Tailwind generates the utility classes the library's React code uses (Tailwind skips `node_modules` unless told otherwise): ```css @import "tailwindcss"; @import "tw-animate-css"; /* ui-vars.css maps the shadcn tokens (--primary, --background, …) to the --ui-* variables the feature sheets read. */ @import "@skaile/forge-editor/react/styles/ui-vars.css"; @import "@skaile/forge-editor/styles/core.css"; @import "@skaile/forge-editor/styles/mentions.css"; /* the sheet of each feature you use */ @source "../node_modules/@skaile/forge-editor/dist"; /* relative path from this CSS file to the installed package's dist */ /* The shadcn theme block follows: @custom-variant dark, @theme inline, :root and .dark tokens, @layer base. `bunx shadcn@latest init` generates it; examples/react-simple/src/index.css has a complete copy. */ ``` The `@source` covers the whole `dist`, not just `dist/react`: the Features' own React components (`features/*/react`, e.g. the image studio dialog), the surfaces (`document`, `markdown`, `code`), the outline and the tree sit beside it, and a host that scans only `dist/react` renders them without their layout classes. In this repository the examples point at the sibling's `src` (`../../../packages/editor/src`) instead. `main.tsx` — icons and the editor context. The toolbar and menus render `i-lucide-*` names through `@iconify/react`; preloading the collection means the page makes no request to the Iconify API. The host owns the context from `createEditorContext()` (`@skaile/forge-editor/core`) and every React component of the library reads it from `EditorContextProvider`; the provider never disposes it, call `context.dispose()` yourself when the context outlives your app. `createEditorContext` takes the same options as the Vue `createEditor` (the default backend syncs the settings with `GET`/`PUT /api/editor/settings`; `{ backend: createMemoryBackend() }` keeps them local, `{ backend: createHttpBackend({ fetch }) }` brings your own HTTP client): ```tsx import lucide from '@iconify-json/lucide/icons.json' import { addCollection } from '@iconify/react' import { createEditorContext } from '@skaile/forge-editor/core' import { EditorContextProvider } from '@skaile/forge-editor/react' import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { App } from './App' import './index.css' addCollection(lucide) const context = createEditorContext() createRoot(document.getElementById('app')!).render( , ) ``` Notices of app-level models (for example the AI model profiles) go through the context's notifier. The simple editor raises none, so `createEditorContext()` is enough here; hosts of features pass `createEditorContext({ notify: reactNotifier })` and mount sonner's `Toaster` once (both from `@skaile/forge-editor/react`, see [Advanced editor in React](https://skaile-ai.github.io/forge-editor/guides/advanced-react)). ## Mount the editor `EditorSurface` takes a preset; `EditorToolbar` is a sibling that works on the editor you pass it (from `onReady`), never on a global active editor. The toolbar renders the groups of the preset registered for that editor; without an editor it renders none. ```tsx import { coreContentFeature, coreUiFeature, defineEditor } from '@skaile/forge-editor/core' import { mentionsFeature } from '@skaile/forge-editor/mentions' import { EditorSurface, EditorToolbar } from '@skaile/forge-editor/react' import type { Editor, JSONContent } from '@tiptap/core' import { useState } from 'react' // Module constants: the preset, `metadata`, `abilities`, `extensions` and `commands` are read once at mount. const preset = defineEditor({ features: [coreContentFeature(), coreUiFeature(), mentionsFeature()] }) const metadata = { mentions: [{ id: 'u1', label: 'Ada Lovelace' }, { id: 'u2', label: 'Alan Turing' }] } export function App() { const [editor, setEditor] = useState(null) const [content, setContent] = useState({ type: 'doc', content: [{ type: 'paragraph' }] }) return ( <> console.warn(error.message)} /> ) } ``` Typing `@Ad` opens the mention popup with the `mentions` list. A list the host does not supply (here `hashtags`) comes from `/api/editor/tags` through the context's HTTP client, and only once the `#` popup is used. The simple editor's `/` menu lists the core blocks; pass `commands={defaults => [...defaults, ...]}` (a module function) to add own entries. ## The one-liner: `SimpleEditor` Without own features, `SimpleEditor` from `@skaile/forge-editor/react/simple` builds the preset itself (`core-content` + `core-ui`, plus the features in its `add` prop) on an `EditorSurface`. The toolbar takes its groups from the editor it is given; the ref exposes `{ editor, openSettings, preset }`: ```tsx import { EditorToolbar } from '@skaile/forge-editor/react' import { SimpleEditor } from '@skaile/forge-editor/react/simple' import type { Editor } from '@tiptap/core' import { useState } from 'react' export function App() { const [editor, setEditor] = useState(null) return ( <> ) } ``` Try the React example here: ## Next steps The [tutorial](https://skaile-ai.github.io/forge-editor/tutorial/react-surface) shows the same editor on the React surface. The advanced editor on React is covered in [Advanced editor in React](https://skaile-ai.github.io/forge-editor/guides/advanced-react); every entry is listed in the [exports reference](https://skaile-ai.github.io/forge-editor/reference/exports). # Quick start: plain Vue > Set up the simple editor in a Vite and Vue app: peers, Nuxt UI plugin, CSS and a mounted SimpleEditor with toolbar and outline. `SimpleEditor` is the two core features alone (`core-content` and `core-ui`): bold/italic/underline/strike/code/link, headings, lists, quotes, code blocks, horizontal rule, undo/redo. No backend requests, no Yjs, no feature code. The runnable reference is [`examples/vue-simple`](https://github.com/skaile-ai/forge-editor/tree/main/examples/vue-simple); start it with `bun run --cwd examples/vue-simple dev` (port 3040). Its sample picker loads five demo documents — „Start“, „Formatierung“ (marks, lists, quote, code block, rule), „Gliederung“ (three outline levels), „Hinweise“ (the own „Hinweis“ block) and „Ungültig (Block-JSON)“ (advanced JSON, rejected through `contentError`) — beside the toolbar and outline, with the JSON and Markdown of the document next to the editor. The `/hinweis` entry and the callout behind it are an own block added through the `extensions` and `commands` props; the [tutorial](https://skaile-ai.github.io/forge-editor/tutorial/own-block-path-a) shows how, and how to go further with a full feature. ## Setup Starting a new app from nothing? The files below plus these three are all that [`examples/vue-simple`](https://github.com/skaile-ai/forge-editor/tree/main/examples/vue-simple) has: - `package.json` dependencies: `@skaile/forge-editor` (`workspace:*` inside this repository), `vue`, `@nuxt/ui`, `tailwindcss`, `@iconify-json/lucide`, `@tiptap/core`, `@tiptap/pm`, `@tiptap/vue-3`, `yjs`, `@hocuspocus/provider`; dev dependencies `vite`, `@vitejs/plugin-vue`, `typescript`, `vue-tsc`; scripts `dev` (`vite`), `build` (`vite build`), `typecheck` (`vue-tsc --noEmit -p tsconfig.json`). Use the exact version ranges of the example's [`package.json`](https://github.com/skaile-ai/forge-editor/blob/main/examples/vue-simple/package.json). - `index.html` with `
` and ``. - `tsconfig.json` with `moduleResolution: "Bundler"`, `types: ["vite/client"]`, `strict`, `verbatimModuleSyntax` and `include: ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]` (see the example's [`tsconfig.json`](https://github.com/skaile-ai/forge-editor/blob/main/examples/vue-simple/tsconfig.json)). Optional: Nuxt UI resolves `i-lucide-*` icons at build time, but a workspace library lies outside the Vite root it scans, so without the extra config the page fetches the toolbar's icons from the Iconify API at runtime. The example's [`vite.config.ts`](https://github.com/skaile-ai/forge-editor/blob/main/examples/vue-simple/vite.config.ts) collects the library's icon names into `ui({ icon: { clientBundle: { scan: true, icons } } })` to bundle them. `vite.config.ts` — Nuxt UI's Vite plugin without a router: ```ts import ui from '@nuxt/ui/vite' import vue from '@vitejs/plugin-vue' import { defineConfig } from 'vite' export default defineConfig({ plugins: [vue(), ui({ router: false })] }) ``` `main.ts` — Nuxt UI and the editor context. `createEditor()` (from the Vue layer, `@skaile/forge-editor/vue`) owns the settings of this app and is required by every editor component: ```ts import ui from '@nuxt/ui/vue-plugin' import { createEditor } from '@skaile/forge-editor/vue' import { createApp } from 'vue' import App from './App.vue' import './style.css' createApp(App).use(ui).use(createEditor()).mount('#app') ``` `createEditor()` loads and saves the settings per account through its backend: with the default backend (`createFetchBackend`, the HTTP routes of the Nuxt module over the platform `fetch`) the first `useEditorSettings()` asks `GET /api/editor/settings` and every change is sent with `PUT`. They are local-only (`localStorage`, `tiptap-editor:settings`) when the backend offers no `settings` capability: `createEditor({ backend: createMemoryBackend() })`. `createEditor({ backend })` swaps the whole data layer of all features (documents, uploads, mention lookups, settings, themes, AI); `createHttpBackend({ fetch })` (`@skaile/forge-editor/adapters/http`) lets a host hand in its own HTTP client such as Nuxt's `$fetch`. `createEditor({ locale: 'en' })` switches every string the editor renders to English (default `'de'`), `createEditor({ messages: { 'toolbar.bold': 'Strong' } })` overrides single messages; see [Language](https://skaile-ai.github.io/forge-editor/guides/language). Components read `useEditorI18n()` (`{ t, locale }`); the Nuxt module takes the same two options as `tiptapEditor: { locale, messages }`. `createEditor()` is a thin Vue plugin. The headless core (`@skaile/forge-editor/core`) imports no Vue: `createEditorContext(options)` returns the framework-free context with the same options, whose `settings` is a store (`get()`, `set(value)`, `update(recipe)`, `subscribe(listener)`) and whose `ready` and `synced` are read-only stores. `createEditor()` creates that context, provides it to the app and disposes it on unmount; Vue components read the settings through `useEditorSettings()` (reactive refs) from `@skaile/forge-editor/vue`. `style.css` — Tailwind, Nuxt UI, then the core CSS, and let Tailwind scan the library's whole `dist` (the Features' `features/*/components` sit beside `vue`; in this repository the examples scan the sibling's `src` instead): ```css @import "tailwindcss"; @import "@nuxt/ui"; @import "@skaile/forge-editor/styles/core.css"; @source "../node_modules/@skaile/forge-editor/dist"; /* relative path from this CSS file to the installed package's dist */ ``` `App.vue` wraps everything in `` (overlays, tooltips, toasts). ## Mount the editor ```vue ``` Try it here: type, use the toolbar, switch to read-only and watch the JSON `v-model` and the outline. ## Next steps The [tutorial](https://skaile-ai.github.io/forge-editor/tutorial/simple-editor) continues from this editor; the props and events of `SimpleEditor` are covered there. To add features such as mentions, media, tables and block UI, see [Compose features](https://skaile-ai.github.io/forge-editor/tutorial/compose-features). # 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 The kitchen sink is the part of the repository's Nuxt app that teaches the framework by example. Start the app with `bun run dev` (port 3056; it needs `NUXT_SESSION_PASSWORD`, at least 32 characters, in `.env`), open `http://localhost:3056/kitchen-sink` and pick a card. The gallery has three tracks, and this guide has one section per page in the same order:
Track Pages
Basics: what a document is made of, how it is structured, stored and acted on Flat & Structure , Content blocks , JSON & Markdown , Acting on blocks , Editor settings
Building blocks: one API of the library, shown on its own Simple , Advanced , Surface
Use cases: a planned application, built from the framework Minutes , Slides , Databases , Science , Podcast , PKM , Epikrise
Read a page in the browser and the matching section here side by side. Every page is one Vue file, `app/pages/kitchen-sink/.vue`, and the snippet in each section is that file's ` ``` ## Blocks What a document is made of, in the words of the [glossary](https://skaile-ai.github.io/forge-editor/getting-started/glossary): - **Document**: the stored unit a user opens; it holds Blocks but is not itself a Block. - **Block**: an addressable unit of a Document's content, either a Simple block or a Container block. - **Simple block**: holds no other Blocks: paragraph, heading, list, quote, code block, media. The items of a list belong to the list. - **Container block**: holds other Blocks, each addressable and movable on its own. - **Typed block**: a Container block with a Block type: title, description, tags, then a body of Blocks. - **Block type**: the exact kind of a Typed block (Section, Task, Note, …); it defines the Property definitions, the Accepted children, the Source media types and the AI brief ([Block types in code and JSON](https://skaile-ai.github.io/forge-editor/guides/block-types)). - **Block properties** and **Property definition**: the values a Block carries (status, due date) and the declaration of one of them (key, label, value type) ([Block properties and Property definitions](https://skaile-ai.github.io/forge-editor/guides/block-properties)). - **Block capabilities**, **Block actions**, **Ability**: what a Block supports, what a user can run on it, and what may be done at one place in the Document ([Block capabilities, abilities and actions](https://skaile-ai.github.io/forge-editor/guides/block-capabilities)). `defaultBlockTypes(t)` returns the twelve built-in Block types: `section`, `task`, `note`, `document`, `recording`, `paper`, `book`, `figure`, `rendition`, `collection`, `dataView` and `folder`, labelled in the editor's language. The page appends the host's own `decisionBlockType` (`app/editor/kitchen-sink/decision.ts`). - **The tour sample.** „Block tour“ introduces each step with a paragraph: Simple blocks (paragraph, heading, list, quote, code block), a `section` Typed block with tags and a nested section, a `task` with properties next to a `note` without any (its Block type defines none), and the host's `decision` with its status. - **Panel.** `CompositeInspector` shows the selected block: its Block type, title, description, tags and properties. `CompositeTable` lists every Typed block of the document as a row, with filters by Block type and tag. Both read the same editor ([Composites and the settings UI](https://skaile-ai.github.io/forge-editor/guides/composites)). ```vue [app/pages/kitchen-sink/blocks.vue] ``` ## JSON and Markdown Document in, document out. The stored form of a document is TipTap JSON: the page hands it to the editor through `v-model` and gets every change back as JSON. The editor gives every block an `id` attribute when it loads the document; look for `attrs.id` in the JSON panel. - **Markdown out.** `editor.getMarkdown()` writes the document as Markdown. Simple blocks and marks become plain Markdown (`**bold**`, `++underline++`, lists, quotes, fenced code); a Typed block becomes a `::: ` fence with its properties as JSON, its title as `## …`, its description as a quote and its tags as a `Tags:` line. Markdown keeps a block id only where something in the document points at it ([Markdown and code](https://skaile-ai.github.io/forge-editor/guides/markdown-and-code)). - **Markdown in.** `parseBlocks(editor, markdown)` from `@skaile/forge-editor/core` runs the block pipeline a file import uses: `parsed` is the whole document as JSON, `blocks` the blocks to insert at the caret. **Load Markdown** in the header replaces the document with `parsed` of a release note written as Markdown. - **The round trip.** The panel writes the document as Markdown, reads it back and compares the result with the document, block ids and the trailing empty paragraph left out (`app/editor/kitchen-sink/round-trip.ts`). „Release note (JSON)“ comes back unchanged; „Lossy: author marks“ does not, because Markdown has no syntax for who wrote a text, and the panel names the first place that differs. - **JSON the editor refuses.** „Invalid: unknown block“ holds a node the schema does not know. The editor keeps its last valid document and emits `contentError`; the page shows the message. ```vue [app/pages/kitchen-sink/json-markdown.vue] ``` ## Actions Commands and block actions are two ways to act on a document. A command (the `/` menu, the toolbar) inserts or formats at the caret; a block action acts on a block that is already there. The drag handle on the left of a block moves it. - **Where block actions appear.** On the hover bar of the block under the pointer, on the toolbar of the block holding the selection, or both: `blockUi.actions` (`'hover'`, `'toolbar'` or `'both'`; [Block UI](https://skaile-ai.github.io/forge-editor/guides/block-ui)). The editor reads `blockUi` once, so the Hover/Toolbar/Both switch in the header remounts it with `:key`; the document stays in `v-model`. - **Own actions.** `blockActions` adds the page's three: „Copy block id“ (`run`, `readOnly: true`, so it stays in a read-only editor), „Insert after“ (`menu`: a dropdown of a paragraph or a divider) and „Count blocks inside“ (`requires: ['container']`, `target: 'typedBlock'`: only on Typed blocks, and on the toolbar also while the caret is in such a block's body). - **Who decides.** Whether an action is offered is decided per block: its `ability` at that place in the document, the Block capabilities it `requires`, and `readOnly` in a read-only editor ([Block capabilities, abilities and actions](https://skaile-ai.github.io/forge-editor/guides/block-capabilities)). The page's `abilities` rule revokes `block.delete` and `block.duplicate` inside every `blockquote`, so the quote's paragraph has no Delete and no Duplicate. Turn off „editable“ and only the reading actions are left. - **Panel.** The action log lists the last runs of the page's own actions with the block they ran on, above the document as Markdown. ```vue [app/pages/kitchen-sink/actions.vue] ``` ## Settings One settings object per app, read by every editor and stored per user ([Settings](https://skaile-ai.github.io/forge-editor/guides/settings)). `useEditorSettings()` returns it as a Vue ref with two flags: `ready` (the stored settings are usable) and `synced` (the account's settings from the server are in effect). Assigning to the ref persists the change; every editor of the app follows it, the Flat & Structure switch included. - **Panel.** `EditorSettingsPanel` sits inline here instead of in the popup. Its tabs (Editing, Author colors, Comments, Theme, AI) are the ones the editor's features contribute; an editor without a feature has no tab for it. Below the panel: the live settings object as JSON, and the keys whose value differs from `EDITOR_SETTINGS_DEFAULTS`. - **Reset.** The button in the header assigns `EDITOR_SETTINGS_DEFAULTS` back, the theme kept. It resets the settings of every editor of the app, not only of this page. - **The tour sample.** „Settings tour“ has a place to try each Editing setting: headings for the „Convert structure“ scope, a Typed block for the block type icon and the drag handle, a long section for the split target size, and a task to type in with author colors on. ```vue [app/pages/kitchen-sink/settings.vue] ``` ## Simple The smallest editor, with the AI sidebar and the selection actions switched on. - **SimpleEditor.** Core blocks only: marks, headings, lists, quotes, code ([Chapter 2: A simple editor](https://skaile-ai.github.io/forge-editor/tutorial/simple-editor)). - **AI through add.** The simple bundle holds no catalog feature, so `aiFeature()` comes in through the `add` prop; it brings the AI sidebar and the selection actions ([AI](https://skaile-ai.github.io/forge-editor/guides/ai), [AI configuration and secrets](https://skaile-ai.github.io/forge-editor/guides/ai-configuration)). - **The same page shell.** Sample select, settings popup and output panes are the ones of the bigger pages. ```vue [app/pages/kitchen-sink/simple.vue] ``` ## Advanced `AdvancedEditor` with its host props, one prop per extension point. - **The preset.** `AdvancedEditor` is the advanced composition behind one component ([Feature composition and entries](https://skaile-ai.github.io/forge-editor/guides/composition)). - **Own block type.** `blockTypes` is `defaultBlockTypes(t)` plus `decisionBlockType`; it shows in the `/` menu, the block badge and the toolbar's Typed block dropdown ([Block types in code and JSON](https://skaile-ai.github.io/forge-editor/guides/block-types)). - **Own command and action.** `commands` receives the default `/` commands and returns them with `/date` appended; `blockActions` adds a hover action gated by the `block.delete` ability ([Block capabilities, abilities and actions](https://skaile-ai.github.io/forge-editor/guides/block-capabilities), [Block UI](https://skaile-ai.github.io/forge-editor/guides/block-ui)). - **Abilities and metadata.** `abilities` revokes `ai.rewrite` inside bullet lists; `metadata` provides hashtags and speakers, and `@metadata-create` logs a newly created item. - **Composition props.** `remove: ['tables']` leaves tables out of the composition and of the schema the samples must fit; `settings-ui="external"` makes the editor emit `settingsOpen` for the page to place the popup ([Composites and the settings UI](https://skaile-ai.github.io/forge-editor/guides/composites)). ```vue [app/pages/kitchen-sink/advanced.vue] ``` ## Surface `EditorSurface` on an explicit composition, with a feature of your own. - **A preset handed to the surface.** `advancedEditor({ blockTypes, add: [calloutFeature()] })` builds the advanced composition plus the host's feature; `EditorSurface` mounts it. Call it in ` ``` ## Minutes Meeting minutes from a transcript: the core scenario of the framework. The sample „Weekly sync“ has speaker segments, a task and a decision; „Status report“ has sections with nested notes. - **Metadata lists.** The page passes `metadata` with `speakers` (the speaker picker of a transcript segment and the `@` menu) and `hashtags` (the `#` menu). Constants, not template literals: a new object on every render would replace the lists each time. See [Media, mentions and transcript features](https://skaile-ai.github.io/forge-editor/guides/media-mentions-transcript). - **Transcript segments.** One paragraph per speaker turn, from the transcript feature of the advanced composition ([Feature composition and entries](https://skaile-ai.github.io/forge-editor/guides/composition)). - **Typed blocks with properties.** `task` is a built-in Block type; `decision` is the host's own, defined as data in `app/editor/kitchen-sink/decision.ts` (a `status` select property) and appended to `defaultBlockTypes(t)` ([Block types in code and JSON](https://skaile-ai.github.io/forge-editor/guides/block-types), [Block properties and Property definitions](https://skaile-ai.github.io/forge-editor/guides/block-properties)). - **Panel.** `EditorOutline` and `CompositeTable` read the same editor: the heading outline, and every Typed block as a row ([Composites and the settings UI](https://skaile-ai.github.io/forge-editor/guides/composites)). ```vue [app/pages/kitchen-sink/minutes.vue] ``` ## Slides A deck written as blocks, with a preview of each slide and render targets for reveal.js, PowerPoint and Marp. - **Block types from an add-on.** `slideBlockTypes(locale)` from `@skaile/forge-editor-slides/client` adds `deck` and `slide`; the page appends them to `defaultBlockTypes(t)`. The „Kickoff deck“ sample has a deck with six slides in different layouts ([Slides: decks, slide layouts and exports](https://skaile-ai.github.io/forge-editor/guides/slides)). - **A feature from the add-on.** `slidesFeature()` from `@skaile/forge-editor-slides/vue` is passed through `add`; it contributes the slide preview section of the inspector and the slide hover actions. - **Transfer transforms.** `interaction` is `[...INTERACTION.full, 'transfer']` and `transfer.extraTransforms` is `slidesTransforms({ locale })`. The „Outline to deck“ sample is plain headings and bullets to drag into a deck with „Insert as slides“ ([Feature composition and entries](https://skaile-ai.github.io/forge-editor/guides/composition)). - **Render targets.** Select the deck block, open its hover menu and choose „Render as…“. The targets live on the server; without `NUXT_PUBLIC_ADDONS_SLIDES=1` the panel shows a `ServerHint` ([Render: export a Block or a Document to a file](https://skaile-ai.github.io/forge-editor/guides/render)). ```vue [app/pages/kitchen-sink/slides.vue] ``` ## Databases Tasks as rows, queried into tables, boards, collections and charts. - **DataQuery.** A `dataView` block stores a query (`source`, `sorts`, `group`) and a layout; it holds no rows. The „Task tracker“ sample has eight `task` blocks, a table view sorted by due date and a board grouped by status ([Data views: a query shown as table, list or board](https://skaile-ai.github.io/forge-editor/guides/data-views), [Query: filter, sort, group and total Blocks](https://skaile-ai.github.io/forge-editor/guides/query)). - **No index needed.** A data view or chart asks the backend's Block index and answers from the open document only when the backend has none. The standalone editor's blocks are in no index, so the page provides its editor a copy of the context whose backend has no `index`; everything else stays the app's ([Workspace: find, read, reference and embed Blocks across Documents](https://skaile-ai.github.io/forge-editor/guides/workspace)). - **Collections.** The „Collection“ sample lists three tasks by block id; the sample gives those tasks fixed ids so the references resolve ([Collections: an ordered list of Blocks](https://skaile-ai.github.io/forge-editor/guides/collections)). - **Charts.** `chartBlockTypes(locale)` and `chartNodeViews` come from the charts add-on's `/client` and `/vue` entries, so charts draw in the browser whatever `NUXT_PUBLIC_ADDONS_CHARTS` says; the flag adds the server's SVG export, and the panel shows a `ServerHint` without it ([Charts: bar, line, pie and scatter over a query](https://skaile-ai.github.io/forge-editor/guides/charts)). ```vue [app/pages/kitchen-sink/databases.vue] ``` ## Science Papers with a bibliography, inline citations, quotes with source anchors and PDF output. - **A bibliography in a property.** A `paper` block keeps one CSL-JSON item in `meta.bib` (two references are two blocks). The „Paper notes“ sample computes the citation keys with `bibtexKey` instead of writing them by hand ([Citations](https://skaile-ai.github.io/forge-editor/guides/citations)). - **Cite nodes.** `cite` nodes (insert with `/cite` or type `[cite:`) name an entry by key and show a short label such as „(Vaswani et al. 2017)“, resolved against the bibliography ([Citations](https://skaile-ai.github.io/forge-editor/guides/citations)). - **Quotes and source anchors.** `quoteBlockTypes(locale)` from `@skaile/forge-editor-pdf-viewer` adds the `quote` type, `pdfViewerFeature()` from its `/vue` entry the viewer in the inspector ([PDF viewer: passages, anchors and quotes](https://skaile-ai.github.io/forge-editor/guides/pdf-viewer)). - **PDF output.** The `typst-pdf` render target is server-side. The page waits for `ctx.backend.ready`, lists the targets and shows a `ServerHint` for `NUXT_PUBLIC_ADDONS_TYPST` when it is missing ([Typst: render a Document as a PDF with citations](https://skaile-ai.github.io/forge-editor/guides/typst), [Render: export a Block or a Document to a file](https://skaile-ai.github.io/forge-editor/guides/render)). ```vue [app/pages/kitchen-sink/science.vue] ``` ## Podcast Recordings with their transcripts and a script, and speech synthesis and mixdown as block actions. - **Recordings and renditions.** `recording` and `rendition` are built-in types; a transcript is a rendition nested in its recording. The „Episode“ sample has no audio file, since the page is about the structure ([Derive methods and the Derivation index](https://skaile-ai.github.io/forge-editor/guides/derive-methods), [Media, mentions and transcript features](https://skaile-ai.github.io/forge-editor/guides/media-mentions-transcript)). - **Speaker segments.** `metadata.speakers` offers the two hosts in the speaker picker of each segment. - **Audio actions.** `audioFeature({ mixdownMode: 'concat' })` from `@skaile/forge-editor-audio` adds two derive methods to the block actions: read a text block aloud into a new recording, and mix several recordings into one. Both record a Derivation, so a mixdown is marked outdated when one of its takes changes ([Audio: read aloud, mixdown and ElevenLabs](https://skaile-ai.github.io/forge-editor/guides/audio)). - **Server ports.** Speech needs the server's `speech` port (`NUXT_PUBLIC_ADDONS_AUDIO=1` and `ELEVENLABS_API_KEY`; the mixdown needs ffmpeg). The page waits for `ctx.backend.ready`, tests `ctx.backend.speech` and shows a `ServerHint` ([Speech: text to audio, and subtitle files](https://skaile-ai.github.io/forge-editor/guides/speech)). ```vue [app/pages/kitchen-sink/podcast.vue] ``` ## PKM A knowledge base in one document: a folder of tagged notes that link to each other. - **Folders and notes.** `folder` and `note` are built-in Typed blocks; the „Vault“ sample nests notes in a folder ([Block types in code and JSON](https://skaile-ai.github.io/forge-editor/guides/block-types)). - **Tags.** `metadata.hashtags` feeds the `#` menu ([Media, mentions and transcript features](https://skaile-ai.github.io/forge-editor/guides/media-mentions-transcript)). - **References.** `[@` inserts a reference chip to another block of the same document; the sample gives its targets fixed ids ([Workspace: find, read, reference and embed Blocks across Documents](https://skaile-ai.github.io/forge-editor/guides/workspace)). - **What the index adds.** The `explore` interaction (backlinks, related notes, the trail) is left out on purpose: it needs the workspace index, which a standalone editor does not have. [Explore](https://skaile-ai.github.io/forge-editor/guides/explore), [Edges](https://skaile-ai.github.io/forge-editor/guides/edges), [Graph](https://skaile-ai.github.io/forge-editor/guides/graph) and [Knowledge graph](https://skaile-ai.github.io/forge-editor/guides/knowledge-graph) show what a workspace adds. ```vue [app/pages/kitchen-sink/pkm.vue] ``` ## Epikrise Epikrise comes last because it is the only page whose state lives on the server: the review records are kept by block id. A discharge report built from domain block types, each with its own AI brief and a review record per block. - **Domain block types.** `EPIKRISE_KINDS` in `app/editor/epikrise/kinds.ts` are Block types as data: properties, placeholders and an `ai-brief` capability per kind. The UI labels are English; the `purpose` and `instructions` prompt texts stay German because the clinical sample is German ([Block types in code and JSON](https://skaile-ai.github.io/forge-editor/guides/block-types), [Block capabilities, abilities and actions](https://skaile-ai.github.io/forge-editor/guides/block-capabilities)). - **AI brief and data.** The `ai-brief` config tells „Fill block (AI)“ what a block is for; `epikriseFeature()` supplies example paragraphs through `aiBlockData` for the reason-for-admission type ([AI](https://skaile-ai.github.io/forge-editor/guides/ai)). - **Prüfung.** The page lists the `pruefung` capability on every Epikrise kind and on the built-in `section` type (editorial check, then medical check; the medical check locks the block), installs the feature with `with: ['pruefung']` and grants `ABILITY.pruefungRecord` at document root. `document-id="kitchen-sink-epikrise"` gives the server port an id to keep the records under, so they survive a reload; the panel states this ([Prüfung and Freigabestatus](https://skaile-ai.github.io/forge-editor/guides/pruefung)). - **Panel.** `CompositeInspector` beside the editor shows the properties and details of the selected block. ```vue [app/pages/kitchen-sink/epikrise.vue] ``` ## Beyond the kitchen sink The kitchen sink keeps every page standalone, so some features are shown elsewhere in the app or only in their guide:
Feature Where to see it
Collaboration ( collaboration prop, Hocuspocus session) app/pages/documents/[id].vue , Collaboration
Comments and authors ( CommentsMargin , v-model:comments-open ) the same document page, Authors and comments
Explore and search ( explore , ExplorePanel , SearchPanel ) app/layouts/default.vue , Explore
Templates ( templatesOf , TEMPLATES_FOLDER ) app/pages/documents/index.vue , Templates
Themes ( appearance.theme , the Theme tab) the settings popup of any page, Themes
The editor-owned settings modal ( settings-ui="internal" , the default; the kitchen sink places its settings itself with settings-ui="external" ) the document page, Settings ; the settings object itself ( useEditorSettings , EDITOR_SETTINGS_DEFAULTS , EditorSettingsPanel ) has its own page, Editor settings
UI kit ( createEditor({ kit }) ) and the module options Nuxt module
Export check Export check
Obsidian vaults, image studio, graph, Typst Obsidian , Image studio , Graph , Typst
## The rest of the app The repository's Nuxt app is the runnable advanced reference: documents with real-time collaboration, the file drawer, AI, and the kitchen sink above. It has no login: a visitor acts as one of the demo users `anna`, `ben` and `bernd` (`shared/utils/demo-users.ts`), starts as `anna` and switches in the user menu top right („Benutzer wechseln“, which reloads the page under the new user). Start it with `bun run dev` (port 3056); it needs `NUXT_SESSION_PASSWORD` (at least 32 characters, for the `nuxt-auth-utils` session cookie) and, for AI, the API key variables below in `.env`. **Walkthrough** 1. Open `http://localhost:3056/`; you are `anna` and land on `/documents`, the app's home: the file drawer on the left lists the documents and the folders, the main area reads „Kein Dokument ausgewählt“. A fresh store (no `.data/documents` yet, or `NUXT_DOCUMENTS_DIR` pointing at a missing directory) starts with the folder „Blocktypen“: an overview and one document per set of block types (text blocks, inline elements and links, media and transcript, sections/tasks/notes, sources, collections/views/folders, the host's own Prüfabschnitt), each explaining its blocks on a working example. They are ordinary documents; to get them back, stop the server and remove the store directory. 2. Press „Neues Dokument“ (in the main area, or „Hinzufügen“ → „Neues Dokument“ on a drawer row) to create a document; the app opens `/documents/` and the new document appears in the drawer. The drawer sorts the documents of a folder by title, numbers by value, and the folders by name; editing a document does not move it. In the drawer you can also create folders, drag documents and folders between folders, rename, filter and delete with undo ([Document tree](https://skaile-ai.github.io/forge-editor/guides/document-tree)); each document row has an export button that downloads a `.cdoc` package. 3. Type. Use the toolbar, the `/` menu, the drag handles and the hover block actions; the AI sidebar (the toolbar's AI button or `Mod-Shift-k`) and the settings cog in the header open their host-placed UI. 4. Open the same URL in a second browser tab, or as another user in a second browser (or a private window) where you switch to `ben` in the user menu; the session cookie is per browser, so tabs of one browser share the user. Text and carets appear in both within a moment and the header badge reads „Synchronisiert“; each tab keeps its own undo history. Text carries its author (switch the colouring on with the toolbar's palette button), and a selection can be commented („Kommentar“, `Mod-Shift-m` for the margin) with replies, resolve and reopen visible to both. 5. Optional: press „Kitchen sink“ at the top of the drawer (`/kitchen-sink`) for the gallery of the fifteen pages above, in three tracks: Basics, Building blocks, Use cases. The layout has no document tree: a navigation on the left lists the pages by track, a link back to „Dokumente“ and the standalone examples (`examples/*`, each its own dev server). Open the settings popup and pick the Theme tab („Theme“) to try the Themes: the whole app, header and navigation included, switches look. **Optional AI configuration.** The editor works without AI credentials; AI requests then fail with the server's message. Keys come from the environment only (see [AI configuration and secrets](https://skaile-ai.github.io/forge-editor/guides/ai-configuration)): `NUXT_TIPTAP_EDITOR_AI_API_KEY` (shared fallback for every Model preset), `NUXT_TIPTAP_EDITOR_AI_PRESETS__API_KEY` (one Model preset; `flashLite` → `NUXT_TIPTAP_EDITOR_AI_PRESETS_FLASH_LITE_API_KEY`) and `NUXT_TIPTAP_EDITOR_AI_TRANSCRIPTION_API_KEY` (a Deepgram key; the host enables `ai.transcription` with Deepgram `nova-3` only when it is set, which makes “Create readable rendition” (English label) available on recordings). ## Where each part lives
Part File
Collaborative page: AdvancedEditor with a HocuspocusProvider session, toolbar and AiSidebar placed by the page, editor-owned settings modal ( settingsUi default 'internal' ), a „Text | Tabelle“ switch to CompositeTable (a row returns to the text and reveals its Typed block) and a docked CompositeInspector with the page's own Document facts in its document slot ( DocumentProperties.vue ) app/pages/documents/[id].vue
The fifteen pages: one Vue file each with its own editor, v-model content, panel and the source its „How it's built“ drawer shows app/pages/kitchen-sink/ : structure.vue , blocks.vue , json-markdown.vue , actions.vue , settings.vue , simple.vue , advanced.vue , surface.vue , minutes.vue , slides.vue , databases.vue , science.vue , podcast.vue , pkm.vue , epikrise.vue ; index.vue is the gallery
Page registry: slug, track, title, tagline, icon, accent, lessons and docs anchor of every page, read by the gallery, the layout's navigation and each page app/editor/kitchen-sink/pages.ts
Shared page shell: header (sample select, toolbar, switches, settings, AI), the panel , actions and sidebar slots, the settings popup and the „How it's built“ drawer app/components/kitchen-sink/KitchenSinkPage.vue
Card for a capability that needs a server port or add-on flag this run lacks app/components/kitchen-sink/ServerHint.vue
Pane state ( createPane , loadSample , showSettings , refreshMarkdown ) and the editor callbacks the pages share ( paneReady , paneUpdated ) app/editor/kitchen-sink/pane.ts
Demo documents, one file per page, with the builders they share ( typed , heading , bullets , withId for fixed block ids, …) and the Markdown for „Insert Markdown“ and „Load Markdown“ app/editor/kitchen-sink/samples/ : helpers.ts (with quote and codeBlock for the block tour) and .ts , among them structure.ts (one brief, flat and structured), blocks.ts (the block tour) and json-markdown.ts (a release note as JSON and as Markdown, a lossy and an invalid document); the round-trip comparison of the JSON & Markdown page is app/editor/kitchen-sink/round-trip.ts ; assets public/demo/diagramm.svg and protokoll.txt
The host's decision Block type, used by Content blocks, Minutes and Advanced app/editor/kitchen-sink/decision.ts
Layout of the kitchen sink: navigation built from the registry, back to „Dokumente“, links to the standalone examples, and the English copy of the editor context app/layouts/kitchen-sink.vue
Accent colours, gallery and page-enter animations app/assets/css/kitchen-sink.css
The own block as a feature: node, node view, styles, feature (toolbar, / , hover menu, conversion) and conversion provider; walked through in the tutorial app/editor/callout/ : node.ts , CalloutView.vue , index.ts , callout.css , feature.ts , convert.ts
The Epikrise domain: Block types as data (properties, AI brief) and the host feature that supplies example paragraphs app/editor/epikrise/kinds.ts , feature.ts
Module registration ( @skaile/forge-editor/nuxt ) nuxt.config.ts
Ports plugin: provideEditorServer with sessions, document store, Yjs state, assets and per-user settings server/plugins/editor-adapter.ts
Store adapter: documents, Yjs state, assets, settings, users on the file system server/utils/documents.ts , state.ts , assets.ts , settings.ts , users.ts
Showcase seeded into a fresh store: fixed document and block ids (so the documents can reference, embed and collect each other), one module per document, and the seeding step of ensureStore shared/showcase/ ( ids.ts , build.ts , index.ts , one .ts per document), server/utils/showcase.ts , server/utils/store-root.ts
File drawer: DocumentTree over the host's DocumentStore (routes, $fetch , toasts, delete-with-undo), selected folder held by the layout app/layouts/default.vue , app/components/AppDocumentTree.vue , app/composables/useDocumentStore.ts (the .cdoc export button comes in through the document-actions slot; the format stays in the host)
The pages and their components import only public entries: `AdvancedEditor` and `advancedEditor` from `@skaile/forge-editor/vue/advanced`, `SimpleEditor` from `@skaile/forge-editor/vue/simple`, `EditorToolbar`, `EditorSettingsModal`, `EditorSettingsPanel`, `EditorSurface`, `useEditorUi`, `useEditorContext`, `useEditorSettings` and `useEditorI18n` from `@skaile/forge-editor/vue`, the `StructureMode` type and `EDITOR_SETTINGS_DEFAULTS` from `@skaile/forge-editor/settings`, `parseBlocks` and the `BlockAction`/`AbilitiesOptions` types from `@skaile/forge-editor/core`, `ROOT_ABILITIES` and `ABILITY` from `@skaile/forge-editor/core/schema`, `AiSidebar` from `@skaile/forge-editor/ai/vue`, `CommentsMargin` from `@skaile/forge-editor/comments/vue`, `CompositeTable`/`CompositeInspector` from `@skaile/forge-editor/composites/vue`, `defaultBlockTypes` from `@skaile/forge-editor/composites`, `EditorOutline` from `@skaile/forge-editor/outline/vue`, `DocumentTree` from `@skaile/forge-editor/tree/vue` (the `DocumentStore` type from `@skaile/forge-editor/tree`) and the add-ons' `/client` and `/vue` entries. The callout files add `command`, `ConversionProvider` and the conversion helpers from `@skaile/forge-editor/core`. The app-level editor context (settings sync, fetch, public AI configuration) comes from the Nuxt module, no per-page setup. The React counterpart of the document page is [`examples/react-advanced`](https://github.com/skaile-ai/forge-editor/tree/main/examples/react-advanced) ([Advanced editor in React](https://skaile-ai.github.io/forge-editor/guides/advanced-react)). ## Runnable examples - **Plain Vue, no backend:** [`examples/vue-simple`](https://github.com/skaile-ai/forge-editor/tree/main/examples/vue-simple) — `SimpleEditor` with a sibling toolbar and heading outline, JSON `v-model`, read-only toggle, external replacement, host `/datum` command and an own block, the „Hinweis“ callout, added through the `extensions` and `commands` props (`/hinweis`, `Mod-Shift-h`, tone select, `!!!` Markdown form). A sample picker loads five demo documents (Start, Formatierung, Gliederung, Hinweise, and „Ungültig (Block-JSON)“, which the simple schema rejects through `contentError`); next to the editor the JSON (with block IDs) and the Markdown of the document are shown. - **Plain React, no backend:** [`examples/react-simple`](https://github.com/skaile-ai/forge-editor/tree/main/examples/react-simple) — the same document picker, read-only toggle and JSON/Markdown panes with the React layer: `EditorToolbar` beside `EditorSurface` over `defineEditor({ features: [coreContentFeature(), coreUiFeature(), mentionsFeature()] })`, the host `/` commands, `@` mentions from `metadata`, and rejected content through `onContentError`. Vite, Tailwind 4 and shadcn tokens, no Nuxt UI. - **Advanced, Nuxt:** this repository's application (`bun run dev`, port 3056) — collaborative documents under switchable demo users (no login: `server/middleware/demo-session.ts` signs every API request into a demo user, `server/api/auth/switch.post.ts` switches it; `app/pages/documents/[id].vue`), the file drawer with `DocumentTree` and a host `DocumentStore` (`app/components/AppDocumentTree.vue`, `app/composables/useDocumentStore.ts`), and the kitchen sink: fifteen standalone pages under their own layout without the document tree (`/kitchen-sink`, `app/pages/kitchen-sink/`), in three tracks: Basics (Flat and Structure mode, content blocks, JSON and Markdown, block actions, editor settings), Building blocks (`SimpleEditor` with AI, `AdvancedEditor` host props, `EditorSurface` with the callout feature in `app/editor/callout/`) and Use cases (minutes, slides, databases and charts, scientific writing, podcast, knowledge base, Epikrise review), all on the shared `KitchenSinkPage` shell. Each kitchen-sink page shows its own builtin Obsidian theme (`theme` of its registry entry, pinned by the layout with `pinObsidianTheme`; see [Themes](https://skaile-ai.github.io/forge-editor/guides/themes)). Everywhere else the Theme settings tab („Theme“) switches the whole site between the Default appearance („Standard“) and the fifteen builtin Obsidian themes (`server/themes/`: Nord, Things, Minimal, Catppuccin, Border, Blue Topaz, Tokyo Night, Flexoki, Cupertino, Prism, iA Writer, Shimmering Focus, Gruvbox, Rosé Pine, Solarized) or an uploaded one, served by the adapter in `server/plugins/editor-adapter.ts`. Walkthrough: see above. How to add a block of your own: the [tutorial](https://skaile-ai.github.io/forge-editor/tutorial/own-feature-path-b). - **Advanced, React:** [`examples/react-advanced`](https://github.com/skaile-ai/forge-editor/tree/main/examples/react-advanced) (`bun run --cwd examples/react-advanced dev`, port 3043; needs the Nuxt app running, Vite proxies `/api` and `/collab` to it) — the React counterpart of the Nuxt document page: a document picker over the editor's `/api/editor/documents`, a collaborative `AdvancedEditor` over `HocuspocusProvider` as the demo session's user, `EditorToolbar`, `EditorSettingsModal`, `CommentsMargin`, `CompositeTable`/`CompositeInspector`, `EditorOutline`, `AiSidebar`, `DocumentTree` over an `/api` adapter of `DocumentStore`, and `Toaster`. Vite, Tailwind 4 and shadcn tokens, no Nuxt UI, no Vue. # Advanced editor in React > Mount the collaborative AdvancedEditor on React with the chrome components beside it, over the same backend as the Vue surface. `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`](https://github.com/skaile-ai/forge-editor/tree/main/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](https://skaile-ai.github.io/forge-editor/getting-started/quick-start-react#setup) plus three things: - **Styles.** Replace the three library imports by one: `@import "@skaile/forge-editor/react/styles/advanced.css";` after `tailwindcss` and `tw-animate-css` (it imports `ui-vars.css`, `core.css`, every feature sheet and `themes.css` last, plus the bridge that lets an Obsidian theme restyle the shadcn tokens). Keep the `@source` for the library's whole `dist` (`src` in this repository, as the example does) and the shadcn theme block; `examples/react-advanced/src/index.css` has a complete copy. - **Notices.** Create the context with `createEditorContext({ notify: reactNotifier })` and render sonner's `` once inside the provider (both exported by `@skaile/forge-editor/react`). Features send `Notice`s to the surface's notifier (import problems, AI rate limits, comment errors, „Dokument gelöscht“ with undo in the tree); without `` none is visible. - **Dependencies.** The simple setup's list plus `sonner` (the `Toaster` and the notice seam), `yjs` and `@hocuspocus/provider` for collaboration; use the exact version ranges of the [example's `package.json`](https://github.com/skaile-ai/forge-editor/blob/main/examples/react-advanced/package.json). `main.tsx`: ```tsx [examples/react-advanced/src/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/`); 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( , ) ``` 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); `` is the advanced editor without tables and AI: ```tsx 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() const [scrollEl, setScrollEl] = useState(null) const [settingsOpen, setSettingsOpen] = useState(false) const [sidebarOpen, setSidebarOpen] = useState(false) const [commentsOpen, setCommentsOpen] = useState(false) return ( <>
setSettingsOpen(true)} />
) } ``` Notes: - **Collaboration.** The `session` is `{ ydoc, provider, user }` (`CollaborationSession` from `@skaile/forge-editor/core`): create the `Y.Doc` and the `HocuspocusProvider` in 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](https://skaile-ai.github.io/forge-editor/guides/collaboration)). Without `collaboration` pass `value`/`onChange` for a standalone editor (the collaboration and comments features then contribute nothing). - **Beside the editor.** `EditorToolbar`, `EditorSettingsModal`/`EditorSettingsPanel`, `CommentsMargin`, `CompositeTable`, `CompositeInspector`, `AiSidebar`, `EditorOutline` and `DocumentTree` are components you place; each takes the `editor` from `onReady`. `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 an `EditorSurface` yourself: `advancedEditor({ add: [myFeature()] })`, as in the Nuxt app's kitchen sink (`app/pages/kitchen-sink/surface.vue`). - **The file browser** is [`DocumentTree` from `@skaile/forge-editor/tree/react`](https://skaile-ai.github.io/forge-editor/guides/document-tree) over a `DocumentStore`; 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](https://skaile-ai.github.io/forge-editor/guides/nuxt-module) and [EditorPorts](https://skaile-ai.github.io/forge-editor/guides/editor-ports)). A React app with its own server brings the same routes. # AI > Add AI to an editor: selection menu, sidebar, ghost text, backend endpoints, integrations with Typed blocks and transcripts, agent operations and fence formats. ## AI on a core editor `aiFeature()` adds the AI features to a core editor: selection menu (rewrite tasks such as shorter/formal/translate and free prompts), the `/ai` command and a „KI" toolbar group, in-document review of rewrites (accept/reject per change), ghost autocomplete while typing (Tab accepts, Esc dismisses), model profiles and the `ai` settings tab. The sidebar (document chat and block generation, `AiSidebar` from `@skaile/forge-editor/ai/vue/chat`) and the quick model switch are components you place yourself; only the sidebar needs `@skaile/forge-chat`. `aiFeature()` and everything else in `/ai/vue` load without it. It needs no Typed blocks, transcript, media, mentions or Yjs; `bun run check:editor-entries` proves it with a `core + ai` fixture. ```vue ``` `aiFeature()` needs no component setup: the model profiles come from the app's editor context (`createEditor()` installed as a Vue plugin; `createEditorContext()` on React), and its notices (rate limits, import problems) go to the surface's notifier. The React binding is `aiFeature()` from `@skaile/forge-editor/ai/react` with the same components and hooks (`useAiAssistant`, `useDocumentChat`, `useModelProfiles`); the framework-free `@skaile/forge-editor/ai` holds `aiFeatureCore(ui)` and the models behind the hooks (`createAiAssistant`, `modelProfiles`). Import the CSS after `core.css`: ```css @import "@skaile/forge-editor/styles/ai.css"; ``` The Vue `AiSidebar` (`@skaile/forge-editor/ai/vue/chat`) renders on `@skaile/forge-chat` (its `ChatPanel`), an **optional peer** of the editor package: install it only when you place the sidebar. Importing `/ai/vue/chat` without it throws an error that names `@skaile/forge-chat` when the module loads unbundled (Node, SSR); in a bundled host (Vite, Nuxt) the bundler's own resolve error for `@skaile/forge-chat` appears first. The entry uses a top-level `await`, so the host's build target must be ES2022 or later. The chat resources belong to the host, not to `ai.css` or the editor's Nuxt module: ```css @import "@skaile/forge-chat/styles/chat.css"; @source "../../../node_modules/@skaile/forge-chat/dist"; /* Tailwind skips node_modules; relative path from this CSS file */ ``` In a Nuxt host also add `'@skaile/forge-chat/nuxt'` to `modules` after `'@nuxt/ui'`: it transpiles the package and lets Nuxt resolve its Nuxt UI components. Components: `AiSidebar` (`editor`; `v-model:open`), `ModelQuickSwitch` (`slot`: the Model role, optional `size` and `editor`; the choice applies to the whole Model role; `editor` makes „Modelle verwalten…" open that editor's settings when the switch sits outside an `EditorSurface`), `AiReviewBar` (`editor`). `AiSelectionMenu` and `AiReviewBar` are overlays the surface mounts for the feature; they are exported for hosts that render their own editor chrome. ### Backend contract and configuration The feature talks to its endpoints through the backend's `ai` port (`EditorContext.backend.ai`; the default HTTP backend calls the routes below, same origin; a host with another transport passes its own `EditorBackend`). You provide the endpoints; the handlers in `packages/editor/src/server/handlers/ai.ts` (mounted by `installEditorServer`, route table `packages/editor/src/server/routes.ts`) are the reference implementation and import the shared protocol from the package:
Endpoint Purpose
GET /api/ai/models { presets: ModelProfile[], roles: Record } — the server Model presets as Model profiles (no keys) and the default Model preset per Model role ( complete , rewrite , generate )
POST /api/ai/generate SSE ( delta / done / error ) for selection tasks and block generation
POST /api/ai/chat SSE for the sidebar's document chat with edit operations
POST /api/ai/complete { text } continuation for ghost autocomplete
The framework-free protocol (`AI_TASKS`, `AiTask`, `AiInput`, `ModelRef`, `ModelProfile`, `modelRefSchema`, `aiBlockSchema`, `AGENT_OP_TYPES`, `checkAgentOps`, `CHAT_LIMITS`, `AI_COMPLETE_LIMITS`, `AI_LIMITS`, …) is exported from `@skaile/forge-editor/ai` (and `/ai/protocol`), so a server may import the same constants and Zod schemas. A 429 from any endpoint shows one German „Zu viele KI-Anfragen, bitte kurz warten“ toast instead of an error. Without a server these endpoints are replaced by `createDirectAi` (`@skaile/forge-editor/adapters/direct-ai`): an `ai` backend that runs `generate`, `complete`, `models` and `transcriptCleanup` against an OpenAI-compatible endpoint from the browser with the same protocol modules (`runGenerate`, `runCompletion`, `runTranscriptCleanup`). The key then lives in the page (the sentence below about keys holds for the server routes only), no rate limit applies, and the document chat and `extract` answer 501. See [Browser-direct AI](https://skaile-ai.github.io/forge-editor/guides/storage-adapters#browser-direct-ai). Public AI configuration is inert data passed to `createEditor`: ```ts app.use(createEditor({ ai: { roles: { complete: 'flashLite', rewrite: 'sonnet', generate: 'sonnet' }, transcription: false } })) ``` `roles` are the default Model preset IDs per Model role until the user chooses one in the `ai` settings tab (stored in `settings.ai.roles`). API keys never reach the client: user-added profiles with their own endpoint/key are kept in this browser's `localStorage` only. In Nuxt the module's `tiptapEditor.ai` options (Model presets, default Model preset per Model role, rate limit) and the env keys `NUXT_TIPTAP_EDITOR_AI_API_KEY` / `NUXT_TIPTAP_EDITOR_AI_PRESETS__API_KEY` supply the server side. ### Extending what AI knows about the document By default the AI sees a flat document with no custom Block types and no Host blocks. Features adjust that through three optional hooks on `EditorFeature`: - `aiContext: (editor) => ({ blockTypes?, blocks?, structure? })` — what the model may build and how. It is **additive**: any number of features may contribute. `defineEditor` merges the contributions, concatenating `blockTypes` and `blocks` in feature order; `structure` (the Structure mode for generated content, `'flat'` | `'structured'`) may be set by one feature only. A block `id` described by two features, or a second feature setting `structure`, throws when the merged context is read (`aiContextOf(editor)`, which returns `{ blockTypes, blocks, structure }`), not when `defineEditor` runs. - `blockTypes` — the Block types of Typed blocks the model may build (`AiContextBlockType`: `id`, `label`, `fields` as Property definitions, `purpose?`). The composites feature contributes them. - `blocks` — Host blocks as `AiBlock { id, label, description, syntax, example }` (exported from `@skaile/forge-editor/core`): `id` is the node type name and the `convert` target id, `label` the German name, `description` its purpose and allowed content, `syntax` the Markdown form with placeholders in angle brackets, `example` one complete example. The document chat and the generate tasks (sidebar and selection menu) list them in the system prompt and send them with the request, so the model can write them; the chat offers a block as `convert` target when a registered conversion provider parses its id. See [tutorial chapter on AI](https://skaile-ai.github.io/forge-editor/tutorial/ai) for the callout's contribution. - `formatAiResult: (editor, markdown, heading?) => string` — formats an AI result's Markdown (with the optional task heading) before the generic parse/normalize/structure pipeline. Without a contributor the result gets the normalized heading (`withHeading`); the `composite-ai` integration (below) contributes the Typed block form for Structure mode. At most one feature may contribute it; `defineEditor` throws on a second one. - `aiBlockData: ({ editor, node, pos, signal }) => AiBlockDatum[] | Promise` — material a feature supplies for one Typed block before „Block füllen (KI)“ asks the model (`AiBlockDatum { label, text }`; the three types are exported from `@skaile/forge-editor/core`). It is **additive**: every contributor runs for the Typed block in parallel, and `defineEditor` concatenates their entries in feature order (`aiBlockDataOf(editor)` returns the merged provider; without contributors it resolves to no data). The library only collects: the fetch, the ranking and what the entries mean belong to your feature. `signal` aborts when the editor is destroyed. A contributor that throws or rejects cancels the fill with the notice „Daten für den Block fehlen“, and no generate request is sent. The kitchen sink's `epikriseFeature()` (`app/editor/epikrise/feature.ts`, on `/kitchen-sink/epikrise`) fetches two example paragraphs from its own route for the reason-for-admission Block type. AI results are parsed with `parseBlocks(editor, markdown)`: inline replacements use the pre-segmentation `parsed` content, block insertion uses `blocks`. ### Positions and collaboration Selection menu, sidebar and review capture ranges as document anchors. Without an `anchorResolver` anchors map through local transactions only, and the review writes with a `closeHistory` step so rewriting does not merge into typing. A collaboration feature contributes an `AnchorResolver` that keeps anchors valid across remote edits and wraps the review's undo step; the AI feature imports no Yjs code either way. ## AI integrations: Typed block AI and transcript AI `aiFeature()` knows neither Typed blocks nor transcripts. Two integration features connect it to them; each imports only the features it names, no parent feature imports the integration, so `core + ai`, `core + composites` and `core + transcript` bundles stay free of them (`bun run check:editor-entries`: fixtures `composite-ai` and `transcript-ai` allow exactly their features plus the integration, and the `ai`, `composites` and `transcript` fixtures forbid any integration module).
Factory ( @skaile/forge-editor/integrations/… ) requires Contributes
composite-ai → compositeAiFeature() composites , ai , media block actions „Block füllen (KI)“ (Block types with the ai-brief capability), „Zusammenfassung erzeugen (KI)“, “Create readable rendition” (English label), „Neu erzeugen (ersetzen)“, „Ableiten (KI)“, „Literaturangaben abrufen“, „Mit KI in Unterblöcke teilen“ / „Mit KI in Blöcke teilen“ and „Gliederung bereinigen (KI)“ (also a ⌘K command); the single formatAiResult
transcript-ai → transcriptAiFeature() transcript , ai block action „Transkript bereinigen (KI)“ and the same row in the AI selection menu
```ts import { aiFeature } from '@skaile/forge-editor/ai/vue' import { compositeFeature } from '@skaile/forge-editor/composites/vue' import { coreContentFeature, coreUiFeature, defineEditor } from '@skaile/forge-editor/core' import { compositeAiFeature } from '@skaile/forge-editor/integrations/composite-ai' import { compositeMediaFeature } from '@skaile/forge-editor/integrations/composite-media' import { transcriptAiFeature } from '@skaile/forge-editor/integrations/transcript-ai' import { mediaFeature } from '@skaile/forge-editor/media/vue' import { mentionsFeature } from '@skaile/forge-editor/mentions' import { transcriptFeature } from '@skaile/forge-editor/transcript/vue' // The integrations are framework-free and fit both surfaces; on React take the bindings from `//react`. const preset = defineEditor({ features: [ coreContentFeature(), coreUiFeature(), mediaFeature(), transcriptFeature(), compositeFeature(), mentionsFeature(), aiFeature(), compositeMediaFeature(), compositeAiFeature(), transcriptAiFeature(), ], }) ``` Install each integration after the features it requires (the order of the list is the order of toolbar/menu entries; actions carry their own `order`). `defineEditor` throws `feature "composite-ai" requires "ai", which is not installed` (likewise for `composites`, `media`, and for `transcript-ai` without `transcript` or `ai`) — it never installs a missing dependency. `bun run check:editor-composition` loads the real modules and asserts every one of these rejections, the accepted full composition and the single-`formatAiResult` rule. CSS: the stylesheets of the features in the preset (`core.css`, `media.css`, `composites.css`, `transcript.css`, `ai.css`); the integrations bring none. **compositeAiFeature()** - **Fill a Typed block.** „Block füllen (KI)“ appears on a Typed block whose Block type has the `ai-brief` capability (its config is the AI brief, `BlockTypeAi`). It sends the block's brief (`compositeAiBrief(editor, pos)`: Block type and `purpose`, titles of the enclosing Typed blocks as `path`, title, description, non-empty property values with `select` option labels, the instruction — a non-empty `meta[ai.instructionsField]` replaces `ai.instructions` — and the AI content template (`ai.contentTemplate`) with `{key}` replaced by `meta[key]`, the current body as Markdown) plus the document as context to the task `fill-block` (`generate` Model role, `POST /api/ai/generate`, rejected with 400 when `block` is missing). The result opens the in-document diff review over the block's body; title and properties stay untouched. `fill-block` is offered nowhere else (`blockTasks()` skips tasks with `needsBlock`). - **Material from features.** Before the request, `fillComposite` runs the Editor preset's merged `aiBlockData` for the Typed block (above). The entries travel as `block.data` in order, at most 10, each `text` cut to `AI_LIMITS.block.data` (20000) characters; the task prompt prints each as `