@brett_lamy/docstream-editor
TipTap editor components for Docstream GitBook-style markdown documents.
@brett_lamy/docstream-editor provides the editable layer for the same document model rendered by @brett_lamy/docstream. It is intended for docs apps that need rich editing while preserving GitBook-flavored markdown blocks for preview, storage, and AI stream rendering.
Features
- Controlled React editor component for GitBook-style markdown.
- TipTap extensions for common writing flows.
- Conversion helpers between the Docstream AST and TipTap JSON.
- Slash menu for inserting supported blocks.
- Code block syntax highlighting through
gpu-lexer(language-agnostic, WebGPU; plain text where WebGPU is unavailable). - Table, task list, heading, quote, and inline formatting support.
- Preserves Docstream/GitBook block semantics when converting back to markdown.
- Copy a docs page (docstream's "Copy page") and paste it in: the editor shows the same page and
serializes it back byte-for-byte — demos with their inline files, install command boxes, titled
tab sets and all. Inline code may combine with links, bold, italic and strike
(
[`@brett_lamy/ui`](url),**bold `x` text**); the editor'sGbCodemark allows it. - Lossless: any Markdown page comes back byte-for-byte through the editor. docstream's layout fields
(blank lines, fences, list markers, tag lines, raw HTML, …) ride on each node's
mdattribute (GbMarkdownLayout); link details and code-span backticks on the link / code marks; escapes as themdEscapedmark; nested same-type emphasis asmoreon the mark; reference definitions and unknown{% tags %}asgbDefinition/gbRawblocks; chips and inline images carry emphasis and link marks. - Preserves mounted source-file/export provenance and can write edits to the real file through Vite.
Installation
pnpm add @brett_lamy/docstream-editor @tiptap/core @tiptap/extension-code @tiptap/extension-code-block \
@tiptap/extension-list @tiptap/extension-placeholder @tiptap/extension-table @tiptap/pm \
@tiptap/react @tiptap/starter-kit @tiptap/suggestion react react-dom
React and the TipTap packages (including @tiptap/extension-code) are peer dependencies. @brett_lamy/docstream is a runtime dependency (pinned to the same version as the editor) because the editor uses the same parser, serializer, OpenAPI rendering, and asset helpers. Nothing in the install runs a build script, and the editor never references Docstream's optional almost-node playground (@agent-wasm/core) — see liveRenderer below to opt in.
Document properties and task states (1.9)
YAML frontmatter is part of the document model. The editor shows a compact, editable property sheet
above the body; documentProperties={false} hides it and documentProperties={{ defaultOpen: true }}
expands it. Property edits use the normal onChange and undo/redo history. Body edits preserve the
original YAML, and controlled Markdown updates refresh both properties and body. Custom editors can
use GbDocumentProperties, DocumentPropertiesEditor and editor.commands.setDocumentProperties().
Task checkboxes support [ ], [~], [?], [b], [d], [!], [x], and [-] with shared Docstream
icons and tooltips. Click any marked state to clear it, then click again to verify it. Right-click or
Shift+F10 opens the state picker with every icon and label. Choices serialize to the original marker
and participate in undo/redo. GbTaskItem and editor.commands.setTaskState(state) are available to
custom editors built with createGitbookExtensions.
Migrating to 1.5
No breaking changes. Copying a selection with demo blocks now writes the docstream
clipboard (see Copy and paste demos between editors),
pasting one restores the demos with their files, and demo blocks gain a drag grip. New props:
demoVFS, onDemoFilesChange, pastedDemoFiles, demoClipboard. react-aria is now a
dependency (useClipboard).
Migrating to 1.4
No API changes. Exports resolve to compiled dist/*.js + .d.ts instead of src/*.ts(x)
(Docstream 1.4 does the same); drop any optimizeDeps / transpilePackages entries or
type-check settings you added for the raw sources.
Migrating to 1.3
GitbookEditorno longer runslivejsx/tsx code blocks in almost-node on its own. PassliveRenderer={almostNodeLiveRenderer}(from@brett_lamy/docstream/playground) to keep the preview.ReactDemoEditortakes the sameliveRendererprop; without it the files stay editable and a short note replaces the preview.
Basic Setup
Import both package styles near your app entrypoint:
import "@brett_lamy/docstream/styles.css"
import "@brett_lamy/docstream-editor/styles.css"
If your TypeScript app checks CSS side-effect imports, include Vite's standard environment declaration or an equivalent CSS module declaration:
/// <reference types="vite/client" />
Usage
GitbookEditor is a controlled component. Pass the current markdown and receive updated markdown through onChange.
import { useState } from "react"
import { GitbookEditor } from "@brett_lamy/docstream-editor"
import "@brett_lamy/docstream/styles.css"
import "@brett_lamy/docstream-editor/styles.css"
const initialMarkdown = `# Getting started
{% hint style="info" %}
Type / to insert supported blocks.
{% endhint %}
`
export function EditorPage() {
const [markdown, setMarkdown] = useState(initialMarkdown)
return (
<GitbookEditor
markdown={markdown}
onChange={setMarkdown}
/>
)
}
Props
Every prop is optional.
export interface GitbookEditorProps {
markdown?: string
onChange?: (markdown: string) => void
documentProperties?: boolean | { defaultOpen?: boolean } // default true
toolbar?: boolean // default true
slashMenu?: boolean | { items?: SlashItem[] } // default true
references?: boolean | ReferenceSources // default true
editable?: boolean // default true
placeholder?: string // default "Write, or type / to insert a block…"
className?: string
autofocus?: boolean // default false
disableHistory?: boolean // default false
extensions?: AnyExtension[]
onEditorReady?: (editor: Editor | null) => void
onKeyDown?: (event: KeyboardEvent) => boolean
onPaste?: (event: ClipboardEvent) => boolean
imagePaste?: "inline" | "chip"
attachments?: EditorAttachment[]
onAttachmentAdd?: (attachment: EditorAttachment & { file: File }) => void
onAttachmentRemove?: (ids: string[]) => void
onAttachmentOpen?: (attachment: EditorAttachment) => void
demoResolver?: DemoResolver
demoRuntime?: InlineDemoRuntime
liveRenderer?: LivePreviewRenderer
demoDependencies?: Record<string, string>
markdownPaste?: boolean
demoVFS?: DemoVFS
onDemoFilesChange?: (files: Readonly<Record<string, DemoFile[]>>, change: DemoVFSChange) => void
pastedDemoFiles?: "inline" | "vfs"
demoClipboard?: boolean
sourceClient?: SourceReferenceClient
sourcePreview?: boolean // default true
sourceAutoSave?: boolean | number // default true (500ms)
onSourceSaved?: (snapshot: SourceFileSnapshot) => void
onSourceError?: (error: Error) => void
resolveAsset?: AssetResolver
pickFile?: FilePicker
uploadFile?: FileUploader
onDropEmbeds?: (data: DataTransfer) => Promise<readonly string[]> | readonly string[] | null
}
-
markdown: Source markdown. When given, the editor is controlled; omit it for an uncontrolled editor that starts empty. -
onChange: Called with serialized markdown whenever TipTap content changes. -
documentProperties,toolbar,slashMenu,references,editable,placeholder,className,autofocus: the property sheet, the formatting toolbar, the/menu, the@/#/$pickers (see Mentions, channels, and codebases), read-only mode, the empty-document placeholder, the wrapper class (gb <className>), focus on mount. -
disableHistory,extensions,onEditorReady: Yjs-style integrations — turn off StarterKit's undo/redo, append TipTap extensions, receive the TipTapEditor(andnullon teardown). -
autofocus,placeholder,slashMenu,references,disableHistoryandextensionsare read when the TipTap editor is created; changing them later has no effect, so remount (e.g. with akey) to switch them.editablefollows the prop at any time. -
onKeyDown/onPaste: Optional host hooks that run before the built-in editor behavior; returntruewhen the application handled the event. -
imagePaste,attachments,onAttachmentAdd,onAttachmentRemove,onAttachmentOpen: how pasted/dropped images are handled — see Pasted and dropped images. -
demoResolver: resolves{% demo src %}ids so demo blocks show a live, read-only preview — see Demo blocks. -
demoRuntime/demoDependencies: run demos that carry their files inline and have no resolver behind them — the same options as Docstream'sDocRenderOptions. -
liveRenderer: renders the preview abovelivejsx/tsx code blocks (as Docstream'sliveRenderer). The editor never imports Docstream's almost-node playground itself; to run live code, passalmostNodeLiveRenderer/createAlmostNodeDemoRuntime()from@brett_lamy/docstream/playground(and install@agent-wasm/core). Without them, live code blocks are edited as plain code and inline demos open on their Code view. -
markdownPaste(defaulttrue): plain-text Markdown pasted into the editor that contains blocks (a "Copy page" export,{% … %}tags, fences, lists, headings …) is parsed into editor blocks instead of pasted as literal text. Plain prose pastes as usual; HTML clipboards keep TipTap's own handling (except VS Code's, which is treated as plain text). -
demoVFS,onDemoFilesChange,pastedDemoFiles,demoClipboard: demo copy / paste and the demo virtual file system — see Copy and paste demos between editors. -
sourceClient,sourcePreview,sourceAutoSave,onSourceSaved,onSourceError: edit the files behind{% source-ref %}blocks — see Edit a referenced source file. -
resolveAsset,pickFile,uploadFile,onDropEmbeds: see Files from the host's project.
The editor tracks the last markdown it emitted so normal controlled updates do not continuously reset the TipTap document. Passing a different external markdown value replaces the editor content.
Supported Editing Surface
The editor supports common ProseMirror/TipTap content plus GitBook-flavored blocks from Docstream.
- Headings
- Paragraphs
- Bold, italic, strike, inline code, and links (a link inside emphasis,
**[x](url)**, stays distinct from emphasis inside a link,[**x**](url)) - Bullet, ordered, and task lists
- Blockquotes
- Code blocks
- Tables
- Hints
- Tabs (including
{% tabs sync="key" %}sync groups, and titled sets{% tabs title="Installation" level="3" %}rendered as a section heading with the switch on its row — click the heading icon in the tab strip to add a title) - Package-manager command boxes (
{% command %}npm install x{% endcommand %},/install command) - Expandables
- Steppers
- Embeds
- Content references
- Component and Storybook source references
- Live demos (
{% demo %}), resolver-backed or carrying their files inline - Columns
- Figures and images
- OpenAPI operation placeholders
Some blocks are intentionally represented as structured nodes rather than fully bespoke editing controls. They are preserved through parse, edit, and serialize flows so the document can continue to round-trip as GitBook-style markdown.
Mentions, channels, and codebases
Typing @, #, or $ opens a picker and inserts a reference chip that serializes as
@id, #id, or $id ($org/repo works too). Feed each trigger a static list or an
async (query) => options function; options can carry a label, description, and
group (rendered as section headers). $ is only enabled when codebases is set, so
dollar signs in ordinary prose never open a menu. Free-form entry is always offered.
<GitbookEditor
markdown={md}
onChange={setMd}
references={{
mentions: [
...people.map((p) => ({ id: p.handle, label: p.name, description: p.email, group: "People" })),
...agents.map((a) => ({ id: a.handle, label: a.name, description: a.harness, group: "Agents" })),
],
tags: async (query) => searchChannels(query), // [{ id: "general", description: "channel" }]
codebases: async (query) => searchRepos(query), // [{ id: "reading-room" }]
}}
/>
Pasted and dropped images
imagePaste controls how pasted or dropped image files enter the document. It runs after
your onPaste (return true there to take over). Clipboard data that also carries plain
text, such as a Word or Excel selection, still pastes as text.
"inline"(default): an image block with the file as a data URL, the way it will render."chip": a compact attachment chip at the caret. The host stores the file. The chip serializes as, shows a thumbnail and size, previews the full image on hover, and opens it on click.
const [markdown, setMarkdown] = useState("")
const [attachments, setAttachments] = useState<EditorAttachment[]>([])
<GitbookEditor
markdown={markdown}
onChange={setMarkdown}
imagePaste="chip"
attachments={attachments}
// once per image: { id, name, src (data URL), size, type, file }
onAttachmentAdd={({ file, ...attachment }) => setAttachments((all) => [...all, attachment])}
// chips deleted, cut or cleared by the user
onAttachmentRemove={(ids) => setAttachments((all) => all.filter((a) => !ids.includes(a.id)))}
// optional; omit it to use the built-in full-screen viewer
onAttachmentOpen={(attachment) => openLightbox(attachment)}
/>
EditorAttachment is { id, name, src?, size?, type? }. Chips find their image in
attachments by id, so src can be a data URL or an uploaded URL. To remove a chip
from outside the editor, call editor.commands.removeAttachment(id) (get the instance
from onEditorReady). Markdown images with an attachment: URL always load as chips,
whatever the mode. serializeEditorMarkdown(ast) is the serializer that writes them in
this form.
Files from the host's project
An app that edits Markdown from a folder can plug in its own files. All four props are optional.
<GitbookEditor
markdown={markdown}
onChange={setMarkdown}
// "./case/part.stl" as written → a URL the browser can load (blob: of the local file).
resolveAsset={(url) => (url.startsWith("./") ? blobUrlFor(url) : null)}
// Adds "Choose file…" to embed blocks ("Choose image…" to figures).
pickFile={({ kind }) => openMyFilePicker({ imagesOnly: kind === "image" })}
// Drops you claim become embeds at the drop point: images as figures, anything else as an embed.
onDropEmbeds={(data) =>
data.types.includes("Files") ? copyIntoProject([...data.files]).then((paths) => paths.map((p) => `./${p}`)) : null
}
// Adds "Upload…" to embed and image blocks; dropped or pasted files that aren't images land here too.
uploadFile={(file) => saveNextToDocument(file).then((path) => `./${path}`)}
/>
resolveAssetcovers figures, inline images and.stl/.strudel/.rrwebembeds (they wait for a promise). A.rrwebembed is an rrweb recording (a JSON event array) played in docstream's replay player.pickFileresolves to the URL to write, or nothing when the user cancels.onDropEmbedsdecides synchronously: returnnulland the editor handles the drop as before (images inline, demo blocks).embedBlockFor(url)is the block a URL becomes.- Thumbnail images (
, docstream 1.18) show as docstream's thumbnail with its hover card; first in a stepper step's heading, the thumbnail sits beside the title input and survives retitling. uploadFile(file, { kind })stores the file and resolves to the URL to write, or nothing to skip it. Dropped or pasted files that aren't images (.rrwebrecordings,.stlmodels, …) are uploaded and embedded where they land; images still go inline (or to chips) as before, andonDropEmbedssees drops first. With docstream's Vite plugin,POST /@docstream/source?mount=…&path=…with the file as the body writes it to disk.
Edit a referenced source file
GitbookEditor edits the Markdown composition, including a structured
source-ref node. SourceFileEditor edits the real file behind that node:
import { createViteSourceClient, type SourceRefNode } from "@brett_lamy/docstream"
import { SourceFileEditor } from "@brett_lamy/docstream-editor"
const client = createViteSourceClient()
const reference: SourceRefNode = {
type: "source-ref",
mount: "ui",
path: "Button.stories.tsx",
exportName: "Primary",
kind: "story",
}
<SourceFileEditor reference={reference} client={client} />
Saving calls the Docstream Vite plugin, writes the mounted file, returns its
provenance, and refreshes the live component/story preview. Configure the mount
with docstreamSources() from @brett_lamy/docstream/vite as shown in the
Docstream README.
Demo blocks
{% demo src="<page>/<example>" %} becomes a gbDemo atom node. Every tag
attribute — src, title, description, height, layout, variants,
viewport, entry — is kept on the node, and so are the files the block form
carries inline (the titled fences between {% demo … %} and {% enddemo %}, as
files: { path, content, language? }[]), so the block serializes back exactly as
written. Docstream's "Copy page" writes every demo this way.
The node shows a compact header with editable src and title inputs (insert
one with /demo) and a Files toggle that opens a small file editor: a tab
per file (rename it in place, mark it the entry, remove it), a code area, and
+ to add a file. Removing every file gives the one-line tag form again.
Under the header, Docstream's read-only DemoViewer renders the demo: from the
demoResolver when it knows src, otherwise from the inline files — so a
pasted docs page looks like the docs page. Inline files run in the Preview
through demoRuntime (e.g. createAlmostNodeDemoRuntime() from
@brett_lamy/docstream/playground) with demoDependencies; without a runtime
the viewer shows their code.
import { createGlobDemoResolver } from "@brett_lamy/docstream/demo"
import { GitbookEditor } from "@brett_lamy/docstream-editor"
const demoResolver = createGlobDemoResolver({ /* see the Docstream README */ })
<GitbookEditor markdown={markdown} onChange={setMarkdown} demoResolver={demoResolver} />
Copy and paste demos between editors
Copying (or cutting) a selection that contains demo blocks — a single selected
demo, or prose with demos in it — writes one clipboard item through react-aria's
useClipboard:
| Type | Content |
|---|---|
text/plain, text/markdown | renderable Markdown: every demo carries its files inline (src-only demos get the resolver's files and meta.json), so it renders anywhere |
application/x-docstream+json | { kind: "docstream", version: 1, markdown, files: { [src]: DemoFile[] }, meta: { [src]: DemoMeta } } — the exact Markdown plus every demo's files |
Any other selection keeps ProseMirror's normal copy. Docstream's DemoViewer
Copy demo button (inside the editor and on rendered pages) writes the same
item for one demo. Resolver files are fetched ahead (prefetchDemoSources), as a
copy event can't wait for them.
Pasting prefers the JSON payload, and falls back to Markdown with inline-file
demos (text/plain / text/markdown only). Pasted demo blocks keep their inline
files, and every demo's files are registered in the editor's demo VFS — an
in-memory { [src]: { files, meta } } store — so they render:
- with the host
demoResolverwhen it knowssrc; - otherwise from the block's inline files or the VFS files, run by
demoRuntime(createAlmostNodeDemoRuntime()from@brett_lamy/docstream/playground, when installed); - otherwise in the Code view with the "needs runtime" note.
pastedDemoFiles decides where a pasted src-only demo's files go: "inline"
(default) — into the VFS and inline in the block (unless the host resolver lists
src), so the saved Markdown is self-contained; "vfs" — the VFS only, the block
keeps its exact copied structure; persist the files yourself.
import { GitbookEditor, useDemoVFS, useDemoVFSFiles } from "@brett_lamy/docstream-editor"
const vfs = useDemoVFS() // one VFS shared by both editors
const files = useDemoVFSFiles(vfs) // { [src]: DemoFile[] } — or use onDemoFilesChange
<GitbookEditor markdown={a} onChange={setA} demoResolver={demoResolver} demoVFS={vfs} />
<GitbookEditor markdown={b} onChange={setB} demoVFS={vfs} demoRuntime={demoRuntime}
pastedDemoFiles="vfs" onDemoFilesChange={(files) => save(files)} />
Without demoVFS each editor has its own. demoClipboard={false} turns the
demo copy / paste handling off. Pass the same VFS to a renderer (demoVFS on
Docstream's MarkdownContent / DocsRenderer) to render the saved Markdown with
the pasted files.
Drag. Each demo block has a grip: drag it into another docstream editor to
copy the demo there (same payload, files included), or within its editor to move
it. The drag uses native HTML5 drag-and-drop with the docstream payload rather than
react-aria's useDrag / useDrop: useDrag insists on useDrop targets, which
would sit on top of ProseMirror's own drop handling. Keyboard / screen-reader drag
is future work (copy / paste covers it).
Lower-level pieces are exported for custom editors: selectionClipboard(editor, { resolver, vfs }),
payloadContent(payload, { resolver, vfs, mode }), registerPayloadFiles,
clipboardForDoc, useEditorClipboard, plus Docstream's createDemoVFS,
readDocstreamClipboard and DOCSTREAM_CLIPBOARD_TYPE.
Command boxes
{% command %}npm install @brett_lamy/ui{% endcommand %} (or the block form, for
several lines) becomes a gbCommand atom node rendered with Docstream's own
command box: terminal glyph, pnpm / npm / yarn / bun switch (synced page-wide),
copy button. When editable, the npm command and per-manager overrides
(pnpm="…", yarn="…", bun="…"; the placeholder shows the derived command)
are edited under the box. sync is preserved. Insert one with /install command.
Slash Menu
The editor includes a slash menu extension for inserting supported block structures. Type / at the
start of a line or after a space to open block insertion options; typing filters them by title and
keywords. Pass slashMenu={{ items }} (default list: SLASH_ITEMS) for your own items, or
slashMenu={false} to turn it off.
Conversion Helpers
Use the conversion helpers when you need to inspect or transform editor state directly.
import { astToTiptap, serializeEditorMarkdown, tiptapToAst } from "@brett_lamy/docstream-editor"
import { parseMarkdown } from "@brett_lamy/docstream"
const ast = parseMarkdown(markdown)
const tiptapJson = astToTiptap(ast)
const back = tiptapToAst(tiptapJson)
// TipTap JSON has no place for footnote definitions; carry them across as GitbookEditor does.
if (ast.citations?.length) back.citations = ast.citations
const markdownAgain = serializeEditorMarkdown(back) // === markdown
Exports:
GitbookEditor,GitbookEditorProps,embedBlockFor, and the prop typesEditorAttachment,FilePicker,FilePickKind,FileUploader,AssetResolverastToTiptap,tiptapToAst,PMNode,serializeEditorMarkdown,markdownPasteContent(the Markdown-paste parser behindmarkdownPaste)SourceFileEditor,SourceFileEditorProps,ReactDemoEditor,ReactDemoEditorPropsGbDocumentProperties,DocumentPropertiesEditor,GbTaskItem, plus Docstream'sTaskCheckbox,TASK_STATE_ICONS,TASK_STATES,TaskState,DocumentPropertiesNode- Building blocks for custom editors:
createGitbookExtensions,GitbookExtensionOptions,ReferenceSources,getGitbookSchema,GbCode,GbTable,GbLinkInner,GbMarkSpelling,gitbookNodes,GbCodeBlock,GbCommand,GbDemo,GbReference,GbAttachment,formatBytes,SlashMenu,createSlashMenu,SLASH_ITEMS,SlashItem,createReferenceSuggestion,resolveReferenceOptions,ReferenceOption,ReferenceSource,ReferenceSuggestionOptions,ReferenceTrigger,SuggestionMenuView,createSuggestionRender,SuggestionMenuItem - Demo clipboard and VFS:
clipboardForDoc,lookupDemoSource,payloadContent,registerPayloadFiles,selectionClipboard,useEditorClipboard,ClipboardBridge,DemoSources,PastedDemoFiles, plus Docstream'sDOCSTREAM_CLIPBOARD_TYPE,createDemoVFS,readDocstreamClipboard,useDemoVFS,useDemoVFSFiles,DemoVFS,DemoVFSChange,DemoVFSEntry,DocstreamClipboardPayload
Two narrow entrypoints are also published. ./editor exports only GitbookEditor, embedBlockFor
and their types — it still loads every node view (including the source-file editor used by
{% source-ref %} blocks), but not ReactDemoEditor or the building-block re-exports. ./convert
exports the conversion functions (plus pmToDemo, pmDemoFiles, pmToCommand) and depends only
on @brett_lamy/docstream/gitbook — no React, no TipTap:
import { GitbookEditor } from "@brett_lamy/docstream-editor/editor"
import { astToTiptap } from "@brett_lamy/docstream-editor/convert"
Styling and Theming
The editor CSS is designed to sit beside @brett_lamy/docstream/styles.css and inherit the same application theme tokens. In a shadcn-style app, define your theme variables globally and import both CSS entrypoints once.
The editor reads the tokens as complete colors (var(--card, #ffffff)), so define them as colors —
not as shadcn v3 HSL channel triplets such as 0 0% 100%, which would produce invalid values:
:root {
--card: #ffffff;
--background: #ffffff;
--foreground: #0f172a;
--border: #e2e8f0;
--muted: #f1f5f9;
--muted-foreground: #64748b;
--accent: #f1f5f9;
--accent-foreground: #0f172a;
--primary: #2563eb;
}
Editor-specific tokens (--gb-radius, --gb-code-bg, the --gb-info / --gb-success /
--gb-warning / --gb-danger hint palette, …) are declared on .gb, so override them there. The
slash / reference menus and the attachment hover card render into document.body and read the
host tokens (plus --popover / --popover-foreground) from :root.
The editor exposes classes such as gb, gb-toolbar, gb-tool, gb-tool-active, and gb-content for host app overrides.
Bundler Notes
Since 1.4 the package ships compiled ES modules (ES2022, react-jsx) with type
declarations and declaration maps — one file per source module in dist/, TipTap,
Docstream and every other dependency left as imports:
{
"exports": {
".": {
"source": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./editor": {
"source": "./src/editor/Editor.tsx",
"types": "./dist/editor/Editor.d.ts",
"import": "./dist/editor/Editor.js",
"default": "./dist/editor/Editor.js"
},
"./convert": {
"source": "./src/editor/convert.ts",
"types": "./dist/editor/convert.d.ts",
"import": "./dist/editor/convert.js",
"default": "./dist/editor/convert.js"
},
"./styles.css": "./dist/styles.css"
}
}
No bundler configuration is needed: Vite pre-bundles the editor (and TipTap's CommonJS
dependencies such as use-sync-external-store) like any other dependency, so no
optimizeDeps entries; Next.js and webpack consume it as plain ESM; your type-check
reads .d.ts files instead of the editor's sources. The TypeScript sources are
published too, behind a source export condition, for monorepos that compile from source.
Renderer Pairing
For read-only previews or AI stream output, pair this package with @brett_lamy/docstream:
import { GitbookStreamdown } from "@brett_lamy/docstream"
<GitbookStreamdown markdown={markdown} isStreaming={false} />
Embed nodes preserve the Docstream media attributes (title, poster,
autoplay, loop, muted, and controls) when converting between the
editable TipTap document and GitBook markdown. This makes short, muted,
autoplaying clips round-trip through the editor without losing their playback
contract.
Live diagram blocks
xstate and tscircuit fences use source-and-preview cards like Mermaid. Edit the source directly; the shared Docstream preview updates after a short pause. Type / and choose XState diagram or tscircuit example to insert a starting point. XState reads JSON or static TypeScript createMachine({...}) configs without executing actions or guards. The isolated hosted tscircuit runtime requires a network connection. Both fence languages are preserved when saving Markdown.