@redvars/peacock 3.5.1 → 3.6.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 (198) hide show
  1. package/dist/{BaseButton-DuASuVth.js → BaseButton-BNFAYn-S.js} +2 -2
  2. package/dist/{BaseButton-DuASuVth.js.map → BaseButton-BNFAYn-S.js.map} +1 -1
  3. package/dist/BaseInput-14YmcfK7.js +27 -0
  4. package/dist/BaseInput-14YmcfK7.js.map +1 -0
  5. package/dist/banner.js +2 -3
  6. package/dist/banner.js.map +1 -1
  7. package/dist/{button-DouvOfEU.js → button-colors-Ccys3hvS.js} +5 -294
  8. package/dist/button-colors-Ccys3hvS.js.map +1 -0
  9. package/dist/button-group.js +226 -6
  10. package/dist/button-group.js.map +1 -1
  11. package/dist/button.js +294 -8
  12. package/dist/button.js.map +1 -1
  13. package/dist/calendar-column-view.js +634 -0
  14. package/dist/calendar-column-view.js.map +1 -0
  15. package/dist/calendar-event-BrQ_SEKD.js +199 -0
  16. package/dist/calendar-event-BrQ_SEKD.js.map +1 -0
  17. package/dist/calendar-month-view.js +376 -0
  18. package/dist/calendar-month-view.js.map +1 -0
  19. package/dist/calendar.js +339 -0
  20. package/dist/calendar.js.map +1 -0
  21. package/dist/canvas.js +361 -0
  22. package/dist/canvas.js.map +1 -0
  23. package/dist/cb-compound-expression.js +125 -0
  24. package/dist/cb-compound-expression.js.map +1 -0
  25. package/dist/cb-divider.js +150 -0
  26. package/dist/cb-divider.js.map +1 -0
  27. package/dist/cb-expression.js +75 -0
  28. package/dist/cb-expression.js.map +1 -0
  29. package/dist/cb-predicate.js +137 -0
  30. package/dist/cb-predicate.js.map +1 -0
  31. package/dist/code-editor.js +2 -1
  32. package/dist/code-editor.js.map +1 -1
  33. package/dist/condition-builder.js +58 -0
  34. package/dist/condition-builder.js.map +1 -0
  35. package/dist/custom-elements-jsdocs.json +7976 -4294
  36. package/dist/custom-elements.json +14358 -7589
  37. package/dist/dropdown-button.js +216 -0
  38. package/dist/dropdown-button.js.map +1 -0
  39. package/dist/event-manager-D-QCmUgR.js +113 -0
  40. package/dist/event-manager-D-QCmUgR.js.map +1 -0
  41. package/dist/fab.js +1 -1
  42. package/dist/flow-designer-dZnLJOQT.js +1656 -0
  43. package/dist/flow-designer-dZnLJOQT.js.map +1 -0
  44. package/dist/flow-designer-node-XMe-jlKg.js +548 -0
  45. package/dist/flow-designer-node-XMe-jlKg.js.map +1 -0
  46. package/dist/flow-designer-node.js +4 -0
  47. package/dist/flow-designer-node.js.map +1 -0
  48. package/dist/flow-designer.js +16 -0
  49. package/dist/flow-designer.js.map +1 -0
  50. package/dist/html-editor.js +358 -0
  51. package/dist/html-editor.js.map +1 -0
  52. package/dist/icon-button-CK1ZuE-2.js +247 -0
  53. package/dist/icon-button-CK1ZuE-2.js.map +1 -0
  54. package/dist/index.js +29 -6
  55. package/dist/index.js.map +1 -1
  56. package/dist/{is-dark-mode-DicqGkCJ.js → is-dark-mode-DOcaw4Yq.js} +2 -27
  57. package/dist/is-dark-mode-DOcaw4Yq.js.map +1 -0
  58. package/dist/modal.js +418 -0
  59. package/dist/modal.js.map +1 -0
  60. package/dist/{navigation-rail-Lxetd5-Z.js → navigation-rail-DyO0oAZU.js} +306 -2197
  61. package/dist/navigation-rail-DyO0oAZU.js.map +1 -0
  62. package/dist/notification-manager.js +268 -0
  63. package/dist/notification-manager.js.map +1 -0
  64. package/dist/peacock-loader.js +84 -8
  65. package/dist/peacock-loader.js.map +1 -1
  66. package/dist/popover-NC7b1lTq.js +1971 -0
  67. package/dist/popover-NC7b1lTq.js.map +1 -0
  68. package/dist/popover-content.js +125 -0
  69. package/dist/popover-content.js.map +1 -0
  70. package/dist/popover.js +4 -0
  71. package/dist/popover.js.map +1 -0
  72. package/dist/split-button.js +388 -0
  73. package/dist/split-button.js.map +1 -0
  74. package/dist/src/__controllers/floating-controller.d.ts +35 -0
  75. package/dist/src/calendar/base-event.d.ts +10 -0
  76. package/dist/src/calendar/calendar-column-view.d.ts +41 -0
  77. package/dist/src/calendar/calendar-event.d.ts +7 -0
  78. package/dist/src/calendar/calendar-month-view.d.ts +31 -0
  79. package/dist/src/calendar/calendar.d.ts +65 -0
  80. package/dist/src/calendar/event-manager.d.ts +17 -0
  81. package/dist/src/calendar/index.d.ts +4 -0
  82. package/dist/src/calendar/types.d.ts +13 -0
  83. package/dist/src/calendar/utils.d.ts +31 -0
  84. package/dist/src/canvas/canvas.d.ts +92 -0
  85. package/dist/src/canvas/index.d.ts +2 -0
  86. package/dist/src/condition-builder/cb-compound-expression.d.ts +31 -0
  87. package/dist/src/condition-builder/cb-divider.d.ts +26 -0
  88. package/dist/src/condition-builder/cb-expression.d.ts +31 -0
  89. package/dist/src/condition-builder/cb-predicate.d.ts +30 -0
  90. package/dist/src/condition-builder/condition-builder.d.ts +27 -0
  91. package/dist/src/condition-builder/index.d.ts +5 -0
  92. package/dist/src/dropdown-button/dropdown-button.d.ts +68 -0
  93. package/dist/src/dropdown-button/index.d.ts +1 -0
  94. package/dist/src/flow-designer/commands.d.ts +66 -0
  95. package/dist/src/flow-designer/flow-designer-node.d.ts +46 -0
  96. package/dist/src/flow-designer/flow-designer.d.ts +133 -0
  97. package/dist/src/flow-designer/index.d.ts +7 -0
  98. package/dist/src/flow-designer/layout.d.ts +30 -0
  99. package/dist/src/flow-designer/types.d.ts +142 -0
  100. package/dist/src/flow-designer/validation.d.ts +43 -0
  101. package/dist/src/flow-designer/workflow-utils.d.ts +40 -0
  102. package/dist/src/html-editor/html-editor.d.ts +56 -0
  103. package/dist/src/html-editor/index.d.ts +2 -0
  104. package/dist/src/index.d.ts +13 -0
  105. package/dist/src/menu/menu/menu.d.ts +5 -7
  106. package/dist/src/menu/menu-item/menu-item.d.ts +14 -13
  107. package/dist/src/modal/index.d.ts +1 -0
  108. package/dist/src/modal/modal.d.ts +63 -0
  109. package/dist/src/notification-manager/index.d.ts +1 -0
  110. package/dist/src/notification-manager/notification-manager.d.ts +44 -0
  111. package/dist/src/popover/index.d.ts +2 -0
  112. package/dist/src/popover/popover-content.d.ts +29 -0
  113. package/dist/src/popover/popover.d.ts +62 -0
  114. package/dist/src/split-button/index.d.ts +1 -0
  115. package/dist/src/split-button/split-button.d.ts +72 -0
  116. package/dist/src/tooltip/tooltip.d.ts +2 -15
  117. package/dist/test/flow-designer.test.d.ts +1 -0
  118. package/dist/tsconfig.tsbuildinfo +1 -1
  119. package/package.json +4 -2
  120. package/readme.md +2 -2
  121. package/src/__controllers/floating-controller.ts +237 -0
  122. package/src/banner/banner.scss +2 -3
  123. package/src/button/button/button.ts +1 -0
  124. package/src/calendar/base-event.ts +49 -0
  125. package/src/calendar/calendar-column-view.scss +326 -0
  126. package/src/calendar/calendar-column-view.ts +392 -0
  127. package/src/calendar/calendar-event.ts +20 -0
  128. package/src/calendar/calendar-month-view.scss +192 -0
  129. package/src/calendar/calendar-month-view.ts +244 -0
  130. package/src/calendar/calendar.scss +71 -0
  131. package/src/calendar/calendar.ts +298 -0
  132. package/src/calendar/event-manager.ts +117 -0
  133. package/src/calendar/index.ts +4 -0
  134. package/src/calendar/types.ts +14 -0
  135. package/src/calendar/utils.ts +180 -0
  136. package/src/canvas/canvas.scss +60 -0
  137. package/src/canvas/canvas.ts +391 -0
  138. package/src/canvas/index.ts +2 -0
  139. package/src/condition-builder/cb-compound-expression.scss +37 -0
  140. package/src/condition-builder/cb-compound-expression.ts +80 -0
  141. package/src/condition-builder/cb-divider.scss +93 -0
  142. package/src/condition-builder/cb-divider.ts +56 -0
  143. package/src/condition-builder/cb-expression.scss +14 -0
  144. package/src/condition-builder/cb-expression.ts +49 -0
  145. package/src/condition-builder/cb-predicate.scss +35 -0
  146. package/src/condition-builder/cb-predicate.ts +102 -0
  147. package/src/condition-builder/condition-builder.scss +13 -0
  148. package/src/condition-builder/condition-builder.ts +38 -0
  149. package/src/condition-builder/index.ts +5 -0
  150. package/src/dropdown-button/demo/index.html +110 -0
  151. package/src/dropdown-button/dropdown-button.scss +22 -0
  152. package/src/dropdown-button/dropdown-button.ts +206 -0
  153. package/src/dropdown-button/index.ts +1 -0
  154. package/src/flow-designer/DEMO.md +239 -0
  155. package/src/flow-designer/commands.ts +278 -0
  156. package/src/flow-designer/flow-designer-node.ts +172 -0
  157. package/src/flow-designer/flow-designer.scss +457 -0
  158. package/src/flow-designer/flow-designer.ts +611 -0
  159. package/src/flow-designer/index.ts +41 -0
  160. package/src/flow-designer/layout.ts +357 -0
  161. package/src/flow-designer/types.ts +166 -0
  162. package/src/flow-designer/validation.ts +284 -0
  163. package/src/flow-designer/workflow-utils.ts +282 -0
  164. package/src/html-editor/html-editor.scss +146 -0
  165. package/src/html-editor/html-editor.ts +276 -0
  166. package/src/html-editor/index.ts +3 -0
  167. package/src/index.ts +25 -0
  168. package/src/menu/menu/menu.scss +2 -2
  169. package/src/menu/menu/menu.ts +91 -101
  170. package/src/menu/menu-item/menu-item.scss +4 -0
  171. package/src/menu/menu-item/menu-item.ts +82 -78
  172. package/src/modal/index.ts +1 -0
  173. package/src/modal/modal.scss +206 -0
  174. package/src/modal/modal.ts +201 -0
  175. package/src/notification-manager/index.ts +1 -0
  176. package/src/notification-manager/notification-manager.scss +113 -0
  177. package/src/notification-manager/notification-manager.ts +199 -0
  178. package/src/peacock-loader.ts +71 -0
  179. package/src/popover/index.ts +2 -0
  180. package/src/popover/popover-content.scss +69 -0
  181. package/src/popover/popover-content.ts +51 -0
  182. package/src/popover/popover.scss +7 -0
  183. package/src/popover/popover.ts +170 -0
  184. package/src/split-button/index.ts +1 -0
  185. package/src/split-button/split-button-colors.scss +56 -0
  186. package/src/split-button/split-button-sizes.scss +28 -0
  187. package/src/split-button/split-button.scss +79 -0
  188. package/src/split-button/split-button.ts +236 -0
  189. package/src/table/table.ts +2 -2
  190. package/src/tooltip/tooltip.scss +4 -3
  191. package/src/tooltip/tooltip.ts +46 -104
  192. package/dist/button-DouvOfEU.js.map +0 -1
  193. package/dist/button-group-CEdMwvJJ.js +0 -464
  194. package/dist/button-group-CEdMwvJJ.js.map +0 -1
  195. package/dist/is-dark-mode-DicqGkCJ.js.map +0 -1
  196. package/dist/navigation-rail-Lxetd5-Z.js.map +0 -1
  197. package/dist/src/menu/menu/MenuSurfaceController.d.ts +0 -18
  198. package/src/menu/menu/MenuSurfaceController.ts +0 -61
@@ -0,0 +1,284 @@
1
+ import type { Workflow, WorkflowNode, ValidationError } from './types.js';
2
+ import {
3
+ findNodeById,
4
+ getAllNodes,
5
+ getNodePath,
6
+ isDescendant,
7
+ } from './workflow-utils.js';
8
+
9
+ /**
10
+ * Workflow validation - checks for common errors and inconsistencies
11
+ */
12
+
13
+ export class WorkflowValidator {
14
+ /**
15
+ * Validate entire workflow
16
+ */
17
+ static validate(workflow: Workflow): ValidationError[] {
18
+ const errors: ValidationError[] = [];
19
+
20
+ // Check root node exists and is a trigger
21
+ if (!workflow.nodes) {
22
+ errors.push({
23
+ nodeId: 'root',
24
+ type: 'orphaned_node',
25
+ message: 'Workflow has no root node',
26
+ severity: 'error',
27
+ });
28
+ return errors;
29
+ }
30
+
31
+ // Validate all nodes
32
+ const allNodes = getAllNodes(workflow.nodes);
33
+
34
+ // Check for circular loops
35
+ errors.push(...this._checkCircularLoops(workflow.nodes, allNodes));
36
+
37
+ // Check for orphaned nodes
38
+ errors.push(...this._checkOrphanedNodes(workflow.nodes, allNodes));
39
+
40
+ // Check valid branches
41
+ errors.push(...this._checkValidBranches(workflow.nodes, allNodes));
42
+
43
+ // Check missing targets
44
+ errors.push(...this._checkMissingTargets(workflow, allNodes));
45
+
46
+ // Check invalid fork/join pairs
47
+ errors.push(...this._checkForkJoinPairs(workflow.nodes, allNodes));
48
+
49
+ return errors;
50
+ }
51
+
52
+ /**
53
+ * Detect circular loop references
54
+ * A loop_end cannot point to a node that is its own descendant (after the loop_start)
55
+ */
56
+ private static _checkCircularLoops(
57
+ rootNode: WorkflowNode,
58
+ allNodes: WorkflowNode[]
59
+ ): ValidationError[] {
60
+ const errors: ValidationError[] = [];
61
+
62
+ for (const node of allNodes) {
63
+ // Only check loop_end nodes
64
+ if (node.type !== 'loop_end') continue;
65
+
66
+ const targetId = node.target_id;
67
+ if (!targetId) continue;
68
+
69
+ // Check if target exists
70
+ const targetNode = findNodeById(rootNode, targetId);
71
+ if (!targetNode) continue;
72
+
73
+ // If loop_end is a descendant of its target, it's circular
74
+ if (isDescendant(rootNode, targetId, node.id)) {
75
+ // This is the intended behavior - loop_end should be a descendant of loop_start
76
+ // So this is actually valid. Skip this check.
77
+ }
78
+
79
+ // However, if the loop_end's target is a descendant of the loop_end, that's circular
80
+ if (isDescendant(rootNode, node.id, targetId)) {
81
+ errors.push({
82
+ nodeId: node.id,
83
+ type: 'circular_loop',
84
+ message: `Loop cannot point to a node (${targetId}) that executes after the loop_end`,
85
+ severity: 'error',
86
+ });
87
+ }
88
+ }
89
+
90
+ return errors;
91
+ }
92
+
93
+ /**
94
+ * Check for orphaned nodes (not reachable from root)
95
+ */
96
+ private static _checkOrphanedNodes(
97
+ rootNode: WorkflowNode,
98
+ allNodes: WorkflowNode[]
99
+ ): ValidationError[] {
100
+ const errors: ValidationError[] = [];
101
+ const paths = new Map<string, string[]>();
102
+
103
+ for (const node of allNodes) {
104
+ const path = getNodePath(rootNode, node.id);
105
+ if (path.length === 0 && node.id !== rootNode.id) {
106
+ errors.push({
107
+ nodeId: node.id,
108
+ type: 'orphaned_node',
109
+ message: `Node "${node.label}" is not reachable from the root trigger`,
110
+ severity: 'error',
111
+ });
112
+ }
113
+ paths.set(node.id, path);
114
+ }
115
+
116
+ // Check nodes in branches - all branch paths must be reachable
117
+ // This is typically valid by structure, but warn if branch has no exit
118
+ for (const node of allNodes) {
119
+ if (!node.branches) continue;
120
+
121
+ for (const [branchKey, branchNodes] of Object.entries(node.branches)) {
122
+ if (branchNodes.length === 0) {
123
+ errors.push({
124
+ nodeId: node.id,
125
+ type: 'invalid_branch',
126
+ message: `Branch "${branchKey}" is empty - no nodes to execute`,
127
+ severity: 'warning',
128
+ });
129
+ }
130
+ }
131
+ }
132
+
133
+ return errors;
134
+ }
135
+
136
+ /**
137
+ * Check that decision nodes have valid branches
138
+ */
139
+ private static _checkValidBranches(
140
+ rootNode: WorkflowNode,
141
+ allNodes: WorkflowNode[]
142
+ ): ValidationError[] {
143
+ const errors: ValidationError[] = [];
144
+
145
+ for (const node of allNodes) {
146
+ if (node.type !== 'decision') continue;
147
+
148
+ if (!node.branches) {
149
+ errors.push({
150
+ nodeId: node.id,
151
+ type: 'invalid_branch',
152
+ message: `Decision node "${node.label}" has no branches defined`,
153
+ severity: 'error',
154
+ });
155
+ continue;
156
+ }
157
+
158
+ // Standard decision should have "yes" and "no"
159
+ const branchKeys = Object.keys(node.branches);
160
+ if (!branchKeys.includes('yes') || !branchKeys.includes('no')) {
161
+ errors.push({
162
+ nodeId: node.id,
163
+ type: 'invalid_branch',
164
+ message: `Decision node "${node.label}" should have "yes" and "no" branches`,
165
+ severity: 'warning',
166
+ });
167
+ }
168
+
169
+ // Check for empty branches
170
+ for (const [branchKey, branchNodes] of Object.entries(node.branches)) {
171
+ if (branchNodes.length === 0) {
172
+ errors.push({
173
+ nodeId: node.id,
174
+ type: 'invalid_branch',
175
+ message: `Decision branch "${branchKey}" is empty`,
176
+ severity: 'warning',
177
+ });
178
+ }
179
+ }
180
+ }
181
+
182
+ return errors;
183
+ }
184
+
185
+ /**
186
+ * Check that loop_end nodes reference valid loop_start nodes
187
+ */
188
+ private static _checkMissingTargets(
189
+ workflow: Workflow,
190
+ allNodes: WorkflowNode[]
191
+ ): ValidationError[] {
192
+ const errors: ValidationError[] = [];
193
+
194
+ for (const node of allNodes) {
195
+ if (node.type === 'loop_end') {
196
+ if (!node.target_id) {
197
+ errors.push({
198
+ nodeId: node.id,
199
+ type: 'missing_target',
200
+ message: `Loop end "${node.label}" does not specify a target loop_start`,
201
+ severity: 'error',
202
+ });
203
+ } else {
204
+ const target = findNodeById(workflow.nodes, node.target_id);
205
+ if (!target || target.type !== 'loop_start') {
206
+ errors.push({
207
+ nodeId: node.id,
208
+ type: 'missing_target',
209
+ message: `Loop end "${node.label}" references non-existent or non-loop_start node "${node.target_id}"`,
210
+ severity: 'error',
211
+ });
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ return errors;
218
+ }
219
+
220
+ /**
221
+ * Check that fork nodes have corresponding join nodes
222
+ */
223
+ private static _checkForkJoinPairs(
224
+ rootNode: WorkflowNode,
225
+ allNodes: WorkflowNode[]
226
+ ): ValidationError[] {
227
+ const errors: ValidationError[] = [];
228
+
229
+ for (const node of allNodes) {
230
+ if (node.type === 'fork') {
231
+ if (!node.join) {
232
+ errors.push({
233
+ nodeId: node.id,
234
+ type: 'invalid_fork_join',
235
+ message: `Fork node "${node.label}" does not have a corresponding join node`,
236
+ severity: 'error',
237
+ });
238
+ } else if (node.join.type !== 'join') {
239
+ errors.push({
240
+ nodeId: node.id,
241
+ type: 'invalid_fork_join',
242
+ message: `Fork node "${node.label}" join is not a join node`,
243
+ severity: 'error',
244
+ });
245
+ }
246
+
247
+ if (!node.tasks || node.tasks.length === 0) {
248
+ errors.push({
249
+ nodeId: node.id,
250
+ type: 'invalid_fork_join',
251
+ message: `Fork node "${node.label}" has no parallel tasks`,
252
+ severity: 'warning',
253
+ });
254
+ }
255
+ }
256
+ }
257
+
258
+ return errors;
259
+ }
260
+
261
+ /**
262
+ * Check if workflow would create a valid execution path
263
+ */
264
+ static isExecutable(workflow: Workflow): boolean {
265
+ const errors = this.validate(workflow);
266
+ return errors.filter((e) => e.severity === 'error').length === 0;
267
+ }
268
+
269
+ /**
270
+ * Get validation warnings only
271
+ */
272
+ static getWarnings(workflow: Workflow): ValidationError[] {
273
+ const errors = this.validate(workflow);
274
+ return errors.filter((e) => e.severity === 'warning');
275
+ }
276
+
277
+ /**
278
+ * Get validation errors only
279
+ */
280
+ static getErrors(workflow: Workflow): ValidationError[] {
281
+ const errors = this.validate(workflow);
282
+ return errors.filter((e) => e.severity === 'error');
283
+ }
284
+ }
@@ -0,0 +1,282 @@
1
+ import type { Workflow, WorkflowNode } from './types.js';
2
+
3
+ /**
4
+ * Workflow utility functions for tree traversal and manipulation
5
+ */
6
+
7
+ /**
8
+ * Deep clone a workflow to ensure immutability
9
+ */
10
+ export function cloneWorkflow(workflow: Workflow): Workflow {
11
+ return JSON.parse(JSON.stringify(workflow));
12
+ }
13
+
14
+ /**
15
+ * Deep clone a workflow node
16
+ */
17
+ export function cloneNode(node: WorkflowNode): WorkflowNode {
18
+ return JSON.parse(JSON.stringify(node));
19
+ }
20
+
21
+ /**
22
+ * Find a node by ID anywhere in the workflow tree
23
+ */
24
+ export function findNodeById(
25
+ node: WorkflowNode,
26
+ id: string
27
+ ): WorkflowNode | null {
28
+ if (node.id === id) return node;
29
+
30
+ // Search children
31
+ if (node.children) {
32
+ for (const child of node.children) {
33
+ const found = findNodeById(child, id);
34
+ if (found) return found;
35
+ }
36
+ }
37
+
38
+ // Search tasks
39
+ if (node.tasks) {
40
+ for (const task of node.tasks) {
41
+ const found = findNodeById(task, id);
42
+ if (found) return found;
43
+ }
44
+ }
45
+
46
+ // Search branches
47
+ if (node.branches) {
48
+ for (const branchNodes of Object.values(node.branches)) {
49
+ for (const branchNode of branchNodes) {
50
+ const found = findNodeById(branchNode, id);
51
+ if (found) return found;
52
+ }
53
+ }
54
+ }
55
+
56
+ // Search join
57
+ if (node.join) {
58
+ const found = findNodeById(node.join, id);
59
+ if (found) return found;
60
+ }
61
+
62
+ return null;
63
+ }
64
+
65
+ /**
66
+ * Remove a node by ID from the workflow tree
67
+ */
68
+ export function removeNodeById(
69
+ node: WorkflowNode,
70
+ id: string
71
+ ): WorkflowNode {
72
+ const result = cloneNode(node);
73
+
74
+ // Remove from children
75
+ if (result.children) {
76
+ result.children = result.children.filter((child) => {
77
+ if (child.id === id) return false;
78
+ removeNodeById(child, id);
79
+ return true;
80
+ });
81
+ }
82
+
83
+ // Remove from tasks
84
+ if (result.tasks) {
85
+ result.tasks = result.tasks.filter((task) => {
86
+ if (task.id === id) return false;
87
+ removeNodeById(task, id);
88
+ return true;
89
+ });
90
+ }
91
+
92
+ // Remove from branches
93
+ if (result.branches) {
94
+ for (const [branchKey, branchNodes] of Object.entries(result.branches)) {
95
+ result.branches[branchKey] = branchNodes.filter((branchNode) => {
96
+ if (branchNode.id === id) return false;
97
+ removeNodeById(branchNode, id);
98
+ return true;
99
+ });
100
+ }
101
+ }
102
+
103
+ // Recursively clean empty nodes
104
+ for (const branchNode of result.children || []) {
105
+ removeNodeById(branchNode, id);
106
+ }
107
+ for (const taskNode of result.tasks || []) {
108
+ removeNodeById(taskNode, id);
109
+ }
110
+ for (const branchNodes of Object.values(result.branches || {})) {
111
+ for (const branchNode of branchNodes) {
112
+ removeNodeById(branchNode, id);
113
+ }
114
+ }
115
+
116
+ return result;
117
+ }
118
+
119
+ /**
120
+ * Insert a node into the workflow tree at a specific location
121
+ */
122
+ export function insertNodeIntoWorkflow(
123
+ parent: WorkflowNode,
124
+ nodeToInsert: WorkflowNode,
125
+ connectionType: 'child' | 'branch' | 'task' = 'child',
126
+ branchKey?: string
127
+ ): void {
128
+ switch (connectionType) {
129
+ case 'child':
130
+ if (!parent.children) parent.children = [];
131
+ parent.children.push(cloneNode(nodeToInsert));
132
+ break;
133
+
134
+ case 'task':
135
+ if (!parent.tasks) parent.tasks = [];
136
+ parent.tasks.push(cloneNode(nodeToInsert));
137
+ break;
138
+
139
+ case 'branch':
140
+ if (!parent.branches) parent.branches = {};
141
+ if (!branchKey) branchKey = 'default';
142
+ if (!parent.branches[branchKey]) parent.branches[branchKey] = [];
143
+ parent.branches[branchKey].push(cloneNode(nodeToInsert));
144
+ break;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Collect all nodes in the workflow (depth-first)
150
+ */
151
+ export function getAllNodes(node: WorkflowNode): WorkflowNode[] {
152
+ const result: WorkflowNode[] = [node];
153
+
154
+ if (node.children) {
155
+ for (const child of node.children) {
156
+ result.push(...getAllNodes(child));
157
+ }
158
+ }
159
+
160
+ if (node.tasks) {
161
+ for (const task of node.tasks) {
162
+ result.push(...getAllNodes(task));
163
+ }
164
+ }
165
+
166
+ if (node.branches) {
167
+ for (const branchNodes of Object.values(node.branches)) {
168
+ for (const branchNode of branchNodes) {
169
+ result.push(...getAllNodes(branchNode));
170
+ }
171
+ }
172
+ }
173
+
174
+ if (node.join) {
175
+ result.push(...getAllNodes(node.join));
176
+ }
177
+
178
+ return result;
179
+ }
180
+
181
+ /**
182
+ * Get all parent node IDs for a given node (path from root to node)
183
+ */
184
+ export function getNodePath(
185
+ rootNode: WorkflowNode,
186
+ targetId: string
187
+ ): string[] {
188
+ const path: string[] = [];
189
+
190
+ function traverse(node: WorkflowNode): boolean {
191
+ path.push(node.id);
192
+
193
+ if (node.id === targetId) return true;
194
+
195
+ // Search children
196
+ if (node.children) {
197
+ for (const child of node.children) {
198
+ if (traverse(child)) return true;
199
+ }
200
+ }
201
+
202
+ // Search tasks
203
+ if (node.tasks) {
204
+ for (const task of node.tasks) {
205
+ if (traverse(task)) return true;
206
+ }
207
+ }
208
+
209
+ // Search branches
210
+ if (node.branches) {
211
+ for (const branchNodes of Object.values(node.branches)) {
212
+ for (const branchNode of branchNodes) {
213
+ if (traverse(branchNode)) return true;
214
+ }
215
+ }
216
+ }
217
+
218
+ // Search join
219
+ if (node.join) {
220
+ if (traverse(node.join)) return true;
221
+ }
222
+
223
+ path.pop();
224
+ return false;
225
+ }
226
+
227
+ traverse(rootNode);
228
+ return path;
229
+ }
230
+
231
+ /**
232
+ * Check if a node is a descendant of another node
233
+ */
234
+ export function isDescendant(
235
+ rootNode: WorkflowNode,
236
+ potentialParentId: string,
237
+ nodeId: string
238
+ ): boolean {
239
+ const path = getNodePath(rootNode, nodeId);
240
+ return path.includes(potentialParentId);
241
+ }
242
+
243
+ /**
244
+ * Replace a node in the tree
245
+ */
246
+ export function replaceNode(
247
+ node: WorkflowNode,
248
+ targetId: string,
249
+ replacement: WorkflowNode
250
+ ): WorkflowNode {
251
+ if (node.id === targetId) {
252
+ return cloneNode(replacement);
253
+ }
254
+
255
+ const result = cloneNode(node);
256
+
257
+ if (result.children) {
258
+ result.children = result.children.map((child) =>
259
+ replaceNode(child, targetId, replacement)
260
+ );
261
+ }
262
+
263
+ if (result.tasks) {
264
+ result.tasks = result.tasks.map((task) =>
265
+ replaceNode(task, targetId, replacement)
266
+ );
267
+ }
268
+
269
+ if (result.branches) {
270
+ for (const [branchKey, branchNodes] of Object.entries(result.branches)) {
271
+ result.branches[branchKey] = branchNodes.map((branchNode) =>
272
+ replaceNode(branchNode, targetId, replacement)
273
+ );
274
+ }
275
+ }
276
+
277
+ if (result.join) {
278
+ result.join = replaceNode(result.join, targetId, replacement);
279
+ }
280
+
281
+ return result;
282
+ }
@@ -0,0 +1,146 @@
1
+ @use '../../scss/mixin';
2
+
3
+ @include mixin.base-styles;
4
+
5
+ :host {
6
+ display: block;
7
+ width: 100%;
8
+ }
9
+
10
+ // ── Field wrapper overrides ─────────────────────────────────────────────────
11
+
12
+ .html-editor-field {
13
+ // Let the field expand to fit content vertically
14
+ --field-height: auto;
15
+ --field-padding-block: 0;
16
+
17
+ width: 100%;
18
+ }
19
+
20
+ // ── Toolbar ─────────────────────────────────────────────────────────────────
21
+
22
+ .html-editor-toolbar {
23
+ display: flex;
24
+ flex-wrap: wrap;
25
+ align-items: center;
26
+ gap: var(--spacing-025, 0.125rem);
27
+ padding: var(--spacing-050, 0.25rem) var(--spacing-100, 0.5rem);
28
+ background: var(
29
+ --html-editor-toolbar-background,
30
+ var(--color-surface-container, #f3edf7)
31
+ );
32
+ border-block-end: 1px solid
33
+ var(
34
+ --html-editor-toolbar-border-color,
35
+ var(--color-outline-variant, #cac4d0)
36
+ );
37
+ border-start-start-radius: inherit;
38
+ border-start-end-radius: inherit;
39
+ }
40
+
41
+ .toolbar-btn {
42
+ display: inline-flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ width: 2rem;
46
+ height: 2rem;
47
+ padding: 0;
48
+ border: none;
49
+ border-radius: var(--shape-corner-extra-small, 0.25rem);
50
+ background: transparent;
51
+ color: var(--color-on-surface-variant, #49454f);
52
+ cursor: pointer;
53
+ transition:
54
+ background 120ms ease,
55
+ color 120ms ease;
56
+
57
+ &:hover:not(:disabled) {
58
+ background: color-mix(
59
+ in srgb,
60
+ var(--color-on-surface-variant, #49454f) 8%,
61
+ transparent
62
+ );
63
+ }
64
+
65
+ &:active:not(:disabled) {
66
+ background: color-mix(
67
+ in srgb,
68
+ var(--color-primary, #6750a4) 12%,
69
+ transparent
70
+ );
71
+ color: var(--color-primary, #6750a4);
72
+ }
73
+
74
+ &:disabled {
75
+ opacity: 0.38;
76
+ cursor: not-allowed;
77
+ }
78
+
79
+ wc-icon {
80
+ pointer-events: none;
81
+ }
82
+ }
83
+
84
+ .toolbar-divider {
85
+ display: inline-block;
86
+ width: 1px;
87
+ height: 1.5rem;
88
+ background: var(--color-outline-variant, #cac4d0);
89
+ margin-inline: var(--spacing-025, 0.125rem);
90
+ flex-shrink: 0;
91
+ }
92
+
93
+ // ── Editable content area ───────────────────────────────────────────────────
94
+
95
+ .html-editor-content {
96
+ min-height: var(--html-editor-min-height, 8rem);
97
+ padding: var(--spacing-200, 1rem) var(--spacing-150, 0.75rem);
98
+ outline: none;
99
+ color: var(--color-on-surface, #1c1b1f);
100
+ font-family: var(--typography-body-medium-font-family, inherit);
101
+ font-size: var(--typography-body-medium-font-size, 0.875rem);
102
+ line-height: var(--typography-body-medium-line-height, 1.5);
103
+ cursor: text;
104
+ word-break: break-word;
105
+ overflow-wrap: break-word;
106
+
107
+ // Placeholder
108
+ &.is-empty::before {
109
+ content: attr(data-placeholder);
110
+ color: var(--color-on-surface-variant, #49454f);
111
+ opacity: 0.6;
112
+ pointer-events: none;
113
+ position: absolute;
114
+ }
115
+
116
+ // Sensible defaults for user-generated rich content
117
+ ul,
118
+ ol {
119
+ padding-inline-start: 1.5rem;
120
+ margin-block: 0.25rem;
121
+ }
122
+
123
+ a {
124
+ color: var(--color-primary, #6750a4);
125
+ text-decoration: underline;
126
+ }
127
+
128
+ strong,
129
+ b {
130
+ font-weight: 600;
131
+ }
132
+ }
133
+
134
+ // ── Read-only tag ───────────────────────────────────────────────────────────
135
+
136
+ .read-only-tag {
137
+ margin: var(--spacing-050, 0.25rem);
138
+ }
139
+
140
+ // ── Disabled / readonly host states ────────────────────────────────────────
141
+
142
+ :host([disabled]) .html-editor-content,
143
+ :host([readonly]) .html-editor-content {
144
+ cursor: not-allowed;
145
+ opacity: 0.6;
146
+ }