ComponentsEditors & drag-and-drop

Rich text / block editor

Edits content as blocks. Text blocks (paragraphs, headings, lists, quotes, code, tables, images, dividers) are edited inline; page blocks such as a hero or a pricing table render as the very component the site uses, configured in an inspector generated from the block's schema. Reads and writes VamosDoc, so the site renders the same content with @vamos-ui/react/render and never loads the editor. A compact RichTextEditor covers notes, comments and announcements.

Tiptap v3 (ProseMirror, MIT extensions only) + cmdk + zod editor/block-editor.tsx, editor/schema-form.tsx, editor/convert.ts, styles/editors.csstheme · material · accent · density · font

Harbor for clinics — landing page

Draft

Installation

$ pnpm add @vamos-ui/react

The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";

Usage

import { BlockEditor, RichTextEditor } from "@vamos-ui/react/editor";
import { siteBlocks } from "@/blocks/registry"; // createRegistry([heroBlock, pricingBlock, …])

// Posts and pages: text + page blocks, "/" menu, drag handle, inserter
<BlockEditor value={doc} onChange={(next) => save(next)} registry={siteBlocks} variant="document" />

// A note field in a form (RichDoc in and out)
<Field label="Summary"><RichTextEditor value={note} onChange={setNote} size="sm" /></Field>

// The site (server component, no editor code)
import { renderDoc } from "@vamos-ui/react/render";
renderDoc(doc, { registry: siteBlocks, locale: "en" });

Keyboard: Esc selects the current block. In block selection, ↑/↓ move between blocks, Alt+Shift+↑/↓ moves the block, ⌘/Ctrl+Shift+D duplicates, Alt+Shift+Z deletes, Enter goes back to the text. Insertions, moves and deletes are announced.

Markdown shortcuts while typing: # to ####, - or *, 1., >, ``` and ---.

Storage: image blocks are { type: "image", props: { src, alt, width, height, caption, assetId, focal } }; tables are table → table-row → table-cell (props.header, colspan, rowspan) with inline content.

Unknown block types (a disabled module) show a placeholder and are saved back exactly as they were loaded.

Examples

Rich-text field in a form (CRM activity)

RichTextEditor reads the surrounding Field: label, hint, error and required are wired to the text area. Value is a RichDoc, the format of f.rich() fields.

Visible to everyone on the account team.

Your own block, end to end

defineBlock once: the component renders on the site and inside the editor; the zod schema drives validation and the inspector; preview props fill a new block. variant="canvas" runs blocks edge to edge, canvasTheme puts the canvas in the site's accent.

SchemaForm on its own

The inspector engine: widgets come from f.* metadata (text, toggle, nested group, sortable list with keyboard move buttons). Validation issues land on the field.

Plans 3

  1. Plan 1

    Features 2

{
  "title": "Simple *pricing*",
  "plans": [
    {
      "name": "Solo",
      "price": "$29",
      "period": "month",
      "features": [
        "1 practitioner",
        "SMS reminders"
      ]
    },
    {
      "name": "Clinic",
      "price": "$89",
      "period": "month",
      "features": [
        "Up to 8 chairs",
        "Waitlist autofill",
        "Online payments"
      ],
      "featured": true
    },
    {
      "name": "Group",
      "price": "$240",
      "period": "month",
      "features": [
        "Unlimited locations",
        "SSO and audit log"
      ]
    }
  ]
}

Restricted block set (CRM quote)

allowedBlocks limits what the / menu and the inserter offer. Tables use Tab to move between cells.

Editor and site output side by side

The same VamosDoc through renderDoc from @vamos-ui/react/render — RSC-safe and free of Tiptap.

Editor

renderDoc — site output

Opening our Kadıköy clinic

From 2 December we see patients six days a week at Moda Caddesi 41. Existing bookings move automatically; nobody needs to call us.

Good to know

Is there parking?

Yes, two free spaces in front of the building.

Markdown and Word paste

Markdown becomes headings, lists, quotes, code and tables; Word and Google Docs HTML is cleaned of styles before parsing; a bare URL becomes a link.

Copy this, then paste below

## Spring opening hours

We now open **an hour earlier** on weekdays.

- Monday to Friday: 08:00–19:00
- Saturday: 09:00–14:00

> Bookings made before 1 March keep their time.

Read-only document with outline (HR policy)

readOnly keeps the text selectable and hides every editing control. The outline lists headings with word count and reading time.

API

BlockEditor

PropTypeDefault
value / defaultValue
Content. Doc-level fields (seo, layout, locales) are kept; blocks and meta are written by the editor.
VamosDoc—
onChange
Debounced 300 ms and flushed on blur. The editor's own state is the truth while typing.
(doc: VamosDoc, info: { wordCount; changedBlockIds }) => void—
registry
Page blocks (createRegistry). The same registry the site renders with. Default: text blocks only.
BlockRegistry—
variant
Reading column (~720px), full-width page builder, or compact field (no inserter or drag handle).
"document" | "canvas" | "rich-text""document"
allowedBlocks
Block types offered in the / menu and inserter: paragraph, heading, list, quote, code, divider, table, image and registry types.
string[] | ({ parentType }) => string[]—
patterns
Ready-made groups of blocks ({ id, title, category, doc }), shown in the inserter's Patterns tab.
BlockPattern[]—
placeholder
Empty-editor text (dictionary key blockEditor.placeholder).
string"Type / to add a block"
readOnly
Display only: no toolbar, menus, handle or inserter.
booleanfalse
media / uploadImage
Pasted and dropped images upload through here. Default: the VamosProvider media adapter.
MediaAdapter / (file, { signal }) => Promise<{ src; alt?; width?; height?; assetId? }>—
linkSearch
Internal page suggestions in the link editor (⌘/Ctrl+K).
(q) => Promise<{ title; href; type }[]>—
canvasTheme
Site theme axes on the canvas, so blocks look as they will live. The canvas is always solid, also inside a glass admin.
{ accent?; font?; density?; theme? }—
lang
Content language: lang attribute (spellcheck, hyphenation), word counting and anchor slugs.
stringvalue.lang → UI locale
editorRef
{ focus, insertBlock(type, props?), getDoc, undo, redo, editor }. editor is the Tiptap instance.
Ref<BlockEditorHandle>—
size / minHeight / maxHeight / toolbar
Rich-text variant sizing and the fixed toolbar.
"sm" | "md" / CSS length / boolean—
aria-label / aria-labelledby
Accessible name of the text area.
string"Content"
labels
Every built-in string (namespace blockEditor).
Partial<BlockEditorLabels>—
children
Your composition of parts. Default: content, bubble menu, / menu, drag handle and inserter (toolbar for rich-text).
ReactNode—

Parts

PropTypeDefault
BlockEditorContent
The canvas: contenteditable with role=textbox, aria-multiline, lang and Field wiring.
HTMLAttributes<div>—
BlockEditorBubbleMenu
role=toolbar on a text selection: turn into, bold, italic, underline, strike, code, highlight, link. Arrow keys move, Alt+F10 focuses, Esc returns.
{ children? }—
BlockEditorToolbar
Fixed toolbar (rich-text): marks, lists, quote, link, undo/redo. scripts adds sub/superscript.
{ scripts?; children? }—
BlockEditorSlashMenu
/ inserter. cmdk listbox driven from the text: arrows, Enter or Tab inserts, Esc closes. Search folds Turkish İ/ı.
{ className? }—
BlockEditorDragHandle
Gutter + (insert below) and grip (drag, accent drop line) with a block menu: duplicate, move, turn into, copy link, hide, delete.
{ className? }—
BlockInserter
+ popover: Blocks | Patterns, cmdk search, live mini preview of the highlighted block.
{ trigger?: ReactNode | null }—
BlockInspector
Settings of the selected page block from its schema: variant, props, anchor, hide. Images: alt text, decorative, caption.
{ widgets?; onShowDocument?; documentLabel? }—
BlockEditorOutline
Heading outline, word count, reading time, warnings for skipped levels, several H1s and images without alt.
{ stats? }—

RichTextEditor

PropTypeDefault
value / defaultValue / onChange
f.rich() value: paragraphs, h2–h4, lists, quotes, code, rules. onChange is debounced.
RichDoc—
id / invalid / required / aria-describedby
Usually set by the surrounding Field.
——
size
Text size and padding.
"sm" | "md""md"
minHeight / maxHeight
The text area scrolls past maxHeight.
CSS length"7rem" / none
toolbar
Fixed toolbar above the text.
booleantrue
placeholder / readOnly / labels / autoFocus
As on BlockEditor.
——

SchemaForm

PropTypeDefault
schema
Usually a block's schema built with f.*.
z.ZodObject—
value / onChange
Props object (localized fields edit the locale entry).
Record<string, unknown>—
fields
Hints; group is content, style or advanced.
Record<key, { widget?; label?; help?; placeholder?; group?; hidden?; when?(v) }>—
widgets
Custom widgets by name (receive id, value, onChange, schema, meta, invalid, locale).
Record<string, WidgetComponent>—
errors
Shown under the matching fields.
ZodError | { path; message }[]—
layout
Stacked, or two columns in a dialog.
"sidebar" | "dialog""sidebar"
variant / locale / disabled
Hide fields of other variants; locale edited in Localized<T>; read-only.
string / string / boolean—

Converters (pure)

PropTypeDefault
toProseMirror(doc, registry?)
Migrates the doc and component blocks to current versions first. Unknown types are carried untouched.
VamosDoc → JSONContent—
fromProseMirror(json, { base?, lang?, meta? })
Stable ids (missing or duplicate ones replaced), merged runs, derived meta (wordCount, readingTime, toc).
JSONContent → VamosDoc—
richDocToProseMirror / proseMirrorToRichDoc
For f.rich() fields; headings clamp to h2–h4.
RichDoc ↔ JSONContent—
markdownToBlocks / cleanPastedHtml
Paste conversion, usable on the server for imports.
string → BlockNode[] / string—