Reference

Editor SDK

NotrEditor is a block-based, always-editable rich text editor in the spirit of Notion. It owns the collaboration plumbing — you give it a noteId, it gives you a live multiplayer document.

The editor currently ships inside the Notr codebase as a Svelte 5 component imported from $lib. A standalone @notr/editor npm package is planned and will keep the same API.

Usage

<script>
  import { NotrEditor } from '$lib'

  let status = $state('connecting')
</script>

<NotrEditor
  noteId="8a1f3c0e-…"
  user={{ name: 'Ada Lovelace', color: '#c8401a' }}
  bind:status
/>

<p>connection: {status}</p>

Props

PropTypeDefaultDescription
noteIdstringrequiredThe note to open. Treat it as immutable — remount the component (e.g. with {#key}) to switch notes.
user{ name, color }randomShown on this user's caret to other collaborators.
tokenstring"session"Collab credential — the default authenticates with the console session cookie; pass a service key from server-side clients.
urlstring/collab on the current hostOverride the collaboration endpoint.
slashCommandsSlashCommandItem[][]Domain-specific commands appended after the built-ins.
onUploadUploadHandlerHost uploader for images & files. Enables paste, drag-drop, and the Image command. Omit to disable uploads. See Images & attachments.
statusbindable stringconnecting · connected · disconnected.
classstringClasses for the editor container. Pairs well with Tailwind's prose.

Built-in blocks

Paragraphs, headings (1–3), bullet / numbered / check lists, quotes, code blocks, dividers, and tables — all reachable by typing /. Bold, italic, strikethrough, inline code, and links work with the usual shortcuts and markdown-style input rules.

Custom slash commands

The slash menu is a framework: Notr provides the trigger, filtering, and keyboard handling; you provide domain commands. Each item gets the Tiptap editor and the range of the typed query to replace.

<script>
  import { NotrEditor } from '$lib'

  const meetingSummary = {
    title: 'Meeting summary',
    description: 'Insert the summary template',
    keywords: ['template', 'recap'],
    command: ({ editor, range }) => {
      editor
        .chain()
        .focus()
        .deleteRange(range)
        .insertContent('<h2>Summary</h2><p></p><h2>Action items</h2>')
        .run()
    }
  }
</script>

<NotrEditor noteId={note.id} slashCommands={[meetingSummary]} />

Commands are plain objects, so they can do anything — insert templates, reference entities from your application, call your API and insert the result.

Images & attachments

Notr is an engine, not a file manager. A note stores only a reference to a piece of media — a URL plus a little metadata — never the bytes. Where the bytes live, how they got there, and who may read them are entirely yours. You wire that in through one callback: onUpload.

Pass it and the editor lights up three gestures: paste an image, drag-drop a file, or run the Image slash command. Each hands your function the File; you store the bytes wherever you like and return a URL. Omit onUpload entirely and there are no upload affordances at all — the capability is opt-in.

<script>
  import { NotrEditor } from '$lib'

  // Store the bytes wherever you like, return a URL.
  async function onUpload(file) {
    const body = new FormData()
    body.append('file', file)
    const res = await fetch('/my-app/uploads', { method: 'POST', body })
    const { url } = await res.json()
    return url                       // shorthand for { url }
  }
</script>

<NotrEditor noteId={note.id} {onUpload} />

For finer control, resolve an object instead of a bare string. mode decides how it renders — 'inline' draws an <img>, 'link' draws a download chip that works for any file. It is stored on the node, so a user can keep an image as a plain link instead of rendering it. When you return only a URL, Notr infers the mode from the file type (images inline, everything else as a link).

async function onUpload(file) {
  const url = await store(file)
  return {
    url,
    mode: 'inline',                  // 'inline' | 'link'
    alt: file.name,
    meta: { id: '…' }                // opaque, round-tripped on the node
  }
}

Two guarantees. Notr makes no promise the URL resolves — link rot, access control, and deletion are the host's concern. But it makes a stronger one: it never breaks on a bad URL. A dead, cross-origin, or hostile link degrades to a placeholder that still surfaces the reference; the editor never throws and the document is never lost.

Returning UploadResult never uploads anything from Notr's side — storage, auth, and cleanup stay with you. That's the same boundary as the rest of Notr: it stores notes, not files.

Styling

The editor renders semantic HTML and inherits your typography. The component ships minimal structural CSS (collaboration carets, checklist layout); everything else is yours via the class prop. The console uses class="prose max-w-none" with the Tailwind typography plugin.

How the document actually syncs: Collaboration.