Home / Docs / Packages / @bpmnkit/editor

@bpmnkit/editor

Full BPMN editor — canvas, properties panel, AI bridge, and storage.

Core Semver at 1.0: nothing breaks without a major release.

Overview

@bpmnkit/editor bundles everything needed to embed a full-featured BPMN editor into any web application:

  • Canvas — SVG viewer with pan/zoom
  • Properties panel — edit element properties inline
  • AI bridge — chat panel connected to a local AI server
  • Storage — auto-save to IndexedDB with project/file management
  • HUD — toolbar with undo/redo, zoom, optimize, and AI buttons
  • Side dock — collapsible right panel (properties + AI tabs)

Installation

pnpm add @bpmnkit/editor

Basic Setup

import { BpmnEditor, initEditorHud } from "@bpmnkit/editor";

// Create the editor
const editor = new BpmnEditor({
  container: document.getElementById("editor"),
  theme: "dark",
  persistTheme: true,   // read/write "bpmn-theme" in localStorage
});

// Initialize the HUD (toolbar overlay)
const hud = initEditorHud(editor);

// Load a diagram
await editor.loadXML(bpmnXml);

With Side Dock

The side dock provides a collapsible properties + AI panel:

import { BpmnEditor, initEditorHud, createSideDock } from "@bpmnkit/editor";

const editor = new BpmnEditor({
  container: document.getElementById("editor"),
});

const dock = createSideDock();
document.body.appendChild(dock.el);

const hud = initEditorHud(editor, {
  aiButton: dock.aiPane.button,
});

HUD Options

type HudOptions = {
  optimizeButton?: HTMLElement;   // inject an external "Optimize" button
  aiButton?: HTMLElement;         // inject an external "AI" button
};

EditorOptions

type EditorOptions = {
  container: HTMLElement;
  theme?: "dark" | "light";
  persistTheme?: boolean;         // auto-save theme to localStorage
  plugins?: CanvasPlugin[];
};

SideDock API

type SideDock = {
  el: HTMLElement;
  propertiesPane: HTMLElement;
  aiPane: { button: HTMLElement; el: HTMLElement };
  switchTab(tab: "properties" | "ai"): void;
  expand(): void;
  collapse(): void;
  collapsed: boolean;
  showPanel(): void;
  hidePanel(): void;
  setDiagramInfo(processName: string, fileName: string): void;
};

Applying a change script

applyProcessDelta applies a change script (parseProcessDelta from @bpmnkit/core) to a diagram someone drew, without laying it out again. It goes through the same modelling functions a person’s edit does, so only what the change touches moves:

  • a new node is placed beside the node it follows, below anything in its way;
  • new nodes written between two connected nodes (a > new > b) replace the flow a > b, keep its condition or default, and sit in the gap — the shapes right of it move along if the gap is too narrow, and pools and lanes widen with them;
  • a new node joins the lane it is drawn in;
  • removing a node with one way in and one way out joins its neighbours, unless the script connected them itself;
  • a retyped node keeps its centre, extensions and flows.
import { parseProcessDelta, writeProcessText } from "@bpmnkit/core";
import { applyProcessDelta } from "@bpmnkit/editor/headless";

const { text, aliases } = writeProcessText(defs);
const result = applyProcessDelta(defs, parseProcessDelta(modelOutput), { aliases });
// result.definitions, created, changed, removed, addressed, fixes, problems

It never throws on the script and never mutates defs. Ids that name nothing, kinds that cannot be created and attributes the Zeebe schema forbids are reported in problems and left out. Like the editor’s other modelling functions, it acts on the first process. Pass ids: createIdFactory(seed) for a result that replays identically.

A script’s with lines configure connectors, on existing nodes or ones the script adds. They need the connector catalog, which this package does not carry. Pass applyConnectorLines from @bpmnkit/core/connectors as applyConnectors. Required inputs the lines left out come back as result.questions.

import { applyConnectorLines, connectorLineFor } from "@bpmnkit/core/connectors";

const { text, aliases } = writeProcessText(defs, { connectorLine: connectorLineFor });
const result = applyProcessDelta(defs, parseProcessDelta(modelOutput), {
  aliases,
  applyConnectors: applyConnectorLines,
});

Full Editor with All Plugins

The landing page editor uses the createStorageTabsBridge plugin which wires together tabs, storage, AI, and command palette in one call:

import { createStorageTabsBridge } from "@bpmnkit/canvas-plugin-storage-tabs-bridge";
import { BpmnEditor, initEditorHud, createSideDock } from "@bpmnkit/editor";

const editor = new BpmnEditor({ container });
const dock = createSideDock();

const bridge = createStorageTabsBridge({
  mainMenu: menuPlugin,
  resolver: fileResolver,
  enableFileImport: true,
  getExamples: (tabsApi) => [
    {
      label: "Approval Flow",
      load: () => tabsApi.openTab({ xml: approvalFlowXml }),
    },
  ],
});

document.body.appendChild(dock.el);
const hud = initEditorHud(editor, { aiButton: dock.aiPane.button });

Internationalisation

The editor and its first-party panels ship in ten languages. English is built in; the other nine are separate entry points, so a bundle carries only the languages it imports.

LanguageImport
German@bpmnkit/editor/locales/de
Spanish@bpmnkit/editor/locales/es
French@bpmnkit/editor/locales/fr
Italian@bpmnkit/editor/locales/it
Dutch@bpmnkit/editor/locales/nl
Polish@bpmnkit/editor/locales/pl
Portuguese (Brazil)@bpmnkit/editor/locales/pt-BR
Japanese@bpmnkit/editor/locales/ja
Chinese (Simplified)@bpmnkit/editor/locales/zh-CN

createTranslate(locale) turns a locale into the translate hook. Pass the same hook to the editor, the side dock and every plugin you mount:

import { BpmnEditor, createSideDock, createTranslate, initEditorHud } from "@bpmnkit/editor";
import { de } from "@bpmnkit/editor/locales/de";
import { createConfigPanelPlugin } from "@bpmnkit/plugins/config-panel";
import { createConfigPanelBpmnPlugin } from "@bpmnkit/plugins/config-panel-bpmn";

const translate = createTranslate(de);
const dock = createSideDock({ translate });
const configPanel = createConfigPanelPlugin({ getDefinitions, applyChange, translate });
const configPanelBpmn = createConfigPanelBpmnPlugin(configPanel, { translate });
const editor = new BpmnEditor({ container, translate, plugins: [configPanel, configPanelBpmn] });
initEditorHud(editor); // the HUD reads the editor's hook

These plugins take a translate option: config-panel, config-panel-bpmn, command-palette, command-palette-editor (third argument), main-menu, history, process-runner, tabs and storage-tabs-bridge. The properties panel translates schema labels, hints, placeholders and option labels when it draws them, so strings from a connector template you add pass through unchanged.

Pick the language at runtime. AVAILABLE_LOCALES lists every language with its own name, and matchLocale(navigator.languages, codes) picks the best match for the browser. Load the chosen locale with a dynamic import(), so each language is its own chunk. The strings are fixed when the UI is built — to switch language, rebuild the editor or reload the page. The main menu has a ready-made Language section:

createMainMenuPlugin({
  translate,
  language: {
    current: "de",
    options: AVAILABLE_LOCALES,
    onSelect(code) {
      localStorage.setItem("editor-locale", code);
      location.reload();
    },
  },
});

Set lang on the page (or on the editor’s container) to the locale’s code. Screen readers use it, and Japanese and Chinese use it to choose the correct form of shared Han characters. The --bpmnkit-ds-font-* tokens in @bpmnkit/ui add CJK fallback fonts under :lang(ja) and :lang(zh).

Keys and plurals. A key is the English text the UI asks for, such as "Undo (Ctrl+Z)" or "{count} elements selected". A key that a locale does not have falls back to English. Placeholders in braces must stay the same in every translation. A message can also be a plural object — createTranslate selects its form with Intl.PluralRules from count:

const pl: Locale = {
  code: "pl",
  name: "Polski",
  messages: {
    "{count} elements selected": {
      one: "Zaznaczono {count} element",
      few: "Zaznaczono {count} elementy",
      many: "Zaznaczono {count} elementów",
      other: "Zaznaczono {count} elementu",
    },
  },
};

Your own translation. A Translate is only a function, so you can wrap a shipped locale to override some strings, or supply a language that is not shipped:

const base = createTranslate(de);
const translate: Translate = (key, vars) =>
  key === "Deploy" ? "Deployen" : base(key, vars);

The shipped translations are machine-assisted and checked against the BPMN terminology that Camunda Modeler uses. Corrections are welcome — see “Improving a translation” in CONTRIBUTING.md.