Plugin Development
Extend the editor with custom tools, panels, export formats, and behaviors using the plugin system. Plugins have full access to the editor API, state store, and event bus.
Plugin Interface
Every plugin implements the EditorPlugin interface. The install method receives a PluginContext with everything needed to extend the editor.
interface EditorPlugin {
name: string;
version: string;
dependencies?: string[];
install(ctx: PluginContext): void | Promise<void>;
uninstall?(ctx: PluginContext): void;
}EditorPlugin Properties
| Name | Type | Default | Description |
|---|---|---|---|
name* | string | — | Unique plugin identifier |
version* | string | — | Semantic version string |
dependencies | string[] | — | Names of plugins that must be installed first |
install* | (ctx) => void | Promise<void> | — | Called when the plugin is registered |
uninstall | (ctx) => void | — | Called when the plugin is removed |
PluginContext API
The context object provides access to the editor internals and registration methods for extending the UI.
interface PluginContext {
// Core access
api: EditorAPI; // Full EditorEngine API
store: ZustandStore; // Zustand state store
events: EventBus; // Event emitter
// Registration methods
registerTool(def: ToolDefinition): void;
registerPanel(def: PanelDefinition): void;
registerExporter(def: ExporterDefinition): void;
registerHook(name: string, handler: HookHandler): void;
registerToolbarItem(def: ToolbarItemDefinition): void;
registerKeyBinding(def: KeyBindingDefinition): void;
}PluginContext Properties
| Name | Type | Default | Description |
|---|---|---|---|
ctx.api | EditorAPI | — | Full editor API (addShape, addText, export, etc.) |
ctx.store | ZustandStore | — | Zustand state store for reading/writing editor state |
ctx.events | EventBus | — | Event emitter for subscribing to editor events |
Registration Methods
| Name | Type | Default | Description |
|---|---|---|---|
registerTool(def) | void | — | Add a new tool to the toolbar tool set |
registerPanel(def) | void | — | Add a custom sidebar panel |
registerExporter(def) | void | — | Add a new export format |
registerHook(name, handler) | void | — | Add a lifecycle hook handler |
registerToolbarItem(def) | void | — | Add a button to the toolbar |
registerKeyBinding(def) | void | — | Add a keyboard shortcut |
Example: QR Code Plugin
A complete example that adds a QR code generator to the toolbar. Click the button, enter a URL, and a QR code image is added to the canvas.
import QRCode from 'qrcode';
const QRCodePlugin: EditorPlugin = {
name: 'qr-code',
version: '1.0.0',
install(ctx) {
// Register a toolbar button
ctx.registerToolbarItem({
id: 'qr-code',
label: 'QR Code',
icon: 'qr-code-icon',
onClick: async () => {
const url = prompt('Enter URL for QR code:');
if (!url) return;
// Generate QR code as data URL
const dataUrl = await QRCode.toDataURL(url, {
width: 200,
margin: 2,
color: { dark: '#000000', light: '#ffffff' },
});
// Add to canvas as an image
await ctx.api.addImage(dataUrl, {
left: 100,
top: 100,
});
},
});
// Register a keyboard shortcut
ctx.registerKeyBinding({
key: 'q',
modifiers: ['ctrl', 'shift'],
description: 'Insert QR Code',
handler: async () => {
// Same logic as toolbar button
},
});
},
uninstall(ctx) {
// Cleanup is automatic — all registrations are disposed
console.log('QR Code plugin uninstalled');
},
};
export default QRCodePlugin;Example: Custom Export Format
Add a GIF animation export that converts each page into a frame.
const GifExportPlugin: EditorPlugin = {
name: 'gif-export',
version: '1.0.0',
dependencies: [], // no dependencies on other plugins
install(ctx) {
ctx.registerExporter({
format: 'gif',
label: 'GIF Animation',
mimeType: 'image/gif',
extension: '.gif',
async export(canvas, options) {
// Use a GIF encoding library
const { GIFEncoder } = await import('gif-encoder-2');
const pm = ctx.api.getPageManager();
const pageCount = pm.getPageCount();
const width = canvas.getWidth();
const height = canvas.getHeight();
const encoder = new GIFEncoder(width, height);
encoder.setDelay(1000); // 1 second per frame
encoder.start();
// Each page becomes a frame
for (let i = 0; i < pageCount; i++) {
await pm.switchToPage(i);
const imageData = canvas.getContext('2d')
.getImageData(0, 0, width, height);
encoder.addFrame(imageData.data);
}
encoder.finish();
return new Blob([encoder.out.getData()], { type: 'image/gif' });
},
});
},
};
export default GifExportPlugin;Example: Custom Panel
Register a sidebar panel that provides stock photo search and one-click insertion.
const StockPhotoPlugin: EditorPlugin = {
name: 'stock-photos',
version: '1.0.0',
install(ctx) {
// Register a custom sidebar panel
ctx.registerPanel({
id: 'stock-photos',
title: 'Stock Photos',
icon: 'image-icon',
position: 'left', // 'left' sidebar
// React component for the panel content
render: () => {
// This is a React component
const [query, setQuery] = useState('');
const [photos, setPhotos] = useState([]);
const search = async () => {
const res = await fetch(
`https://api.unsplash.com/search/photos?query=${query}`,
{ headers: { Authorization: 'Client-ID YOUR_KEY' } }
);
const data = await res.json();
setPhotos(data.results);
};
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
onKeyDown={e => e.key === 'Enter' && search()}
placeholder="Search photos..."
/>
<div className="photo-grid">
{photos.map(photo => (
<img
key={photo.id}
src={photo.urls.thumb}
onClick={() => ctx.api.addImage(photo.urls.regular)}
/>
))}
</div>
</div>
);
},
});
},
};Hook System
Hooks let you intercept and modify editor operations at key lifecycle points. Return false from a before* hook to prevent the operation.
const AuditPlugin: EditorPlugin = {
name: 'audit-log',
version: '1.0.0',
install(ctx) {
// Before an object is added to the canvas
ctx.registerHook('beforeObjectAdd', (object) => {
console.log('Adding object:', object.type, object.id);
// Return false to prevent the addition
// return false;
});
// After an object is added
ctx.registerHook('afterObjectAdd', (object) => {
sendToAnalytics('object_added', {
type: object.type,
id: object.id,
});
});
// Before export
ctx.registerHook('beforeExport', (format, options) => {
console.log('Exporting as:', format);
// Modify options or prevent export
});
// After export completes
ctx.registerHook('afterExport', (format, blob) => {
console.log('Exported:', format, blob.size, 'bytes');
});
// Before saving (JSON serialization)
ctx.registerHook('beforeSave', (data) => {
// Inject custom metadata
data.metadata = {
savedBy: 'audit-plugin',
timestamp: Date.now(),
};
return data;
});
// After loading from JSON
ctx.registerHook('afterLoad', (data) => {
console.log('Loaded design with metadata:', data.metadata);
});
},
};Available Hooks
| Name | Type | Default | Description |
|---|---|---|---|
beforeObjectAdd | (object) => void | false | — | Before an object is added to the canvas |
afterObjectAdd | (object) => void | — | After an object is added to the canvas |
beforeExport | (format, options) => void | false | — | Before an export operation starts |
afterExport | (format, blob) => void | — | After an export completes |
beforeSave | (data) => data | — | Before design data is serialized (can modify data) |
afterLoad | (data) => void | — | After design data is loaded from JSON |
Plugin Registration
import QRCodePlugin from './plugins/qr-code';
import GifExportPlugin from './plugins/gif-export';
// Register plugins after creating the engine
const engine = new EditorEngine({ canvasElement, width: 1080, height: 1080 });
// Register one at a time
await engine.registerPlugin(QRCodePlugin);
// Or register multiple
await engine.registerPlugin(GifExportPlugin);
// Check registered plugins
const plugins = engine.getPlugins();
// Returns: Array<{ name: string, version: string }>
// Unregister a plugin
await engine.unregisterPlugin('qr-code');PluginContext are automatically disposed when the plugin is unregistalled. You do not need to manually remove toolbar items, panels, or hooks in the uninstall method.dependencies, those plugins must be registered first. The engine will throw an error if dependencies are not satisfied.