@alggtta/editor
v2.1.0
Published
Universal Editor with Tiptap 3.x and DaisyUI 5
Maintainers
Readme
@alggtta/editor
A powerful, extensible rich text editor built on Tiptap 3.x and React with AI integration, real-time collaboration, and 25+ features out of the box.
Live Demo | Landing Page | GitHub
Table of Contents
- Features
- Installation
- Quick Start
- Component API
- Ref API (External Access)
- Custom Extensions
- Custom Slash Commands
- Image Upload
- AI Integration
- i18n (Internationalization)
- ReadOnlyViewer Advanced
- Content Compatibility
- Markdown Import/Export
- Theming
- Collaboration (Yjs)
- Comments & Threads
- Database Tables
- Code Blocks
- Edge Runtime & Next.js Compatibility
- CSS Isolation
- Bundle Size
- TypeScript Exports
- Browser Support
- License
Features
- Rich Text Editing — Headings, lists, task lists, quotes, code blocks, tables, images, and more
- Slash Commands — Type
/to quickly insert any block type with fuzzy search - Bubble Menu — Context-aware floating toolbar for inline formatting
- Drag & Drop — Drag handles to reorder any block
- Custom Extensions — Add domain-specific blocks with React NodeView support
- AI Writing Assistant — Streaming text generation with diff-based accept/reject
- AI Autocompletion — Inline suggestions as you type, powered by any LLM
- AI Image Generation — Generate images from text prompts
- Real-time Collaboration — Multi-user editing via Yjs with presence cursors
- Comments & Threads — Inline comment threads with resolve/unresolve workflow
- Offline Support — Continue editing offline with automatic sync on reconnect
- Database Tables — Full-featured tables with sorting, column resizing, and click-to-edit cells (TanStack Table v8)
- Code Blocks — 190+ languages with syntax highlighting and execution support
- Markdown Import/Export — Seamless conversion between Markdown and editor content
- 6 Built-in Themes — Neuroscience-inspired themes using OKLCH color space
- i18n Support — Built-in English and Korean locales with full label override API
- Version History — Checkpoint-based document snapshots
- Read-Only Viewer — Lightweight component with custom extension and content filtering support
- Ref API — Programmatic editor access via React ref (
insertContent,getJSON, etc.) - TypeScript First — Full type safety with comprehensive type exports
Installation
npm install @alggtta/editorPeer Dependencies
Required:
npm install react react-dom @tiptap/core @tiptap/react @tiptap/starter-kit @tiptap/pm @heroicons/reactOptional (for collaboration):
npm install yjs y-prosemirror y-protocols y-indexeddb @tiptap/extension-collaboration @tiptap/y-tiptapOptional (for tables):
npm install @tanstack/react-tableOptional (for math):
npm install katexQuick Start
import { AlggttaEditor } from '@alggtta/editor'
import '@alggtta/editor/styles.css'
function App() {
return (
<AlggttaEditor
token="your-license-token"
content="<p>Start writing...</p>"
onUpdate={(editor) => {
const json = editor.getJSON()
console.log(json)
}}
/>
)
}Component API
<AlggttaEditor />
The main editor component with built-in slash menu, bubble menu, and drag handle. Supports React.forwardRef for programmatic access.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| token | string | (required) | License token for production use |
| content | string \| JSONContent | '' | Initial content (HTML string or Tiptap JSON) |
| placeholder | string | "Type '/' for commands..." | Placeholder text when editor is empty |
| editable | boolean | true | Whether the editor is editable |
| autofocus | boolean \| 'start' \| 'end' | false | Auto-focus the editor on mount |
| theme | string | undefined | DaisyUI theme name (sets data-theme on wrapper) |
| className | string | '' | Additional CSS classes on the editor wrapper |
| locale | 'en' \| 'ko' | 'en' | Built-in locale preset for UI labels |
| labels | Partial<EditorLabels> | undefined | Override individual UI labels (merged on top of locale) |
| onUpdate | (editor: Editor) => void | undefined | Called on every document change |
| onCreate | (editor: Editor) => void | undefined | Called when editor is initialized |
| onSelectionUpdate | (editor: Editor) => void | undefined | Called when selection changes |
| onBlur | (editor: Editor) => void | undefined | Called when editor loses focus |
| onFocus | (editor: Editor) => void | undefined | Called when editor gains focus |
| onImageUpload | (file: File) => Promise<string> | undefined | Custom image upload handler (returns URL) |
| onFileUpload | (file: File) => Promise<string> | undefined | Custom file upload handler (returns URL) |
| onAIRequest | OnAIRequest | undefined | AI text generation handler (enables AI features) |
| onExecuteCode | (req: CodeExecutionRequest) => Promise<CodeExecutionResult> | undefined | Code execution handler for code blocks |
| verifyEndpoint | string | 'https://cdn.alggtta.com' | CDN gateway URL for license verification |
| extensionConfig | ExtensionConfig | undefined | Extension configuration (see below) |
ExtensionConfig
Passed via the extensionConfig prop:
| Field | Type | Description |
|-------|------|-------------|
| customExtensions | AnyExtension[] | Additional Tiptap extensions to register |
| customSlashCommands | SlashCommand[] | Additional slash commands (merged with defaults) |
| collaboration | { document: YDoc, field?: string } | Yjs collaboration config |
| onAISuggest | (context: string, signal: AbortSignal) => Promise<string> | AI autocomplete handler |
| aiAutocompletionDebounceMs | number | Autocomplete debounce (default: 600ms) |
| enableCounter | boolean | Enable heading/figure auto-numbering |
<ReadOnlyViewer />
Lightweight viewer for displaying editor content without editing capabilities. No ProseMirror state — renders pure static HTML.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| content | JSONContent \| null | (required) | Tiptap JSON document to render |
| className | string | '' | Additional CSS classes |
| extensions | AnyExtension[] | [StarterKit] | Custom extensions for rendering custom node types |
| contentFilter | (doc: JSONContent) => JSONContent | undefined | Pre-process document before rendering |
Ref API (External Access)
Use React.useRef to access the editor instance and convenience methods from parent components.
import { useRef } from 'react'
import { AlggttaEditor } from '@alggtta/editor'
import type { AlggttaEditorRef } from '@alggtta/editor'
function MyPage() {
const editorRef = useRef<AlggttaEditorRef>(null)
const handleAIResult = (html: string) => {
editorRef.current?.insertContent(html)
}
const handleSave = () => {
const json = editorRef.current?.getJSON()
fetch('/api/save', { method: 'POST', body: JSON.stringify(json) })
}
const handleGetSelection = () => {
const selected = editorRef.current?.getSelectedText()
console.log('Selected:', selected)
}
return (
<>
<AlggttaEditor ref={editorRef} token="..." />
<button onClick={handleSave}>Save</button>
<button onClick={handleGetSelection}>Get Selection</button>
</>
)
}AlggttaEditorRef Methods
| Method | Signature | Description |
|--------|-----------|-------------|
| editor | Editor \| null | The underlying Tiptap Editor instance |
| insertImage | (url: string, attrs?: { alt?, title?, width? }) => void | Insert an image at cursor |
| insertContent | (content: string \| JSONContent) => void | Insert HTML or JSON at cursor |
| getSelectedText | () => string | Get the currently selected text |
| getJSON | () => JSONContent | Get full document as Tiptap JSON |
| getHTML | () => string | Get full document as HTML |
| focus | (position?: 'start' \| 'end' \| number \| boolean) => void | Focus the editor |
Alternative: onCreate callback
If you prefer callbacks over refs:
const [editor, setEditor] = useState<Editor | null>(null)
<AlggttaEditor
token="..."
onCreate={(editor) => setEditor(editor)}
/>
// Then use editor?.getJSON(), editor?.commands.insertContent(), etc.Custom Extensions
React NodeView Extensions
Pass custom Tiptap extensions with React NodeViews via extensionConfig.customExtensions. The editor fully supports:
atom: trueblocks (no internal editing, attribute-only)content: "block+"wrapper blocks (contain other blocks)ReactNodeViewRendererfor custom React componentsaddCommands()for programmatic insertion
import { Node, mergeAttributes } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
// Example: MapBlock Extension
const MapBlockExtension = Node.create({
name: 'mapBlock',
group: 'block',
atom: true,
addAttributes() {
return {
location: { default: '' },
lat: { default: null },
lng: { default: null },
}
},
addNodeView() {
return ReactNodeViewRenderer(MapBlockComponent)
},
parseHTML() {
return [{ tag: 'div[data-type="map-block"]' }]
},
renderHTML({ HTMLAttributes }) {
return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'map-block' }), 0]
},
})
// Usage
<AlggttaEditor
token="..."
extensionConfig={{
customExtensions: [MapBlockExtension, HotelBlockExtension],
}}
/>Wrapper Blocks (content: "block+")
Blocks that wrap other blocks are fully supported:
const StaffOnlyBlock = Node.create({
name: 'staffOnlyBlock',
group: 'block',
content: 'block+', // Contains child blocks
defining: true,
draggable: true,
addNodeView() {
return ReactNodeViewRenderer(StaffOnlyBlockNodeView)
},
addCommands() {
return {
setStaffOnlyBlock: () => ({ commands }) => commands.wrapIn('staffOnlyBlock'),
toggleStaffOnlyBlock: () => ({ commands }) => commands.toggleWrap('staffOnlyBlock'),
}
},
})Built-in GuideFlow Extensions
Pre-built domain extensions for travel guide apps:
import {
guideFlowExtensions,
guideFlowSlashCommands,
// Individual imports:
MapBlockExtension,
HotelBlockExtension,
ContactBlockExtension,
ScheduleBlockExtension,
StaffOnlyBlockExtension,
DividerBlockExtension,
} from '@alggtta/editor'
<AlggttaEditor
token="..."
extensionConfig={{
customExtensions: guideFlowExtensions,
customSlashCommands: guideFlowSlashCommands,
}}
/>Custom Slash Commands
SlashCommand Interface
interface SlashCommand {
title: string // Display name
description?: string // Shown below title
icon?: React.ComponentType<{ className?: string }> // Heroicon component
command: (editor: Editor) => void // Executed when selected
keywords?: string[] // Fuzzy search terms
category?: string // Group header in menu
shortcut?: string // Keyboard shortcut hint
requiresAI?: boolean // Only show when AI is enabled
}Custom commands are merged (appended) with the built-in defaults — they do not replace them. Commands are grouped by category with section headers in the slash menu.
Note: The
commandfunction receives only(editor: Editor). The/trigger character is automatically deleted before your command runs — you don't need to handlerangedeletion.
External Callbacks in Commands
Since customSlashCommands is defined in your component scope, you can capture external functions via closures:
function MyEditor({ onOpenAIPanel, guideId }) {
const customCommands = useMemo(() => [
{
title: 'AI Write',
description: 'Open AI writing panel',
icon: SparklesIcon,
category: 'AI',
keywords: ['ai', 'write', 'generate'],
command: (editor) => {
// Access external state/callbacks via closure
onOpenAIPanel('write')
},
},
{
title: 'Map',
description: 'Insert a map block',
icon: MapPinIcon,
category: 'Blocks',
keywords: ['map', 'location', 'place'],
command: (editor) => {
editor.commands.setMapBlock({ location: '' })
},
},
], [onOpenAIPanel])
return (
<AlggttaEditor
token="..."
extensionConfig={{ customSlashCommands: customCommands }}
/>
)
}Image Upload
The onImageUpload callback handles all image insertion scenarios through a single handler:
<AlggttaEditor
token="..."
onImageUpload={async (file: File) => {
const formData = new FormData()
formData.append('file', file)
const res = await fetch('/api/upload/image', { method: 'POST', body: formData })
const { url } = await res.json()
return url // Editor inserts the image with this URL
}}
/>Covered Scenarios
| Scenario | Handled |
|----------|---------|
| Toolbar image button / slash command /image | Yes |
| Drag and drop image files onto editor | Yes |
| Ctrl+V / Cmd+V clipboard image paste | Yes |
| File input dialog | Yes |
Upload UX
During upload, the editor shows:
previewUrl— local blob URL for instant previewuploadProgress— 0-100 progress indicatoruploadError— error state with retry option
Once your handler resolves, the src attribute is replaced with the returned URL.
File Uploads
Non-image files are handled separately via onFileUpload:
onFileUpload={async (file) => {
// Upload to your storage
return downloadUrl
}}AI Integration
Enable AI features by providing the onAIRequest callback. When set, the editor shows:
- AI slash commands (
/ai-write,/ai-translate,/ai-summarize) - AI button in the bubble menu
- Floating AI prompt input
- AI side panel with Write, Translate, Image, and Ingest tabs
AIRequest Interface
interface AIRequest {
type?: 'chat' | 'translate' | 'summarize' | 'write' | 'ingest' | 'image' | string
prompt: string // User's input prompt
selectedText?: string // Currently selected text (if any)
context?: string // Surrounding text context
signal?: AbortSignal // For cancellation support
}
type OnAIRequest = (request: AIRequest) =>
| Promise<ReadableStream<string> | string> // Async streaming or one-shot
| ReadableStream<string> // Sync streaming
| string // Sync one-shotStreaming Responses
Return a ReadableStream<string> for real-time streaming into the editor:
<AlggttaEditor
token="..."
onAIRequest={async (request) => {
const response = await fetch('/api/ai/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: request.prompt,
context: request.context,
selectedText: request.selectedText,
}),
signal: request.signal, // Pass abort signal for cancellation
})
return response.body! // ReadableStream — editor inserts chunks in real-time
}}
/>Streaming behavior:
- An AI skeleton placeholder appears at the cursor
- First chunk arrives — skeleton is replaced with real content
- Subsequent chunks are appended in real-time
- All streaming changes are batched into a single undo entry
- HTML is sanitized via DOMPurify before insertion
AI Diff Review
After AI generates content, users can review changes with accept/reject:
import type { UseAIDiffOptions, UseAIDiffReturn, AIDiffChange } from '@alggtta/editor'The diff system uses LCS (Longest Common Subsequence) for block-level change detection with individual and batch accept/reject.
External AI Panel Integration
You can use slash commands to trigger external AI panels instead of (or in addition to) the built-in AI:
const customCommands = [
{
title: 'AI Write',
category: 'AI',
command: (editor) => {
// Open your external AI panel
onOpenExternalAIPanel('write')
},
},
{
title: 'AI Image',
category: 'AI',
command: (editor) => {
onOpenExternalAIPanel('image')
},
},
]AI Autocompletion (Copilot-style)
Enable inline ghost-text suggestions:
<AlggttaEditor
token="..."
extensionConfig={{
onAISuggest: async (context, signal) => {
const res = await fetch('/api/ai/complete', {
method: 'POST',
body: JSON.stringify({ context }),
signal,
})
return res.text()
},
aiAutocompletionDebounceMs: 600,
}}
/>Tab to accept, Escape to dismiss.
i18n (Internationalization)
The editor supports full UI text localization with built-in English (en) and Korean (ko) presets.
Using locale Presets
// Korean UI
<AlggttaEditor token="..." locale="ko" />
// English UI (default)
<AlggttaEditor token="..." locale="en" />Custom Label Overrides
Override specific labels while keeping the rest from the locale preset:
<AlggttaEditor
token="..."
locale="ko"
labels={{
placeholder: '여기에 입력하세요...',
bubbleMenu: {
bold: '굵게 (Ctrl+B)',
},
ai: {
promptPlaceholder: 'AI에게 물어보세요...',
},
}}
/>EditorLabels Structure
interface EditorLabels {
placeholder: string
bubbleMenu: { bold, italic, underline, strike, highlight, aiAssistant }
dragHandle: { addBlock, blockActions, delete, duplicate, turnInto, text, heading1-3, bulletList, numberedList, taskList, quote, codeBlock }
slashMenu: { noResults }
ai: { assistant, generating, promptPlaceholder, stopGenerating, sendPrompt, close, enterToSend, escToClose, write, translate, image, ingest }
mobileToolbar: { addBlock, bold, italic, underline, strike, highlight, moreOptions, heading1-3, quote, bulletList, numberedList, taskList, codeBlock, moveUp, moveDown, duplicate, delete, link, undo, redo, linkPlaceholder, applyLink, cancel }
mobileBlockMenu: { title, close, text, textDesc, heading1, heading1Desc, ... }
comments: { title, noComments, noCommentsHint, placeholder, reply, edit, save, cancel, delete, resolve, unresolve, resolved, confirmDelete, justNow, minutesAgo(m), hoursAgo(h), daysAgo(d), ... }
database: { untitled, newRow, addColumn, deleteRow, dragToReorder, filter, new, uncategorized, boardViewHint }
}useLabels Hook
Access labels from within custom child components:
import { useLabels } from '@alggtta/editor'
function MyCustomComponent() {
const labels = useLabels()
return <span>{labels.comments.title}</span>
}Available Locale Presets
import { labelsEn, labelsKo } from '@alggtta/editor'
import type { EditorLabels } from '@alggtta/editor'
// Create a custom locale based on English
const myLabels: EditorLabels = {
...labelsEn,
placeholder: 'Write something amazing...',
}ReadOnlyViewer Advanced
Custom Extensions in Viewer
When your documents contain custom node types, pass the same extensions to the viewer so it can render them via renderHTML():
import { ReadOnlyViewer } from '@alggtta/editor'
import StarterKit from '@tiptap/starter-kit'
<ReadOnlyViewer
content={document}
extensions={[StarterKit, MapBlockExtension, HotelBlockExtension]}
/>Without extensions, only StarterKit nodes are rendered. Unknown node types are silently skipped.
Content Filtering
Filter out specific nodes before rendering (e.g., hide staff-only content in public views):
import { ReadOnlyViewer, filterNodeTypes } from '@alggtta/editor'
// Method A: Using the built-in filterNodeTypes utility
<ReadOnlyViewer
content={document}
extensions={[StarterKit, StaffOnlyBlockExtension]}
contentFilter={(doc) => filterNodeTypes(doc, ['staffOnlyBlock'])}
/>
// Method B: Custom filter function
<ReadOnlyViewer
content={document}
contentFilter={(doc) => {
// Your custom filtering logic
return transformedDoc
}}
/>filterNodeTypes Utility
Recursively removes specific node types from a Tiptap JSON document:
import { filterNodeTypes } from '@alggtta/editor'
const publicDoc = filterNodeTypes(document, ['staffOnlyBlock', 'internalNote'])
// All staffOnlyBlock and internalNote nodes (and their children) are removedExternal Integration Hooks
These hooks operate outside the editor (in side panels, dialogs, etc.) but can optionally interact with the editor via AlggttaEditorRef.
Edit Lock (useEditLock)
Manages document-level edit locks with automatic heartbeat renewal.
import { useEditLock } from '@alggtta/editor'
function DocumentEditor({ docId }: { docId: string }) {
const { isMyLock, editable, lockOwner, acquire, release, error, isPending } = useEditLock({
documentId: docId,
autoAcquire: true,
heartbeatInterval: 30_000,
callbacks: {
acquireLock: async (signal) => {
const res = await fetch(`/api/docs/${docId}/lock`, { method: 'POST', signal })
return res.json() // { lockOwner, isMyLock, expiresAt }
},
releaseLock: async (signal) => {
await fetch(`/api/docs/${docId}/lock`, { method: 'DELETE', signal })
},
heartbeat: async (signal) => {
const res = await fetch(`/api/docs/${docId}/lock/heartbeat`, { method: 'POST', signal })
return res.json()
},
},
})
if (!editable) return <p>Locked by {lockOwner}</p>
return <AlggttaEditor token="..." editable={editable} />
}AI Document Parsing (useAIIngest)
Parses uploaded files (PDF, DOCX, etc.) via an AI backend and optionally inserts the result.
import { useRef } from 'react'
import { useAIIngest, AlggttaEditor } from '@alggtta/editor'
import type { AlggttaEditorRef } from '@alggtta/editor'
function IngestPanel() {
const editorRef = useRef<AlggttaEditorRef>(null)
const { ingest, isProcessing, result, insertToEditor } = useAIIngest({
editorRef,
onAIIngest: async ({ file, prompt, signal }) => {
const form = new FormData()
form.append('file', file)
if (prompt) form.append('prompt', prompt)
const res = await fetch('/api/ai/ingest', { method: 'POST', body: form, signal })
return res.text()
},
})
return (
<>
<input type="file" onChange={(e) => e.target.files?.[0] && ingest(e.target.files[0])} />
{isProcessing && <p>Processing...</p>}
{result && <button onClick={insertToEditor}>Insert to Editor</button>}
<AlggttaEditor ref={editorRef} token="..." />
</>
)
}AI Full Translation (useAITranslate)
Translates the full document or selected text via an AI backend.
import { useRef } from 'react'
import { useAITranslate, AlggttaEditor } from '@alggtta/editor'
import type { AlggttaEditorRef } from '@alggtta/editor'
function TranslatePanel() {
const editorRef = useRef<AlggttaEditorRef>(null)
const { translate, isTranslating, result, replaceInEditor } = useAITranslate({
editorRef,
onAITranslate: async ({ text, targetLang, signal }) => {
const res = await fetch('/api/ai/translate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, targetLang }),
signal,
})
return res.text()
},
})
return (
<>
<button onClick={() => translate({ targetLang: 'ko' })} disabled={isTranslating}>
Translate to Korean
</button>
{result && <button onClick={replaceInEditor}>Replace Content</button>}
<AlggttaEditor ref={editorRef} token="..." />
</>
)
}AI Multi-turn Image Editor (useAIImageEditor)
Multi-turn conversational image generation with iterative refinement.
import { useRef } from 'react'
import { useAIImageEditor, AlggttaEditor } from '@alggtta/editor'
import type { AlggttaEditorRef } from '@alggtta/editor'
function ImageEditorPanel() {
const editorRef = useRef<AlggttaEditorRef>(null)
const { messages, sendMessage, isGenerating, currentImage, insertToEditor, reset } = useAIImageEditor({
editorRef,
maxHistory: 20,
onAIImageEdit: async ({ prompt, history, currentImageUrl, style, signal }) => {
const res = await fetch('/api/ai/image-edit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, history, currentImageUrl, style }),
signal,
})
const { imageUrl } = await res.json()
return imageUrl
},
})
return (
<div>
{messages.map(msg => (
<div key={msg.id}>
<strong>{msg.role}:</strong> {msg.content}
{msg.imageUrl && <img src={msg.imageUrl} alt={msg.content} />}
</div>
))}
<input onKeyDown={(e) => {
if (e.key === 'Enter') sendMessage(e.currentTarget.value)
}} />
{currentImage && <button onClick={() => insertToEditor()}>Insert to Editor</button>}
<button onClick={reset}>New Conversation</button>
</div>
)
}Editor Preview Frame (PreviewFrame)
Device-shaped preview frame for responsive content viewing.
import { PreviewFrame } from '@alggtta/editor'
function PreviewPanel({ content }) {
return (
<div className="flex gap-4">
<PreviewFrame content={content} device="mobile" />
<PreviewFrame content={content} device="tablet" scale={0.5} />
<PreviewFrame content={content} device="desktop" scale={0.4} />
</div>
)
}Props: content, device ('mobile' | 'tablet' | 'desktop'), extensions, contentFilter, className, showChrome, scale.
Content Compatibility
Tiptap 2.x → 3.x JSON
Tiptap 3.x is backward compatible with Tiptap 2.x JSON format. Documents saved with editor.getJSON() in Tiptap 2.x can be loaded directly:
{
"type": "doc",
"content": [
{ "type": "paragraph", "content": [{ "type": "text", "text": "Hello" }] },
{ "type": "mapBlock", "attrs": { "location": "Seoul", "lat": 37.55, "lng": 126.97 } }
]
}Unknown node types: If a node type (e.g., mapBlock) is not registered via customExtensions, ProseMirror silently skips it during rendering. However, the node will be lost on save if the extension is not registered — always register extensions for all node types in your documents.
Recommended Save Format
Use editor.getJSON() (not getHTML()) for persistence:
- JSON is lossless and compact
- HTML requires DOMPurify sanitization and may lose attributes on round-trip
Markdown Import/Export
import { markdownToHtml, htmlToMarkdown } from '@alggtta/editor'
// Import Markdown → HTML (for editor content)
const html = markdownToHtml('# Hello\n\nThis is **bold** text.')
// Export HTML → Markdown
const markdown = htmlToMarkdown('<h1>Hello</h1><p>This is <strong>bold</strong> text.</p>')Theming
The editor ships with 6 neuroscience-inspired themes using OKLCH color space, plus all 30+ built-in DaisyUI themes:
| Theme | Type | Description |
|-------|------|-------------|
| neuro-light | Light | Solarized-inspired for sustained focus |
| neuro-dark | Dark | Warm tones to reduce melatonin suppression |
| neuro-sepia | Light | Optimized for dyslexia and Irlen Syndrome |
| midnight | Dark | Deep blue/purple for calm environments |
| matcha | Light | Soft green, nature-inspired |
| sunset | Light | Warm gradient tones |
Applying a theme
// Built-in theme
<AlggttaEditor token="..." theme="neuro-dark" />
// Any DaisyUI theme
<AlggttaEditor token="..." theme="dracula" />
// Custom theme — define in your CSS, then pass the name
<AlggttaEditor token="..." theme="my-custom-theme" />Theming works via the data-theme attribute on the editor wrapper. Any DaisyUI-compatible theme defined in your CSS will work.
Theme Toggle Components
import { ThemeToggle, SimpleThemeToggle } from '@alggtta/editor'
<ThemeToggle /> // Full dropdown with all themes
<SimpleThemeToggle /> // Minimal light/dark toggleCollaboration (Yjs)
Enable real-time collaboration with Yjs:
import * as Y from 'yjs'
import { AlggttaEditor } from '@alggtta/editor'
const ydoc = new Y.Doc()
<AlggttaEditor
token="..."
extensionConfig={{
collaboration: {
document: ydoc,
field: 'default', // Y.XmlFragment field name
},
}}
/>You must provide your own WebSocket server for syncing Yjs documents. The editor also provides offline support utilities:
import { createOfflineProvider, createNetworkMonitor } from '@alggtta/editor'Comments & Threads
The editor includes a complete inline commenting system:
import type { UseCommentsOptions, UseCommentsReturn, Comment, CommentThread } from '@alggtta/editor'Features:
- Select text → add comment thread
- Reply to threads
- Resolve / unresolve threads
- Edit and delete comments
- Draft mode for unsaved comments
- Thread navigation with scroll + flash animation
Comments are stored locally (no built-in server sync). Implement your own persistence layer using the hook's callbacks.
Database Tables
Full-featured database tables powered by TanStack Table v8:
- 4 view types: Table, Board (Kanban), Gallery, List
- Column types: text, number, select, multi-select, date, checkbox
- Column resizing, sorting, global search
- Click-to-edit cells
- Row reordering via drag-and-drop
- Data stored in Tiptap node
attrs(single source of truth)
import type { DatabaseColumn, DatabaseRow, ViewType, ColumnType } from '@alggtta/editor'Code Blocks
- 190+ languages with syntax highlighting via Lowlight (highlight.js)
- Language selector with search
- Optional code execution via
onExecuteCodecallback - Copy button, line numbers
<AlggttaEditor
token="..."
onExecuteCode={async ({ language, code, signal }) => {
const result = await executeInSandbox(language, code, signal)
return { stdout: result.output, stderr: result.error }
}}
/>Edge Runtime & Next.js Compatibility
Cloudflare Workers / Edge Runtime
The editor package is fully compatible with edge environments:
- No Node.js-only APIs (
fs,path,crypto,buffer) used - All APIs are browser-compatible (DOM, Web APIs, fetch)
- Safe for Cloudflare Workers, Vercel Edge Functions, Deno Deploy
Next.js App Router
The library includes "use client" directives automatically (added at build time). For Next.js App Router:
// app/editor/page.tsx
import dynamic from 'next/dynamic'
const Editor = dynamic(
() => import('@alggtta/editor').then((m) => m.AlggttaEditor),
{ ssr: false }
)
export default function EditorPage() {
return <Editor token="..." />
}SSR Safety
immediatelyRender: falseis set by default to prevent SSR hydration mismatches- The editor only initializes in the browser
ReadOnlyViewerrenders static HTML and is SSR-safe (but DOMPurify requires a browser environment — usecreateStaticRenderer()for server-side rendering)
CSS Isolation
The editor's styles are scoped to prevent conflicts with your app:
- All styles scoped under
.ue-editor-rootclass - DaisyUI classes use
ue-prefix (ue-btn,ue-card,ue-dropdown, etc.) - CSS variables defined under
.ue-editor-root(plus:rootfor font stacks) - Single bundled CSS file — no CSS-in-JS runtime
Import styles
import '@alggtta/editor/styles.css'Potential Conflicts
If your project also uses Tailwind CSS / DaisyUI:
- The editor's prefixed classes (
ue-btn, etc.) won't conflict with yourbtnclasses - Some Tailwind utility classes (e.g.,
prose,flex,p-4) are shared — this is intentional and generally safe - CSS variables under
.ue-editor-rootare scoped and won't affect your global theme
Bundle Size
| Output | Size | Description | |--------|------|-------------| | Entry (ESM) | ~4KB | Re-export entry point | | Main chunk (ESM) | ~1.4MB | Core editor + all features (pre-gzip) | | Main chunk (CJS) | ~956KB | CommonJS equivalent | | CSS | ~273KB | All styles (pre-gzip) |
Tree-shaking: ESM format with code splitting. Heavy features are lazy-loaded:
- Mermaid diagrams — loaded on first use
- Code highlighting languages — loaded per language
- html2pdf — loaded on export
Externalized (not in bundle): react, @tiptap/*, prosemirror-*, @heroicons/react, yjs, @tanstack/react-table, katex
TypeScript Exports
All props, hooks, utilities, and types are fully typed and exported:
// Components
import { AlggttaEditor, ReadOnlyViewer, ThemeToggle } from '@alggtta/editor'
// Types
import type {
AlggttaEditorRef,
AlggttaEditorProps,
ReadOnlyViewerProps,
EditorLabels,
SlashCommand,
AIRequest,
OnAIRequest,
AIImageRequest,
AIDiffChange,
CodeExecutionRequest,
CodeExecutionResult,
ExtensionConfig,
CollaborationConfig,
DatabaseColumn,
DatabaseRow,
Comment,
CommentThread,
DaisyUITheme,
} from '@alggtta/editor'
// Utilities
import {
filterNodeTypes,
markdownToHtml,
htmlToMarkdown,
renderToStaticHTML,
createStaticRenderer,
createExtensions,
useLabels,
labelsEn,
labelsKo,
} from '@alggtta/editor'
// GuideFlow extensions
import {
guideFlowExtensions,
guideFlowSlashCommands,
MapBlockExtension,
HotelBlockExtension,
ContactBlockExtension,
ScheduleBlockExtension,
StaffOnlyBlockExtension,
} from '@alggtta/editor'Browser Support
- Chrome / Edge 90+
- Firefox 90+
- Safari 15+
