@bpmnkit/plugins 0.0.8

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.
Files changed (140) hide show
  1. package/README.md +153 -0
  2. package/dist/ai-bridge/css.d.ts +2 -0
  3. package/dist/ai-bridge/css.js +366 -0
  4. package/dist/ai-bridge/index.d.ts +44 -0
  5. package/dist/ai-bridge/index.js +132 -0
  6. package/dist/ai-bridge/panel.d.ts +27 -0
  7. package/dist/ai-bridge/panel.js +715 -0
  8. package/dist/ascii-view/css.d.ts +4 -0
  9. package/dist/ascii-view/css.js +83 -0
  10. package/dist/ascii-view/index.d.ts +25 -0
  11. package/dist/ascii-view/index.js +92 -0
  12. package/dist/command-palette/css.d.ts +4 -0
  13. package/dist/command-palette/css.js +157 -0
  14. package/dist/command-palette/index.d.ts +53 -0
  15. package/dist/command-palette/index.js +378 -0
  16. package/dist/command-palette-editor/index.d.ts +35 -0
  17. package/dist/command-palette-editor/index.js +60 -0
  18. package/dist/config-panel/css.d.ts +4 -0
  19. package/dist/config-panel/css.js +719 -0
  20. package/dist/config-panel/index.d.ts +36 -0
  21. package/dist/config-panel/index.js +73 -0
  22. package/dist/config-panel/renderer.d.ts +127 -0
  23. package/dist/config-panel/renderer.js +1162 -0
  24. package/dist/config-panel/types.d.ts +118 -0
  25. package/dist/config-panel/types.js +2 -0
  26. package/dist/config-panel-bpmn/index.d.ts +58 -0
  27. package/dist/config-panel-bpmn/index.js +1320 -0
  28. package/dist/config-panel-bpmn/template-engine.d.ts +17 -0
  29. package/dist/config-panel-bpmn/template-engine.js +307 -0
  30. package/dist/config-panel-bpmn/template-to-service-task.d.ts +30 -0
  31. package/dist/config-panel-bpmn/template-to-service-task.js +92 -0
  32. package/dist/config-panel-bpmn/template-types.d.ts +126 -0
  33. package/dist/config-panel-bpmn/template-types.js +6 -0
  34. package/dist/config-panel-bpmn/templates/generated.d.ts +3 -0
  35. package/dist/config-panel-bpmn/templates/generated.js +57865 -0
  36. package/dist/config-panel-bpmn/util.d.ts +46 -0
  37. package/dist/config-panel-bpmn/util.js +195 -0
  38. package/dist/dmn-editor/css.d.ts +3 -0
  39. package/dist/dmn-editor/css.js +667 -0
  40. package/dist/dmn-editor/dmn-editor.d.ts +34 -0
  41. package/dist/dmn-editor/dmn-editor.js +646 -0
  42. package/dist/dmn-editor/drd-canvas.d.ts +85 -0
  43. package/dist/dmn-editor/drd-canvas.js +1087 -0
  44. package/dist/dmn-editor/index.d.ts +3 -0
  45. package/dist/dmn-editor/index.js +2 -0
  46. package/dist/dmn-viewer/css.d.ts +3 -0
  47. package/dist/dmn-viewer/css.js +151 -0
  48. package/dist/dmn-viewer/dmn-viewer.d.ts +30 -0
  49. package/dist/dmn-viewer/dmn-viewer.js +160 -0
  50. package/dist/dmn-viewer/feel.d.ts +3 -0
  51. package/dist/dmn-viewer/feel.js +2 -0
  52. package/dist/dmn-viewer/index.d.ts +14 -0
  53. package/dist/dmn-viewer/index.js +12 -0
  54. package/dist/element-docs/content.d.ts +20 -0
  55. package/dist/element-docs/content.js +1032 -0
  56. package/dist/element-docs/index.d.ts +7 -0
  57. package/dist/element-docs/index.js +643 -0
  58. package/dist/feel-playground/css.d.ts +4 -0
  59. package/dist/feel-playground/css.js +292 -0
  60. package/dist/feel-playground/index.d.ts +4 -0
  61. package/dist/feel-playground/index.js +3 -0
  62. package/dist/feel-playground/playground-plugin.d.ts +14 -0
  63. package/dist/feel-playground/playground-plugin.js +292 -0
  64. package/dist/form-editor/css.d.ts +3 -0
  65. package/dist/form-editor/css.js +703 -0
  66. package/dist/form-editor/form-editor.d.ts +48 -0
  67. package/dist/form-editor/form-editor.js +1039 -0
  68. package/dist/form-editor/index.d.ts +3 -0
  69. package/dist/form-editor/index.js +2 -0
  70. package/dist/form-viewer/css.d.ts +3 -0
  71. package/dist/form-viewer/css.js +303 -0
  72. package/dist/form-viewer/form-viewer.d.ts +29 -0
  73. package/dist/form-viewer/form-viewer.js +376 -0
  74. package/dist/form-viewer/index.d.ts +12 -0
  75. package/dist/form-viewer/index.js +11 -0
  76. package/dist/history/checkpoint.d.ts +10 -0
  77. package/dist/history/checkpoint.js +92 -0
  78. package/dist/history/css.d.ts +2 -0
  79. package/dist/history/css.js +130 -0
  80. package/dist/history/history-panel.d.ts +12 -0
  81. package/dist/history/history-panel.js +142 -0
  82. package/dist/history/index.d.ts +5 -0
  83. package/dist/history/index.js +3 -0
  84. package/dist/main-menu/css.d.ts +4 -0
  85. package/dist/main-menu/css.js +272 -0
  86. package/dist/main-menu/index.d.ts +59 -0
  87. package/dist/main-menu/index.js +285 -0
  88. package/dist/minimap/css.d.ts +10 -0
  89. package/dist/minimap/css.js +54 -0
  90. package/dist/minimap/index.d.ts +38 -0
  91. package/dist/minimap/index.js +69 -0
  92. package/dist/minimap/minimap.d.ts +35 -0
  93. package/dist/minimap/minimap.js +166 -0
  94. package/dist/optimize/index.d.ts +21 -0
  95. package/dist/optimize/index.js +304 -0
  96. package/dist/process-runner/css.d.ts +2 -0
  97. package/dist/process-runner/css.js +467 -0
  98. package/dist/process-runner/index.d.ts +56 -0
  99. package/dist/process-runner/index.js +549 -0
  100. package/dist/storage/auto-save.d.ts +11 -0
  101. package/dist/storage/auto-save.js +44 -0
  102. package/dist/storage/css.d.ts +2 -0
  103. package/dist/storage/css.js +2 -0
  104. package/dist/storage/db.d.ts +99 -0
  105. package/dist/storage/db.js +116 -0
  106. package/dist/storage/dialog.d.ts +15 -0
  107. package/dist/storage/dialog.js +201 -0
  108. package/dist/storage/export.d.ts +3 -0
  109. package/dist/storage/export.js +137 -0
  110. package/dist/storage/index.d.ts +44 -0
  111. package/dist/storage/index.js +201 -0
  112. package/dist/storage/sidebar.d.ts +2 -0
  113. package/dist/storage/sidebar.js +2 -0
  114. package/dist/storage/storage-api.d.ts +76 -0
  115. package/dist/storage/storage-api.js +342 -0
  116. package/dist/storage/types.d.ts +44 -0
  117. package/dist/storage/types.js +2 -0
  118. package/dist/storage-tabs-bridge/index.d.ts +96 -0
  119. package/dist/storage-tabs-bridge/index.js +502 -0
  120. package/dist/tabs/css.d.ts +3 -0
  121. package/dist/tabs/css.js +642 -0
  122. package/dist/tabs/file-resolver.d.ts +71 -0
  123. package/dist/tabs/file-resolver.js +46 -0
  124. package/dist/tabs/index.d.ts +15 -0
  125. package/dist/tabs/index.js +13 -0
  126. package/dist/tabs/tabs-plugin.d.ts +250 -0
  127. package/dist/tabs/tabs-plugin.js +1119 -0
  128. package/dist/token-highlight/css.d.ts +2 -0
  129. package/dist/token-highlight/css.js +81 -0
  130. package/dist/token-highlight/index.d.ts +44 -0
  131. package/dist/token-highlight/index.js +164 -0
  132. package/dist/watermark/css.d.ts +10 -0
  133. package/dist/watermark/css.js +54 -0
  134. package/dist/watermark/index.d.ts +48 -0
  135. package/dist/watermark/index.js +64 -0
  136. package/dist/zoom-controls/css.d.ts +4 -0
  137. package/dist/zoom-controls/css.js +48 -0
  138. package/dist/zoom-controls/index.d.ts +36 -0
  139. package/dist/zoom-controls/index.js +111 -0
  140. package/package.json +121 -0
@@ -0,0 +1,1119 @@
1
+ import { Bpmn, Dmn, Form, } from "@bpmnkit/core";
2
+ import { DmnEditor } from "../dmn-editor/index.js";
3
+ import { buildFeelPlaygroundPanel, injectPlaygroundStyles } from "../feel-playground/index.js";
4
+ import { FormEditor } from "../form-editor/index.js";
5
+ import { injectTabsStyles } from "./css.js";
6
+ import { InMemoryFileResolver } from "./file-resolver.js";
7
+ let _tabCounter = 0;
8
+ const RAW_MODE_ICON = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="5,3 1,8 5,13"/><polyline points="11,3 15,8 11,13"/></svg>';
9
+ /** The order in which type groups appear in the tab bar. */
10
+ const GROUP_TYPES = ["bpmn", "dmn", "form", "feel"];
11
+ /**
12
+ * Creates a tabs plugin instance.
13
+ *
14
+ * When installed into a `BpmnCanvas` (or `BpmnEditor`), it adds a tab bar overlay
15
+ * at the top of the canvas container and manages multiple BPMN/DMN/Form views.
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const resolver = new InMemoryFileResolver();
20
+ * resolver.registerDmn(dmnDefs);
21
+ * resolver.registerForm(formDef);
22
+ *
23
+ * const tabsPlugin = createTabsPlugin({ resolver });
24
+ * const canvas = new BpmnCanvas({ container, xml, plugins: [tabsPlugin] });
25
+ *
26
+ * // Navigate programmatically
27
+ * tabsPlugin.api.openDecision("Decision_1");
28
+ * ```
29
+ */
30
+ export function createTabsPlugin(options = {}) {
31
+ const resolver = options.resolver ?? null;
32
+ const tabs = [];
33
+ let activeId = null;
34
+ let canvasApi = null;
35
+ let tabBar = null;
36
+ let contentArea = null;
37
+ let welcomeEl = null;
38
+ let dynamicSectionsEl = null;
39
+ let dropdownEl = null;
40
+ let recentBtnEl = null;
41
+ let recentListEl = null;
42
+ let theme = "dark";
43
+ let offDiagramChange;
44
+ let isRawMode = false;
45
+ let rawPaneEl = null;
46
+ let rawPreEl = null;
47
+ let rawModeBtn = null;
48
+ let isProjectMode = false;
49
+ let centerSlotEl = null;
50
+ let fileInputEl = null;
51
+ let dndDragoverHandler = null;
52
+ let dndDropHandler = null;
53
+ async function importFiles(files) {
54
+ for (const file of Array.from(files)) {
55
+ const name = file.name;
56
+ const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
57
+ try {
58
+ const text = await file.text();
59
+ if (ext === ".bpmn" || ext === ".xml") {
60
+ Bpmn.parse(text); // validate — throws on malformed XML
61
+ api.openTab({ type: "bpmn", xml: text, name });
62
+ }
63
+ else if (ext === ".dmn") {
64
+ const defs = Dmn.parse(text);
65
+ if (resolver instanceof InMemoryFileResolver)
66
+ resolver.registerDmn(defs);
67
+ api.openTab({ type: "dmn", defs, name: defs.name ?? name });
68
+ }
69
+ else if (ext === ".form" || ext === ".json") {
70
+ const form = Form.parse(text);
71
+ if (resolver instanceof InMemoryFileResolver)
72
+ resolver.registerForm(form);
73
+ api.openTab({ type: "form", form, name: form.id ?? name });
74
+ }
75
+ }
76
+ catch (err) {
77
+ console.error(`[bpmn-sdk] Failed to import ${name}:`, err);
78
+ }
79
+ }
80
+ }
81
+ /** Tracks which tab ID each BPMN process ID belongs to. */
82
+ const bpmnProcessToTabId = new Map();
83
+ /** Display name for each tracked BPMN process ID. */
84
+ const bpmnProcessNames = new Map();
85
+ /** Tracks the last-activated tab ID per type group. */
86
+ const groupActiveId = new Map();
87
+ /** Which type group's dropdown is currently open, if any. */
88
+ let openDropdownType = null;
89
+ let outsideClickHandler = null;
90
+ // --- Close-confirmation dialog ---
91
+ /** Returns true when a tab has in-memory content worth offering to save. */
92
+ function hasContent(config) {
93
+ // A BPMN tab with xml:"" represents the main canvas — skip dialog for it.
94
+ if (config.type === "bpmn")
95
+ return config.xml.length > 0;
96
+ if (config.type === "feel")
97
+ return false;
98
+ return true; // dmn and form always carry parsed content
99
+ }
100
+ /**
101
+ * Shows an in-canvas confirmation dialog asking the user whether to download
102
+ * the tab's content before closing.
103
+ */
104
+ function showCloseDialog(container, tabName, onDownload, onClose) {
105
+ const overlay = document.createElement("div");
106
+ overlay.className = "bpmnkit-close-overlay";
107
+ const dialog = document.createElement("div");
108
+ dialog.className = "bpmnkit-close-dialog";
109
+ dialog.dataset.theme = theme;
110
+ const titleEl = document.createElement("div");
111
+ titleEl.className = "bpmnkit-close-dialog-title";
112
+ titleEl.textContent = `Close "${tabName}"?`;
113
+ const bodyEl = document.createElement("div");
114
+ bodyEl.className = "bpmnkit-close-dialog-body";
115
+ bodyEl.textContent =
116
+ "This file only exists in memory and will be lost. Download a copy before closing?";
117
+ const actionsEl = document.createElement("div");
118
+ actionsEl.className = "bpmnkit-close-dialog-actions";
119
+ function dismiss() {
120
+ overlay.remove();
121
+ document.removeEventListener("keydown", handleKey);
122
+ }
123
+ const cancelBtn = document.createElement("button");
124
+ cancelBtn.type = "button";
125
+ cancelBtn.className = "bpmnkit-close-dialog-btn ghost";
126
+ cancelBtn.textContent = "Cancel";
127
+ cancelBtn.addEventListener("click", dismiss);
128
+ const discardBtn = document.createElement("button");
129
+ discardBtn.type = "button";
130
+ discardBtn.className = "bpmnkit-close-dialog-btn secondary";
131
+ discardBtn.textContent = "Close without saving";
132
+ discardBtn.addEventListener("click", () => {
133
+ dismiss();
134
+ onClose();
135
+ });
136
+ actionsEl.appendChild(cancelBtn);
137
+ actionsEl.appendChild(discardBtn);
138
+ if (onDownload) {
139
+ const downloadBtn = document.createElement("button");
140
+ downloadBtn.type = "button";
141
+ downloadBtn.className = "bpmnkit-close-dialog-btn primary";
142
+ downloadBtn.textContent = "Download & Close";
143
+ downloadBtn.addEventListener("click", () => {
144
+ dismiss();
145
+ onDownload();
146
+ onClose();
147
+ });
148
+ actionsEl.appendChild(downloadBtn);
149
+ // Delay focus so the button is in the DOM and focusable
150
+ requestAnimationFrame(() => downloadBtn.focus());
151
+ }
152
+ dialog.appendChild(titleEl);
153
+ dialog.appendChild(bodyEl);
154
+ dialog.appendChild(actionsEl);
155
+ overlay.appendChild(dialog);
156
+ container.appendChild(overlay);
157
+ function handleKey(e) {
158
+ if (e.key === "Escape")
159
+ dismiss();
160
+ }
161
+ document.addEventListener("keydown", handleKey);
162
+ }
163
+ // --- Welcome screen ---
164
+ function createWelcomeEl() {
165
+ const el = document.createElement("div");
166
+ el.className = "bpmnkit-welcome";
167
+ el.dataset.theme = theme;
168
+ el.style.display = "none";
169
+ const inner = document.createElement("div");
170
+ inner.className = "bpmnkit-welcome-inner";
171
+ const iconEl = document.createElement("div");
172
+ iconEl.className = "bpmnkit-welcome-icon";
173
+ iconEl.innerHTML =
174
+ '<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">' +
175
+ '<polygon points="50,10 90,50 50,90 10,50" fill="currentColor"/>' +
176
+ '<line x1="50" y1="27" x2="50" y2="73" style="stroke: var(--welcome-bg, #1e1e2e)" stroke-width="7.5" stroke-linecap="round"/>' +
177
+ '<line x1="27" y1="50" x2="73" y2="50" style="stroke: var(--welcome-bg, #1e1e2e)" stroke-width="7.5" stroke-linecap="round"/>' +
178
+ "</svg>";
179
+ inner.appendChild(iconEl);
180
+ const title = document.createElement("h2");
181
+ title.className = "bpmnkit-welcome-title";
182
+ title.textContent = "BPMN Editor";
183
+ inner.appendChild(title);
184
+ const sub = document.createElement("p");
185
+ sub.className = "bpmnkit-welcome-sub";
186
+ sub.textContent = "Open a diagram or start fresh to get going.";
187
+ inner.appendChild(sub);
188
+ const actions = document.createElement("div");
189
+ actions.className = "bpmnkit-welcome-actions";
190
+ const newBtn = document.createElement("button");
191
+ newBtn.type = "button";
192
+ newBtn.className = "bpmnkit-welcome-btn primary";
193
+ newBtn.textContent = "New diagram";
194
+ newBtn.addEventListener("click", () => options.onNewDiagram?.());
195
+ const importBtn = document.createElement("button");
196
+ importBtn.type = "button";
197
+ importBtn.className = "bpmnkit-welcome-btn secondary";
198
+ importBtn.textContent = "Import files\u2026";
199
+ importBtn.addEventListener("click", () => {
200
+ if (options.onImportFiles) {
201
+ options.onImportFiles();
202
+ }
203
+ else if (options.enableFileImport) {
204
+ api.openFilePicker();
205
+ }
206
+ });
207
+ actions.appendChild(newBtn);
208
+ actions.appendChild(importBtn);
209
+ // Recent projects dropdown button — populated each time the welcome screen is shown
210
+ if (options.getRecentProjects) {
211
+ const recentBtn = document.createElement("button");
212
+ recentBtn.type = "button";
213
+ recentBtn.className = "bpmnkit-welcome-btn secondary";
214
+ recentBtn.disabled = true;
215
+ recentBtnEl = recentBtn;
216
+ const recentList = document.createElement("div");
217
+ recentList.className = "bpmnkit-welcome-recent-list";
218
+ recentList.style.display = "none";
219
+ recentListEl = recentList;
220
+ recentBtn.addEventListener("click", () => {
221
+ if (!recentListEl)
222
+ return;
223
+ recentListEl.style.display = recentListEl.style.display === "none" ? "" : "none";
224
+ });
225
+ actions.appendChild(recentBtn);
226
+ actions.appendChild(recentList);
227
+ }
228
+ inner.appendChild(actions);
229
+ if (options.examples && options.examples.length > 0) {
230
+ const divider = document.createElement("div");
231
+ divider.className = "bpmnkit-welcome-divider";
232
+ inner.appendChild(divider);
233
+ const examplesLabel = document.createElement("div");
234
+ examplesLabel.className = "bpmnkit-welcome-examples-label";
235
+ examplesLabel.textContent = "Examples";
236
+ inner.appendChild(examplesLabel);
237
+ const list = document.createElement("div");
238
+ list.className = "bpmnkit-welcome-examples";
239
+ for (const example of options.examples) {
240
+ const item = document.createElement("button");
241
+ item.type = "button";
242
+ item.className = "bpmnkit-welcome-example";
243
+ item.addEventListener("click", () => example.onOpen());
244
+ if (example.badge) {
245
+ const badge = document.createElement("span");
246
+ badge.className = `bpmnkit-welcome-example-badge ${example.badge.toLowerCase()}`;
247
+ badge.textContent = example.badge;
248
+ item.appendChild(badge);
249
+ }
250
+ const text = document.createElement("span");
251
+ text.className = "bpmnkit-welcome-example-text";
252
+ const labelEl = document.createElement("span");
253
+ labelEl.className = "bpmnkit-welcome-example-label";
254
+ labelEl.textContent = example.label;
255
+ text.appendChild(labelEl);
256
+ if (example.description) {
257
+ const descEl = document.createElement("span");
258
+ descEl.className = "bpmnkit-welcome-example-desc";
259
+ descEl.textContent = example.description;
260
+ text.appendChild(descEl);
261
+ }
262
+ item.appendChild(text);
263
+ const arrow = document.createElement("span");
264
+ arrow.className = "bpmnkit-welcome-example-arrow";
265
+ arrow.innerHTML =
266
+ '<svg viewBox="0 0 8 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="1,1 7,6 1,11"/></svg>';
267
+ item.appendChild(arrow);
268
+ list.appendChild(item);
269
+ }
270
+ inner.appendChild(list);
271
+ }
272
+ // Placeholder for dynamic sections (e.g. projects) — populated on each show
273
+ if (options.getWelcomeSections) {
274
+ const sectionsContainer = document.createElement("div");
275
+ sectionsContainer.className = "bpmnkit-welcome-dynamic-sections";
276
+ inner.appendChild(sectionsContainer);
277
+ dynamicSectionsEl = sectionsContainer;
278
+ }
279
+ el.appendChild(inner);
280
+ return el;
281
+ }
282
+ function renderDynamicSections() {
283
+ const container = dynamicSectionsEl;
284
+ if (!container || !options.getWelcomeSections)
285
+ return;
286
+ container.textContent = "";
287
+ const sections = options.getWelcomeSections();
288
+ for (const section of sections) {
289
+ const items = section.getItems();
290
+ const divider = document.createElement("div");
291
+ divider.className = "bpmnkit-welcome-divider";
292
+ container.appendChild(divider);
293
+ const sectionLabel = document.createElement("div");
294
+ sectionLabel.className = "bpmnkit-welcome-examples-label";
295
+ sectionLabel.textContent = section.label;
296
+ container.appendChild(sectionLabel);
297
+ if (items.length === 0 && section.emptyText) {
298
+ const empty = document.createElement("div");
299
+ empty.className = "bpmnkit-welcome-empty";
300
+ empty.textContent = section.emptyText;
301
+ container.appendChild(empty);
302
+ continue;
303
+ }
304
+ const list = document.createElement("div");
305
+ list.className = "bpmnkit-welcome-examples";
306
+ for (const item of items) {
307
+ const btn = document.createElement("button");
308
+ btn.type = "button";
309
+ btn.className = "bpmnkit-welcome-example";
310
+ btn.addEventListener("click", () => item.onOpen());
311
+ const text = document.createElement("span");
312
+ text.className = "bpmnkit-welcome-example-text";
313
+ const labelEl = document.createElement("span");
314
+ labelEl.className = "bpmnkit-welcome-example-label";
315
+ labelEl.textContent = item.label;
316
+ text.appendChild(labelEl);
317
+ if (item.description) {
318
+ const descEl = document.createElement("span");
319
+ descEl.className = "bpmnkit-welcome-example-desc";
320
+ descEl.textContent = item.description;
321
+ text.appendChild(descEl);
322
+ }
323
+ btn.appendChild(text);
324
+ const arrow = document.createElement("span");
325
+ arrow.className = "bpmnkit-welcome-example-arrow";
326
+ arrow.innerHTML =
327
+ '<svg viewBox="0 0 8 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="1,1 7,6 1,11"/></svg>';
328
+ btn.appendChild(arrow);
329
+ list.appendChild(btn);
330
+ }
331
+ container.appendChild(list);
332
+ }
333
+ }
334
+ function renderRecentProjects() {
335
+ if (!recentBtnEl || !recentListEl || !options.getRecentProjects)
336
+ return;
337
+ const items = options.getRecentProjects();
338
+ if (!items || items.length === 0) {
339
+ recentBtnEl.disabled = true;
340
+ recentBtnEl.textContent = "Open recent\u2026";
341
+ recentListEl.style.display = "none";
342
+ return;
343
+ }
344
+ recentBtnEl.disabled = false;
345
+ recentBtnEl.innerHTML =
346
+ 'Open recent\u2026 <svg style="width:8px;height:5px;vertical-align:middle;margin-left:2px" viewBox="0 0 10 6" fill="currentColor"><path d="M0 0l5 6 5-6z"/></svg>';
347
+ // Collapse the list on re-render
348
+ recentListEl.style.display = "none";
349
+ recentListEl.textContent = "";
350
+ for (const item of items) {
351
+ const btn = document.createElement("button");
352
+ btn.type = "button";
353
+ btn.className = "bpmnkit-welcome-example";
354
+ btn.addEventListener("click", () => {
355
+ if (recentListEl)
356
+ recentListEl.style.display = "none";
357
+ item.onOpen();
358
+ });
359
+ const text = document.createElement("span");
360
+ text.className = "bpmnkit-welcome-example-text";
361
+ const labelEl = document.createElement("span");
362
+ labelEl.className = "bpmnkit-welcome-example-label";
363
+ labelEl.textContent = item.label;
364
+ text.appendChild(labelEl);
365
+ if (item.description) {
366
+ const descEl = document.createElement("span");
367
+ descEl.className = "bpmnkit-welcome-example-desc";
368
+ descEl.textContent = item.description;
369
+ text.appendChild(descEl);
370
+ }
371
+ btn.appendChild(text);
372
+ const arrow = document.createElement("span");
373
+ arrow.className = "bpmnkit-welcome-example-arrow";
374
+ arrow.innerHTML =
375
+ '<svg viewBox="0 0 8 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="1,1 7,6 1,11"/></svg>';
376
+ btn.appendChild(arrow);
377
+ recentListEl.appendChild(btn);
378
+ }
379
+ }
380
+ function showWelcomeScreen() {
381
+ if (welcomeEl)
382
+ welcomeEl.style.display = "";
383
+ if (tabBar)
384
+ tabBar.style.display = "none";
385
+ canvasApi?.container.classList.add("bpmnkit-welcome-active");
386
+ renderDynamicSections();
387
+ renderRecentProjects();
388
+ // Defer so the callback runs after all synchronous plugin/HUD initialization
389
+ // completes. On initial install the HUD elements don't exist yet; rAF fires
390
+ // after the current JS task, by which time initEditorHud() has run.
391
+ requestAnimationFrame(() => options.onWelcomeShow?.());
392
+ }
393
+ function hideWelcomeScreen() {
394
+ if (welcomeEl)
395
+ welcomeEl.style.display = "none";
396
+ if (tabBar)
397
+ tabBar.style.display = "";
398
+ canvasApi?.container.classList.remove("bpmnkit-welcome-active");
399
+ }
400
+ // --- Group dropdown ---
401
+ function closeDropdownEl() {
402
+ if (dropdownEl)
403
+ dropdownEl.classList.remove("open");
404
+ openDropdownType = null;
405
+ }
406
+ function openDropdown(type, group, anchorEl) {
407
+ if (!dropdownEl)
408
+ return;
409
+ dropdownEl.innerHTML = "";
410
+ dropdownEl.dataset.theme = theme;
411
+ for (const tab of group) {
412
+ const item = document.createElement("div");
413
+ item.className = "bpmnkit-tab-drop-item";
414
+ if (tab.id === groupActiveId.get(type))
415
+ item.classList.add("active");
416
+ const nameSpan = document.createElement("span");
417
+ nameSpan.className = "bpmnkit-tab-drop-name";
418
+ nameSpan.textContent = tab.config.name ?? tab.id;
419
+ item.appendChild(nameSpan);
420
+ if (!isProjectMode) {
421
+ const closeBtn = document.createElement("span");
422
+ closeBtn.className = "bpmnkit-tab-close";
423
+ closeBtn.textContent = "×";
424
+ closeBtn.addEventListener("click", (e) => {
425
+ e.stopPropagation();
426
+ closeDropdownEl();
427
+ requestClose(tab);
428
+ });
429
+ item.appendChild(closeBtn);
430
+ }
431
+ item.addEventListener("click", () => {
432
+ groupActiveId.set(type, tab.id);
433
+ api.setActiveTab(tab.id);
434
+ closeDropdownEl();
435
+ });
436
+ dropdownEl.appendChild(item);
437
+ }
438
+ const rect = anchorEl.getBoundingClientRect();
439
+ dropdownEl.style.top = `${rect.bottom}px`;
440
+ dropdownEl.style.left = `${rect.left}px`;
441
+ dropdownEl.classList.add("open");
442
+ openDropdownType = type;
443
+ }
444
+ function toggleDropdown(type, group, anchorEl) {
445
+ if (openDropdownType === type) {
446
+ closeDropdownEl();
447
+ }
448
+ else {
449
+ openDropdown(type, group, anchorEl);
450
+ }
451
+ }
452
+ // --- Tab bar rendering ---
453
+ function requestClose(tab) {
454
+ if (isProjectMode)
455
+ return;
456
+ if (hasContent(tab.config) && canvasApi) {
457
+ const tabName = tab.config.name ?? `${tab.config.type} tab`;
458
+ const onDownload = options.onDownloadTab ? () => options.onDownloadTab?.(tab.config) : null;
459
+ showCloseDialog(canvasApi.container, tabName, onDownload, () => api.closeTab(tab.id));
460
+ }
461
+ else {
462
+ api.closeTab(tab.id);
463
+ }
464
+ }
465
+ /**
466
+ * Rebuilds the tab bar from scratch.
467
+ * At most three group tabs are rendered (one per type: BPMN, DMN, Form).
468
+ */
469
+ function renderTabBar() {
470
+ if (!tabBar)
471
+ return;
472
+ tabBar.innerHTML = "";
473
+ for (const type of GROUP_TYPES) {
474
+ const group = tabs.filter((t) => t.config.type === type);
475
+ if (group.length === 0)
476
+ continue;
477
+ // Ensure groupActiveId[type] points to a valid tab in this group
478
+ if (!group.some((t) => t.id === groupActiveId.get(type))) {
479
+ const first = group[0];
480
+ if (first)
481
+ groupActiveId.set(type, first.id);
482
+ }
483
+ const isGroupActive = group.some((t) => t.id === activeId);
484
+ createGroupTabEl(type, group, isGroupActive);
485
+ }
486
+ // Center slot must be re-appended after innerHTML clear
487
+ if (centerSlotEl)
488
+ tabBar.appendChild(centerSlotEl);
489
+ updateRawModeBtn();
490
+ }
491
+ function createGroupTabEl(type, group, isGroupActive) {
492
+ const el = document.createElement("div");
493
+ el.className = `bpmnkit-tab${isGroupActive ? " active" : ""}`;
494
+ // Type badge
495
+ const typeBadge = document.createElement("span");
496
+ typeBadge.className = `bpmnkit-tab-type ${type}`;
497
+ typeBadge.textContent = type.toUpperCase();
498
+ el.appendChild(typeBadge);
499
+ // Active file name within this group
500
+ const activeTabId = groupActiveId.get(type);
501
+ const activeTab = group.find((t) => t.id === activeTabId) ?? group[0];
502
+ const nameEl = document.createElement("span");
503
+ nameEl.className = "bpmnkit-tab-name";
504
+ nameEl.textContent = activeTab?.config.name ?? type;
505
+ el.appendChild(nameEl);
506
+ // Warn indicator — shown if the group's active tab has a warning
507
+ if (activeTab?.hasWarning) {
508
+ const warnEl = document.createElement("span");
509
+ warnEl.className = "bpmnkit-tab-warn";
510
+ warnEl.textContent = "⚠";
511
+ warnEl.title = "Referenced file not found in registry";
512
+ el.appendChild(warnEl);
513
+ }
514
+ if (group.length > 1) {
515
+ // Chevron — opens the group dropdown listing all files of this type
516
+ const chevron = document.createElement("span");
517
+ chevron.className = "bpmnkit-tab-chevron";
518
+ chevron.innerHTML =
519
+ '<svg viewBox="0 0 10 6" fill="currentColor"><path d="M0 0l5 6 5-6z"/></svg>';
520
+ el.appendChild(chevron);
521
+ }
522
+ else if (!isProjectMode) {
523
+ // Close button — only when the group has a single file and not in project mode
524
+ const tab = group[0];
525
+ if (tab) {
526
+ const closeBtn = document.createElement("span");
527
+ closeBtn.className = "bpmnkit-tab-close";
528
+ closeBtn.textContent = "×";
529
+ closeBtn.addEventListener("click", (e) => {
530
+ e.stopPropagation();
531
+ requestClose(tab);
532
+ });
533
+ el.appendChild(closeBtn);
534
+ }
535
+ }
536
+ el.addEventListener("click", () => {
537
+ // Toggle dropdown first (while el is still in the DOM for correct positioning)
538
+ if (group.length > 1) {
539
+ toggleDropdown(type, group, el);
540
+ }
541
+ // Activate the group's last-selected file
542
+ const id = groupActiveId.get(type);
543
+ if (id)
544
+ api.setActiveTab(id);
545
+ });
546
+ tabBar?.appendChild(el);
547
+ }
548
+ // --- Tab content mounting ---
549
+ function mountTabContent(tab) {
550
+ const pane = document.createElement("div");
551
+ pane.className = "bpmnkit-tab-pane hidden";
552
+ contentArea?.appendChild(pane);
553
+ tab.pane = pane;
554
+ if (tab.config.type === "dmn") {
555
+ const editor = new DmnEditor({ container: pane, theme });
556
+ tab.dmnEditor = editor;
557
+ const xml = Dmn.export(tab.config.defs);
558
+ void editor.loadXML(xml);
559
+ tab.offDmnChange = editor.onChange(() => {
560
+ void (async () => {
561
+ const newXml = await editor.getXML();
562
+ if (tab.config.type === "dmn") {
563
+ try {
564
+ tab.config = { type: "dmn", defs: Dmn.parse(newXml), name: tab.config.name };
565
+ }
566
+ catch {
567
+ // keep old defs if parse fails
568
+ }
569
+ options.onTabChange?.(tab.id, tab.config);
570
+ }
571
+ })();
572
+ });
573
+ }
574
+ else if (tab.config.type === "form") {
575
+ const editor = new FormEditor({ container: pane, theme });
576
+ tab.formEditor = editor;
577
+ const schema = JSON.parse(Form.export(tab.config.form));
578
+ void editor.loadSchema(schema);
579
+ tab.offFormChange = editor.onChange(() => {
580
+ if (tab.config.type === "form") {
581
+ const newSchema = editor.getSchema();
582
+ try {
583
+ tab.config = {
584
+ type: "form",
585
+ form: Form.parse(JSON.stringify(newSchema)),
586
+ name: tab.config.name,
587
+ };
588
+ }
589
+ catch {
590
+ // keep old form if parse fails
591
+ }
592
+ options.onTabChange?.(tab.id, tab.config);
593
+ }
594
+ });
595
+ }
596
+ else if (tab.config.type === "feel") {
597
+ injectPlaygroundStyles();
598
+ pane.appendChild(buildFeelPlaygroundPanel(undefined, tab.config.expression));
599
+ }
600
+ // BPMN pane is intentionally empty and transparent — the main canvas SVG
601
+ // shows through. pointer-events are set to none in setActiveTab.
602
+ }
603
+ function applyThemeToTab(tab) {
604
+ tab.dmnEditor?.setTheme(theme);
605
+ tab.formEditor?.setTheme(theme);
606
+ }
607
+ function showWarning(tab, show) {
608
+ tab.hasWarning = show;
609
+ renderTabBar();
610
+ }
611
+ // --- BPMN process tracking ---
612
+ function trackBpmnProcesses(tabId, xml) {
613
+ try {
614
+ for (const proc of Bpmn.parse(xml).processes) {
615
+ bpmnProcessToTabId.set(proc.id, tabId);
616
+ bpmnProcessNames.set(proc.id, proc.name ?? proc.id);
617
+ }
618
+ }
619
+ catch {
620
+ // Malformed XML — tab still opens, process map unchanged
621
+ }
622
+ }
623
+ function untrackBpmnProcesses(tabId) {
624
+ for (const [procId, tid] of [...bpmnProcessToTabId]) {
625
+ if (tid === tabId) {
626
+ bpmnProcessToTabId.delete(procId);
627
+ bpmnProcessNames.delete(procId);
628
+ }
629
+ }
630
+ }
631
+ // --- Raw mode ---
632
+ function getTabSource(tab) {
633
+ if (tab.config.type === "bpmn")
634
+ return tab.config.xml;
635
+ if (tab.config.type === "dmn")
636
+ return Dmn.export(tab.config.defs);
637
+ if (tab.config.type === "form")
638
+ return Form.export(tab.config.form);
639
+ return "";
640
+ }
641
+ function updateRawModeBtn() {
642
+ if (!rawModeBtn)
643
+ return;
644
+ const activeTab = activeId !== null ? tabs.find((t) => t.id === activeId) : undefined;
645
+ rawModeBtn.disabled = activeTab === undefined || activeTab.config.type === "feel";
646
+ rawModeBtn.classList.toggle("active", isRawMode);
647
+ }
648
+ function updateRawPane() {
649
+ if (!rawPaneEl || !rawPreEl)
650
+ return;
651
+ const activeTab = activeId !== null ? tabs.find((t) => t.id === activeId) : undefined;
652
+ if (!isRawMode || !activeTab || activeTab.config.type === "feel") {
653
+ rawPaneEl.style.display = "none";
654
+ return;
655
+ }
656
+ rawPreEl.textContent = getTabSource(activeTab);
657
+ rawPaneEl.style.display = "";
658
+ }
659
+ // --- Public API ---
660
+ const api = {
661
+ openTab(config) {
662
+ const id = `tab_${++_tabCounter}`;
663
+ const tab = {
664
+ id,
665
+ config,
666
+ pane: null,
667
+ hasWarning: false,
668
+ };
669
+ tabs.push(tab);
670
+ groupActiveId.set(config.type, id);
671
+ hideWelcomeScreen();
672
+ if (contentArea)
673
+ mountTabContent(tab);
674
+ // Track BPMN processes for navigation and the config panel picker
675
+ if (config.type === "bpmn") {
676
+ trackBpmnProcesses(id, config.xml);
677
+ }
678
+ renderTabBar();
679
+ api.setActiveTab(id);
680
+ return id;
681
+ },
682
+ closeTab(id) {
683
+ const idx = tabs.findIndex((t) => t.id === id);
684
+ if (idx === -1)
685
+ return;
686
+ const [tab] = tabs.splice(idx, 1);
687
+ if (!tab)
688
+ return;
689
+ tab.offDmnChange?.();
690
+ tab.offFormChange?.();
691
+ tab.dmnEditor?.destroy();
692
+ tab.formEditor?.destroy();
693
+ tab.pane.remove();
694
+ if (tab.config.type === "bpmn") {
695
+ untrackBpmnProcesses(id);
696
+ }
697
+ if (activeId === id) {
698
+ const next = tabs[idx] ?? tabs[idx - 1];
699
+ activeId = null;
700
+ if (next) {
701
+ api.setActiveTab(next.id);
702
+ }
703
+ else {
704
+ // All tabs closed — restore pointer-events and show welcome screen
705
+ if (contentArea)
706
+ contentArea.style.pointerEvents = "";
707
+ showWelcomeScreen();
708
+ }
709
+ }
710
+ renderTabBar();
711
+ },
712
+ closeAllTabs() {
713
+ bpmnProcessToTabId.clear();
714
+ bpmnProcessNames.clear();
715
+ isRawMode = false;
716
+ for (const tab of [...tabs]) {
717
+ tab.offDmnChange?.();
718
+ tab.offFormChange?.();
719
+ tab.dmnEditor?.destroy();
720
+ tab.formEditor?.destroy();
721
+ tab.pane.remove();
722
+ }
723
+ tabs.length = 0;
724
+ activeId = null;
725
+ groupActiveId.clear();
726
+ if (contentArea)
727
+ contentArea.style.pointerEvents = "";
728
+ if (rawPaneEl)
729
+ rawPaneEl.style.display = "none";
730
+ renderTabBar();
731
+ showWelcomeScreen();
732
+ },
733
+ setActiveTab(id) {
734
+ const tab = tabs.find((t) => t.id === id);
735
+ if (!tab)
736
+ return;
737
+ // Deactivate current
738
+ if (activeId) {
739
+ const prev = tabs.find((t) => t.id === activeId);
740
+ if (prev)
741
+ prev.pane.classList.add("hidden");
742
+ }
743
+ // Activate new
744
+ activeId = id;
745
+ tab.pane.classList.remove("hidden");
746
+ groupActiveId.set(tab.config.type, id);
747
+ // BPMN panes are transparent; disable pointer-events so the canvas is interactive.
748
+ // In raw mode the raw pane overlays everything so pointer-events must stay on.
749
+ if (contentArea) {
750
+ contentArea.style.pointerEvents = tab.config.type === "bpmn" && !isRawMode ? "none" : "";
751
+ }
752
+ renderTabBar();
753
+ options.onTabActivate?.(id, tab.config);
754
+ updateRawPane();
755
+ },
756
+ getActiveTabId() {
757
+ return activeId;
758
+ },
759
+ getTabIds() {
760
+ return tabs.map((t) => t.id);
761
+ },
762
+ navigateToProcess(processId) {
763
+ const tabId = bpmnProcessToTabId.get(processId);
764
+ if (tabId && tabs.some((t) => t.id === tabId)) {
765
+ api.setActiveTab(tabId);
766
+ }
767
+ },
768
+ getAvailableProcesses() {
769
+ return [...bpmnProcessToTabId.keys()].map((id) => ({
770
+ id,
771
+ name: bpmnProcessNames.get(id),
772
+ }));
773
+ },
774
+ getAvailableDecisions() {
775
+ const result = [];
776
+ for (const tab of tabs) {
777
+ if (tab.config.type !== "dmn")
778
+ continue;
779
+ for (const decision of tab.config.defs.decisions) {
780
+ result.push({ id: decision.id, name: decision.name ?? undefined });
781
+ }
782
+ }
783
+ return result;
784
+ },
785
+ getAvailableForms() {
786
+ const result = [];
787
+ for (const tab of tabs) {
788
+ if (tab.config.type !== "form")
789
+ continue;
790
+ const formId = tab.config.form.id;
791
+ if (formId)
792
+ result.push({ id: formId, name: tab.config.name });
793
+ }
794
+ return result;
795
+ },
796
+ getAllTabContent() {
797
+ const result = [];
798
+ for (const tab of tabs) {
799
+ if (tab.config.type === "bpmn") {
800
+ result.push({
801
+ tabId: tab.id,
802
+ name: tab.config.name ?? "Diagram",
803
+ type: "bpmn",
804
+ content: tab.config.xml,
805
+ });
806
+ }
807
+ else if (tab.config.type === "dmn") {
808
+ result.push({
809
+ tabId: tab.id,
810
+ name: tab.config.name ?? "Decision",
811
+ type: "dmn",
812
+ content: Dmn.export(tab.config.defs),
813
+ });
814
+ }
815
+ else if (tab.config.type === "form") {
816
+ result.push({
817
+ tabId: tab.id,
818
+ name: tab.config.name ?? "Form",
819
+ type: "form",
820
+ content: Form.export(tab.config.form),
821
+ });
822
+ }
823
+ // FEEL tabs have no file content — skipped
824
+ }
825
+ return result;
826
+ },
827
+ refreshWelcomeScreen() {
828
+ renderRecentProjects();
829
+ renderDynamicSections();
830
+ },
831
+ setProjectMode(enabled) {
832
+ isProjectMode = enabled;
833
+ renderTabBar();
834
+ },
835
+ renameTab(id, name) {
836
+ const tab = tabs.find((t) => t.id === id);
837
+ if (!tab)
838
+ return;
839
+ tab.config = { ...tab.config, name };
840
+ renderTabBar();
841
+ },
842
+ get rawModeButton() {
843
+ return rawModeBtn;
844
+ },
845
+ openFilePicker() {
846
+ fileInputEl?.click();
847
+ },
848
+ setPlayMode(enabled) {
849
+ if (tabBar === null)
850
+ return;
851
+ if (enabled) {
852
+ tabBar.classList.add("bpmnkit-play-mode");
853
+ }
854
+ else {
855
+ tabBar.classList.remove("bpmnkit-play-mode");
856
+ }
857
+ },
858
+ openDecision(decisionId) {
859
+ // Check if already open
860
+ const existing = tabs.find((t) => t.config.type === "dmn" && t.config.defs.decisions.some((d) => d.id === decisionId));
861
+ if (existing) {
862
+ api.setActiveTab(existing.id);
863
+ return;
864
+ }
865
+ const defs = resolver?.resolveDmn(decisionId) ?? null;
866
+ if (!defs) {
867
+ // Open a tab with a warning
868
+ const id = api.openTab({
869
+ type: "dmn",
870
+ defs: {
871
+ id: decisionId,
872
+ name: decisionId,
873
+ namespace: "",
874
+ namespaces: {},
875
+ modelerAttributes: {},
876
+ decisions: [],
877
+ inputData: [],
878
+ knowledgeSources: [],
879
+ businessKnowledgeModels: [],
880
+ textAnnotations: [],
881
+ associations: [],
882
+ },
883
+ name: decisionId,
884
+ });
885
+ const tab = tabs.find((t) => t.id === id);
886
+ if (tab)
887
+ showWarning(tab, true);
888
+ return;
889
+ }
890
+ const decision = defs.decisions.find((d) => d.id === decisionId);
891
+ api.openTab({ type: "dmn", defs, name: decision?.name ?? decisionId });
892
+ },
893
+ openForm(formId) {
894
+ // Check if already open
895
+ const existing = tabs.find((t) => t.config.type === "form" && t.config.form.id === formId);
896
+ if (existing) {
897
+ api.setActiveTab(existing.id);
898
+ return;
899
+ }
900
+ const form = resolver?.resolveForm(formId) ?? null;
901
+ if (!form) {
902
+ const id = api.openTab({
903
+ type: "form",
904
+ form: { id: formId, type: "default", components: [] },
905
+ name: formId,
906
+ });
907
+ const tab = tabs.find((t) => t.id === id);
908
+ if (tab)
909
+ showWarning(tab, true);
910
+ return;
911
+ }
912
+ api.openTab({ type: "form", form, name: form.id });
913
+ },
914
+ };
915
+ // --- CanvasPlugin ---
916
+ return {
917
+ name: "tabs",
918
+ api,
919
+ install(cApi) {
920
+ canvasApi = cApi;
921
+ injectTabsStyles();
922
+ // Detect theme from canvas (canvas sets data-theme="dark" for dark, removes it for light)
923
+ const container = cApi.container;
924
+ theme = container.dataset.theme === "dark" ? "dark" : "light";
925
+ // Expand container to be position:relative for absolute children
926
+ if (getComputedStyle(container).position === "static") {
927
+ container.style.position = "relative";
928
+ }
929
+ // Create tab bar
930
+ tabBar = document.createElement("div");
931
+ tabBar.className = "bpmnkit-tabs";
932
+ tabBar.dataset.theme = theme;
933
+ container.appendChild(tabBar);
934
+ if (options.centerSlot) {
935
+ centerSlotEl = document.createElement("div");
936
+ centerSlotEl.className = "bpmnkit-tabs-center";
937
+ centerSlotEl.appendChild(options.centerSlot);
938
+ tabBar.appendChild(centerSlotEl);
939
+ }
940
+ // Create content area (below tab bar)
941
+ contentArea = document.createElement("div");
942
+ contentArea.className = "bpmnkit-tab-content";
943
+ container.appendChild(contentArea);
944
+ // Create welcome screen (shown until the first tab opens)
945
+ welcomeEl = createWelcomeEl();
946
+ contentArea.appendChild(welcomeEl);
947
+ showWelcomeScreen();
948
+ // Create raw source pane (overlaid on top of content when raw mode is on)
949
+ rawPaneEl = document.createElement("div");
950
+ rawPaneEl.className = "bpmnkit-raw-pane";
951
+ rawPaneEl.dataset.theme = theme;
952
+ rawPaneEl.style.display = "none";
953
+ const rawCopyBtn = document.createElement("button");
954
+ rawCopyBtn.type = "button";
955
+ rawCopyBtn.className = "bpmnkit-raw-copy-btn";
956
+ rawCopyBtn.textContent = "Copy";
957
+ rawCopyBtn.addEventListener("click", () => {
958
+ const text = rawPreEl?.textContent ?? "";
959
+ void navigator.clipboard.writeText(text).then(() => {
960
+ rawCopyBtn.textContent = "Copied!";
961
+ setTimeout(() => {
962
+ rawCopyBtn.textContent = "Copy";
963
+ }, 1500);
964
+ });
965
+ });
966
+ rawPaneEl.appendChild(rawCopyBtn);
967
+ rawPreEl = document.createElement("pre");
968
+ rawPreEl.className = "bpmnkit-raw-content";
969
+ rawPaneEl.appendChild(rawPreEl);
970
+ contentArea.appendChild(rawPaneEl);
971
+ // Raw mode toggle button — exposed via api.rawModeButton; caller places it in the HUD
972
+ rawModeBtn = document.createElement("button");
973
+ rawModeBtn.type = "button";
974
+ rawModeBtn.title = "Toggle raw source";
975
+ rawModeBtn.disabled = true;
976
+ rawModeBtn.innerHTML = RAW_MODE_ICON;
977
+ rawModeBtn.addEventListener("click", () => {
978
+ isRawMode = !isRawMode;
979
+ const activeTab = activeId !== null ? tabs.find((t) => t.id === activeId) : undefined;
980
+ if (contentArea && activeTab?.config.type === "bpmn") {
981
+ contentArea.style.pointerEvents = isRawMode ? "" : "none";
982
+ }
983
+ updateRawPane();
984
+ updateRawModeBtn();
985
+ options.onRawModeChange?.(isRawMode);
986
+ });
987
+ // Create body-level dropdown for groups with multiple files
988
+ dropdownEl = document.createElement("div");
989
+ dropdownEl.className = "bpmnkit-tab-dropdown";
990
+ dropdownEl.dataset.theme = theme;
991
+ document.body.appendChild(dropdownEl);
992
+ // Close dropdown when clicking outside the tab bar or dropdown
993
+ outsideClickHandler = (e) => {
994
+ if (dropdownEl &&
995
+ !dropdownEl.contains(e.target) &&
996
+ !tabBar?.contains(e.target)) {
997
+ closeDropdownEl();
998
+ }
999
+ };
1000
+ document.addEventListener("pointerdown", outsideClickHandler);
1001
+ // File import — hidden input + drag-and-drop
1002
+ if (options.enableFileImport) {
1003
+ const fileInput = document.createElement("input");
1004
+ fileInput.type = "file";
1005
+ fileInput.multiple = true;
1006
+ fileInput.accept = ".bpmn,.xml,.dmn,.form,.json";
1007
+ fileInput.style.display = "none";
1008
+ document.body.appendChild(fileInput);
1009
+ fileInput.addEventListener("change", () => {
1010
+ if (fileInput.files) {
1011
+ void importFiles(fileInput.files);
1012
+ fileInput.value = "";
1013
+ }
1014
+ });
1015
+ fileInputEl = fileInput;
1016
+ dndDragoverHandler = (e) => {
1017
+ e.preventDefault();
1018
+ };
1019
+ dndDropHandler = (e) => {
1020
+ e.preventDefault();
1021
+ const files = e.dataTransfer?.files;
1022
+ if (files && files.length > 0)
1023
+ void importFiles(files);
1024
+ };
1025
+ container.addEventListener("dragover", dndDragoverHandler);
1026
+ container.addEventListener("drop", dndDropHandler);
1027
+ }
1028
+ // Subscribe to diagram:change and diagram:load to keep BPMN tab XML in sync.
1029
+ // diagram:load fires when XML is programmatically loaded (e.g. AI apply, history restore).
1030
+ // diagram:change fires on every user edit.
1031
+ const anyOn = cApi.on.bind(cApi);
1032
+ function onDiagramUpdate(rawDefs) {
1033
+ if (!activeId)
1034
+ return;
1035
+ const activeTab = tabs.find((t) => t.id === activeId);
1036
+ if (!activeTab || activeTab.config.type !== "bpmn")
1037
+ return;
1038
+ const defs = rawDefs;
1039
+ const xml = Bpmn.export(defs);
1040
+ // Update stored XML so onTabActivate always receives the latest content
1041
+ activeTab.config = { ...activeTab.config, xml };
1042
+ // Refresh process map for this tab
1043
+ untrackBpmnProcesses(activeId);
1044
+ trackBpmnProcesses(activeId, xml);
1045
+ // Keep raw pane in sync
1046
+ if (isRawMode)
1047
+ updateRawPane();
1048
+ }
1049
+ offDiagramChange = anyOn("diagram:change", onDiagramUpdate);
1050
+ anyOn("diagram:load", onDiagramUpdate);
1051
+ // Listen for theme changes (canvas toggles data-theme="dark"; absence means light)
1052
+ const observer = new MutationObserver(() => {
1053
+ const t = container.dataset.theme;
1054
+ theme = t === "dark" ? "dark" : "light";
1055
+ if (tabBar)
1056
+ tabBar.dataset.theme = theme;
1057
+ if (welcomeEl)
1058
+ welcomeEl.dataset.theme = theme;
1059
+ if (dropdownEl)
1060
+ dropdownEl.dataset.theme = theme;
1061
+ if (rawPaneEl)
1062
+ rawPaneEl.dataset.theme = theme;
1063
+ for (const tab of tabs)
1064
+ applyThemeToTab(tab);
1065
+ });
1066
+ observer.observe(container, { attributes: true, attributeFilter: ["data-theme"] });
1067
+ },
1068
+ uninstall() {
1069
+ offDiagramChange?.();
1070
+ offDiagramChange = undefined;
1071
+ if (outsideClickHandler) {
1072
+ document.removeEventListener("pointerdown", outsideClickHandler);
1073
+ outsideClickHandler = null;
1074
+ }
1075
+ if (fileInputEl) {
1076
+ fileInputEl.remove();
1077
+ fileInputEl = null;
1078
+ }
1079
+ if (canvasApi && dndDragoverHandler) {
1080
+ canvasApi.container.removeEventListener("dragover", dndDragoverHandler);
1081
+ dndDragoverHandler = null;
1082
+ }
1083
+ if (canvasApi && dndDropHandler) {
1084
+ canvasApi.container.removeEventListener("drop", dndDropHandler);
1085
+ dndDropHandler = null;
1086
+ }
1087
+ dropdownEl?.remove();
1088
+ dropdownEl = null;
1089
+ welcomeEl?.remove();
1090
+ welcomeEl = null;
1091
+ dynamicSectionsEl = null;
1092
+ recentBtnEl = null;
1093
+ recentListEl = null;
1094
+ isRawMode = false;
1095
+ rawPaneEl = null;
1096
+ rawPreEl = null;
1097
+ rawModeBtn = null;
1098
+ bpmnProcessToTabId.clear();
1099
+ bpmnProcessNames.clear();
1100
+ for (const tab of tabs) {
1101
+ tab.offDmnChange?.();
1102
+ tab.offFormChange?.();
1103
+ tab.dmnEditor?.destroy();
1104
+ tab.formEditor?.destroy();
1105
+ }
1106
+ tabs.length = 0;
1107
+ activeId = null;
1108
+ groupActiveId.clear();
1109
+ openDropdownType = null;
1110
+ tabBar?.remove();
1111
+ contentArea?.remove();
1112
+ tabBar = null;
1113
+ contentArea = null;
1114
+ centerSlotEl = null;
1115
+ canvasApi = null;
1116
+ },
1117
+ };
1118
+ }
1119
+ //# sourceMappingURL=tabs-plugin.js.map