@redvars/peacock 3.6.0 → 3.6.2

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 (105) hide show
  1. package/dist/assets/styles.css +1 -1
  2. package/dist/assets/styles.css.map +1 -1
  3. package/dist/assets/tokens.css +1 -1
  4. package/dist/assets/tokens.css.map +1 -1
  5. package/dist/{button-colors-Ccys3hvS.js → button-colors-AvGh22Zn.js} +18 -18
  6. package/dist/{button-colors-Ccys3hvS.js.map → button-colors-AvGh22Zn.js.map} +1 -1
  7. package/dist/button-group.js +2 -2
  8. package/dist/button.js +2 -3
  9. package/dist/button.js.map +1 -1
  10. package/dist/card.js +1 -1
  11. package/dist/card.js.map +1 -1
  12. package/dist/code-highlighter.js +34 -9
  13. package/dist/code-highlighter.js.map +1 -1
  14. package/dist/custom-elements-jsdocs.json +4973 -3553
  15. package/dist/custom-elements.json +7899 -6346
  16. package/dist/{flow-designer-node-XMe-jlKg.js → flow-designer-node-BWrPuxAR.js} +2 -2
  17. package/dist/flow-designer-node-BWrPuxAR.js.map +1 -0
  18. package/dist/flow-designer-node.js +1 -1
  19. package/dist/flow-designer.js +1402 -8
  20. package/dist/flow-designer.js.map +1 -1
  21. package/dist/html-editor.js +27245 -87
  22. package/dist/html-editor.js.map +1 -1
  23. package/dist/icon-CueRR7wx.js +260 -0
  24. package/dist/icon-CueRR7wx.js.map +1 -0
  25. package/dist/{icon-button-CK1ZuE-2.js → icon-button-ohxHhy4t.js} +2 -2
  26. package/dist/{icon-button-CK1ZuE-2.js.map → icon-button-ohxHhy4t.js.map} +1 -1
  27. package/dist/index.js +10 -9
  28. package/dist/index.js.map +1 -1
  29. package/dist/modal.js +12 -18
  30. package/dist/modal.js.map +1 -1
  31. package/dist/{navigation-rail-DyO0oAZU.js → navigation-rail-CD7IrqbN.js} +952 -279
  32. package/dist/navigation-rail-CD7IrqbN.js.map +1 -0
  33. package/dist/peacock-loader.js +39 -30
  34. package/dist/peacock-loader.js.map +1 -1
  35. package/dist/{popover-NC7b1lTq.js → popover-DUPmMVWS.js} +9 -4
  36. package/dist/{popover-NC7b1lTq.js.map → popover-DUPmMVWS.js.map} +1 -1
  37. package/dist/popover.js +1 -1
  38. package/dist/src/__controllers/floating-controller.d.ts +1 -0
  39. package/dist/src/avatar/avatar.d.ts +1 -1
  40. package/dist/src/breadcrumb/breadcrumb/breadcrumb.d.ts +0 -1
  41. package/dist/src/chip/chip/chip.d.ts +14 -11
  42. package/dist/src/chip/chip-set/chip-set.d.ts +20 -0
  43. package/dist/src/chip/chip-set/index.d.ts +1 -0
  44. package/dist/src/code-highlighter/code-highlighter.d.ts +4 -0
  45. package/dist/src/html-editor/html-editor.d.ts +44 -11
  46. package/dist/src/index.d.ts +3 -0
  47. package/dist/src/list/index.d.ts +2 -0
  48. package/dist/src/list/list-item.d.ts +35 -0
  49. package/dist/src/list/list.d.ts +28 -0
  50. package/dist/src/menu/menu/menu.d.ts +1 -0
  51. package/dist/src/modal/modal.d.ts +2 -8
  52. package/dist/src/navigation-rail/navigation-rail.d.ts +3 -7
  53. package/dist/src/number-field/number-field.d.ts +2 -2
  54. package/dist/src/svg/index.d.ts +1 -0
  55. package/dist/src/svg/svg.d.ts +38 -0
  56. package/dist/src/toolbar/toolbar.d.ts +3 -3
  57. package/dist/test/chip.test.d.ts +1 -0
  58. package/dist/toolbar.js +3 -3
  59. package/dist/toolbar.js.map +1 -1
  60. package/dist/tsconfig.tsbuildinfo +1 -1
  61. package/package.json +7 -1
  62. package/readme.md +3 -3
  63. package/scss/styles.scss +3 -3
  64. package/scss/tokens.css +1 -1
  65. package/src/__controllers/floating-controller.ts +9 -3
  66. package/src/avatar/avatar.scss +4 -4
  67. package/src/avatar/avatar.ts +1 -1
  68. package/src/breadcrumb/breadcrumb/breadcrumb.ts +0 -1
  69. package/src/button/button/button.scss +17 -17
  70. package/src/button/button/button.ts +1 -2
  71. package/src/card/card.ts +1 -1
  72. package/src/chip/chip/chip.scss +119 -45
  73. package/src/chip/chip/chip.ts +97 -38
  74. package/src/chip/chip-set/chip-set.scss +13 -0
  75. package/src/chip/chip-set/chip-set.ts +25 -0
  76. package/src/chip/chip-set/index.ts +1 -0
  77. package/src/code-highlighter/code-highlighter.ts +33 -6
  78. package/src/field/field.scss +1 -1
  79. package/src/flow-designer/flow-designer-node.ts +1 -1
  80. package/src/html-editor/html-editor.scss +44 -2
  81. package/src/html-editor/html-editor.ts +309 -94
  82. package/src/index.ts +3 -1
  83. package/src/list/index.ts +2 -0
  84. package/src/list/list-item.scss +111 -0
  85. package/src/list/list-item.ts +175 -0
  86. package/src/list/list.scss +24 -0
  87. package/src/list/list.ts +51 -0
  88. package/src/menu/menu/menu.ts +11 -0
  89. package/src/modal/modal.scss +10 -10
  90. package/src/modal/modal.ts +2 -8
  91. package/src/navigation-rail/navigation-rail-item.scss +7 -38
  92. package/src/navigation-rail/navigation-rail-item.ts +1 -2
  93. package/src/navigation-rail/navigation-rail.scss +17 -21
  94. package/src/navigation-rail/navigation-rail.ts +6 -9
  95. package/src/number-field/number-field.ts +2 -2
  96. package/src/peacock-loader.ts +36 -22
  97. package/src/svg/index.ts +1 -0
  98. package/src/svg/svg.scss +91 -0
  99. package/src/svg/svg.ts +160 -0
  100. package/src/toolbar/toolbar.ts +3 -3
  101. package/dist/flow-designer-dZnLJOQT.js +0 -1656
  102. package/dist/flow-designer-dZnLJOQT.js.map +0 -1
  103. package/dist/flow-designer-node-XMe-jlKg.js.map +0 -1
  104. package/dist/navigation-rail-DyO0oAZU.js.map +0 -1
  105. package/src/chip/chip/chip-colors.scss +0 -31
@@ -1,16 +1,1410 @@
1
- import './IndividualComponent-DUINtMGK.js';
2
- import './property-1psGvXOq.js';
3
- import './state-DwbEjqVk.js';
4
- import './query-QBcUV-L_.js';
1
+ import { _ as __decorate, I as IndividualComponent, i, b, A } from './IndividualComponent-DUINtMGK.js';
2
+ import { n } from './property-1psGvXOq.js';
3
+ import { r } from './state-DwbEjqVk.js';
4
+ import { e } from './query-QBcUV-L_.js';
5
5
  import './toolbar.js';
6
- import './icon-button-CK1ZuE-2.js';
7
- export { F as FlowDesigner } from './flow-designer-dZnLJOQT.js';
8
- import './flow-designer-node-XMe-jlKg.js';
6
+ import './icon-button-ohxHhy4t.js';
7
+ import './icon-CueRR7wx.js';
8
+ import { c as css_248z } from './flow-designer-node-BWrPuxAR.js';
9
9
  import './class-map-YU7g0o3B.js';
10
10
  import './directive-ZPhl09Yt.js';
11
11
  import './BaseButton-BNFAYn-S.js';
12
12
  import './dispatch-event-utils-CuEqjlPT.js';
13
13
  import './BaseHyperlinkMixin-BNuwbiEf.js';
14
- import './button-colors-Ccys3hvS.js';
14
+ import './button-colors-AvGh22Zn.js';
15
15
  import './unsafe-html-BsGUjx94.js';
16
+
17
+ /**
18
+ * Workflow utility functions for tree traversal and manipulation
19
+ */
20
+ /**
21
+ * Deep clone a workflow to ensure immutability
22
+ */
23
+ function cloneWorkflow(workflow) {
24
+ return JSON.parse(JSON.stringify(workflow));
25
+ }
26
+ /**
27
+ * Deep clone a workflow node
28
+ */
29
+ function cloneNode(node) {
30
+ return JSON.parse(JSON.stringify(node));
31
+ }
32
+ /**
33
+ * Find a node by ID anywhere in the workflow tree
34
+ */
35
+ function findNodeById(node, id) {
36
+ if (node.id === id)
37
+ return node;
38
+ // Search children
39
+ if (node.children) {
40
+ for (const child of node.children) {
41
+ const found = findNodeById(child, id);
42
+ if (found)
43
+ return found;
44
+ }
45
+ }
46
+ // Search tasks
47
+ if (node.tasks) {
48
+ for (const task of node.tasks) {
49
+ const found = findNodeById(task, id);
50
+ if (found)
51
+ return found;
52
+ }
53
+ }
54
+ // Search branches
55
+ if (node.branches) {
56
+ for (const branchNodes of Object.values(node.branches)) {
57
+ for (const branchNode of branchNodes) {
58
+ const found = findNodeById(branchNode, id);
59
+ if (found)
60
+ return found;
61
+ }
62
+ }
63
+ }
64
+ // Search join
65
+ if (node.join) {
66
+ const found = findNodeById(node.join, id);
67
+ if (found)
68
+ return found;
69
+ }
70
+ return null;
71
+ }
72
+ /**
73
+ * Remove a node by ID from the workflow tree
74
+ */
75
+ function removeNodeById(node, id) {
76
+ const result = cloneNode(node);
77
+ // Remove from children
78
+ if (result.children) {
79
+ result.children = result.children.filter((child) => {
80
+ if (child.id === id)
81
+ return false;
82
+ removeNodeById(child, id);
83
+ return true;
84
+ });
85
+ }
86
+ // Remove from tasks
87
+ if (result.tasks) {
88
+ result.tasks = result.tasks.filter((task) => {
89
+ if (task.id === id)
90
+ return false;
91
+ removeNodeById(task, id);
92
+ return true;
93
+ });
94
+ }
95
+ // Remove from branches
96
+ if (result.branches) {
97
+ for (const [branchKey, branchNodes] of Object.entries(result.branches)) {
98
+ result.branches[branchKey] = branchNodes.filter((branchNode) => {
99
+ if (branchNode.id === id)
100
+ return false;
101
+ removeNodeById(branchNode, id);
102
+ return true;
103
+ });
104
+ }
105
+ }
106
+ // Recursively clean empty nodes
107
+ for (const branchNode of result.children || []) {
108
+ removeNodeById(branchNode, id);
109
+ }
110
+ for (const taskNode of result.tasks || []) {
111
+ removeNodeById(taskNode, id);
112
+ }
113
+ for (const branchNodes of Object.values(result.branches || {})) {
114
+ for (const branchNode of branchNodes) {
115
+ removeNodeById(branchNode, id);
116
+ }
117
+ }
118
+ return result;
119
+ }
120
+ /**
121
+ * Insert a node into the workflow tree at a specific location
122
+ */
123
+ function insertNodeIntoWorkflow(parent, nodeToInsert, connectionType = 'child', branchKey) {
124
+ switch (connectionType) {
125
+ case 'child':
126
+ if (!parent.children)
127
+ parent.children = [];
128
+ parent.children.push(cloneNode(nodeToInsert));
129
+ break;
130
+ case 'task':
131
+ if (!parent.tasks)
132
+ parent.tasks = [];
133
+ parent.tasks.push(cloneNode(nodeToInsert));
134
+ break;
135
+ case 'branch':
136
+ if (!parent.branches)
137
+ parent.branches = {};
138
+ if (!branchKey)
139
+ branchKey = 'default';
140
+ if (!parent.branches[branchKey])
141
+ parent.branches[branchKey] = [];
142
+ parent.branches[branchKey].push(cloneNode(nodeToInsert));
143
+ break;
144
+ }
145
+ }
146
+ /**
147
+ * Collect all nodes in the workflow (depth-first)
148
+ */
149
+ function getAllNodes(node) {
150
+ const result = [node];
151
+ if (node.children) {
152
+ for (const child of node.children) {
153
+ result.push(...getAllNodes(child));
154
+ }
155
+ }
156
+ if (node.tasks) {
157
+ for (const task of node.tasks) {
158
+ result.push(...getAllNodes(task));
159
+ }
160
+ }
161
+ if (node.branches) {
162
+ for (const branchNodes of Object.values(node.branches)) {
163
+ for (const branchNode of branchNodes) {
164
+ result.push(...getAllNodes(branchNode));
165
+ }
166
+ }
167
+ }
168
+ if (node.join) {
169
+ result.push(...getAllNodes(node.join));
170
+ }
171
+ return result;
172
+ }
173
+ /**
174
+ * Get all parent node IDs for a given node (path from root to node)
175
+ */
176
+ function getNodePath(rootNode, targetId) {
177
+ const path = [];
178
+ function traverse(node) {
179
+ path.push(node.id);
180
+ if (node.id === targetId)
181
+ return true;
182
+ // Search children
183
+ if (node.children) {
184
+ for (const child of node.children) {
185
+ if (traverse(child))
186
+ return true;
187
+ }
188
+ }
189
+ // Search tasks
190
+ if (node.tasks) {
191
+ for (const task of node.tasks) {
192
+ if (traverse(task))
193
+ return true;
194
+ }
195
+ }
196
+ // Search branches
197
+ if (node.branches) {
198
+ for (const branchNodes of Object.values(node.branches)) {
199
+ for (const branchNode of branchNodes) {
200
+ if (traverse(branchNode))
201
+ return true;
202
+ }
203
+ }
204
+ }
205
+ // Search join
206
+ if (node.join) {
207
+ if (traverse(node.join))
208
+ return true;
209
+ }
210
+ path.pop();
211
+ return false;
212
+ }
213
+ traverse(rootNode);
214
+ return path;
215
+ }
216
+ /**
217
+ * Check if a node is a descendant of another node
218
+ */
219
+ function isDescendant(rootNode, potentialParentId, nodeId) {
220
+ const path = getNodePath(rootNode, nodeId);
221
+ return path.includes(potentialParentId);
222
+ }
223
+
224
+ /**
225
+ * Add Node Command
226
+ */
227
+ class AddNodeCommand {
228
+ constructor(nodeToAdd, parentNodeId, connectionType = 'child', branchKey) {
229
+ this.nodeToAdd = nodeToAdd;
230
+ this.parentNodeId = parentNodeId;
231
+ this.connectionType = connectionType;
232
+ this.branchKey = branchKey;
233
+ this.description = 'Add node';
234
+ }
235
+ execute(workflow) {
236
+ const result = cloneWorkflow(workflow);
237
+ const parent = findNodeById(result.nodes, this.parentNodeId);
238
+ if (!parent)
239
+ return workflow; // Validation in parent component
240
+ insertNodeIntoWorkflow(parent, this.nodeToAdd, this.connectionType, this.branchKey);
241
+ return result;
242
+ }
243
+ undo(workflow) {
244
+ const result = cloneWorkflow(workflow);
245
+ removeNodeById(result.nodes, this.nodeToAdd.id);
246
+ return result;
247
+ }
248
+ }
249
+ /**
250
+ * Delete Node Command
251
+ */
252
+ class DeleteNodeCommand {
253
+ constructor(nodeId, workflow) {
254
+ this.nodeId = nodeId;
255
+ this.description = 'Delete node';
256
+ this.deletedNode = null;
257
+ this.parentReference = null;
258
+ if (workflow) {
259
+ this.captureNodeContext(workflow);
260
+ }
261
+ }
262
+ captureNodeContext(workflow) {
263
+ const node = findNodeById(workflow.nodes, this.nodeId);
264
+ if (!node)
265
+ return;
266
+ this.deletedNode = cloneWorkflow({ workflow_id: '', nodes: node }).nodes;
267
+ // Find parent reference
268
+ this.findParentReference(workflow.nodes);
269
+ }
270
+ findParentReference(node) {
271
+ if (node.children) {
272
+ const idx = node.children.findIndex((n) => n.id === this.nodeId);
273
+ if (idx !== -1) {
274
+ this.parentReference = {
275
+ parentId: node.id,
276
+ connectionType: 'child',
277
+ };
278
+ return;
279
+ }
280
+ for (const child of node.children) {
281
+ this.findParentReference(child);
282
+ }
283
+ }
284
+ if (node.tasks) {
285
+ const idx = node.tasks.findIndex((n) => n.id === this.nodeId);
286
+ if (idx !== -1) {
287
+ this.parentReference = {
288
+ parentId: node.id,
289
+ connectionType: 'task',
290
+ };
291
+ return;
292
+ }
293
+ for (const task of node.tasks) {
294
+ this.findParentReference(task);
295
+ }
296
+ }
297
+ if (node.branches) {
298
+ for (const [branchKey, branchNodes] of Object.entries(node.branches)) {
299
+ const idx = branchNodes.findIndex((n) => n.id === this.nodeId);
300
+ if (idx !== -1) {
301
+ this.parentReference = {
302
+ parentId: node.id,
303
+ connectionType: 'branch',
304
+ branchKey,
305
+ };
306
+ return;
307
+ }
308
+ for (const branchNode of branchNodes) {
309
+ this.findParentReference(branchNode);
310
+ }
311
+ }
312
+ }
313
+ }
314
+ execute(workflow) {
315
+ const result = cloneWorkflow(workflow);
316
+ removeNodeById(result.nodes, this.nodeId);
317
+ return result;
318
+ }
319
+ undo(workflow) {
320
+ if (!this.deletedNode || !this.parentReference)
321
+ return workflow;
322
+ const result = cloneWorkflow(workflow);
323
+ const parent = findNodeById(result.nodes, this.parentReference.parentId);
324
+ if (!parent)
325
+ return workflow;
326
+ insertNodeIntoWorkflow(parent, this.deletedNode, this.parentReference.connectionType, this.parentReference.branchKey);
327
+ return result;
328
+ }
329
+ }
330
+ /**
331
+ * Edit Node Command
332
+ */
333
+ class EditNodeCommand {
334
+ constructor(nodeId, updates, workflow) {
335
+ this.nodeId = nodeId;
336
+ this.updates = updates;
337
+ this.description = 'Edit node';
338
+ this.previousState = {};
339
+ if (workflow) {
340
+ const node = findNodeById(workflow.nodes, nodeId);
341
+ if (node) {
342
+ // Store only edited fields
343
+ Object.keys(updates).forEach((key) => {
344
+ this.previousState[key] = node[key];
345
+ });
346
+ }
347
+ }
348
+ }
349
+ execute(workflow) {
350
+ const result = cloneWorkflow(workflow);
351
+ const node = findNodeById(result.nodes, this.nodeId);
352
+ if (!node)
353
+ return workflow;
354
+ Object.assign(node, this.updates);
355
+ return result;
356
+ }
357
+ undo(workflow) {
358
+ const result = cloneWorkflow(workflow);
359
+ const node = findNodeById(result.nodes, this.nodeId);
360
+ if (!node)
361
+ return workflow;
362
+ Object.assign(node, this.previousState);
363
+ return result;
364
+ }
365
+ }
366
+ /**
367
+ * Move Node Command - reorder in array or change parent
368
+ */
369
+ class MoveNodeCommand {
370
+ constructor(nodeId, newParentId, newIndex, newConnectionType = 'child', newBranchKey, workflow) {
371
+ this.nodeId = nodeId;
372
+ this.newParentId = newParentId;
373
+ this.newIndex = newIndex;
374
+ this.newConnectionType = newConnectionType;
375
+ this.newBranchKey = newBranchKey;
376
+ this.description = 'Move node';
377
+ this.previousState = null;
378
+ if (workflow) {
379
+ this.captureCurrentPosition(workflow);
380
+ }
381
+ }
382
+ captureCurrentPosition(workflow) {
383
+ // Store current parent/position for undo
384
+ // Implementation depends on finding current parent location
385
+ }
386
+ execute(workflow) {
387
+ // Remove from old parent, insert at new parent
388
+ let result = cloneWorkflow(workflow);
389
+ result.nodes = removeNodeById(result.nodes, this.nodeId);
390
+ const newParent = findNodeById(result.nodes, this.newParentId);
391
+ if (!newParent)
392
+ return workflow;
393
+ const node = findNodeById(workflow.nodes, this.nodeId);
394
+ if (!node)
395
+ return workflow;
396
+ insertNodeIntoWorkflow(newParent, node, this.newConnectionType, this.newBranchKey);
397
+ return result;
398
+ }
399
+ undo(workflow) {
400
+ // Restore to previous position
401
+ if (!this.previousState)
402
+ return workflow;
403
+ let result = cloneWorkflow(workflow);
404
+ result.nodes = removeNodeById(result.nodes, this.nodeId);
405
+ const prevParent = findNodeById(result.nodes, this.previousState.parentId);
406
+ if (!prevParent)
407
+ return workflow;
408
+ const node = findNodeById(workflow.nodes, this.nodeId);
409
+ if (!node)
410
+ return workflow;
411
+ insertNodeIntoWorkflow(prevParent, node, this.previousState.connectionType, this.previousState.branchKey);
412
+ return result;
413
+ }
414
+ }
415
+
416
+ const NODE_WIDTH = 200;
417
+ const NODE_HEIGHT = 100;
418
+ const HORIZONTAL_GAP = 60; // Gap between depth levels (columns)
419
+ const VERTICAL_GAP = 40; // Gap between lanes
420
+ const LANE_HEIGHT = 140; // Height of each swimlane row
421
+ const LANE_HEADER_HEIGHT = 84; // Top offset so first row is not clipped by floating UI
422
+ class SwimlaneLayout {
423
+ /**
424
+ * Calculate layout positions for all nodes in a workflow
425
+ */
426
+ static calculateLayout(rootNode) {
427
+ const layoutNodes = [];
428
+ const lanes = new Map(); // lane -> nodes in that lane
429
+ // First pass: assign lanes and depths
430
+ this._traverseAndAssignLanes(rootNode, null, 'main', 0, layoutNodes, lanes);
431
+ // Second pass: calculate positions
432
+ const positionedNodes = [];
433
+ const nodePositions = new Map();
434
+ for (const layoutNode of layoutNodes) {
435
+ const x = layoutNode.depth * (NODE_WIDTH + HORIZONTAL_GAP) + HORIZONTAL_GAP;
436
+ const laneIndex = Array.from(lanes.keys()).indexOf(layoutNode.lane);
437
+ const y = laneIndex * (LANE_HEIGHT + VERTICAL_GAP) + LANE_HEADER_HEIGHT;
438
+ nodePositions.set(layoutNode.node.id, { x, y });
439
+ const positioned = {
440
+ node: layoutNode.node,
441
+ x,
442
+ y,
443
+ width: NODE_WIDTH,
444
+ height: NODE_HEIGHT,
445
+ lane: layoutNode.lane,
446
+ depth: layoutNode.depth,
447
+ branchPath: layoutNode.branchPath,
448
+ parentId: layoutNode.parent?.id,
449
+ connectorPoints: [],
450
+ };
451
+ positionedNodes.push(positioned);
452
+ }
453
+ // Third pass: calculate connector points
454
+ this._calculateConnectors(positionedNodes, nodePositions);
455
+ return positionedNodes;
456
+ }
457
+ /**
458
+ * Traverse workflow tree and assign lane/depth to each node
459
+ */
460
+ static _traverseAndAssignLanes(node, parent, baseLane, depth, layoutNodes, lanes) {
461
+ const layoutNode = {
462
+ node,
463
+ parent,
464
+ lane: baseLane,
465
+ depth,
466
+ width: NODE_WIDTH,
467
+ height: NODE_HEIGHT,
468
+ };
469
+ // Add to layout nodes
470
+ layoutNodes.push(layoutNode);
471
+ // Register in lanes map
472
+ if (!lanes.has(baseLane)) {
473
+ lanes.set(baseLane, []);
474
+ }
475
+ lanes.get(baseLane).push(layoutNode);
476
+ // Process children
477
+ if (node.children && node.children.length > 0) {
478
+ for (const child of node.children) {
479
+ this._traverseAndAssignLanes(child, node, baseLane, depth + 1, layoutNodes, lanes);
480
+ }
481
+ }
482
+ // Process decision branches into separate swimlanes
483
+ if (node.branches) {
484
+ let branchIndex = 0;
485
+ for (const [branchKey, branchNodes] of Object.entries(node.branches)) {
486
+ const branchLane = `${baseLane}_${branchKey}_${branchIndex}`;
487
+ for (const branchNode of branchNodes) {
488
+ this._traverseAndAssignLanes(branchNode, node, branchLane, depth + 1, layoutNodes, lanes);
489
+ }
490
+ branchIndex++;
491
+ }
492
+ }
493
+ // Process fork into parallel lanes
494
+ if (node.type === 'fork' && node.tasks) {
495
+ let taskIndex = 0;
496
+ for (const task of node.tasks) {
497
+ const parallelLane = `${baseLane}_parallel_${taskIndex}`;
498
+ this._traverseAndAssignLanes(task, node, parallelLane, depth + 1, layoutNodes, lanes);
499
+ taskIndex++;
500
+ }
501
+ }
502
+ // Process fork join node
503
+ if (node.type === 'fork' && node.join) {
504
+ // Join node goes back to main lane at next depth
505
+ this._traverseAndAssignLanes(node.join, node, baseLane, depth + 2, layoutNodes, lanes);
506
+ }
507
+ // Process task nodes (used in forks)
508
+ if (node.tasks && node.type !== 'fork') {
509
+ for (const task of node.tasks) {
510
+ this._traverseAndAssignLanes(task, node, baseLane, depth + 1, layoutNodes, lanes);
511
+ }
512
+ }
513
+ }
514
+ /**
515
+ * Calculate SVG connector points between nodes
516
+ */
517
+ static _calculateConnectors(positionedNodes, nodePositions) {
518
+ const nodeMap = new Map(positionedNodes.map((n) => [n.node.id, n]));
519
+ for (const positioned of positionedNodes) {
520
+ const connectors = [];
521
+ const nodeMiddleRight = {
522
+ x: positioned.x + positioned.width,
523
+ y: positioned.y + positioned.height / 2,
524
+ };
525
+ // Connect to children (sequential)
526
+ if (positioned.node.children && positioned.node.children.length > 0) {
527
+ for (const child of positioned.node.children) {
528
+ const childPos = nodeMap.get(child.id);
529
+ if (childPos) {
530
+ connectors.push({
531
+ from: nodeMiddleRight,
532
+ to: {
533
+ x: childPos.x,
534
+ y: childPos.y + childPos.height / 2,
535
+ },
536
+ type: 'straight',
537
+ });
538
+ }
539
+ }
540
+ }
541
+ // Connect to branches (decision)
542
+ if (positioned.node.branches) {
543
+ for (const branchNodes of Object.values(positioned.node.branches)) {
544
+ for (const branchNode of branchNodes) {
545
+ const childPos = nodeMap.get(branchNode.id);
546
+ if (childPos) {
547
+ // Curved path to branch
548
+ connectors.push({
549
+ from: nodeMiddleRight,
550
+ to: {
551
+ x: childPos.x,
552
+ y: childPos.y + childPos.height / 2,
553
+ },
554
+ type: 'branch',
555
+ });
556
+ }
557
+ }
558
+ }
559
+ }
560
+ // Connect fork to parallel tasks
561
+ if (positioned.node.type === 'fork' && positioned.node.tasks) {
562
+ for (const task of positioned.node.tasks) {
563
+ const taskPos = nodeMap.get(task.id);
564
+ if (taskPos) {
565
+ connectors.push({
566
+ from: nodeMiddleRight,
567
+ to: {
568
+ x: taskPos.x,
569
+ y: taskPos.y + taskPos.height / 2,
570
+ },
571
+ type: 'fork',
572
+ });
573
+ }
574
+ }
575
+ }
576
+ // Connect to join
577
+ if (positioned.node.type === 'fork' && positioned.node.join) {
578
+ const joinPos = nodeMap.get(positioned.node.join.id);
579
+ if (joinPos) {
580
+ connectors.push({
581
+ from: nodeMiddleRight,
582
+ to: {
583
+ x: joinPos.x,
584
+ y: joinPos.y + joinPos.height / 2,
585
+ },
586
+ type: 'join',
587
+ });
588
+ }
589
+ }
590
+ // Connect loop back
591
+ if (positioned.node.type === 'loop_end' && positioned.node.target_id) {
592
+ const targetPos = nodeMap.get(positioned.node.target_id);
593
+ if (targetPos) {
594
+ connectors.push({
595
+ from: {
596
+ x: positioned.x + positioned.width,
597
+ y: positioned.y + positioned.height / 2,
598
+ },
599
+ to: {
600
+ x: targetPos.x,
601
+ y: targetPos.y + targetPos.height / 2,
602
+ },
603
+ type: 'curved',
604
+ });
605
+ }
606
+ }
607
+ positioned.connectorPoints = connectors;
608
+ }
609
+ }
610
+ /**
611
+ * Get swimlane configurations for rendering
612
+ */
613
+ static getSwimlanes(positionedNodes) {
614
+ const swimlanesMap = new Map();
615
+ for (const node of positionedNodes) {
616
+ if (!swimlanesMap.has(node.lane)) {
617
+ swimlanesMap.set(node.lane, []);
618
+ }
619
+ swimlanesMap.get(node.lane).push(node);
620
+ }
621
+ const swimlanes = [];
622
+ for (const [laneId, nodes] of swimlanesMap.entries()) {
623
+ const isParallel = laneId.includes('parallel');
624
+ const name = this._getSwimlaneName(laneId);
625
+ swimlanes.push({
626
+ id: laneId,
627
+ name,
628
+ nodes,
629
+ isParallel,
630
+ });
631
+ }
632
+ return swimlanes;
633
+ }
634
+ /**
635
+ * Generate human-readable swimlane name
636
+ */
637
+ static _getSwimlaneName(laneId) {
638
+ if (laneId === 'main')
639
+ return 'Main Flow';
640
+ if (laneId.includes('yes'))
641
+ return 'Yes Path';
642
+ if (laneId.includes('no'))
643
+ return 'No Path';
644
+ if (laneId.includes('parallel')) {
645
+ const match = laneId.match(/parallel_(\d+)/);
646
+ if (match)
647
+ return `Parallel Task ${parseInt(match[1]) + 1}`;
648
+ }
649
+ return laneId;
650
+ }
651
+ /**
652
+ * Calculate canvas bounds for sizing
653
+ */
654
+ static getCanvasBounds(positionedNodes) {
655
+ if (positionedNodes.length === 0) {
656
+ return { width: 600, height: 400 };
657
+ }
658
+ let maxX = 0;
659
+ let maxY = 0;
660
+ for (const node of positionedNodes) {
661
+ maxX = Math.max(maxX, node.x + node.width + HORIZONTAL_GAP);
662
+ maxY = Math.max(maxY, node.y + node.height + VERTICAL_GAP);
663
+ }
664
+ return {
665
+ width: Math.max(600, maxX),
666
+ height: Math.max(400, maxY),
667
+ };
668
+ }
669
+ }
670
+
671
+ /**
672
+ * Workflow validation - checks for common errors and inconsistencies
673
+ */
674
+ class WorkflowValidator {
675
+ /**
676
+ * Validate entire workflow
677
+ */
678
+ static validate(workflow) {
679
+ const errors = [];
680
+ // Check root node exists and is a trigger
681
+ if (!workflow.nodes) {
682
+ errors.push({
683
+ nodeId: 'root',
684
+ type: 'orphaned_node',
685
+ message: 'Workflow has no root node',
686
+ severity: 'error',
687
+ });
688
+ return errors;
689
+ }
690
+ // Validate all nodes
691
+ const allNodes = getAllNodes(workflow.nodes);
692
+ // Check for circular loops
693
+ errors.push(...this._checkCircularLoops(workflow.nodes, allNodes));
694
+ // Check for orphaned nodes
695
+ errors.push(...this._checkOrphanedNodes(workflow.nodes, allNodes));
696
+ // Check valid branches
697
+ errors.push(...this._checkValidBranches(workflow.nodes, allNodes));
698
+ // Check missing targets
699
+ errors.push(...this._checkMissingTargets(workflow, allNodes));
700
+ // Check invalid fork/join pairs
701
+ errors.push(...this._checkForkJoinPairs(workflow.nodes, allNodes));
702
+ return errors;
703
+ }
704
+ /**
705
+ * Detect circular loop references
706
+ * A loop_end cannot point to a node that is its own descendant (after the loop_start)
707
+ */
708
+ static _checkCircularLoops(rootNode, allNodes) {
709
+ const errors = [];
710
+ for (const node of allNodes) {
711
+ // Only check loop_end nodes
712
+ if (node.type !== 'loop_end')
713
+ continue;
714
+ const targetId = node.target_id;
715
+ if (!targetId)
716
+ continue;
717
+ // Check if target exists
718
+ const targetNode = findNodeById(rootNode, targetId);
719
+ if (!targetNode)
720
+ continue;
721
+ // If loop_end is a descendant of its target, it's circular
722
+ if (isDescendant(rootNode, targetId, node.id)) ;
723
+ // However, if the loop_end's target is a descendant of the loop_end, that's circular
724
+ if (isDescendant(rootNode, node.id, targetId)) {
725
+ errors.push({
726
+ nodeId: node.id,
727
+ type: 'circular_loop',
728
+ message: `Loop cannot point to a node (${targetId}) that executes after the loop_end`,
729
+ severity: 'error',
730
+ });
731
+ }
732
+ }
733
+ return errors;
734
+ }
735
+ /**
736
+ * Check for orphaned nodes (not reachable from root)
737
+ */
738
+ static _checkOrphanedNodes(rootNode, allNodes) {
739
+ const errors = [];
740
+ const paths = new Map();
741
+ for (const node of allNodes) {
742
+ const path = getNodePath(rootNode, node.id);
743
+ if (path.length === 0 && node.id !== rootNode.id) {
744
+ errors.push({
745
+ nodeId: node.id,
746
+ type: 'orphaned_node',
747
+ message: `Node "${node.label}" is not reachable from the root trigger`,
748
+ severity: 'error',
749
+ });
750
+ }
751
+ paths.set(node.id, path);
752
+ }
753
+ // Check nodes in branches - all branch paths must be reachable
754
+ // This is typically valid by structure, but warn if branch has no exit
755
+ for (const node of allNodes) {
756
+ if (!node.branches)
757
+ continue;
758
+ for (const [branchKey, branchNodes] of Object.entries(node.branches)) {
759
+ if (branchNodes.length === 0) {
760
+ errors.push({
761
+ nodeId: node.id,
762
+ type: 'invalid_branch',
763
+ message: `Branch "${branchKey}" is empty - no nodes to execute`,
764
+ severity: 'warning',
765
+ });
766
+ }
767
+ }
768
+ }
769
+ return errors;
770
+ }
771
+ /**
772
+ * Check that decision nodes have valid branches
773
+ */
774
+ static _checkValidBranches(rootNode, allNodes) {
775
+ const errors = [];
776
+ for (const node of allNodes) {
777
+ if (node.type !== 'decision')
778
+ continue;
779
+ if (!node.branches) {
780
+ errors.push({
781
+ nodeId: node.id,
782
+ type: 'invalid_branch',
783
+ message: `Decision node "${node.label}" has no branches defined`,
784
+ severity: 'error',
785
+ });
786
+ continue;
787
+ }
788
+ // Standard decision should have "yes" and "no"
789
+ const branchKeys = Object.keys(node.branches);
790
+ if (!branchKeys.includes('yes') || !branchKeys.includes('no')) {
791
+ errors.push({
792
+ nodeId: node.id,
793
+ type: 'invalid_branch',
794
+ message: `Decision node "${node.label}" should have "yes" and "no" branches`,
795
+ severity: 'warning',
796
+ });
797
+ }
798
+ // Check for empty branches
799
+ for (const [branchKey, branchNodes] of Object.entries(node.branches)) {
800
+ if (branchNodes.length === 0) {
801
+ errors.push({
802
+ nodeId: node.id,
803
+ type: 'invalid_branch',
804
+ message: `Decision branch "${branchKey}" is empty`,
805
+ severity: 'warning',
806
+ });
807
+ }
808
+ }
809
+ }
810
+ return errors;
811
+ }
812
+ /**
813
+ * Check that loop_end nodes reference valid loop_start nodes
814
+ */
815
+ static _checkMissingTargets(workflow, allNodes) {
816
+ const errors = [];
817
+ for (const node of allNodes) {
818
+ if (node.type === 'loop_end') {
819
+ if (!node.target_id) {
820
+ errors.push({
821
+ nodeId: node.id,
822
+ type: 'missing_target',
823
+ message: `Loop end "${node.label}" does not specify a target loop_start`,
824
+ severity: 'error',
825
+ });
826
+ }
827
+ else {
828
+ const target = findNodeById(workflow.nodes, node.target_id);
829
+ if (!target || target.type !== 'loop_start') {
830
+ errors.push({
831
+ nodeId: node.id,
832
+ type: 'missing_target',
833
+ message: `Loop end "${node.label}" references non-existent or non-loop_start node "${node.target_id}"`,
834
+ severity: 'error',
835
+ });
836
+ }
837
+ }
838
+ }
839
+ }
840
+ return errors;
841
+ }
842
+ /**
843
+ * Check that fork nodes have corresponding join nodes
844
+ */
845
+ static _checkForkJoinPairs(rootNode, allNodes) {
846
+ const errors = [];
847
+ for (const node of allNodes) {
848
+ if (node.type === 'fork') {
849
+ if (!node.join) {
850
+ errors.push({
851
+ nodeId: node.id,
852
+ type: 'invalid_fork_join',
853
+ message: `Fork node "${node.label}" does not have a corresponding join node`,
854
+ severity: 'error',
855
+ });
856
+ }
857
+ else if (node.join.type !== 'join') {
858
+ errors.push({
859
+ nodeId: node.id,
860
+ type: 'invalid_fork_join',
861
+ message: `Fork node "${node.label}" join is not a join node`,
862
+ severity: 'error',
863
+ });
864
+ }
865
+ if (!node.tasks || node.tasks.length === 0) {
866
+ errors.push({
867
+ nodeId: node.id,
868
+ type: 'invalid_fork_join',
869
+ message: `Fork node "${node.label}" has no parallel tasks`,
870
+ severity: 'warning',
871
+ });
872
+ }
873
+ }
874
+ }
875
+ return errors;
876
+ }
877
+ /**
878
+ * Check if workflow would create a valid execution path
879
+ */
880
+ static isExecutable(workflow) {
881
+ const errors = this.validate(workflow);
882
+ return errors.filter((e) => e.severity === 'error').length === 0;
883
+ }
884
+ /**
885
+ * Get validation warnings only
886
+ */
887
+ static getWarnings(workflow) {
888
+ const errors = this.validate(workflow);
889
+ return errors.filter((e) => e.severity === 'warning');
890
+ }
891
+ /**
892
+ * Get validation errors only
893
+ */
894
+ static getErrors(workflow) {
895
+ const errors = this.validate(workflow);
896
+ return errors.filter((e) => e.severity === 'error');
897
+ }
898
+ }
899
+
900
+ /**
901
+ * @label Flow Designer
902
+ * @tag wc-flow-designer
903
+ * @rawTag flow-designer
904
+ * @summary Low-code business process flow designer with swimlane layout, undo/redo, and interactive editing.
905
+ *
906
+ * @cssprop --flow-designer-height - Height of the flow designer container. Defaults to 400px.
907
+ * @cssprop --flow-designer-border-color - Border color of the flow designer. Defaults to outline-variant.
908
+ * @cssprop --flow-designer-background - Background color of the designer. Defaults to surface.
909
+ * @cssprop --flow-designer-border-radius - Corner radius. Defaults to medium shape.
910
+ * @cssprop --flow-designer-action-bar-bg - Background color of the action bar. Defaults to surface-container.
911
+ *
912
+ * @example
913
+ * ```html
914
+ * <wc-flow-designer id="editor"></wc-flow-designer>
915
+ * <script>
916
+ * const workflow = {
917
+ * workflow_id: "demo",
918
+ * nodes: {
919
+ * id: "node_1",
920
+ * type: "trigger",
921
+ * label: "Start"
922
+ * }
923
+ * };
924
+ * document.querySelector('#editor').workflow = workflow;
925
+ * </script>
926
+ * ```
927
+ */
928
+ let FlowDesigner = class FlowDesigner extends i {
929
+ constructor() {
930
+ super(...arguments);
931
+ /**
932
+ * The workflow definition to display and edit
933
+ */
934
+ this.workflow = { workflow_id: '', nodes: { id: 'root', type: 'trigger', label: 'Start' } };
935
+ /**
936
+ * Whether the flow designer is in read-only mode
937
+ */
938
+ this.readonly = false;
939
+ /**
940
+ * Whether the flow designer is disabled
941
+ */
942
+ this.disabled = false;
943
+ /**
944
+ * Show validation errors/warnings
945
+ */
946
+ this.showValidation = false;
947
+ this._editor = {
948
+ selectedNodeId: null,
949
+ isEditing: false,
950
+ editingNode: null,
951
+ hoveredNodeId: null,
952
+ isDragging: false,
953
+ draggedNodeId: null,
954
+ zoom: 1,
955
+ panX: 0,
956
+ panY: 0,
957
+ };
958
+ this._positionedNodes = [];
959
+ this._history = [];
960
+ this._historyIndex = -1;
961
+ this._isDragScrolling = false;
962
+ this._dragStartX = 0;
963
+ this._dragStartY = 0;
964
+ this._scrollStartX = 0;
965
+ this._scrollStartY = 0;
966
+ this._handleKeyDown = (event) => {
967
+ if (this.disabled || this.readonly)
968
+ return;
969
+ if (event.ctrlKey || event.metaKey) {
970
+ if (event.key === 'z') {
971
+ event.preventDefault();
972
+ this.undo();
973
+ }
974
+ else if (event.key === 'y') {
975
+ event.preventDefault();
976
+ this.redo();
977
+ }
978
+ }
979
+ if (event.key === 'Delete' && this._editor.selectedNodeId) {
980
+ event.preventDefault();
981
+ this.deleteNode(this._editor.selectedNodeId);
982
+ }
983
+ };
984
+ this._handleMouseUp = () => {
985
+ this._isDragScrolling = false;
986
+ };
987
+ this._handleCanvasMouseDown = (e) => {
988
+ if (this.disabled)
989
+ return;
990
+ if (e.target === this.scrollElm || e.target.classList.contains('canvas-container')) {
991
+ this._isDragScrolling = true;
992
+ this._dragStartX = e.clientX;
993
+ this._dragStartY = e.clientY;
994
+ if (this.scrollElm) {
995
+ this._scrollStartX = this.scrollElm.scrollLeft;
996
+ this._scrollStartY = this.scrollElm.scrollTop;
997
+ }
998
+ }
999
+ };
1000
+ this._handleCanvasMouseMove = (e) => {
1001
+ if (!this._isDragScrolling || !this.scrollElm)
1002
+ return;
1003
+ const deltaX = e.clientX - this._dragStartX;
1004
+ const deltaY = e.clientY - this._dragStartY;
1005
+ this.scrollElm.scrollLeft = this._scrollStartX - deltaX;
1006
+ this.scrollElm.scrollTop = this._scrollStartY - deltaY;
1007
+ };
1008
+ this._handleNodeClick = (e) => {
1009
+ const nodeId = e.detail.nodeId;
1010
+ this._editor.selectedNodeId = nodeId;
1011
+ this.requestUpdate();
1012
+ };
1013
+ this._handleNodeDelete = (e) => {
1014
+ const nodeId = e.detail.nodeId;
1015
+ this.deleteNode(nodeId);
1016
+ };
1017
+ this._handleNodeEdit = (e) => {
1018
+ const nodeId = e.detail.nodeId;
1019
+ this._editor.selectedNodeId = nodeId;
1020
+ this._editor.isEditing = true;
1021
+ this.requestUpdate();
1022
+ };
1023
+ this._handleZoomIn = () => {
1024
+ this._editor.zoom = Math.min(2, this._editor.zoom + 0.1);
1025
+ this.requestUpdate();
1026
+ };
1027
+ this._handleZoomOut = () => {
1028
+ this._editor.zoom = Math.max(0.5, this._editor.zoom - 0.1);
1029
+ this.requestUpdate();
1030
+ };
1031
+ }
1032
+ connectedCallback() {
1033
+ super.connectedCallback();
1034
+ window.addEventListener('mouseup', this._handleMouseUp);
1035
+ window.addEventListener('keydown', this._handleKeyDown);
1036
+ this._recalculateLayout();
1037
+ }
1038
+ disconnectedCallback() {
1039
+ window.removeEventListener('mouseup', this._handleMouseUp);
1040
+ window.removeEventListener('keydown', this._handleKeyDown);
1041
+ super.disconnectedCallback();
1042
+ }
1043
+ willUpdate() {
1044
+ this._recalculateLayout();
1045
+ }
1046
+ /**
1047
+ * Recalculate layout when workflow changes
1048
+ */
1049
+ _recalculateLayout() {
1050
+ if (!this.workflow?.nodes)
1051
+ return;
1052
+ this._positionedNodes = SwimlaneLayout.calculateLayout(this.workflow.nodes);
1053
+ }
1054
+ /**
1055
+ * Add a new node
1056
+ */
1057
+ addNode(newNode, parentNodeId, connectionType = 'child', branchKey) {
1058
+ const command = new AddNodeCommand(newNode, parentNodeId, connectionType, branchKey);
1059
+ this._executeCommand(command);
1060
+ }
1061
+ /**
1062
+ * Delete a node by ID
1063
+ */
1064
+ deleteNode(nodeId) {
1065
+ const command = new DeleteNodeCommand(nodeId, this.workflow);
1066
+ this._executeCommand(command);
1067
+ }
1068
+ /**
1069
+ * Edit a node
1070
+ */
1071
+ editNode(nodeId, updates) {
1072
+ const command = new EditNodeCommand(nodeId, updates, this.workflow);
1073
+ this._executeCommand(command);
1074
+ }
1075
+ /**
1076
+ * Move a node to a different parent/position
1077
+ */
1078
+ moveNode(nodeId, newParentId, newIndex, connectionType = 'child', branchKey) {
1079
+ const command = new MoveNodeCommand(nodeId, newParentId, newIndex, connectionType, branchKey, this.workflow);
1080
+ this._executeCommand(command);
1081
+ }
1082
+ /**
1083
+ * Execute a command and add to history
1084
+ */
1085
+ _executeCommand(command) {
1086
+ const newWorkflow = command.execute(this.workflow);
1087
+ // Validate workflow after change
1088
+ const errors = WorkflowValidator.validate(newWorkflow);
1089
+ const hasErrors = errors.some((e) => e.severity === 'error');
1090
+ if (hasErrors && !confirm('Workflow has errors. Continue anyway?')) {
1091
+ return;
1092
+ }
1093
+ // Add to history
1094
+ this._history = this._history.slice(0, this._historyIndex + 1);
1095
+ this._history.push({
1096
+ command,
1097
+ workflow: newWorkflow,
1098
+ timestamp: Date.now(),
1099
+ });
1100
+ this._historyIndex++;
1101
+ // Update workflow
1102
+ this.workflow = newWorkflow;
1103
+ // Emit change event
1104
+ this._emitWorkflowChange('node-edited', undefined);
1105
+ }
1106
+ /**
1107
+ * Undo last operation
1108
+ */
1109
+ undo() {
1110
+ if (this._historyIndex <= 0)
1111
+ return;
1112
+ this._historyIndex--;
1113
+ const entry = this._history[this._historyIndex];
1114
+ this.workflow = cloneWorkflow(entry.workflow);
1115
+ this._emitWorkflowChange('undo', undefined);
1116
+ }
1117
+ /**
1118
+ * Redo last undone operation
1119
+ */
1120
+ redo() {
1121
+ if (this._historyIndex >= this._history.length - 1)
1122
+ return;
1123
+ this._historyIndex++;
1124
+ const entry = this._history[this._historyIndex];
1125
+ this.workflow = cloneWorkflow(entry.workflow);
1126
+ this._emitWorkflowChange('redo', undefined);
1127
+ }
1128
+ /**
1129
+ * Check if undo is available
1130
+ */
1131
+ canUndo() {
1132
+ return this._historyIndex > 0;
1133
+ }
1134
+ /**
1135
+ * Check if redo is available
1136
+ */
1137
+ canRedo() {
1138
+ return this._historyIndex < this._history.length - 1;
1139
+ }
1140
+ /**
1141
+ * Export current workflow as JSON
1142
+ */
1143
+ exportWorkflow() {
1144
+ return JSON.stringify(this.workflow, null, 2);
1145
+ }
1146
+ /**
1147
+ * Validate workflow
1148
+ */
1149
+ validate() {
1150
+ const errors = WorkflowValidator.validate(this.workflow);
1151
+ this.dispatchEvent(new CustomEvent('validation-result', {
1152
+ detail: { errors },
1153
+ bubbles: true,
1154
+ composed: true,
1155
+ }));
1156
+ }
1157
+ _emitWorkflowChange(type, nodeId) {
1158
+ this.dispatchEvent(new CustomEvent('workflow-changed', {
1159
+ detail: {
1160
+ type,
1161
+ nodeId,
1162
+ workflow: this.workflow,
1163
+ },
1164
+ bubbles: true,
1165
+ composed: true,
1166
+ }));
1167
+ }
1168
+ render() {
1169
+ if (!this.workflow?.nodes) {
1170
+ return b `<div class="flow-designer-container">
1171
+ <p class="empty-state">No workflow loaded</p>
1172
+ </div>`;
1173
+ }
1174
+ const validationErrors = this.showValidation
1175
+ ? WorkflowValidator.validate(this.workflow)
1176
+ : [];
1177
+ const canvasBounds = SwimlaneLayout.getCanvasBounds(this._positionedNodes);
1178
+ return b `
1179
+ <div class="flow-designer-container">
1180
+ <wc-toolbar
1181
+ class="editor-toolbar"
1182
+ variant="floating"
1183
+ orientation="horizontal"
1184
+ elevated
1185
+ >
1186
+ <wc-icon-button
1187
+ variant="text"
1188
+ ?disabled=${this._editor.zoom <= 0.5}
1189
+ @click=${this._handleZoomOut}
1190
+ title="Zoom Out (Ctrl+-)"
1191
+ >
1192
+ <wc-icon name="remove"></wc-icon>
1193
+ </wc-icon-button>
1194
+ <span class="zoom-display">${Math.round(this._editor.zoom * 100)}%</span>
1195
+ <wc-icon-button
1196
+ variant="text"
1197
+ ?disabled=${this._editor.zoom >= 2}
1198
+ @click=${this._handleZoomIn}
1199
+ title="Zoom In (Ctrl++)"
1200
+ >
1201
+ <wc-icon name="add"></wc-icon>
1202
+ </wc-icon-button>
1203
+ <wc-icon-button
1204
+ variant="text"
1205
+ ?disabled=${!this.canUndo()}
1206
+ @click=${() => this.undo()}
1207
+ title="Undo (Ctrl+Z)"
1208
+ >
1209
+ <wc-icon name="undo"></wc-icon>
1210
+ </wc-icon-button>
1211
+ <wc-icon-button
1212
+ variant="text"
1213
+ ?disabled=${!this.canRedo()}
1214
+ @click=${() => this.redo()}
1215
+ title="Redo (Ctrl+Y)"
1216
+ >
1217
+ <wc-icon name="redo"></wc-icon>
1218
+ </wc-icon-button>
1219
+ ${!this.readonly
1220
+ ? b `
1221
+ <wc-icon-button
1222
+ variant="text"
1223
+ @click=${() => this.validate()}
1224
+ title="Validate Workflow"
1225
+ >
1226
+ <wc-icon name="check_circle"></wc-icon>
1227
+ </wc-icon-button>
1228
+ `
1229
+ : A}
1230
+ </wc-toolbar>
1231
+
1232
+ <!-- Validation messages -->
1233
+ ${validationErrors.length > 0
1234
+ ? b `
1235
+ <div class="validation-panel">
1236
+ ${validationErrors.map((error) => b `
1237
+ <div class="validation-item ${error.severity}">
1238
+ <wc-icon
1239
+ name=${error.severity === 'error' ? 'error' : 'warning'}
1240
+ ></wc-icon>
1241
+ <span>${error.message}</span>
1242
+ </div>
1243
+ `)}
1244
+ </div>
1245
+ `
1246
+ : A}
1247
+
1248
+ <!-- Flow canvas -->
1249
+ <div
1250
+ class="flow-designer"
1251
+ @mousedown=${this._handleCanvasMouseDown}
1252
+ @mousemove=${this._handleCanvasMouseMove}
1253
+ >
1254
+ <div
1255
+ class="canvas-container"
1256
+ style="
1257
+ transform: scale(${this._editor.zoom});
1258
+ width: ${canvasBounds.width}px;
1259
+ height: ${canvasBounds.height}px;
1260
+ "
1261
+ >
1262
+ <!-- SVG Connectors -->
1263
+ <svg
1264
+ class="connectors-layer"
1265
+ width="${canvasBounds.width}"
1266
+ height="${canvasBounds.height}"
1267
+ viewBox="0 0 ${canvasBounds.width} ${canvasBounds.height}"
1268
+ >
1269
+ <defs>
1270
+ <marker
1271
+ id="arrowhead"
1272
+ markerWidth="10"
1273
+ markerHeight="10"
1274
+ refX="9"
1275
+ refY="3"
1276
+ orient="auto"
1277
+ >
1278
+ <polygon points="0 0, 10 3, 0 6" fill="currentColor"></polygon>
1279
+ </marker>
1280
+ </defs>
1281
+ ${this._renderConnectors()}
1282
+ </svg>
1283
+
1284
+ <!-- Swimlane backgrounds -->
1285
+ <div class="swimlanes-container">
1286
+ ${this._renderSwimlanes()}
1287
+ </div>
1288
+
1289
+ <!-- Positioned nodes -->
1290
+ <div class="nodes-layer">
1291
+ ${this._renderNodes()}
1292
+ </div>
1293
+ </div>
1294
+ </div>
1295
+ </div>
1296
+ `;
1297
+ }
1298
+ _renderConnectors() {
1299
+ return this._positionedNodes.flatMap((node) => {
1300
+ if (!node.connectorPoints)
1301
+ return [];
1302
+ return node.connectorPoints.map((connector, idx) => {
1303
+ const { from, to, type } = connector;
1304
+ const isLoopback = type === 'curved';
1305
+ if (isLoopback) {
1306
+ // Render curved path for loop back
1307
+ const midY = (from.y + to.y) / 2;
1308
+ const d = `M ${from.x} ${from.y} ` +
1309
+ `L ${from.x + 30} ${from.y} ` +
1310
+ `Q ${from.x + 60} ${midY} ${to.x - 30} ${to.y} ` +
1311
+ `L ${to.x} ${to.y}`;
1312
+ return b `
1313
+ <path
1314
+ key=${`${node.node.id}-connector-${idx}`}
1315
+ d=${d}
1316
+ class="connector ${type}"
1317
+ marker-end="url(#arrowhead)"
1318
+ vector-effect="non-scaling-stroke"
1319
+ ></path>
1320
+ `;
1321
+ }
1322
+ // Render straight connector
1323
+ const d = `M ${from.x} ${from.y} L ${to.x} ${to.y}`;
1324
+ return b `
1325
+ <path
1326
+ key=${`${node.node.id}-connector-${idx}`}
1327
+ d=${d}
1328
+ class="connector ${type}"
1329
+ marker-end="url(#arrowhead)"
1330
+ vector-effect="non-scaling-stroke"
1331
+ ></path>
1332
+ `;
1333
+ });
1334
+ });
1335
+ }
1336
+ _renderSwimlanes() {
1337
+ const swimlanes = SwimlaneLayout.getSwimlanes(this._positionedNodes);
1338
+ return swimlanes.map((lane) => {
1339
+ const laneTop = Math.min(...lane.nodes.map((n) => n.y)) - 14;
1340
+ const laneBottom = Math.max(...lane.nodes.map((n) => n.y + n.height)) + 14;
1341
+ const laneHeight = Math.max(120, laneBottom - laneTop);
1342
+ return b `
1343
+ <div
1344
+ class="swimlane ${lane.isParallel ? 'parallel' : ''}"
1345
+ style="top: ${laneTop}px; height: ${laneHeight}px;"
1346
+ >
1347
+ <div class="swimlane-header">${lane.name}</div>
1348
+ </div>
1349
+ `;
1350
+ });
1351
+ }
1352
+ _renderNodes() {
1353
+ return this._positionedNodes.map((posNode) => b `
1354
+ <div
1355
+ class="positioned-node"
1356
+ style="
1357
+ left: ${posNode.x}px;
1358
+ top: ${posNode.y}px;
1359
+ width: ${posNode.width}px;
1360
+ height: ${posNode.height}px;
1361
+ "
1362
+ >
1363
+ <wc-flow-designer-node
1364
+ .node=${posNode.node}
1365
+ ?selected=${posNode.node.id === this._editor.selectedNodeId}
1366
+ ?editing=${this._editor.isEditing &&
1367
+ posNode.node.id === this._editor.selectedNodeId}
1368
+ ?disabled=${this.disabled}
1369
+ @node-click=${this._handleNodeClick}
1370
+ @node-delete=${this._handleNodeDelete}
1371
+ @node-edit-start=${this._handleNodeEdit}
1372
+ ></wc-flow-designer-node>
1373
+ </div>
1374
+ `);
1375
+ }
1376
+ };
1377
+ FlowDesigner.styles = [css_248z];
1378
+ __decorate([
1379
+ n({ type: Object })
1380
+ ], FlowDesigner.prototype, "workflow", void 0);
1381
+ __decorate([
1382
+ n({ type: Boolean, reflect: true, attribute: 'readonly' })
1383
+ ], FlowDesigner.prototype, "readonly", void 0);
1384
+ __decorate([
1385
+ n({ type: Boolean, reflect: true })
1386
+ ], FlowDesigner.prototype, "disabled", void 0);
1387
+ __decorate([
1388
+ n({ type: Boolean, attribute: 'show-validation' })
1389
+ ], FlowDesigner.prototype, "showValidation", void 0);
1390
+ __decorate([
1391
+ r()
1392
+ ], FlowDesigner.prototype, "_editor", void 0);
1393
+ __decorate([
1394
+ r()
1395
+ ], FlowDesigner.prototype, "_positionedNodes", void 0);
1396
+ __decorate([
1397
+ r()
1398
+ ], FlowDesigner.prototype, "_history", void 0);
1399
+ __decorate([
1400
+ r()
1401
+ ], FlowDesigner.prototype, "_historyIndex", void 0);
1402
+ __decorate([
1403
+ e('.flow-designer')
1404
+ ], FlowDesigner.prototype, "scrollElm", void 0);
1405
+ FlowDesigner = __decorate([
1406
+ IndividualComponent
1407
+ ], FlowDesigner);
1408
+
1409
+ export { FlowDesigner };
16
1410
  //# sourceMappingURL=flow-designer.js.map