@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 CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  <!-- description from package.json -->
4
4
 
5
- Thin React adapter for `@statelyai/flow` and `@statelyai/flow-dom`.
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
  };
@@ -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,8 @@
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
+ 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
- }): react10.JSX.Element;
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>): react10.JSX.Element;
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): react10.JSX.Element | null;
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): react10.JSX.Element;
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): react10.JSX.Element;
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): react10.JSX.Element;
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): react10.JSX.Element;
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): react10.JSX.Element;
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 { 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
+ 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.0",
4
- "description": "Thin React adapter for @statelyai/flow and @statelyai/flow-dom.",
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": "workspace:*",
46
- "@statelyai/flow-dom": "workspace:*"
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 };