Your First Plugin

A step-by-step tutorial to create a working OpenGPEX plugin from scratch. By the end, you will have built a Color Picker sidebar drawer that reads the selected layer's color and lets you apply a new background fill.


Prerequisites

  • OpenGPEX dev server running (pnpm dev)
  • Basic familiarity with React and TypeScript
  • Read Plugin System Overview for context

Step 1: Create the Plugin Directory

mkdir -p src/lib/opengpex/plugins/user/ColorPickerDrawer

All user plugins live under plugins/user/. The folder name becomes part of the plugin identity.


Step 2: Define Protocols (protocols.ts)

// protocols.ts — Constants and type definitions

export const PLUGIN_ID = 'drawers.color_picker';

export const CMD = {
  APPLY_COLOR: 'cmd.apply_color',
} as const;

export interface ColorPickerConfig {
  currentColor: string;
  recentColors: string[];
}

export const DEFAULT_CONFIG: ColorPickerConfig = {
  currentColor: '#ff6600',
  recentColors: [],
};

Step 3: Implement Commands (commands.ts)

// commands.ts — Business logic (state mutations)
import type { EditorCommand } from '@/lib/opengpex/core/types/plugins';
import { CMD } from './protocols';

export const commands: EditorCommand[] = [
  {
    id: CMD.APPLY_COLOR,
    displayName: 'Apply Background Color',
    execute: (ctx, payload: { color: string }) => {
      const { actions, scoped } = ctx;
      const { frameId, layerId } = actions.getActiveTarget();

      if (!frameId || !layerId) return;

      // Update the layer's fill color
      actions.updateLayer(frameId, layerId, {
        backgroundColor: payload.color,
      });

      // Update plugin config (recent colors list)
      const config = scoped.selfConfig() as { recentColors: string[] };
      const recent = [payload.color, ...config.recentColors.filter(c => c !== payload.color)].slice(0, 8);
      scoped.updateConfig({ currentColor: payload.color, recentColors: recent });
    },
  },
];

Step 4: Create Hooks (hooks.ts)

// hooks.ts — Bridge between UI and commands
import { useEditorServices } from '@/lib/opengpex/core/context';
import { usePluginConfig } from '@/lib/opengpex/core/plugin';
import { CMD, type ColorPickerConfig, DEFAULT_CONFIG } from './protocols';

export function useColorPicker() {
  const { actions } = useEditorServices();
  const config = usePluginConfig<ColorPickerConfig>(DEFAULT_CONFIG);

  const applyColorCmd = (color: string) => {
    actions.executeCommand(CMD.APPLY_COLOR, { color });
  };

  return {
    currentColor: config.currentColor,
    recentColors: config.recentColors,
    applyColorCmd,
  };
}

Step 5: Build the UI (components.tsx)

// components.tsx — Pure presentation React component
import React, { useState } from 'react';
import { useColorPicker } from './hooks';

export function ColorPickerPanel() {
  const { currentColor, recentColors, applyColorCmd } = useColorPicker();
  const [inputColor, setInputColor] = useState(currentColor);

  return (
    <div className="p-3 space-y-3">
      <h3 className="text-xs font-bold uppercase text-muted">Color Picker</h3>

      {/* Color input */}
      <div className="flex items-center gap-2">
        <input
          type="color"
          value={inputColor}
          onChange={(e) => setInputColor(e.target.value)}
          className="w-8 h-8 rounded cursor-pointer"
        />
        <input
          type="text"
          value={inputColor}
          onChange={(e) => setInputColor(e.target.value)}
          className="flex-1 px-2 py-1 text-xs bg-panel border border-subtle rounded"
        />
      </div>

      {/* Apply button */}
      <button
        onClick={() => applyColorCmd(inputColor)}
        className="w-full py-1.5 text-xs font-bold rounded bg-accent text-white"
      >
        Apply Color
      </button>

      {/* Recent colors */}
      {recentColors.length > 0 && (
        <div>
          <p className="text-[10px] text-muted mb-1">Recent</p>
          <div className="flex gap-1 flex-wrap">
            {recentColors.map((c) => (
              <button
                key={c}
                onClick={() => { setInputColor(c); applyColorCmd(c); }}
                className="w-5 h-5 rounded border border-subtle"
                style={{ backgroundColor: c }}
              />
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

Step 6: Assemble the Plugin Entry (index.tsx)

// index.tsx — Plugin registration entry point
import React from 'react';
import { Palette } from 'lucide-react';
import type { EditorPlugin } from '@/lib/opengpex/core/types/plugins';
import { ColorPickerPanel } from './components';
import { commands } from './commands';
import { PLUGIN_ID, DEFAULT_CONFIG } from './protocols';

export const plugin: EditorPlugin = {
  manifest: {
    id: PLUGIN_ID,
    displayName: 'Color Picker',
    version: '1.0.0',
    description: 'A simple background color picker tool',
    category: 'drawers',
    author: 'my-username',
  },

  slot: 'SIDE_BAR',
  show: 'frame-required',
  icon: <Palette size={20} />,
  order: 900,
  side: 'left',

  component: ColorPickerPanel,
  initialConfig: DEFAULT_CONFIG,
  commands,
};

Step 7: Register the Plugin

Run the plugin scanner to automatically detect your new plugin:

pnpm scan-plugins

Or simply restart the dev server — it runs the scanner automatically:

pnpm dev

Your plugin should now appear in the left sidebar with a 🎨 palette icon!


Step 8: Test It

  1. Open the editor and load an image
  2. Click your Color Picker icon in the sidebar
  3. Choose a color and click "Apply Color"
  4. Verify the layer background changes
  5. Press Ctrl+Z — the change should undo cleanly

Step 9: Package for Distribution

When you are ready to share your plugin:

pnpm pack-plugin ColorPickerDrawer

This produces a .zip file in dist/plugins/ that can be uploaded to GPEX Hub or shared directly.


Summary: The 5-File Pattern

File Responsibility
protocols.ts Constants, command IDs, config types
commands.ts Business logic (state mutations)
hooks.ts Bridge layer (UI ↔ commands)
components.tsx Pure React presentation
index.tsx Plugin metadata & assembly

Next Steps


Last updated: 2026-06-14