@unofficialbox/box-open-elements 0.23.0 → 0.25.0

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 (54) hide show
  1. package/README.md +15 -13
  2. package/dist/components/actions/button.d.ts +1 -0
  3. package/dist/components/actions/button.js +16 -3
  4. package/dist/components/actions/segmented-control.d.ts +1 -0
  5. package/dist/components/actions/segmented-control.js +20 -6
  6. package/dist/components/feedback/alert.js +1 -1
  7. package/dist/components/feedback/progress-bar.js +2 -2
  8. package/dist/components/forms/checkbox.d.ts +1 -0
  9. package/dist/components/forms/checkbox.js +6 -2
  10. package/dist/components/forms/code-editor.d.ts +86 -0
  11. package/dist/components/forms/code-editor.js +447 -0
  12. package/dist/components/forms/combobox.js +7 -8
  13. package/dist/components/forms/number-input.d.ts +1 -0
  14. package/dist/components/forms/number-input.js +8 -2
  15. package/dist/components/forms/select.d.ts +1 -0
  16. package/dist/components/forms/select.js +3 -1
  17. package/dist/components/forms/text-area.js +5 -7
  18. package/dist/components/forms/text-field.d.ts +1 -0
  19. package/dist/components/forms/text-field.js +2 -0
  20. package/dist/components/navigation/accordion.d.ts +7 -0
  21. package/dist/components/navigation/accordion.js +31 -8
  22. package/dist/components/output/code-block.js +3 -1
  23. package/dist/components/overlays/drawer.js +2 -2
  24. package/dist/core/form-associated.d.ts +1 -0
  25. package/dist/core/form-associated.js +3 -0
  26. package/dist/entries/code-editor.d.ts +1 -0
  27. package/dist/entries/code-editor.js +1 -0
  28. package/dist/foundations/geometry/tokens.d.ts +2 -2
  29. package/dist/foundations/geometry/tokens.js +3 -3
  30. package/dist/foundations/icons/box-iconography.generated.js +1 -0
  31. package/dist/foundations/icons/glyphs/constants.d.ts +472 -0
  32. package/dist/foundations/icons/glyphs/constants.js +473 -0
  33. package/dist/foundations/icons/glyphs/index.d.ts +1 -1
  34. package/dist/foundations/icons/glyphs/index.js +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +1 -0
  37. package/dist/patterns/flow-builder/flow-builder.d.ts +7 -1
  38. package/dist/patterns/flow-builder/flow-builder.js +32 -15
  39. package/dist/patterns/flow-builder/model.d.ts +2 -0
  40. package/dist/patterns/flow-builder/model.js +3 -3
  41. package/dist/patterns/flow-builder/primitives.d.ts +1 -0
  42. package/dist/patterns/flow-builder/primitives.js +5 -4
  43. package/dist/patterns/insights/metric-card.js +2 -2
  44. package/dist/patterns/process-modeler/bridge.d.ts +10 -0
  45. package/dist/patterns/process-modeler/bridge.js +173 -0
  46. package/dist/patterns/process-modeler/geometry.d.ts +18 -0
  47. package/dist/patterns/process-modeler/geometry.js +307 -0
  48. package/dist/patterns/process-modeler/index.d.ts +4 -0
  49. package/dist/patterns/process-modeler/index.js +4 -0
  50. package/dist/patterns/process-modeler/model.d.ts +135 -0
  51. package/dist/patterns/process-modeler/model.js +113 -0
  52. package/dist/patterns/process-modeler/process-modeler.d.ts +130 -0
  53. package/dist/patterns/process-modeler/process-modeler.js +1507 -0
  54. package/package.json +21 -3
@@ -0,0 +1,1507 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ import { announce } from "../../foundations/a11y/index.js";
3
+ import { boeFocusVisibleStyles } from "../../foundations/tokens/interaction.js";
4
+ import { dismissModal, promoteModal } from "../../foundations/overlay/index.js";
5
+ import { KindPicker } from "../flow-builder/primitives.js";
6
+ import { arrangeProcess, routeProcessLine, lineMidpoint, nearProcessLine } from "./geometry.js";
7
+ import { restoreProcessPositions, snapshotProcessPositions, graphChecks, normalizeProcessSelectionPath } from "./bridge.js";
8
+ import { completeLayout, ProcessHistory, validateProjection, } from "./model.js";
9
+ const svgElement = (tag) => document.createElementNS("http://www.w3.org/2000/svg", tag);
10
+ const emit = (element, name, detail) => element.dispatchEvent(new CustomEvent(name, {
11
+ detail,
12
+ bubbles: true,
13
+ composed: true,
14
+ cancelable: true,
15
+ }));
16
+ /** Projects host documents without owning workflow schemas or persistence. */
17
+ export class ProcessModeler extends BaseElement {
18
+ static tagName = "box-process-modeler";
19
+ static get observedAttributes() {
20
+ return ["locked", "snap-to-grid", "heading-level", "disable-connections"];
21
+ }
22
+ documentValue;
23
+ modelValue = {
24
+ project: (document) => document,
25
+ };
26
+ projection = { boxes: [], lines: [] };
27
+ layoutValue = { boxes: {} };
28
+ catalogValue = [];
29
+ selectedId = null;
30
+ connecting = null;
31
+ checks = [];
32
+ renderer;
33
+ cleanupInspector;
34
+ inspected;
35
+ inspectedId;
36
+ historyValue = new ProcessHistory();
37
+ viewport = { x: 0, y: 0, zoom: 1 };
38
+ pointers = new Map();
39
+ drag;
40
+ pinchDistance = 0;
41
+ gestureZoom = 1;
42
+ insertion;
43
+ selectedIds = new Set();
44
+ versionValue = 0;
45
+ lastProjection = "";
46
+ lastDocument;
47
+ connectionsValue = [];
48
+ variablesValue = [];
49
+ activePane = "Outline";
50
+ portDrag;
51
+ marquee;
52
+ segmentDrag;
53
+ dropLine;
54
+ narrowValue = false;
55
+ suppressClick = false;
56
+ resizeObserver;
57
+ routedLines = new Map();
58
+ computedChecks = [];
59
+ get selectedBoxes() { return this.projection.boxes.filter(box => this.selectedIds.has(box.id)); }
60
+ get selectedPath() { return this.selected?.path ? [...this.selected.path] : null; }
61
+ set selectedPath(path) {
62
+ if (!this.isRendered && this.documentValue !== undefined) {
63
+ this.projection = this.model.project(this.documentValue);
64
+ validateProjection(this.projection);
65
+ }
66
+ const normalized = normalizeProcessSelectionPath(this.projection, path);
67
+ this.select(normalized === null ? null : this.projection.boxes.find(box => JSON.stringify(box.path) === JSON.stringify(normalized))?.id ?? null);
68
+ }
69
+ get connections() { return this.connectionsValue; }
70
+ set connections(value) { this.connectionsValue = value; this.refresh(); }
71
+ get variables() { return this.variablesValue; }
72
+ set variables(value) { this.variablesValue = value; this.refresh(); }
73
+ get version() { return this.versionValue; }
74
+ get narrow() { return this.narrowValue; }
75
+ get positions() { return snapshotProcessPositions(this.projection, this.layoutValue); }
76
+ load(document, options = {}) {
77
+ const projection = this.model.project(document);
78
+ validateProjection(projection);
79
+ this.documentValue = document;
80
+ this.projection = projection;
81
+ this.versionValue = options.version ?? 0;
82
+ this.lastProjection = "";
83
+ this.layoutValue = restoreProcessPositions(projection, options.positions ?? []);
84
+ this.history.clear();
85
+ this.selectedId = null;
86
+ this.selectedIds.clear();
87
+ this.refresh();
88
+ if (options.selectedPath)
89
+ this.selectedPath = options.selectedPath;
90
+ }
91
+ selectMany(ids) {
92
+ if (!this.isRendered && this.documentValue !== undefined) {
93
+ this.projection = this.model.project(this.documentValue);
94
+ validateProjection(this.projection);
95
+ }
96
+ const next = new Set(ids.filter(id => this.projection.boxes.some(box => box.id === id)));
97
+ if (next.size === this.selectedIds.size && [...next].every(id => this.selectedIds.has(id)))
98
+ return;
99
+ this.selectedIds = next;
100
+ this.selectedId = this.selectedIds.values().next().value ?? null;
101
+ this.renderSelection();
102
+ if (this.isRendered)
103
+ this.updateToolbar();
104
+ emit(this, "selection-changed", { box: this.selected, boxes: this.selectedBoxes, path: this.selectedPath });
105
+ }
106
+ get headingLevel() { const level = Number(this.getAttribute("heading-level") ?? 2); return Number.isInteger(level) && level >= 1 && level <= 6 ? level : 2; }
107
+ set headingLevel(value) { this.setAttribute("heading-level", String(value)); }
108
+ get disableConnections() { return this.hasAttribute("disable-connections"); }
109
+ set disableConnections(value) { this.toggleAttribute("disable-connections", value); }
110
+ arrangedLayout(layout) {
111
+ const arranged = this.model.arrange?.(this.projection) ?? arrangeProcess(this.projection);
112
+ return completeLayout(this.projection, { ...layout, boxes: { ...arranged?.boxes, ...layout.boxes } });
113
+ }
114
+ get document() {
115
+ return this.documentValue;
116
+ }
117
+ get history() {
118
+ return this.historyValue;
119
+ }
120
+ set history(value) {
121
+ this.historyValue = value;
122
+ this.refresh();
123
+ }
124
+ set document(value) {
125
+ this.documentValue = value;
126
+ this.refresh();
127
+ }
128
+ get model() {
129
+ return this.modelValue;
130
+ }
131
+ set model(value) {
132
+ this.modelValue = value;
133
+ this.refresh();
134
+ }
135
+ get layout() {
136
+ return structuredClone(this.layoutValue);
137
+ }
138
+ set layout(value) {
139
+ if (JSON.stringify(this.layoutValue) === JSON.stringify(value))
140
+ return;
141
+ this.layoutValue = structuredClone(value);
142
+ if (!this.history.replaying)
143
+ this.history.clear();
144
+ this.refresh();
145
+ }
146
+ get catalog() {
147
+ return this.catalogValue;
148
+ }
149
+ set catalog(value) {
150
+ this.catalogValue = value;
151
+ this.refresh();
152
+ }
153
+ get locked() {
154
+ return this.hasAttribute("locked");
155
+ }
156
+ set locked(value) {
157
+ this.toggleAttribute("locked", value);
158
+ }
159
+ get snapToGrid() {
160
+ return this.hasAttribute("snap-to-grid");
161
+ }
162
+ set snapToGrid(value) {
163
+ this.toggleAttribute("snap-to-grid", value);
164
+ }
165
+ get selected() {
166
+ return (this.projection.boxes.find((box) => box.id === this.selectedId) ?? null);
167
+ }
168
+ get renderInspector() {
169
+ return this.renderer;
170
+ }
171
+ set renderInspector(value) {
172
+ this.cleanupInspector?.();
173
+ this.cleanupInspector = undefined;
174
+ this.inspected = undefined;
175
+ this.renderer = value;
176
+ this.refresh();
177
+ }
178
+ disconnectedCallback() {
179
+ this.resizeObserver?.disconnect();
180
+ this.shadowRoot?.querySelectorAll("[part=pane-drawer]").forEach(dialog => dismissModal(dialog));
181
+ const chooser = this.shadowRoot?.querySelector("[part=insert-chooser]");
182
+ if (chooser)
183
+ dismissModal(chooser);
184
+ this.cleanupInspector?.();
185
+ this.cleanupInspector = undefined;
186
+ this.inspected = undefined;
187
+ this.pointers.clear();
188
+ this.drag = undefined;
189
+ this.portDrag = undefined;
190
+ this.marquee = undefined;
191
+ this.segmentDrag = undefined;
192
+ }
193
+ connectedCallback() { super.connectedCallback(); this.resizeObserver?.observe(this); }
194
+ refresh() {
195
+ if (this.isRendered)
196
+ this.update();
197
+ }
198
+ select(id) {
199
+ if (!this.isRendered && this.documentValue !== undefined) {
200
+ this.projection = this.model.project(this.documentValue);
201
+ validateProjection(this.projection);
202
+ }
203
+ if (id !== null && !this.projection.boxes.some((box) => box.id === id))
204
+ return;
205
+ if (id === this.selectedId && this.selectedIds.size === (id ? 1 : 0))
206
+ return;
207
+ this.selectedId = id;
208
+ this.selectedIds = new Set(id ? [id] : []);
209
+ this.renderSelection();
210
+ if (this.isRendered)
211
+ this.updateToolbar();
212
+ emit(this, "selection-changed", { box: this.selected, boxes: this.selectedBoxes, path: this.selectedPath });
213
+ }
214
+ setValidation(checks) {
215
+ this.checks = checks;
216
+ this.refresh();
217
+ }
218
+ setValidationAtPath(message, path) {
219
+ this.setValidation(message ? [{ message, path }] : []);
220
+ }
221
+ requestEdit(edit) {
222
+ if (this.locked || (this.disableConnections && (edit.type === "connect" || edit.type === "disconnect")))
223
+ return;
224
+ const before = this.layout;
225
+ const previousIds = new Set(this.projection.boxes.map((box) => box.id));
226
+ let accepted = false;
227
+ const request = {
228
+ ...edit,
229
+ accept: (command) => {
230
+ if (accepted)
231
+ return;
232
+ accepted = true;
233
+ if (command.layout)
234
+ this.layoutValue = structuredClone(command.layout);
235
+ this.refresh();
236
+ if (!command.layout &&
237
+ edit.position &&
238
+ Number.isFinite(edit.position.x) &&
239
+ Number.isFinite(edit.position.y)) {
240
+ const added = this.projection.boxes.find(box => box.id === edit.boxId) ?? this.projection.boxes.find((box) => !previousIds.has(box.id));
241
+ if (added)
242
+ this.layoutValue = this.translatedLayout(added.id, edit.position.x, edit.position.y);
243
+ }
244
+ const after = this.layout;
245
+ const restore = (layout) => {
246
+ this.layoutValue = structuredClone(layout);
247
+ this.refresh();
248
+ this.emitPositions();
249
+ };
250
+ this.history.record({
251
+ undo: () => {
252
+ command.undo();
253
+ restore(before);
254
+ },
255
+ redo: () => {
256
+ command.redo();
257
+ restore(after);
258
+ },
259
+ });
260
+ this.refresh();
261
+ if (JSON.stringify(before) !== JSON.stringify(after))
262
+ this.emitPositions();
263
+ announce("Process updated", "polite", this.ownerDocument);
264
+ },
265
+ };
266
+ emit(this, "process-edit-request", request);
267
+ }
268
+ undo() {
269
+ if (this.locked)
270
+ return;
271
+ this.history.undo();
272
+ this.refresh();
273
+ }
274
+ redo() {
275
+ if (this.locked)
276
+ return;
277
+ this.history.redo();
278
+ this.refresh();
279
+ }
280
+ commitLayout(next) {
281
+ const before = this.layout;
282
+ const after = structuredClone(next);
283
+ if (JSON.stringify(before) === JSON.stringify(after) || this.locked)
284
+ return;
285
+ const apply = (layout) => {
286
+ this.layoutValue = structuredClone(layout);
287
+ this.refresh();
288
+ this.emitPositions();
289
+ };
290
+ apply(after);
291
+ this.history.record({
292
+ undo: () => apply(before),
293
+ redo: () => apply(after),
294
+ });
295
+ this.updateToolbar();
296
+ }
297
+ move(id, x, y) {
298
+ if (!Number.isFinite(x) ||
299
+ !Number.isFinite(y) ||
300
+ !this.layoutValue.boxes[id] ||
301
+ this.locked)
302
+ return;
303
+ const next = this.translatedLayout(id, x, y);
304
+ if (emit(this, "move-request", { boxId: id, position: next.boxes[id] }))
305
+ this.commitLayout(next);
306
+ }
307
+ emitPositions() {
308
+ emit(this, "layout-changed", { layout: this.layout });
309
+ emit(this, "positions-changed", { positions: this.positions, version: this.versionValue });
310
+ }
311
+ translatedLayout(id, x, y) {
312
+ const snap = (n) => (this.snapToGrid ? Math.round(n / 20) * 20 : n);
313
+ const next = this.layout;
314
+ next.boxes[id] = { ...next.boxes[id], x: snap(x), y: snap(y) };
315
+ const dx = next.boxes[id].x - this.layoutValue.boxes[id].x;
316
+ const dy = next.boxes[id].y - this.layoutValue.boxes[id].y;
317
+ const descendants = new Set([id]);
318
+ for (let changed = true; changed;) {
319
+ changed = false;
320
+ for (const box of this.projection.boxes) {
321
+ if (box.parentId &&
322
+ descendants.has(box.parentId) &&
323
+ !descendants.has(box.id)) {
324
+ descendants.add(box.id);
325
+ changed = true;
326
+ next.boxes[box.id] = {
327
+ ...next.boxes[box.id],
328
+ x: next.boxes[box.id].x + dx,
329
+ y: next.boxes[box.id].y + dy,
330
+ };
331
+ }
332
+ }
333
+ }
334
+ return next;
335
+ }
336
+ tidy() {
337
+ if (this.locked)
338
+ return;
339
+ this.commitLayout(this.arrangedLayout({ ...this.layout, boxes: {} }));
340
+ this.fit();
341
+ }
342
+ resize(id, width, height) {
343
+ if (!this.layoutValue.boxes[id] ||
344
+ !Number.isFinite(width) ||
345
+ !Number.isFinite(height))
346
+ return;
347
+ const next = this.layout;
348
+ next.boxes[id] = {
349
+ ...next.boxes[id],
350
+ width: Math.max(120, width),
351
+ height: Math.max(64, height),
352
+ };
353
+ this.commitLayout(next);
354
+ }
355
+ routeLine(id, points) {
356
+ if (!this.projection.lines.some((line) => line.id === id) ||
357
+ points.some((point) => !Number.isFinite(point.x) || !Number.isFinite(point.y)))
358
+ return;
359
+ this.commitLayout({
360
+ ...this.layout,
361
+ lines: {
362
+ ...this.layoutValue.lines,
363
+ [id]: points.map((point) => ({ ...point })),
364
+ },
365
+ });
366
+ }
367
+ setNote(note, id = note?.id) {
368
+ if (!id || (note && (!Number.isFinite(note.x) || !Number.isFinite(note.y))))
369
+ return;
370
+ this.commitLayout({
371
+ ...this.layout,
372
+ notes: [
373
+ ...(this.layoutValue.notes ?? []).filter((item) => item.id !== id),
374
+ ...(note ? [{ ...note, id }] : []),
375
+ ],
376
+ });
377
+ }
378
+ setSection(section, id = section?.id) {
379
+ if (!id ||
380
+ (section &&
381
+ [section.x, section.y, section.width, section.height].some((value) => !Number.isFinite(value))))
382
+ return;
383
+ this.commitLayout({
384
+ ...this.layout,
385
+ sections: [
386
+ ...(this.layoutValue.sections ?? []).filter((item) => item.id !== id),
387
+ ...(section ? [{ ...section, id }] : []),
388
+ ],
389
+ });
390
+ }
391
+ zoomBy(factor) {
392
+ if (!Number.isFinite(factor) || factor <= 0)
393
+ return;
394
+ const viewport = this.shadowRoot.querySelector("[part=canvas]");
395
+ const centerX = viewport.clientWidth / 2;
396
+ const centerY = viewport.clientHeight / 2;
397
+ const previous = this.viewport.zoom;
398
+ const next = Math.max(0.25, Math.min(3, previous * factor));
399
+ this.viewport.x = centerX - ((centerX - this.viewport.x) / previous) * next;
400
+ this.viewport.y = centerY - ((centerY - this.viewport.y) / previous) * next;
401
+ this.viewport.zoom = next;
402
+ this.paintViewport();
403
+ }
404
+ fit() {
405
+ const bounds = this.bounds();
406
+ const canvas = this.shadowRoot.querySelector("[part=canvas]");
407
+ this.viewport.zoom = Math.max(0.25, Math.min(1.5, ((canvas.clientWidth || 700) - 40) / bounds.width, ((canvas.clientHeight || 480) - 40) / bounds.height));
408
+ this.viewport.x = 20 - bounds.x * this.viewport.zoom;
409
+ this.viewport.y = 20 - bounds.y * this.viewport.zoom;
410
+ this.paintViewport();
411
+ }
412
+ bounds() {
413
+ const positions = this.projection.boxes.map((box) => this.layoutValue.boxes[box.id]);
414
+ const x = Math.min(0, ...positions.map((pos) => pos.x));
415
+ const y = Math.min(0, ...positions.map((pos) => pos.y));
416
+ return {
417
+ x,
418
+ y,
419
+ width: Math.max(100, ...positions.map((pos) => pos.x + (pos.width ?? 220))) -
420
+ x,
421
+ height: Math.max(100, ...positions.map((pos) => pos.y + (pos.height ?? 96))) -
422
+ y,
423
+ };
424
+ }
425
+ renderTemplate() {
426
+ this.shadowRoot.innerHTML = `<style>
427
+ :host{display:block;font:inherit;color:var(--boe-token-text-text,#222);min-width:0}:host([hidden]){display:none!important}
428
+ *{box-sizing:border-box}button,input{font:inherit;color:inherit}button{background:var(--boe-token-surface-surface,#fff);border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:8px;padding:8px;min-height:36px;cursor:pointer}button:disabled{opacity:.5;cursor:default}
429
+ ${boeFocusVisibleStyles(":is(button,input,[part=canvas],[part=minimap])")}
430
+ [part=toolbar]{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}
431
+ [part=layout]{display:grid;grid-template-columns:minmax(0,1fr) minmax(var(--boe-process-inspector-min-width,240px),var(--boe-process-inspector-width,320px));gap:16px}
432
+ [part=canvas]{position:relative;overflow:hidden;height:var(--boe-process-height,560px);background:var(--boe-token-surface-surface-secondary,#fbfbfb);border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:12px;touch-action:none}
433
+ [part=world]{position:absolute;inset:0;transform-origin:0 0}
434
+ [part=lines]{position:absolute;inset:0;width:1px;height:1px;overflow:visible;pointer-events:none}
435
+ [part=line]{stroke:var(--boe-token-text-text-secondary,#666);stroke-width:2;fill:none}
436
+ [part=box],[part=frame]{position:absolute;padding:12px;display:flex;flex-direction:column;gap:6px;text-align:start;background:var(--boe-token-surface-surface,#fff);border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:8px;touch-action:none}
437
+ [part=box] strong,[part=frame] strong{overflow-wrap:anywhere}[part=box] small{color:var(--boe-token-text-text-secondary,#666)}
438
+ [part=frame]{background:color-mix(in srgb,var(--boe-token-surface-surface-brand,#0061d5) 4%,var(--boe-token-surface-surface,#fff));justify-content:flex-start}
439
+ [aria-pressed=true]{border-color:var(--boe-token-surface-surface-brand,#0061d5)}[data-invalid=true]{border-color:var(--boe-token-text-status-text-error,#b92340);border-width:2px}
440
+ [part=connection]{position:absolute;display:flex;gap:4px;align-items:center;background:var(--boe-token-surface-surface,#fff);border-radius:8px;padding:4px;font-size:12px;white-space:nowrap}
441
+ [part=connection] button{font-size:12px;min-height:32px;padding:4px}
442
+ [part=connection] button{opacity:0;pointer-events:none}
443
+ [part=connection]:hover button,[part=connection]:focus-within button{opacity:1;pointer-events:auto}
444
+ [part=connection]{min-width:40px;min-height:32px}
445
+ [part=insert-chooser]{padding:12px;background:var(--boe-token-surface-surface,#fff);border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:12px;max-width:calc(100vw - 32px);max-height:65vh;overflow:auto}
446
+ [part=inspector]{min-width:0;padding:12px;border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:12px;background:var(--boe-token-surface-surface,#fff)}
447
+ [hidden]{display:none!important}
448
+ [part=palette]{display:grid;gap:8px}[part=palette] input{width:100%;min-width:0;padding:8px;background:var(--boe-token-surface-surface,#fff);border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:8px}
449
+ [part=choices]{display:grid;gap:6px;max-height:300px;overflow:auto}[part=group]{font-weight:600;margin:8px 0 0}
450
+ [part=checks]{display:grid;gap:6px;margin-top:12px}[part=checks] button{text-align:start;color:var(--boe-token-text-status-text-error,#b92340)}
451
+ [part=minimap]{position:absolute;right:12px;bottom:12px;width:150px;height:90px;background:var(--boe-token-surface-surface,#fff);border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:8px;touch-action:none}
452
+ [part=minimap] rect{fill:var(--boe-token-surface-surface-brand,#0061d5)}[part=minimap] [data-viewport]{fill:none;stroke:var(--boe-token-text-text,#222);stroke-width:3}
453
+ [part=icon]{width:20px;height:20px;color:var(--boe-token-surface-surface-brand,#0061d5)}[part=icon] svg{width:20px;height:20px}
454
+ [part=note],[part=section]{position:absolute;border:1px dashed var(--boe-token-stroke-stroke,#ddd);background:var(--boe-token-surface-surface,#fff);padding:8px;pointer-events:none}
455
+ [part=help]{font-size:13px;color:var(--boe-token-text-text-secondary,#666)}
456
+ @media(max-width:800px){[part=layout]{grid-template-columns:minmax(0,1fr)}[part=canvas]{height:420px}}
457
+ @media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important;scroll-behavior:auto!important}}
458
+ </style><div part="toolbar" role="toolbar" aria-label="Diagram controls"><button data-command="zoom-out">Zoom out</button><button data-command="reset">100%</button><button data-command="zoom-in">Zoom in</button><button data-command="fit">Fit the whole process</button><button data-command="tidy">Tidy up</button><button data-command="undo">Undo</button><button data-command="redo">Redo</button><button data-command="snap" aria-pressed="false">Snap to grid</button><button data-command="lock" aria-pressed="false">Lock diagram</button></div><div part="layout"><div part="canvas" tabindex="0" role="region" aria-label="Process diagram" aria-describedby="process-help"><div part="world"></div><svg part="minimap" role="img" aria-label="Diagram overview. Click to jump"></svg></div><div part="inspector" role="region" aria-label="Process details"><div part="palette"><label>Find a building block<input type="search" part="search"></label><div part="choices"></div></div><div part="editor"></div><div part="checks" role="region" aria-label="Checks"></div></div></div><p part="help" id="process-help">Select a box. Arrow keys move it; C then another box connects it; Delete removes it. Drag the background to pan. Use zoom controls or pinch to zoom. Escape cancels connecting.</p><div part="status" role="status" aria-live="polite"></div>`;
459
+ const chooser = document.createElement("dialog");
460
+ chooser.setAttribute("part", "insert-chooser");
461
+ chooser.setAttribute("aria-label", "Insert a building block");
462
+ const picker = new KindPicker();
463
+ picker.variant = "menu";
464
+ chooser.append(picker);
465
+ this.shadowRoot.append(chooser);
466
+ picker.addEventListener("kind-pick", event => {
467
+ const insertion = this.insertion;
468
+ dismissModal(chooser);
469
+ this.insertion = undefined;
470
+ if (insertion)
471
+ this.requestEdit({ ...insertion, kind: event.detail.kind });
472
+ });
473
+ picker.addEventListener("picker-cancel", () => { dismissModal(chooser); this.insertion = undefined; });
474
+ this.setupPanes();
475
+ }
476
+ setupPanes() {
477
+ const root = this.shadowRoot;
478
+ const style = document.createElement("style");
479
+ style.textContent = `
480
+ [part=layout]{grid-template-columns:180px minmax(0,1fr) minmax(var(--boe-process-inspector-min-width,220px),var(--boe-process-inspector-width,280px))}
481
+ [part=pane-drawer]{display:contents;color:inherit}[part=pane-drawer]::backdrop{background:rgb(0 0 0 / .45)}
482
+ [part=pane-close],[data-command=palette],[data-command=details]{display:none}
483
+ [part=palette]{align-content:start;border:1px solid var(--boe-control-edge,#6f6f6f);border-radius:12px;padding:12px;background:var(--boe-token-surface-surface,#fff)}
484
+ [part=pane-tabs]{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:12px}[part=pane-tabs] button{font-size:12px;min-height:28px}
485
+ [part=pane-content]{display:grid;gap:8px}[part=pane-content] button{min-height:28px;text-align:start}
486
+ [part=port]{position:absolute;width:24px;height:24px;min-height:24px;border-radius:50%;padding:0;background:var(--boe-token-surface-surface-brand,#0061d5);border:2px solid var(--boe-token-surface-surface,#fff);opacity:0;z-index:3}
487
+ [part=box]:hover [part=port],[part=box]:focus-within [part=port],[part=box][aria-current=true] [part=port],[part=frame]:hover [part=port]{opacity:1}
488
+ [data-side=north]{left:calc(50% - 12px);top:-12px}[data-side=south]{left:calc(50% - 12px);bottom:-12px}[data-side=east]{right:-12px;top:calc(50% - 12px)}[data-side=west]{left:-12px;top:calc(50% - 12px)}
489
+ [part=box][aria-current=true],[part=frame][aria-current=true]{border:2px solid var(--boe-token-surface-surface-brand,#0061d5)}
490
+ [part=line-hit]{fill:none;stroke:transparent;stroke-width:24;pointer-events:stroke}[data-drop=true]{stroke:var(--boe-token-surface-surface-brand,#0061d5);stroke-width:4}
491
+ [part=connection]{transform:translate(-50%,-50%);font-size:12px}
492
+ [part=connection-actions]{position:absolute;top:100%;left:50%;transform:translateX(-50%);display:flex;gap:4px;width:max-content;background:var(--boe-token-surface-surface,#fff);border-radius:8px}
493
+ [part=marquee]{position:absolute;border:2px solid var(--boe-token-surface-surface-brand,#0061d5);background:color-mix(in srgb,var(--boe-token-surface-surface-brand,#0061d5) 12%,transparent);pointer-events:none}
494
+ [part=guide]{position:absolute;background:var(--boe-token-surface-surface-brand,#0061d5);pointer-events:none}
495
+ [part=measure]{position:absolute;background:var(--boe-token-surface-surface,#fff);font-size:12px;padding:4px;pointer-events:none}
496
+ [part=selection-toolbar]{position:absolute;left:12px;bottom:12px;display:flex;gap:4px;flex-wrap:wrap;background:var(--boe-token-surface-surface,#fff);padding:6px;border:1px solid var(--boe-control-edge,#6f6f6f);border-radius:8px;z-index:5}
497
+ :host([data-narrow]) [part=layout]{grid-template-columns:minmax(0,1fr)}
498
+ :host([data-narrow]) [part=pane-drawer]{display:none;max-width:calc(100vw - 24px);width:320px;max-height:85vh;overflow:auto;border:1px solid var(--boe-control-edge,#6f6f6f);border-radius:12px;padding:12px;background:var(--boe-token-surface-surface,#fff)}
499
+ :host([data-narrow]) [part=pane-drawer][open]{display:block}
500
+ :host([data-narrow]) [part=pane-close],:host([data-narrow]) [data-command=palette],:host([data-narrow]) [data-command=details]{display:inline-block}
501
+ `;
502
+ root.append(style);
503
+ const layout = root.querySelector('[part=layout]');
504
+ for (const [part, label] of [["palette", "Building blocks"], ["inspector", "Process details"]]) {
505
+ const pane = root.querySelector(`[part=${part}]`);
506
+ const dialog = document.createElement("dialog");
507
+ dialog.setAttribute("part", "pane-drawer");
508
+ dialog.setAttribute("aria-label", label);
509
+ const close = document.createElement("button");
510
+ close.setAttribute("part", "pane-close");
511
+ close.textContent = `Close ${label.toLowerCase()}`;
512
+ close.onclick = () => dismissModal(dialog);
513
+ dialog.append(close, pane);
514
+ if (part === "palette")
515
+ layout.prepend(dialog);
516
+ else
517
+ layout.append(dialog);
518
+ const trigger = document.createElement("button");
519
+ trigger.dataset.command = part === "palette" ? "palette" : "details";
520
+ trigger.textContent = label;
521
+ trigger.onclick = () => promoteModal(dialog);
522
+ root.querySelector('[part=toolbar]').append(trigger);
523
+ }
524
+ const inspector = root.querySelector('[part=inspector]');
525
+ const tabs = document.createElement("div");
526
+ tabs.setAttribute("part", "pane-tabs");
527
+ tabs.setAttribute("role", "tablist");
528
+ tabs.setAttribute("aria-label", "Process details");
529
+ for (const name of ["Outline", "Checks", "Variables", "Connections", "Shortcuts"]) {
530
+ const button = document.createElement("button");
531
+ button.textContent = name;
532
+ button.setAttribute("role", "tab");
533
+ button.id = `process-tab-${name.toLowerCase()}`;
534
+ button.setAttribute("aria-controls", "process-pane-content");
535
+ button.onclick = () => { this.activePane = name; this.renderPane(); };
536
+ button.onkeydown = event => {
537
+ const buttons = Array.from(tabs.querySelectorAll("button"));
538
+ const index = buttons.indexOf(button);
539
+ const next = event.key === "ArrowRight" ? (index + 1) % buttons.length : event.key === "ArrowLeft" ? (index + buttons.length - 1) % buttons.length : event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : -1;
540
+ if (next >= 0) {
541
+ event.preventDefault();
542
+ buttons[next].click();
543
+ buttons[next].focus();
544
+ }
545
+ };
546
+ tabs.append(button);
547
+ }
548
+ const content = document.createElement("div");
549
+ content.setAttribute("part", "pane-content");
550
+ content.setAttribute("role", "tabpanel");
551
+ content.id = "process-pane-content";
552
+ inspector.prepend(tabs);
553
+ inspector.append(content);
554
+ root.querySelector('[part=checks]').removeAttribute("role");
555
+ const canvas = root.querySelector('[part=canvas]');
556
+ const floating = document.createElement("div");
557
+ floating.setAttribute("part", "selection-toolbar");
558
+ floating.setAttribute("role", "toolbar");
559
+ floating.setAttribute("aria-label", "Selected steps");
560
+ for (const [command, label] of [["align", "Align tops"], ["space", "Space evenly"], ["delete-many", "Delete selected"]]) {
561
+ const button = document.createElement("button");
562
+ button.textContent = label;
563
+ button.onclick = () => this.selectionCommand(command);
564
+ floating.append(button);
565
+ }
566
+ canvas.append(floating);
567
+ if (typeof ResizeObserver !== "undefined") {
568
+ this.resizeObserver = new ResizeObserver(entries => {
569
+ const narrow = entries[0].contentRect.width < 1000;
570
+ this.narrowValue = narrow;
571
+ this.toggleAttribute("data-narrow", narrow);
572
+ if (!narrow)
573
+ root.querySelectorAll('[part=pane-drawer]').forEach(dialog => dismissModal(dialog));
574
+ });
575
+ this.resizeObserver.observe(this);
576
+ }
577
+ }
578
+ renderPane() {
579
+ if (!this.isRendered)
580
+ return;
581
+ const root = this.shadowRoot;
582
+ root.querySelectorAll('[part=pane-tabs] button').forEach(button => {
583
+ const selected = button.textContent === this.activePane;
584
+ button.setAttribute("aria-selected", String(selected));
585
+ button.tabIndex = selected ? 0 : -1;
586
+ });
587
+ const content = root.querySelector('[part=pane-content]');
588
+ content.setAttribute("aria-labelledby", `process-tab-${this.activePane.toLowerCase()}`);
589
+ const checks = root.querySelector('[part=checks]');
590
+ checks.hidden = this.activePane !== "Checks";
591
+ // Move the checks node out before replacing its parent; keep its handlers.
592
+ root.querySelector('[part=inspector]').append(checks);
593
+ content.replaceChildren();
594
+ root.querySelector('[part=editor]').hidden = this.activePane !== "Outline";
595
+ if (this.activePane === "Outline") {
596
+ this.projection.boxes.forEach(box => {
597
+ const button = document.createElement("button");
598
+ button.textContent = box.title;
599
+ button.setAttribute("aria-current", String(this.selectedIds.has(box.id)));
600
+ button.onclick = () => { this.select(box.id); this.focusBox(box.id); };
601
+ content.append(button);
602
+ });
603
+ }
604
+ else if (this.activePane === "Checks") {
605
+ content.append(checks);
606
+ if (!this.allChecks.length) {
607
+ const message = document.createElement("p");
608
+ message.textContent = "No checks to resolve.";
609
+ content.prepend(message);
610
+ }
611
+ }
612
+ else if (this.activePane === "Connections" || this.activePane === "Variables") {
613
+ const items = this.activePane === "Connections" ? this.connections : this.variables;
614
+ for (const item of items) {
615
+ const row = document.createElement("p");
616
+ row.textContent = "kind" in item ? `${item.name} (${item.kind})` : `${item.name}${item.description ? `: ${item.description}` : ""}`;
617
+ content.append(row);
618
+ }
619
+ if (!items.length)
620
+ content.textContent = "None supplied by the host.";
621
+ }
622
+ else if (this.activePane === "Shortcuts") {
623
+ content.textContent = "Arrow keys: move selected step. Shift+arrows: move farther. C: connect selected step. Shift+drag background: select steps. Delete: remove selected. Escape: cancel. Ctrl/Command+Z: undo. Shift+Ctrl/Command+Z: redo.";
624
+ }
625
+ }
626
+ selectionCommand(command) {
627
+ if (this.locked)
628
+ return;
629
+ const boxes = this.selectedBoxes;
630
+ if (command === "delete-many") {
631
+ for (const box of boxes)
632
+ this.requestEdit({ type: "delete", boxId: box.id });
633
+ return;
634
+ }
635
+ if (boxes.length < 2)
636
+ return;
637
+ const next = this.layout;
638
+ const sorted = [...boxes].filter(box => {
639
+ let parent = box.parentId;
640
+ while (parent) {
641
+ if (this.selectedIds.has(parent))
642
+ return false;
643
+ parent = this.projection.boxes.find(box => box.id === parent)?.parentId;
644
+ }
645
+ return true;
646
+ }).sort((a, b) => next.boxes[a.id].x - next.boxes[b.id].x);
647
+ if (sorted.length < 2)
648
+ return;
649
+ const first = next.boxes[sorted[0].id];
650
+ const last = next.boxes[sorted.at(-1).id];
651
+ sorted.forEach((box, i) => {
652
+ const current = next.boxes[box.id];
653
+ const moved = this.translatedLayout(box.id, command === "align" ? current.x : first.x + i * (last.x - first.x) / (sorted.length - 1), command === "align" ? first.y : current.y);
654
+ for (const candidate of this.projection.boxes)
655
+ if (moved.boxes[candidate.id].x !== this.layoutValue.boxes[candidate.id].x || moved.boxes[candidate.id].y !== this.layoutValue.boxes[candidate.id].y)
656
+ next.boxes[candidate.id] = moved.boxes[candidate.id];
657
+ });
658
+ if (emit(this, "move-request", { boxId: sorted[0].id, position: next.boxes[sorted[0].id], boxIds: sorted.map(box => box.id), positions: next.boxes }))
659
+ this.commitLayout(next);
660
+ }
661
+ setupListeners() {
662
+ const toolbar = this.shadowRoot.querySelector("[part=toolbar]");
663
+ for (const [command, label] of [
664
+ ["connect", "Connect selected"],
665
+ ["delete", "Delete selected"],
666
+ ]) {
667
+ const button = document.createElement("button");
668
+ button.type = "button";
669
+ button.dataset.command = command;
670
+ button.textContent = label;
671
+ toolbar.append(button);
672
+ }
673
+ this.shadowRoot.querySelector("[part=toolbar]").addEventListener("click", (event) => {
674
+ const command = event.target.closest("button")?.dataset.command;
675
+ if (command === "zoom-in")
676
+ this.zoomBy(1.2);
677
+ else if (command === "zoom-out")
678
+ this.zoomBy(1 / 1.2);
679
+ else if (command === "reset")
680
+ this.zoomBy(1 / this.viewport.zoom);
681
+ else if (command === "fit")
682
+ this.fit();
683
+ else if (command === "tidy")
684
+ this.tidy();
685
+ else if (command === "undo")
686
+ this.undo();
687
+ else if (command === "redo")
688
+ this.redo();
689
+ else if (command === "snap")
690
+ this.snapToGrid = !this.snapToGrid;
691
+ else if (command === "lock")
692
+ this.locked = !this.locked;
693
+ else if (command === "connect" && this.selected && !this.locked && !this.disableConnections) {
694
+ this.connecting = this.selected.id;
695
+ this.setStatus(`Select the next box to connect from ${this.selected.title}`);
696
+ }
697
+ else if (command === "delete" && this.selected)
698
+ this.selectionCommand("delete-many");
699
+ });
700
+ this.shadowRoot.querySelector("[part=search]").addEventListener("input", () => this.renderPalette());
701
+ const canvas = this.shadowRoot.querySelector("[part=canvas]");
702
+ canvas.addEventListener("keydown", (event) => this.onKey(event));
703
+ canvas.addEventListener("wheel", (event) => {
704
+ event.preventDefault();
705
+ if (event.ctrlKey || event.metaKey)
706
+ this.zoomBy(Math.exp(-event.deltaY * 0.01));
707
+ else {
708
+ this.viewport.x -= event.deltaX;
709
+ this.viewport.y -= event.deltaY;
710
+ this.paintViewport();
711
+ }
712
+ }, { passive: false });
713
+ canvas.addEventListener("pointerdown", (event) => this.pointerDown(event));
714
+ canvas.addEventListener("pointermove", (event) => this.pointerMove(event));
715
+ canvas.addEventListener("pointerup", (event) => this.pointerEnd(event, true));
716
+ canvas.addEventListener("pointercancel", (event) => this.pointerEnd(event, false));
717
+ canvas.addEventListener("lostpointercapture", event => {
718
+ // Touch transfers implicit box capture to the canvas during a drag.
719
+ if (event.target === canvas)
720
+ this.pointerEnd(event, false);
721
+ });
722
+ canvas.addEventListener("gesturestart", (event) => {
723
+ event.preventDefault();
724
+ this.gestureZoom = this.viewport.zoom;
725
+ });
726
+ canvas.addEventListener("gesturechange", (event) => {
727
+ event.preventDefault();
728
+ const scale = event.scale;
729
+ if (scale > 0)
730
+ this.zoomBy((this.gestureZoom * scale) / this.viewport.zoom);
731
+ });
732
+ const minimap = this.shadowRoot.querySelector("[part=minimap]");
733
+ minimap.setAttribute("tabindex", "0");
734
+ minimap.setAttribute("role", "button");
735
+ minimap.setAttribute("aria-label", "Diagram overview. Arrow keys pan; Enter fits the whole process");
736
+ minimap.addEventListener("keydown", (event) => {
737
+ const key = event;
738
+ key.stopPropagation();
739
+ if (key.key === "Enter" || key.key === " ") {
740
+ key.preventDefault();
741
+ this.fit();
742
+ return;
743
+ }
744
+ const direction = {
745
+ ArrowLeft: [100, 0],
746
+ ArrowRight: [-100, 0],
747
+ ArrowUp: [0, 100],
748
+ ArrowDown: [0, -100],
749
+ }[key.key];
750
+ if (direction) {
751
+ key.preventDefault();
752
+ this.viewport.x += direction[0];
753
+ this.viewport.y += direction[1];
754
+ this.paintViewport();
755
+ }
756
+ });
757
+ minimap.addEventListener("pointerdown", (event) => {
758
+ event.stopPropagation();
759
+ const rect = minimap.getBoundingClientRect();
760
+ if (!rect.width || !rect.height)
761
+ return;
762
+ const bounds = this.bounds();
763
+ this.viewport.x =
764
+ canvas.clientWidth / 2 -
765
+ (bounds.x + ((event.clientX - rect.left) / rect.width) * bounds.width) *
766
+ this.viewport.zoom;
767
+ this.viewport.y =
768
+ canvas.clientHeight / 2 -
769
+ (bounds.y +
770
+ ((event.clientY - rect.top) / rect.height) * bounds.height) *
771
+ this.viewport.zoom;
772
+ this.paintViewport();
773
+ });
774
+ }
775
+ onKey(event) {
776
+ if (event.target.closest("input,textarea,select,[contenteditable]"))
777
+ return;
778
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "z") {
779
+ if (this.locked || !(event.shiftKey ? this.history.canRedo : this.history.canUndo))
780
+ return;
781
+ event.preventDefault();
782
+ if (event.shiftKey)
783
+ this.redo();
784
+ else
785
+ this.undo();
786
+ return;
787
+ }
788
+ if (event.key === "Escape") {
789
+ this.connecting = null;
790
+ this.portDrag = undefined;
791
+ this.marquee = undefined;
792
+ this.segmentDrag = undefined;
793
+ this.drag = undefined;
794
+ this.pointers.clear();
795
+ this.refresh();
796
+ this.setStatus("Connecting cancelled");
797
+ return;
798
+ }
799
+ const box = this.selected;
800
+ if (!box || this.locked)
801
+ return;
802
+ if (event.key === "Enter" || event.key === " ")
803
+ return;
804
+ if (event.key.toLowerCase() === "c" && !this.disableConnections) {
805
+ event.preventDefault();
806
+ this.connecting = box.id;
807
+ this.setStatus(`Select the next box to connect from ${box.title}`);
808
+ }
809
+ if (event.key === "Delete" || event.key === "Backspace") {
810
+ event.preventDefault();
811
+ this.selectionCommand("delete-many");
812
+ }
813
+ const direction = {
814
+ ArrowLeft: [-1, 0],
815
+ ArrowRight: [1, 0],
816
+ ArrowUp: [0, -1],
817
+ ArrowDown: [0, 1],
818
+ }[event.key];
819
+ if (direction) {
820
+ event.preventDefault();
821
+ const pos = this.layoutValue.boxes[box.id];
822
+ const distance = event.shiftKey ? 100 : this.snapToGrid ? 20 : 10;
823
+ this.move(box.id, pos.x + direction[0] * distance, pos.y + direction[1] * distance);
824
+ this.focusBox(box.id);
825
+ }
826
+ }
827
+ pointerDown(event) {
828
+ if (event.button !== 0)
829
+ return;
830
+ this.suppressClick = false;
831
+ if (this.portDrag || this.segmentDrag || this.marquee)
832
+ return;
833
+ const control = event.target.closest("[part=port]");
834
+ if (control && !this.locked && !this.disableConnections) {
835
+ event.preventDefault();
836
+ const point = this.canvasPoint(event);
837
+ this.portDrag = { pointerId: event.pointerId, id: control.dataset.owner, side: control.dataset.side, start: point, point };
838
+ this.shadowRoot.querySelector('[part=canvas]').setPointerCapture?.(event.pointerId);
839
+ return;
840
+ }
841
+ const segment = event.target.closest("[data-segment]");
842
+ if (segment && !this.locked) {
843
+ const line = this.projection.lines.find(line => line.id === segment.dataset.lineId);
844
+ this.segmentDrag = { pointerId: event.pointerId, id: line.id, index: Number(segment.dataset.segment), points: routeProcessLine(line, this.layoutValue, this.projection) };
845
+ this.shadowRoot.querySelector('[part=canvas]').setPointerCapture?.(event.pointerId);
846
+ event.preventDefault();
847
+ return;
848
+ }
849
+ if (event.button !== 0 ||
850
+ event.target.closest("[part=connection],button,input,select,textarea,[contenteditable]"))
851
+ return;
852
+ this.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
853
+ if (this.pointers.size === 2) {
854
+ this.drag = undefined;
855
+ this.pinchDistance = this.distance();
856
+ this.refresh();
857
+ return;
858
+ }
859
+ const target = event.target.closest("[data-box-id]");
860
+ const id = target?.dataset.boxId;
861
+ const canvas = this.shadowRoot.querySelector("[part=canvas]");
862
+ if (id && !this.selectedIds.has(id) && !event.shiftKey)
863
+ this.select(id);
864
+ if (!id && event.shiftKey && !this.locked) {
865
+ const point = this.canvasPoint(event);
866
+ this.marquee = { pointerId: event.pointerId, start: point, point };
867
+ canvas.setPointerCapture?.(event.pointerId);
868
+ event.preventDefault();
869
+ return;
870
+ }
871
+ this.drag = {
872
+ id: this.locked ? undefined : id,
873
+ x: event.clientX,
874
+ y: event.clientY,
875
+ position: id ? { ...this.layoutValue.boxes[id] } : undefined,
876
+ panX: this.viewport.x,
877
+ panY: this.viewport.y,
878
+ };
879
+ }
880
+ distance() {
881
+ const [a, b] = [...this.pointers.values()];
882
+ return a && b ? Math.hypot(a.x - b.x, a.y - b.y) : 0;
883
+ }
884
+ pointerMove(event) {
885
+ const gesture = this.portDrag ?? this.segmentDrag ?? this.marquee;
886
+ if (gesture && gesture.pointerId !== event.pointerId)
887
+ return;
888
+ const point = this.canvasPoint(event);
889
+ if (this.portDrag) {
890
+ this.portDrag.point = point;
891
+ let ghost = this.shadowRoot.querySelector('[part=connection-preview]');
892
+ if (!ghost) {
893
+ ghost = svgElement("polyline");
894
+ ghost.setAttribute("part", "connection-preview");
895
+ ghost.style.cssText = "fill:none;stroke:var(--boe-token-surface-surface-brand,#0061d5);stroke-width:3";
896
+ this.shadowRoot.querySelector('[part=lines]').append(ghost);
897
+ }
898
+ const start = this.portDrag.start;
899
+ ghost.setAttribute("points", `${start.x},${start.y} ${point.x},${start.y} ${point.x},${point.y}`);
900
+ return;
901
+ }
902
+ if (this.segmentDrag) {
903
+ const { index, points } = this.segmentDrag;
904
+ if (points[index].x === points[index + 1].x)
905
+ points[index].x = points[index + 1].x = point.x;
906
+ else
907
+ points[index].y = points[index + 1].y = point.y;
908
+ const path = Array.from(this.shadowRoot.querySelectorAll('[part=line]')).find(path => path.dataset.lineId === this.segmentDrag.id);
909
+ path?.setAttribute("points", points.map(p => `${p.x},${p.y}`).join(" "));
910
+ return;
911
+ }
912
+ if (this.marquee) {
913
+ this.marquee.point = point;
914
+ let region = this.shadowRoot.querySelector('[part=marquee]');
915
+ if (!region) {
916
+ region = document.createElement("div");
917
+ region.setAttribute("part", "marquee");
918
+ this.shadowRoot.querySelector('[part=world]').append(region);
919
+ }
920
+ this.place(region, { x: Math.min(point.x, this.marquee.start.x), y: Math.min(point.y, this.marquee.start.y), width: Math.abs(point.x - this.marquee.start.x), height: Math.abs(point.y - this.marquee.start.y) });
921
+ return;
922
+ }
923
+ if (!this.pointers.has(event.pointerId))
924
+ return;
925
+ this.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
926
+ if (this.pointers.size === 2) {
927
+ const distance = this.distance();
928
+ if (this.pinchDistance)
929
+ this.zoomBy(distance / this.pinchDistance);
930
+ this.pinchDistance = distance;
931
+ return;
932
+ }
933
+ if (!this.drag)
934
+ return;
935
+ const dx = event.clientX - this.drag.x;
936
+ const dy = event.clientY - this.drag.y;
937
+ if (Math.hypot(dx, dy) <= 4)
938
+ return;
939
+ this.shadowRoot.querySelector('[part=canvas]').setPointerCapture?.(event.pointerId);
940
+ if (this.drag.id && this.drag.position) {
941
+ const element = Array.from(this.shadowRoot.querySelectorAll("[data-box-id]")).find((box) => box.dataset.boxId === this.drag.id);
942
+ if (element) {
943
+ const position = this.alignedPoint(this.drag.id, this.drag.position.x + dx / this.viewport.zoom, this.drag.position.y + dy / this.viewport.zoom);
944
+ element.style.left = `${position.x}px`;
945
+ element.style.top = `${position.y}px`;
946
+ this.markDropLine(point, this.drag.id);
947
+ }
948
+ }
949
+ else {
950
+ this.viewport.x = this.drag.panX + dx;
951
+ this.viewport.y = this.drag.panY + dy;
952
+ this.paintViewport();
953
+ }
954
+ }
955
+ pointerEnd(event, commit) {
956
+ const gesture = this.portDrag ?? this.segmentDrag ?? this.marquee;
957
+ if (gesture && gesture.pointerId !== event.pointerId)
958
+ return;
959
+ const point = this.canvasPoint(event);
960
+ if (this.portDrag) {
961
+ const port = this.portDrag;
962
+ this.portDrag = undefined;
963
+ this.shadowRoot.querySelector('[part=connection-preview]')?.remove();
964
+ if (commit) {
965
+ if (Math.hypot(point.x - port.start.x, point.y - port.start.y) < 8) {
966
+ const pos = this.layoutValue.boxes[port.id];
967
+ const offsets = { east: [320, 0], west: [-320, 0], north: [0, -170], south: [0, 170] }[port.side];
968
+ this.openChooser({ type: "add", from: port.id, fromSide: port.side, position: { x: pos.x + offsets[0], y: pos.y + offsets[1] } });
969
+ }
970
+ else {
971
+ const target = this.boxAt(point, port.id);
972
+ if (target)
973
+ this.requestEdit({ type: "connect", from: port.id, to: target.id, fromSide: port.side });
974
+ }
975
+ }
976
+ return;
977
+ }
978
+ if (this.segmentDrag) {
979
+ const segment = this.segmentDrag;
980
+ this.segmentDrag = undefined;
981
+ if (commit)
982
+ this.routeLine(segment.id, segment.points.slice(1, -1));
983
+ else
984
+ this.refresh();
985
+ return;
986
+ }
987
+ if (this.marquee) {
988
+ const { start } = this.marquee;
989
+ this.marquee = undefined;
990
+ if (commit)
991
+ this.selectMany(this.projection.boxes.filter(box => { const p = this.layoutValue.boxes[box.id]; return p.x < Math.max(start.x, point.x) && p.x + (p.width ?? 220) > Math.min(start.x, point.x) && p.y < Math.max(start.y, point.y) && p.y + (p.height ?? 96) > Math.min(start.y, point.y); }).map(box => box.id));
992
+ this.shadowRoot.querySelector('[part=marquee]')?.remove();
993
+ this.pointers.delete(event.pointerId);
994
+ return;
995
+ }
996
+ if (!this.pointers.has(event.pointerId))
997
+ return;
998
+ this.pointers.delete(event.pointerId);
999
+ const drag = this.drag;
1000
+ this.drag = undefined;
1001
+ this.pinchDistance = 0;
1002
+ if (drag?.id && drag.position) {
1003
+ const dx = event.clientX - drag.x;
1004
+ const dy = event.clientY - drag.y;
1005
+ if (commit && Math.hypot(dx, dy) > 4) {
1006
+ this.suppressClick = true;
1007
+ const position = this.alignedPoint(drag.id, drag.position.x + dx / this.viewport.zoom, drag.position.y + dy / this.viewport.zoom);
1008
+ const line = this.lineAt(point, drag.id);
1009
+ const frame = this.boxAt(point, drag.id, true);
1010
+ if (line)
1011
+ this.requestEdit({ type: "insert", boxId: drag.id, lineId: line.id, from: line.from, to: line.to, position });
1012
+ else if (this.projection.boxes.find(box => box.id === drag.id)?.parentId !== frame?.id)
1013
+ this.requestEdit({ type: "reparent", boxId: drag.id, parentId: frame?.id, position });
1014
+ else if (this.selectedIds.size > 1 && this.selectedIds.has(drag.id)) {
1015
+ const next = this.layout;
1016
+ const dx = position.x - drag.position.x;
1017
+ const dy = position.y - drag.position.y;
1018
+ const moved = new Set(this.selectedIds);
1019
+ for (let changed = true; changed;) {
1020
+ changed = false;
1021
+ for (const box of this.projection.boxes)
1022
+ if (box.parentId && moved.has(box.parentId) && !moved.has(box.id)) {
1023
+ moved.add(box.id);
1024
+ changed = true;
1025
+ }
1026
+ }
1027
+ const primary = next.boxes[drag.id];
1028
+ const shiftX = this.snapToGrid ? Math.round((primary.x + dx) / 20) * 20 - primary.x : dx;
1029
+ const shiftY = this.snapToGrid ? Math.round((primary.y + dy) / 20) * 20 - primary.y : dy;
1030
+ for (const id of moved)
1031
+ next.boxes[id] = { ...next.boxes[id], x: next.boxes[id].x + shiftX, y: next.boxes[id].y + shiftY };
1032
+ if (emit(this, "move-request", { boxId: drag.id, position: next.boxes[drag.id], boxIds: [...moved], positions: next.boxes }))
1033
+ this.commitLayout(next);
1034
+ }
1035
+ else
1036
+ this.move(drag.id, position.x, position.y);
1037
+ }
1038
+ else if (!commit)
1039
+ this.refresh();
1040
+ }
1041
+ this.shadowRoot.querySelectorAll('[part=guide],[part=measure]').forEach(element => element.remove());
1042
+ this.markDropLine(undefined);
1043
+ if (drag?.id && (!commit || Math.hypot(event.clientX - drag.x, event.clientY - drag.y) > 4))
1044
+ this.refresh();
1045
+ }
1046
+ canvasPoint(event) {
1047
+ const rect = this.shadowRoot.querySelector('[part=canvas]').getBoundingClientRect();
1048
+ return { x: (event.clientX - rect.left - this.viewport.x) / this.viewport.zoom, y: (event.clientY - rect.top - this.viewport.y) / this.viewport.zoom };
1049
+ }
1050
+ boxAt(point, except, frameOnly = false) {
1051
+ return [...this.projection.boxes].reverse().find(box => {
1052
+ if (box.id === except || (frameOnly && !box.frame))
1053
+ return false;
1054
+ // A frame cannot be dropped inside its own descendants.
1055
+ let parent = box.parentId;
1056
+ while (parent) {
1057
+ if (parent === except)
1058
+ return false;
1059
+ parent = this.projection.boxes.find(b => b.id === parent)?.parentId;
1060
+ }
1061
+ const p = this.layoutValue.boxes[box.id];
1062
+ return point.x >= p.x - 12 && point.x <= p.x + (p.width ?? 220) + 12 && point.y >= p.y - 12 && point.y <= p.y + (p.height ?? 96) + 12;
1063
+ });
1064
+ }
1065
+ lineAt(point, except) {
1066
+ return this.projection.lines.find(line => line.from !== except && line.to !== except && nearProcessLine(point, this.routedLines.get(line.id) ?? [], 16 / this.viewport.zoom));
1067
+ }
1068
+ markDropLine(point, except) {
1069
+ this.dropLine = point ? this.lineAt(point, except)?.id : undefined;
1070
+ this.shadowRoot.querySelectorAll('[part=line]').forEach(path => path.dataset.drop = String(path.dataset.lineId === this.dropLine));
1071
+ }
1072
+ alignedPoint(id, x, y) {
1073
+ const world = this.shadowRoot.querySelector('[part=world]');
1074
+ world.querySelectorAll('[part=guide],[part=measure]').forEach(element => element.remove());
1075
+ for (const box of this.projection.boxes) {
1076
+ if (box.id === id)
1077
+ continue;
1078
+ const p = this.layoutValue.boxes[box.id];
1079
+ for (const axis of ["x", "y"]) {
1080
+ const value = axis === "x" ? x : y;
1081
+ if (Math.abs(value - p[axis]) < 8 / this.viewport.zoom) {
1082
+ if (axis === "x")
1083
+ x = p.x;
1084
+ else
1085
+ y = p.y;
1086
+ const guide = document.createElement("div");
1087
+ guide.setAttribute("part", "guide");
1088
+ guide.style.cssText = axis === "x" ? `left:${x}px;top:${Math.min(y, p.y)}px;width:1px;height:${Math.abs(y - p.y) + 96}px` : `left:${Math.min(x, p.x)}px;top:${y}px;height:1px;width:${Math.abs(x - p.x) + 220}px`;
1089
+ world.append(guide);
1090
+ }
1091
+ }
1092
+ if (Math.abs(y - p.y) < 8) {
1093
+ const distance = x - p.x - (p.width ?? 220);
1094
+ if (distance > 0) {
1095
+ const measure = document.createElement("span");
1096
+ measure.setAttribute("part", "measure");
1097
+ measure.textContent = `${Math.round(distance)}px`;
1098
+ measure.style.cssText = `left:${p.x + (p.width ?? 220)}px;top:${y + 40}px`;
1099
+ world.append(measure);
1100
+ }
1101
+ }
1102
+ }
1103
+ return { x, y };
1104
+ }
1105
+ openChooser(edit) {
1106
+ this.insertion = edit;
1107
+ const chooser = this.shadowRoot.querySelector('[part=insert-chooser]');
1108
+ const picker = chooser.querySelector('box-kind-picker');
1109
+ picker.catalog = this.catalog;
1110
+ picker.refresh();
1111
+ promoteModal(chooser);
1112
+ picker.focus();
1113
+ }
1114
+ update() {
1115
+ const selectionBefore = JSON.stringify({ ids: [...this.selectedIds], path: this.selectedPath });
1116
+ if (this.documentValue !== undefined) {
1117
+ this.projection = this.model.project(this.documentValue);
1118
+ validateProjection(this.projection);
1119
+ }
1120
+ else
1121
+ this.projection = { boxes: [], lines: [] };
1122
+ this.layoutValue = this.arrangedLayout(this.layoutValue);
1123
+ this.routedLines = new Map(this.projection.lines.map(line => [line.id, routeProcessLine(line, this.layoutValue, this.projection)]));
1124
+ this.computedChecks = this.collectChecks();
1125
+ if (this.selectedId && !this.selected)
1126
+ this.selectedId = null;
1127
+ this.selectedIds = new Set([...this.selectedIds].filter(id => this.projection.boxes.some(box => box.id === id)));
1128
+ if (!this.selectedId)
1129
+ this.selectedId = this.selectedIds.values().next().value ?? null;
1130
+ const serialized = JSON.stringify({
1131
+ boxes: this.projection.boxes.map(box => ({ id: box.id, kind: box.kind, title: box.title, description: box.description, path: box.path, fingerprint: box.fingerprint, frame: box.frame, parentId: box.parentId })),
1132
+ lines: this.projection.lines,
1133
+ });
1134
+ if (serialized !== this.lastProjection || this.documentValue !== this.lastDocument) {
1135
+ if (this.lastProjection)
1136
+ this.versionValue = typeof this.versionValue === "number" ? this.versionValue + 1 : `${this.versionValue}+1`;
1137
+ this.lastProjection = serialized;
1138
+ this.lastDocument = this.documentValue;
1139
+ emit(this, "projection-changed", { projection: this.projection, version: this.versionValue, checks: this.allChecks });
1140
+ }
1141
+ const focused = this.shadowRoot.activeElement
1142
+ ?.dataset.boxId;
1143
+ this.renderWorld();
1144
+ this.renderPalette();
1145
+ this.renderChecks();
1146
+ this.renderSelection();
1147
+ this.paintViewport();
1148
+ this.updateToolbar();
1149
+ if (selectionBefore !== JSON.stringify({ ids: [...this.selectedIds], path: this.selectedPath }))
1150
+ emit(this, "selection-changed", { box: this.selected, boxes: this.selectedBoxes, path: this.selectedPath });
1151
+ if (focused)
1152
+ this.focusBox(focused);
1153
+ }
1154
+ get allChecks() { return this.computedChecks; }
1155
+ collectChecks() {
1156
+ const routes = this.projection.lines.filter(line => !this.routedLines.get(line.id)?.length)
1157
+ .map(line => ({ message: "A connection is blocked by overlapping steps. Move a step or adjust its route.", boxId: line.from }));
1158
+ return [...this.checks, ...graphChecks(this.projection), ...routes, ...(this.documentValue === undefined ? [] : this.model.validate?.(this.documentValue, this.projection) ?? [])];
1159
+ }
1160
+ renderWorld() {
1161
+ const world = this.shadowRoot.querySelector("[part=world]");
1162
+ world.replaceChildren();
1163
+ for (const section of this.layoutValue.sections ?? []) {
1164
+ const element = document.createElement("div");
1165
+ element.setAttribute("part", "section");
1166
+ element.textContent = section.title;
1167
+ this.place(element, section);
1168
+ world.append(element);
1169
+ }
1170
+ for (const box of [...this.projection.boxes].sort((a, b) => Number(Boolean(b.frame)) - Number(Boolean(a.frame)))) {
1171
+ const element = document.createElement("div");
1172
+ element.tabIndex = 0;
1173
+ element.setAttribute("role", "group");
1174
+ element.setAttribute("part", box.frame ? "frame" : "box");
1175
+ element.dataset.boxId = box.id;
1176
+ element.setAttribute("aria-label", [box.title, box.description, box.frame ? "Frame" : ""]
1177
+ .filter(Boolean)
1178
+ .join(". "));
1179
+ element.setAttribute("aria-current", String(this.selectedIds.has(box.id)));
1180
+ this.place(element, this.layoutValue.boxes[box.id]);
1181
+ const kind = this.catalog.find((kind) => kind.kind === box.kind);
1182
+ if (kind?.icon) {
1183
+ const icon = document.createElement("span");
1184
+ icon.setAttribute("part", "icon");
1185
+ icon.setAttribute("aria-hidden", "true");
1186
+ icon.append(kind.icon());
1187
+ element.append(icon);
1188
+ }
1189
+ const title = document.createElement("strong");
1190
+ title.textContent = box.title;
1191
+ element.append(title);
1192
+ if (box.description) {
1193
+ const description = document.createElement("small");
1194
+ description.textContent = box.description;
1195
+ element.append(description);
1196
+ }
1197
+ element.addEventListener("keydown", event => {
1198
+ if (event.target === element && (event.key === "Enter" || event.key === " ")) {
1199
+ event.preventDefault();
1200
+ element.click();
1201
+ }
1202
+ });
1203
+ element.addEventListener("click", event => {
1204
+ if (this.suppressClick && event.detail !== 0) {
1205
+ this.suppressClick = false;
1206
+ return;
1207
+ }
1208
+ if (this.connecting && this.connecting !== box.id) {
1209
+ this.requestEdit({
1210
+ type: "connect",
1211
+ from: this.connecting,
1212
+ to: box.id,
1213
+ });
1214
+ this.connecting = null;
1215
+ }
1216
+ if (event.shiftKey)
1217
+ this.selectMany(this.selectedIds.has(box.id) ? [...this.selectedIds].filter(id => id !== box.id) : [...this.selectedIds, box.id]);
1218
+ else
1219
+ this.select(box.id);
1220
+ });
1221
+ if (!this.disableConnections && !this.locked) {
1222
+ for (const side of ["north", "east", "south", "west"]) {
1223
+ const port = document.createElement("button");
1224
+ port.type = "button";
1225
+ port.setAttribute("part", "port");
1226
+ port.dataset.side = side;
1227
+ port.dataset.owner = box.id;
1228
+ port.setAttribute("aria-label", `Add or connect a step ${side} of ${box.title}`);
1229
+ port.addEventListener("click", event => {
1230
+ event.stopPropagation();
1231
+ if (!event.detail) {
1232
+ const pos = this.layoutValue.boxes[box.id];
1233
+ const offsets = { east: [320, 0], west: [-320, 0], north: [0, -170], south: [0, 170] }[side];
1234
+ this.openChooser({ type: "add", from: box.id, fromSide: side, position: { x: pos.x + offsets[0], y: pos.y + offsets[1] } });
1235
+ }
1236
+ });
1237
+ element.append(port);
1238
+ }
1239
+ }
1240
+ world.append(element);
1241
+ }
1242
+ const lines = svgElement("svg");
1243
+ lines.setAttribute("part", "lines");
1244
+ lines.setAttribute("aria-hidden", "true");
1245
+ world.append(lines);
1246
+ for (const line of this.projection.lines) {
1247
+ const points = this.routedLines.get(line.id);
1248
+ if (!points.length)
1249
+ continue;
1250
+ const midpoint = lineMidpoint(points);
1251
+ const path = svgElement("polyline");
1252
+ path.setAttribute("part", "line");
1253
+ path.dataset.lineId = line.id;
1254
+ path.setAttribute("vector-effect", "non-scaling-stroke");
1255
+ path.setAttribute("points", points
1256
+ .map((point) => `${point.x},${point.y}`)
1257
+ .join(" "));
1258
+ if (line.dashed)
1259
+ path.setAttribute("stroke-dasharray", "6 4");
1260
+ lines.append(path);
1261
+ for (let i = 1; i < points.length - 2; i++) {
1262
+ const hit = svgElement("polyline");
1263
+ hit.setAttribute("part", "line-hit");
1264
+ hit.dataset.lineId = line.id;
1265
+ hit.dataset.segment = String(i);
1266
+ hit.setAttribute("vector-effect", "non-scaling-stroke");
1267
+ hit.setAttribute("points", `${points[i].x},${points[i].y} ${points[i + 1].x},${points[i + 1].y}`);
1268
+ lines.append(hit);
1269
+ }
1270
+ const label = document.createElement("div");
1271
+ label.setAttribute("part", "connection");
1272
+ label.style.left = `${midpoint.x}px`;
1273
+ label.style.top = `${midpoint.y}px`;
1274
+ const names = `${this.projection.boxes.find((box) => box.id === line.from).title} to ${this.projection.boxes.find((box) => box.id === line.to).title}${line.label ? `, ${line.label}` : ""}`;
1275
+ const text = document.createElement("span");
1276
+ const source = this.projection.boxes.find(box => box.id === line.from);
1277
+ const branchIndex = this.projection.lines.filter(candidate => candidate.from === line.from).indexOf(line);
1278
+ text.textContent = line.label ?? (line.weight !== undefined ? String(line.weight) : /decision/i.test(source.kind) ? ["Yes", "No"][branchIndex] ?? `Route ${branchIndex + 1}` : /try/i.test(source.kind) && line.dashed ? "If it fails" : "");
1279
+ label.append(text);
1280
+ const actions = document.createElement("div");
1281
+ actions.setAttribute("part", "connection-actions");
1282
+ label.append(actions);
1283
+ if (points.length >= 4 && !this.locked) {
1284
+ const adjust = document.createElement("button");
1285
+ adjust.type = "button";
1286
+ adjust.textContent = "Route";
1287
+ adjust.dataset.segment = "1";
1288
+ adjust.dataset.lineId = line.id;
1289
+ adjust.setAttribute("aria-label", `Adjust route: ${names}. Use arrow keys`);
1290
+ adjust.onkeydown = event => {
1291
+ const direction = { ArrowLeft: [-10, 0], ArrowRight: [10, 0], ArrowUp: [0, -10], ArrowDown: [0, 10] }[event.key];
1292
+ if (!direction)
1293
+ return;
1294
+ event.preventDefault();
1295
+ event.stopPropagation();
1296
+ const current = routeProcessLine(line, this.layoutValue, this.projection);
1297
+ const vertical = current[1].x === current[2].x;
1298
+ for (const point of [current[1], current[2]]) {
1299
+ if (vertical)
1300
+ point.x += direction[0];
1301
+ else
1302
+ point.y += direction[1];
1303
+ }
1304
+ this.routeLine(line.id, current.slice(1, -1));
1305
+ Array.from(this.shadowRoot.querySelectorAll('[data-segment]')).find(button => button.dataset.lineId === line.id)?.focus();
1306
+ };
1307
+ actions.append(adjust);
1308
+ }
1309
+ for (const [type, title] of [
1310
+ ["insert", "Insert a step here"],
1311
+ ["disconnect", "Remove connection"],
1312
+ ]) {
1313
+ if (type === "disconnect" && this.disableConnections)
1314
+ continue;
1315
+ const button = document.createElement("button");
1316
+ button.type = "button";
1317
+ button.textContent = type === "insert" ? "+" : "Remove";
1318
+ button.setAttribute("aria-label", `${title}: ${names}`);
1319
+ button.disabled = this.locked;
1320
+ button.addEventListener("click", () => {
1321
+ const edit = {
1322
+ type,
1323
+ lineId: line.id,
1324
+ from: line.from,
1325
+ to: line.to,
1326
+ };
1327
+ if (type === "insert" && this.catalog.length) {
1328
+ this.openChooser(edit);
1329
+ }
1330
+ else
1331
+ this.requestEdit(edit);
1332
+ });
1333
+ actions.append(button);
1334
+ }
1335
+ world.append(label);
1336
+ }
1337
+ for (const note of this.layoutValue.notes ?? []) {
1338
+ const element = document.createElement("div");
1339
+ element.setAttribute("part", "note");
1340
+ element.textContent = note.text;
1341
+ this.place(element, { ...note, width: 180, height: 80 });
1342
+ world.append(element);
1343
+ }
1344
+ }
1345
+ place(element, position) {
1346
+ Object.assign(element.style, {
1347
+ left: `${position.x}px`,
1348
+ top: `${position.y}px`,
1349
+ width: `${position.width ?? 220}px`,
1350
+ height: `${position.height ?? 96}px`,
1351
+ });
1352
+ }
1353
+ renderPalette() {
1354
+ const choices = this.shadowRoot.querySelector("[part=choices]");
1355
+ const query = this.shadowRoot.querySelector("[part=search]").value.toLowerCase();
1356
+ choices.replaceChildren();
1357
+ let group = "";
1358
+ for (const kind of this.catalog.filter((kind) => `${kind.label} ${kind.description} ${kind.group ?? ""}`
1359
+ .toLowerCase()
1360
+ .includes(query))) {
1361
+ if (kind.group && kind.group !== group) {
1362
+ group = kind.group;
1363
+ const heading = document.createElement("p");
1364
+ heading.setAttribute("part", "group");
1365
+ heading.textContent = group;
1366
+ choices.append(heading);
1367
+ }
1368
+ const button = document.createElement("button");
1369
+ button.type = "button";
1370
+ button.textContent = kind.label;
1371
+ button.disabled = this.locked;
1372
+ button.draggable = !this.locked;
1373
+ button.addEventListener("click", () => this.requestEdit({ type: "add", kind }));
1374
+ button.addEventListener("dragstart", (event) => event.dataTransfer?.setData("application/boe-process-kind", kind.kind));
1375
+ choices.append(button);
1376
+ }
1377
+ const canvas = this.shadowRoot.querySelector("[part=canvas]");
1378
+ canvas.ondragover = (event) => {
1379
+ if (!this.locked) {
1380
+ event.preventDefault();
1381
+ this.markDropLine(this.canvasPoint(event));
1382
+ }
1383
+ };
1384
+ canvas.ondragleave = () => this.markDropLine(undefined);
1385
+ canvas.ondrop = (event) => {
1386
+ event.preventDefault();
1387
+ const kind = this.catalog.find((kind) => kind.kind ===
1388
+ event.dataTransfer?.getData("application/boe-process-kind"));
1389
+ if (!kind || this.locked)
1390
+ return;
1391
+ const position = this.canvasPoint(event);
1392
+ const line = this.lineAt(position);
1393
+ const frame = this.boxAt(position, undefined, true);
1394
+ this.requestEdit({
1395
+ type: line ? "insert" : "add",
1396
+ kind,
1397
+ lineId: line?.id, from: line?.from, to: line?.to, parentId: frame?.id,
1398
+ position,
1399
+ });
1400
+ this.markDropLine(undefined);
1401
+ };
1402
+ }
1403
+ renderChecks() {
1404
+ const checks = this.shadowRoot.querySelector("[part=checks]");
1405
+ checks.replaceChildren();
1406
+ this.allChecks.forEach((check) => {
1407
+ const button = document.createElement("button");
1408
+ button.type = "button";
1409
+ button.textContent = check.message;
1410
+ const box = this.projection.boxes.find((box) => box.id === check.boxId ||
1411
+ (check.path &&
1412
+ JSON.stringify(box.path) === JSON.stringify(check.path)));
1413
+ button.addEventListener("click", () => {
1414
+ if (box) {
1415
+ this.select(box.id);
1416
+ this.focusBox(box.id);
1417
+ }
1418
+ this.setStatus(check.message);
1419
+ });
1420
+ checks.append(button);
1421
+ });
1422
+ }
1423
+ renderSelection() {
1424
+ if (!this.isRendered)
1425
+ return;
1426
+ this.shadowRoot.querySelectorAll("[data-box-id]").forEach((element) => {
1427
+ element.setAttribute("aria-current", String(this.selectedIds.has(element.dataset.boxId)));
1428
+ const box = this.projection.boxes.find((box) => box.id === element.dataset.boxId);
1429
+ element.dataset.invalid = String(this.allChecks.some((check) => check.boxId === box.id ||
1430
+ (check.path &&
1431
+ JSON.stringify(check.path) === JSON.stringify(box.path))));
1432
+ });
1433
+ const editor = this.shadowRoot.querySelector("[part=editor]");
1434
+ const selected = this.selected;
1435
+ this.shadowRoot.querySelector("[part=palette]").hidden = false;
1436
+ this.shadowRoot.querySelector('[part=selection-toolbar]').hidden = this.selectedIds.size === 0;
1437
+ this.shadowRoot.querySelectorAll('[part=selection-toolbar] button').forEach((button, index) => button.disabled = this.locked || (index < 2 && this.selectedIds.size < 2));
1438
+ this.renderPane();
1439
+ if (this.inspected === selected?.node &&
1440
+ this.inspectedId === selected?.id &&
1441
+ editor.querySelector(`[part=inspector-heading]`)?.tagName === `H${this.headingLevel}`) {
1442
+ editor.querySelector("[part=inspector-heading]").textContent = selected?.title ?? "";
1443
+ return;
1444
+ }
1445
+ this.cleanupInspector?.();
1446
+ this.cleanupInspector = undefined;
1447
+ editor.replaceChildren();
1448
+ this.inspected = selected?.node;
1449
+ this.inspectedId = selected?.id;
1450
+ if (selected) {
1451
+ const heading = document.createElement(`h${this.headingLevel}`);
1452
+ heading.setAttribute("part", "inspector-heading");
1453
+ heading.textContent = selected.title;
1454
+ editor.append(heading);
1455
+ this.cleanupInspector =
1456
+ this.renderer?.(selected.node, editor) || undefined;
1457
+ }
1458
+ }
1459
+ updateToolbar() {
1460
+ const button = (command) => this.shadowRoot.querySelector(`[data-command=${command}]`);
1461
+ button("undo").disabled = !this.history.canUndo || this.locked;
1462
+ button("redo").disabled = !this.history.canRedo || this.locked;
1463
+ button("tidy").disabled = this.locked;
1464
+ button("connect").disabled = !this.selected || this.locked || this.disableConnections;
1465
+ button("delete").disabled = !this.selected || this.locked;
1466
+ button("lock").setAttribute("aria-pressed", String(this.locked));
1467
+ button("snap").setAttribute("aria-pressed", String(this.snapToGrid));
1468
+ }
1469
+ paintViewport() {
1470
+ this.shadowRoot.querySelector("[part=world]").style.transform =
1471
+ `translate(${this.viewport.x}px,${this.viewport.y}px) scale(${this.viewport.zoom})`;
1472
+ this.shadowRoot.querySelectorAll('[part=port]').forEach(port => port.style.transform = `scale(${1 / this.viewport.zoom})`);
1473
+ this.shadowRoot.querySelector("[data-command=reset]").textContent =
1474
+ `${Math.round(this.viewport.zoom * 100)}%`;
1475
+ const minimap = this.shadowRoot.querySelector("[part=minimap]");
1476
+ const bounds = this.bounds();
1477
+ minimap.replaceChildren();
1478
+ minimap.setAttribute("viewBox", `${bounds.x} ${bounds.y} ${bounds.width} ${bounds.height}`);
1479
+ minimap.setAttribute("preserveAspectRatio", "none");
1480
+ this.projection.boxes.forEach((box) => {
1481
+ const pos = this.layoutValue.boxes[box.id];
1482
+ const rect = svgElement("rect");
1483
+ rect.setAttribute("x", String(pos.x));
1484
+ rect.setAttribute("y", String(pos.y));
1485
+ rect.setAttribute("width", String(pos.width ?? 220));
1486
+ rect.setAttribute("height", String(pos.height ?? 96));
1487
+ minimap.append(rect);
1488
+ });
1489
+ const canvas = this.shadowRoot.querySelector("[part=canvas]");
1490
+ const viewport = svgElement("rect");
1491
+ viewport.dataset.viewport = "true";
1492
+ viewport.setAttribute("x", String(-this.viewport.x / this.viewport.zoom));
1493
+ viewport.setAttribute("y", String(-this.viewport.y / this.viewport.zoom));
1494
+ viewport.setAttribute("width", String(canvas.clientWidth / this.viewport.zoom));
1495
+ viewport.setAttribute("height", String(canvas.clientHeight / this.viewport.zoom));
1496
+ minimap.append(viewport);
1497
+ }
1498
+ focusBox(id) {
1499
+ Array.from(this.shadowRoot.querySelectorAll("[data-box-id]"))
1500
+ .find((box) => box.dataset.boxId === id)
1501
+ ?.focus();
1502
+ }
1503
+ setStatus(message) {
1504
+ this.shadowRoot.querySelector("[part=status]").textContent = message;
1505
+ }
1506
+ }
1507
+ ProcessModeler.register();