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
The @design-on-web/plugin-qrcode package adds a QR code generator to the toolbar. It uses the qrcode npm package to generate real, scannable QR codes as data-URL images on the canvas.
import { QrCodePlugin } from '@design-on-web/plugin-qrcode';
// Register the plugin
await engine.registerPlugin(QrCodePlugin);
// That's it! The plugin adds:
// - A "Add QR Code" toolbar button
// - Ctrl+Shift+Q keyboard shortcut
// When triggered, it prompts for a URL and generates a scannable QR code.Implementation Details
The plugin registers a toolbar button and a keyboard shortcut. When triggered, it prompts the user for a URL, generates a 400px QR code with medium error correction, and adds it to the canvas as an image.
// Under the hood — the plugin uses the `qrcode` npm package
import QRCode from 'qrcode';
import type { EditorPlugin, PluginContext } from '@design-on-web/core';
async function addQrCode(ctx: PluginContext): Promise<void> {
const url = prompt('Enter URL for QR code:', 'https://example.com');
if (!url) return;
try {
const dataUrl = await QRCode.toDataURL(url, {
width: 400,
margin: 2,
color: { dark: '#000000', light: '#ffffff' },
errorCorrectionLevel: 'M',
});
await ctx.api.addImage(dataUrl, {
left: 100,
top: 100,
scaleX: 0.5,
scaleY: 0.5,
});
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
alert(`Failed to generate QR code: ${message}`);
}
}
export const QrCodePlugin: EditorPlugin = {
name: 'qr-code',
version: '0.1.0',
install(ctx) {
ctx.registerToolbarItem({
name: 'qr-code',
icon: '<svg>...</svg>',
label: 'Add QR Code',
onClick: () => { void addQrCode(ctx); },
});
ctx.registerKeyBinding({
key: 'q',
modifiers: ['ctrl', 'shift'],
handler: () => { void addQrCode(ctx); },
description: 'Add QR Code (Ctrl+Shift+Q)',
});
},
};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>
);
},
});
},
};Example: Unsplash Stock Photo Plugin
The @design-on-web/plugin-unsplash package adds a stock photo panel to the left sidebar. Users can search the Unsplash library and click any photo to add it to the canvas.
import { createUnsplashPlugin } from '@design-on-web/plugin-unsplash';
// Create the plugin with your Unsplash API access key
const unsplashPlugin = createUnsplashPlugin({
accessKey: 'your-unsplash-access-key',
});
await engine.registerPlugin(unsplashPlugin);
// The plugin registers a "Stock Photos" panel in the left sidebar.
// Users can search photos, browse results in a grid, and click
// to add a stock photo directly to the canvas.Features
- Search — type to search with 400ms debounce; shows popular photos on initial load
- Image grid — 2-column thumbnail grid with lazy loading
- Click to add — clicking a photo inserts the regular-resolution image onto the canvas
- Photographer attribution — each thumbnail shows the photographer name with a link back to their Unsplash profile (required by the Unsplash API terms)
Default Export (No API Key)
The package also provides a default UnsplashPlugin export that renders a placeholder message prompting the user to configure their API key. This is useful during development.
import { UnsplashPlugin } from '@design-on-web/plugin-unsplash';
// Default export — shows a "configure API key" placeholder panel.
// Useful during development before you have an Unsplash key.
await engine.registerPlugin(UnsplashPlugin);createUnsplashPlugin({ accessKey }) with your key.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.