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.

EditorPlugin interfacetypescript
interface EditorPlugin {
  name: string;
  version: string;
  dependencies?: string[];
  install(ctx: PluginContext): void | Promise<void>;
  uninstall?(ctx: PluginContext): void;
}

EditorPlugin Properties

NameTypeDefaultDescription
name*stringUnique plugin identifier
version*stringSemantic version string
dependenciesstring[]Names of plugins that must be installed first
install*(ctx) => void | Promise<void>Called when the plugin is registered
uninstall(ctx) => voidCalled when the plugin is removed

PluginContext API

The context object provides access to the editor internals and registration methods for extending the UI.

PluginContexttypescript
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

NameTypeDefaultDescription
ctx.apiEditorAPIFull editor API (addShape, addText, export, etc.)
ctx.storeZustandStoreZustand state store for reading/writing editor state
ctx.eventsEventBusEvent emitter for subscribing to editor events

Registration Methods

NameTypeDefaultDescription
registerTool(def)voidAdd a new tool to the toolbar tool set
registerPanel(def)voidAdd a custom sidebar panel
registerExporter(def)voidAdd a new export format
registerHook(name, handler)voidAdd a lifecycle hook handler
registerToolbarItem(def)voidAdd a button to the toolbar
registerKeyBinding(def)voidAdd 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.

QR Code plugintypescript
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.

GIF export plugintypescript
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.

Stock photo panel plugintsx
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.

Lifecycle hookstypescript
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

NameTypeDefaultDescription
beforeObjectAdd(object) => void | falseBefore an object is added to the canvas
afterObjectAdd(object) => voidAfter an object is added to the canvas
beforeExport(format, options) => void | falseBefore an export operation starts
afterExport(format, blob) => voidAfter an export completes
beforeSave(data) => dataBefore design data is serialized (can modify data)
afterLoad(data) => voidAfter design data is loaded from JSON

Plugin Registration

Registering pluginstypescript
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');
ℹ️
Auto-cleanup
All registrations made through 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
If a plugin declares dependencies, those plugins must be registered first. The engine will throw an error if dependencies are not satisfied.