---
title: "Kitchen sink: learning path"
description: "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"
canonical_url: "https://skaile-ai.github.io/forge-editor/guides/advanced-nuxt-example"
---
# 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:

<table>
<thead>
  <tr>
    <th>
      Track
    </th>
    
    <th>
      Pages
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Basics: what a document is made of, how it is structured, stored and acted on
    </td>
    
    <td>
      <a href="#structure">
        Flat & Structure
      </a>
      
      , <a href="#blocks">
        Content blocks
      </a>
      
      , <a href="#json-and-markdown">
        JSON & Markdown
      </a>
      
      , <a href="#actions">
        Acting on blocks
      </a>
      
      , <a href="#settings">
        Editor settings
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Building blocks: one API of the library, shown on its own
    </td>
    
    <td>
      <a href="#simple">
        Simple
      </a>
      
      , <a href="#advanced">
        Advanced
      </a>
      
      , <a href="#surface">
        Surface
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Use cases: a planned application, built from the framework
    </td>
    
    <td>
      <a href="#minutes">
        Minutes
      </a>
      
      , <a href="#slides">
        Slides
      </a>
      
      , <a href="#databases">
        Databases
      </a>
      
      , <a href="#science">
        Science
      </a>
      
      , <a href="#podcast">
        Podcast
      </a>
      
      , <a href="#pkm">
        PKM
      </a>
      
      , <a href="#epikrise">
        Epikrise
      </a>
    </td>
  </tr>
</tbody>
</table>

Read a page in the browser and the matching section here side by side. Every page is one Vue file, `app/pages/kitchen-sink/<slug>.vue`, and the snippet in each section is that file's `<script setup>`, pulled from the repository by `bun run docs:snippets`.

**What every page has.** A header with the sample select (each page ships tuned sample documents and an empty one), the toolbar, an „editable“ switch, the settings button and the AI sidebar button. Settings open in a popup the page places itself (`settings-ui="external"`, see [Composites and the settings UI](https://skaile-ai.github.io/forge-editor/guides/composites)), the AI sidebar comes in through a slot ([AI](https://skaile-ai.github.io/forge-editor/guides/ai)). The body is the editor and a side panel on the right; the panel differs per page, and where it has nothing page-specific it shows the document as JSON and Markdown. **How it's built** (the code icon in the header) opens a drawer with the page's lessons and its own source text, and **Open in docs** leads to the section below.

**English editor chrome.** The rest of the app is German, the kitchen sink is English. `app/layouts/kitchen-sink.vue` provides a copy of the app's editor context with `locale: 'en'` and `t: createTranslator('en')`, so the toolbar, the `/` menu and `defaultBlockTypes(t)` read English under that layout while settings, backend and notifier stay the app's own ([Language](https://skaile-ai.github.io/forge-editor/guides/language)).

**Server hints.** Add-ons are imported straight into the pages that use them, so their block types, node views and inspector sections work in a plain `bun run dev`. What runs on the server (the render targets of slides, charts and Typst, speech synthesis, review records) depends on the `NUXT_PUBLIC_ADDONS_*` flags of `.env.example`. A page that demonstrates such a capability asks the backend for it (`ctx.backend.render?.targets()`, `ctx.backend.speech`) and shows a `ServerHint` card in its panel, naming the flag, when the server lacks it.

**One editor per page.** Every component placed beside an editor names it: `EditorToolbar :editor`, `AiSidebar :editor`, `CompositeInspector :editor`, `useEditorUi(() => editor)`. The editor registers its chrome (settings routing, sidebar toggle, block actions) under its own Tiptap instance. Content lives in the page as TipTap JSON behind `v-model`, with its own undo history; no document store, no websocket. External content replacement (picking a sample) emits no update event, so the page refreshes the Markdown output itself once the editor took the new content (`loadSample` in `app/editor/kitchen-sink/pane.ts`). Switching pages remounts the editor.

**Read in this order.** The five Basics pages first: Flat and Structure mode, the blocks a document is made of, the document as JSON and Markdown, the actions on its blocks and the settings every editor reads; every other page assumes them. Then the three editor shapes (Simple, Advanced, Surface), then the applications built from them. Epikrise comes last: it is the only page whose state lives on the server, where the review records are kept by block id.

## Structure

Flat mode and Structure mode, shown on one project brief written twice. The mode is the editor setting `blocks.structure` (`'flat'` or `'structured'`, default `'structured'`); it is one setting for the whole app, stored per user, not state of the page ([Settings](https://skaile-ai.github.io/forge-editor/guides/settings)).

- **The switch.** The Flat | Structure tabs in the header read and write `useEditorSettings().settings.value.blocks.structure`; the settings bridge deep-watches the ref and persists the change, so every editor of the app follows it. Users find the same choice in the settings popup under Editing.
- **What changes.** In Flat mode the toolbar offers Heading 1–3; in Structure mode it does not, and new sections are Typed blocks. The AI shapes its output the same way: Markdown headings in Flat mode, nested Typed blocks in Structure mode ([AI](https://skaile-ai.github.io/forge-editor/guides/ai)).
- **Two samples.** „Flat: project brief“ is headings with paragraphs and lists below them; its Markdown output has `##` headings. „Structure: the same brief“ is the same text as `section` Typed blocks (title, description, tags, body; „Out of scope“ nested inside „Scope“) plus one `task` with status, due date and assignee; its Markdown output wraps each block in a `::: section` fence ([Markdown and code](https://skaile-ai.github.io/forge-editor/guides/markdown-and-code)).
- **Converting.** The two header buttons run the composites feature's commands on the whole document: `convertHeadingsToComposites({ recursive: true, scope: 'document', kind })` groups every heading level into nested Typed blocks of the default Block type, `convertCompositesToHeadings({ recursive: true, scope: 'document' })` turns Typed blocks into headings of their nesting level, with description, tags and properties as text lines. The `/` menu has the same conversions for one level or all levels („Headings to Typed blocks“, „Typed blocks to headings“ and their nested variants), the toolbar's „Convert structure“ menu too. Converting changes the document, not the setting ([Composites and the settings UI](https://skaile-ai.github.io/forge-editor/guides/composites)).
- **No panel.** The page leaves the `panel` slot empty on purpose: the default JSON and Markdown output is where the difference shows.

```vue [app/pages/kitchen-sink/structure.vue]
<script setup lang="ts">
import { AdvancedEditor } from '@skaile/forge-editor/vue/advanced'
import { AiSidebar } from '@skaile/forge-editor/ai/vue/chat'
import { type BlockType, defaultBlockTypes } from '@skaile/forge-editor/composites'
import type { StructureMode } from '@skaile/forge-editor/settings'
import { useEditorI18n, useEditorSettings } from '@skaile/forge-editor/vue'
import KitchenSinkPage from '~/components/kitchen-sink/KitchenSinkPage.vue'
import { pageOf } from '~/editor/kitchen-sink/pages'
import { createPane, paneReady, paneUpdated, showSettings } from '~/editor/kitchen-sink/pane'
import { SAMPLES_STRUCTURE } from '~/editor/kitchen-sink/samples/structure'
import source from './structure.vue?raw'

definePageMeta({ layout: 'kitchen-sink' })

// Flat mode & Structure mode. The switch in the header is `blocks.structure`, the app's shared and persisted editor
// setting (the same one as settings → Editing), not state of this page: every editor of the app follows it. It decides
// what the toolbar offers (Heading 1-3 only in Flat mode) and how the AI shapes its output. The two buttons convert the
// whole document with the composites feature's commands; converting does not change the setting. No `panel` on purpose:
// the default JSON/Markdown output shows the difference, `##` headings against `::: section` fences.

// The built-in Block types put `typedBlock` into the schema, so `section` blocks and the conversion commands exist.
const { t } = useEditorI18n()
const blockTypes: BlockType[] = [...defaultBlockTypes(t)]

const { settings } = useEditorSettings()
// The settings bridge deep-watches the ref and persists the change.
const mode = computed<StructureMode>({
  get: () => settings.value.blocks.structure,
  set: (value) => { settings.value.blocks.structure = value },
})
const modes = [{ label: 'Flat', value: 'flat' }, { label: 'Structure', value: 'structured' }]

const page = pageOf('structure')!
// Called in setup: the AI/profile features of the composition inject the app context.
const pane = createPane(page.title, SAMPLES_STRUCTURE)

// Whole document, every heading level, no matter where the caret is (a non-empty selection narrows it).
const groupOptions = () => ({ recursive: true, scope: 'document' as const, kind: settings.value.blocks.defaultBlockType })
const flattenOptions = { recursive: true, scope: 'document' as const }
// `pane.content` is read so the checks re-run on every edit: the TipTap instance itself is not reactive.
const canGroup = computed(() => pane.content && pane.editable && !!pane.editor?.can().convertHeadingsToComposites(groupOptions()))
const canFlatten = computed(() => pane.content && pane.editable && !!pane.editor?.can().convertCompositesToHeadings(flattenOptions))
const group = () => pane.editor?.chain().focus().convertHeadingsToComposites(groupOptions()).run()
const flatten = () => pane.editor?.chain().focus().convertCompositesToHeadings(flattenOptions).run()
</script>
```

## 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]
<script setup lang="ts">
import { AdvancedEditor } from '@skaile/forge-editor/vue/advanced'
import { AiSidebar } from '@skaile/forge-editor/ai/vue/chat'
import { type BlockType, defaultBlockTypes } from '@skaile/forge-editor/composites'
import { CompositeInspector, CompositeTable } from '@skaile/forge-editor/composites/vue'
import { useEditorI18n } from '@skaile/forge-editor/vue'
import KitchenSinkPage from '~/components/kitchen-sink/KitchenSinkPage.vue'
import { decisionBlockType } from '~/editor/kitchen-sink/decision'
import { pageOf } from '~/editor/kitchen-sink/pages'
import { createPane, paneReady, paneUpdated, showSettings } from '~/editor/kitchen-sink/pane'
import { SAMPLES_BLOCKS } from '~/editor/kitchen-sink/samples/blocks'
import source from './blocks.vue?raw'

definePageMeta({ layout: 'kitchen-sink' })

// Content blocks: what a document is made of. The sample walks from Simple blocks (paragraph, heading, list, quote,
// code) to Container blocks and Typed blocks, then to Block types and their properties. The editor knows the built-in
// Block types plus the host's own `decision`; the panel shows two readers of the same document: the inspector for the
// selected block's properties and the table of every Typed block.

// The built-in Block types (labelled in the editor's language) plus the host's own.
const { t } = useEditorI18n()
const blockTypes: BlockType[] = [...defaultBlockTypes(t), decisionBlockType]

const page = pageOf('blocks')!
// Called in setup: the AI/profile features of the composition inject the app context.
const pane = createPane(page.title, SAMPLES_BLOCKS)
</script>
```

## 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 `::: <kind>` 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]
<script setup lang="ts">
import { AdvancedEditor } from '@skaile/forge-editor/vue/advanced'
import { AiSidebar } from '@skaile/forge-editor/ai/vue/chat'
import { type BlockType, defaultBlockTypes } from '@skaile/forge-editor/composites'
import { parseBlocks } from '@skaile/forge-editor/core'
import { useEditorI18n } from '@skaile/forge-editor/vue'
import KitchenSinkPage from '~/components/kitchen-sink/KitchenSinkPage.vue'
import { pageOf } from '~/editor/kitchen-sink/pages'
import { createPane, paneReady, paneUpdated, refreshMarkdown, showSettings } from '~/editor/kitchen-sink/pane'
import { roundTrip } from '~/editor/kitchen-sink/round-trip'
import { RELEASE_MARKDOWN, SAMPLES_JSON_MARKDOWN } from '~/editor/kitchen-sink/samples/json-markdown'
import source from './json-markdown.vue?raw'

definePageMeta({ layout: 'kitchen-sink' })

// Document in, document out. The stored form is TipTap JSON behind `v-model`; Markdown is a second way in
// (`parseBlocks`, the block pipeline an import uses) and out (`editor.getMarkdown()`). The panel writes the document as
// Markdown, reads it back and compares the two, block ids left out. JSON the schema does not know is refused through
// `contentError`: the editor keeps its last valid document, and the page shows why.

const { t } = useEditorI18n()
const blockTypes: BlockType[] = [...defaultBlockTypes(t)]

const page = pageOf('json-markdown')!
// Called in setup: the AI/profile features of the composition inject the app context.
const pane = createPane(page.title, SAMPLES_JSON_MARKDOWN)

const contentError = ref<string | null>(null)
// `sync`: cleared the moment new content is handed in, before the editor checks it and possibly reports an error.
watch(() => pane.content, () => { contentError.value = null }, { flush: 'sync' })

/** Replaces the document with Markdown read through the block pipeline: `parsed` is the whole document as JSON. */
async function loadMarkdown() {
  const editor = pane.editor
  if (!editor) return
  pane.content = parseBlocks(editor, RELEASE_MARKDOWN).parsed
  await nextTick()
  refreshMarkdown(pane)
}

// `pane.markdown` is refreshed on every update, so the round trip follows the document.
const trip = computed(() => pane.editor && pane.markdown ? roundTrip(pane.editor, pane.markdown) : null)
</script>
```

## 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]
<script setup lang="ts">
import { AdvancedEditor } from '@skaile/forge-editor/vue/advanced'
import { AiSidebar } from '@skaile/forge-editor/ai/vue/chat'
import { type BlockType, defaultBlockTypes } from '@skaile/forge-editor/composites'
import type { AbilitiesOptions, BlockAction, BlockActionContext, BlockActionSurface } from '@skaile/forge-editor/core'
import { ABILITY } from '@skaile/forge-editor/core/schema'
import { useEditorI18n } from '@skaile/forge-editor/vue'
import KitchenSinkPage from '~/components/kitchen-sink/KitchenSinkPage.vue'
import { pageOf } from '~/editor/kitchen-sink/pages'
import { createPane, paneReady, paneUpdated, showSettings } from '~/editor/kitchen-sink/pane'
import { SAMPLES_ACTIONS } from '~/editor/kitchen-sink/samples/actions'
import source from './actions.vue?raw'

definePageMeta({ layout: 'kitchen-sink' })

// Acting on blocks. Commands (`/` menu, toolbar) insert; block actions act on a block that is there, on its hover bar,
// on the toolbar of the block holding the selection, or both (`blockUi`, read once at setup: the editor is remounted
// with `:key` when the surface changes, the document stays in `v-model`). Whether an action is offered is decided per
// block: its `ability` at that place in the document, the capabilities it `requires`, and `readOnly` in a read-only
// editor. The page's ability rule takes delete and duplicate away inside quotes; the panel logs every run.

const { t } = useEditorI18n()
const blockTypes: BlockType[] = [...defaultBlockTypes(t)]

const page = pageOf('actions')!
// Called in setup: the AI/profile features of the composition inject the app context.
const pane = createPane(page.title, SAMPLES_ACTIONS)

const surface = ref<BlockActionSurface | 'both'>('hover')
const surfaces = [{ label: 'Hover', value: 'hover' }, { label: 'Toolbar', value: 'toolbar' }, { label: 'Both', value: 'both' }]

// Inside a quote, its blocks may not be deleted or duplicated: the built-in actions with these abilities disappear there.
const abilities: Partial<AbilitiesOptions> = { rules: { blockquote: { revokes: [ABILITY.blockDelete, ABILITY.blockDuplicate] } } }

const log = ref<{ at: string, action: string, block: string }[]>([])
function record(action: string, { node }: BlockActionContext) {
  const block = node.attrs.blockType ? `${node.type.name} (${node.attrs.blockType})` : node.type.name
  log.value = [{ at: new Date().toLocaleTimeString('en-GB'), action, block }, ...log.value].slice(0, 8)
}

const toast = useToast()
const blockActions: BlockAction[] = [
  {
    // Reads only, so it stays on the bar of a read-only editor.
    id: 'copy-id',
    label: 'Copy block id',
    icon: 'i-lucide-fingerprint',
    readOnly: true,
    run: (ctx) => {
      void navigator.clipboard?.writeText(String(ctx.node.attrs.id ?? ''))
      toast.add({ title: 'Block id copied', description: String(ctx.node.attrs.id ?? 'no id'), icon: 'i-lucide-fingerprint' })
      record('Copy block id', ctx)
    },
  },
  {
    // A `menu` instead of `run`: a dropdown of the items it returns.
    id: 'insert-after',
    label: 'Insert after',
    icon: 'i-lucide-list-plus',
    menu: ctx => [
      { label: 'Paragraph', icon: 'i-lucide-pilcrow', onSelect: () => insertAfter(ctx, { type: 'paragraph' }, 'Insert paragraph') },
      { label: 'Divider', icon: 'i-lucide-minus', onSelect: () => insertAfter(ctx, { type: 'horizontalRule' }, 'Insert divider') },
    ],
  },
  {
    // Only on blocks that hold other blocks; on the toolbar also while the caret is inside such a block's body.
    id: 'count-children',
    label: 'Count blocks inside',
    icon: 'i-lucide-hash',
    requires: ['container'],
    target: 'typedBlock',
    readOnly: true,
    run: (ctx) => {
      let count = 0
      // Title, description and tags are the Typed block's chrome, not blocks of its body.
      ctx.node.forEach((child) => { if (!['typedBlockTitle', 'typedBlockDescription', 'typedBlockTags'].includes(child.type.name)) count++ })
      toast.add({ title: `${count} blocks inside`, icon: 'i-lucide-hash' })
      record('Count blocks inside', ctx)
    },
  },
]

function insertAfter(ctx: BlockActionContext, node: { type: string }, action: string) {
  ctx.editor.chain().focus().insertContentAt(ctx.pos + ctx.node.nodeSize, node).run()
  record(action, ctx)
}
</script>
```

## 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]
<script setup lang="ts">
import { AdvancedEditor } from '@skaile/forge-editor/vue/advanced'
import { AiSidebar } from '@skaile/forge-editor/ai/vue/chat'
import { type BlockType, defaultBlockTypes } from '@skaile/forge-editor/composites'
import { EDITOR_SETTINGS_DEFAULTS } from '@skaile/forge-editor/settings'
import { EditorSettingsPanel, useEditorI18n, useEditorSettings } from '@skaile/forge-editor/vue'
import KitchenSinkPage from '~/components/kitchen-sink/KitchenSinkPage.vue'
import { pageOf } from '~/editor/kitchen-sink/pages'
import { createPane, paneReady, paneUpdated, showSettings } from '~/editor/kitchen-sink/pane'
import { SAMPLES_SETTINGS } from '~/editor/kitchen-sink/samples/settings'
import source from './settings.vue?raw'

definePageMeta({ layout: 'kitchen-sink' })

// Editor settings: one object per app (`useEditorSettings()`), stored per user, read by every editor. The panel places
// `EditorSettingsPanel` inline instead of the popup (its tabs are the ones the editor's features contribute) above the
// live settings object, the keys that differ from `EDITOR_SETTINGS_DEFAULTS`, and a reset. `ready` turns true when the
// stored settings are usable, `synced` when the account's settings from the server are in effect.

const { t } = useEditorI18n()
const blockTypes: BlockType[] = [...defaultBlockTypes(t)]

const page = pageOf('settings')!
// Called in setup: the AI/profile features of the composition inject the app context.
const pane = createPane(page.title, SAMPLES_SETTINGS)

const { settings, ready, synced } = useEditorSettings()
const tab = ref<string>()

type Sections = Record<string, Record<string, unknown>>
// `section.key` of every setting whose value is not the default one.
const changed = computed(() => Object.entries(EDITOR_SETTINGS_DEFAULTS as unknown as Sections)
  .filter(([, values]) => typeof values === 'object' && values !== null)
  .flatMap(([section, values]) => Object.keys(values)
    .filter(key => JSON.stringify((settings.value as unknown as Sections)[section]?.[key]) !== JSON.stringify(values[key]))
    .map(key => `${section}.${key}`)))

// Everything back to the defaults except the theme: the settings bridge deep-watches the ref and persists the change.
function reset() {
  settings.value = { ...structuredClone(EDITOR_SETTINGS_DEFAULTS), appearance: settings.value.appearance }
}
</script>
```

## 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]
<script setup lang="ts">
import { SimpleEditor } from '@skaile/forge-editor/vue/simple'
import { aiFeature } from '@skaile/forge-editor/ai/vue'
import { AiSidebar } from '@skaile/forge-editor/ai/vue/chat'
import KitchenSinkPage from '~/components/kitchen-sink/KitchenSinkPage.vue'
import { pageOf } from '~/editor/kitchen-sink/pages'
import { createPane, paneReady, paneUpdated, showSettings } from '~/editor/kitchen-sink/pane'
import { SAMPLES_SIMPLE } from '~/editor/kitchen-sink/samples/simple'
import source from './simple.vue?raw'

definePageMeta({ layout: 'kitchen-sink' })

// A standalone editor (TipTap JSON via v-model, own undo history, no websocket) built as a `SimpleEditor` with
// `add: [aiFeature()]`: core blocks plus the AI sidebar and selection actions. The settings popup is
// `settings-ui="external"`: the editor emits `settingsOpen`, `KitchenSinkPage` places `EditorSettingsModal`.
const page = pageOf('simple')!

// Called in setup: the AI/profile features of the compositions inject the app context.
const pane = createPane(page.title, SAMPLES_SIMPLE)
// The simple editor's bundle holds no catalog feature, so the AI feature comes in through `add`.
const add = [aiFeature()]
</script>
```

## 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]
<script setup lang="ts">
import { AdvancedEditor } from '@skaile/forge-editor/vue/advanced'
import { AiSidebar } from '@skaile/forge-editor/ai/vue/chat'
import { type BlockType, defaultBlockTypes } from '@skaile/forge-editor/composites'
import type { AbilitiesOptions, BlockAction, CommandDef, EditorMetadata, MetadataItem } from '@skaile/forge-editor/core'
import { useEditorI18n } from '@skaile/forge-editor/vue'
import KitchenSinkPage from '~/components/kitchen-sink/KitchenSinkPage.vue'
import { decisionBlockType } from '~/editor/kitchen-sink/decision'
import { pageOf } from '~/editor/kitchen-sink/pages'
import { createPane, paneReady, paneUpdated, showSettings } from '~/editor/kitchen-sink/pane'
import { SAMPLES_ADVANCED } from '~/editor/kitchen-sink/samples/advanced'
import source from './advanced.vue?raw'

definePageMeta({ layout: 'kitchen-sink' })

// A standalone AdvancedEditor (TipTap JSON via v-model, own undo history, no websocket) that shows the host extension
// points as plain props: metadata lists, an ability override, an own hover action, an own `/` command and an own Block
// type (offered in the `/` menu, the Block type badge and the toolbar's Typed block dropdown). `remove: ['tables']`
// leaves tables out of the composition, and out of the schema its samples must fit. The settings popup is
// `settings-ui="external"`: the editor emits `settingsOpen`, `KitchenSinkPage` places `EditorSettingsModal`.
const page = pageOf('advanced')!

// Constants, not template literals: a new `metadata` object on every re-render would replace the lists each time.
const metadata: EditorMetadata = {
  hashtags: [{ id: 'demo', label: 'demo' }, { id: 'reference', label: 'reference' }],
  speakers: [{ id: 'anna', label: 'Anna' }, { id: 'ben', label: 'Ben' }],
}
const abilities: Partial<AbilitiesOptions> = { rules: { bulletList: { revokes: ['ai.rewrite'] } } }
// Shown on every block that may be deleted, after the default hover actions.
const blockActions: BlockAction[] = [
  { id: 'log', label: 'Log to console', icon: 'i-lucide-terminal', ability: 'block.delete', run: ({ node }) => console.log(node.toJSON()) },
]
// Appended to the default `/` commands (incl. `/decision` from `blockTypes`); no `ability`: plain text may go wherever
// the `/` menu opens.
function withDateCommand(defaults: CommandDef[]): CommandDef[] {
  return [...defaults, { id: 'date', label: 'Insert date', icon: 'i-lucide-calendar', slash: {}, run: editor => editor.chain().focus().insertContent(new Date().toLocaleDateString('en-GB')).run() }]
}
// The built-in Block types (labelled in the editor's language) plus one of the host's own.
const { t } = useEditorI18n()
const blockTypes: BlockType[] = [...defaultBlockTypes(t), decisionBlockType]
const remove = ['tables'] as const

function logCreated(list: string, item: MetadataItem) {
  console.log('create', list, item)
}

// Called in setup: the AI/profile features of the compositions inject the app context.
const pane = createPane(page.title, SAMPLES_ADVANCED)
</script>
```

## 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 `<script setup>`: some features hook the app's lifetime ([Feature composition and entries](https://skaile-ai.github.io/forge-editor/guides/composition)).
- **An own feature.** `app/editor/callout/` is a block of the host: node, node view, a toolbar button, the `/callout` command, a hover menu for the tone, „Convert to …“ in both directions and a description for the AI. The tutorial builds it step by step: [Chapter 5: Your own feature, path B](https://skaile-ai.github.io/forge-editor/tutorial/own-feature-path-b).
- **An own ability.** `CALLOUT_ABILITY` gates the callout's items; the `abilities` prop grants it at document root next to `ROOT_ABILITIES`.
- **Markdown.** „Insert Markdown“ runs `parseBlocks(editor, CALLOUT_MARKDOWN)` and inserts the result, which shows the callout's `!!!` syntax going through the block pipeline ([Markdown and code](https://skaile-ai.github.io/forge-editor/guides/markdown-and-code)).

```vue [app/pages/kitchen-sink/surface.vue]
<script setup lang="ts">
import { advancedEditor } from '@skaile/forge-editor/vue/advanced'
import { AiSidebar } from '@skaile/forge-editor/ai/vue/chat'
import { defaultBlockTypes } from '@skaile/forge-editor/composites'
import { type AbilitiesOptions, parseBlocks } from '@skaile/forge-editor/core'
import { ROOT_ABILITIES } from '@skaile/forge-editor/core/schema'
import { EditorSurface, useEditorI18n } from '@skaile/forge-editor/vue'
import KitchenSinkPage from '~/components/kitchen-sink/KitchenSinkPage.vue'
import { CALLOUT_ABILITY, calloutFeature } from '~/editor/callout/feature'
import { pageOf } from '~/editor/kitchen-sink/pages'
import { createPane, paneReady, paneUpdated, showSettings } from '~/editor/kitchen-sink/pane'
import { CALLOUT_MARKDOWN, SAMPLES_SURFACE } from '~/editor/kitchen-sink/samples/surface'
import source from './surface.vue?raw'

definePageMeta({ layout: 'kitchen-sink' })

// A standalone editor (TipTap JSON via v-model, own undo history, no websocket) built as an `EditorSurface` on the
// advanced composition (`advancedEditor({ add })`) plus one own feature (`app/editor/callout`): a block of the host with
// toolbar button, `/` command, hover menu, "Convert to …" target and Markdown form. The settings popup is
// `settings-ui="external"`: the editor emits `settingsOpen`, `KitchenSinkPage` places `EditorSettingsModal`.
const page = pageOf('surface')!

// Called in setup: the AI/profile features of the compositions inject the app context.
// The advanced composition plus the host's own feature, handed to the surface as one preset.
const preset = advancedEditor({ blockTypes: defaultBlockTypes(useEditorI18n().t), add: [calloutFeature()] })
const pane = createPane(page.title, SAMPLES_SURFACE)
// The custom ability must be granted at document root, or the callout's toolbar button, `/callout` and hover menu stay hidden.
const abilities: Partial<AbilitiesOptions> = { root: [...ROOT_ABILITIES, CALLOUT_ABILITY] }

/** Imports Markdown with the callout syntax through the editor's block pipeline (the tokenizer of `callout/node.ts`). */
function insertCalloutMarkdown() {
  const editor = pane.editor
  if (!editor) return
  editor.chain().focus().insertContent(parseBlocks(editor, CALLOUT_MARKDOWN).blocks).run()
}
</script>
```

## 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]
<script setup lang="ts">
import { AdvancedEditor } from '@skaile/forge-editor/vue/advanced'
import { AiSidebar } from '@skaile/forge-editor/ai/vue/chat'
import { type BlockType, defaultBlockTypes } from '@skaile/forge-editor/composites'
import { CompositeTable } from '@skaile/forge-editor/composites/vue'
import type { EditorMetadata } from '@skaile/forge-editor/core'
import { EditorOutline } from '@skaile/forge-editor/outline/vue'
import { useEditorI18n } from '@skaile/forge-editor/vue'
import KitchenSinkPage from '~/components/kitchen-sink/KitchenSinkPage.vue'
import { decisionBlockType } from '~/editor/kitchen-sink/decision'
import { pageOf } from '~/editor/kitchen-sink/pages'
import { createPane, paneReady, paneUpdated, showSettings } from '~/editor/kitchen-sink/pane'
import { SAMPLES_MINUTES } from '~/editor/kitchen-sink/samples/minutes'
import source from './minutes.vue?raw'

definePageMeta({ layout: 'kitchen-sink' })

// The core scenario: meeting minutes. The default advanced composition plus the host's metadata lists (the speakers
// of the transcript segments, the hashtags the `#` menu offers) and one own Block type, `decision`. The panel shows
// two views of the same document: the heading outline and the table of its Typed blocks.

// Constants, not template literals: a new `metadata` object on every re-render would replace the lists each time.
const metadata: EditorMetadata = {
  speakers: [{ id: 'anna', label: 'Anna' }, { id: 'ben', label: 'Ben' }, { id: 'clara', label: 'Clara' }],
  hashtags: [{ id: 'northlight', label: 'northlight' }, { id: 'decision', label: 'decision' }, { id: 'risk', label: 'risk' }],
}
// The built-in Block types (labelled in the editor's language) plus the host's own.
const { t } = useEditorI18n()
const blockTypes: BlockType[] = [...defaultBlockTypes(t), decisionBlockType]

const page = pageOf('minutes')!
// Called in setup: the AI/profile features of the composition inject the app context.
const pane = createPane(page.title, SAMPLES_MINUTES)
</script>
```

## 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]
<script setup lang="ts">
import { AdvancedEditor } from '@skaile/forge-editor/vue/advanced'
import { AiSidebar } from '@skaile/forge-editor/ai/vue/chat'
import { type BlockType, defaultBlockTypes } from '@skaile/forge-editor/composites'
import { CompositeInspector } from '@skaile/forge-editor/composites/vue'
import { type FeatureId, INTERACTION } from '@skaile/forge-editor/presets/compose'
import type { TransferFeatureOptions } from '@skaile/forge-editor/transfer'
import { useEditorContext, useEditorI18n } from '@skaile/forge-editor/vue'
import { slideBlockTypes, slidesTransforms } from '@skaile/forge-editor-slides/client'
import { slidesFeature } from '@skaile/forge-editor-slides/vue'
import KitchenSinkPage from '~/components/kitchen-sink/KitchenSinkPage.vue'
import ServerHint from '~/components/kitchen-sink/ServerHint.vue'
import { pageOf } from '~/editor/kitchen-sink/pages'
import { createPane, paneReady, paneUpdated, showSettings } from '~/editor/kitchen-sink/pane'
import { SAMPLES_SLIDES } from '~/editor/kitchen-sink/samples/slides'
import source from './slides.vue?raw'

definePageMeta({ layout: 'kitchen-sink' })

// The slides add-on on an AdvancedEditor: it brings two Block types (`deck`, `slide`), a host feature (the preview section in the
// inspector and the slide hover actions) and transfer transforms ("Insert as slides"). The page imports the add-on's `/client`
// and `/vue` entries directly, so the editor shows decks whatever `NUXT_PUBLIC_ADDONS_SLIDES` says; only the server render targets
// (reveal.js, PowerPoint, Marp) need that flag.
const page = pageOf('slides')!
const { t, locale } = useEditorI18n()
const ctx = useEditorContext()
// Called in setup, before the first await: the AI/profile features of the composition inject the app context.
const pane = createPane(page.title, SAMPLES_SLIDES)

// Constants, not template literals: a new object on every re-render would replace the editor's options each time.
const blockTypes: BlockType[] = [...defaultBlockTypes(t), ...slideBlockTypes(locale)]
const interaction: FeatureId[] = [...INTERACTION.full, 'transfer']
const transfer: TransferFeatureOptions = { extraTransforms: slidesTransforms({ locale }) }
const features = [slidesFeature()]

// The renderer is a server port: without it (or without the slides targets) the "Render as…" menu has no deck formats.
const targets = await ctx.backend.render?.targets().catch(() => []) ?? []
const hasRender = targets.some(target => target.id === 'reveal-html')
</script>
```

## 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]
<script setup lang="ts">
import { AdvancedEditor } from '@skaile/forge-editor/vue/advanced'
import { AiSidebar } from '@skaile/forge-editor/ai/vue/chat'
import { type BlockType, defaultBlockTypes } from '@skaile/forge-editor/composites'
import { CompositeTable } from '@skaile/forge-editor/composites/vue'
import { EDITOR_CONTEXT, useEditorContext, useEditorI18n } from '@skaile/forge-editor/vue'
import { chartBlockTypes } from '@skaile/forge-editor-charts/client'
import { chartNodeViews } from '@skaile/forge-editor-charts/vue'
import KitchenSinkPage from '~/components/kitchen-sink/KitchenSinkPage.vue'
import ServerHint from '~/components/kitchen-sink/ServerHint.vue'
import { pageOf } from '~/editor/kitchen-sink/pages'
import { createPane, paneReady, paneUpdated, showSettings } from '~/editor/kitchen-sink/pane'
import { SAMPLES_DATABASES } from '~/editor/kitchen-sink/samples/databases'
import source from './databases.vue?raw'

definePageMeta({ layout: 'kitchen-sink' })

// Typed blocks as database rows. `task` blocks carry properties (status, due, assignee); the built-in `dataView` block shows a stored
// query as a table or a board, and the charts add-on's `chart` block draws one. The page imports the charts add-on's `/client`
// and `/vue` entries directly, whatever `NUXT_PUBLIC_ADDONS_CHARTS` says; only the server's chart export needs that flag.
const page = pageOf('databases')!
const { t, locale } = useEditorI18n()
const ctx = useEditorContext()
// A data view or chart asks the backend's Block index (every Document of the workspace) and answers from the open Document
// only when the backend has none. This editor is standalone: its Blocks are in no index, so the page hands its editor a
// backend without `index`, and the views show the sample's tasks. Everything else (render, AI, settings) stays the app's.
const { index: _workspaceIndex, ...localBackend } = ctx.backend
provide(EDITOR_CONTEXT, { ...ctx, backend: localBackend })
// Called in setup, before the first await: the AI/profile features of the composition inject the app context.
const pane = createPane(page.title, SAMPLES_DATABASES)

// Constant, not a template literal: a new array on every re-render would replace the editor's Block types each time.
const blockTypes: BlockType[] = [...defaultBlockTypes(t), ...chartBlockTypes(locale)]

// The renderer is a server port: without it (or without the chart target) a chart cannot be exported as SVG.
const targets = await ctx.backend.render?.targets().catch(() => []) ?? []
const hasChartExport = targets.some(target => target.id === 'chart-svg')
</script>
```

## 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]
<script setup lang="ts">
import { AdvancedEditor } from '@skaile/forge-editor/vue/advanced'
import { AiSidebar } from '@skaile/forge-editor/ai/vue/chat'
import { type BlockType, defaultBlockTypes } from '@skaile/forge-editor/composites'
import { CompositeInspector } from '@skaile/forge-editor/composites/vue'
import { useEditorContext, useEditorI18n } from '@skaile/forge-editor/vue'
import { quoteBlockTypes } from '@skaile/forge-editor-pdf-viewer'
import { pdfViewerFeature } from '@skaile/forge-editor-pdf-viewer/vue'
import KitchenSinkPage from '~/components/kitchen-sink/KitchenSinkPage.vue'
import ServerHint from '~/components/kitchen-sink/ServerHint.vue'
import { pageOf } from '~/editor/kitchen-sink/pages'
import { createPane, paneReady, paneUpdated, showSettings } from '~/editor/kitchen-sink/pane'
import { SAMPLES_SCIENCE } from '~/editor/kitchen-sink/samples/science'
import source from './science.vue?raw'

definePageMeta({ layout: 'kitchen-sink' })

// Scientific writing: the default composition plus the PDF-viewer add-on. A `paper` block keeps one CSL-JSON item in
// `meta.bib`; `cite` nodes (`/cite` or `[cite:`) name an entry by its key and show "(Vaswani et al. 2017)". `quoteBlockTypes`
// adds the `quote` kind (a passage of a PDF with a source anchor), `pdfViewerFeature` the viewer in the inspector. The
// "PDF (Typst)" render target lives on the server, so the page asks the backend whether it is there.
const page = pageOf('science')!
const pane = createPane(page.title, SAMPLES_SCIENCE)

const ctx = useEditorContext()
const { t, locale } = useEditorI18n()
const blockTypes: BlockType[] = [...defaultBlockTypes(t), ...quoteBlockTypes(locale)]
// The `/vue` entry's feature draws the viewer section of the inspector (the root entry has the framework-free core).
const add = [pdfViewerFeature()]

// `typst-pdf` is the id of `packages/editor-typst`'s render target (`NUXT_PUBLIC_ADDONS_TYPST=1` installs it on the server).
// `backend.ready` settles once the HTTP backend has dropped the capabilities the server lacks.
await ctx.backend.ready
const targets = (await ctx.backend.render?.targets().catch(() => [])) ?? []
const typstMissing = !targets.some(target => target.id === 'typst-pdf')
</script>
```

## 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]
<script setup lang="ts">
import { AdvancedEditor } from '@skaile/forge-editor/vue/advanced'
import { AiSidebar } from '@skaile/forge-editor/ai/vue/chat'
import { CompositeInspector } from '@skaile/forge-editor/composites/vue'
import type { EditorMetadata } from '@skaile/forge-editor/core'
import { useEditorContext } from '@skaile/forge-editor/vue'
import { audioFeature } from '@skaile/forge-editor-audio'
import KitchenSinkPage from '~/components/kitchen-sink/KitchenSinkPage.vue'
import ServerHint from '~/components/kitchen-sink/ServerHint.vue'
import { pageOf } from '~/editor/kitchen-sink/pages'
import { createPane, paneReady, paneUpdated, showSettings } from '~/editor/kitchen-sink/pane'
import { SAMPLES_PODCAST } from '~/editor/kitchen-sink/samples/podcast'
import source from './podcast.vue?raw'

definePageMeta({ layout: 'kitchen-sink' })

// A podcast script: the built-in `recording` and `rendition` kinds (a transcript is a rendition nested in its recording) plus
// the audio add-on. `audioFeature` adds two derive methods to the block actions: read a text block aloud into a new recording
// (text to speech) and mix several recordings into one. Both record a Derivation, so the library marks a mixdown outdated when
// one of its takes changes. Speech needs the server's `speech` port, which the page looks up in the backend.
const page = pageOf('podcast')!
const pane = createPane(page.title, SAMPLES_PODCAST)

const ctx = useEditorContext()
// The hosts offered in the speaker picker of a transcript segment; a constant, so a re-render does not replace the list.
const metadata: EditorMetadata = {
  speakers: [{ id: 'anna', label: 'Anna' }, { id: 'ben', label: 'Ben' }],
}
const add = [audioFeature({ mixdownMode: 'concat' })]
// `backend.ready` settles once the HTTP backend has dropped the capabilities the server lacks (`speech` without the audio add-on).
await ctx.backend.ready
const speechMissing = !ctx.backend.speech
</script>
```

## 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]
<script setup lang="ts">
import { AdvancedEditor } from '@skaile/forge-editor/vue/advanced'
import { AiSidebar } from '@skaile/forge-editor/ai/vue/chat'
import { CompositeTable } from '@skaile/forge-editor/composites/vue'
import type { EditorMetadata } from '@skaile/forge-editor/core'
import { EditorOutline } from '@skaile/forge-editor/outline/vue'
import KitchenSinkPage from '~/components/kitchen-sink/KitchenSinkPage.vue'
import { pageOf } from '~/editor/kitchen-sink/pages'
import { createPane, paneReady, paneUpdated, showSettings } from '~/editor/kitchen-sink/pane'
import { SAMPLES_PKM } from '~/editor/kitchen-sink/samples/pkm'
import source from './pkm.vue?raw'

definePageMeta({ layout: 'kitchen-sink' })

// A knowledge base in one document: a `folder` of `note` blocks, tags from the host's hashtag list and `[@` reference chips
// between the notes. The table beside the editor lists every Typed block with its tags, the outline the headings. Left out on
// purpose: the `explore` interaction (backlinks, search, related notes across documents) needs the workspace index, which a
// standalone editor does not have.
const page = pageOf('pkm')!
const pane = createPane(page.title, SAMPLES_PKM)

// A constant, so a re-render does not replace the list.
const metadata: EditorMetadata = {
  hashtags: [{ id: 'idea', label: 'idea' }, { id: 'source', label: 'source' }, { id: 'todo', label: 'todo' }],
}
</script>
```

## 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]
<script setup lang="ts">
import { advancedEditor } from '@skaile/forge-editor/vue/advanced'
import { AiSidebar } from '@skaile/forge-editor/ai/vue/chat'
import { type BlockType, defaultBlockTypes } from '@skaile/forge-editor/composites'
import { CompositeInspector } from '@skaile/forge-editor/composites/vue'
import type { AbilitiesOptions, BlockTypeCapability } from '@skaile/forge-editor/core'
import { ABILITY, ROOT_ABILITIES } from '@skaile/forge-editor/core/schema'
import { EditorSurface, useEditorI18n } from '@skaile/forge-editor/vue'
import KitchenSinkPage from '~/components/kitchen-sink/KitchenSinkPage.vue'
import { epikriseFeature } from '~/editor/epikrise/feature'
import { EPIKRISE_KINDS } from '~/editor/epikrise/kinds'
import { pageOf } from '~/editor/kitchen-sink/pages'
import { createPane, paneReady, paneUpdated, showSettings } from '~/editor/kitchen-sink/pane'
import { SAMPLES_EPIKRISE } from '~/editor/kitchen-sink/samples/epikrise'
import source from './epikrise.vue?raw'

definePageMeta({ layout: 'kitchen-sink' })

// A clinical report with review. An `EditorSurface` on the advanced composition (`advancedEditor`) with the host's
// domain Block type (`aufnahme-anlass`, its AI brief and properties are data in `app/editor/epikrise/kinds.ts`), the
// host feature that supplies example paragraphs, and the opt-in `pruefung` feature. The page lists the `pruefung`
// capability on the reason for admission and on the built-in `section` type, so each part of the report has its own
// review; the server keeps the records per document.

// The Prüfung records are stored under this id by the server port, so they survive a reload.
const DOCUMENT_ID = 'kitchen-sink-epikrise'
// `pruefung.record` is not part of ROOT_ABILITIES: the host grants it at document root.
const abilities: Partial<AbilitiesOptions> = { root: [...ROOT_ABILITIES, ABILITY.pruefungRecord] }
// An editorial check, then a medical one (`chain`); the medical check locks the block until it changes or someone lifts
// the lock (`lock`). Kinds are host data, the server stores them as given. Listed here and not in `EPIKRISE_KINDS`: a
// capability no installed feature declares makes the editor throw, and only this page installs `pruefung`.
const review: BlockTypeCapability = { id: 'pruefung', config: { kinds: ['editorial', 'medical'], chain: true, lock: 'medical' } }
const reviewed = (type: BlockType): BlockType => ({ ...type, capabilities: [...type.capabilities, review] })

const { t } = useEditorI18n()
const page = pageOf('epikrise')!
// Called in setup: the AI/profile features of the composition inject the app context.
const preset = advancedEditor({
  blockTypes: [...defaultBlockTypes(t).map(type => type.id === 'section' ? reviewed(type) : type), ...EPIKRISE_KINDS.map(reviewed)],
  add: [epikriseFeature()],
  // `pruefung` is a catalog feature in no interaction level list: added on top of the bundle's `full` interaction.
  with: ['pruefung'],
})
const pane = createPane(page.title, SAMPLES_EPIKRISE)
</script>
```

## 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:

<table>
<thead>
  <tr>
    <th>
      Feature
    </th>
    
    <th>
      Where to see it
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Collaboration (<code>
        collaboration
      </code>
      
       prop, Hocuspocus session)
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/blob/main/app/pages/documents/%5Bid%5D.vue" rel="nofollow">
        <code>
          app/pages/documents/[id].vue
        </code>
      </a>
      
      , <a href="https://skaile-ai.github.io/forge-editor/guides/collaboration">
        Collaboration
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Comments and authors (<code>
        CommentsMargin
      </code>
      
      , <code>
        v-model:comments-open
      </code>
      
      )
    </td>
    
    <td>
      the same document page, <a href="https://skaile-ai.github.io/forge-editor/guides/authors-and-comments">
        Authors and comments
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Explore and search (<code>
        explore
      </code>
      
      , <code>
        ExplorePanel
      </code>
      
      , <code>
        SearchPanel
      </code>
      
      )
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/blob/main/app/layouts/default.vue" rel="nofollow">
        <code>
          app/layouts/default.vue
        </code>
      </a>
      
      , <a href="https://skaile-ai.github.io/forge-editor/guides/explore">
        Explore
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Templates (<code>
        templatesOf
      </code>
      
      , <code>
        TEMPLATES_FOLDER
      </code>
      
      )
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/blob/main/app/pages/documents/index.vue" rel="nofollow">
        <code>
          app/pages/documents/index.vue
        </code>
      </a>
      
      , <a href="https://skaile-ai.github.io/forge-editor/guides/templates">
        Templates
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Themes (<code>
        appearance.theme
      </code>
      
      , the Theme tab)
    </td>
    
    <td>
      the settings popup of any page, <a href="https://skaile-ai.github.io/forge-editor/guides/themes">
        Themes
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      The editor-owned settings modal (<code>
        settings-ui="internal"
      </code>
      
      , the default; the kitchen sink places its settings itself with <code>
        settings-ui="external"
      </code>
      
      )
    </td>
    
    <td>
      the document page, <a href="https://skaile-ai.github.io/forge-editor/guides/settings">
        Settings
      </a>
      
      ; the settings object itself (<code>
        useEditorSettings
      </code>
      
      , <code>
        EDITOR_SETTINGS_DEFAULTS
      </code>
      
      , <code>
        EditorSettingsPanel
      </code>
      
      ) has its own page, <a href="#settings">
        Editor settings
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      UI kit (<code>
        createEditor({ kit })
      </code>
      
      ) and the module options
    </td>
    
    <td>
      <a href="https://skaile-ai.github.io/forge-editor/guides/nuxt-module">
        Nuxt module
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Export check
    </td>
    
    <td>
      <a href="https://skaile-ai.github.io/forge-editor/guides/export-check">
        Export check
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Obsidian vaults, image studio, graph, Typst
    </td>
    
    <td>
      <a href="https://skaile-ai.github.io/forge-editor/guides/obsidian">
        Obsidian
      </a>
      
      , <a href="https://skaile-ai.github.io/forge-editor/guides/image-studio">
        Image studio
      </a>
      
      , <a href="https://skaile-ai.github.io/forge-editor/guides/graph">
        Graph
      </a>
      
      , <a href="https://skaile-ai.github.io/forge-editor/guides/typst">
        Typst
      </a>
    </td>
  </tr>
</tbody>
</table>

## 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/<id>` 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_<ID>_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

<table>
<thead>
  <tr>
    <th>
      Part
    </th>
    
    <th>
      File
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Collaborative page: <code>
        AdvancedEditor
      </code>
      
       with a <code>
        HocuspocusProvider
      </code>
      
       session, toolbar and <code>
        AiSidebar
      </code>
      
       placed by the page, editor-owned settings modal (<code>
        settingsUi
      </code>
      
       default <code>
        'internal'
      </code>
      
      ), a „Text | Tabelle“ switch to <code>
        CompositeTable
      </code>
      
       (a row returns to the text and reveals its Typed block) and a docked <code>
        CompositeInspector
      </code>
      
       with the page's own Document facts in its <code>
        document
      </code>
      
       slot (<a href="https://github.com/skaile-ai/forge-editor/blob/main/app/components/DocumentProperties.vue" rel="nofollow">
        <code>
          DocumentProperties.vue
        </code>
      </a>
      
      )
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/blob/main/app/pages/documents/%5Bid%5D.vue" rel="nofollow">
        <code>
          app/pages/documents/[id].vue
        </code>
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      The fifteen pages: one Vue file each with its own editor, <code>
        v-model
      </code>
      
       content, panel and the source its „How it's built“ drawer shows
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/tree/main/app/pages/kitchen-sink" rel="nofollow">
        <code>
          app/pages/kitchen-sink/
        </code>
      </a>
      
      : <code>
        structure.vue
      </code>
      
      , <code>
        blocks.vue
      </code>
      
      , <code>
        json-markdown.vue
      </code>
      
      , <code>
        actions.vue
      </code>
      
      , <code>
        settings.vue
      </code>
      
      , <code>
        simple.vue
      </code>
      
      , <code>
        advanced.vue
      </code>
      
      , <code>
        surface.vue
      </code>
      
      , <code>
        minutes.vue
      </code>
      
      , <code>
        slides.vue
      </code>
      
      , <code>
        databases.vue
      </code>
      
      , <code>
        science.vue
      </code>
      
      , <code>
        podcast.vue
      </code>
      
      , <code>
        pkm.vue
      </code>
      
      , <code>
        epikrise.vue
      </code>
      
      ; <code>
        index.vue
      </code>
      
       is the gallery
    </td>
  </tr>
  
  <tr>
    <td>
      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
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/blob/main/app/editor/kitchen-sink/pages.ts" rel="nofollow">
        <code>
          app/editor/kitchen-sink/pages.ts
        </code>
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Shared page shell: header (sample select, toolbar, switches, settings, AI), the <code>
        panel
      </code>
      
      , <code>
        actions
      </code>
      
       and <code>
        sidebar
      </code>
      
       slots, the settings popup and the „How it's built“ drawer
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/blob/main/app/components/kitchen-sink/KitchenSinkPage.vue" rel="nofollow">
        <code>
          app/components/kitchen-sink/KitchenSinkPage.vue
        </code>
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Card for a capability that needs a server port or add-on flag this run lacks
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/blob/main/app/components/kitchen-sink/ServerHint.vue" rel="nofollow">
        <code>
          app/components/kitchen-sink/ServerHint.vue
        </code>
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Pane state (<code>
        createPane
      </code>
      
      , <code>
        loadSample
      </code>
      
      , <code>
        showSettings
      </code>
      
      , <code>
        refreshMarkdown
      </code>
      
      ) and the editor callbacks the pages share (<code>
        paneReady
      </code>
      
      , <code>
        paneUpdated
      </code>
      
      )
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/blob/main/app/editor/kitchen-sink/pane.ts" rel="nofollow">
        <code>
          app/editor/kitchen-sink/pane.ts
        </code>
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Demo documents, one file per page, with the builders they share (<code>
        typed
      </code>
      
      , <code>
        heading
      </code>
      
      , <code>
        bullets
      </code>
      
      , <code>
        withId
      </code>
      
       for fixed block ids, …) and the Markdown for „Insert Markdown“ and „Load Markdown“
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/tree/main/app/editor/kitchen-sink/samples" rel="nofollow">
        <code>
          app/editor/kitchen-sink/samples/
        </code>
      </a>
      
      : <code>
        helpers.ts
      </code>
      
       (with <code>
        quote
      </code>
      
       and <code>
        codeBlock
      </code>
      
       for the block tour) and <code>
        <slug>.ts
      </code>
      
      , among them <code>
        structure.ts
      </code>
      
       (one brief, flat and structured), <code>
        blocks.ts
      </code>
      
       (the block tour) and <code>
        json-markdown.ts
      </code>
      
       (a release note as JSON and as Markdown, a lossy and an invalid document); the round-trip comparison of the JSON & Markdown page is <a href="https://github.com/skaile-ai/forge-editor/blob/main/app/editor/kitchen-sink/round-trip.ts" rel="nofollow">
        <code>
          app/editor/kitchen-sink/round-trip.ts
        </code>
      </a>
      
      ; assets <a href="https://github.com/skaile-ai/forge-editor/blob/main/public/demo/diagramm.svg" rel="nofollow">
        <code>
          public/demo/diagramm.svg
        </code>
      </a>
      
       and <a href="https://github.com/skaile-ai/forge-editor/blob/main/public/demo/protokoll.txt" rel="nofollow">
        <code>
          protokoll.txt
        </code>
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      The host's <code>
        decision
      </code>
      
       Block type, used by Content blocks, Minutes and Advanced
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/blob/main/app/editor/kitchen-sink/decision.ts" rel="nofollow">
        <code>
          app/editor/kitchen-sink/decision.ts
        </code>
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      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
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/blob/main/app/layouts/kitchen-sink.vue" rel="nofollow">
        <code>
          app/layouts/kitchen-sink.vue
        </code>
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Accent colours, gallery and page-enter animations
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/blob/main/app/assets/css/kitchen-sink.css" rel="nofollow">
        <code>
          app/assets/css/kitchen-sink.css
        </code>
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      The own block as a feature: node, node view, styles, feature (toolbar, <code>
        /
      </code>
      
      , hover menu, conversion) and conversion provider; walked through in the <a href="https://skaile-ai.github.io/forge-editor/tutorial/own-feature-path-b">
        tutorial
      </a>
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/tree/main/app/editor/callout" rel="nofollow">
        <code>
          app/editor/callout/
        </code>
      </a>
      
      : <code>
        node.ts
      </code>
      
      , <code>
        CalloutView.vue
      </code>
      
      , <code>
        index.ts
      </code>
      
      , <code>
        callout.css
      </code>
      
      , <code>
        feature.ts
      </code>
      
      , <code>
        convert.ts
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      The Epikrise domain: Block types as data (properties, AI brief) and the host feature that supplies example paragraphs
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/blob/main/app/editor/epikrise/kinds.ts" rel="nofollow">
        <code>
          app/editor/epikrise/kinds.ts
        </code>
      </a>
      
      , <a href="https://github.com/skaile-ai/forge-editor/blob/main/app/editor/epikrise/feature.ts" rel="nofollow">
        <code>
          feature.ts
        </code>
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Module registration (<code>
        @skaile/forge-editor/nuxt
      </code>
      
      )
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/blob/main/nuxt.config.ts" rel="nofollow">
        <code>
          nuxt.config.ts
        </code>
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Ports plugin: <code>
        provideEditorServer
      </code>
      
       with sessions, document store, Yjs state, assets and per-user settings
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/blob/main/server/plugins/editor-adapter.ts" rel="nofollow">
        <code>
          server/plugins/editor-adapter.ts
        </code>
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Store adapter: documents, Yjs state, assets, settings, users on the file system
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/blob/main/server/utils/documents.ts" rel="nofollow">
        <code>
          server/utils/documents.ts
        </code>
      </a>
      
      , <a href="https://github.com/skaile-ai/forge-editor/blob/main/server/utils/state.ts" rel="nofollow">
        <code>
          state.ts
        </code>
      </a>
      
      , <a href="https://github.com/skaile-ai/forge-editor/blob/main/server/utils/assets.ts" rel="nofollow">
        <code>
          assets.ts
        </code>
      </a>
      
      , <a href="https://github.com/skaile-ai/forge-editor/blob/main/server/utils/settings.ts" rel="nofollow">
        <code>
          settings.ts
        </code>
      </a>
      
      , <a href="https://github.com/skaile-ai/forge-editor/blob/main/server/utils/users.ts" rel="nofollow">
        <code>
          users.ts
        </code>
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      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 <code>
        ensureStore
      </code>
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/tree/main/shared/showcase" rel="nofollow">
        <code>
          shared/showcase/
        </code>
      </a>
      
       (<code>
        ids.ts
      </code>
      
      , <code>
        build.ts
      </code>
      
      , <code>
        index.ts
      </code>
      
      , one <code>
        <slug>.ts
      </code>
      
       per document), <a href="https://github.com/skaile-ai/forge-editor/blob/main/server/utils/showcase.ts" rel="nofollow">
        <code>
          server/utils/showcase.ts
        </code>
      </a>
      
      , <a href="https://github.com/skaile-ai/forge-editor/blob/main/server/utils/store-root.ts" rel="nofollow">
        <code>
          server/utils/store-root.ts
        </code>
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      File drawer: <code>
        DocumentTree
      </code>
      
       over the host's <code>
        DocumentStore
      </code>
      
       (routes, <code>
        $fetch
      </code>
      
      , toasts, delete-with-undo), selected folder held by the layout
    </td>
    
    <td>
      <a href="https://github.com/skaile-ai/forge-editor/blob/main/app/layouts/default.vue" rel="nofollow">
        <code>
          app/layouts/default.vue
        </code>
      </a>
      
      , <a href="https://github.com/skaile-ai/forge-editor/blob/main/app/components/AppDocumentTree.vue" rel="nofollow">
        <code>
          app/components/AppDocumentTree.vue
        </code>
      </a>
      
      , <a href="https://github.com/skaile-ai/forge-editor/blob/main/app/composables/useDocumentStore.ts" rel="nofollow">
        <code>
          app/composables/useDocumentStore.ts
        </code>
      </a>
      
       (the <code>
        .cdoc
      </code>
      
       export button comes in through the <code>
        document-actions
      </code>
      
       slot; the format stays in the host)
    </td>
  </tr>
</tbody>
</table>

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.


## Sitemap

See the full [sitemap](https://skaile-ai.github.io/forge-editor/sitemap.md) for all pages.
