@bpmnkit/plugins 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * @bpmnkit/canvas-plugin-command-palette-editor — editor extension for the
3
- * command palette plugin. Adds one command per BPMN element type.
3
+ * command palette plugin. Adds one command per BPMN element type. When
4
+ * executed, shows a second-step picker listing candidate source nodes to
5
+ * connect after. Falls back to `setTool` (free-click placement) when the
6
+ * diagram is empty.
4
7
  *
5
8
  * Must be used together with `@bpmnkit/canvas-plugin-command-palette`.
6
9
  *
@@ -11,9 +14,7 @@
11
14
  *
12
15
  * let editorRef: BpmnEditor | null = null;
13
16
  * const palette = createCommandPalettePlugin({ ... });
14
- * const paletteEditor = createCommandPaletteEditorPlugin(palette, (tool) => {
15
- * editorRef?.setTool(tool);
16
- * });
17
+ * const paletteEditor = createCommandPaletteEditorPlugin(palette, () => editorRef);
17
18
  * const editor = new BpmnEditor({ container, xml, plugins: [palette, paletteEditor] });
18
19
  * editorRef = editor;
19
20
  * ```
@@ -27,33 +28,116 @@ const ELEMENT_COMMANDS = ELEMENT_GROUPS.flatMap((group) => group.types.map((type
27
28
  title: `Add ${ELEMENT_TYPE_LABELS[type]}`,
28
29
  description: group.title,
29
30
  })));
31
+ // BPMN element types that cannot have outgoing sequence flows
32
+ const NO_OUTGOING_TYPES = new Set([
33
+ "endEvent",
34
+ "messageEndEvent",
35
+ "escalationEndEvent",
36
+ "errorEndEvent",
37
+ "compensationEndEvent",
38
+ "signalEndEvent",
39
+ "terminateEndEvent",
40
+ "textAnnotation",
41
+ ]);
42
+ // Only gateways may have more than one outgoing sequence flow.
43
+ // Every other connectable element is limited to one outgoing flow.
44
+ const MULTI_OUTGOING_TYPES = new Set([
45
+ "exclusiveGateway",
46
+ "parallelGateway",
47
+ "inclusiveGateway",
48
+ "eventBasedGateway",
49
+ "complexGateway",
50
+ ]);
51
+ // ── Candidate helpers ─────────────────────────────────────────────────────────
52
+ function getCandidates(api) {
53
+ return api
54
+ .getShapes()
55
+ .filter((s) => {
56
+ const el = s.flowElement;
57
+ if (el == null)
58
+ return false;
59
+ if (NO_OUTGOING_TYPES.has(el.type))
60
+ return false;
61
+ // Non-gateway elements can only have one outgoing flow
62
+ if (!MULTI_OUTGOING_TYPES.has(el.type) && el.outgoing.length >= 1)
63
+ return false;
64
+ return true;
65
+ })
66
+ .sort((a, b) => {
67
+ // Prefer leaf nodes (no outgoing) — most natural append targets
68
+ const aOut = a.flowElement?.outgoing.length ?? 0;
69
+ const bOut = b.flowElement?.outgoing.length ?? 0;
70
+ return aOut - bOut;
71
+ });
72
+ }
73
+ function candidateLabel(s) {
74
+ const el = s.flowElement;
75
+ if (!el)
76
+ return s.id;
77
+ const typeLabel = ELEMENT_TYPE_LABELS[el.type] ?? el.type;
78
+ return el.name ? el.name : typeLabel;
79
+ }
80
+ function candidateDescription(s) {
81
+ const el = s.flowElement;
82
+ if (!el)
83
+ return undefined;
84
+ const typeLabel = ELEMENT_TYPE_LABELS[el.type] ?? el.type;
85
+ const out = el.outgoing.length;
86
+ if (el.name) {
87
+ return out > 0 ? `${typeLabel} · ${out} outgoing` : typeLabel;
88
+ }
89
+ return out > 0 ? `${out} outgoing` : undefined;
90
+ }
30
91
  // ── Factory ───────────────────────────────────────────────────────────────────
31
92
  /**
32
93
  * Creates the editor command palette extension plugin.
33
94
  *
34
- * @param palette - The base command palette plugin returned by
35
- * `createCommandPalettePlugin`. Commands are registered into it.
36
- * @param setTool - Callback that activates an element creation tool on the
37
- * editor (e.g. `editor.setTool`). May reference the editor lazily — it is
38
- * only called when the user executes a command, well after construction.
95
+ * @param palette The base command palette plugin.
96
+ * @param getEditor Lazy getter returning the editor instance (or null before
97
+ * it is created). Called only when the user executes a command.
39
98
  */
40
- export function createCommandPaletteEditorPlugin(palette, setTool) {
99
+ export function createCommandPaletteEditorPlugin(palette, getEditor) {
100
+ let _api = null;
41
101
  let _deregister = null;
42
102
  return {
43
103
  name: "command-palette-editor",
44
- install(_api) {
104
+ install(api) {
105
+ _api = api;
45
106
  _deregister = palette.addCommands(ELEMENT_COMMANDS.map((cmd) => ({
46
107
  id: `create:${cmd.type}`,
47
108
  title: cmd.title,
48
109
  description: cmd.description,
49
110
  action() {
50
- setTool(`create:${cmd.type}`);
111
+ const candidates = _api ? getCandidates(_api) : [];
112
+ if (candidates.length === 0) {
113
+ // Empty diagram — fall back to tool mode so the user can
114
+ // click anywhere to place the element.
115
+ getEditor()?.setTool(`create:${cmd.type}`);
116
+ return;
117
+ }
118
+ // Step 2: pick a connection target.
119
+ palette.pushView(candidates.map((s) => ({
120
+ id: `connect:${s.id}:${cmd.type}`,
121
+ title: candidateLabel(s),
122
+ description: candidateDescription(s),
123
+ action() {
124
+ // Step 3: enter a label, then insert.
125
+ const typeLabel = ELEMENT_TYPE_LABELS[cmd.type] ?? cmd.type;
126
+ palette.pushView([], {
127
+ placeholder: `Label for new ${typeLabel} (optional)\u2026`,
128
+ onConfirm(label) {
129
+ getEditor()?.addConnectedElement(s.id, cmd.type, label.trim() || undefined);
130
+ },
131
+ });
132
+ },
133
+ })), { placeholder: "Connect after which element?" });
51
134
  },
52
135
  })));
53
136
  },
54
137
  uninstall() {
55
138
  _deregister?.();
56
139
  _deregister = null;
140
+ _api = null;
57
141
  },
58
142
  };
59
143
  }
@@ -1,4 +1,4 @@
1
1
  export declare const MAIN_MENU_STYLE_ID = "bpmnkit-main-menu-styles-v2";
2
- export declare const MAIN_MENU_CSS = "\n.bpmnkit-main-menu-panel {\n position: absolute;\n top: 0;\n right: 0;\n height: 36px;\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 0 6px;\n background: #f0f4f8;\n border: none;\n border-left: 1px solid #d0d0d0;\n border-radius: 0;\n box-shadow: none;\n z-index: 10000;\n}\n[data-theme=\"dark\"] .bpmnkit-main-menu-panel {\n background: var(--bpmnkit-surface-2, #1e1e2e);\n border-left-color: #313244;\n}\n.bpmnkit-canvas-host:has(.bpmnkit-main-menu-panel:not([style*=\"none\"])) .bpmnkit-tabs {\n padding-right: 160px;\n}\n.bpmnkit-main-menu-title {\n padding: 0 6px;\n font-size: 12px;\n font-weight: 600;\n font-family: system-ui, sans-serif;\n color: var(--bpmnkit-text, #333333);\n white-space: nowrap;\n user-select: none;\n opacity: 0.75;\n}\n.bpmnkit-main-menu-sep {\n width: 1px;\n height: 16px;\n background: var(--bpmnkit-overlay-border, var(--bpmnkit-panel-border, rgba(0, 0, 0, 0.08)));\n flex-shrink: 0;\n}\n.bpmnkit-menu-btn {\n width: 28px;\n height: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n border: 1px solid transparent;\n border-radius: 4px;\n color: var(--bpmnkit-text, #333333);\n cursor: pointer;\n padding: 0;\n flex-shrink: 0;\n transition: background 0.1s;\n}\n.bpmnkit-menu-btn:hover {\n background: var(--bpmnkit-overlay-border, var(--bpmnkit-panel-border, rgba(0, 0, 0, 0.08)));\n}\n.bpmnkit-menu-btn svg {\n width: 16px;\n height: 16px;\n pointer-events: none;\n}\n.bpmnkit-menu-dropdown {\n position: fixed;\n display: none;\n flex-direction: column;\n background: var(--bpmnkit-overlay-bg, rgba(248, 249, 250, 0.96));\n border: 1px solid var(--bpmnkit-overlay-border, var(--bpmnkit-panel-border, rgba(0, 0, 0, 0.08)));\n border-radius: 8px;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);\n z-index: 10001;\n min-width: 220px;\n overflow: hidden;\n}\n.bpmnkit-menu-dropdown.open { display: flex; }\n.bpmnkit-menu-level {\n display: flex;\n flex-direction: column;\n gap: 1px;\n padding: 4px;\n position: relative;\n z-index: 1;\n}\n@keyframes bpmnkit-menu-in-right {\n from { opacity: 0; transform: translateX(20px); }\n to { opacity: 1; transform: translateX(0); }\n}\n@keyframes bpmnkit-menu-in-left {\n from { opacity: 0; transform: translateX(-20px); }\n to { opacity: 1; transform: translateX(0); }\n}\n@keyframes bpmnkit-menu-out-left {\n from { opacity: 1; transform: translateX(0); }\n to { opacity: 0; transform: translateX(-20px); }\n}\n@keyframes bpmnkit-menu-out-right {\n from { opacity: 1; transform: translateX(0); }\n to { opacity: 0; transform: translateX(20px); }\n}\n.bpmnkit-menu-level--in-right { animation: bpmnkit-menu-in-right 180ms ease-out forwards; }\n.bpmnkit-menu-level--in-left { animation: bpmnkit-menu-in-left 180ms ease-out forwards; }\n.bpmnkit-menu-level--out-left { animation: bpmnkit-menu-out-left 150ms ease-in forwards; }\n.bpmnkit-menu-level--out-right { animation: bpmnkit-menu-out-right 150ms ease-in forwards; }\n.bpmnkit-menu-drop-label {\n padding: 3px 8px 1px;\n font-size: 10px;\n font-weight: 600;\n font-family: system-ui, sans-serif;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: var(--bpmnkit-text, #333333);\n opacity: 0.45;\n}\n.bpmnkit-menu-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n background: transparent;\n border: none;\n border-radius: 5px;\n color: var(--bpmnkit-text, #333333);\n cursor: pointer;\n font-family: system-ui, sans-serif;\n font-size: 12px;\n text-align: left;\n width: 100%;\n transition: background 0.1s;\n}\n.bpmnkit-menu-item:hover {\n background: var(--bpmnkit-overlay-border, rgba(0, 0, 0, 0.06));\n}\n.bpmnkit-menu-item-check {\n width: 12px;\n height: 12px;\n flex-shrink: 0;\n color: var(--bpmnkit-highlight, var(--bpmnkit-accent, #1a56db));\n}\n.bpmnkit-menu-item-icon {\n width: 14px;\n height: 14px;\n flex-shrink: 0;\n opacity: 0.65;\n}\n.bpmnkit-menu-item-label {\n flex: 1;\n}\n.bpmnkit-menu-item-arrow {\n width: 12px;\n height: 12px;\n flex-shrink: 0;\n opacity: 0.45;\n}\n.bpmnkit-menu-item-icon svg,\n.bpmnkit-menu-item-check svg,\n.bpmnkit-menu-item-arrow svg {\n width: 100%;\n height: 100%;\n}\n.bpmnkit-menu-back-row {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 2px 4px;\n}\n.bpmnkit-menu-back-btn {\n width: 24px;\n height: 24px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n border: none;\n border-radius: 4px;\n color: var(--bpmnkit-text, #333333);\n cursor: pointer;\n padding: 0;\n flex-shrink: 0;\n transition: background 0.1s;\n}\n.bpmnkit-menu-back-btn:hover {\n background: var(--bpmnkit-overlay-border, rgba(0, 0, 0, 0.06));\n}\n.bpmnkit-menu-back-btn svg {\n width: 12px;\n height: 12px;\n pointer-events: none;\n}\n.bpmnkit-menu-level-title {\n font-size: 12px;\n font-weight: 600;\n font-family: system-ui, sans-serif;\n color: var(--bpmnkit-text, #333333);\n flex: 1;\n}\n.bpmnkit-menu-info-row {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 5px 8px;\n font-family: system-ui, sans-serif;\n font-size: 12px;\n color: var(--bpmnkit-text, #333333);\n opacity: 0.75;\n}\n.bpmnkit-menu-info-text {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.bpmnkit-menu-info-action {\n flex-shrink: 0;\n border: 1px solid var(--bpmnkit-overlay-border, var(--bpmnkit-panel-border, rgba(0, 0, 0, 0.08)));\n border-radius: 4px;\n background: transparent;\n color: var(--bpmnkit-text, #333333);\n cursor: pointer;\n padding: 2px 7px;\n font-size: 11px;\n font-family: system-ui, sans-serif;\n transition: background 0.1s;\n}\n.bpmnkit-menu-info-action:hover {\n background: var(--bpmnkit-overlay-border, rgba(0, 0, 0, 0.06));\n}\n.bpmnkit-menu-drop-sep {\n height: 1px;\n background: var(--bpmnkit-overlay-border, rgba(0,0,0,0.1));\n margin: 3px 4px;\n}\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-dropdown {\n background: var(--bpmnkit-panel-bg, rgba(13,13,22,0.92));\n border-color: var(--bpmnkit-panel-border, rgba(255, 255, 255, 0.08));\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);\n}\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-item,\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-back-btn,\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-level-title,\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-info-row,\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-info-action,\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-drop-label {\n color: rgba(205, 214, 244, 0.9);\n}\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-item:hover,\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-back-btn:hover,\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-info-action:hover {\n background: rgba(255, 255, 255, 0.08);\n}\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-info-action {\n border-color: rgba(255, 255, 255, 0.15);\n}\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-drop-sep {\n background: var(--bpmnkit-panel-border, rgba(255, 255, 255, 0.08));\n}\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-item-check {\n color: var(--bpmnkit-accent-bright, #89b4fa);\n}\n";
2
+ export declare const MAIN_MENU_CSS = "\n.bpmnkit-main-menu-panel {\n position: absolute;\n top: 0;\n right: 0;\n height: 36px;\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 0 6px;\n background: #f0f4f8;\n border: none;\n border-left: 1px solid #d0d0d0;\n border-radius: 0;\n box-shadow: none;\n z-index: 10000;\n}\n[data-theme=\"dark\"] .bpmnkit-main-menu-panel {\n background: var(--bpmnkit-surface-2, #1e1e2e);\n border-left-color: #313244;\n}\n[data-theme=\"neon\"] .bpmnkit-main-menu-panel {\n background: oklch(7% 0.035 280);\n border-left-color: oklch(65% 0.28 280 / 0.2);\n}\n.bpmnkit-canvas-host:has(.bpmnkit-main-menu-panel:not([style*=\"none\"])) .bpmnkit-tabs {\n padding-right: 160px;\n}\n.bpmnkit-main-menu-title {\n padding: 0 6px;\n font-size: 12px;\n font-weight: 600;\n font-family: system-ui, sans-serif;\n color: var(--bpmnkit-text, #333333);\n white-space: nowrap;\n user-select: none;\n opacity: 0.75;\n}\n.bpmnkit-main-menu-sep {\n width: 1px;\n height: 16px;\n background: var(--bpmnkit-overlay-border, var(--bpmnkit-panel-border, rgba(0, 0, 0, 0.08)));\n flex-shrink: 0;\n}\n.bpmnkit-menu-btn {\n width: 28px;\n height: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n border: 1px solid transparent;\n border-radius: 4px;\n color: var(--bpmnkit-text, #333333);\n cursor: pointer;\n padding: 0;\n flex-shrink: 0;\n transition: background 0.1s;\n}\n.bpmnkit-menu-btn:hover {\n background: var(--bpmnkit-overlay-border, var(--bpmnkit-panel-border, rgba(0, 0, 0, 0.08)));\n}\n.bpmnkit-menu-btn svg {\n width: 16px;\n height: 16px;\n pointer-events: none;\n}\n.bpmnkit-menu-dropdown {\n position: fixed;\n display: none;\n flex-direction: column;\n background: var(--bpmnkit-overlay-bg, rgba(248, 249, 250, 0.96));\n border: 1px solid var(--bpmnkit-overlay-border, var(--bpmnkit-panel-border, rgba(0, 0, 0, 0.08)));\n border-radius: 8px;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);\n z-index: 10001;\n min-width: 220px;\n overflow: hidden;\n}\n.bpmnkit-menu-dropdown.open { display: flex; }\n.bpmnkit-menu-level {\n display: flex;\n flex-direction: column;\n gap: 1px;\n padding: 4px;\n position: relative;\n z-index: 1;\n}\n@keyframes bpmnkit-menu-in-right {\n from { opacity: 0; transform: translateX(20px); }\n to { opacity: 1; transform: translateX(0); }\n}\n@keyframes bpmnkit-menu-in-left {\n from { opacity: 0; transform: translateX(-20px); }\n to { opacity: 1; transform: translateX(0); }\n}\n@keyframes bpmnkit-menu-out-left {\n from { opacity: 1; transform: translateX(0); }\n to { opacity: 0; transform: translateX(-20px); }\n}\n@keyframes bpmnkit-menu-out-right {\n from { opacity: 1; transform: translateX(0); }\n to { opacity: 0; transform: translateX(20px); }\n}\n.bpmnkit-menu-level--in-right { animation: bpmnkit-menu-in-right 180ms ease-out forwards; }\n.bpmnkit-menu-level--in-left { animation: bpmnkit-menu-in-left 180ms ease-out forwards; }\n.bpmnkit-menu-level--out-left { animation: bpmnkit-menu-out-left 150ms ease-in forwards; }\n.bpmnkit-menu-level--out-right { animation: bpmnkit-menu-out-right 150ms ease-in forwards; }\n.bpmnkit-menu-drop-label {\n padding: 3px 8px 1px;\n font-size: 10px;\n font-weight: 600;\n font-family: system-ui, sans-serif;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: var(--bpmnkit-text, #333333);\n opacity: 0.45;\n}\n.bpmnkit-menu-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n background: transparent;\n border: none;\n border-radius: 5px;\n color: var(--bpmnkit-text, #333333);\n cursor: pointer;\n font-family: system-ui, sans-serif;\n font-size: 12px;\n text-align: left;\n width: 100%;\n transition: background 0.1s;\n}\n.bpmnkit-menu-item:hover {\n background: var(--bpmnkit-overlay-border, rgba(0, 0, 0, 0.06));\n}\n.bpmnkit-menu-item-check {\n width: 12px;\n height: 12px;\n flex-shrink: 0;\n color: var(--bpmnkit-highlight, var(--bpmnkit-accent, #1a56db));\n}\n.bpmnkit-menu-item-icon {\n width: 14px;\n height: 14px;\n flex-shrink: 0;\n opacity: 0.65;\n}\n.bpmnkit-menu-item-label {\n flex: 1;\n}\n.bpmnkit-menu-item-arrow {\n width: 12px;\n height: 12px;\n flex-shrink: 0;\n opacity: 0.45;\n}\n.bpmnkit-menu-item-icon svg,\n.bpmnkit-menu-item-check svg,\n.bpmnkit-menu-item-arrow svg {\n width: 100%;\n height: 100%;\n}\n.bpmnkit-menu-back-row {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 2px 4px;\n}\n.bpmnkit-menu-back-btn {\n width: 24px;\n height: 24px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n border: none;\n border-radius: 4px;\n color: var(--bpmnkit-text, #333333);\n cursor: pointer;\n padding: 0;\n flex-shrink: 0;\n transition: background 0.1s;\n}\n.bpmnkit-menu-back-btn:hover {\n background: var(--bpmnkit-overlay-border, rgba(0, 0, 0, 0.06));\n}\n.bpmnkit-menu-back-btn svg {\n width: 12px;\n height: 12px;\n pointer-events: none;\n}\n.bpmnkit-menu-level-title {\n font-size: 12px;\n font-weight: 600;\n font-family: system-ui, sans-serif;\n color: var(--bpmnkit-text, #333333);\n flex: 1;\n}\n.bpmnkit-menu-info-row {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 5px 8px;\n font-family: system-ui, sans-serif;\n font-size: 12px;\n color: var(--bpmnkit-text, #333333);\n opacity: 0.75;\n}\n.bpmnkit-menu-info-text {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.bpmnkit-menu-info-action {\n flex-shrink: 0;\n border: 1px solid var(--bpmnkit-overlay-border, var(--bpmnkit-panel-border, rgba(0, 0, 0, 0.08)));\n border-radius: 4px;\n background: transparent;\n color: var(--bpmnkit-text, #333333);\n cursor: pointer;\n padding: 2px 7px;\n font-size: 11px;\n font-family: system-ui, sans-serif;\n transition: background 0.1s;\n}\n.bpmnkit-menu-info-action:hover {\n background: var(--bpmnkit-overlay-border, rgba(0, 0, 0, 0.06));\n}\n.bpmnkit-menu-drop-sep {\n height: 1px;\n background: var(--bpmnkit-overlay-border, rgba(0,0,0,0.1));\n margin: 3px 4px;\n}\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-dropdown {\n background: var(--bpmnkit-panel-bg, rgba(13,13,22,0.92));\n border-color: var(--bpmnkit-panel-border, rgba(255, 255, 255, 0.08));\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);\n}\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-item,\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-back-btn,\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-level-title,\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-info-row,\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-info-action,\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-drop-label {\n color: rgba(205, 214, 244, 0.9);\n}\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-item:hover,\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-back-btn:hover,\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-info-action:hover {\n background: rgba(255, 255, 255, 0.08);\n}\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-info-action {\n border-color: rgba(255, 255, 255, 0.15);\n}\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-drop-sep {\n background: var(--bpmnkit-panel-border, rgba(255, 255, 255, 0.08));\n}\n[data-bpmnkit-hud-theme=\"dark\"] .bpmnkit-menu-item-check {\n color: var(--bpmnkit-accent-bright, #89b4fa);\n}\n[data-bpmnkit-hud-theme=\"neon\"] .bpmnkit-menu-dropdown {\n background: oklch(8% 0.03 270 / 0.96);\n border-color: oklch(65% 0.28 280 / 0.2);\n box-shadow: 0 4px 20px oklch(0% 0 0 / 0.6), 0 0 0 1px oklch(65% 0.28 280 / 0.1);\n}\n[data-bpmnkit-hud-theme=\"neon\"] .bpmnkit-menu-item,\n[data-bpmnkit-hud-theme=\"neon\"] .bpmnkit-menu-back-btn,\n[data-bpmnkit-hud-theme=\"neon\"] .bpmnkit-menu-level-title,\n[data-bpmnkit-hud-theme=\"neon\"] .bpmnkit-menu-info-row,\n[data-bpmnkit-hud-theme=\"neon\"] .bpmnkit-menu-info-action,\n[data-bpmnkit-hud-theme=\"neon\"] .bpmnkit-menu-drop-label {\n color: oklch(73% 0.16 280);\n}\n[data-bpmnkit-hud-theme=\"neon\"] .bpmnkit-menu-item:hover,\n[data-bpmnkit-hud-theme=\"neon\"] .bpmnkit-menu-back-btn:hover,\n[data-bpmnkit-hud-theme=\"neon\"] .bpmnkit-menu-info-action:hover {\n background: oklch(65% 0.28 280 / 0.1);\n}\n[data-bpmnkit-hud-theme=\"neon\"] .bpmnkit-menu-info-action {\n border-color: oklch(65% 0.28 280 / 0.2);\n}\n[data-bpmnkit-hud-theme=\"neon\"] .bpmnkit-menu-drop-sep {\n background: oklch(65% 0.28 280 / 0.15);\n}\n[data-bpmnkit-hud-theme=\"neon\"] .bpmnkit-menu-item-check {\n color: oklch(72% 0.18 185);\n}\n";
3
3
  export declare function injectMainMenuStyles(): void;
4
4
  //# sourceMappingURL=css.d.ts.map
@@ -20,6 +20,10 @@ export const MAIN_MENU_CSS = `
20
20
  background: var(--bpmnkit-surface-2, #1e1e2e);
21
21
  border-left-color: #313244;
22
22
  }
23
+ [data-theme="neon"] .bpmnkit-main-menu-panel {
24
+ background: oklch(7% 0.035 280);
25
+ border-left-color: oklch(65% 0.28 280 / 0.2);
26
+ }
23
27
  .bpmnkit-canvas-host:has(.bpmnkit-main-menu-panel:not([style*="none"])) .bpmnkit-tabs {
24
28
  padding-right: 160px;
25
29
  }
@@ -258,6 +262,33 @@ export const MAIN_MENU_CSS = `
258
262
  [data-bpmnkit-hud-theme="dark"] .bpmnkit-menu-item-check {
259
263
  color: var(--bpmnkit-accent-bright, #89b4fa);
260
264
  }
265
+ [data-bpmnkit-hud-theme="neon"] .bpmnkit-menu-dropdown {
266
+ background: oklch(8% 0.03 270 / 0.96);
267
+ border-color: oklch(65% 0.28 280 / 0.2);
268
+ box-shadow: 0 4px 20px oklch(0% 0 0 / 0.6), 0 0 0 1px oklch(65% 0.28 280 / 0.1);
269
+ }
270
+ [data-bpmnkit-hud-theme="neon"] .bpmnkit-menu-item,
271
+ [data-bpmnkit-hud-theme="neon"] .bpmnkit-menu-back-btn,
272
+ [data-bpmnkit-hud-theme="neon"] .bpmnkit-menu-level-title,
273
+ [data-bpmnkit-hud-theme="neon"] .bpmnkit-menu-info-row,
274
+ [data-bpmnkit-hud-theme="neon"] .bpmnkit-menu-info-action,
275
+ [data-bpmnkit-hud-theme="neon"] .bpmnkit-menu-drop-label {
276
+ color: oklch(73% 0.16 280);
277
+ }
278
+ [data-bpmnkit-hud-theme="neon"] .bpmnkit-menu-item:hover,
279
+ [data-bpmnkit-hud-theme="neon"] .bpmnkit-menu-back-btn:hover,
280
+ [data-bpmnkit-hud-theme="neon"] .bpmnkit-menu-info-action:hover {
281
+ background: oklch(65% 0.28 280 / 0.1);
282
+ }
283
+ [data-bpmnkit-hud-theme="neon"] .bpmnkit-menu-info-action {
284
+ border-color: oklch(65% 0.28 280 / 0.2);
285
+ }
286
+ [data-bpmnkit-hud-theme="neon"] .bpmnkit-menu-drop-sep {
287
+ background: oklch(65% 0.28 280 / 0.15);
288
+ }
289
+ [data-bpmnkit-hud-theme="neon"] .bpmnkit-menu-item-check {
290
+ color: oklch(72% 0.18 185);
291
+ }
261
292
  `;
262
293
  export function injectMainMenuStyles() {
263
294
  if (typeof document === "undefined")
@@ -15,12 +15,14 @@ const CHECK_ICON = '<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" s
15
15
  const MOON_ICON = '<svg viewBox="0 0 16 16" fill="currentColor"><path d="M13 9.5a6 6 0 1 1-7.5-7.5 7 7 0 0 0 7.5 7.5z"/></svg>';
16
16
  const SUN_ICON = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="8" cy="8" r="2.8"/><line x1="8" y1="1.5" x2="8" y2="3"/><line x1="8" y1="13" x2="8" y2="14.5"/><line x1="1.5" y1="8" x2="3" y2="8"/><line x1="13" y1="8" x2="14.5" y2="8"/><line x1="3.3" y1="3.3" x2="4.4" y2="4.4"/><line x1="11.6" y1="11.6" x2="12.7" y2="12.7"/><line x1="3.3" y1="12.7" x2="4.4" y2="11.6"/><line x1="11.6" y1="4.4" x2="12.7" y2="3.3"/></svg>';
17
17
  const AUTO_ICON = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="8" cy="8" r="5.5"/><path d="M8 8V3.5"/><path d="M8 8l3.2 2"/></svg>';
18
+ const NEON_ICON = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9.5,1.5 5.5,8 8.5,8 6.5,14.5 10.5,8 7.5,8 9.5,1.5"/></svg>';
18
19
  const BACK_ICON = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="10,3 5,8 10,13"/></svg>';
19
20
  const ARROW_ICON = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="6,3 11,8 6,13"/></svg>';
20
21
  const THEMES = [
21
22
  { value: "dark", label: "Dark", icon: MOON_ICON },
22
23
  { value: "light", label: "Light", icon: SUN_ICON },
23
24
  { value: "auto", label: "System", icon: AUTO_ICON },
25
+ { value: "neon", label: "Neon", icon: NEON_ICON },
24
26
  ];
25
27
  /**
26
28
  * Creates a main menu plugin instance.
package/dist/tabs/css.js CHANGED
@@ -52,6 +52,22 @@ export const TABS_CSS = `
52
52
  --tab-type-form: #a6e3a1;
53
53
  }
54
54
 
55
+ .bpmnkit-tabs[data-theme="neon"] {
56
+ --tabs-bg: oklch(7% 0.035 280);
57
+ --tabs-border: oklch(65% 0.28 280 / 0.2);
58
+ --tab-fg: oklch(60% 0.12 280);
59
+ --tab-active-bg: oklch(5% 0.025 270);
60
+ --tab-active-fg: oklch(88% 0.02 270);
61
+ --tab-active-border: oklch(72% 0.18 185);
62
+ --tab-hover-bg: oklch(65% 0.28 280 / 0.08);
63
+ --tab-close-hover: oklch(65% 0.28 280 / 0.12);
64
+ --tab-warn-fg: oklch(75% 0.15 60);
65
+ --tab-type-bpmn: oklch(73% 0.16 280);
66
+ --tab-type-dmn: oklch(70% 0.18 300);
67
+ --tab-type-feel: oklch(75% 0.15 60);
68
+ --tab-type-form: oklch(72% 0.18 185);
69
+ }
70
+
55
71
  /* ── Play mode: hide tab groups, show only center slot ───────────────── */
56
72
 
57
73
  .bpmnkit-tabs.bpmnkit-play-mode .bpmnkit-tab {
@@ -203,6 +219,18 @@ export const TABS_CSS = `
203
219
  --welcome-btn-secondary-border: rgba(255,255,255,0.12);
204
220
  }
205
221
 
222
+ .bpmnkit-welcome[data-theme="neon"] {
223
+ --welcome-bg: oklch(5% 0.025 270);
224
+ --welcome-icon: oklch(72% 0.18 185);
225
+ --welcome-title: oklch(88% 0.02 270);
226
+ --welcome-sub: oklch(55% 0.06 280);
227
+ --welcome-btn-primary-bg: oklch(55% 0.22 280);
228
+ --welcome-btn-primary-fg: oklch(95% 0.01 270);
229
+ --welcome-btn-secondary-bg: oklch(65% 0.28 280 / 0.08);
230
+ --welcome-btn-secondary-fg: oklch(73% 0.16 280);
231
+ --welcome-btn-secondary-border: oklch(65% 0.28 280 / 0.25);
232
+ }
233
+
206
234
  .bpmnkit-welcome-inner {
207
235
  display: flex;
208
236
  flex-direction: column;
@@ -431,6 +459,12 @@ export const TABS_CSS = `
431
459
  border-top: none;
432
460
  }
433
461
 
462
+ .bpmnkit-tab-dropdown[data-theme="neon"] {
463
+ background: oklch(7% 0.035 280);
464
+ border: 1px solid oklch(65% 0.28 280 / 0.2);
465
+ border-top: none;
466
+ }
467
+
434
468
  .bpmnkit-tab-drop-item {
435
469
  display: flex;
436
470
  align-items: center;
@@ -471,6 +505,17 @@ export const TABS_CSS = `
471
505
  color: #cdd6f4;
472
506
  }
473
507
 
508
+ .bpmnkit-tab-dropdown[data-theme="neon"] .bpmnkit-tab-drop-item {
509
+ color: oklch(60% 0.12 280);
510
+ }
511
+ .bpmnkit-tab-dropdown[data-theme="neon"] .bpmnkit-tab-drop-item:hover {
512
+ background: oklch(65% 0.28 280 / 0.08);
513
+ }
514
+ .bpmnkit-tab-dropdown[data-theme="neon"] .bpmnkit-tab-drop-item.active {
515
+ background: oklch(5% 0.025 270);
516
+ color: oklch(88% 0.02 270);
517
+ }
518
+
474
519
  .bpmnkit-tab-drop-name {
475
520
  flex: 1;
476
521
  overflow: hidden;
@@ -527,6 +572,20 @@ export const TABS_CSS = `
527
572
  --cd-ghost-hover: rgba(255,255,255,0.05);
528
573
  }
529
574
 
575
+ .bpmnkit-close-dialog[data-theme="neon"] {
576
+ --cd-bg: oklch(9% 0.025 270);
577
+ --cd-border: oklch(65% 0.28 280 / 0.2);
578
+ --cd-title: oklch(88% 0.02 270);
579
+ --cd-body: oklch(65% 0.1 280);
580
+ --cd-primary-bg: oklch(55% 0.22 280);
581
+ --cd-primary-fg: oklch(95% 0.01 270);
582
+ --cd-secondary-bg: oklch(65% 0.28 280 / 0.08);
583
+ --cd-secondary-fg: oklch(73% 0.16 280);
584
+ --cd-secondary-hover: oklch(65% 0.28 280 / 0.14);
585
+ --cd-ghost-fg: oklch(50% 0.08 280);
586
+ --cd-ghost-hover: oklch(65% 0.28 280 / 0.06);
587
+ }
588
+
530
589
  .bpmnkit-close-dialog-title {
531
590
  font-size: 14px;
532
591
  font-weight: 600;
@@ -592,6 +651,7 @@ export const TABS_CSS = `
592
651
  }
593
652
 
594
653
  .bpmnkit-raw-pane[data-theme="light"] { --raw-bg: #f8f9fa; }
654
+ .bpmnkit-raw-pane[data-theme="neon"] { --raw-bg: oklch(5% 0.025 270); }
595
655
 
596
656
  .bpmnkit-raw-copy-btn {
597
657
  position: absolute;
@@ -614,6 +674,12 @@ export const TABS_CSS = `
614
674
  border-color: rgba(0,0,0,0.15);
615
675
  }
616
676
  .bpmnkit-raw-pane[data-theme="light"] .bpmnkit-raw-copy-btn:hover { background: rgba(0,0,0,0.12); }
677
+ .bpmnkit-raw-pane[data-theme="neon"] .bpmnkit-raw-copy-btn {
678
+ background: oklch(65% 0.28 280 / 0.1);
679
+ color: oklch(73% 0.16 280);
680
+ border-color: oklch(65% 0.28 280 / 0.2);
681
+ }
682
+ .bpmnkit-raw-pane[data-theme="neon"] .bpmnkit-raw-copy-btn:hover { background: oklch(65% 0.28 280 / 0.18); }
617
683
 
618
684
  .bpmnkit-raw-content {
619
685
  margin: 0;
@@ -628,6 +694,7 @@ export const TABS_CSS = `
628
694
  }
629
695
 
630
696
  .bpmnkit-raw-pane[data-theme="light"] .bpmnkit-raw-content { --raw-fg: #374151; }
697
+ .bpmnkit-raw-pane[data-theme="neon"] .bpmnkit-raw-content { --raw-fg: oklch(73% 0.16 280); }
631
698
 
632
699
  `.trim();
633
700
  const STYLE_ID = "bpmn-sdk-tabs-css";
@@ -552,7 +552,7 @@ export function createTabsPlugin(options = {}) {
552
552
  contentArea?.appendChild(pane);
553
553
  tab.pane = pane;
554
554
  if (tab.config.type === "dmn") {
555
- const editor = new DmnEditor({ container: pane, theme });
555
+ const editor = new DmnEditor({ container: pane, theme: theme === "neon" ? "dark" : theme });
556
556
  tab.dmnEditor = editor;
557
557
  const xml = Dmn.export(tab.config.defs);
558
558
  void editor.loadXML(xml);
@@ -572,7 +572,7 @@ export function createTabsPlugin(options = {}) {
572
572
  });
573
573
  }
574
574
  else if (tab.config.type === "form") {
575
- const editor = new FormEditor({ container: pane, theme });
575
+ const editor = new FormEditor({ container: pane, theme: theme === "neon" ? "dark" : theme });
576
576
  tab.formEditor = editor;
577
577
  const schema = JSON.parse(Form.export(tab.config.form));
578
578
  void editor.loadSchema(schema);
@@ -601,8 +601,9 @@ export function createTabsPlugin(options = {}) {
601
601
  // shows through. pointer-events are set to none in setActiveTab.
602
602
  }
603
603
  function applyThemeToTab(tab) {
604
- tab.dmnEditor?.setTheme(theme);
605
- tab.formEditor?.setTheme(theme);
604
+ const editorTheme = theme === "neon" ? "dark" : theme;
605
+ tab.dmnEditor?.setTheme(editorTheme);
606
+ tab.formEditor?.setTheme(editorTheme);
606
607
  }
607
608
  function showWarning(tab, show) {
608
609
  tab.hasWarning = show;
@@ -919,9 +920,10 @@ export function createTabsPlugin(options = {}) {
919
920
  install(cApi) {
920
921
  canvasApi = cApi;
921
922
  injectTabsStyles();
922
- // Detect theme from canvas (canvas sets data-theme="dark" for dark, removes it for light)
923
+ // Detect theme from canvas (canvas sets data-theme="dark"/"neon"; absence means light)
923
924
  const container = cApi.container;
924
- theme = container.dataset.theme === "dark" ? "dark" : "light";
925
+ const initialTheme = container.dataset.theme;
926
+ theme = initialTheme === "dark" || initialTheme === "neon" ? initialTheme : "light";
925
927
  // Expand container to be position:relative for absolute children
926
928
  if (getComputedStyle(container).position === "static") {
927
929
  container.style.position = "relative";
@@ -1048,10 +1050,10 @@ export function createTabsPlugin(options = {}) {
1048
1050
  }
1049
1051
  offDiagramChange = anyOn("diagram:change", onDiagramUpdate);
1050
1052
  anyOn("diagram:load", onDiagramUpdate);
1051
- // Listen for theme changes (canvas toggles data-theme="dark"; absence means light)
1053
+ // Listen for theme changes (canvas sets data-theme="dark"/"neon"; absence means light)
1052
1054
  const observer = new MutationObserver(() => {
1053
1055
  const t = container.dataset.theme;
1054
- theme = t === "dark" ? "dark" : "light";
1056
+ theme = t === "dark" || t === "neon" ? t : "light";
1055
1057
  if (tabBar)
1056
1058
  tabBar.dataset.theme = theme;
1057
1059
  if (welcomeEl)
@@ -13,6 +13,8 @@ const CSS = `
13
13
  animation: bpmnkit-token-pulse 1.4s ease-in-out infinite;
14
14
  }
15
15
  .bpmnkit-token-active .bpmnkit-shape-body,
16
+ .bpmnkit-token-active .bpmnkit-callactivity-body,
17
+ .bpmnkit-token-active .bpmnkit-eventsubprocess-body,
16
18
  .bpmnkit-token-active .bpmnkit-event-body,
17
19
  .bpmnkit-token-active .bpmnkit-end-body,
18
20
  .bpmnkit-token-active .bpmnkit-gw-body {
@@ -23,6 +25,8 @@ const CSS = `
23
25
 
24
26
  /* ── Visited shapes (token has passed through) ──────────────────────────── */
25
27
  .bpmnkit-token-visited .bpmnkit-shape-body,
28
+ .bpmnkit-token-visited .bpmnkit-callactivity-body,
29
+ .bpmnkit-token-visited .bpmnkit-eventsubprocess-body,
26
30
  .bpmnkit-token-visited .bpmnkit-event-body,
27
31
  .bpmnkit-token-visited .bpmnkit-end-body,
28
32
  .bpmnkit-token-visited .bpmnkit-gw-body {
@@ -53,22 +53,63 @@ export function createTokenHighlightPlugin() {
53
53
  }
54
54
  }
55
55
  // Edge highlights
56
- // active edge : source visited, target active (token just left source, entering target)
57
- // visited edge : source visited, target visited (token has fully traversed this edge)
58
- // This correctly handles exclusive gateways: only the taken branch's target enters
59
- // visited/active, so only the taken edge is highlighted.
60
- for (const [flowId, { sourceRef, targetRef }] of flowIndex) {
61
- const el = edgeEl(flowId);
62
- if (el === undefined)
63
- continue;
64
- const srcVisited = visitedIds.has(sourceRef);
65
- if (!srcVisited)
66
- continue;
67
- if (activeIds.has(targetRef)) {
68
- el.classList.add("bpmnkit-token-edge-active");
56
+ // Prefer direct sequence-flow tracking: Camunda's element-instances API returns
57
+ // sequence flow element instances (with their flow ID), so visitedIds/activeIds
58
+ // will contain the actual flow IDs that were traversed.
59
+ // Fallback to source/target heuristic only when no flow IDs are present (older
60
+ // engines or environments that don't track sequence flows as element instances).
61
+ // The heuristic is deliberately disabled when direct tracking is available because
62
+ // it over-highlights: when a gateway's default path leads to a node that was
63
+ // reached via a different branch, both edges appear highlighted incorrectly.
64
+ let hasFlowTracking = false;
65
+ for (const flowId of flowIndex.keys()) {
66
+ if (visitedIds.has(flowId) || activeIds.has(flowId)) {
67
+ hasFlowTracking = true;
68
+ break;
69
69
  }
70
- else if (visitedIds.has(targetRef)) {
71
- el.classList.add("bpmnkit-token-edge-visited");
70
+ }
71
+ if (hasFlowTracking) {
72
+ // Direct mode: flow IDs are in visitedIds/activeIds — highlight exactly what was traversed.
73
+ for (const [flowId] of flowIndex) {
74
+ const el = edgeEl(flowId);
75
+ if (el === undefined)
76
+ continue;
77
+ if (activeIds.has(flowId)) {
78
+ el.classList.add("bpmnkit-token-edge-active");
79
+ }
80
+ else if (visitedIds.has(flowId)) {
81
+ el.classList.add("bpmnkit-token-edge-visited");
82
+ }
83
+ }
84
+ }
85
+ else {
86
+ // Heuristic fallback for engines that don't return sequence-flow element instances.
87
+ //
88
+ // "Unique winner" rule: group outgoing flows by source and only highlight an
89
+ // edge when it is the SOLE outgoing flow from that source whose target is
90
+ // visited/active. If multiple candidates exist we cannot determine which path
91
+ // was actually taken (e.g. both branches of an exclusive gateway converge on
92
+ // the same downstream node), so we highlight none to avoid false positives.
93
+ const bySource = new Map();
94
+ for (const [flowId, { sourceRef, targetRef }] of flowIndex) {
95
+ if (!visitedIds.has(sourceRef))
96
+ continue;
97
+ const isActive = activeIds.has(targetRef);
98
+ if (!isActive && !visitedIds.has(targetRef))
99
+ continue;
100
+ const list = bySource.get(sourceRef) ?? [];
101
+ list.push({ flowId, isActive });
102
+ bySource.set(sourceRef, list);
103
+ }
104
+ for (const candidates of bySource.values()) {
105
+ if (candidates.length !== 1)
106
+ continue;
107
+ for (const { flowId, isActive } of candidates) {
108
+ const el = edgeEl(flowId);
109
+ if (el === undefined)
110
+ continue;
111
+ el.classList.add(isActive ? "bpmnkit-token-edge-active" : "bpmnkit-token-edge-visited");
112
+ }
72
113
  }
73
114
  }
74
115
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bpmnkit/plugins",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -104,11 +104,11 @@
104
104
  "dist/**/*.d.ts"
105
105
  ],
106
106
  "dependencies": {
107
- "@bpmnkit/ascii": "0.0.11",
108
- "@bpmnkit/canvas": "0.0.11",
109
- "@bpmnkit/core": "0.0.11",
110
- "@bpmnkit/editor": "0.0.11",
111
- "@bpmnkit/feel": "0.0.11"
107
+ "@bpmnkit/ascii": "0.0.12",
108
+ "@bpmnkit/core": "0.0.12",
109
+ "@bpmnkit/canvas": "0.0.12",
110
+ "@bpmnkit/editor": "0.0.12",
111
+ "@bpmnkit/feel": "0.0.12"
112
112
  },
113
113
  "description": "22 composable canvas plugins for BPMN editors and viewers — minimap, AI chat, process simulation, storage, and more",
114
114
  "keywords": [