@novahelm/editor
v2026.6.1
Published
NovaHelm editor — TipTap-based rich-text editor and extensions.
Maintainers
Readme
@novahelm/editor
Rich text editor for NovaHelm — built on TipTap with custom nodes, slash commands, emoji picker, mentions, Unsplash integration, and upload storage.
Quick Start
pnpm add @novahelm/editorimport { NovaEditor, UploadStorage } from "@novahelm/editor";
import "@novahelm/editor/styles.css"; // required — import once at app root
// Configure upload integration (once, at app startup)
UploadStorage.configure({
projectSlug: "my-project",
uploadHandler: async (file: File) => {
const formData = new FormData();
formData.append("file", file);
const res = await fetch("/api/upload", { method: "POST", body: formData });
const { url } = await res.json();
return url;
},
});
// Render the editor
export function Editor({ content, onChange }) {
return (
<NovaEditor
variant="full"
format="html"
defaultValue={content}
onChange={onChange}
/>
);
}Editor Variants
| Variant | Description | Use case |
|---------|-------------|----------|
| full | All nodes, slash commands, mentions, Unsplash | Blog posts, docs, rich content |
| compact | Basic marks + headings, no slash command | Comments, notes |
| email | Email-safe subset only | Email template builder |
<NovaEditor variant="compact" format="html" onChange={setContent} />Output Formats
| Format | Description |
|--------|-------------|
| html | Sanitized HTML string (default) |
| json | TipTap JSON document (for re-editing) |
| markdown | GitHub Flavored Markdown |
Upload Storage Configuration
The editor receives its upload, search, and storage capabilities through UploadStorage.configure(). This keeps the editor decoupled from the storage layer.
import { UploadStorage } from "@novahelm/editor";
UploadStorage.configure({
// Required: project context for asset scoping
projectSlug: "my-project",
// Image/file upload handler — return the public URL
uploadHandler: async (file: File): Promise<string> => {
const { url } = await trpc.storage.upload.mutate({ file });
return url;
},
// Optional: Unsplash API key for the image search panel
unsplashKey: process.env.NEXT_PUBLIC_UNSPLASH_KEY,
// Optional: @mention search (return { id, label, avatar? }[])
searchMentions: async (query: string) => {
return trpc.users.search.query({ q: query });
},
});Custom Extensions
Use getExtensions() to get the base extension set and add your own:
import { getFullExtensions } from "@novahelm/editor";
import { MyCustomNode } from "./my-node";
const extensions = [
...getFullExtensions({ projectSlug: "my-project" }),
MyCustomNode,
];
// Pass to TipTap directly if you're using useEditor()
const editor = useEditor({ extensions, content });| Function | Extensions included |
|----------|-------------------|
| getExtensions() | Core marks, headings, lists, links |
| getFullExtensions() | Core + all custom nodes + slash commands |
| getCompactExtensions() | Core only (no slash command, no custom nodes) |
Built-in Custom Nodes
| Node | Description |
|------|-------------|
| callout | Info / warning / error callout block |
| toggle | Collapsible section with summary |
| gallery | Image gallery with lightbox |
| bookmark | Link preview card |
| paywall | Content gate marker for subscriber content |
| product | Product card embed (commerce integration) |
| email-content | Email-safe content block |
| aside | Pull quote / sidebar content |
| header | Cover image + title hero block |
| html | Raw HTML pass-through block |
| markdown | Fenced code rendered as Markdown |
| signup | Embedded signup form block |
useNovaEditor Hook
For more control, use the hook directly:
import { useNovaEditor, EditorBubbleMenu } from "@novahelm/editor";
export function CustomEditor() {
const { editor, content } = useNovaEditor({
variant: "full",
format: "html",
defaultValue: "<p>Hello world</p>",
});
return (
<div>
{editor && <EditorBubbleMenu editor={editor} />}
<EditorContent editor={editor} />
</div>
);
}Utilities
import { sanitizeHtml, countTKs, REPLACEMENT_TOKENS } from "@novahelm/editor";
// Safe HTML rendering (DOMPurify)
const safeHtml = sanitizeHtml(untrustedHtml);
// Count TK (to-come) placeholders in content
const tkCount = countTKs(htmlContent);
// Replacement token registry ({{author}}, {{date}}, etc.)
console.log(REPLACEMENT_TOKENS); // [{ key, label, value }]Snippets
Save and retrieve reusable text snippets (stored in localStorage):
import { saveSnippet, getSnippets, deleteSnippet } from "@novahelm/editor";
saveSnippet({ id: "intro", label: "Standard intro", content: "<p>...</p>" });
const snippets = getSnippets();
deleteSnippet("intro");