@statelyai/flow-react 0.4.0 → 0.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.
- package/README.md +5 -3
- package/dist/esm/FlowInstanceContext.js +20 -0
- package/dist/esm/backgrounds.d.ts +8 -0
- package/dist/esm/backgrounds.js +53 -10
- package/dist/esm/index.d.ts +11 -10
- package/dist/esm/index.js +237 -3
- package/package.json +15 -15
- package/dist/esm/hooks.js +0 -255
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- description from package.json -->
|
|
4
4
|
|
|
5
|
-
Thin React adapter for `@statelyai/flow` and
|
|
5
|
+
Thin React adapter for `@statelyai/flow` with core and DOM re-exports.
|
|
6
6
|
|
|
7
7
|
## Philosophy
|
|
8
8
|
|
|
9
|
-
`@statelyai/flow-react` should be completely event-driven and composable. React should not hide a second flow model behind component internals; it should adapt the same `FlowInstance` that consumers can create, inspect, subscribe to, and trigger directly.
|
|
9
|
+
`@statelyai/flow-react` should be completely event-driven and composable. React should not hide a second flow model behind component internals; it should adapt the same `FlowInstance` that consumers can create, inspect, subscribe to, and trigger directly. It re-exports the lower layers so React consumers can install and import from one package.
|
|
10
10
|
|
|
11
11
|
The adapter abstracts over two lower layers:
|
|
12
12
|
|
|
@@ -54,11 +54,13 @@ Color mode is store data, not engine behavior. `@statelyai/flow-dom` exports sma
|
|
|
54
54
|
|
|
55
55
|
```tsx
|
|
56
56
|
import {
|
|
57
|
+
createEdge,
|
|
58
|
+
createGraph,
|
|
59
|
+
createNode,
|
|
57
60
|
StatelyFlow,
|
|
58
61
|
useFlow,
|
|
59
62
|
useFlowSelector,
|
|
60
63
|
} from "@statelyai/flow-react";
|
|
61
|
-
import { createEdge, createGraph, createNode } from "@statelyai/flow";
|
|
62
64
|
|
|
63
65
|
const graph = createGraph({
|
|
64
66
|
nodes: [
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
//#region src/FlowInstanceContext.tsx
|
|
4
|
+
const FlowInstanceContext = createContext(null);
|
|
5
|
+
function FlowInstanceProvider(props) {
|
|
6
|
+
return /* @__PURE__ */ jsx(FlowInstanceContext.Provider, {
|
|
7
|
+
value: props.flow,
|
|
8
|
+
children: props.children
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function useFlowInstance() {
|
|
12
|
+
const flow = useContext(FlowInstanceContext);
|
|
13
|
+
if (!flow) throw new Error("Flow instance is missing. Wrap this tree in <FlowInstanceProvider flow={flow}>.");
|
|
14
|
+
return flow;
|
|
15
|
+
}
|
|
16
|
+
function useOptionalFlowInstance() {
|
|
17
|
+
return useContext(FlowInstanceContext);
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { useFlowInstance as n, useOptionalFlowInstance as r, FlowInstanceProvider as t };
|
|
@@ -5,10 +5,18 @@ import { CSSProperties } from "react";
|
|
|
5
5
|
type BackgroundVariant = "dots" | "lines" | "cross";
|
|
6
6
|
type BackgroundProps = {
|
|
7
7
|
variant?: BackgroundVariant;
|
|
8
|
+
/** Grid spacing in canvas units at zoom 1. */
|
|
8
9
|
gap?: number;
|
|
10
|
+
/** Dot radius / line thickness in screen pixels (constant across zoom). */
|
|
9
11
|
size?: number;
|
|
10
12
|
color?: string;
|
|
11
13
|
backgroundColor?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Minimum on-screen spacing (px) before the grid subdivides. The effective
|
|
16
|
+
* spacing is kept in `[minSpacing, 2 * minSpacing)` by snapping `gap` to a
|
|
17
|
+
* power-of-two multiple, so the grid never looks crowded or sparse.
|
|
18
|
+
*/
|
|
19
|
+
minSpacing?: number;
|
|
12
20
|
className?: string;
|
|
13
21
|
style?: CSSProperties;
|
|
14
22
|
};
|
package/dist/esm/backgrounds.js
CHANGED
|
@@ -1,27 +1,70 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { n as useFlowInstance } from "./FlowInstanceContext.js";
|
|
2
|
+
import { useLayoutEffect, useMemo, useRef } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
4
|
//#region src/backgrounds.tsx
|
|
4
5
|
function Background(props) {
|
|
5
|
-
const
|
|
6
|
+
const instance = useFlowInstance();
|
|
6
7
|
const variant = props.variant ?? "dots";
|
|
7
8
|
const gap = props.gap ?? 24;
|
|
8
9
|
const size = props.size ?? 1;
|
|
9
10
|
const color = props.color ?? "#cbd5e1";
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
11
|
+
const minSpacing = props.minSpacing ?? 24;
|
|
12
|
+
const image = useMemo(() => getBackgroundImage(variant, color, size), [
|
|
13
|
+
variant,
|
|
14
|
+
color,
|
|
15
|
+
size
|
|
16
|
+
]);
|
|
17
|
+
const majorRef = useRef(null);
|
|
18
|
+
const minorRef = useRef(null);
|
|
19
|
+
useLayoutEffect(() => {
|
|
20
|
+
const major = majorRef.current;
|
|
21
|
+
const minor = minorRef.current;
|
|
22
|
+
if (!major || !minor) return;
|
|
23
|
+
const phase = variant === "lines" ? 0 : .5;
|
|
24
|
+
const apply = (viewport) => {
|
|
25
|
+
const screenGap = gap * viewport.zoom;
|
|
26
|
+
const majorGap = screenGap * 2 ** Math.ceil(Math.log2(minSpacing / screenGap));
|
|
27
|
+
const minorGap = majorGap / 2;
|
|
28
|
+
const t = majorGap / minSpacing - 1;
|
|
29
|
+
major.style.backgroundSize = `${majorGap}px ${majorGap}px`;
|
|
30
|
+
major.style.backgroundPosition = `${viewport.x - phase * majorGap}px ${viewport.y - phase * majorGap}px`;
|
|
31
|
+
minor.style.backgroundSize = `${minorGap}px ${minorGap}px`;
|
|
32
|
+
minor.style.backgroundPosition = `${viewport.x - phase * minorGap}px ${viewport.y - phase * minorGap}px`;
|
|
33
|
+
minor.style.opacity = String(t);
|
|
34
|
+
};
|
|
35
|
+
apply(instance.getSnapshot().viewport);
|
|
36
|
+
return instance.listen((context) => context.viewport, (viewport) => apply(viewport));
|
|
37
|
+
}, [
|
|
38
|
+
instance,
|
|
39
|
+
gap,
|
|
40
|
+
minSpacing,
|
|
41
|
+
variant
|
|
42
|
+
]);
|
|
43
|
+
const layerStyle = {
|
|
44
|
+
position: "absolute",
|
|
45
|
+
inset: 0,
|
|
46
|
+
backgroundImage: image
|
|
47
|
+
};
|
|
48
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
13
49
|
"aria-hidden": true,
|
|
14
50
|
className: props.className,
|
|
15
51
|
style: {
|
|
16
52
|
position: "absolute",
|
|
17
53
|
inset: 0,
|
|
18
54
|
backgroundColor: props.backgroundColor,
|
|
19
|
-
backgroundImage: getBackgroundImage(variant, color, scaledSize),
|
|
20
|
-
backgroundPosition: `${viewport.x}px ${viewport.y}px`,
|
|
21
|
-
backgroundSize: `${scaledGap}px ${scaledGap}px`,
|
|
22
55
|
pointerEvents: "none",
|
|
23
56
|
...props.style
|
|
24
|
-
}
|
|
57
|
+
},
|
|
58
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
59
|
+
ref: minorRef,
|
|
60
|
+
style: {
|
|
61
|
+
...layerStyle,
|
|
62
|
+
opacity: 0
|
|
63
|
+
}
|
|
64
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
65
|
+
ref: majorRef,
|
|
66
|
+
style: layerStyle
|
|
67
|
+
})]
|
|
25
68
|
});
|
|
26
69
|
}
|
|
27
70
|
function DotsBackground(props) {
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react4 from "react";
|
|
2
2
|
import { CSSProperties, HTMLAttributes, ReactNode, RefObject } from "react";
|
|
3
|
-
import { AnchorSpec, ConnectionEndedEvent, CreateFlowOptions, EdgePathData, FlowColorMode, FlowController, FlowEdge, FlowGraph, FlowInputMode, FlowNode, FlowProfile, FlowStoreContext, LayoutRequestedEvent, Point, Rect, Viewport, VisualPort } from "@statelyai/flow";
|
|
4
3
|
import { ResolvedFlowColorMode } from "@statelyai/flow-dom";
|
|
4
|
+
import { AnchorSpec, ConnectionEndedEvent, CreateFlowOptions, EdgePathData, FlowColorMode, FlowController, FlowEdge, FlowGraph, FlowInputMode, FlowNode, FlowProfile, FlowStoreContext, LayoutRequestedEvent, Point, Rect, Viewport, VisualPort } from "@statelyai/flow";
|
|
5
|
+
export * from "@statelyai/flow-dom";
|
|
5
6
|
|
|
6
7
|
//#region src/createFlowInstance.d.ts
|
|
7
8
|
type FlowSelector<T> = (context: FlowStoreContext) => T;
|
|
@@ -43,7 +44,7 @@ declare function createFlowInstance(options: CreateFlowInstanceOptions): FlowIns
|
|
|
43
44
|
declare function FlowInstanceProvider(props: {
|
|
44
45
|
flow: FlowInstance;
|
|
45
46
|
children: ReactNode;
|
|
46
|
-
}):
|
|
47
|
+
}): react4.JSX.Element;
|
|
47
48
|
declare function useFlowInstance(): FlowInstance;
|
|
48
49
|
declare function useOptionalFlowInstance(): FlowInstance | null;
|
|
49
50
|
//#endregion
|
|
@@ -169,7 +170,7 @@ type StatelyFlowProps<TNodeData = unknown, TEdgeData = unknown> = {
|
|
|
169
170
|
background?: ReactNode;
|
|
170
171
|
children?: ReactNode;
|
|
171
172
|
};
|
|
172
|
-
declare function StatelyFlow<TNodeData = unknown, TEdgeData = unknown>(props: StatelyFlowProps<TNodeData, TEdgeData>):
|
|
173
|
+
declare function StatelyFlow<TNodeData = unknown, TEdgeData = unknown>(props: StatelyFlowProps<TNodeData, TEdgeData>): react4.JSX.Element;
|
|
173
174
|
//#endregion
|
|
174
175
|
//#region src/FlowToolbar.d.ts
|
|
175
176
|
/** Border-box size of an element, kept current via ResizeObserver. */
|
|
@@ -211,7 +212,7 @@ type FlowToolbarProps = UseFlowToolbarOptions & {
|
|
|
211
212
|
* Marked `data-flow-interactive`, so its contents receive pointer events
|
|
212
213
|
* unopposed by canvas gestures.
|
|
213
214
|
*/
|
|
214
|
-
declare function FlowToolbar(props: FlowToolbarProps):
|
|
215
|
+
declare function FlowToolbar(props: FlowToolbarProps): react4.JSX.Element | null;
|
|
215
216
|
//#endregion
|
|
216
217
|
//#region src/MiniMap.d.ts
|
|
217
218
|
type MiniMapProps = {
|
|
@@ -230,7 +231,7 @@ type MiniMapProps = {
|
|
|
230
231
|
* plain store trigger — this component is exactly what you would write in
|
|
231
232
|
* userland, kept here as a convenience.
|
|
232
233
|
*/
|
|
233
|
-
declare function MiniMap(props: MiniMapProps):
|
|
234
|
+
declare function MiniMap(props: MiniMapProps): react4.JSX.Element;
|
|
234
235
|
//#endregion
|
|
235
236
|
//#region src/Controls.d.ts
|
|
236
237
|
type ControlsProps = {
|
|
@@ -243,7 +244,7 @@ type ControlsProps = {
|
|
|
243
244
|
* Zoom/fit toolbar. Deliberately thin: every button is one store trigger
|
|
244
245
|
* (`zoomBy`, `fitView`) — replicate or extend it freely in userland.
|
|
245
246
|
*/
|
|
246
|
-
declare function Controls(props: ControlsProps):
|
|
247
|
+
declare function Controls(props: ControlsProps): react4.JSX.Element;
|
|
247
248
|
//#endregion
|
|
248
249
|
//#region src/EntityAnchor.d.ts
|
|
249
250
|
type EntityAnchorProps = {
|
|
@@ -254,7 +255,7 @@ type EntityAnchorProps = {
|
|
|
254
255
|
style?: CSSProperties;
|
|
255
256
|
children: ReactNode;
|
|
256
257
|
};
|
|
257
|
-
declare function EntityAnchor(props: EntityAnchorProps):
|
|
258
|
+
declare function EntityAnchor(props: EntityAnchorProps): react4.JSX.Element;
|
|
258
259
|
//#endregion
|
|
259
260
|
//#region src/Port.d.ts
|
|
260
261
|
type PortProps = {
|
|
@@ -288,7 +289,7 @@ type PortProps = {
|
|
|
288
289
|
* observe results via `flow.on("connectionEnded", …)` or the store's
|
|
289
290
|
* `connection` state.
|
|
290
291
|
*/
|
|
291
|
-
declare function Port(props: PortProps):
|
|
292
|
+
declare function Port(props: PortProps): react4.JSX.Element;
|
|
292
293
|
//#endregion
|
|
293
294
|
//#region src/ColorMode.d.ts
|
|
294
295
|
type FlowColorModeScopeProps = {
|
|
@@ -296,7 +297,7 @@ type FlowColorModeScopeProps = {
|
|
|
296
297
|
className?: string;
|
|
297
298
|
style?: CSSProperties;
|
|
298
299
|
};
|
|
299
|
-
declare function FlowColorModeScope(props: FlowColorModeScopeProps):
|
|
300
|
+
declare function FlowColorModeScope(props: FlowColorModeScopeProps): react4.JSX.Element;
|
|
300
301
|
declare function useFlowColorMode(): {
|
|
301
302
|
colorMode: FlowColorMode;
|
|
302
303
|
resolvedColorMode: ResolvedFlowColorMode;
|
package/dist/esm/index.js
CHANGED
|
@@ -1,8 +1,242 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as useFlowInstance, r as useOptionalFlowInstance, t as FlowInstanceProvider } from "./FlowInstanceContext.js";
|
|
2
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
3
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, computeRenderableIds, createMeasurementObserver, getAttachedContainer, nodePositionStyle, renderableIdsEqual, resolveFlowColorMode, viewportStyle } from "@statelyai/flow-dom";
|
|
5
|
+
import { CONNECTION_PREVIEW_EDGE_ID, createConnectionPreviewGraph, createEntityChangeNotifier, createFlow, createFlowInputRuntime, getAnchorTransform, getAutoPanDelta, getMinimapProjection, getToolbarPlacement, nodeDepths, pathToSVG, profileModesForContext, viewportCenteredOn } from "@statelyai/flow";
|
|
6
|
+
export * from "@statelyai/flow-dom";
|
|
7
|
+
//#region src/createFlowInstance.ts
|
|
8
|
+
function createFlowInstance(options) {
|
|
9
|
+
const flow = createFlow({
|
|
10
|
+
graph: options.graph,
|
|
11
|
+
...options.settings
|
|
12
|
+
});
|
|
13
|
+
const cleanup = /* @__PURE__ */ new Set();
|
|
14
|
+
function getSnapshot() {
|
|
15
|
+
return flow.store.getSnapshot().context;
|
|
16
|
+
}
|
|
17
|
+
function clientToCanvas(point) {
|
|
18
|
+
const rect = getAttachedContainer(flow.store)?.getBoundingClientRect();
|
|
19
|
+
return flow.screenToCanvas({
|
|
20
|
+
x: point.x - (rect?.left ?? 0),
|
|
21
|
+
y: point.y - (rect?.top ?? 0)
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function listen(selector, listener, listenerOptions = {}) {
|
|
25
|
+
const isEqual = listenerOptions.isEqual ?? Object.is;
|
|
26
|
+
let previousValue = selector(getSnapshot());
|
|
27
|
+
if (listenerOptions.fireImmediately) listener(previousValue, previousValue, getSnapshot());
|
|
28
|
+
const unsubscribe = flow.subscribe((context) => {
|
|
29
|
+
const nextValue = selector(context);
|
|
30
|
+
if (isEqual(previousValue, nextValue)) return;
|
|
31
|
+
const currentPreviousValue = previousValue;
|
|
32
|
+
previousValue = nextValue;
|
|
33
|
+
listener(nextValue, currentPreviousValue, context);
|
|
34
|
+
});
|
|
35
|
+
cleanup.add(unsubscribe);
|
|
36
|
+
return () => {
|
|
37
|
+
cleanup.delete(unsubscribe);
|
|
38
|
+
unsubscribe();
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function subscribeEvent(event, listener) {
|
|
42
|
+
const subscription = flow.on(event, listener);
|
|
43
|
+
const unsubscribe = () => subscription.unsubscribe();
|
|
44
|
+
cleanup.add(unsubscribe);
|
|
45
|
+
return () => {
|
|
46
|
+
cleanup.delete(unsubscribe);
|
|
47
|
+
unsubscribe();
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (options.events?.connectionEnded) subscribeEvent("connectionEnded", options.events.connectionEnded);
|
|
51
|
+
if (options.events?.layoutRequested) subscribeEvent("layoutRequested", options.events.layoutRequested);
|
|
52
|
+
for (const listener of options.listeners ?? []) listen(listener.selector, listener.onChange, listener);
|
|
53
|
+
return {
|
|
54
|
+
...flow,
|
|
55
|
+
getSnapshot,
|
|
56
|
+
listen,
|
|
57
|
+
clientToCanvas,
|
|
58
|
+
destroy() {
|
|
59
|
+
for (const unsubscribe of cleanup) unsubscribe();
|
|
60
|
+
cleanup.clear();
|
|
61
|
+
flow.destroy();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/hooks.ts
|
|
67
|
+
function useFlow(options) {
|
|
68
|
+
return useCreateFlowInstance(options);
|
|
69
|
+
}
|
|
70
|
+
function useCreateFlowInstance(options) {
|
|
71
|
+
const instanceRef = useRef(null);
|
|
72
|
+
const listenersRef = useRef(options.listeners);
|
|
73
|
+
const eventsRef = useRef(options.events);
|
|
74
|
+
const didMountRef = useRef(false);
|
|
75
|
+
listenersRef.current = options.listeners;
|
|
76
|
+
eventsRef.current = options.events;
|
|
77
|
+
if (!instanceRef.current) instanceRef.current = createFlowInstance({
|
|
78
|
+
graph: options.graph,
|
|
79
|
+
settings: options.settings
|
|
80
|
+
});
|
|
81
|
+
const flow = instanceRef.current;
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!didMountRef.current) {
|
|
84
|
+
didMountRef.current = true;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
flow.trigger.setGraph({ graph: options.graph });
|
|
88
|
+
}, [flow, options.graph]);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
const cleanup = [];
|
|
91
|
+
if (eventsRef.current?.connectionEnded) {
|
|
92
|
+
const subscription = flow.on("connectionEnded", eventsRef.current.connectionEnded);
|
|
93
|
+
cleanup.push(() => subscription.unsubscribe());
|
|
94
|
+
}
|
|
95
|
+
if (eventsRef.current?.layoutRequested) {
|
|
96
|
+
const subscription = flow.on("layoutRequested", eventsRef.current.layoutRequested);
|
|
97
|
+
cleanup.push(() => subscription.unsubscribe());
|
|
98
|
+
}
|
|
99
|
+
for (const listener of listenersRef.current ?? []) cleanup.push(flow.listen(listener.selector, listener.onChange, listener));
|
|
100
|
+
return () => {
|
|
101
|
+
for (const dispose of cleanup) dispose();
|
|
102
|
+
};
|
|
103
|
+
}, [
|
|
104
|
+
flow,
|
|
105
|
+
options.listeners,
|
|
106
|
+
options.events
|
|
107
|
+
]);
|
|
108
|
+
useEffect(() => () => flow.destroy(), [flow]);
|
|
109
|
+
return flow;
|
|
110
|
+
}
|
|
111
|
+
function useFlowSelector(selector, options = {}) {
|
|
112
|
+
const contextFlow = useOptionalFlowInstance();
|
|
113
|
+
const flow = options.flow ?? contextFlow;
|
|
114
|
+
if (!flow) throw new Error("Flow instance is missing. Pass { flow } or render inside <FlowInstanceProvider>.");
|
|
115
|
+
const selectorRef = useRef(selector);
|
|
116
|
+
const isEqualRef = useRef(options.isEqual ?? Object.is);
|
|
117
|
+
selectorRef.current = selector;
|
|
118
|
+
isEqualRef.current = options.isEqual ?? Object.is;
|
|
119
|
+
const [selected, setSelected] = useState(() => selector(flow.getSnapshot()));
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
setSelected((previous) => {
|
|
122
|
+
const next = selectorRef.current(flow.getSnapshot());
|
|
123
|
+
return isEqualRef.current(previous, next) ? previous : next;
|
|
124
|
+
});
|
|
125
|
+
return flow.listen((context) => selectorRef.current(context), (value) => {
|
|
126
|
+
setSelected((previous) => isEqualRef.current(previous, value) ? previous : value);
|
|
127
|
+
});
|
|
128
|
+
}, [flow]);
|
|
129
|
+
return selected;
|
|
130
|
+
}
|
|
131
|
+
function useFlowContext() {
|
|
132
|
+
return useFlowSelector((context) => context);
|
|
133
|
+
}
|
|
134
|
+
const entityNotifiers = /* @__PURE__ */ new WeakMap();
|
|
135
|
+
function getEntityNotifier(flow) {
|
|
136
|
+
let notifier = entityNotifiers.get(flow.store);
|
|
137
|
+
if (!notifier) {
|
|
138
|
+
notifier = createEntityChangeNotifier(flow.store);
|
|
139
|
+
entityNotifiers.set(flow.store, notifier);
|
|
140
|
+
}
|
|
141
|
+
return notifier;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Like `useFlowSelector`, but subscribed through the entity-change notifier:
|
|
145
|
+
* the selector re-runs only when the entity with `id` may have changed, not
|
|
146
|
+
* on every store transition. This is what makes thousands of mounted entity
|
|
147
|
+
* views affordable — see BENCHMARK.md.
|
|
148
|
+
*/
|
|
149
|
+
function useFlowEntity(id, selector, options = {}) {
|
|
150
|
+
const contextFlow = useOptionalFlowInstance();
|
|
151
|
+
const flow = options.flow ?? contextFlow;
|
|
152
|
+
if (!flow) throw new Error("Flow instance is missing. Pass { flow } or render inside <FlowInstanceProvider>.");
|
|
153
|
+
const selectorRef = useRef(selector);
|
|
154
|
+
const isEqualRef = useRef(options.isEqual ?? Object.is);
|
|
155
|
+
selectorRef.current = selector;
|
|
156
|
+
isEqualRef.current = options.isEqual ?? Object.is;
|
|
157
|
+
const [selected, setSelected] = useState(() => selector(flow.getSnapshot()));
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
const update = () => {
|
|
160
|
+
setSelected((previous) => {
|
|
161
|
+
const next = selectorRef.current(flow.getSnapshot());
|
|
162
|
+
return isEqualRef.current(previous, next) ? previous : next;
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
update();
|
|
166
|
+
return getEntityNotifier(flow).subscribe(id, update);
|
|
167
|
+
}, [flow, id]);
|
|
168
|
+
return selected;
|
|
169
|
+
}
|
|
170
|
+
function useFlowValue(selector, isEqual) {
|
|
171
|
+
return useFlowSelector(selector, { isEqual });
|
|
172
|
+
}
|
|
173
|
+
function useFlowNode(id, selector, isEqual) {
|
|
174
|
+
return useFlowSelector((context) => {
|
|
175
|
+
const node = context.graph.nodes.find((candidate) => candidate.id === id);
|
|
176
|
+
if (!node) throw new Error(`Flow node "${id}" was not found.`);
|
|
177
|
+
if (selector) return selector(node, context);
|
|
178
|
+
return {
|
|
179
|
+
...node,
|
|
180
|
+
...getDerivedFields(context, id, {
|
|
181
|
+
"data-flow-target": "entity",
|
|
182
|
+
"data-flow-entity-id": id,
|
|
183
|
+
"data-entity-id": id
|
|
184
|
+
})
|
|
185
|
+
};
|
|
186
|
+
}, { isEqual });
|
|
187
|
+
}
|
|
188
|
+
function useFlowEdge(id, selector, isEqual) {
|
|
189
|
+
const flow = useOptionalFlowInstance();
|
|
190
|
+
return useFlowSelector((context) => {
|
|
191
|
+
const edge = context.graph.edges.find((candidate) => candidate.id === id);
|
|
192
|
+
if (!edge) throw new Error(`Flow edge "${id}" was not found.`);
|
|
193
|
+
if (selector) return selector(edge, context);
|
|
194
|
+
return {
|
|
195
|
+
...edge,
|
|
196
|
+
pathData: flow?.getAllEdgePaths().get(id) ?? null,
|
|
197
|
+
...getDerivedFields(context, id, {
|
|
198
|
+
"data-flow-target": "entity",
|
|
199
|
+
"data-flow-entity-id": id,
|
|
200
|
+
"data-entity-id": id
|
|
201
|
+
})
|
|
202
|
+
};
|
|
203
|
+
}, { isEqual });
|
|
204
|
+
}
|
|
205
|
+
function useFlowPort(id, selector, isEqual) {
|
|
206
|
+
const flow = useFlowInstance();
|
|
207
|
+
return useFlowSelector((context) => {
|
|
208
|
+
const addressable = flow?.getAddressable(id);
|
|
209
|
+
if (!addressable || addressable.kind !== "port") throw new Error(`Flow port "${id}" was not found.`);
|
|
210
|
+
const binding = {
|
|
211
|
+
...addressable.port,
|
|
212
|
+
id,
|
|
213
|
+
nodeId: addressable.nodeId,
|
|
214
|
+
portName: addressable.portName,
|
|
215
|
+
...getDerivedFields(context, id, {
|
|
216
|
+
"data-flow-target": "port",
|
|
217
|
+
"data-flow-port-id": id,
|
|
218
|
+
"data-flow-node-id": addressable.nodeId,
|
|
219
|
+
"data-flow-port-name": addressable.portName
|
|
220
|
+
})
|
|
221
|
+
};
|
|
222
|
+
return selector ? selector(binding, context) : binding;
|
|
223
|
+
}, { isEqual });
|
|
224
|
+
}
|
|
225
|
+
function useFlowViewport() {
|
|
226
|
+
return useFlowSelector((context) => context.viewport);
|
|
227
|
+
}
|
|
228
|
+
function useFlowMeasurement(id) {
|
|
229
|
+
return useFlowSelector((context) => context.measurements.get(id) ?? null);
|
|
230
|
+
}
|
|
231
|
+
function getDerivedFields(context, id, props) {
|
|
232
|
+
return {
|
|
233
|
+
selected: context.selection.has(id),
|
|
234
|
+
focused: context.focusedEntityId === id,
|
|
235
|
+
hovered: context.hoveredEntityIds.has(id),
|
|
236
|
+
props
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
6
240
|
//#region src/useFlowInput.ts
|
|
7
241
|
/**
|
|
8
242
|
* Bind a container element to the core input runtime: all pointer, wheel,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statelyai/flow-react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Thin React adapter for @statelyai/flow and
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Thin React adapter for @statelyai/flow with core and DOM re-exports.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"flow",
|
|
7
7
|
"diagram",
|
|
@@ -30,20 +30,9 @@
|
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
32
|
"license": "MIT",
|
|
33
|
-
"scripts": {
|
|
34
|
-
"dev": "tsdown --watch",
|
|
35
|
-
"build": "tsdown",
|
|
36
|
-
"test": "vitest run",
|
|
37
|
-
"typecheck": "tsc --noEmit",
|
|
38
|
-
"test:browser": "vitest run --config vitest.browser.config.mjs",
|
|
39
|
-
"test:e2e": "playwright test",
|
|
40
|
-
"storybook": "storybook dev -p 6008",
|
|
41
|
-
"storybook:build": "storybook build",
|
|
42
|
-
"test:stories": "storybook build --quiet && concurrently -k -s first -n SB,TEST \"http-server storybook-static -p 6010 --silent\" \"wait-on tcp:127.0.0.1:6010 && test-storybook --url http://127.0.0.1:6010 --maxWorkers=2\""
|
|
43
|
-
},
|
|
44
33
|
"dependencies": {
|
|
45
|
-
"@statelyai/flow": "
|
|
46
|
-
"@statelyai/flow-dom": "
|
|
34
|
+
"@statelyai/flow": "0.4.2",
|
|
35
|
+
"@statelyai/flow-dom": "3.1.0"
|
|
47
36
|
},
|
|
48
37
|
"peerDependencies": {
|
|
49
38
|
"react": "^18.2.0 || ^19.0.0",
|
|
@@ -83,5 +72,16 @@
|
|
|
83
72
|
"vitest": "^4.1.0",
|
|
84
73
|
"wait-on": "^9.0.10",
|
|
85
74
|
"yjs": "^13.6.31"
|
|
75
|
+
},
|
|
76
|
+
"scripts": {
|
|
77
|
+
"dev": "tsdown --watch",
|
|
78
|
+
"build": "tsdown",
|
|
79
|
+
"test": "vitest run",
|
|
80
|
+
"typecheck": "tsc --noEmit",
|
|
81
|
+
"test:browser": "vitest run --config vitest.browser.config.mjs",
|
|
82
|
+
"test:e2e": "playwright test",
|
|
83
|
+
"storybook": "storybook dev -p 6008",
|
|
84
|
+
"storybook:build": "storybook build",
|
|
85
|
+
"test:stories": "storybook build --quiet && concurrently -k -s first -n SB,TEST \"http-server storybook-static -p 6010 --silent\" \"wait-on tcp:127.0.0.1:6010 && test-storybook --url http://127.0.0.1:6010 --maxWorkers=2\""
|
|
86
86
|
}
|
|
87
87
|
}
|
package/dist/esm/hooks.js
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
|
2
|
-
import { createEntityChangeNotifier, createFlow } from "@statelyai/flow";
|
|
3
|
-
import { getAttachedContainer } from "@statelyai/flow-dom";
|
|
4
|
-
import { jsx } from "react/jsx-runtime";
|
|
5
|
-
//#region src/createFlowInstance.ts
|
|
6
|
-
function createFlowInstance(options) {
|
|
7
|
-
const flow = createFlow({
|
|
8
|
-
graph: options.graph,
|
|
9
|
-
...options.settings
|
|
10
|
-
});
|
|
11
|
-
const cleanup = /* @__PURE__ */ new Set();
|
|
12
|
-
function getSnapshot() {
|
|
13
|
-
return flow.store.getSnapshot().context;
|
|
14
|
-
}
|
|
15
|
-
function clientToCanvas(point) {
|
|
16
|
-
const rect = getAttachedContainer(flow.store)?.getBoundingClientRect();
|
|
17
|
-
return flow.screenToCanvas({
|
|
18
|
-
x: point.x - (rect?.left ?? 0),
|
|
19
|
-
y: point.y - (rect?.top ?? 0)
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
function listen(selector, listener, listenerOptions = {}) {
|
|
23
|
-
const isEqual = listenerOptions.isEqual ?? Object.is;
|
|
24
|
-
let previousValue = selector(getSnapshot());
|
|
25
|
-
if (listenerOptions.fireImmediately) listener(previousValue, previousValue, getSnapshot());
|
|
26
|
-
const unsubscribe = flow.subscribe((context) => {
|
|
27
|
-
const nextValue = selector(context);
|
|
28
|
-
if (isEqual(previousValue, nextValue)) return;
|
|
29
|
-
const currentPreviousValue = previousValue;
|
|
30
|
-
previousValue = nextValue;
|
|
31
|
-
listener(nextValue, currentPreviousValue, context);
|
|
32
|
-
});
|
|
33
|
-
cleanup.add(unsubscribe);
|
|
34
|
-
return () => {
|
|
35
|
-
cleanup.delete(unsubscribe);
|
|
36
|
-
unsubscribe();
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
function subscribeEvent(event, listener) {
|
|
40
|
-
const subscription = flow.on(event, listener);
|
|
41
|
-
const unsubscribe = () => subscription.unsubscribe();
|
|
42
|
-
cleanup.add(unsubscribe);
|
|
43
|
-
return () => {
|
|
44
|
-
cleanup.delete(unsubscribe);
|
|
45
|
-
unsubscribe();
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
if (options.events?.connectionEnded) subscribeEvent("connectionEnded", options.events.connectionEnded);
|
|
49
|
-
if (options.events?.layoutRequested) subscribeEvent("layoutRequested", options.events.layoutRequested);
|
|
50
|
-
for (const listener of options.listeners ?? []) listen(listener.selector, listener.onChange, listener);
|
|
51
|
-
return {
|
|
52
|
-
...flow,
|
|
53
|
-
getSnapshot,
|
|
54
|
-
listen,
|
|
55
|
-
clientToCanvas,
|
|
56
|
-
destroy() {
|
|
57
|
-
for (const unsubscribe of cleanup) unsubscribe();
|
|
58
|
-
cleanup.clear();
|
|
59
|
-
flow.destroy();
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
//#endregion
|
|
64
|
-
//#region src/FlowInstanceContext.tsx
|
|
65
|
-
const FlowInstanceContext = createContext(null);
|
|
66
|
-
function FlowInstanceProvider(props) {
|
|
67
|
-
return /* @__PURE__ */ jsx(FlowInstanceContext.Provider, {
|
|
68
|
-
value: props.flow,
|
|
69
|
-
children: props.children
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
function useFlowInstance() {
|
|
73
|
-
const flow = useContext(FlowInstanceContext);
|
|
74
|
-
if (!flow) throw new Error("Flow instance is missing. Wrap this tree in <FlowInstanceProvider flow={flow}>.");
|
|
75
|
-
return flow;
|
|
76
|
-
}
|
|
77
|
-
function useOptionalFlowInstance() {
|
|
78
|
-
return useContext(FlowInstanceContext);
|
|
79
|
-
}
|
|
80
|
-
//#endregion
|
|
81
|
-
//#region src/hooks.ts
|
|
82
|
-
function useFlow(options) {
|
|
83
|
-
return useCreateFlowInstance(options);
|
|
84
|
-
}
|
|
85
|
-
function useCreateFlowInstance(options) {
|
|
86
|
-
const instanceRef = useRef(null);
|
|
87
|
-
const listenersRef = useRef(options.listeners);
|
|
88
|
-
const eventsRef = useRef(options.events);
|
|
89
|
-
const didMountRef = useRef(false);
|
|
90
|
-
listenersRef.current = options.listeners;
|
|
91
|
-
eventsRef.current = options.events;
|
|
92
|
-
if (!instanceRef.current) instanceRef.current = createFlowInstance({
|
|
93
|
-
graph: options.graph,
|
|
94
|
-
settings: options.settings
|
|
95
|
-
});
|
|
96
|
-
const flow = instanceRef.current;
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
if (!didMountRef.current) {
|
|
99
|
-
didMountRef.current = true;
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
flow.trigger.setGraph({ graph: options.graph });
|
|
103
|
-
}, [flow, options.graph]);
|
|
104
|
-
useEffect(() => {
|
|
105
|
-
const cleanup = [];
|
|
106
|
-
if (eventsRef.current?.connectionEnded) {
|
|
107
|
-
const subscription = flow.on("connectionEnded", eventsRef.current.connectionEnded);
|
|
108
|
-
cleanup.push(() => subscription.unsubscribe());
|
|
109
|
-
}
|
|
110
|
-
if (eventsRef.current?.layoutRequested) {
|
|
111
|
-
const subscription = flow.on("layoutRequested", eventsRef.current.layoutRequested);
|
|
112
|
-
cleanup.push(() => subscription.unsubscribe());
|
|
113
|
-
}
|
|
114
|
-
for (const listener of listenersRef.current ?? []) cleanup.push(flow.listen(listener.selector, listener.onChange, listener));
|
|
115
|
-
return () => {
|
|
116
|
-
for (const dispose of cleanup) dispose();
|
|
117
|
-
};
|
|
118
|
-
}, [
|
|
119
|
-
flow,
|
|
120
|
-
options.listeners,
|
|
121
|
-
options.events
|
|
122
|
-
]);
|
|
123
|
-
useEffect(() => () => flow.destroy(), [flow]);
|
|
124
|
-
return flow;
|
|
125
|
-
}
|
|
126
|
-
function useFlowSelector(selector, options = {}) {
|
|
127
|
-
const contextFlow = useOptionalFlowInstance();
|
|
128
|
-
const flow = options.flow ?? contextFlow;
|
|
129
|
-
if (!flow) throw new Error("Flow instance is missing. Pass { flow } or render inside <FlowInstanceProvider>.");
|
|
130
|
-
const selectorRef = useRef(selector);
|
|
131
|
-
const isEqualRef = useRef(options.isEqual ?? Object.is);
|
|
132
|
-
selectorRef.current = selector;
|
|
133
|
-
isEqualRef.current = options.isEqual ?? Object.is;
|
|
134
|
-
const [selected, setSelected] = useState(() => selector(flow.getSnapshot()));
|
|
135
|
-
useEffect(() => {
|
|
136
|
-
setSelected((previous) => {
|
|
137
|
-
const next = selectorRef.current(flow.getSnapshot());
|
|
138
|
-
return isEqualRef.current(previous, next) ? previous : next;
|
|
139
|
-
});
|
|
140
|
-
return flow.listen((context) => selectorRef.current(context), (value) => {
|
|
141
|
-
setSelected((previous) => isEqualRef.current(previous, value) ? previous : value);
|
|
142
|
-
});
|
|
143
|
-
}, [flow]);
|
|
144
|
-
return selected;
|
|
145
|
-
}
|
|
146
|
-
function useFlowContext() {
|
|
147
|
-
return useFlowSelector((context) => context);
|
|
148
|
-
}
|
|
149
|
-
const entityNotifiers = /* @__PURE__ */ new WeakMap();
|
|
150
|
-
function getEntityNotifier(flow) {
|
|
151
|
-
let notifier = entityNotifiers.get(flow.store);
|
|
152
|
-
if (!notifier) {
|
|
153
|
-
notifier = createEntityChangeNotifier(flow.store);
|
|
154
|
-
entityNotifiers.set(flow.store, notifier);
|
|
155
|
-
}
|
|
156
|
-
return notifier;
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Like `useFlowSelector`, but subscribed through the entity-change notifier:
|
|
160
|
-
* the selector re-runs only when the entity with `id` may have changed, not
|
|
161
|
-
* on every store transition. This is what makes thousands of mounted entity
|
|
162
|
-
* views affordable — see BENCHMARK.md.
|
|
163
|
-
*/
|
|
164
|
-
function useFlowEntity(id, selector, options = {}) {
|
|
165
|
-
const contextFlow = useOptionalFlowInstance();
|
|
166
|
-
const flow = options.flow ?? contextFlow;
|
|
167
|
-
if (!flow) throw new Error("Flow instance is missing. Pass { flow } or render inside <FlowInstanceProvider>.");
|
|
168
|
-
const selectorRef = useRef(selector);
|
|
169
|
-
const isEqualRef = useRef(options.isEqual ?? Object.is);
|
|
170
|
-
selectorRef.current = selector;
|
|
171
|
-
isEqualRef.current = options.isEqual ?? Object.is;
|
|
172
|
-
const [selected, setSelected] = useState(() => selector(flow.getSnapshot()));
|
|
173
|
-
useEffect(() => {
|
|
174
|
-
const update = () => {
|
|
175
|
-
setSelected((previous) => {
|
|
176
|
-
const next = selectorRef.current(flow.getSnapshot());
|
|
177
|
-
return isEqualRef.current(previous, next) ? previous : next;
|
|
178
|
-
});
|
|
179
|
-
};
|
|
180
|
-
update();
|
|
181
|
-
return getEntityNotifier(flow).subscribe(id, update);
|
|
182
|
-
}, [flow, id]);
|
|
183
|
-
return selected;
|
|
184
|
-
}
|
|
185
|
-
function useFlowValue(selector, isEqual) {
|
|
186
|
-
return useFlowSelector(selector, { isEqual });
|
|
187
|
-
}
|
|
188
|
-
function useFlowNode(id, selector, isEqual) {
|
|
189
|
-
return useFlowSelector((context) => {
|
|
190
|
-
const node = context.graph.nodes.find((candidate) => candidate.id === id);
|
|
191
|
-
if (!node) throw new Error(`Flow node "${id}" was not found.`);
|
|
192
|
-
if (selector) return selector(node, context);
|
|
193
|
-
return {
|
|
194
|
-
...node,
|
|
195
|
-
...getDerivedFields(context, id, {
|
|
196
|
-
"data-flow-target": "entity",
|
|
197
|
-
"data-flow-entity-id": id,
|
|
198
|
-
"data-entity-id": id
|
|
199
|
-
})
|
|
200
|
-
};
|
|
201
|
-
}, { isEqual });
|
|
202
|
-
}
|
|
203
|
-
function useFlowEdge(id, selector, isEqual) {
|
|
204
|
-
const flow = useOptionalFlowInstance();
|
|
205
|
-
return useFlowSelector((context) => {
|
|
206
|
-
const edge = context.graph.edges.find((candidate) => candidate.id === id);
|
|
207
|
-
if (!edge) throw new Error(`Flow edge "${id}" was not found.`);
|
|
208
|
-
if (selector) return selector(edge, context);
|
|
209
|
-
return {
|
|
210
|
-
...edge,
|
|
211
|
-
pathData: flow?.getAllEdgePaths().get(id) ?? null,
|
|
212
|
-
...getDerivedFields(context, id, {
|
|
213
|
-
"data-flow-target": "entity",
|
|
214
|
-
"data-flow-entity-id": id,
|
|
215
|
-
"data-entity-id": id
|
|
216
|
-
})
|
|
217
|
-
};
|
|
218
|
-
}, { isEqual });
|
|
219
|
-
}
|
|
220
|
-
function useFlowPort(id, selector, isEqual) {
|
|
221
|
-
const flow = useFlowInstance();
|
|
222
|
-
return useFlowSelector((context) => {
|
|
223
|
-
const addressable = flow?.getAddressable(id);
|
|
224
|
-
if (!addressable || addressable.kind !== "port") throw new Error(`Flow port "${id}" was not found.`);
|
|
225
|
-
const binding = {
|
|
226
|
-
...addressable.port,
|
|
227
|
-
id,
|
|
228
|
-
nodeId: addressable.nodeId,
|
|
229
|
-
portName: addressable.portName,
|
|
230
|
-
...getDerivedFields(context, id, {
|
|
231
|
-
"data-flow-target": "port",
|
|
232
|
-
"data-flow-port-id": id,
|
|
233
|
-
"data-flow-node-id": addressable.nodeId,
|
|
234
|
-
"data-flow-port-name": addressable.portName
|
|
235
|
-
})
|
|
236
|
-
};
|
|
237
|
-
return selector ? selector(binding, context) : binding;
|
|
238
|
-
}, { isEqual });
|
|
239
|
-
}
|
|
240
|
-
function useFlowViewport() {
|
|
241
|
-
return useFlowSelector((context) => context.viewport);
|
|
242
|
-
}
|
|
243
|
-
function useFlowMeasurement(id) {
|
|
244
|
-
return useFlowSelector((context) => context.measurements.get(id) ?? null);
|
|
245
|
-
}
|
|
246
|
-
function getDerivedFields(context, id, props) {
|
|
247
|
-
return {
|
|
248
|
-
selected: context.selection.has(id),
|
|
249
|
-
focused: context.focusedEntityId === id,
|
|
250
|
-
hovered: context.hoveredEntityIds.has(id),
|
|
251
|
-
props
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
//#endregion
|
|
255
|
-
export { useFlowEntity as a, useFlowPort as c, useFlowViewport as d, FlowInstanceProvider as f, createFlowInstance as h, useFlowEdge as i, useFlowSelector as l, useOptionalFlowInstance as m, useFlow as n, useFlowMeasurement as o, useFlowInstance as p, useFlowContext as r, useFlowNode as s, useCreateFlowInstance as t, useFlowValue as u };
|