@vibeflui/editors
v1.1.0
Published
Rich-text (TipTap), code (CodeMirror), markdown (react-md-editor), and Editor.js (with full block-tool wiring) field adapters for VibeFlui / FluiKit.
Maintainers
Readme
@vibeflui/editors
Rich-text, code, markdown, and Editor.js field adapters for VibeFlui / FluiKit.
| Field type | Adapter key | Library |
| --- | --- | --- |
| richtext | richtext | TipTap (@tiptap/react, @tiptap/starter-kit, @tiptap/pm) |
| code | code | CodeMirror (@uiw/react-codemirror) |
| markdown | markdown | react-md-editor (@uiw/react-md-editor) |
| json | editorjs | Editor.js (@editorjs/editorjs, plus any @editorjs/* block tool you wire with buildEditorJsTools — see the Editor.js section below) |
The editor libraries are optional peer dependencies — install only the ones you use. Core FluiKit never pulls them in. @vibeflui/editors itself never imports @editorjs/* tool packages (Header, List, Image, ...) either — those are wired from your own registry file, so your bundle only includes the tools you actually installed.
The included adapters follow the active light/dark theme: TipTap receives dark-aware container classes, CodeMirror switches between light and dark themes, react-md-editor receives the matching data-color-mode, and the Editor.js holder receives dark-aware container classes.
Install
# pick what you need
npm install @vibeflui/editors @uiw/react-codemirror
npm install @vibeflui/editors @tiptap/react @tiptap/starter-kit @tiptap/pm
npm install @vibeflui/editors @uiw/react-md-editor
npm install @vibeflui/editors @editorjs/editorjsUsage
Register a per-editor plugin (tree-shakeable — only the imported editor's library ends up in your bundle):
import { FluiKit, FluiKitProvider } from "@vibeflui/core"
import { codePlugin } from "@vibeflui/editors"
<FluiKitProvider plugins={[codePlugin()]}>
<FluiKit schema={schema} />
</FluiKitProvider>Reference the adapter from schema:
form: {
fields: [
{ name: "config", label: "Config", type: "code", adapter: "code" },
{ name: "body", label: "Body", type: "richtext", adapter: "richtext" },
{ name: "notes", label: "Notes", type: "markdown", adapter: "markdown" },
{ name: "content", label: "Content", type: "json", adapter: "editorjs" }
]
}Use createEditorsPlugin() to register all four at once (requires all four libraries).
Editor.js
Editor.js is a block editor — its field value is Editor.js's own OutputData shape ({ time, blocks, version }), not a string, which is why it pairs with type: "json" rather than a dedicated field type.
npm install @vibeflui/editors @editorjs/editorjsimport { FluiKitProvider } from "@vibeflui/core"
import { editorJsPlugin } from "@vibeflui/editors"
<FluiKitProvider plugins={[editorJsPlugin()]}>
<FluiKit schema={schema} />
</FluiKitProvider>With no tools configured, Editor.js only renders its built-in tools: paragraph blocks, plus inline Bold/Italic/Link formatting. That's it — no Header, List, Quote, Table, Image, or anything else, since every other Editor.js block is a separate npm package. This is the "only text" behavior you get out of the box.
Enabling the full tool set
Header, List, Checklist, Quote, Warning, Table, Code, Embed, Image, Link preview, Raw HTML, Marker, Inline Code, and Underline are each a separate @editorjs/* package. Install the ones you want, import their classes in your own registry file, and wire them with buildEditorJsTools — @vibeflui/editors itself never imports these packages, so your bundle only ever includes the tools you actually installed:
npm install @editorjs/header @editorjs/list @editorjs/checklist @editorjs/quote \
@editorjs/warning @editorjs/delimiter @editorjs/table @editorjs/code \
@editorjs/embed @editorjs/image @editorjs/link @editorjs/raw \
@editorjs/marker @editorjs/inline-code @editorjs/underlinelib/vibeflui/editorjs-tools.ts
import Header from "@editorjs/header"
import EditorjsList from "@editorjs/list"
import Checklist from "@editorjs/checklist"
import Quote from "@editorjs/quote"
import Warning from "@editorjs/warning"
import Delimiter from "@editorjs/delimiter"
import EditorjsTable from "@editorjs/table"
import CodeTool from "@editorjs/code"
import Embed from "@editorjs/embed"
import ImageTool from "@editorjs/image"
import LinkTool from "@editorjs/link"
import RawTool from "@editorjs/raw"
import Marker from "@editorjs/marker"
import InlineCode from "@editorjs/inline-code"
import Underline from "@editorjs/underline"
import { buildEditorJsTools } from "@vibeflui/editors"
export const editorJsTools = buildEditorJsTools(
{
header: Header,
list: EditorjsList,
checklist: Checklist,
quote: Quote,
warning: Warning,
delimiter: Delimiter,
table: EditorjsTable,
code: CodeTool,
embed: Embed,
image: ImageTool,
linkTool: LinkTool,
raw: RawTool,
marker: Marker,
inlineCode: InlineCode,
underline: Underline
},
{
image: { endpointByFile: "/api/editorjs/upload-image" },
link: { endpoint: "/api/editorjs/link-preview" }
}
)lib/vibeflui/registry.ts
import { editorJsPlugin } from "@vibeflui/editors"
import { editorJsTools } from "./editorjs-tools"
export const editorJs = editorJsPlugin({ tools: editorJsTools })buildEditorJsTools only registers the tools whose class you actually passed in — skip an import to leave that tool out. It also applies sensible defaults per tool (inline toolbar enabled where it makes sense, Embed's common service list, Header's levels) so you don't have to know each tool's raw config shape.
Only pass an image/link endpoint once you have a server route for it (see below) — buildEditorJsTools skips registering the Image / Link Preview tools entirely when no endpoint is configured, instead of registering a tool that would fail on first use.
Narrowing tools per schema field
field.adapterProps (a plain FluiKitFieldConfig key, JSON-friendly) can restrict which of the app-registered tools one specific field enables, or override its image/link endpoint, without a second plugin registration:
form: {
fields: [
{
name: "body",
label: "Body",
type: "json",
adapter: "editorjs",
adapterProps: {
tools: ["header", "list", "quote", "linkTool"]
}
},
{
name: "avatarBio",
label: "Bio",
type: "json",
adapter: "editorjs",
adapterProps: {
tools: ["image"],
image: { endpointByFile: "/api/editorjs/upload-avatar" }
}
}
]
}Image upload and link preview endpoints
The Image and Link Preview tools need a server route — VibeFlui is FE/UI-only and doesn't provide one, but the contract each tool expects is fixed by Editor.js itself:
- Image (
endpointByFile): receives amultipart/form-dataPOST with the file under thefieldname ("file"by default). Must respond with{ "success": 1, "file": { "url": "https://..." } }. - Link preview (
endpoint): receives a GET request with the target URL as a?url=query param. Must respond with{ "success": 1, "link": "https://...", "meta": { "title": "...", "description": "...", "image": { "url": "https://..." } } }(fetch the page server-side and parse its Open Graph tags to buildmeta).
Using Editor.js outside a form
EditorJsEditor is a plain controlled component — it doesn't need <FluiKit>, <FluiKitForm>, or a registry. Use it anywhere in a React tree, e.g. a standalone content editor page:
"use client"
import { useState } from "react"
import { EditorJsEditor, type EditorJsOutputData } from "@vibeflui/editors"
import { editorJsTools } from "@/lib/vibeflui/editorjs-tools"
export function ArticleBodyEditor() {
const [value, setValue] = useState<EditorJsOutputData>();
return <EditorJsEditor value={value} onChange={setValue} tools={editorJsTools} />
}Pass tools={buildEditorJsTools({...})} the same way you would for the form field adapter (see above) — omit tools and you get Editor.js's built-in paragraph/bold/italic/inline-link only.
Stylesheets
Import the stylesheets your chosen editors need, once in your app:
import "@uiw/react-md-editor/markdown-editor.css" // markdown
// CodeMirror ships its own styles; TipTap is headless — style the `.ProseMirror` content yourself.
// Editor.js is headless too — style `.editorjs-holder` (the wrapper div) yourself.