Editor Documentation
Complete guide to the Plate.js rich text editor in Constellation.
Overview
Constellation uses Plate.js (built on Slate), a powerful headless editor for rich content creation.
Version: 52.x
Documentation: plate.dev
Editor Features
Plugins Included
| Plugin | Feature |
|---|---|
| Basic Nodes | Paragraphs, headings, quotes |
| Basic Styles | Bold, italic, underline, code |
| Lists | Ordered, unordered, to-do lists |
| Code Block | Syntax highlighting |
| Table | Table creation and editing |
| Link | Hyperlink insertion |
| Media | Images, videos, embeds |
| Mention | @mention users |
| Emoji | Emoji picker |
| Slash Command | / command palette |
| Comment | Collaborative comments |
| Break | Block insertion (Cmd+Enter) |
Basic Usage
PlateEditor Component
import { PlateEditor } from '@/src/components/editor/PlateEditor';
export function EditorPage() {
const [content, setContent] = useState([]);
return (
<PlateEditor
content={content}
onChange={setContent}
readOnly={false}
/>
);
}
Props
interface PlateEditorProps {
content?: SlateElement[];
onChange?: (content: SlateElement[]) => void;
readOnly?: boolean;
placeholder?: string;
disabled?: boolean;
plugins?: PluginOptions[];
className?: string;
}
Editor Content Structure
Document Format
// Content is an array of blocks
const content = [
{
type: 'p',
children: [
{
text: 'This is a paragraph with ',
},
{
text: 'bold text',
bold: true,
},
{
text: '.',
},
],
},
{
type: 'h1',
children: [{ text: 'Heading' }],
},
];
Common Element Types
// Paragraph
{ type: 'p', children: [{ text: 'Content' }] }
// Headings
{ type: 'h1', children: [{ text: 'Heading' }] }
{ type: 'h2', children: [{ text: 'Subheading' }] }
// Lists
{
type: 'ul',
children: [
{ type: 'li', children: [{ text: 'Item 1' }] },
{ type: 'li', children: [{ text: 'Item 2' }] },
],
}
// Code block
{
type: 'code_block',
language: 'javascript',
children: [{ text: 'const x = 1;' }],
}
// Quote
{ type: 'blockquote', children: [{ text: 'Quote text' }] }
// Table
{
type: 'table',
children: [
{
type: 'tr',
children: [
{ type: 'td', children: [{ text: 'Cell' }] },
],
},
],
}
Toolbar
EditorToolbar Component
import { EditorToolbar } from '@/src/components/editor/EditorToolbar';
export function EditorWithToolbar() {
return (
<>
<EditorToolbar />
<PlateEditor />
</>
);
}
Toolbar Buttons
| Button | Shortcut | Action |
|---|---|---|
| Bold | Cmd+B | Toggle bold |
| Italic | Cmd+I | Toggle italic |
| Underline | Cmd+U | Toggle underline |
| Code | Cmd+` | Toggle inline code |
| Bullet List | Cmd+Shift+8 | Create bullet list |
| Numbered List | Cmd+Shift+7 | Create numbered list |
| Quote | Cmd+Shift+. | Create blockquote |
| Code Block | Cmd+Alt+C | Create code block |
| Link | Cmd+K | Insert link |
| Image | - | Insert image |
| Table | - | Insert table |
Mentions Plugin
Usage
Type @ to trigger mentions:
// Editor automatically detects and shows users
// Select user to insert mention
// Output:
{
type: 'mention',
value: 'user-123',
name: 'John Doe',
children: [{ text: '@John Doe' }],
}
Configuring Mention Sources
const mentionOptions = {
trigger: '@',
search: async (query: string) => {
// Search users matching query
const users = await apiClient.get('/users/search', {
params: { q: query },
});
return users.map(user => ({
key: user.id,
label: user.name,
}));
},
};
Emoji Plugin
Usage
Type : to trigger emoji picker:
:smile: โ ๐
:fire: โ ๐ฅ
:rocket: โ ๐
Custom Emoji List
const emojiOptions = {
trigger: ':',
data: emojiData, // @emoji-mart/data
};
Slash Commands
Usage
Type / to see available commands:
/heading โ Insert heading
/list โ Insert bullet list
/table โ Insert table
/code โ Insert code block
/quote โ Insert quote
/image โ Insert image
Creating Custom Commands
const slashCommandPlugins = [
{
name: 'heading',
description: 'Insert heading',
onSelect: (editor) => {
insertElement(editor, { type: 'h1', children: [{ text: '' }] });
},
},
];
Comments
Adding Comments
// Select text and press Cmd+Shift+M
// Or click comment button in toolbar
// Enter comment text
// Comments appear in sidebar
Comment Structure
{
type: 'comment',
data: {
id: 'comment-123',
author: 'user-456',
text: 'This needs fixing',
resolved: false,
replies: [],
},
children: [{ text: 'commented text' }],
}
Serialization
Save to JSON
// Get editor content
const content = editor.getChildren();
const json = JSON.stringify(content);
// Save to database
await apiClient.post('/documents', { content: json });
Load from JSON
const json = await apiClient.get(`/documents/${id}`);
const content = JSON.parse(json.content);
setEditorContent(content);
HTML Export
import { serializeHtml } from '@plate.js/markdown';
const html = serializeHtml(editor, {
nodes: { /* config */ },
});
Markdown Import/Export
import { serializeMarkdown } from '@plate.js/markdown';
// Export to markdown
const markdown = serializeMarkdown(editor);
// Import from markdown
const content = deserializeMarkdown(markdownText);
Collaborative Editing
Yjs Integration
import { useYjs } from '@/src/hooks/use-yjs';
export function CollaborativeEditor() {
const { yText, connected } = useYjs('document-123');
return (
<PlateEditor
content={yText.toString()}
onChange={(content) => {
yText.delete(0, yText.length);
yText.insert(0, JSON.stringify(content));
}}
/>
);
}
Real-time Sync
// Connect via Hocuspocus
const provider = hocuspocusProvider.connect('document-id', {
awareness: true,
onUpdate: (data) => {
// Update when other users edit
},
});
Customization
Custom Plugins
const customPlugin = {
name: 'customElement',
handler: ({ isActive, type }) => ({
isActive: isActive(type),
type,
}),
};
Custom Renders
// Custom render for specific element type
const renderers = {
[ELEMENT_H1]: (props) => (
<h1 className="text-3xl font-bold" {...props}>
{props.children}
</h1>
),
};
Theme Customization
const editorTheme = {
colors: {
primary: '#0066cc',
background: '#ffffff',
},
typography: {
fontFamily: 'Inter, sans-serif',
},
};
API Reference
Editor Methods
const editor = useEditor();
// Insert element
editor.insertNode({ type: 'p', children: [{ text: 'New paragraph' }] });
// Delete
editor.delete();
// Get content
editor.getChildren();
// Set content
editor.setNodes(newContent);
// Undo/Redo
editor.undo();
editor.redo();
// Check if dirty (unsaved changes)
editor.isDirty();
Plate UI Components
Located: src/components/plate-ui/
| Component | Purpose |
|---|---|
Plate |
Main editor wrapper |
PlateContent |
Editable area |
PlateToolbar |
Toolbar container |
ToolbarButton |
Toolbar button |
ToolbarGroup |
Grouped buttons |
Performance Tips
โ Best Practices
- โ Debounce onChange to prevent excessive updates
- โ Memoize editor instance
- โ Lazy load plugins
- โ Optimize re-renders
- โ Use controlled component properly
Example: Debounced Save
import { useDebounce } from '@/src/hooks/use-debounce';
export function EditorWithAutosave() {
const [content, setContent] = useState([]);
const debouncedContent = useDebounce(content, 1000);
useEffect(() => {
if (debouncedContent) {
// Auto-save
apiClient.post('/documents/autosave', {
content: debouncedContent,
});
}
}, [debouncedContent]);
return <PlateEditor content={content} onChange={setContent} />;
}
Troubleshooting
Editor Not Showing
- Verify PlateEditor component is imported
- Check Plate UI plugins are loaded
- Verify CSS is included
- Check for console errors
Formatting Not Working
- Ensure toolbar buttons are connected
- Verify plugins are enabled
- Check TypeScript types
Real-time Sync Issues
- Verify Hocuspocus connection
- Check WebSocket URL in env
- Verify document ID is consistent
Examples
Basic Editor
'use client';
import { useState } from 'react';
import { PlateEditor } from '@/src/components/editor/PlateEditor';
import { EditorToolbar } from '@/src/components/editor/EditorToolbar';
export function SimpleEditor() {
const [content, setContent] = useState([
{
type: 'p',
children: [{ text: 'Start typing...' }],
},
]);
return (
<div>
<EditorToolbar />
<PlateEditor content={content} onChange={setContent} />
</div>
);
}
Read-only Viewer
<PlateEditor content={content} readOnly={true} />
Collaborative Document
import { useYjs } from '@/src/hooks/use-yjs';
export function CollaborativeDoc() {
const { yText, connected } = useYjs('doc-id');
return (
<PlateEditor
content={yText.toString()}
onChange={(content) => {
// Update Yjs text
}}
/>
);
}
Related Documentation
- ๐ COMPONENTS.md - Editor components
- ๐จ STYLING.md - Editor styling
- ๐๏ธ ARCHITECTURE.md - Architecture patterns
Last Updated: January 2026
Official Docs: plate.dev