@import-ai/omnibox-editor
A production-ready React block editor built on Tiptap. It ships a complete editor UI, Markdown/JSON conversion helpers, table editing, image upload, math, diagrams, table of contents, optional AI writing hooks, and optional Yjs collaboration support.
Features
- React component with a batteries-included Tiptap editor experience
- Editable and read-only rendering modes
- Markdown input, Tiptap JSON input, and Markdown export helpers
- GitHub-like Markdown styling for headings, lists, links, code, blockquotes, tables, images, and math
- Notion-like table editing with row/column handles, context menus, resizing, merge/split, sorting, alignment, and fit-to-width actions
- Image upload pipeline with progress, abort, success, error, size, and count controls
- KaTeX math rendering and in-editor formula editing
- Mermaid/ECharts-style diagram rendering from code blocks
- Optional table of contents sidebar
- Optional AI writing integration through host-provided handlers
- Optional Yjs collaboration and collaborative cursors
- English and Simplified Chinese UI copy with translation overrides
Installation
npm install @import-ai/omnibox-editorpnpm add @import-ai/omnibox-editoryarn add @import-ai/omnibox-editorreact and react-dom are peer dependencies and must be installed by the consuming app.
npm install react react-domQuick Start
Import the component and the compiled CSS once in your app.
import { OmniboxEditor } from "@import-ai/omnibox-editor"
import "@import-ai/omnibox-editor/style.css"
export function App() {
return (
<OmniboxEditor
placeholder="Start writing..."
onUpdate={({ json, html, markdown }) => {
console.log(json, html, markdown)
}}
/>
)
}Recommended Production Data Flow
Use Tiptap JSON as the canonical format in production. It preserves editor-specific structures such as tables, math, images, alignment, colors, and custom nodes more reliably than plain Markdown.
import { OmniboxEditor, contentToMarkdown } from "@import-ai/omnibox-editor"
import "@import-ai/omnibox-editor/style.css"
export function ResourceEditor({
initialContent,
saveResource,
}: {
initialContent: string
saveResource: (content: unknown) => Promise<void>
}) {
return (
<OmniboxEditor
content={initialContent}
onUpdate={({ json }) => {
void saveResource(json)
}}
/>
)
}
export function exportMarkdown(savedContent: unknown) {
return contentToMarkdown(savedContent as Parameters<typeof contentToMarkdown>[0], {
debug: false,
})
}For backward compatibility, content accepts both existing Markdown strings and serialized JSON strings. New writes should store json from onUpdate.
Basic Usage
Markdown Input
<OmniboxEditor content={"# Hello\n\nThis content starts as Markdown."} />Tiptap JSON Input
<OmniboxEditor
content={{
type: "doc",
content: [
{
type: "heading",
attrs: { level: 1 },
content: [{ type: "text", text: "Hello" }],
},
],
}}
/>Serialized JSON String Input
<OmniboxEditor content={JSON.stringify(savedTiptapJson)} />Read-Only Rendering
<OmniboxEditor content={savedContent} editable={false} showHeader={false} />Embedded Mode
Use variant="embedded" when the editor is rendered inside an existing page shell or panel.
<OmniboxEditor
variant="embedded"
content={savedContent}
contentWidth="100%"
showHeader={false}
showToc={false}
/>Component API
import type { OmniboxEditorProps } from "@import-ai/omnibox-editor"| Prop | Type | Default | Description |
|---|---|---|---|
content |
Content | string |
demo content | Initial or externally controlled content. Accepts Markdown, serialized JSON, Tiptap JSON documents, or Tiptap node arrays. |
editable |
boolean |
true |
Enables editing. Set to false for read-only rendering. |
placeholder |
string |
locale placeholder | Placeholder shown in empty editable blocks. |
onUpdate |
(payload) => void |
undefined |
Called after editor content changes. Use payload.json as the preferred saved value. |
imageUpload |
UploadFunction |
built-in object URL fallback | Custom image uploader. Should return the final image URL. |
imageUploadMaxSize |
number |
package default | Maximum image size in bytes. |
imageUploadLimit |
number |
3 |
Maximum number of images accepted per upload operation. |
onImageUploadError |
(error: Error) => void |
logs to console | Called when image upload fails. |
onImageUploadSuccess |
(url: string) => void |
undefined |
Called when image upload succeeds. |
linkBase |
string |
undefined |
Base URL used to resolve relative links and image sources while parsing Markdown/content. |
locale |
"en" | "zh-CN" | string |
"en" |
Built-in UI locale. |
translations |
OmniboxEditorTranslations |
undefined |
Overrides individual UI labels. |
theme |
"light" | "dark" |
undefined |
Applies package dark-mode classes when set to "dark". |
variant |
"page" | "embedded" |
"page" |
Page mode shows the default editor chrome; embedded mode is more compact. |
contentWidth |
number | string |
CSS default | Sets the editor content width. Numbers are treated as pixels. |
showHeader |
boolean |
true in page mode |
Controls the top editor header. |
showToc |
boolean |
true in page mode |
Controls the table of contents sidebar. |
mentionUsers |
OmniboxEditorMentionUser[] |
undefined |
Users shown in the mention dropdown. |
user |
OmniboxEditorCollaborationUser |
undefined |
Current user identity for local UI and collaboration fallback. |
collaboration |
false | object |
undefined |
Yjs collaboration config. |
ai |
boolean | OmniboxEditorAiConfig |
undefined |
Enables the AI UI and optionally provides an AI submit handler. |
Update Payload
onUpdate receives the live editor instance plus multiple content formats.
import type { OmniboxEditorUpdatePayload } from "@import-ai/omnibox-editor"
type OmniboxEditorUpdatePayload = {
editor: Editor
json: JSONContent
html: string
markdown: string
}Recommended usage:
- Save
jsonto your backend. - Use
markdownfor export/download/copy workflows. - Use
htmlonly when your application explicitly needs HTML output. - Debounce or autosave outside the package if the backend should not receive every keystroke.
Markdown and JSON Helpers
The package exports conversion helpers for import/export workflows.
import {
contentToMarkdown,
contentToTiptapJson,
htmlTableToTiptapNode,
markdownToTiptapJson,
markdownWithHtmlTablesToTiptapJson,
tiptapJsonToMarkdown,
type MarkdownParseOptions,
type TiptapJsonContent,
} from "@import-ai/omnibox-editor"| API | Description |
|---|---|
markdownToTiptapJson(markdown, options?) |
Parses Markdown into a Tiptap JSON document using Tiptap's official MarkdownManager, then applies package normalization for links, images, tables, and diagrams. |
markdownWithHtmlTablesToTiptapJson(markdown, options?) |
Parses Markdown that may contain raw HTML <table> blocks. HTML tables are converted into Tiptap table nodes. |
contentToTiptapJson(content, options?) |
Accepts Markdown, serialized JSON, or JSON content and returns a normalized Tiptap JSON document. Use this when the input format is unknown. |
tiptapJsonToMarkdown(json, options?) |
Serializes Tiptap JSON back to Markdown. Merged tables are emitted as HTML tables when Markdown tables cannot represent them safely. |
contentToMarkdown(content, options?) |
Accepts Markdown, serialized JSON, or JSON content and returns Markdown. Use this for export from saved server content. |
htmlTableToTiptapNode(html) |
Converts a single HTML table string into a Tiptap table node. |
Markdown Parse Options
type MarkdownParseOptions = {
debug?: boolean
linkBase?: string
}| Option | Default | Description |
|---|---|---|
debug |
true |
Logs conversion input/output to the console. Set debug: false in production import/export paths. |
linkBase |
undefined |
Resolves relative links and image sources against a base URL. |
Production example:
const json = contentToTiptapJson(serverContent, {
linkBase: "https://example.com/resources/current-folder",
debug: false,
})
const markdown = contentToMarkdown(json, { debug: false })Image Upload
Provide imageUpload to connect the editor to your storage service.
import { OmniboxEditor, type UploadFunction } from "@import-ai/omnibox-editor"
import "@import-ai/omnibox-editor/style.css"
const uploadImage: UploadFunction = async (file, onProgress, abortSignal) => {
const formData = new FormData()
formData.append("file", file)
const response = await fetch("/api/uploads", {
method: "POST",
body: formData,
signal: abortSignal,
})
if (!response.ok) {
throw new Error("Image upload failed")
}
onProgress?.({ progress: 100 })
const result = (await response.json()) as { url: string }
return result.url
}
export function Editor() {
return (
<OmniboxEditor
imageUpload={uploadImage}
imageUploadMaxSize={10 * 1024 * 1024}
imageUploadLimit={5}
onImageUploadError={(error) => {
console.error(error)
}}
/>
)
}The upload function must resolve with the final URL that should be stored in the document.
AI Integration
The package renders the AI UI only when ai is enabled. The host application owns the actual model call.
import type { OmniboxEditorAiSubmitPayload } from "@import-ai/omnibox-editor"
async function handleAiSubmit({
action,
prompt,
signal,
onChunk,
}: OmniboxEditorAiSubmitPayload) {
const response = await fetch("/api/ai/write", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action, prompt }),
signal,
})
if (!response.ok || !response.body) {
throw new Error("AI request failed")
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
onChunk?.(decoder.decode(value))
}
}
<OmniboxEditor
ai={{
enabled: true,
onSubmit: handleAiSubmit,
}}
/>onSubmit can stream plain Markdown through onChunk, or provide structured Tiptap JSON through onContent / onContentPreview.
Collaboration
Collaboration uses Yjs through Tiptap's collaboration extensions. Pass a Yjs document and, optionally, a provider with awareness support.
import * as Y from "yjs"
import { OmniboxEditor } from "@import-ai/omnibox-editor"
import "@import-ai/omnibox-editor/style.css"
const document = new Y.Doc()
<OmniboxEditor
collaboration={{
document,
provider,
user: {
id: "user-1",
name: "Ada",
color: "#3b82f6",
avatar: "https://example.com/avatar.png",
},
}}
/>When collaboration is enabled, the package disables StarterKit undo/redo and does not sync external content prop updates after initialization. Treat the Yjs document as the source of truth.
Mentions
<OmniboxEditor
mentionUsers={[
{
id: "user-1",
name: "Ada Lovelace",
color: "#3b82f6",
avatar: "https://example.com/avatar.png",
position: "Engineer",
},
]}
/>Internationalization
Use locale for the built-in language, and translations for targeted overrides.
<OmniboxEditor
locale="zh-CN"
translations={{
placeholder: "开始写点什么...",
uploadFailed: "图片上传失败",
}}
/>You can also read the merged labels:
import { getEditorTranslations } from "@import-ai/omnibox-editor"
const labels = getEditorTranslations("zh-CN", {
placeholder: "开始写点什么...",
})Styling
The package ships compiled CSS at:
import "@import-ai/omnibox-editor/style.css"The CSS includes editor layout, toolbar UI, table controls, GitHub-like Markdown rules, image states, KaTeX styles, and component variables. It is generated from Tailwind CSS v4 layers and does not require Sass.
Set dark mode with the component prop:
<OmniboxEditor theme="dark" />Constrain content width with contentWidth:
<OmniboxEditor contentWidth={860} />
<OmniboxEditor contentWidth="min(100%, 920px)" />Exported API
Components
export { OmniboxEditor }Conversion Helpers
export {
contentToMarkdown,
contentToTiptapJson,
htmlTableToTiptapNode,
markdownToTiptapJson,
markdownWithHtmlTablesToTiptapJson,
tiptapJsonToMarkdown,
}Utilities
export { getEditorTranslations }Types
export type {
EditorProviderProps,
MarkdownParseOptions,
OmniboxEditorAiAction,
OmniboxEditorAiConfig,
OmniboxEditorAiFeature,
OmniboxEditorAiSubmitPayload,
OmniboxEditorCollaborationConfig,
OmniboxEditorCollaborationProvider,
OmniboxEditorCollaborationUser,
OmniboxEditorLocale,
OmniboxEditorMentionUser,
OmniboxEditorProps,
OmniboxEditorTheme,
OmniboxEditorTranslations,
OmniboxEditorUpdatePayload,
TiptapJsonContent,
UploadFunction,
}Production Checklist
- Import
@import-ai/omnibox-editor/style.cssonce at the application entry. - Save
onUpdate().jsonas the canonical document value. - Use
contentToMarkdown(content, { debug: false })for Markdown export. - Set
debug: falsein conversion helpers used by production code. - Provide a real
imageUploadimplementation before enabling image uploads for users. - Enforce file size, MIME type, authentication, authorization, and malware scanning on the upload API.
- Debounce autosave outside the editor if you persist on
onUpdate. - Use
editable={false}for read-only views. - Pass
linkBasewhen rendering legacy Markdown with relative links or images. - Keep collaboration state in Yjs when
collaborationis enabled. - Keep AI credentials and model calls on your server; never expose secrets in the browser.
Browser and SSR Notes
@import-ai/omnibox-editor is a browser-facing React component. In SSR frameworks, render it only on the client.
Next.js example:
"use client"
import { OmniboxEditor } from "@import-ai/omnibox-editor"
import "@import-ai/omnibox-editor/style.css"
export default function EditorPage() {
return <OmniboxEditor />
}Local Development
From the repository root:
pnpm install
pnpm --filter @import-ai/omnibox-editor test
pnpm --filter @import-ai/omnibox-editor buildCreate a local tarball:
pnpm --filter @import-ai/omnibox-editor pack:localLicense
MIT