@statelyai/flow-react 0.4.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.
- package/README.md +103 -0
- package/dist/esm/backgrounds.d.ts +20 -0
- package/dist/esm/backgrounds.js +53 -0
- package/dist/esm/hooks.js +255 -0
- package/dist/esm/index.d.ts +306 -0
- package/dist/esm/index.js +979 -0
- package/package.json +87 -0
|
@@ -0,0 +1,979 @@
|
|
|
1
|
+
import { a as useFlowEntity, c as useFlowPort, d as useFlowViewport, f as FlowInstanceProvider, h as createFlowInstance, i as useFlowEdge, l as useFlowSelector, m as useOptionalFlowInstance, n as useFlow, o as useFlowMeasurement, p as useFlowInstance, r as useFlowContext, s as useFlowNode, t as useCreateFlowInstance, u as useFlowValue } from "./hooks.js";
|
|
2
|
+
import { createContext, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { CONNECTION_PREVIEW_EDGE_ID, createConnectionPreviewGraph, createFlowInputRuntime, getAnchorTransform, getAutoPanDelta, getMinimapProjection, getToolbarPlacement, nodeDepths, pathToSVG, profileModesForContext, viewportCenteredOn } from "@statelyai/flow";
|
|
4
|
+
import { applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, computeRenderableIds, createMeasurementObserver, nodePositionStyle, renderableIdsEqual, resolveFlowColorMode, viewportStyle } from "@statelyai/flow-dom";
|
|
5
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/useFlowInput.ts
|
|
7
|
+
/**
|
|
8
|
+
* Bind a container element to the core input runtime: all pointer, wheel,
|
|
9
|
+
* and keyboard gestures inside the container are recognized by input modes
|
|
10
|
+
* and applied to the flow store. This replaces hand-written gesture handlers.
|
|
11
|
+
*/
|
|
12
|
+
function useFlowInput(flow, containerRef, options = {}) {
|
|
13
|
+
const optionsRef = useRef(options);
|
|
14
|
+
optionsRef.current = options;
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const container = containerRef.current;
|
|
17
|
+
if (!container) return;
|
|
18
|
+
const { modes, profile } = optionsRef.current;
|
|
19
|
+
const runtime = createFlowInputRuntime({
|
|
20
|
+
store: flow.store,
|
|
21
|
+
engine: flow.engine,
|
|
22
|
+
modes: modes ?? (profile ? profileModesForContext(profile) : void 0)
|
|
23
|
+
});
|
|
24
|
+
return attachInput({
|
|
25
|
+
container,
|
|
26
|
+
store: flow.store,
|
|
27
|
+
runtime,
|
|
28
|
+
autoPan: (context, screenPoint) => getAutoPanDelta(context, screenPoint, flow.engine)
|
|
29
|
+
});
|
|
30
|
+
}, [flow, containerRef]);
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/NodeContext.tsx
|
|
34
|
+
const NodeRenderContext = createContext(null);
|
|
35
|
+
const NodeRenderContextProvider = NodeRenderContext.Provider;
|
|
36
|
+
function useNodeRenderContext() {
|
|
37
|
+
const context = useContext(NodeRenderContext);
|
|
38
|
+
if (!context) throw new Error("Port must be rendered inside a StatelyFlow node.");
|
|
39
|
+
return context;
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/StatelyFlow.tsx
|
|
43
|
+
function stringArraysEqual(a, b) {
|
|
44
|
+
if (a.length !== b.length) return false;
|
|
45
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
function useRenderableIds(instance, enabled) {
|
|
49
|
+
return useFlowSelector((context) => computeRenderableIds(instance.engine, context, enabled), { isEqual: renderableIdsEqual });
|
|
50
|
+
}
|
|
51
|
+
function useViewportTransform(ref, instance) {
|
|
52
|
+
useLayoutEffect(() => {
|
|
53
|
+
const el = ref.current;
|
|
54
|
+
if (!el) return;
|
|
55
|
+
return attachViewportTransform(instance.store, el);
|
|
56
|
+
}, [ref, instance]);
|
|
57
|
+
}
|
|
58
|
+
function StatelyFlow(props) {
|
|
59
|
+
if (props.flow) return /* @__PURE__ */ jsx(StatelyFlowView, {
|
|
60
|
+
...props,
|
|
61
|
+
flow: props.flow
|
|
62
|
+
});
|
|
63
|
+
return /* @__PURE__ */ jsx(OwnedStatelyFlow, { ...props });
|
|
64
|
+
}
|
|
65
|
+
function OwnedStatelyFlow(props) {
|
|
66
|
+
const flow = useCreateFlowInstance({
|
|
67
|
+
graph: props.graph,
|
|
68
|
+
settings: props.settings,
|
|
69
|
+
listeners: props.listeners,
|
|
70
|
+
events: props.events
|
|
71
|
+
});
|
|
72
|
+
return /* @__PURE__ */ jsx(StatelyFlowView, {
|
|
73
|
+
...props,
|
|
74
|
+
flow
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function StatelyFlowView(props) {
|
|
78
|
+
const containerRef = useRef(null);
|
|
79
|
+
const viewportRef = useRef(null);
|
|
80
|
+
useViewportTransform(viewportRef, props.flow);
|
|
81
|
+
useLayoutEffect(() => {
|
|
82
|
+
const element = containerRef.current;
|
|
83
|
+
if (!element) return;
|
|
84
|
+
return attachContainer(props.flow.store, element);
|
|
85
|
+
}, [props.flow]);
|
|
86
|
+
useFlowInput(props.flow, containerRef, {
|
|
87
|
+
modes: props.inputModes,
|
|
88
|
+
profile: props.settings?.store?.profile
|
|
89
|
+
});
|
|
90
|
+
return /* @__PURE__ */ jsx(FlowInstanceProvider, {
|
|
91
|
+
flow: props.flow,
|
|
92
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
93
|
+
ref: containerRef,
|
|
94
|
+
className: props.className,
|
|
95
|
+
style: {
|
|
96
|
+
position: "relative",
|
|
97
|
+
overflow: "hidden",
|
|
98
|
+
width: "100%",
|
|
99
|
+
height: "100%",
|
|
100
|
+
minHeight: 320,
|
|
101
|
+
touchAction: "none",
|
|
102
|
+
...props.style
|
|
103
|
+
},
|
|
104
|
+
children: [
|
|
105
|
+
props.background,
|
|
106
|
+
/* @__PURE__ */ jsxs("div", {
|
|
107
|
+
ref: viewportRef,
|
|
108
|
+
"data-flow-viewport": true,
|
|
109
|
+
style: viewportStyle(),
|
|
110
|
+
children: [
|
|
111
|
+
/* @__PURE__ */ jsx(EdgeLayer, {
|
|
112
|
+
renderEdge: props.renderEdge,
|
|
113
|
+
onlyRenderVisible: props.onlyRenderVisible
|
|
114
|
+
}),
|
|
115
|
+
/* @__PURE__ */ jsx(NodeLayer, {
|
|
116
|
+
renderNode: props.renderNode,
|
|
117
|
+
onlyRenderVisible: props.onlyRenderVisible
|
|
118
|
+
}),
|
|
119
|
+
/* @__PURE__ */ jsx(SnapLinesLayer, {}),
|
|
120
|
+
/* @__PURE__ */ jsx(EdgeLabelLayer, { onlyRenderVisible: props.onlyRenderVisible })
|
|
121
|
+
]
|
|
122
|
+
}),
|
|
123
|
+
/* @__PURE__ */ jsx("div", {
|
|
124
|
+
"data-flow-overlay": true,
|
|
125
|
+
children: props.children
|
|
126
|
+
})
|
|
127
|
+
]
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function SnapLinesLayer() {
|
|
132
|
+
const instance = useFlowInstance();
|
|
133
|
+
const snapResult = useFlowSelector(() => instance.engine.getSnapResult());
|
|
134
|
+
const viewport = useFlowSelector((context) => context.viewport);
|
|
135
|
+
const viewportSize = useFlowSelector((context) => context.viewportSize);
|
|
136
|
+
if (!snapResult) return null;
|
|
137
|
+
const visibleBounds = {
|
|
138
|
+
x: -viewport.x / viewport.zoom,
|
|
139
|
+
y: -viewport.y / viewport.zoom,
|
|
140
|
+
width: viewportSize.width / viewport.zoom,
|
|
141
|
+
height: viewportSize.height / viewport.zoom
|
|
142
|
+
};
|
|
143
|
+
const strokeWidth = 1.5 / viewport.zoom;
|
|
144
|
+
const capSize = 4 / viewport.zoom;
|
|
145
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
146
|
+
"aria-hidden": "true",
|
|
147
|
+
"data-flow-canvas-layer": true,
|
|
148
|
+
style: {
|
|
149
|
+
position: "absolute",
|
|
150
|
+
inset: 0,
|
|
151
|
+
width: "100%",
|
|
152
|
+
height: "100%",
|
|
153
|
+
overflow: "visible",
|
|
154
|
+
pointerEvents: "none"
|
|
155
|
+
},
|
|
156
|
+
children: /* @__PURE__ */ jsxs("g", { children: [snapResult.snapLines.map((line, index) => line.axis === "x" ? /* @__PURE__ */ jsx("line", {
|
|
157
|
+
x1: line.value,
|
|
158
|
+
y1: visibleBounds.y,
|
|
159
|
+
x2: line.value,
|
|
160
|
+
y2: visibleBounds.y + visibleBounds.height,
|
|
161
|
+
stroke: "#ef4444",
|
|
162
|
+
strokeWidth,
|
|
163
|
+
strokeDasharray: `${6 / viewport.zoom} ${4 / viewport.zoom}`
|
|
164
|
+
}, `snap-x-${index}`) : /* @__PURE__ */ jsx("line", {
|
|
165
|
+
x1: visibleBounds.x,
|
|
166
|
+
y1: line.value,
|
|
167
|
+
x2: visibleBounds.x + visibleBounds.width,
|
|
168
|
+
y2: line.value,
|
|
169
|
+
stroke: "#ef4444",
|
|
170
|
+
strokeWidth,
|
|
171
|
+
strokeDasharray: `${6 / viewport.zoom} ${4 / viewport.zoom}`
|
|
172
|
+
}, `snap-y-${index}`)), snapResult.spacingGuides?.flatMap((guide, guideIndex) => guide.ranges.flatMap((range, rangeIndex) => {
|
|
173
|
+
const key = `spacing-${guideIndex}-${rangeIndex}`;
|
|
174
|
+
if (guide.axis === "x") return [
|
|
175
|
+
/* @__PURE__ */ jsx("line", {
|
|
176
|
+
x1: range.start,
|
|
177
|
+
y1: guide.position,
|
|
178
|
+
x2: range.end,
|
|
179
|
+
y2: guide.position,
|
|
180
|
+
stroke: "#ef4444",
|
|
181
|
+
strokeWidth
|
|
182
|
+
}, `${key}-line`),
|
|
183
|
+
/* @__PURE__ */ jsx("line", {
|
|
184
|
+
x1: range.start,
|
|
185
|
+
y1: guide.position - capSize,
|
|
186
|
+
x2: range.start,
|
|
187
|
+
y2: guide.position + capSize,
|
|
188
|
+
stroke: "#ef4444",
|
|
189
|
+
strokeWidth,
|
|
190
|
+
strokeLinecap: "round"
|
|
191
|
+
}, `${key}-start`),
|
|
192
|
+
/* @__PURE__ */ jsx("line", {
|
|
193
|
+
x1: range.end,
|
|
194
|
+
y1: guide.position - capSize,
|
|
195
|
+
x2: range.end,
|
|
196
|
+
y2: guide.position + capSize,
|
|
197
|
+
stroke: "#ef4444",
|
|
198
|
+
strokeWidth,
|
|
199
|
+
strokeLinecap: "round"
|
|
200
|
+
}, `${key}-end`)
|
|
201
|
+
];
|
|
202
|
+
return [
|
|
203
|
+
/* @__PURE__ */ jsx("line", {
|
|
204
|
+
x1: guide.position,
|
|
205
|
+
y1: range.start,
|
|
206
|
+
x2: guide.position,
|
|
207
|
+
y2: range.end,
|
|
208
|
+
stroke: "#ef4444",
|
|
209
|
+
strokeWidth
|
|
210
|
+
}, `${key}-line`),
|
|
211
|
+
/* @__PURE__ */ jsx("line", {
|
|
212
|
+
x1: guide.position - capSize,
|
|
213
|
+
y1: range.start,
|
|
214
|
+
x2: guide.position + capSize,
|
|
215
|
+
y2: range.start,
|
|
216
|
+
stroke: "#ef4444",
|
|
217
|
+
strokeWidth,
|
|
218
|
+
strokeLinecap: "round"
|
|
219
|
+
}, `${key}-start`),
|
|
220
|
+
/* @__PURE__ */ jsx("line", {
|
|
221
|
+
x1: guide.position - capSize,
|
|
222
|
+
y1: range.end,
|
|
223
|
+
x2: guide.position + capSize,
|
|
224
|
+
y2: range.end,
|
|
225
|
+
stroke: "#ef4444",
|
|
226
|
+
strokeWidth,
|
|
227
|
+
strokeLinecap: "round"
|
|
228
|
+
}, `${key}-end`)
|
|
229
|
+
];
|
|
230
|
+
}))] })
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function EdgeLayer(props) {
|
|
234
|
+
const instance = useFlowInstance();
|
|
235
|
+
const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
|
|
236
|
+
const edgeIds = useFlowSelector((context) => context.graph.edges.map((edge) => edge.id), { isEqual: stringArraysEqual });
|
|
237
|
+
const renderEdge = props.renderEdge;
|
|
238
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
239
|
+
"aria-label": "edges",
|
|
240
|
+
"data-flow-canvas-layer": true,
|
|
241
|
+
style: {
|
|
242
|
+
position: "absolute",
|
|
243
|
+
inset: 0,
|
|
244
|
+
width: "100%",
|
|
245
|
+
height: "100%",
|
|
246
|
+
overflow: "visible",
|
|
247
|
+
pointerEvents: "auto"
|
|
248
|
+
},
|
|
249
|
+
children: /* @__PURE__ */ jsxs("g", { children: [edgeIds.map((id) => {
|
|
250
|
+
if (renderableIds && !renderableIds.edgeIds.has(id)) return null;
|
|
251
|
+
return /* @__PURE__ */ jsx(EdgeView, {
|
|
252
|
+
flow: instance,
|
|
253
|
+
id,
|
|
254
|
+
renderEdge
|
|
255
|
+
}, id);
|
|
256
|
+
}), /* @__PURE__ */ jsx(ConnectionPreviewEdge, {
|
|
257
|
+
flow: instance,
|
|
258
|
+
renderEdge
|
|
259
|
+
})] })
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
const EdgeView = memo(function EdgeView(props) {
|
|
263
|
+
const { edge, pathData, selected, hovered, highlights } = useFlowEntity(props.id, (context) => ({
|
|
264
|
+
edge: props.flow.getEdge(props.id) ?? null,
|
|
265
|
+
pathData: props.flow.getAllEdgePaths().get(props.id) ?? null,
|
|
266
|
+
selected: context.selection.has(props.id),
|
|
267
|
+
hovered: context.hoveredEntityIds.has(props.id),
|
|
268
|
+
highlights: props.flow.engine.getHighlights(props.id)
|
|
269
|
+
}), { isEqual: (a, b) => a.edge === b.edge && a.pathData === b.pathData && a.selected === b.selected && a.hovered === b.hovered && a.highlights === b.highlights });
|
|
270
|
+
if (!edge || !pathData) return null;
|
|
271
|
+
if (props.renderEdge) return /* @__PURE__ */ jsxs("g", {
|
|
272
|
+
"data-entity-id": props.id,
|
|
273
|
+
"data-flow-selected": selected ? "true" : void 0,
|
|
274
|
+
style: { pointerEvents: "auto" },
|
|
275
|
+
children: [/* @__PURE__ */ jsx(EdgeHitTarget, {
|
|
276
|
+
flow: props.flow,
|
|
277
|
+
edge,
|
|
278
|
+
pathData,
|
|
279
|
+
selected
|
|
280
|
+
}), props.renderEdge({
|
|
281
|
+
flow: props.flow,
|
|
282
|
+
edge,
|
|
283
|
+
pathData,
|
|
284
|
+
selected,
|
|
285
|
+
hovered,
|
|
286
|
+
highlights
|
|
287
|
+
})]
|
|
288
|
+
});
|
|
289
|
+
return /* @__PURE__ */ jsx("g", {
|
|
290
|
+
"data-entity-id": props.id,
|
|
291
|
+
"data-flow-selected": selected ? "true" : void 0,
|
|
292
|
+
style: { pointerEvents: "auto" },
|
|
293
|
+
children: /* @__PURE__ */ jsx(DefaultEdge, {
|
|
294
|
+
flow: props.flow,
|
|
295
|
+
edge,
|
|
296
|
+
pathData,
|
|
297
|
+
selected
|
|
298
|
+
})
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
/** The in-progress connection gesture's preview edge (renders only mid-gesture). */
|
|
302
|
+
function ConnectionPreviewEdge(props) {
|
|
303
|
+
const connection = useFlowSelector((context) => context.connection);
|
|
304
|
+
if (!connection?.active) return null;
|
|
305
|
+
const previewGraph = createConnectionPreviewGraph(props.flow.getSnapshot().graph, connection);
|
|
306
|
+
const pathData = props.flow.getAllEdgePaths({ graph: previewGraph }).get(CONNECTION_PREVIEW_EDGE_ID);
|
|
307
|
+
const edge = previewGraph.edges.find((candidate) => candidate.id === CONNECTION_PREVIEW_EDGE_ID);
|
|
308
|
+
if (!edge || !pathData) return null;
|
|
309
|
+
return /* @__PURE__ */ jsx("g", {
|
|
310
|
+
style: { pointerEvents: "none" },
|
|
311
|
+
children: props.renderEdge ? props.renderEdge({
|
|
312
|
+
flow: props.flow,
|
|
313
|
+
edge,
|
|
314
|
+
pathData,
|
|
315
|
+
selected: false,
|
|
316
|
+
hovered: false,
|
|
317
|
+
highlights: []
|
|
318
|
+
}) : /* @__PURE__ */ jsx(DefaultEdge, {
|
|
319
|
+
flow: props.flow,
|
|
320
|
+
edge,
|
|
321
|
+
pathData,
|
|
322
|
+
selected: false,
|
|
323
|
+
preview: true
|
|
324
|
+
})
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
function DefaultEdge(props) {
|
|
328
|
+
return /* @__PURE__ */ jsx(Fragment, { children: props.pathData.segments.map((segment, index) => {
|
|
329
|
+
const d = pathToSVG(segment);
|
|
330
|
+
return /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("path", {
|
|
331
|
+
d,
|
|
332
|
+
fill: "none",
|
|
333
|
+
stroke: props.preview ? props.edge.data?.isValid === false ? "#dc2626" : props.edge.data?.isValid === true ? "#16a34a" : "#0f766e" : props.selected ? "#2563eb" : "#94a3b8",
|
|
334
|
+
strokeDasharray: props.preview ? "6 6" : void 0,
|
|
335
|
+
strokeWidth: props.selected ? 3 : 2
|
|
336
|
+
}), props.preview ? null : /* @__PURE__ */ jsx("path", {
|
|
337
|
+
d,
|
|
338
|
+
"data-entity-id": props.edge.id,
|
|
339
|
+
fill: "none",
|
|
340
|
+
stroke: "#000",
|
|
341
|
+
strokeOpacity: 0,
|
|
342
|
+
strokeWidth: 18,
|
|
343
|
+
pointerEvents: "stroke"
|
|
344
|
+
})] }, index);
|
|
345
|
+
}) });
|
|
346
|
+
}
|
|
347
|
+
function EdgeHitTarget(props) {
|
|
348
|
+
return /* @__PURE__ */ jsx(Fragment, { children: props.pathData.segments.map((segment, index) => {
|
|
349
|
+
return /* @__PURE__ */ jsx("path", {
|
|
350
|
+
d: pathToSVG(segment),
|
|
351
|
+
"data-entity-id": props.edge.id,
|
|
352
|
+
fill: "none",
|
|
353
|
+
stroke: "#000",
|
|
354
|
+
strokeOpacity: 0,
|
|
355
|
+
strokeWidth: 18,
|
|
356
|
+
pointerEvents: "stroke"
|
|
357
|
+
}, index);
|
|
358
|
+
}) });
|
|
359
|
+
}
|
|
360
|
+
function EdgeLabelLayer(props) {
|
|
361
|
+
const instance = useFlowInstance();
|
|
362
|
+
const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
|
|
363
|
+
return /* @__PURE__ */ jsx("div", {
|
|
364
|
+
"aria-label": "edge labels",
|
|
365
|
+
style: {
|
|
366
|
+
position: "absolute",
|
|
367
|
+
inset: 0,
|
|
368
|
+
pointerEvents: "none"
|
|
369
|
+
},
|
|
370
|
+
children: useFlowSelector((context) => context.graph.edges.filter((edge) => edge.label).map((edge) => edge.id), { isEqual: stringArraysEqual }).map((id) => {
|
|
371
|
+
if (renderableIds && !renderableIds.edgeIds.has(id)) return null;
|
|
372
|
+
return /* @__PURE__ */ jsx(EdgeLabelItem, {
|
|
373
|
+
flow: instance,
|
|
374
|
+
id
|
|
375
|
+
}, id);
|
|
376
|
+
})
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
const EdgeLabelItem = memo(function EdgeLabelItem(props) {
|
|
380
|
+
const { edge, pathData, selected } = useFlowEntity(props.id, (context) => ({
|
|
381
|
+
edge: props.flow.getEdge(props.id) ?? null,
|
|
382
|
+
pathData: props.flow.getAllEdgePaths().get(props.id) ?? null,
|
|
383
|
+
selected: context.selection.has(props.id)
|
|
384
|
+
}), { isEqual: (a, b) => a.edge === b.edge && a.pathData === b.pathData && a.selected === b.selected });
|
|
385
|
+
if (!edge?.label || !pathData?.labelBounds) return null;
|
|
386
|
+
return /* @__PURE__ */ jsx(EdgeLabelView, {
|
|
387
|
+
flow: props.flow,
|
|
388
|
+
edge,
|
|
389
|
+
bounds: pathData.labelBounds,
|
|
390
|
+
selected
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
function EdgeLabelView(props) {
|
|
394
|
+
const ref = useRef(null);
|
|
395
|
+
const observer = useMemo(() => createMeasurementObserver(props.flow.store), [props.flow.store]);
|
|
396
|
+
useLayoutEffect(() => {
|
|
397
|
+
const element = ref.current;
|
|
398
|
+
if (!element) return;
|
|
399
|
+
observer.observe(element, { edgeId: props.edge.id });
|
|
400
|
+
return () => observer.disconnect();
|
|
401
|
+
}, [observer, props.edge.id]);
|
|
402
|
+
const text = (props.edge.label?.data)?.text;
|
|
403
|
+
return /* @__PURE__ */ jsx("div", {
|
|
404
|
+
ref,
|
|
405
|
+
"data-entity-id": props.edge.id,
|
|
406
|
+
"data-flow-edge-label": "",
|
|
407
|
+
style: {
|
|
408
|
+
position: "absolute",
|
|
409
|
+
left: props.bounds.x,
|
|
410
|
+
top: props.bounds.y,
|
|
411
|
+
width: props.bounds.width,
|
|
412
|
+
height: props.bounds.height,
|
|
413
|
+
boxSizing: "border-box",
|
|
414
|
+
padding: "4px 8px",
|
|
415
|
+
border: `1px solid ${props.selected ? "#2563eb" : "#cbd5e1"}`,
|
|
416
|
+
borderRadius: 999,
|
|
417
|
+
background: "#fff",
|
|
418
|
+
color: "#334155",
|
|
419
|
+
display: "grid",
|
|
420
|
+
placeItems: "center",
|
|
421
|
+
cursor: "grab",
|
|
422
|
+
font: "600 12px/1 system-ui, sans-serif",
|
|
423
|
+
pointerEvents: "auto",
|
|
424
|
+
userSelect: "none"
|
|
425
|
+
},
|
|
426
|
+
children: String(text ?? props.edge.id)
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
function NodeLayer(props) {
|
|
430
|
+
const instance = useFlowInstance();
|
|
431
|
+
const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
|
|
432
|
+
const nodeIds = useFlowSelector((context) => {
|
|
433
|
+
const depths = nodeDepths(context.graph.nodes);
|
|
434
|
+
return context.graph.nodes.map((node) => node.id).sort((a, b) => (depths.get(a) ?? 0) - (depths.get(b) ?? 0));
|
|
435
|
+
}, { isEqual: stringArraysEqual });
|
|
436
|
+
const observer = useMemo(() => createMeasurementObserver(instance.store), [instance]);
|
|
437
|
+
useLayoutEffect(() => () => observer.disconnect(), [observer]);
|
|
438
|
+
return /* @__PURE__ */ jsx("div", {
|
|
439
|
+
"aria-label": "nodes",
|
|
440
|
+
"data-flow-canvas-layer": true,
|
|
441
|
+
style: {
|
|
442
|
+
position: "absolute",
|
|
443
|
+
inset: 0,
|
|
444
|
+
transformOrigin: "0 0",
|
|
445
|
+
pointerEvents: "none"
|
|
446
|
+
},
|
|
447
|
+
children: nodeIds.map((id) => {
|
|
448
|
+
if (renderableIds && !renderableIds.nodeIds.has(id)) return null;
|
|
449
|
+
return /* @__PURE__ */ jsx(NodeView, {
|
|
450
|
+
flow: instance,
|
|
451
|
+
id,
|
|
452
|
+
observer,
|
|
453
|
+
renderNode: props.renderNode
|
|
454
|
+
}, id);
|
|
455
|
+
})
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
function rectsShallowEqual(a, b) {
|
|
459
|
+
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
|
|
460
|
+
}
|
|
461
|
+
const NodeView = memo(function NodeView(props) {
|
|
462
|
+
const ref = useRef(null);
|
|
463
|
+
const { node, hidden, selected, dragging, focused, hovered, highlights, bounds } = useFlowEntity(props.id, (context) => {
|
|
464
|
+
const current = props.flow.getNode(props.id) ?? null;
|
|
465
|
+
return {
|
|
466
|
+
node: current,
|
|
467
|
+
hidden: !current || props.flow.engine.resolveEntityConfig(current).hidden,
|
|
468
|
+
selected: context.selection.has(props.id),
|
|
469
|
+
dragging: context.draggedEntityId === props.id,
|
|
470
|
+
focused: context.focusedEntityId === props.id,
|
|
471
|
+
hovered: context.hoveredEntityIds.has(props.id),
|
|
472
|
+
highlights: props.flow.engine.getHighlights(props.id),
|
|
473
|
+
bounds: context.resizingEntityId === props.id && context.resizeCorners ? props.flow.engine.getResizeBounds() ?? props.flow.engine.resolvedBounds(props.id) : props.flow.engine.resolvedBounds(props.id)
|
|
474
|
+
};
|
|
475
|
+
}, { isEqual: (a, b) => a.node === b.node && a.hidden === b.hidden && a.selected === b.selected && a.dragging === b.dragging && a.focused === b.focused && a.hovered === b.hovered && a.highlights === b.highlights && rectsShallowEqual(a.bounds, b.bounds) });
|
|
476
|
+
useLayoutEffect(() => {
|
|
477
|
+
const element = ref.current;
|
|
478
|
+
if (!element) return;
|
|
479
|
+
const target = props.flow.getNode(props.id)?.group?.type === "derived" ? element.querySelector("[data-flow-group-content-measure]") : element;
|
|
480
|
+
if (!target) return;
|
|
481
|
+
props.observer.observe(target, { nodeId: props.id });
|
|
482
|
+
return () => props.observer.unobserve(target);
|
|
483
|
+
}, [
|
|
484
|
+
props.id,
|
|
485
|
+
props.observer,
|
|
486
|
+
props.flow,
|
|
487
|
+
hidden
|
|
488
|
+
]);
|
|
489
|
+
if (!node || hidden) return null;
|
|
490
|
+
return /* @__PURE__ */ jsx(NodeRenderContextProvider, {
|
|
491
|
+
value: {
|
|
492
|
+
node,
|
|
493
|
+
bounds
|
|
494
|
+
},
|
|
495
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
496
|
+
ref,
|
|
497
|
+
"data-entity-id": props.id,
|
|
498
|
+
"data-flow-selected": selected ? "true" : void 0,
|
|
499
|
+
role: "group",
|
|
500
|
+
"aria-roledescription": "flow node",
|
|
501
|
+
"aria-label": props.flow.engine.getAriaLabel(props.id) + (selected ? ", selected" : ""),
|
|
502
|
+
tabIndex: 0,
|
|
503
|
+
onFocus: () => props.flow.trigger.focus({ entityId: props.id }),
|
|
504
|
+
style: {
|
|
505
|
+
...nodePositionStyle(bounds, {
|
|
506
|
+
autoSize: node.autoSize,
|
|
507
|
+
dragging
|
|
508
|
+
}),
|
|
509
|
+
cursor: dragging ? "grabbing" : "grab",
|
|
510
|
+
pointerEvents: "auto",
|
|
511
|
+
userSelect: "none",
|
|
512
|
+
outline: focused ? "2px solid #2563eb" : "none",
|
|
513
|
+
outlineOffset: 2
|
|
514
|
+
},
|
|
515
|
+
children: props.renderNode ? props.renderNode({
|
|
516
|
+
flow: props.flow,
|
|
517
|
+
node,
|
|
518
|
+
bounds,
|
|
519
|
+
selected,
|
|
520
|
+
dragging,
|
|
521
|
+
hovered,
|
|
522
|
+
highlights
|
|
523
|
+
}) : node.group ? /* @__PURE__ */ jsx(DefaultGroupNode, {
|
|
524
|
+
flow: props.flow,
|
|
525
|
+
node,
|
|
526
|
+
bounds,
|
|
527
|
+
selected,
|
|
528
|
+
dragging,
|
|
529
|
+
hovered,
|
|
530
|
+
highlights
|
|
531
|
+
}) : /* @__PURE__ */ jsx(DefaultNode, {
|
|
532
|
+
flow: props.flow,
|
|
533
|
+
node,
|
|
534
|
+
bounds,
|
|
535
|
+
selected,
|
|
536
|
+
dragging,
|
|
537
|
+
hovered,
|
|
538
|
+
highlights
|
|
539
|
+
})
|
|
540
|
+
})
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
/**
|
|
544
|
+
* Default group rendering: content block (the drag handle) on its configured
|
|
545
|
+
* side plus a children-area backdrop. The geometry comes entirely from
|
|
546
|
+
* `engine.getGroupGeometry`; custom renderers can do the same.
|
|
547
|
+
*/
|
|
548
|
+
function DefaultGroupNode(props) {
|
|
549
|
+
const geometry = props.flow.engine.getGroupGeometry(props.node.id);
|
|
550
|
+
const label = props.node.data?.label;
|
|
551
|
+
if (!geometry) return /* @__PURE__ */ jsx(DefaultNode, { ...props });
|
|
552
|
+
const relative = (rect) => ({
|
|
553
|
+
left: rect.x - props.bounds.x,
|
|
554
|
+
top: rect.y - props.bounds.y,
|
|
555
|
+
width: rect.width,
|
|
556
|
+
height: rect.height
|
|
557
|
+
});
|
|
558
|
+
const dropTarget = props.highlights.some((entry) => entry.kind === "drop-target");
|
|
559
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", { style: {
|
|
560
|
+
position: "absolute",
|
|
561
|
+
...relative(geometry.childArea),
|
|
562
|
+
boxSizing: "border-box",
|
|
563
|
+
border: `2px dashed ${dropTarget ? "#16a34a" : props.selected ? "#93c5fd" : "#cbd5e1"}`,
|
|
564
|
+
...dropTarget ? { background: "rgba(22, 163, 74, 0.08)" } : null,
|
|
565
|
+
borderRadius: 10,
|
|
566
|
+
background: "rgba(148, 163, 184, 0.07)",
|
|
567
|
+
pointerEvents: "none"
|
|
568
|
+
} }), /* @__PURE__ */ jsx("div", {
|
|
569
|
+
"data-flow-drag-handle": true,
|
|
570
|
+
style: {
|
|
571
|
+
position: "absolute",
|
|
572
|
+
...relative(geometry.contentBounds),
|
|
573
|
+
boxSizing: "border-box",
|
|
574
|
+
display: "flex",
|
|
575
|
+
alignItems: "center",
|
|
576
|
+
padding: "0 12px",
|
|
577
|
+
border: `2px solid ${props.selected ? "#2563eb" : "#94a3b8"}`,
|
|
578
|
+
borderRadius: 8,
|
|
579
|
+
background: props.selected ? "#eff6ff" : "#f8fafc",
|
|
580
|
+
color: "#0f172a",
|
|
581
|
+
font: "600 13px/1.2 system-ui, sans-serif",
|
|
582
|
+
cursor: props.dragging ? "grabbing" : "grab",
|
|
583
|
+
userSelect: "none"
|
|
584
|
+
},
|
|
585
|
+
children: String(label ?? props.node.id)
|
|
586
|
+
})] });
|
|
587
|
+
}
|
|
588
|
+
function DefaultNode(props) {
|
|
589
|
+
const label = props.node.data?.label;
|
|
590
|
+
return /* @__PURE__ */ jsx("div", {
|
|
591
|
+
style: {
|
|
592
|
+
display: "grid",
|
|
593
|
+
placeItems: "center",
|
|
594
|
+
width: "100%",
|
|
595
|
+
height: "100%",
|
|
596
|
+
boxSizing: "border-box",
|
|
597
|
+
padding: "10px 14px",
|
|
598
|
+
border: `2px solid ${props.selected ? "#2563eb" : props.hovered ? "#94a3b8" : "#cbd5e1"}`,
|
|
599
|
+
borderRadius: 8,
|
|
600
|
+
background: props.selected ? "#eff6ff" : "#ffffff",
|
|
601
|
+
color: "#0f172a",
|
|
602
|
+
font: "600 14px/1.2 system-ui, sans-serif",
|
|
603
|
+
boxShadow: props.selected ? "0 0 0 4px rgba(37, 99, 235, 0.16)" : "0 1px 2px rgba(15, 23, 42, 0.1)"
|
|
604
|
+
},
|
|
605
|
+
children: String(label ?? props.node.id)
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
//#endregion
|
|
609
|
+
//#region src/FlowToolbar.tsx
|
|
610
|
+
/** Border-box size of an element, kept current via ResizeObserver. */
|
|
611
|
+
function useMeasuredSize(deps = []) {
|
|
612
|
+
const [size, setSize] = useState({
|
|
613
|
+
width: 0,
|
|
614
|
+
height: 0
|
|
615
|
+
});
|
|
616
|
+
const ref = useRef(null);
|
|
617
|
+
const measure = useCallback((node) => {
|
|
618
|
+
const rect = node.getBoundingClientRect();
|
|
619
|
+
setSize((prev) => prev.width === rect.width && prev.height === rect.height ? prev : {
|
|
620
|
+
width: rect.width,
|
|
621
|
+
height: rect.height
|
|
622
|
+
});
|
|
623
|
+
}, []);
|
|
624
|
+
const setRef = useCallback((node) => {
|
|
625
|
+
ref.current = node;
|
|
626
|
+
if (node) measure(node);
|
|
627
|
+
}, [measure]);
|
|
628
|
+
useLayoutEffect(() => {
|
|
629
|
+
const node = ref.current;
|
|
630
|
+
if (!node) return;
|
|
631
|
+
measure(node);
|
|
632
|
+
const observer = new ResizeObserver(() => measure(node));
|
|
633
|
+
observer.observe(node);
|
|
634
|
+
return () => observer.disconnect();
|
|
635
|
+
}, [measure, ...deps]);
|
|
636
|
+
return [setRef, size];
|
|
637
|
+
}
|
|
638
|
+
function rectsEqual(a, b) {
|
|
639
|
+
return a === b || a !== null && b !== null && a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
|
|
640
|
+
}
|
|
641
|
+
function useFlowToolbar(options = {}) {
|
|
642
|
+
const flow = useFlowInstance();
|
|
643
|
+
const idsKey = options.ids?.join(" ");
|
|
644
|
+
const anchor = useFlowSelector((context) => {
|
|
645
|
+
const ids = options.ids ?? [...context.selection];
|
|
646
|
+
if (ids.length === 0) return null;
|
|
647
|
+
const canvasRect = flow.engine.getToolbarAnchor(ids);
|
|
648
|
+
if (!canvasRect) return null;
|
|
649
|
+
const zoom = context.viewport.zoom;
|
|
650
|
+
return {
|
|
651
|
+
x: canvasRect.x * zoom + context.viewport.x,
|
|
652
|
+
y: canvasRect.y * zoom + context.viewport.y,
|
|
653
|
+
width: canvasRect.width * zoom,
|
|
654
|
+
height: canvasRect.height * zoom
|
|
655
|
+
};
|
|
656
|
+
}, {
|
|
657
|
+
flow,
|
|
658
|
+
isEqual: rectsEqual
|
|
659
|
+
});
|
|
660
|
+
const viewportSize = useFlowSelector((context) => context.viewportSize, { flow });
|
|
661
|
+
const [setRef, size] = useMeasuredSize([!!anchor, idsKey]);
|
|
662
|
+
if (!anchor) return {
|
|
663
|
+
setRef,
|
|
664
|
+
style: { display: "none" },
|
|
665
|
+
visible: false,
|
|
666
|
+
anchor: null,
|
|
667
|
+
side: "top"
|
|
668
|
+
};
|
|
669
|
+
const placement = getToolbarPlacement({
|
|
670
|
+
anchor,
|
|
671
|
+
toolbarWidth: size.width,
|
|
672
|
+
toolbarHeight: size.height,
|
|
673
|
+
viewportWidth: viewportSize.width,
|
|
674
|
+
viewportHeight: viewportSize.height,
|
|
675
|
+
offset: options.offset,
|
|
676
|
+
padding: options.padding
|
|
677
|
+
});
|
|
678
|
+
return {
|
|
679
|
+
setRef,
|
|
680
|
+
style: {
|
|
681
|
+
position: "absolute",
|
|
682
|
+
left: placement.left,
|
|
683
|
+
top: placement.top,
|
|
684
|
+
visibility: size.width > 0 ? "visible" : "hidden",
|
|
685
|
+
pointerEvents: "auto"
|
|
686
|
+
},
|
|
687
|
+
visible: true,
|
|
688
|
+
anchor,
|
|
689
|
+
side: placement.side
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Unstyled positioned toolbar anchored to the selection (or explicit `ids`).
|
|
694
|
+
* Marked `data-flow-interactive`, so its contents receive pointer events
|
|
695
|
+
* unopposed by canvas gestures.
|
|
696
|
+
*/
|
|
697
|
+
function FlowToolbar(props) {
|
|
698
|
+
const { children, className, style, ...options } = props;
|
|
699
|
+
const toolbar = useFlowToolbar(options);
|
|
700
|
+
if (!toolbar.visible) return null;
|
|
701
|
+
return /* @__PURE__ */ jsx("div", {
|
|
702
|
+
ref: toolbar.setRef,
|
|
703
|
+
"data-flow-interactive": true,
|
|
704
|
+
className,
|
|
705
|
+
style: {
|
|
706
|
+
...toolbar.style,
|
|
707
|
+
...style
|
|
708
|
+
},
|
|
709
|
+
children
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
//#endregion
|
|
713
|
+
//#region src/MiniMap.tsx
|
|
714
|
+
/**
|
|
715
|
+
* Overview map. Deliberately thin: all math lives in core
|
|
716
|
+
* (`getMinimapProjection`, `viewportCenteredOn`) and every interaction is a
|
|
717
|
+
* plain store trigger — this component is exactly what you would write in
|
|
718
|
+
* userland, kept here as a convenience.
|
|
719
|
+
*/
|
|
720
|
+
function MiniMap(props) {
|
|
721
|
+
const flow = useFlowInstance();
|
|
722
|
+
const width = props.width ?? 200;
|
|
723
|
+
const height = props.height ?? 140;
|
|
724
|
+
const nodes = useFlowSelector((context) => context.graph.nodes);
|
|
725
|
+
const viewport = useFlowSelector((context) => context.viewport);
|
|
726
|
+
const viewportSize = useFlowSelector((context) => context.viewportSize);
|
|
727
|
+
const draggingRef = useRef(false);
|
|
728
|
+
const projection = getMinimapProjection({
|
|
729
|
+
contentBounds: flow.getGraphBounds(),
|
|
730
|
+
viewport,
|
|
731
|
+
viewportSize,
|
|
732
|
+
width,
|
|
733
|
+
height,
|
|
734
|
+
padding: props.padding
|
|
735
|
+
});
|
|
736
|
+
const jumpTo = (event) => {
|
|
737
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
738
|
+
const canvasPoint = projection.toCanvas({
|
|
739
|
+
x: event.clientX - rect.left,
|
|
740
|
+
y: event.clientY - rect.top
|
|
741
|
+
});
|
|
742
|
+
flow.trigger.setViewport(viewportCenteredOn(canvasPoint, viewportSize, viewport.zoom));
|
|
743
|
+
};
|
|
744
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
745
|
+
"aria-label": "minimap",
|
|
746
|
+
width,
|
|
747
|
+
height,
|
|
748
|
+
className: props.className,
|
|
749
|
+
onPointerDown: (event) => {
|
|
750
|
+
draggingRef.current = true;
|
|
751
|
+
jumpTo(event);
|
|
752
|
+
try {
|
|
753
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
754
|
+
} catch {}
|
|
755
|
+
},
|
|
756
|
+
onPointerMove: (event) => {
|
|
757
|
+
if (draggingRef.current) jumpTo(event);
|
|
758
|
+
},
|
|
759
|
+
onPointerUp: (event) => {
|
|
760
|
+
draggingRef.current = false;
|
|
761
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
|
|
762
|
+
},
|
|
763
|
+
style: {
|
|
764
|
+
position: "absolute",
|
|
765
|
+
right: 12,
|
|
766
|
+
bottom: 12,
|
|
767
|
+
background: "rgba(248, 250, 252, 0.9)",
|
|
768
|
+
border: "1px solid #e2e8f0",
|
|
769
|
+
borderRadius: 8,
|
|
770
|
+
boxShadow: "0 2px 10px rgba(15, 23, 42, 0.12)",
|
|
771
|
+
cursor: "pointer",
|
|
772
|
+
pointerEvents: "auto",
|
|
773
|
+
...props.style
|
|
774
|
+
},
|
|
775
|
+
children: [nodes.map((node) => {
|
|
776
|
+
const rect = projection.rectToMinimap(flow.engine.resolvedBounds(node.id));
|
|
777
|
+
return props.renderNode?.(node, rect) ?? /* @__PURE__ */ jsx("rect", {
|
|
778
|
+
x: rect.x,
|
|
779
|
+
y: rect.y,
|
|
780
|
+
width: Math.max(rect.width, 1),
|
|
781
|
+
height: Math.max(rect.height, 1),
|
|
782
|
+
rx: 1,
|
|
783
|
+
fill: "#94a3b8"
|
|
784
|
+
}, node.id);
|
|
785
|
+
}), /* @__PURE__ */ jsx("rect", {
|
|
786
|
+
x: projection.viewportRect.x,
|
|
787
|
+
y: projection.viewportRect.y,
|
|
788
|
+
width: projection.viewportRect.width,
|
|
789
|
+
height: projection.viewportRect.height,
|
|
790
|
+
fill: "rgba(37, 99, 235, 0.08)",
|
|
791
|
+
stroke: "#2563eb",
|
|
792
|
+
strokeWidth: 1
|
|
793
|
+
})]
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
//#endregion
|
|
797
|
+
//#region src/Controls.tsx
|
|
798
|
+
const buttonStyle = {
|
|
799
|
+
width: 28,
|
|
800
|
+
height: 28,
|
|
801
|
+
display: "grid",
|
|
802
|
+
placeItems: "center",
|
|
803
|
+
border: "1px solid #e2e8f0",
|
|
804
|
+
borderRadius: 6,
|
|
805
|
+
background: "#ffffff",
|
|
806
|
+
color: "#0f172a",
|
|
807
|
+
font: "600 14px/1 system-ui, sans-serif",
|
|
808
|
+
cursor: "pointer"
|
|
809
|
+
};
|
|
810
|
+
/**
|
|
811
|
+
* Zoom/fit toolbar. Deliberately thin: every button is one store trigger
|
|
812
|
+
* (`zoomBy`, `fitView`) — replicate or extend it freely in userland.
|
|
813
|
+
*/
|
|
814
|
+
function Controls(props) {
|
|
815
|
+
const flow = useFlowInstance();
|
|
816
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
817
|
+
role: "toolbar",
|
|
818
|
+
"aria-label": "canvas controls",
|
|
819
|
+
className: props.className,
|
|
820
|
+
style: {
|
|
821
|
+
position: "absolute",
|
|
822
|
+
left: 12,
|
|
823
|
+
bottom: 12,
|
|
824
|
+
display: "flex",
|
|
825
|
+
flexDirection: "column",
|
|
826
|
+
gap: 4,
|
|
827
|
+
pointerEvents: "auto",
|
|
828
|
+
...props.style
|
|
829
|
+
},
|
|
830
|
+
children: [
|
|
831
|
+
/* @__PURE__ */ jsx("button", {
|
|
832
|
+
type: "button",
|
|
833
|
+
"aria-label": "zoom in",
|
|
834
|
+
style: buttonStyle,
|
|
835
|
+
onClick: () => flow.trigger.zoomBy({ factor: 1.2 }),
|
|
836
|
+
children: "+"
|
|
837
|
+
}),
|
|
838
|
+
/* @__PURE__ */ jsx("button", {
|
|
839
|
+
type: "button",
|
|
840
|
+
"aria-label": "zoom out",
|
|
841
|
+
style: buttonStyle,
|
|
842
|
+
onClick: () => flow.trigger.zoomBy({ factor: 1 / 1.2 }),
|
|
843
|
+
children: "−"
|
|
844
|
+
}),
|
|
845
|
+
/* @__PURE__ */ jsx("button", {
|
|
846
|
+
type: "button",
|
|
847
|
+
"aria-label": "fit view",
|
|
848
|
+
style: buttonStyle,
|
|
849
|
+
onClick: () => flow.trigger.fitView({ padding: .1 }),
|
|
850
|
+
children: "⛶"
|
|
851
|
+
}),
|
|
852
|
+
props.children
|
|
853
|
+
]
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
//#endregion
|
|
857
|
+
//#region src/EntityAnchor.tsx
|
|
858
|
+
function EntityAnchor(props) {
|
|
859
|
+
const viewport = useFlowSelector((context) => context.viewport);
|
|
860
|
+
const transform = getAnchorTransform(props.bounds, viewport, props.anchor);
|
|
861
|
+
return /* @__PURE__ */ jsx("div", {
|
|
862
|
+
className: props.className,
|
|
863
|
+
style: {
|
|
864
|
+
position: "absolute",
|
|
865
|
+
left: transform.x,
|
|
866
|
+
top: transform.y,
|
|
867
|
+
transform: props.inverseScale ? `translate(-50%, -50%) scale(${transform.scale})` : "translate(-50%, -50%)",
|
|
868
|
+
transformOrigin: "center",
|
|
869
|
+
pointerEvents: "auto",
|
|
870
|
+
...props.style
|
|
871
|
+
},
|
|
872
|
+
children: props.children
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
//#endregion
|
|
876
|
+
//#region src/Port.tsx
|
|
877
|
+
/**
|
|
878
|
+
* A connection port. Purely declarative: it renders the
|
|
879
|
+
* `data-flow-port-name`/`data-flow-node-id` attributes that the input
|
|
880
|
+
* runtime's `connectMode` recognizes, and reports its measured bounds to the
|
|
881
|
+
* store. The connection gesture itself (start/move/validate/end) lives in
|
|
882
|
+
* core — customize it by swapping `connectMode({ isValid })` in the
|
|
883
|
+
* `inputModes` prop of `StatelyFlow` (or your own `useFlowInput` call), and
|
|
884
|
+
* observe results via `flow.on("connectionEnded", …)` or the store's
|
|
885
|
+
* `connection` state.
|
|
886
|
+
*/
|
|
887
|
+
function Port(props) {
|
|
888
|
+
const flow = useFlowInstance();
|
|
889
|
+
const { node } = useNodeRenderContext();
|
|
890
|
+
const ref = useRef(null);
|
|
891
|
+
const observerRef = useRef(null);
|
|
892
|
+
const measured = props.measured ?? true;
|
|
893
|
+
useLayoutEffect(() => {
|
|
894
|
+
const element = ref.current;
|
|
895
|
+
if (!element || !measured) return;
|
|
896
|
+
const observer = createMeasurementObserver(flow.store);
|
|
897
|
+
observerRef.current = observer;
|
|
898
|
+
observer.observe(element, {
|
|
899
|
+
nodeId: node.id,
|
|
900
|
+
portName: props.name
|
|
901
|
+
});
|
|
902
|
+
return () => {
|
|
903
|
+
observerRef.current = null;
|
|
904
|
+
observer.disconnect();
|
|
905
|
+
};
|
|
906
|
+
}, [
|
|
907
|
+
flow.store,
|
|
908
|
+
node.id,
|
|
909
|
+
props.name,
|
|
910
|
+
measured
|
|
911
|
+
]);
|
|
912
|
+
useLayoutEffect(() => {
|
|
913
|
+
const element = ref.current;
|
|
914
|
+
if (!element) return;
|
|
915
|
+
if (flow.getSnapshot().canvasState !== "idle") return;
|
|
916
|
+
observerRef.current?.measure(element);
|
|
917
|
+
});
|
|
918
|
+
const candidate = useFlowSelector((context) => {
|
|
919
|
+
const connection = context.connection;
|
|
920
|
+
if (!connection?.active) return null;
|
|
921
|
+
if (connection.targetId !== node.id || connection.targetPort !== props.name) return null;
|
|
922
|
+
return connection.isValid === false ? "invalid" : "valid";
|
|
923
|
+
}, { flow });
|
|
924
|
+
return /* @__PURE__ */ jsx("div", {
|
|
925
|
+
ref,
|
|
926
|
+
className: props.className,
|
|
927
|
+
"data-flow-port-name": props.name,
|
|
928
|
+
"data-flow-node-id": node.id,
|
|
929
|
+
"data-connection-candidate": candidate ?? void 0,
|
|
930
|
+
style: props.style,
|
|
931
|
+
children: props.children
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
//#endregion
|
|
935
|
+
//#region src/ColorMode.tsx
|
|
936
|
+
function FlowColorModeScope(props) {
|
|
937
|
+
const { colorMode, resolvedColorMode } = useFlowColorMode();
|
|
938
|
+
return /* @__PURE__ */ jsx("div", {
|
|
939
|
+
className: props.className,
|
|
940
|
+
"data-flow-color-mode": resolvedColorMode,
|
|
941
|
+
"data-flow-color-mode-preference": colorMode,
|
|
942
|
+
style: {
|
|
943
|
+
colorScheme: resolvedColorMode,
|
|
944
|
+
...props.style
|
|
945
|
+
},
|
|
946
|
+
children: props.children
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
function useFlowColorMode() {
|
|
950
|
+
const colorMode = useFlowSelector((context) => context.colorMode);
|
|
951
|
+
const [systemColorMode, setSystemColorMode] = useState(() => resolveFlowColorMode("system"));
|
|
952
|
+
useEffect(() => {
|
|
953
|
+
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
954
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
955
|
+
function update() {
|
|
956
|
+
setSystemColorMode(resolveFlowColorMode("system"));
|
|
957
|
+
}
|
|
958
|
+
update();
|
|
959
|
+
media.addEventListener("change", update);
|
|
960
|
+
return () => media.removeEventListener("change", update);
|
|
961
|
+
}, []);
|
|
962
|
+
return useMemo(() => ({
|
|
963
|
+
colorMode,
|
|
964
|
+
resolvedColorMode: colorMode === "system" ? systemColorMode : colorMode
|
|
965
|
+
}), [colorMode, systemColorMode]);
|
|
966
|
+
}
|
|
967
|
+
function useApplyFlowColorMode(element) {
|
|
968
|
+
const { colorMode, resolvedColorMode } = useFlowColorMode();
|
|
969
|
+
useEffect(() => {
|
|
970
|
+
if (!element) return;
|
|
971
|
+
applyFlowColorMode(element, colorMode);
|
|
972
|
+
}, [
|
|
973
|
+
colorMode,
|
|
974
|
+
element,
|
|
975
|
+
resolvedColorMode
|
|
976
|
+
]);
|
|
977
|
+
}
|
|
978
|
+
//#endregion
|
|
979
|
+
export { Controls, EntityAnchor, FlowColorModeScope, FlowInstanceProvider, FlowToolbar, MiniMap, Port, StatelyFlow, createFlowInstance, useApplyFlowColorMode, useCreateFlowInstance, useFlow, useFlowColorMode, useFlowContext, useFlowEdge, useFlowEntity, useFlowInput, useFlowInstance, useFlowMeasurement, useFlowNode, useFlowPort, useFlowSelector, useFlowToolbar, useFlowValue, useFlowViewport, useMeasuredSize, useOptionalFlowInstance };
|