@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.
@@ -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
  };
@@ -1,27 +1,70 @@
1
- import { l as useFlowSelector } from "./hooks.js";
2
- import { jsx } from "react/jsx-runtime";
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 viewport = useFlowSelector((context) => context.viewport);
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 scaledGap = gap * viewport.zoom;
11
- const scaledSize = size * viewport.zoom;
12
- return /* @__PURE__ */ jsx("div", {
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) {
@@ -1,7 +1,7 @@
1
- import * as react10 from "react";
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
- }): react10.JSX.Element;
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>): react10.JSX.Element;
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): react10.JSX.Element | null;
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): react10.JSX.Element;
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): react10.JSX.Element;
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): react10.JSX.Element;
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): react10.JSX.Element;
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): react10.JSX.Element;
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 { 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";
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.4.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": "workspace:*",
46
- "@statelyai/flow-dom": "workspace:*"
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 };