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

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.

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

QR Code plugin internalstypescript
// 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.

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>
        );
      },
    });
  },
};

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.

Unsplash plugin with API keytypescript
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.

Default plugin (no key)typescript
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);
⚠️
API key required
You need an Unsplash Access Key to use this plugin in production. Create a free developer account at unsplash.com/developers and use 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.

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.