@redvars/peacock 3.9.0 → 3.9.1
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.
- package/dist/code-highlighter.js +1 -4
- package/dist/code-highlighter.js.map +1 -1
- package/dist/custom-elements-jsdocs.json +39 -223
- package/dist/custom-elements.json +391 -2205
- package/dist/elevation.js +55 -2
- package/dist/elevation.js.map +1 -1
- package/dist/flow.js +762 -0
- package/dist/flow.js.map +1 -0
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/loader.js +2 -9
- package/dist/loader.js.map +1 -1
- package/dist/menu-item.js +144 -63
- package/dist/menu-item.js.map +1 -1
- package/dist/menu.js +161 -73
- package/dist/menu.js.map +1 -1
- package/dist/src/flow/flow.d.ts +102 -0
- package/dist/src/flow/index.d.ts +2 -0
- package/dist/src/index.d.ts +2 -4
- package/dist/src/menu/menu/menu.d.ts +10 -9
- package/dist/src/menu/menu-item/menu-item.d.ts +1 -3
- package/dist/src/timeline/timeline.d.ts +0 -1
- package/dist/sub-menu.js +28 -1
- package/dist/sub-menu.js.map +1 -1
- package/dist/timeline-item.js +22 -34
- package/dist/timeline-item.js.map +1 -1
- package/dist/timeline.js +1 -18
- package/dist/timeline.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/readme.md +2 -2
- package/src/code-highlighter/code-highlighter.scss +1 -7
- package/src/elevation/demo/index.html +52 -0
- package/src/elevation/elevation.scss +52 -5
- package/src/elevation/elevation.ts +8 -1
- package/src/flow/flow.scss +195 -0
- package/src/flow/flow.ts +683 -0
- package/src/flow/index.ts +2 -0
- package/src/index.ts +2 -16
- package/src/loader.ts +2 -9
- package/src/menu/menu/copy-component-files.mjs +47 -0
- package/src/menu/menu/menu.scss +55 -17
- package/src/menu/menu/menu.ts +155 -84
- package/src/menu/menu-item/menu-item-colors.scss +3 -3
- package/src/menu/menu-item/menu-item.scss +118 -24
- package/src/menu/menu-item/menu-item.ts +61 -55
- package/src/menu/sub-menu/sub-menu.scss +30 -1
- package/src/timeline/timeline-item.scss +21 -31
- package/src/timeline/timeline-item.ts +2 -1
- package/src/timeline/timeline.scss +0 -10
- package/src/timeline/timeline.ts +0 -2
- package/dist/canvas.js +0 -385
- package/dist/canvas.js.map +0 -1
- package/dist/flow-designer-node-CGSm6cUH.js +0 -553
- package/dist/flow-designer-node-CGSm6cUH.js.map +0 -1
- package/dist/flow-designer-node.js +0 -4
- package/dist/flow-designer-node.js.map +0 -1
- package/dist/flow-designer.js +0 -1417
- package/dist/flow-designer.js.map +0 -1
- package/dist/src/canvas/canvas.d.ts +0 -92
- package/dist/src/canvas/index.d.ts +0 -2
- package/dist/src/flow-designer/commands.d.ts +0 -66
- package/dist/src/flow-designer/flow-designer-node.d.ts +0 -46
- package/dist/src/flow-designer/flow-designer.d.ts +0 -133
- package/dist/src/flow-designer/index.d.ts +0 -7
- package/dist/src/flow-designer/layout.d.ts +0 -30
- package/dist/src/flow-designer/types.d.ts +0 -142
- package/dist/src/flow-designer/validation.d.ts +0 -43
- package/dist/src/flow-designer/workflow-utils.d.ts +0 -40
- package/src/canvas/canvas.scss +0 -72
- package/src/canvas/canvas.ts +0 -413
- package/src/canvas/index.ts +0 -2
- package/src/flow-designer/DEMO.md +0 -239
- package/src/flow-designer/commands.ts +0 -278
- package/src/flow-designer/flow-designer-node.ts +0 -172
- package/src/flow-designer/flow-designer.scss +0 -457
- package/src/flow-designer/flow-designer.ts +0 -611
- package/src/flow-designer/index.ts +0 -41
- package/src/flow-designer/layout.ts +0 -357
- package/src/flow-designer/types.ts +0 -166
- package/src/flow-designer/validation.ts +0 -284
- package/src/flow-designer/workflow-utils.ts +0 -282
- /package/dist/test/{flow-designer.test.d.ts → elevation.test.d.ts} +0 -0
package/src/flow/flow.ts
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import { html, LitElement, nothing, svg } from "lit";
|
|
2
|
+
import { property, state } from "lit/decorators.js";
|
|
3
|
+
import { classMap } from "lit/directives/class-map.js";
|
|
4
|
+
import IndividualComponent from "@/IndividualComponent.js";
|
|
5
|
+
import styles from "./flow.scss";
|
|
6
|
+
|
|
7
|
+
export type FlowDirection = "up" | "down" | "left" | "right";
|
|
8
|
+
|
|
9
|
+
export type FlowStrokeVariant = "solid" | "dashed" | "animated-dashed";
|
|
10
|
+
|
|
11
|
+
export interface FlowPoint {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FlowPathSegment {
|
|
17
|
+
direction: FlowDirection;
|
|
18
|
+
length: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface FlowNode {
|
|
22
|
+
id: string;
|
|
23
|
+
position: { x: number; y: number };
|
|
24
|
+
inputs?: ("top" | "left" | "bottom" | "right")[];
|
|
25
|
+
outputs?: ("top" | "left" | "bottom" | "right")[];
|
|
26
|
+
data: {
|
|
27
|
+
label?: string;
|
|
28
|
+
element?: HTMLElement;
|
|
29
|
+
[key: string]: any;
|
|
30
|
+
} | HTMLElement;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FlowEdge {
|
|
34
|
+
id: string;
|
|
35
|
+
source: string;
|
|
36
|
+
target: string;
|
|
37
|
+
sourceHandle?: "top" | "left" | "bottom" | "right";
|
|
38
|
+
targetHandle?: "top" | "left" | "bottom" | "right";
|
|
39
|
+
type?: "smoothstep" | "bezier" | "straight";
|
|
40
|
+
variant?: FlowStrokeVariant;
|
|
41
|
+
label?: string;
|
|
42
|
+
color?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @label Flow
|
|
47
|
+
* @tag wc-flow
|
|
48
|
+
* @rawTag flow
|
|
49
|
+
* @summary An interactive node editor canvas with draggable nodes, custom HTML element rendering, and mouse-based edge connection creation on a dotted grid.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```html
|
|
53
|
+
* <wc-flow id="my-flow"></wc-flow>
|
|
54
|
+
* <script>
|
|
55
|
+
* const flow = document.getElementById('my-flow');
|
|
56
|
+
* flow.nodes = [
|
|
57
|
+
* { id: '1', position: { x: 50, y: 50 }, data: { label: 'Node 1' } },
|
|
58
|
+
* { id: '2', position: { x: 250, y: 50 }, data: { label: 'Node 2' } }
|
|
59
|
+
* ];
|
|
60
|
+
* flow.edges = [
|
|
61
|
+
* { id: 'e1-2', source: '1', target: '2' }
|
|
62
|
+
* ];
|
|
63
|
+
* </script>
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @cssprop --flow-background - Background color for the canvas wrapper. Defaults to surface-container-low.
|
|
67
|
+
* @cssprop --flow-dot-color - Color of the background grid dots. Defaults to outline-variant.
|
|
68
|
+
* @cssprop --flow-line-color - Default stroke color for lines and connectors. Defaults to on-surface.
|
|
69
|
+
* @cssprop --flow-hover-color - Stroke color on hover for clickable shapes. Defaults to primary.
|
|
70
|
+
* @cssprop --flow-arrow-color - Stroke color for arrow markers. Defaults to on-surface.
|
|
71
|
+
*/
|
|
72
|
+
@IndividualComponent
|
|
73
|
+
export class Flow extends LitElement {
|
|
74
|
+
static styles = [styles];
|
|
75
|
+
|
|
76
|
+
@property({ type: Array })
|
|
77
|
+
nodes: FlowNode[] = [];
|
|
78
|
+
|
|
79
|
+
@property({ type: Array })
|
|
80
|
+
edges: FlowEdge[] = [];
|
|
81
|
+
|
|
82
|
+
@property({ type: Number, reflect: true })
|
|
83
|
+
zoom: number = 1;
|
|
84
|
+
|
|
85
|
+
@property({ type: Number, reflect: true })
|
|
86
|
+
padding: number = 50;
|
|
87
|
+
|
|
88
|
+
// Node dimensions tracking
|
|
89
|
+
private nodeDimensions = new Map<string, { width: number; height: number }>();
|
|
90
|
+
|
|
91
|
+
// Drag node state
|
|
92
|
+
@state()
|
|
93
|
+
private draggingNodeId: string | null = null;
|
|
94
|
+
private dragStartMouseX = 0;
|
|
95
|
+
private dragStartMouseY = 0;
|
|
96
|
+
private dragStartNodeX = 0;
|
|
97
|
+
private dragStartNodeY = 0;
|
|
98
|
+
|
|
99
|
+
// Connection drag state
|
|
100
|
+
@state()
|
|
101
|
+
private isDrawingConnection = false;
|
|
102
|
+
@state()
|
|
103
|
+
private draggingSourceId: string | null = null;
|
|
104
|
+
@state()
|
|
105
|
+
private draggingSourceHandle: "top" | "left" | "bottom" | "right" | null = null;
|
|
106
|
+
@state()
|
|
107
|
+
private connectionStartX = 0;
|
|
108
|
+
@state()
|
|
109
|
+
private connectionStartY = 0;
|
|
110
|
+
@state()
|
|
111
|
+
private currentMouseX = 0;
|
|
112
|
+
@state()
|
|
113
|
+
private currentMouseY = 0;
|
|
114
|
+
|
|
115
|
+
protected updated() {
|
|
116
|
+
let dimensionsChanged = false;
|
|
117
|
+
this.shadowRoot?.querySelectorAll(".flow-node").forEach((el) => {
|
|
118
|
+
const id = el.getAttribute("data-id");
|
|
119
|
+
if (id) {
|
|
120
|
+
const current = this.nodeDimensions.get(id);
|
|
121
|
+
const w = (el as HTMLElement).offsetWidth || 150;
|
|
122
|
+
const h = (el as HTMLElement).offsetHeight || 70;
|
|
123
|
+
if (!current || current.width !== w || current.height !== h) {
|
|
124
|
+
this.nodeDimensions.set(id, { width: w, height: h });
|
|
125
|
+
dimensionsChanged = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
if (dimensionsChanged) {
|
|
130
|
+
this.requestUpdate();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// --- Node Dragging Handlers ---
|
|
135
|
+
private onNodeMouseDown(event: MouseEvent, node: FlowNode) {
|
|
136
|
+
const target = event.target as HTMLElement;
|
|
137
|
+
if (
|
|
138
|
+
target.closest(".handle") ||
|
|
139
|
+
target.closest("button") ||
|
|
140
|
+
target.closest("input") ||
|
|
141
|
+
target.closest("a")
|
|
142
|
+
) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
event.preventDefault();
|
|
146
|
+
this.draggingNodeId = node.id;
|
|
147
|
+
this.dragStartMouseX = event.clientX;
|
|
148
|
+
this.dragStartMouseY = event.clientY;
|
|
149
|
+
this.dragStartNodeX = node.position.x;
|
|
150
|
+
this.dragStartNodeY = node.position.y;
|
|
151
|
+
|
|
152
|
+
window.addEventListener("mousemove", this.onNodeMouseMove);
|
|
153
|
+
window.addEventListener("mouseup", this.onNodeMouseUp);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private onNodeMouseMove = (event: MouseEvent) => {
|
|
157
|
+
if (!this.draggingNodeId) return;
|
|
158
|
+
const deltaX = (event.clientX - this.dragStartMouseX) / this.zoom;
|
|
159
|
+
const deltaY = (event.clientY - this.dragStartMouseY) / this.zoom;
|
|
160
|
+
|
|
161
|
+
const node = this.nodes.find((n) => n.id === this.draggingNodeId);
|
|
162
|
+
if (node) {
|
|
163
|
+
node.position = {
|
|
164
|
+
x: this.dragStartNodeX + deltaX,
|
|
165
|
+
y: this.dragStartNodeY + deltaY,
|
|
166
|
+
};
|
|
167
|
+
this.dispatchEvent(
|
|
168
|
+
new CustomEvent("node-drag", {
|
|
169
|
+
detail: { id: node.id, position: node.position },
|
|
170
|
+
bubbles: true,
|
|
171
|
+
composed: true,
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
this.requestUpdate();
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
private onNodeMouseUp = () => {
|
|
179
|
+
this.draggingNodeId = null;
|
|
180
|
+
window.removeEventListener("mousemove", this.onNodeMouseMove);
|
|
181
|
+
window.removeEventListener("mouseup", this.onNodeMouseUp);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// --- Handle Helpers ---
|
|
185
|
+
private getNodeInputs(node: FlowNode): ("top" | "left" | "bottom" | "right")[] {
|
|
186
|
+
if (node.inputs && Array.isArray(node.inputs)) {
|
|
187
|
+
return node.inputs;
|
|
188
|
+
}
|
|
189
|
+
if (node.data && typeof node.data === "object" && Array.isArray((node.data as any).inputs)) {
|
|
190
|
+
return (node.data as any).inputs;
|
|
191
|
+
}
|
|
192
|
+
return ["left"];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private getNodeOutputs(node: FlowNode): ("top" | "left" | "bottom" | "right")[] {
|
|
196
|
+
if (node.outputs && Array.isArray(node.outputs)) {
|
|
197
|
+
return node.outputs;
|
|
198
|
+
}
|
|
199
|
+
if (node.data && typeof node.data === "object" && Array.isArray((node.data as any).outputs)) {
|
|
200
|
+
return (node.data as any).outputs;
|
|
201
|
+
}
|
|
202
|
+
return ["right"];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private getHandleCoordinates(
|
|
206
|
+
node: FlowNode,
|
|
207
|
+
position: "top" | "left" | "bottom" | "right",
|
|
208
|
+
dim: { width: number; height: number },
|
|
209
|
+
): FlowPoint {
|
|
210
|
+
const x = node.position.x;
|
|
211
|
+
const y = node.position.y;
|
|
212
|
+
const w = dim.width;
|
|
213
|
+
const h = dim.height;
|
|
214
|
+
|
|
215
|
+
switch (position) {
|
|
216
|
+
case "top":
|
|
217
|
+
return { x: x + w / 2, y };
|
|
218
|
+
case "bottom":
|
|
219
|
+
return { x: x + w / 2, y: y + h };
|
|
220
|
+
case "left":
|
|
221
|
+
return { x, y: y + h / 2 };
|
|
222
|
+
case "right":
|
|
223
|
+
default:
|
|
224
|
+
return { x: x + w, y: y + h / 2 };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- Connection Dragging Handlers ---
|
|
229
|
+
private onHandleMouseDown(
|
|
230
|
+
event: MouseEvent,
|
|
231
|
+
node: FlowNode,
|
|
232
|
+
position: "top" | "left" | "bottom" | "right",
|
|
233
|
+
) {
|
|
234
|
+
event.stopPropagation();
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
|
|
237
|
+
const dim = this.nodeDimensions.get(node.id) || { width: 150, height: 70 };
|
|
238
|
+
this.draggingSourceId = node.id;
|
|
239
|
+
this.draggingSourceHandle = position;
|
|
240
|
+
|
|
241
|
+
const coords = this.getHandleCoordinates(node, position, dim);
|
|
242
|
+
this.connectionStartX = coords.x;
|
|
243
|
+
this.connectionStartY = coords.y;
|
|
244
|
+
|
|
245
|
+
this.isDrawingConnection = true;
|
|
246
|
+
this.currentMouseX = this.connectionStartX;
|
|
247
|
+
this.currentMouseY = this.connectionStartY;
|
|
248
|
+
|
|
249
|
+
window.addEventListener("mousemove", this.onConnectionMouseMove);
|
|
250
|
+
window.addEventListener("mouseup", this.onConnectionMouseUp);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private onConnectionMouseMove = (event: MouseEvent) => {
|
|
254
|
+
if (!this.isDrawingConnection) return;
|
|
255
|
+
|
|
256
|
+
const workspaceEl = this.shadowRoot?.querySelector(".flow-workspace");
|
|
257
|
+
if (workspaceEl) {
|
|
258
|
+
const rect = workspaceEl.getBoundingClientRect();
|
|
259
|
+
const bounds = this.getBounds();
|
|
260
|
+
const viewBoxX = bounds.minX - this.padding;
|
|
261
|
+
const viewBoxY = bounds.minY - this.padding;
|
|
262
|
+
|
|
263
|
+
this.currentMouseX = viewBoxX + (event.clientX - rect.left) / this.zoom;
|
|
264
|
+
this.currentMouseY = viewBoxY + (event.clientY - rect.top) / this.zoom;
|
|
265
|
+
this.requestUpdate();
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
private onConnectionMouseUp = (event: MouseEvent) => {
|
|
270
|
+
if (!this.isDrawingConnection) return;
|
|
271
|
+
this.isDrawingConnection = false;
|
|
272
|
+
|
|
273
|
+
window.removeEventListener("mousemove", this.onConnectionMouseMove);
|
|
274
|
+
window.removeEventListener("mouseup", this.onConnectionMouseUp);
|
|
275
|
+
|
|
276
|
+
const target = this.shadowRoot?.elementFromPoint(event.clientX, event.clientY);
|
|
277
|
+
const inputHandle = target?.closest(".handle-input");
|
|
278
|
+
if (inputHandle) {
|
|
279
|
+
const targetNodeId = inputHandle.getAttribute("data-node-id");
|
|
280
|
+
const targetHandle = inputHandle.getAttribute("data-position") as "top" | "left" | "bottom" | "right";
|
|
281
|
+
if (targetNodeId && targetNodeId !== this.draggingSourceId) {
|
|
282
|
+
this.dispatchEvent(
|
|
283
|
+
new CustomEvent("connect", {
|
|
284
|
+
detail: {
|
|
285
|
+
source: this.draggingSourceId,
|
|
286
|
+
target: targetNodeId,
|
|
287
|
+
sourceHandle: this.draggingSourceHandle,
|
|
288
|
+
targetHandle: targetHandle,
|
|
289
|
+
},
|
|
290
|
+
bubbles: true,
|
|
291
|
+
composed: true,
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
this.draggingSourceId = null;
|
|
297
|
+
this.draggingSourceHandle = null;
|
|
298
|
+
this.requestUpdate();
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// --- Path Builders ---
|
|
302
|
+
private getRoundedPath(points: FlowPoint[], radius = 12) {
|
|
303
|
+
if (points.length < 3) {
|
|
304
|
+
return points.map((p, i) => (i === 0 ? `M ${p.x} ${p.y}` : `L ${p.x} ${p.y}`)).join(" ");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let path = `M ${points[0].x} ${points[0].y}`;
|
|
308
|
+
|
|
309
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
310
|
+
const prev = points[i - 1];
|
|
311
|
+
const curr = points[i];
|
|
312
|
+
const next = points[i + 1];
|
|
313
|
+
|
|
314
|
+
const dxp = prev.x - curr.x;
|
|
315
|
+
const dyp = prev.y - curr.y;
|
|
316
|
+
const lenp = Math.hypot(dxp, dyp);
|
|
317
|
+
|
|
318
|
+
const dxn = next.x - curr.x;
|
|
319
|
+
const dyn = next.y - curr.y;
|
|
320
|
+
const lenn = Math.hypot(dxn, dyn);
|
|
321
|
+
|
|
322
|
+
if (lenp === 0 || lenn === 0) {
|
|
323
|
+
path += ` L ${curr.x} ${curr.y}`;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const r = Math.min(radius, lenp / 2, lenn / 2);
|
|
328
|
+
|
|
329
|
+
if (r > 0) {
|
|
330
|
+
const x1 = curr.x + (dxp / lenp) * r;
|
|
331
|
+
const y1 = curr.y + (dyp / lenp) * r;
|
|
332
|
+
const x2 = curr.x + (dxn / lenn) * r;
|
|
333
|
+
const y2 = curr.y + (dyn / lenn) * r;
|
|
334
|
+
|
|
335
|
+
path += ` L ${x1} ${y1} Q ${curr.x} ${curr.y} ${x2} ${y2}`;
|
|
336
|
+
} else {
|
|
337
|
+
path += ` L ${curr.x} ${curr.y}`;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const last = points[points.length - 1];
|
|
342
|
+
path += ` L ${last.x} ${last.y}`;
|
|
343
|
+
return path;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private getSmoothStepPath(
|
|
347
|
+
x1: number,
|
|
348
|
+
y1: number,
|
|
349
|
+
x2: number,
|
|
350
|
+
y2: number,
|
|
351
|
+
sourceHandle: "top" | "left" | "bottom" | "right" = "right",
|
|
352
|
+
targetHandle: "top" | "left" | "bottom" | "right" = "left",
|
|
353
|
+
) {
|
|
354
|
+
const isSourceHorizontal = sourceHandle === "left" || sourceHandle === "right";
|
|
355
|
+
const isTargetHorizontal = targetHandle === "left" || targetHandle === "right";
|
|
356
|
+
|
|
357
|
+
let points: FlowPoint[] = [];
|
|
358
|
+
|
|
359
|
+
if (isSourceHorizontal && isTargetHorizontal) {
|
|
360
|
+
const midX = x1 + (x2 - x1) / 2;
|
|
361
|
+
points = [
|
|
362
|
+
{ x: x1, y: y1 },
|
|
363
|
+
{ x: midX, y: y1 },
|
|
364
|
+
{ x: midX, y: y2 },
|
|
365
|
+
{ x: x2, y: y2 },
|
|
366
|
+
];
|
|
367
|
+
} else if (!isSourceHorizontal && !isTargetHorizontal) {
|
|
368
|
+
const midY = y1 + (y2 - y1) / 2;
|
|
369
|
+
points = [
|
|
370
|
+
{ x: x1, y: y1 },
|
|
371
|
+
{ x: x1, y: midY },
|
|
372
|
+
{ x: x2, y: midY },
|
|
373
|
+
{ x: x2, y: y2 },
|
|
374
|
+
];
|
|
375
|
+
} else if (isSourceHorizontal && !isTargetHorizontal) {
|
|
376
|
+
points = [
|
|
377
|
+
{ x: x1, y: y1 },
|
|
378
|
+
{ x: x2, y: y1 },
|
|
379
|
+
{ x: x2, y: y2 },
|
|
380
|
+
];
|
|
381
|
+
} else {
|
|
382
|
+
points = [
|
|
383
|
+
{ x: x1, y: y1 },
|
|
384
|
+
{ x: x1, y: y2 },
|
|
385
|
+
{ x: x2, y: y2 },
|
|
386
|
+
];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return this.getRoundedPath(points, 12);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private getBezierPath(
|
|
393
|
+
x1: number,
|
|
394
|
+
y1: number,
|
|
395
|
+
x2: number,
|
|
396
|
+
y2: number,
|
|
397
|
+
sourceHandle: "top" | "left" | "bottom" | "right" = "right",
|
|
398
|
+
targetHandle: "top" | "left" | "bottom" | "right" = "left",
|
|
399
|
+
) {
|
|
400
|
+
const normals = {
|
|
401
|
+
left: { x: -1, y: 0 },
|
|
402
|
+
right: { x: 1, y: 0 },
|
|
403
|
+
top: { x: 0, y: -1 },
|
|
404
|
+
bottom: { x: 0, y: 1 },
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const sNormal = normals[sourceHandle] || { x: 1, y: 0 };
|
|
408
|
+
const tNormal = normals[targetHandle] || { x: -1, y: 0 };
|
|
409
|
+
|
|
410
|
+
const dx = x2 - x1;
|
|
411
|
+
const dy = y2 - y1;
|
|
412
|
+
const dist = Math.hypot(dx, dy);
|
|
413
|
+
const offset = Math.max(dist / 2, 50);
|
|
414
|
+
|
|
415
|
+
const cx1 = x1 + sNormal.x * offset;
|
|
416
|
+
const cy1 = y1 + sNormal.y * offset;
|
|
417
|
+
const cx2 = x2 + tNormal.x * offset;
|
|
418
|
+
const cy2 = y2 + tNormal.y * offset;
|
|
419
|
+
|
|
420
|
+
return `M ${x1} ${y1} C ${cx1} ${cy1}, ${cx2} ${cy2}, ${x2} ${y2}`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private getStraightPath(x1: number, y1: number, x2: number, y2: number) {
|
|
424
|
+
return `M ${x1} ${y1} L ${x2} ${y2}`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private drawPath(
|
|
428
|
+
sourceId: string,
|
|
429
|
+
targetId: string,
|
|
430
|
+
type?: "smoothstep" | "bezier" | "straight",
|
|
431
|
+
sourceHandle?: "top" | "left" | "bottom" | "right",
|
|
432
|
+
targetHandle?: "top" | "left" | "bottom" | "right",
|
|
433
|
+
) {
|
|
434
|
+
const sourceNode = this.nodes.find((n) => n.id === sourceId);
|
|
435
|
+
const targetNode = this.nodes.find((n) => n.id === targetId);
|
|
436
|
+
if (!sourceNode || !targetNode) return "";
|
|
437
|
+
|
|
438
|
+
const sDim = this.nodeDimensions.get(sourceId) || { width: 150, height: 70 };
|
|
439
|
+
const tDim = this.nodeDimensions.get(targetId) || { width: 150, height: 70 };
|
|
440
|
+
|
|
441
|
+
const sHandle = sourceHandle || this.getNodeOutputs(sourceNode)[0] || "right";
|
|
442
|
+
const tHandle = targetHandle || this.getNodeInputs(targetNode)[0] || "left";
|
|
443
|
+
|
|
444
|
+
const startCoords = this.getHandleCoordinates(sourceNode, sHandle, sDim);
|
|
445
|
+
const endCoords = this.getHandleCoordinates(targetNode, tHandle, tDim);
|
|
446
|
+
|
|
447
|
+
const edgeType = type || "smoothstep";
|
|
448
|
+
if (edgeType === "bezier") {
|
|
449
|
+
return this.getBezierPath(startCoords.x, startCoords.y, endCoords.x, endCoords.y, sHandle, tHandle);
|
|
450
|
+
}
|
|
451
|
+
if (edgeType === "straight") {
|
|
452
|
+
return this.getStraightPath(startCoords.x, startCoords.y, endCoords.x, endCoords.y);
|
|
453
|
+
}
|
|
454
|
+
return this.getSmoothStepPath(startCoords.x, startCoords.y, endCoords.x, endCoords.y, sHandle, tHandle);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// --- Auto viewbox math ---
|
|
458
|
+
private getBounds() {
|
|
459
|
+
if (this.nodes.length === 0) {
|
|
460
|
+
return { minX: 0, minY: 0, maxX: 400, maxY: 300 };
|
|
461
|
+
}
|
|
462
|
+
let minX = Infinity;
|
|
463
|
+
let minY = Infinity;
|
|
464
|
+
let maxX = -Infinity;
|
|
465
|
+
let maxY = -Infinity;
|
|
466
|
+
|
|
467
|
+
this.nodes.forEach((node) => {
|
|
468
|
+
const dim = this.nodeDimensions.get(node.id) ||
|
|
469
|
+
{ width: 150, height: 70 };
|
|
470
|
+
const x = node.position.x;
|
|
471
|
+
const y = node.position.y;
|
|
472
|
+
if (x < minX) minX = x;
|
|
473
|
+
if (y < minY) minY = y;
|
|
474
|
+
if (x + dim.width > maxX) maxX = x + dim.width;
|
|
475
|
+
if (y + dim.height > maxY) maxY = y + dim.height;
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
return { minX, minY, maxX, maxY };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// --- Content Renderers ---
|
|
482
|
+
private isCustomNode(node: FlowNode) {
|
|
483
|
+
if (node.data instanceof HTMLElement) {
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
if (
|
|
487
|
+
node.data &&
|
|
488
|
+
typeof node.data === "object" &&
|
|
489
|
+
(node.data as any).element instanceof HTMLElement
|
|
490
|
+
) {
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private renderNodeContent(node: FlowNode) {
|
|
497
|
+
if (node.data instanceof HTMLElement) {
|
|
498
|
+
return node.data;
|
|
499
|
+
}
|
|
500
|
+
if (
|
|
501
|
+
node.data &&
|
|
502
|
+
typeof node.data === "object" &&
|
|
503
|
+
(node.data as any).element instanceof HTMLElement
|
|
504
|
+
) {
|
|
505
|
+
return (node.data as any).element;
|
|
506
|
+
}
|
|
507
|
+
const label = node.data && typeof node.data === "object"
|
|
508
|
+
? (node.data as any).label || node.id
|
|
509
|
+
: String(node.data);
|
|
510
|
+
return html`
|
|
511
|
+
<div class="flow-node-default-card">
|
|
512
|
+
<div class="flow-node-title">${label}</div>
|
|
513
|
+
</div>
|
|
514
|
+
`;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
protected render() {
|
|
518
|
+
const bounds = this.getBounds();
|
|
519
|
+
const viewBoxX = bounds.minX - this.padding;
|
|
520
|
+
const viewBoxY = bounds.minY - this.padding;
|
|
521
|
+
const viewBoxWidth = bounds.maxX - bounds.minX + 2 * this.padding;
|
|
522
|
+
const viewBoxHeight = bounds.maxY - bounds.minY + 2 * this.padding;
|
|
523
|
+
|
|
524
|
+
const svgViewBox =
|
|
525
|
+
`${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}`;
|
|
526
|
+
|
|
527
|
+
return html`
|
|
528
|
+
<div class="flow-container">
|
|
529
|
+
<div
|
|
530
|
+
class="flow-scroller"
|
|
531
|
+
style="width: ${viewBoxWidth * this.zoom}px; height: ${viewBoxHeight * this.zoom}px; position: relative;"
|
|
532
|
+
>
|
|
533
|
+
<div
|
|
534
|
+
class="flow-workspace"
|
|
535
|
+
style="transform: scale(${this.zoom}); transform-origin: top left; width: ${viewBoxWidth}px; height: ${viewBoxHeight}px; position: absolute; left: 0; top: 0;"
|
|
536
|
+
>
|
|
537
|
+
<!-- SVG canvas for rendering grid and paths -->
|
|
538
|
+
<svg class="flow-svg" viewBox="${svgViewBox}">
|
|
539
|
+
<defs>
|
|
540
|
+
<pattern
|
|
541
|
+
id="dot-grid"
|
|
542
|
+
width="20"
|
|
543
|
+
height="20"
|
|
544
|
+
patternUnits="userSpaceOnUse"
|
|
545
|
+
>
|
|
546
|
+
<circle cx="1" cy="1" r="1" />
|
|
547
|
+
</pattern>
|
|
548
|
+
<marker
|
|
549
|
+
id="arrowhead"
|
|
550
|
+
markerWidth="8"
|
|
551
|
+
markerHeight="8"
|
|
552
|
+
refX="7"
|
|
553
|
+
refY="4"
|
|
554
|
+
orient="auto"
|
|
555
|
+
>
|
|
556
|
+
<polygon
|
|
557
|
+
points="0 0, 8 4, 0 8"
|
|
558
|
+
fill="var(--flow-line-color, var(--color-outline))"
|
|
559
|
+
/>
|
|
560
|
+
</marker>
|
|
561
|
+
</defs>
|
|
562
|
+
|
|
563
|
+
<!-- Background Grid -->
|
|
564
|
+
<rect
|
|
565
|
+
x="${viewBoxX}"
|
|
566
|
+
y="${viewBoxY}"
|
|
567
|
+
width="${viewBoxWidth}"
|
|
568
|
+
height="${viewBoxHeight}"
|
|
569
|
+
fill="url(#dot-grid)"
|
|
570
|
+
/>
|
|
571
|
+
|
|
572
|
+
<!-- Connection Edges -->
|
|
573
|
+
${this.edges.map((edge) => {
|
|
574
|
+
const pathString = this.drawPath(
|
|
575
|
+
edge.source,
|
|
576
|
+
edge.target,
|
|
577
|
+
edge.type,
|
|
578
|
+
edge.sourceHandle,
|
|
579
|
+
edge.targetHandle,
|
|
580
|
+
);
|
|
581
|
+
const color = edge.color ||
|
|
582
|
+
"var(--flow-line-color, var(--color-outline))";
|
|
583
|
+
const variant = edge.variant || "solid";
|
|
584
|
+
return svg`
|
|
585
|
+
<path
|
|
586
|
+
class="${classMap({
|
|
587
|
+
"flow-edge": true,
|
|
588
|
+
"variant-dashed": variant === "dashed" ||
|
|
589
|
+
variant === "animated-dashed",
|
|
590
|
+
"variant-animated-dashed": variant === "animated-dashed",
|
|
591
|
+
})}"
|
|
592
|
+
d="${pathString}"
|
|
593
|
+
stroke="${color}"
|
|
594
|
+
fill="none"
|
|
595
|
+
stroke-width="2"
|
|
596
|
+
marker-end="url(#arrowhead)"
|
|
597
|
+
/>
|
|
598
|
+
`;
|
|
599
|
+
})}
|
|
600
|
+
|
|
601
|
+
<!-- Connection Draft line preview -->
|
|
602
|
+
${this.isDrawingConnection && this.draggingSourceHandle
|
|
603
|
+
? svg`
|
|
604
|
+
<path
|
|
605
|
+
class="flow-edge-preview"
|
|
606
|
+
d="${this.getBezierPath(
|
|
607
|
+
this.connectionStartX,
|
|
608
|
+
this.connectionStartY,
|
|
609
|
+
this.currentMouseX,
|
|
610
|
+
this.currentMouseY,
|
|
611
|
+
this.draggingSourceHandle,
|
|
612
|
+
({
|
|
613
|
+
left: "right",
|
|
614
|
+
right: "left",
|
|
615
|
+
top: "bottom",
|
|
616
|
+
bottom: "top",
|
|
617
|
+
})[this.draggingSourceHandle] as any
|
|
618
|
+
)}"
|
|
619
|
+
stroke="var(--flow-hover-color, var(--color-primary))"
|
|
620
|
+
fill="none"
|
|
621
|
+
stroke-width="2"
|
|
622
|
+
stroke-dasharray="4 4"
|
|
623
|
+
/>
|
|
624
|
+
`
|
|
625
|
+
: nothing}
|
|
626
|
+
</svg>
|
|
627
|
+
|
|
628
|
+
<!-- Nodes Workspace -->
|
|
629
|
+
<div
|
|
630
|
+
class="flow-nodes"
|
|
631
|
+
style="position: absolute; left: ${-viewBoxX}px; top: ${-viewBoxY}px;"
|
|
632
|
+
>
|
|
633
|
+
${this.nodes.map((node) => {
|
|
634
|
+
const inputs = this.getNodeInputs(node);
|
|
635
|
+
const outputs = this.getNodeOutputs(node);
|
|
636
|
+
return html`
|
|
637
|
+
<div
|
|
638
|
+
class="${classMap({
|
|
639
|
+
"flow-node": true,
|
|
640
|
+
"flow-node-custom": this.isCustomNode(node),
|
|
641
|
+
})}"
|
|
642
|
+
data-id="${node.id}"
|
|
643
|
+
style="position: absolute; left: ${node.position
|
|
644
|
+
.x}px; top: ${node.position.y}px;"
|
|
645
|
+
@mousedown="${(e: MouseEvent) =>
|
|
646
|
+
this.onNodeMouseDown(e, node)}"
|
|
647
|
+
>
|
|
648
|
+
<!-- Input connection handles -->
|
|
649
|
+
${inputs.map((pos) => html`
|
|
650
|
+
<div
|
|
651
|
+
class="handle handle-input handle-position-${pos}"
|
|
652
|
+
data-node-id="${node.id}"
|
|
653
|
+
data-position="${pos}"
|
|
654
|
+
>
|
|
655
|
+
</div>
|
|
656
|
+
`)}
|
|
657
|
+
|
|
658
|
+
<!-- Node Inner Content -->
|
|
659
|
+
<div class="flow-node-content">
|
|
660
|
+
${this.renderNodeContent(node)}
|
|
661
|
+
</div>
|
|
662
|
+
|
|
663
|
+
<!-- Output connection handles -->
|
|
664
|
+
${outputs.map((pos) => html`
|
|
665
|
+
<div
|
|
666
|
+
class="handle handle-output handle-position-${pos}"
|
|
667
|
+
data-node-id="${node.id}"
|
|
668
|
+
data-position="${pos}"
|
|
669
|
+
@mousedown="${(e: MouseEvent) =>
|
|
670
|
+
this.onHandleMouseDown(e, node, pos)}"
|
|
671
|
+
>
|
|
672
|
+
</div>
|
|
673
|
+
`)}
|
|
674
|
+
</div>
|
|
675
|
+
`;
|
|
676
|
+
})}
|
|
677
|
+
</div>
|
|
678
|
+
</div>
|
|
679
|
+
</div>
|
|
680
|
+
</div>
|
|
681
|
+
`;
|
|
682
|
+
}
|
|
683
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -86,22 +86,8 @@ export {
|
|
|
86
86
|
CalendarMonthView,
|
|
87
87
|
} from './calendar/index.js';
|
|
88
88
|
export type { CalendarViewType, EventType } from './calendar/index.js';
|
|
89
|
-
export {
|
|
90
|
-
export type {
|
|
91
|
-
export { FlowDesigner, FlowDesignerNode } from './flow-designer/index.js';
|
|
92
|
-
export type {
|
|
93
|
-
Workflow,
|
|
94
|
-
WorkflowNode,
|
|
95
|
-
WorkflowCommand,
|
|
96
|
-
PositionedNode,
|
|
97
|
-
ValidationError,
|
|
98
|
-
HistoryEntry,
|
|
99
|
-
EditorState,
|
|
100
|
-
NodeType,
|
|
101
|
-
WorkflowChangeEvent,
|
|
102
|
-
SwimlaneConfig,
|
|
103
|
-
NodeTemplate,
|
|
104
|
-
} from './flow-designer/index.js';
|
|
89
|
+
export { Flow } from './flow/index.js';
|
|
90
|
+
export type { FlowNode, FlowEdge, FlowStrokeVariant } from './flow/index.js';
|
|
105
91
|
export {
|
|
106
92
|
ConditionBuilder,
|
|
107
93
|
CbPredicate,
|