@statelyai/flow-react 0.4.0 → 0.5.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/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 +10 -10
- package/dist/esm/index.js +236 -3
- package/package.json +14 -14
- package/dist/esm/hooks.js +0 -255
|
@@ -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,7 @@
|
|
|
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
5
|
|
|
6
6
|
//#region src/createFlowInstance.d.ts
|
|
7
7
|
type FlowSelector<T> = (context: FlowStoreContext) => T;
|
|
@@ -43,7 +43,7 @@ declare function createFlowInstance(options: CreateFlowInstanceOptions): FlowIns
|
|
|
43
43
|
declare function FlowInstanceProvider(props: {
|
|
44
44
|
flow: FlowInstance;
|
|
45
45
|
children: ReactNode;
|
|
46
|
-
}):
|
|
46
|
+
}): react4.JSX.Element;
|
|
47
47
|
declare function useFlowInstance(): FlowInstance;
|
|
48
48
|
declare function useOptionalFlowInstance(): FlowInstance | null;
|
|
49
49
|
//#endregion
|
|
@@ -169,7 +169,7 @@ type StatelyFlowProps<TNodeData = unknown, TEdgeData = unknown> = {
|
|
|
169
169
|
background?: ReactNode;
|
|
170
170
|
children?: ReactNode;
|
|
171
171
|
};
|
|
172
|
-
declare function StatelyFlow<TNodeData = unknown, TEdgeData = unknown>(props: StatelyFlowProps<TNodeData, TEdgeData>):
|
|
172
|
+
declare function StatelyFlow<TNodeData = unknown, TEdgeData = unknown>(props: StatelyFlowProps<TNodeData, TEdgeData>): react4.JSX.Element;
|
|
173
173
|
//#endregion
|
|
174
174
|
//#region src/FlowToolbar.d.ts
|
|
175
175
|
/** Border-box size of an element, kept current via ResizeObserver. */
|
|
@@ -211,7 +211,7 @@ type FlowToolbarProps = UseFlowToolbarOptions & {
|
|
|
211
211
|
* Marked `data-flow-interactive`, so its contents receive pointer events
|
|
212
212
|
* unopposed by canvas gestures.
|
|
213
213
|
*/
|
|
214
|
-
declare function FlowToolbar(props: FlowToolbarProps):
|
|
214
|
+
declare function FlowToolbar(props: FlowToolbarProps): react4.JSX.Element | null;
|
|
215
215
|
//#endregion
|
|
216
216
|
//#region src/MiniMap.d.ts
|
|
217
217
|
type MiniMapProps = {
|
|
@@ -230,7 +230,7 @@ type MiniMapProps = {
|
|
|
230
230
|
* plain store trigger — this component is exactly what you would write in
|
|
231
231
|
* userland, kept here as a convenience.
|
|
232
232
|
*/
|
|
233
|
-
declare function MiniMap(props: MiniMapProps):
|
|
233
|
+
declare function MiniMap(props: MiniMapProps): react4.JSX.Element;
|
|
234
234
|
//#endregion
|
|
235
235
|
//#region src/Controls.d.ts
|
|
236
236
|
type ControlsProps = {
|
|
@@ -243,7 +243,7 @@ type ControlsProps = {
|
|
|
243
243
|
* Zoom/fit toolbar. Deliberately thin: every button is one store trigger
|
|
244
244
|
* (`zoomBy`, `fitView`) — replicate or extend it freely in userland.
|
|
245
245
|
*/
|
|
246
|
-
declare function Controls(props: ControlsProps):
|
|
246
|
+
declare function Controls(props: ControlsProps): react4.JSX.Element;
|
|
247
247
|
//#endregion
|
|
248
248
|
//#region src/EntityAnchor.d.ts
|
|
249
249
|
type EntityAnchorProps = {
|
|
@@ -254,7 +254,7 @@ type EntityAnchorProps = {
|
|
|
254
254
|
style?: CSSProperties;
|
|
255
255
|
children: ReactNode;
|
|
256
256
|
};
|
|
257
|
-
declare function EntityAnchor(props: EntityAnchorProps):
|
|
257
|
+
declare function EntityAnchor(props: EntityAnchorProps): react4.JSX.Element;
|
|
258
258
|
//#endregion
|
|
259
259
|
//#region src/Port.d.ts
|
|
260
260
|
type PortProps = {
|
|
@@ -288,7 +288,7 @@ type PortProps = {
|
|
|
288
288
|
* observe results via `flow.on("connectionEnded", …)` or the store's
|
|
289
289
|
* `connection` state.
|
|
290
290
|
*/
|
|
291
|
-
declare function Port(props: PortProps):
|
|
291
|
+
declare function Port(props: PortProps): react4.JSX.Element;
|
|
292
292
|
//#endregion
|
|
293
293
|
//#region src/ColorMode.d.ts
|
|
294
294
|
type FlowColorModeScopeProps = {
|
|
@@ -296,7 +296,7 @@ type FlowColorModeScopeProps = {
|
|
|
296
296
|
className?: string;
|
|
297
297
|
style?: CSSProperties;
|
|
298
298
|
};
|
|
299
|
-
declare function FlowColorModeScope(props: FlowColorModeScopeProps):
|
|
299
|
+
declare function FlowColorModeScope(props: FlowColorModeScopeProps): react4.JSX.Element;
|
|
300
300
|
declare function useFlowColorMode(): {
|
|
301
301
|
colorMode: FlowColorMode;
|
|
302
302
|
resolvedColorMode: ResolvedFlowColorMode;
|
package/dist/esm/index.js
CHANGED
|
@@ -1,8 +1,241 @@
|
|
|
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
|
+
//#region src/createFlowInstance.ts
|
|
7
|
+
function createFlowInstance(options) {
|
|
8
|
+
const flow = createFlow({
|
|
9
|
+
graph: options.graph,
|
|
10
|
+
...options.settings
|
|
11
|
+
});
|
|
12
|
+
const cleanup = /* @__PURE__ */ new Set();
|
|
13
|
+
function getSnapshot() {
|
|
14
|
+
return flow.store.getSnapshot().context;
|
|
15
|
+
}
|
|
16
|
+
function clientToCanvas(point) {
|
|
17
|
+
const rect = getAttachedContainer(flow.store)?.getBoundingClientRect();
|
|
18
|
+
return flow.screenToCanvas({
|
|
19
|
+
x: point.x - (rect?.left ?? 0),
|
|
20
|
+
y: point.y - (rect?.top ?? 0)
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function listen(selector, listener, listenerOptions = {}) {
|
|
24
|
+
const isEqual = listenerOptions.isEqual ?? Object.is;
|
|
25
|
+
let previousValue = selector(getSnapshot());
|
|
26
|
+
if (listenerOptions.fireImmediately) listener(previousValue, previousValue, getSnapshot());
|
|
27
|
+
const unsubscribe = flow.subscribe((context) => {
|
|
28
|
+
const nextValue = selector(context);
|
|
29
|
+
if (isEqual(previousValue, nextValue)) return;
|
|
30
|
+
const currentPreviousValue = previousValue;
|
|
31
|
+
previousValue = nextValue;
|
|
32
|
+
listener(nextValue, currentPreviousValue, context);
|
|
33
|
+
});
|
|
34
|
+
cleanup.add(unsubscribe);
|
|
35
|
+
return () => {
|
|
36
|
+
cleanup.delete(unsubscribe);
|
|
37
|
+
unsubscribe();
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function subscribeEvent(event, listener) {
|
|
41
|
+
const subscription = flow.on(event, listener);
|
|
42
|
+
const unsubscribe = () => subscription.unsubscribe();
|
|
43
|
+
cleanup.add(unsubscribe);
|
|
44
|
+
return () => {
|
|
45
|
+
cleanup.delete(unsubscribe);
|
|
46
|
+
unsubscribe();
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (options.events?.connectionEnded) subscribeEvent("connectionEnded", options.events.connectionEnded);
|
|
50
|
+
if (options.events?.layoutRequested) subscribeEvent("layoutRequested", options.events.layoutRequested);
|
|
51
|
+
for (const listener of options.listeners ?? []) listen(listener.selector, listener.onChange, listener);
|
|
52
|
+
return {
|
|
53
|
+
...flow,
|
|
54
|
+
getSnapshot,
|
|
55
|
+
listen,
|
|
56
|
+
clientToCanvas,
|
|
57
|
+
destroy() {
|
|
58
|
+
for (const unsubscribe of cleanup) unsubscribe();
|
|
59
|
+
cleanup.clear();
|
|
60
|
+
flow.destroy();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/hooks.ts
|
|
66
|
+
function useFlow(options) {
|
|
67
|
+
return useCreateFlowInstance(options);
|
|
68
|
+
}
|
|
69
|
+
function useCreateFlowInstance(options) {
|
|
70
|
+
const instanceRef = useRef(null);
|
|
71
|
+
const listenersRef = useRef(options.listeners);
|
|
72
|
+
const eventsRef = useRef(options.events);
|
|
73
|
+
const didMountRef = useRef(false);
|
|
74
|
+
listenersRef.current = options.listeners;
|
|
75
|
+
eventsRef.current = options.events;
|
|
76
|
+
if (!instanceRef.current) instanceRef.current = createFlowInstance({
|
|
77
|
+
graph: options.graph,
|
|
78
|
+
settings: options.settings
|
|
79
|
+
});
|
|
80
|
+
const flow = instanceRef.current;
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!didMountRef.current) {
|
|
83
|
+
didMountRef.current = true;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
flow.trigger.setGraph({ graph: options.graph });
|
|
87
|
+
}, [flow, options.graph]);
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const cleanup = [];
|
|
90
|
+
if (eventsRef.current?.connectionEnded) {
|
|
91
|
+
const subscription = flow.on("connectionEnded", eventsRef.current.connectionEnded);
|
|
92
|
+
cleanup.push(() => subscription.unsubscribe());
|
|
93
|
+
}
|
|
94
|
+
if (eventsRef.current?.layoutRequested) {
|
|
95
|
+
const subscription = flow.on("layoutRequested", eventsRef.current.layoutRequested);
|
|
96
|
+
cleanup.push(() => subscription.unsubscribe());
|
|
97
|
+
}
|
|
98
|
+
for (const listener of listenersRef.current ?? []) cleanup.push(flow.listen(listener.selector, listener.onChange, listener));
|
|
99
|
+
return () => {
|
|
100
|
+
for (const dispose of cleanup) dispose();
|
|
101
|
+
};
|
|
102
|
+
}, [
|
|
103
|
+
flow,
|
|
104
|
+
options.listeners,
|
|
105
|
+
options.events
|
|
106
|
+
]);
|
|
107
|
+
useEffect(() => () => flow.destroy(), [flow]);
|
|
108
|
+
return flow;
|
|
109
|
+
}
|
|
110
|
+
function useFlowSelector(selector, options = {}) {
|
|
111
|
+
const contextFlow = useOptionalFlowInstance();
|
|
112
|
+
const flow = options.flow ?? contextFlow;
|
|
113
|
+
if (!flow) throw new Error("Flow instance is missing. Pass { flow } or render inside <FlowInstanceProvider>.");
|
|
114
|
+
const selectorRef = useRef(selector);
|
|
115
|
+
const isEqualRef = useRef(options.isEqual ?? Object.is);
|
|
116
|
+
selectorRef.current = selector;
|
|
117
|
+
isEqualRef.current = options.isEqual ?? Object.is;
|
|
118
|
+
const [selected, setSelected] = useState(() => selector(flow.getSnapshot()));
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
setSelected((previous) => {
|
|
121
|
+
const next = selectorRef.current(flow.getSnapshot());
|
|
122
|
+
return isEqualRef.current(previous, next) ? previous : next;
|
|
123
|
+
});
|
|
124
|
+
return flow.listen((context) => selectorRef.current(context), (value) => {
|
|
125
|
+
setSelected((previous) => isEqualRef.current(previous, value) ? previous : value);
|
|
126
|
+
});
|
|
127
|
+
}, [flow]);
|
|
128
|
+
return selected;
|
|
129
|
+
}
|
|
130
|
+
function useFlowContext() {
|
|
131
|
+
return useFlowSelector((context) => context);
|
|
132
|
+
}
|
|
133
|
+
const entityNotifiers = /* @__PURE__ */ new WeakMap();
|
|
134
|
+
function getEntityNotifier(flow) {
|
|
135
|
+
let notifier = entityNotifiers.get(flow.store);
|
|
136
|
+
if (!notifier) {
|
|
137
|
+
notifier = createEntityChangeNotifier(flow.store);
|
|
138
|
+
entityNotifiers.set(flow.store, notifier);
|
|
139
|
+
}
|
|
140
|
+
return notifier;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Like `useFlowSelector`, but subscribed through the entity-change notifier:
|
|
144
|
+
* the selector re-runs only when the entity with `id` may have changed, not
|
|
145
|
+
* on every store transition. This is what makes thousands of mounted entity
|
|
146
|
+
* views affordable — see BENCHMARK.md.
|
|
147
|
+
*/
|
|
148
|
+
function useFlowEntity(id, selector, options = {}) {
|
|
149
|
+
const contextFlow = useOptionalFlowInstance();
|
|
150
|
+
const flow = options.flow ?? contextFlow;
|
|
151
|
+
if (!flow) throw new Error("Flow instance is missing. Pass { flow } or render inside <FlowInstanceProvider>.");
|
|
152
|
+
const selectorRef = useRef(selector);
|
|
153
|
+
const isEqualRef = useRef(options.isEqual ?? Object.is);
|
|
154
|
+
selectorRef.current = selector;
|
|
155
|
+
isEqualRef.current = options.isEqual ?? Object.is;
|
|
156
|
+
const [selected, setSelected] = useState(() => selector(flow.getSnapshot()));
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
const update = () => {
|
|
159
|
+
setSelected((previous) => {
|
|
160
|
+
const next = selectorRef.current(flow.getSnapshot());
|
|
161
|
+
return isEqualRef.current(previous, next) ? previous : next;
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
update();
|
|
165
|
+
return getEntityNotifier(flow).subscribe(id, update);
|
|
166
|
+
}, [flow, id]);
|
|
167
|
+
return selected;
|
|
168
|
+
}
|
|
169
|
+
function useFlowValue(selector, isEqual) {
|
|
170
|
+
return useFlowSelector(selector, { isEqual });
|
|
171
|
+
}
|
|
172
|
+
function useFlowNode(id, selector, isEqual) {
|
|
173
|
+
return useFlowSelector((context) => {
|
|
174
|
+
const node = context.graph.nodes.find((candidate) => candidate.id === id);
|
|
175
|
+
if (!node) throw new Error(`Flow node "${id}" was not found.`);
|
|
176
|
+
if (selector) return selector(node, context);
|
|
177
|
+
return {
|
|
178
|
+
...node,
|
|
179
|
+
...getDerivedFields(context, id, {
|
|
180
|
+
"data-flow-target": "entity",
|
|
181
|
+
"data-flow-entity-id": id,
|
|
182
|
+
"data-entity-id": id
|
|
183
|
+
})
|
|
184
|
+
};
|
|
185
|
+
}, { isEqual });
|
|
186
|
+
}
|
|
187
|
+
function useFlowEdge(id, selector, isEqual) {
|
|
188
|
+
const flow = useOptionalFlowInstance();
|
|
189
|
+
return useFlowSelector((context) => {
|
|
190
|
+
const edge = context.graph.edges.find((candidate) => candidate.id === id);
|
|
191
|
+
if (!edge) throw new Error(`Flow edge "${id}" was not found.`);
|
|
192
|
+
if (selector) return selector(edge, context);
|
|
193
|
+
return {
|
|
194
|
+
...edge,
|
|
195
|
+
pathData: flow?.getAllEdgePaths().get(id) ?? null,
|
|
196
|
+
...getDerivedFields(context, id, {
|
|
197
|
+
"data-flow-target": "entity",
|
|
198
|
+
"data-flow-entity-id": id,
|
|
199
|
+
"data-entity-id": id
|
|
200
|
+
})
|
|
201
|
+
};
|
|
202
|
+
}, { isEqual });
|
|
203
|
+
}
|
|
204
|
+
function useFlowPort(id, selector, isEqual) {
|
|
205
|
+
const flow = useFlowInstance();
|
|
206
|
+
return useFlowSelector((context) => {
|
|
207
|
+
const addressable = flow?.getAddressable(id);
|
|
208
|
+
if (!addressable || addressable.kind !== "port") throw new Error(`Flow port "${id}" was not found.`);
|
|
209
|
+
const binding = {
|
|
210
|
+
...addressable.port,
|
|
211
|
+
id,
|
|
212
|
+
nodeId: addressable.nodeId,
|
|
213
|
+
portName: addressable.portName,
|
|
214
|
+
...getDerivedFields(context, id, {
|
|
215
|
+
"data-flow-target": "port",
|
|
216
|
+
"data-flow-port-id": id,
|
|
217
|
+
"data-flow-node-id": addressable.nodeId,
|
|
218
|
+
"data-flow-port-name": addressable.portName
|
|
219
|
+
})
|
|
220
|
+
};
|
|
221
|
+
return selector ? selector(binding, context) : binding;
|
|
222
|
+
}, { isEqual });
|
|
223
|
+
}
|
|
224
|
+
function useFlowViewport() {
|
|
225
|
+
return useFlowSelector((context) => context.viewport);
|
|
226
|
+
}
|
|
227
|
+
function useFlowMeasurement(id) {
|
|
228
|
+
return useFlowSelector((context) => context.measurements.get(id) ?? null);
|
|
229
|
+
}
|
|
230
|
+
function getDerivedFields(context, id, props) {
|
|
231
|
+
return {
|
|
232
|
+
selected: context.selection.has(id),
|
|
233
|
+
focused: context.focusedEntityId === id,
|
|
234
|
+
hovered: context.hoveredEntityIds.has(id),
|
|
235
|
+
props
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
6
239
|
//#region src/useFlowInput.ts
|
|
7
240
|
/**
|
|
8
241
|
* Bind a container element to the core input runtime: all pointer, wheel,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statelyai/flow-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Thin React adapter for @statelyai/flow and @statelyai/flow-dom.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"flow",
|
|
@@ -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.0.1"
|
|
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 };
|