← All components
forms / beta

Editor

Selection formatting, project-configured references, inline media and / commands with canonical JSON and portable derived Markdown; no fixed toolbar.

LIVE PREVIEW
Loading preview…

When to use

  • Record descriptions, documents, and comment composers.
  • Configure reference sources once with EditorProvider; editor sources extend the provider and replace sources with the same id for a project.
  • Use value JSON for saved records; defaultMarkdown loads legacy records. onChange emits both; servers re-derive Markdown from validated JSON.
  • Paste, drop or pick inline images with onUploadImage; paste YouTube, Vimeo or Loom URLs on their own line, or use /video and onUploadVideo.
  • Upload callbacks receive file and { onProgress } and return { src, displaySrc?, alt?, width?, height? } or null. src is a durable token-free path; displaySrc is a session-only preview.
  • Private media uses resolveMediaSrc on EditorProvider, Editor or EditorReadOnly; JSON and Markdown keep the storage path. MCP consumers resolve paths through their own storage.
  • Import from @revido-llc/kit/editor and install its optional TipTap 3.30.5 peers.

Consider an alternative

  • Single-line names (TextField or EditableTitle).
  • Editing the JSON and Markdown independently: JSON preserves reference identities, media dimensions, marks and anchors; Markdown serves integrations, search, email and diffs.
  • Persisting private signed URLs or local previews: upload callbacks return a durable src, with an optional signed displaySrc for immediate preview.

Import

import { Editor } from '@revido-llc/kit/editor';

Keyboard interaction

  • ⌘B / ⌘I / ⌘U / ⌘⇧X — formatting
  • ⌘K — inline link field
  • ⌘E — code
  • ⌥F10 — focus the format bar
  • / after whitespace or at a block start — insert a block; unmatched commands close the menu
  • @ — search configured sources; # / [[ — dedicated triggers when configured
  • Arrow keys / Tab / Enter / Escape — navigate reference groups, choose or dismiss suggestions
  • Composer Enter — send; Shift+Enter — new line
  • Resize handle ↑ / ↓ — 16px; Home / End — minimum / maximum

Props

Props documented by the Kit. Refer to the TypeScript types for the complete interface, including inherited props.

aria-label (required)string—
agentEditorAgentConfig—Opt into the mounted agent bridge for this app-authorized record field.
classNamestring—
commentAnchors{ anchors: { id: string; from: number; to: number; }[]; onCommentSelection?: ((anchor: { from: number; to: number; text: string; }) => void); onAnchorClick?:…—
defaultMarkdownstring—
extraSlashCommandsSlashCommand[]—
onChange((value: { json: RichDoc; markdown: string; }) => void)—
onSubmit(() => void)—
onUploadImageMediaUploader—
onUploadVideoMediaUploader—
placeholderstring—
readOnlyboolean—
resolveMediaSrcMediaSrcResolver—Overrides the provider's display resolver without changing saved media identities.
sourcesReferenceSource<unknown>[]—Extends EditorProvider sources, overriding an existing source with the same id.
submitOnModEnterboolean—Composer integration: reserve Cmd/Ctrl+Enter for submission instead of a hard break. Default false.
templateHeadingsreadonly TemplateHeading[]—Create-only template headings: protect their text and show a guidance line under each. Read at mount.
valueRichDoc—
variant"inline" | "document" | "composer"—

Related exports

EditorComposer, EditorReadOnly, EditorProvider

Examples

Descriptions and documents

import { Editor } from "@revido-llc/kit/editor";
<Editor aria-label="Description" defaultMarkdown={legacyMarkdown} onChange={({ json, markdown }) => save({ json, markdown })} />

References

One @ search groups People, Docs, Issues and Projects. Each source controls its search row, inline chip and link target.

import { Editor, type ReferenceSource } from "@revido-llc/kit/editor";
const usersSource: ReferenceSource = {
  id: "user", label: "People",
  search: (query, { signal }) => api.searchPeople(query, { signal }),
  renderItem: (item) => <PersonRow person={item} />,
  renderChip: (item) => <PersonChip name={item.label} />,
  href: (item) => `/people/${item.id}`,
};
<Editor aria-label="Description" sources={[usersSource, docsSource, issuesSource, projectsSource]} />

Per-project setup

Share one registry across a project's document and comment editors. Dedicated # and [[ triggers search Issues and Docs; @ searches People and Projects.

import { Editor, EditorComposer, EditorProvider } from "@revido-llc/kit/editor";
const projectSources = [
  usersSource,
  { ...docsSource, trigger: "[[" },
  { ...issuesSource, trigger: "#" },
  projectsSource,
];
<EditorProvider sources={projectSources}>
  <Editor aria-label="Project document" value={document} />
  <EditorComposer aria-label="Comment" value={draft} onSubmit={send} />
</EditorProvider>

Inline images

Paste, drop or choose images with /image. This fixture uploader reports progress and resolves after 800ms; selection, alt text, caption and resize remain in the editor.

import { Editor } from "@revido-llc/kit/editor";
<Editor aria-label="Description" onUploadImage={async (file, { onProgress }) => {
  const image = await storage.uploadImage(file, { onProgress });
  return { src: image.url, alt: file.name, width: image.width, height: image.height };
}} />

Private media

The document keeps a durable storage path. This demo denies access and shows the neutral placeholder; your app resolves authorized paths to signed display URLs.

import { Editor, EditorProvider } from "@revido-llc/kit/editor";
<EditorProvider resolveMediaSrc={(src, { kind, signal }) => storage.signAuthorizedMedia(src, { kind, signal })}>
  <Editor aria-label="Description" onUploadImage={async (file, { onProgress }) => {
    const media = await storage.uploadImage(file, { onProgress });
    return { src: media.path, displaySrc: media.signedUrl, alt: file.name };
  }} />
</EditorProvider>
// Signed display URLs stay in the session; JSON and Markdown retain media.path.

Image upload failure and retry

The demo fails each file's first attempt. Its inline Retry reuses the file; Remove discards the placeholder.

import { Editor } from "@revido-llc/kit/editor";
<Editor aria-label="Description" onUploadImage={async (file, { onProgress }) => {
  // Throw on failure; the editor keeps the file for Retry and Remove.
  return storage.uploadImage(file, { onProgress });
}} />

Video embeds and upload

Paste a YouTube, Vimeo or Loom URL on its own line for a privacy-friendly embed. /video accepts a URL or a file when onUploadVideo is configured.

import { Editor } from "@revido-llc/kit/editor";
<Editor aria-label="Description" onUploadVideo={async (file, { onProgress }) => {
  const video = await storage.uploadVideo(file, { onProgress });
  return { src: video.url, alt: file.name };
}} />
// JSON stores video nodes; derived Markdown uses a fenced video directive
// containing JSON attributes so agent edits round-trip hosted and uploaded videos.

Comment composer

import { EditorComposer } from "@revido-llc/kit/editor";
<EditorComposer aria-label="Comment" sources={projectSources} value={draft} onChange={({ json }) => setDraft(json)} onSubmit={send} storageKey="comment-height" footer={{ left: <InternalOnly />, right: <Send /> }} />

Compact record comment

Rest, focus, retained draft and dark states. One centred 20px line on a subtle sunken surface; the ghost send becomes primary for content, and focus reveals tools beneath it.

import { EditorComposer } from "@revido-llc/kit/editor";
<EditorComposer size="compact" aria-label="Comment" value={draft} onChange={({ json }) => setDraft(json)} onSubmit={send} sendAction={{ onPress: send, loading: sending }} storageKey="record-comment-height" footer={{ left: <InternalOnly /> }} />

Read-only with identity links and anchors

import { EditorProvider, EditorReadOnly } from "@revido-llc/kit/editor";
<EditorProvider sources={projectSources}>
  <EditorReadOnly value={record.description_json} markdown={record.description} />
</EditorProvider>

Portable description

A literal Markdown seed composes without application callbacks or reference sources.

import { Editor } from "@revido-llc/kit/editor";
<Editor aria-label="Portable description" defaultMarkdown="Write the delivery plan here." />

Portable composer

import { EditorComposer } from "@revido-llc/kit/editor";
<EditorComposer aria-label="Portable comment" defaultMarkdown="Please review the plan." />

Portable read-only document

import { EditorReadOnly } from "@revido-llc/kit/editor";
<EditorReadOnly value={{ type: "doc", content: [{ type: "paragraph", attrs: { blockId: "summary" }, content: [{ type: "text", text: "Delivery is ready for review." }] }] }} />

Related components