@bpmnkit/plugins 0.0.18 → 0.0.23

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.
@@ -0,0 +1,680 @@
1
+ // ── SVG helpers ───────────────────────────────────────────────────────────────
2
+ const NS = "http://www.w3.org/2000/svg";
3
+ function svgEl(tag) {
4
+ return document.createElementNS(NS, tag);
5
+ }
6
+ function attrs(el, map) {
7
+ for (const [k, v] of Object.entries(map))
8
+ el.setAttribute(k, String(v));
9
+ }
10
+ // ── CSS ───────────────────────────────────────────────────────────────────────
11
+ const CSS_ID = "bpmnkit-presentation-v1";
12
+ const CSS = `
13
+ .bpmnkit-pres-fullscreen {
14
+ position: fixed !important;
15
+ inset: 0 !important;
16
+ z-index: 9000 !important;
17
+ }
18
+ .bpmnkit-pres-fullscreen #hud-top-center,
19
+ .bpmnkit-pres-fullscreen #hud-bottom-center {
20
+ display: none !important;
21
+ }
22
+ .bpmnkit-pres-overlay {
23
+ position: absolute; inset: 0; pointer-events: none; z-index: 500;
24
+ user-select: none;
25
+ font-family: var(--bpmnkit-font, system-ui, -apple-system, sans-serif);
26
+ }
27
+ .bpmnkit-pres-progress {
28
+ position: absolute; top: 0; left: 0; right: 0; height: 3px;
29
+ background: rgba(128,128,128,0.18);
30
+ }
31
+ .bpmnkit-pres-progress-fill {
32
+ height: 100%; background: var(--bpmnkit-accent, #1a56db);
33
+ transition: width 0.35s ease;
34
+ }
35
+ .bpmnkit-pres-stat {
36
+ position: absolute; top: 8px; left: 50%; transform: translateX(-50%);
37
+ font-size: 11px; color: var(--bpmnkit-fg-muted, #6666a0);
38
+ background: var(--bpmnkit-surface, #fff);
39
+ border: 1px solid var(--bpmnkit-border, #d0d0e8);
40
+ border-radius: 4px; padding: 2px 10px;
41
+ box-shadow: 0 1px 4px rgba(0,0,0,0.10);
42
+ white-space: nowrap;
43
+ }
44
+ .bpmnkit-pres-minimap {
45
+ position: absolute; bottom: 12px; right: 12px;
46
+ width: 160px; height: 100px;
47
+ pointer-events: auto;
48
+ background: var(--bpmnkit-surface, #fff);
49
+ border: 1px solid var(--bpmnkit-border, #d0d0e8);
50
+ border-radius: 6px; overflow: hidden;
51
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
52
+ cursor: crosshair;
53
+ }
54
+ .bpmnkit-pres-minimap > svg {
55
+ display: block; width: 100%; height: 100%; pointer-events: none;
56
+ }
57
+ .bpmnkit-pres-hints {
58
+ position: absolute; bottom: 12px; left: 12px;
59
+ display: flex; flex-direction: column; gap: 4px;
60
+ }
61
+ .bpmnkit-pres-hint {
62
+ display: inline-flex; align-items: center; gap: 5px;
63
+ background: var(--bpmnkit-surface, #fff);
64
+ border: 1px solid var(--bpmnkit-border, #d0d0e8);
65
+ border-radius: 4px; padding: 3px 8px;
66
+ font-size: 11px; color: var(--bpmnkit-fg-muted, #6666a0);
67
+ box-shadow: 0 1px 4px rgba(0,0,0,0.08);
68
+ }
69
+ .bpmnkit-pres-hint kbd {
70
+ font-family: var(--bpmnkit-font-mono, monospace);
71
+ font-size: 10px; color: var(--bpmnkit-fg, #1a1a2e);
72
+ background: var(--bpmnkit-surface-2, #eeeef8);
73
+ border: 1px solid var(--bpmnkit-border, #d0d0e8);
74
+ border-radius: 3px; padding: 1px 5px;
75
+ }
76
+ `;
77
+ function injectPresStyles() {
78
+ if (typeof document === "undefined")
79
+ return;
80
+ if (document.getElementById(CSS_ID))
81
+ return;
82
+ const style = document.createElement("style");
83
+ style.id = CSS_ID;
84
+ style.textContent = CSS;
85
+ document.head.appendChild(style);
86
+ }
87
+ // ── Flow graph builder ─────────────────────────────────────────────────────────
88
+ function buildGraph(rawDefs) {
89
+ const graph = new Map();
90
+ const startIds = [];
91
+ const defs = rawDefs;
92
+ function walk(elements, flows) {
93
+ for (const el of elements) {
94
+ if (!graph.has(el.id))
95
+ graph.set(el.id, []);
96
+ if (el.type === "startEvent")
97
+ startIds.push(el.id);
98
+ if (el.flowElements)
99
+ walk(el.flowElements, el.sequenceFlows ?? []);
100
+ }
101
+ for (const f of flows) {
102
+ const list = graph.get(f.sourceRef);
103
+ if (list)
104
+ list.push({
105
+ flowId: f.id,
106
+ targetId: f.targetRef,
107
+ label: f.name ?? f.conditionExpression,
108
+ });
109
+ }
110
+ }
111
+ for (const proc of defs.processes)
112
+ walk(proc.flowElements, proc.sequenceFlows);
113
+ // BFS to count reachable nodes (for progress indicator)
114
+ const seen = new Set();
115
+ const q = [...startIds];
116
+ while (q.length > 0) {
117
+ const id = q.shift();
118
+ if (!id || seen.has(id))
119
+ continue;
120
+ seen.add(id);
121
+ for (const c of graph.get(id) ?? []) {
122
+ if (!seen.has(c.targetId))
123
+ q.push(c.targetId);
124
+ }
125
+ }
126
+ return { graph, startIds, reachableCount: seen.size };
127
+ }
128
+ // ── PresentationMinimap ────────────────────────────────────────────────────────
129
+ /**
130
+ * A 160×100 minimap for the presentation overlay. Uses `CanvasApi.getShapes()`
131
+ * and `CanvasApi.getEdges()` so it stays decoupled from BpmnDefinitions internals.
132
+ */
133
+ class PresentationMinimap {
134
+ host;
135
+ svg;
136
+ edgesG;
137
+ shapesG;
138
+ vpRect;
139
+ mmScale = 1;
140
+ mmOffX = 0;
141
+ mmOffY = 0;
142
+ mmW = 160;
143
+ mmH = 100;
144
+ /** id → minimap shape element for fast colour updates */
145
+ shapeMap = new Map();
146
+ constructor(container, onNavigate) {
147
+ this.host = document.createElement("div");
148
+ this.host.className = "bpmnkit-pres-minimap";
149
+ this.host.setAttribute("aria-hidden", "true");
150
+ this.svg = svgEl("svg");
151
+ attrs(this.svg, {
152
+ viewBox: `0 0 ${this.mmW} ${this.mmH}`,
153
+ preserveAspectRatio: "none",
154
+ });
155
+ this.host.appendChild(this.svg);
156
+ this.edgesG = svgEl("g");
157
+ this.shapesG = svgEl("g");
158
+ this.vpRect = svgEl("rect");
159
+ attrs(this.vpRect, {
160
+ fill: "none",
161
+ stroke: "var(--bpmnkit-accent,#1a56db)",
162
+ "stroke-width": "1.5",
163
+ rx: "1",
164
+ opacity: "0.7",
165
+ });
166
+ this.svg.appendChild(this.edgesG);
167
+ this.svg.appendChild(this.shapesG);
168
+ this.svg.appendChild(this.vpRect);
169
+ container.appendChild(this.host);
170
+ this.host.addEventListener("click", (e) => {
171
+ const r = this.host.getBoundingClientRect();
172
+ onNavigate((e.clientX - r.left - this.mmOffX) / this.mmScale, (e.clientY - r.top - this.mmOffY) / this.mmScale);
173
+ });
174
+ }
175
+ update(api) {
176
+ this.edgesG.innerHTML = "";
177
+ this.shapesG.innerHTML = "";
178
+ this.shapeMap.clear();
179
+ const shapes = api.getShapes();
180
+ const edges = api.getEdges();
181
+ if (shapes.length === 0)
182
+ return;
183
+ // Compute diagram bounding box from shapes
184
+ let minX = Number.POSITIVE_INFINITY;
185
+ let minY = Number.POSITIVE_INFINITY;
186
+ let maxX = Number.NEGATIVE_INFINITY;
187
+ let maxY = Number.NEGATIVE_INFINITY;
188
+ for (const s of shapes) {
189
+ const b = s.shape.bounds;
190
+ if (b.x < minX)
191
+ minX = b.x;
192
+ if (b.y < minY)
193
+ minY = b.y;
194
+ if (b.x + b.width > maxX)
195
+ maxX = b.x + b.width;
196
+ if (b.y + b.height > maxY)
197
+ maxY = b.y + b.height;
198
+ }
199
+ // Expand with edge waypoints
200
+ for (const e of edges) {
201
+ const wps = e.edge.waypoints ?? [];
202
+ for (const wp of wps) {
203
+ if (wp.x < minX)
204
+ minX = wp.x;
205
+ if (wp.y < minY)
206
+ minY = wp.y;
207
+ if (wp.x > maxX)
208
+ maxX = wp.x;
209
+ if (wp.y > maxY)
210
+ maxY = wp.y;
211
+ }
212
+ }
213
+ const pad = 8;
214
+ const dW = maxX - minX;
215
+ const dH = maxY - minY;
216
+ this.mmScale = Math.min((this.mmW - pad * 2) / dW, (this.mmH - pad * 2) / dH);
217
+ this.mmOffX = pad + (this.mmW - pad * 2 - dW * this.mmScale) / 2 - minX * this.mmScale;
218
+ this.mmOffY = pad + (this.mmH - pad * 2 - dH * this.mmScale) / 2 - minY * this.mmScale;
219
+ // Render edges
220
+ for (const e of edges) {
221
+ const wps = e.edge.waypoints ?? [];
222
+ if (wps.length < 2)
223
+ continue;
224
+ const pts = wps
225
+ .map((wp) => `${wp.x * this.mmScale + this.mmOffX},${wp.y * this.mmScale + this.mmOffY}`)
226
+ .join(" ");
227
+ const poly = svgEl("polyline");
228
+ attrs(poly, {
229
+ points: pts,
230
+ stroke: "var(--bpmnkit-shape-stroke,#404040)",
231
+ "stroke-width": "0.5",
232
+ fill: "none",
233
+ opacity: "0.35",
234
+ });
235
+ this.edgesG.appendChild(poly);
236
+ }
237
+ // Render shapes
238
+ for (const s of shapes) {
239
+ const b = s.shape.bounds;
240
+ const x = b.x * this.mmScale + this.mmOffX;
241
+ const y = b.y * this.mmScale + this.mmOffY;
242
+ const w = b.width * this.mmScale;
243
+ const h = b.height * this.mmScale;
244
+ let el;
245
+ if (w < 10) {
246
+ el = svgEl("circle");
247
+ attrs(el, {
248
+ cx: x + w / 2,
249
+ cy: y + h / 2,
250
+ r: Math.max(w / 2, 2),
251
+ fill: "var(--bpmnkit-shape-stroke,#404040)",
252
+ opacity: "0.45",
253
+ });
254
+ }
255
+ else {
256
+ el = svgEl("rect");
257
+ attrs(el, {
258
+ x,
259
+ y,
260
+ width: Math.max(w, 1),
261
+ height: Math.max(h, 1),
262
+ rx: 1,
263
+ fill: "var(--bpmnkit-shape-stroke,#404040)",
264
+ opacity: "0.45",
265
+ });
266
+ }
267
+ this.shapeMap.set(s.id, el);
268
+ this.shapesG.appendChild(el);
269
+ }
270
+ }
271
+ highlight(currentId, visited) {
272
+ for (const [id, el] of this.shapeMap) {
273
+ if (id === currentId) {
274
+ el.setAttribute("fill", "var(--bpmnkit-accent,#1a56db)");
275
+ el.setAttribute("opacity", "0.9");
276
+ }
277
+ else if (visited.has(id)) {
278
+ el.setAttribute("fill", "var(--bpmnkit-success,#16a34a)");
279
+ el.setAttribute("opacity", "0.6");
280
+ }
281
+ else {
282
+ el.setAttribute("fill", "var(--bpmnkit-shape-stroke,#404040)");
283
+ el.setAttribute("opacity", "0.45");
284
+ }
285
+ }
286
+ }
287
+ syncViewport(state, svgW, svgH) {
288
+ const left = -state.tx / state.scale;
289
+ const top = -state.ty / state.scale;
290
+ const w = svgW / state.scale;
291
+ const h = svgH / state.scale;
292
+ attrs(this.vpRect, {
293
+ x: left * this.mmScale + this.mmOffX,
294
+ y: top * this.mmScale + this.mmOffY,
295
+ width: Math.max(w * this.mmScale, 2),
296
+ height: Math.max(h * this.mmScale, 2),
297
+ });
298
+ }
299
+ destroy() {
300
+ this.host.remove();
301
+ }
302
+ }
303
+ // ── PresentationMode ──────────────────────────────────────────────────────────
304
+ class PresentationMode {
305
+ canvasApi;
306
+ onEnter;
307
+ onExit;
308
+ isActive = false;
309
+ graph = new Map();
310
+ startIds = [];
311
+ reachableCount = 0;
312
+ currentId = null;
313
+ visited = new Set();
314
+ // Overlay DOM
315
+ overlay = null;
316
+ progressFill = null;
317
+ statEl = null;
318
+ hintsEl = null;
319
+ minimap = null;
320
+ // SVG layers (inside viewportEl, use diagram coordinates)
321
+ badgeG = null;
322
+ // Navigation history for back navigation
323
+ history = [];
324
+ // Cleanup handles
325
+ offViewport = null;
326
+ keyHandler = null;
327
+ constructor(api, opts) {
328
+ this.canvasApi = api;
329
+ this.onEnter = opts?.onEnter;
330
+ this.onExit = opts?.onExit;
331
+ }
332
+ setDefs(defs) {
333
+ const result = buildGraph(defs);
334
+ this.graph = result.graph;
335
+ this.startIds = result.startIds;
336
+ this.reachableCount = result.reachableCount;
337
+ }
338
+ enter() {
339
+ if (this.isActive)
340
+ return;
341
+ if (this.canvasApi.getShapes().length === 0)
342
+ return;
343
+ this.isActive = true;
344
+ this.onEnter?.();
345
+ this.visited.clear();
346
+ this.history = [];
347
+ this.currentId = null;
348
+ injectPresStyles();
349
+ this.canvasApi.container.classList.add("bpmnkit-pres-fullscreen");
350
+ document.body.style.overflow = "hidden";
351
+ this.buildOverlay();
352
+ this.buildBadgeLayer();
353
+ this.setupKeyboard();
354
+ // Navigate to the first start event (or first shape as fallback)
355
+ const first = this.startIds[0] ?? this.canvasApi.getShapes()[0]?.id;
356
+ if (first)
357
+ this.navigateTo(first);
358
+ }
359
+ exit() {
360
+ if (!this.isActive)
361
+ return;
362
+ this.isActive = false;
363
+ if (this.keyHandler) {
364
+ window.removeEventListener("keydown", this.keyHandler, true);
365
+ this.keyHandler = null;
366
+ }
367
+ this.offViewport?.();
368
+ this.offViewport = null;
369
+ this.badgeG?.remove();
370
+ this.badgeG = null;
371
+ this.overlay?.remove();
372
+ this.overlay = null;
373
+ this.progressFill = null;
374
+ this.statEl = null;
375
+ this.hintsEl = null;
376
+ this.minimap?.destroy();
377
+ this.minimap = null;
378
+ // Remove smooth-transition style we injected
379
+ this.canvasApi.viewportEl.style.transition = "";
380
+ this.canvasApi.container.classList.remove("bpmnkit-pres-fullscreen");
381
+ document.body.style.overflow = "";
382
+ this.onExit?.();
383
+ this.visited.clear();
384
+ this.history = [];
385
+ this.currentId = null;
386
+ }
387
+ // ── Build overlay ───────────────────────────────────────────────────────────
388
+ buildOverlay() {
389
+ const ov = document.createElement("div");
390
+ ov.className = "bpmnkit-pres-overlay";
391
+ // Progress bar
392
+ const pb = document.createElement("div");
393
+ pb.className = "bpmnkit-pres-progress";
394
+ const fill = document.createElement("div");
395
+ fill.className = "bpmnkit-pres-progress-fill";
396
+ fill.style.width = "0%";
397
+ pb.appendChild(fill);
398
+ this.progressFill = fill;
399
+ // Progress label (centre-top)
400
+ const stat = document.createElement("div");
401
+ stat.className = "bpmnkit-pres-stat";
402
+ stat.textContent = "0 / 0";
403
+ this.statEl = stat;
404
+ // Minimap
405
+ const mm = new PresentationMinimap(ov, (diagX, diagY) => {
406
+ const { scale } = this.canvasApi.getViewport();
407
+ this.canvasApi.setViewport({
408
+ tx: this.canvasApi.svg.clientWidth / 2 - diagX * scale,
409
+ ty: this.canvasApi.svg.clientHeight / 2 - diagY * scale,
410
+ });
411
+ });
412
+ mm.update(this.canvasApi);
413
+ mm.syncViewport(this.canvasApi.getViewport(), this.canvasApi.svg.clientWidth, this.canvasApi.svg.clientHeight);
414
+ this.minimap = mm;
415
+ // Keyboard hints
416
+ const hints = document.createElement("div");
417
+ hints.className = "bpmnkit-pres-hints";
418
+ this.hintsEl = hints;
419
+ ov.appendChild(pb);
420
+ ov.appendChild(stat);
421
+ ov.appendChild(hints);
422
+ this.canvasApi.container.appendChild(ov);
423
+ this.overlay = ov;
424
+ // Keep minimap viewport indicator in sync
425
+ this.offViewport = this.canvasApi.on("viewport:change", (state) => {
426
+ this.minimap?.syncViewport(state, this.canvasApi.svg.clientWidth, this.canvasApi.svg.clientHeight);
427
+ });
428
+ }
429
+ // ── Build SVG badge layer (diagram coordinate space) ────────────────────────
430
+ buildBadgeLayer() {
431
+ const bg = svgEl("g");
432
+ bg.style.pointerEvents = "none";
433
+ this.canvasApi.viewportEl.appendChild(bg);
434
+ this.badgeG = bg;
435
+ }
436
+ // ── Navigation ──────────────────────────────────────────────────────────────
437
+ navigateTo(id, addToHistory = true) {
438
+ if (!this.isActive)
439
+ return;
440
+ const shape = this.canvasApi.getShapes().find((s) => s.id === id);
441
+ if (!shape)
442
+ return;
443
+ if (addToHistory && this.currentId) {
444
+ this.history.push(this.currentId);
445
+ }
446
+ this.visited.add(id);
447
+ this.currentId = id;
448
+ this.centerOn(shape.shape.bounds);
449
+ this.updateBadges(id);
450
+ this.updateProgress();
451
+ this.minimap?.highlight(id, this.visited);
452
+ this.updateHints(id);
453
+ }
454
+ navigateBack() {
455
+ const prev = this.history.pop();
456
+ if (prev)
457
+ this.navigateTo(prev, false);
458
+ }
459
+ centerOn(bounds) {
460
+ const sw = this.canvasApi.svg.clientWidth;
461
+ const sh = this.canvasApi.svg.clientHeight;
462
+ const scale = Math.min((sw * 0.45) / Math.max(bounds.width, 1), (sh * 0.45) / Math.max(bounds.height, 1), 2.5);
463
+ const cx = bounds.x + bounds.width / 2;
464
+ const cy = bounds.y + bounds.height / 2;
465
+ this.canvasApi.viewportEl.style.transition = "transform 0.35s cubic-bezier(0.4,0,0.2,1)";
466
+ this.canvasApi.setViewport({
467
+ tx: sw / 2 - cx * scale,
468
+ ty: sh / 2 - cy * scale,
469
+ scale,
470
+ });
471
+ setTimeout(() => {
472
+ if (this.isActive && this.canvasApi.viewportEl) {
473
+ this.canvasApi.viewportEl.style.transition = "";
474
+ }
475
+ }, 380);
476
+ }
477
+ // ── Choice badges ───────────────────────────────────────────────────────────
478
+ updateBadges(nodeId) {
479
+ const bg = this.badgeG;
480
+ if (!bg)
481
+ return;
482
+ bg.innerHTML = "";
483
+ const choices = this.graph.get(nodeId) ?? [];
484
+ if (choices.length <= 1)
485
+ return;
486
+ for (let i = 0; i < choices.length; i++) {
487
+ const choice = choices[i];
488
+ if (!choice)
489
+ continue;
490
+ const target = this.canvasApi.getShapes().find((s) => s.id === choice.targetId);
491
+ if (!target)
492
+ continue;
493
+ const b = target.shape.bounds;
494
+ const cx = b.x + b.width / 2;
495
+ const cy = b.y - 20;
496
+ const g = svgEl("g");
497
+ attrs(g, { transform: `translate(${cx},${cy})` });
498
+ const circle = svgEl("circle");
499
+ attrs(circle, { r: 14, fill: "var(--bpmnkit-accent,#1a56db)" });
500
+ const num = svgEl("text");
501
+ attrs(num, {
502
+ "text-anchor": "middle",
503
+ "dominant-baseline": "central",
504
+ fill: "white",
505
+ "font-size": "13",
506
+ "font-weight": "bold",
507
+ "font-family": "var(--bpmnkit-font,system-ui)",
508
+ });
509
+ num.textContent = String(i + 1);
510
+ g.appendChild(circle);
511
+ g.appendChild(num);
512
+ const label = choice.label;
513
+ if (label) {
514
+ const truncated = label.length > 20 ? `${label.slice(0, 20)}…` : label;
515
+ const lbg = svgEl("rect");
516
+ attrs(lbg, {
517
+ x: -40,
518
+ y: 18,
519
+ width: 80,
520
+ height: 15,
521
+ rx: 3,
522
+ fill: "var(--bpmnkit-surface,#fff)",
523
+ stroke: "var(--bpmnkit-border,#d0d0e8)",
524
+ });
525
+ const lt = svgEl("text");
526
+ attrs(lt, {
527
+ "text-anchor": "middle",
528
+ y: 26,
529
+ "font-size": "9",
530
+ fill: "var(--bpmnkit-fg-muted,#6666a0)",
531
+ "font-family": "var(--bpmnkit-font,system-ui)",
532
+ });
533
+ lt.textContent = truncated;
534
+ g.appendChild(lbg);
535
+ g.appendChild(lt);
536
+ }
537
+ bg.appendChild(g);
538
+ }
539
+ }
540
+ // ── Progress ────────────────────────────────────────────────────────────────
541
+ updateProgress() {
542
+ if (!this.progressFill || !this.statEl)
543
+ return;
544
+ const total = Math.max(this.reachableCount, 1);
545
+ const done = this.visited.size;
546
+ this.progressFill.style.width = `${(done / total) * 100}%`;
547
+ this.statEl.textContent = `${done} / ${total}`;
548
+ }
549
+ // ── Hints ───────────────────────────────────────────────────────────────────
550
+ updateHints(nodeId) {
551
+ if (!this.hintsEl)
552
+ return;
553
+ const choices = this.graph.get(nodeId) ?? [];
554
+ this.hintsEl.innerHTML = "";
555
+ const hint = (html) => {
556
+ const d = document.createElement("div");
557
+ d.className = "bpmnkit-pres-hint";
558
+ d.innerHTML = html;
559
+ return d;
560
+ };
561
+ if (choices.length > 1) {
562
+ this.hintsEl.appendChild(hint(`<kbd>→</kbd> Next &nbsp;<kbd>1</kbd>–<kbd>${choices.length}</kbd> Choose path`));
563
+ }
564
+ else if (choices.length === 1) {
565
+ this.hintsEl.appendChild(hint("<kbd>→</kbd> Next"));
566
+ }
567
+ else {
568
+ this.hintsEl.appendChild(hint("End of process"));
569
+ }
570
+ if (this.history.length > 0) {
571
+ this.hintsEl.appendChild(hint("<kbd>←</kbd> Back"));
572
+ }
573
+ this.hintsEl.appendChild(hint("<kbd>↑</kbd><kbd>↓</kbd> Zoom"));
574
+ this.hintsEl.appendChild(hint("<kbd>Esc</kbd> Exit"));
575
+ }
576
+ // ── Keyboard ────────────────────────────────────────────────────────────────
577
+ setupKeyboard() {
578
+ const handler = (e) => {
579
+ if (!this.isActive)
580
+ return;
581
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
582
+ return;
583
+ switch (e.key) {
584
+ case "Escape":
585
+ e.preventDefault();
586
+ e.stopPropagation();
587
+ this.exit();
588
+ break;
589
+ case "ArrowLeft":
590
+ e.preventDefault();
591
+ e.stopPropagation();
592
+ this.navigateBack();
593
+ break;
594
+ case "ArrowRight":
595
+ case "Enter": {
596
+ e.preventDefault();
597
+ e.stopPropagation();
598
+ const choices = this.graph.get(this.currentId ?? "") ?? [];
599
+ const next = choices[0];
600
+ if (next)
601
+ this.navigateTo(next.targetId);
602
+ break;
603
+ }
604
+ case "ArrowUp":
605
+ e.preventDefault();
606
+ e.stopPropagation();
607
+ this.zoomAround(1.2);
608
+ break;
609
+ case "ArrowDown":
610
+ e.preventDefault();
611
+ e.stopPropagation();
612
+ this.zoomAround(1 / 1.2);
613
+ break;
614
+ default:
615
+ if (e.key >= "1" && e.key <= "9") {
616
+ e.preventDefault();
617
+ e.stopPropagation();
618
+ const idx = Number(e.key) - 1;
619
+ const choices = this.graph.get(this.currentId ?? "") ?? [];
620
+ const pick = choices[idx];
621
+ if (pick)
622
+ this.navigateTo(pick.targetId);
623
+ }
624
+ }
625
+ };
626
+ window.addEventListener("keydown", handler, true);
627
+ this.keyHandler = handler;
628
+ }
629
+ zoomAround(factor) {
630
+ const vp = this.canvasApi.getViewport();
631
+ const hw = this.canvasApi.svg.clientWidth / 2;
632
+ const hh = this.canvasApi.svg.clientHeight / 2;
633
+ this.canvasApi.setViewport({
634
+ scale: vp.scale * factor,
635
+ tx: hw - (hw - vp.tx) * factor,
636
+ ty: hh - (hh - vp.ty) * factor,
637
+ });
638
+ }
639
+ }
640
+ // ── Plugin factory ─────────────────────────────────────────────────────────────
641
+ export function createPresentationPlugin(options = {}) {
642
+ let mode = null;
643
+ const unsubs = [];
644
+ const api = {
645
+ enter() {
646
+ mode?.enter();
647
+ },
648
+ exit() {
649
+ mode?.exit();
650
+ },
651
+ };
652
+ return {
653
+ name: "presentation",
654
+ api,
655
+ install(canvasApi) {
656
+ mode = new PresentationMode(canvasApi, { onEnter: options.onEnter, onExit: options.onExit });
657
+ unsubs.push(canvasApi.on("diagram:load", (defs) => mode?.setDefs(defs)), canvasApi.on("diagram:clear", () => mode?.exit()));
658
+ if (options.palette) {
659
+ unsubs.push(options.palette.addCommands([
660
+ {
661
+ id: "presentation:start",
662
+ title: "Start Presentation Mode",
663
+ description: "Walk through the process step by step from start event to end",
664
+ action() {
665
+ mode?.enter();
666
+ },
667
+ },
668
+ ]));
669
+ }
670
+ },
671
+ uninstall() {
672
+ mode?.exit();
673
+ for (const off of unsubs)
674
+ off();
675
+ unsubs.length = 0;
676
+ mode = null;
677
+ },
678
+ };
679
+ }
680
+ //# sourceMappingURL=index.js.map