@statelyai/flow-react 0.7.2 → 0.9.2

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
@@ -17,7 +17,7 @@ The React package owns React lifecycle, context, selector subscriptions, and a c
17
17
 
18
18
  <!-- transient entity rendering split between src/StatelyFlow.tsx and flow-dom src/entity-geometry.ts -->
19
19
 
20
- Optimization is part of the architecture, not a later cleanup pass. Components subscribe only to durable content and interaction state. During drag, resize, and animated layout sessions, `@statelyai/flow-dom` batches node rectangles, edge paths, endpoint handles, and label positions into imperative animation-frame writes, so React commits only at transient-session boundaries. Default edge paths expose `data-flow-edge-segment`; custom edge renderers can use the same indexed attribute to join the imperative path updates. Router-provided `PathData.svgPath` geometry is honored by both initial React rendering and those imperative updates.
20
+ Optimization is part of the architecture, not a later cleanup pass. Components subscribe only to durable content and interaction state. During drag and animated layout sessions, `@statelyai/flow-dom` batches node rectangles, edge paths, endpoint handles, label positions, and multi-selection bounds into imperative animation-frame writes, so React commits only at transient-session boundaries. Default edge paths expose `data-flow-edge-segment`; custom edge renderers can use the same indexed attribute to join the imperative path updates. Router-provided `PathData.svgPath` geometry is honored by both initial React rendering and those imperative updates.
21
21
 
22
22
  The graph data structure is the source of truth for graph content, not for transient UI state. Core node and edge data should not grow flags like `selected: true` or `dragging: true`; selection, focus, dragging, hover, connection state, and similar interaction state live in the flow store and are derived into render props such as `selected` or `dragging`.
23
23
 
@@ -27,40 +27,41 @@ DOM measurement is one-directional. Nodes, ports, and edge labels report their r
27
27
 
28
28
  <!-- exported React adapter API from src/index.ts -->
29
29
 
30
- | Export | Description |
31
- | ------------------------------------- | -------------------------------------------------------------------------------- |
32
- | `createFlowInstance(options)` | Creates the shared instance used by React and custom renderers |
33
- | `useFlow(options)` | TipTap-style hook for creating a stable flow controller |
34
- | `useCreateFlowInstance(options)` | Deprecated alias for `useFlow(options)` |
35
- | `useFlowSelector(selector, options?)` | Subscribe to any part of the flow store from React |
36
- | `useFlowValue(selector, isEqual?)` | Short selector hook for the nearest provider |
37
- | `useFlowContext()` | Subscribe to the full flow store context |
38
- | `useConnection()` | Subscribe to the active connection gesture state |
39
- | `useFlowConnections(query)` | Subscribe to stable, port-aware connections for one node |
40
- | `useFlowView()` | Access view-owned image export and measurement refresh |
41
- | `useKeyPress(keys)` | Track whether one or more keys are currently pressed |
42
- | `useNodesInitialized()` | Check whether visible auto-size nodes have measurements |
43
- | `useFlowNode(id, selector?, isEqual?)` | Subscribe to node data plus derived render fields |
44
- | `useFlowEdge(id, selector?, isEqual?)` | Subscribe to edge data plus derived render fields |
45
- | `useFlowPort(id, selector?, isEqual?)` | Subscribe to port data plus derived render fields |
46
- | `useFlowViewport()` | Subscribe to viewport state |
47
- | `useFlowMeasurement(id)` | Subscribe to measured bounds for an entity ref |
48
- | `FlowInstanceProvider` | Provides a `FlowInstance` through React context |
49
- | `useFlowInstance()` | Reads the nearest `FlowInstance` |
50
- | `useOptionalFlowInstance()` | Reads the nearest `FlowInstance` or returns `null` |
51
- | `StatelyFlow` | Convenience component that renders nodes, edges, backgrounds, projections, and overlays |
52
- | `createEntityGeometryObserver(store)` | Batches transient node/label rectangles and edge paths into imperative animation-frame DOM writes |
53
- | `PanePortal` / `ViewportPortal` | Render custom content into fixed screen-coordinate or transformed canvas-coordinate layers |
54
- | `EntityAnchor` | Position custom controls on entity sides or free-standing canvas bounds with screen-stable offsets and inverse scaling |
55
- | `RenderEdgeLabelProps` | Edge, path, bounds, interaction state, and projected attrs passed to `renderEdgeLabel` |
56
- | `Panel` | Positioned overlay panel for controls and custom UI |
57
- | `NodeResizer` | Resize handles for custom node renderers; constraints live on `node.resize` |
58
- | `ReactActiveGraphProjection` | React-typed active projection for `<StatelyFlow projections={...} />` or controller-owned projections |
59
- | `ReactGraphNodeAttrs` / `ReactGraphEdgeAttrs` | React-typed visual attrs exposed to custom node/edge renderers |
60
- | `FlowColorModeScope` | Applies the current flow color mode as DOM data attributes and `color-scheme` |
61
- | `useFlowColorMode()` | Reads the store color mode and resolves `system` to `light` or `dark` |
62
- | `useApplyFlowColorMode(element)` | Applies flow color mode attributes to an existing DOM element from inside a flow |
63
- | `FLOW_BASE_CSS` / `useFlowBaseStyles()` | Base data-attribute stylesheet injected by `StatelyFlow` |
30
+ | Export | Description |
31
+ | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
32
+ | `createFlowInstance(options)` | Creates the shared instance used by React and custom renderers |
33
+ | `useFlow(options)` | TipTap-style hook for creating a stable flow controller |
34
+ | `useCreateFlowInstance(options)` | Deprecated alias for `useFlow(options)` |
35
+ | `useFlowSelector(selector, options?)` | Subscribe to any part of the flow store from React |
36
+ | `useFlowValue(selector, isEqual?)` | Short selector hook for the nearest provider |
37
+ | `useFlowContext()` | Subscribe to the full flow store context |
38
+ | `useConnection()` | Subscribe to the active connection gesture state |
39
+ | `useFlowConnections(query)` | Subscribe to stable, port-aware connections for one node |
40
+ | `useFlowView()` | Access view-owned image export and measurement refresh |
41
+ | `useKeyPress(keys)` | Track whether one or more keys are currently pressed |
42
+ | `useNodesInitialized()` | Check whether visible auto-size nodes have measurements |
43
+ | `useFlowNode(id, selector?, isEqual?)` | Subscribe to node data plus derived render fields |
44
+ | `useFlowEdge(id, selector?, isEqual?)` | Subscribe to edge data plus derived render fields |
45
+ | `useFlowPort(id, selector?, isEqual?)` | Subscribe to port data plus derived render fields |
46
+ | `useFlowViewport()` | Subscribe to viewport state |
47
+ | `useFlowMeasurement(id)` | Subscribe to measured bounds for an entity ref |
48
+ | `FlowInstanceProvider` | Provides a `FlowInstance` through React context |
49
+ | `useFlowInstance()` | Reads the nearest `FlowInstance` |
50
+ | `useOptionalFlowInstance()` | Reads the nearest `FlowInstance` or returns `null` |
51
+ | `StatelyFlow` | Convenience component that renders nodes, edges, backgrounds, projections, and overlays |
52
+ | `createEntityGeometryObserver(store)` | Batches transient node/label rectangles, multi-entity bounds, and edge paths into imperative animation-frame DOM writes |
53
+ | `PanePortal` / `ViewportPortal` | Render custom content into fixed screen-coordinate or transformed canvas-coordinate layers |
54
+ | `useFitViewExclusion()` | Register an overlay element whose measured edge band fit and center commands avoid |
55
+ | `EntityAnchor` | Position custom controls on entity sides or free-standing canvas bounds with screen-stable offsets and inverse scaling |
56
+ | `Port` | Render a measured connection port or a graph-owned readonly inspection port |
57
+ | `RenderEdgeLabelProps` | Edge, path, bounds, interaction state, and projected attrs passed to `renderEdgeLabel` |
58
+ | `Panel` | Positioned overlay panel for controls and custom UI |
59
+ | `ReactActiveGraphProjection` | React-typed active projection for `<StatelyFlow projections={...} />` or controller-owned projections |
60
+ | `ReactGraphNodeAttrs` / `ReactGraphEdgeAttrs` | React-typed visual attrs exposed to custom node/edge renderers |
61
+ | `FlowColorModeScope` | Applies the current flow color mode as DOM data attributes and `color-scheme` |
62
+ | `useFlowColorMode()` | Reads the store color mode and resolves `system` to `light` or `dark` |
63
+ | `useApplyFlowColorMode(element)` | Applies flow color mode attributes to an existing DOM element from inside a flow |
64
+ | `FLOW_BASE_CSS` / `useFlowBaseStyles()` | Base data-attribute stylesheet injected by `StatelyFlow` |
64
65
 
65
66
  `@statelyai/flow-react` and `@statelyai/flow-react/backgrounds` export `Background`, `DotsBackground`, `LinesBackground`, and `CrossBackground` helpers for viewport-aware canvas backgrounds. Backgrounds are plain React components, so consumers can also build custom ones with `useFlowSelector((context) => context.viewport)`.
66
67
 
@@ -70,7 +71,15 @@ Color mode is store data, not engine behavior. `@statelyai/flow-dom` exports sma
70
71
 
71
72
  <!-- StatelyFlow renderEdgeLabel and layers props from src/StatelyFlow.tsx plus portal exports from src/LayerPortals.tsx -->
72
73
 
73
- `renderEdgeLabel` replaces the built-in label content while retaining Flow's positioned, measured, interactive label wrapper. Set `edge.label.autoSize: true` for content-driven custom labels; the wrapper then leaves width/height unset while its measurement drives stable routed geometry. Render props include the edge's current `pathData`: the same `EdgePathData` object already routed by `StatelyFlow`, including both source-to-label and label-to-target segments. Transient drag geometry remains on Flow's imperative DOM path, and the renderer receives current committed geometry at gesture boundaries. Use `layers.viewport` for canvas-coordinate content and `layers.pane` for screen-coordinate content. Descendants can target the same named layers with `ViewportPortal` and `PanePortal`. Layer wrappers ignore pointer input; opt individual controls in with `pointerEvents: "auto"` and mark controls inside labels or canvas layers with `data-flow-interactive` when Flow gestures should ignore them.
74
+ `renderEdgeLabel` replaces the built-in label content while retaining Flow's positioned, measured, interactive label wrapper. Set `edge.label.autoSize: true` for content-driven custom labels; the wrapper then leaves width/height unset while its measurement drives stable routed geometry. Render props include the edge's current `pathData`: the same `EdgePathData` object already routed by `StatelyFlow`, including both source-to-label and label-to-target segments. Transient drag geometry remains on Flow's imperative DOM path, and the renderer receives current committed geometry at gesture boundaries. Use `layers.viewport` for canvas-coordinate interaction content and `layers.pane` for screen-coordinate content. Descendants can target the same named layers with `ViewportPortal` and `PanePortal`. Flow enforces paint bands (`edges` → `entities` → `labels` → `interaction` → `guides` → `chrome`) so entity-local z-index cannot cover labels, selection chrome, anchors, or portaled toolbars. Override the namespaced `--flow-layer-*` variables only when intentionally changing that contract. Layer wrappers ignore pointer input; opt individual controls in with `pointerEvents: "auto"` and mark controls inside labels or canvas layers with `data-flow-interactive` when Flow gestures should ignore them.
75
+
76
+ Attach the ref from `useFitViewExclusion({ side, gap })` to floating controls that cover the canvas. The hook measures the element relative to the Flow root at command time, including content portaled outside the root, so responsive toolbars and panel-relative chrome do not need guessed fit padding.
77
+
78
+ Set `showSelectionBounds` to render a zoom-stable, square frame around established multi-selections. Its geometry follows drag and layout frames through Flow DOM without React commits; theme `[data-flow-selection-bounds]`, `--flow-selection-color`, or the screen-pixel `--flow-selection-bounds-outset`. Set `preserveViewportCenterOnResize` when root container resizes should retain the world point at its center.
79
+
80
+ The default edge renderer honors `edge.style.markerStart` and `markerEnd` values `arrow`, `arrow-closed`, `circle`, and `none`, plus safe local custom marker IDs or `url(#id)` references. External and malformed marker URLs are rejected. Built-in marker definitions are namespaced per `StatelyFlow`, so multiple mounted flows cannot collide. It also renders canonical graph `edge.points`/`routing` and optional `edge.svgPath` without rerouting or splitting the route around a label.
81
+
82
+ Render layout-owned ports with `<Port measured={false} interaction="inspect" />`. Inspection ports are focusable, selectable addressables (`${node.id}:${port.name}`), emit the normal entity click events, and never start a connection gesture. Omit `interaction` (or use `"connect"`) to retain existing connection behavior.
74
83
 
75
84
  Custom node and edge renderers should branch on the application-defined `entity.kind`. Keep `entity.type` as the structural `"node"` or `"edge"` discriminant so core selection, drag, connection, and CRUD behavior remains valid.
76
85
 
@@ -78,7 +87,9 @@ For controlled reconnect gestures, `reconnectMode({ commit: false, preview: fals
78
87
 
79
88
  `EntityAnchor` can be nested inside a node or edge-label renderer with `side`, a child `anchor` point, cross-axis `align`, and an outward `offset`. The nested anchor follows the parent entity's imperative drag geometry without React commits. With `inverseScale`, its size and offset remain constant in screen pixels and its transform origin matches the selected child anchor. The existing `bounds` plus `AnchorSpec` form remains available for free-standing canvas-space overlays.
80
89
 
81
- `StatelyFlow` exposes stable, namespaced selector roles while keeping `data-flow-*` attributes authoritative: `.stately-flow` / `.stately-flow__pane` (interactive root), `.stately-flow__viewport` (transformed canvas), `.stately-flow__edges`, `.stately-flow__nodes`, `.stately-flow__edge-labels`, their singular entity classes, `.stately-flow__background`, `.stately-flow__viewport-layer`, `.stately-flow__pane-layer`, and `.stately-flow__overlays`. Consumer `className` values are merged with these roles.
90
+ <!-- namespaced selector roles and entity state attributes from src/StatelyFlow.tsx -->
91
+
92
+ `StatelyFlow` exposes stable, namespaced selector roles while keeping `data-flow-*` attributes authoritative: `.stately-flow` / `.stately-flow__pane` (interactive root), `.stately-flow__viewport` (transformed canvas), `.stately-flow__edges`, `.stately-flow__nodes`, `.stately-flow__edge-labels`, `.stately-flow__selection-bounds`, their singular entity classes, `.stately-flow__background`, `.stately-flow__viewport-layer`, `.stately-flow__pane-layer`, and `.stately-flow__overlays`. Consumer `className` values are merged with these roles. Focus presentation is consumer-owned and visually off by default: set `--flow-focus-outline` or target `:focus-visible` for browser focus, and use `[data-flow-focused="true"]` for synchronized Flow focus state.
82
93
 
83
94
  Use `rootRef` and `rootProps` to access that input-owned root or attach ordinary div attributes and events such as context menu, double-click, and drag/drop. `rootProps.className` and `rootProps.style` merge with the top-level values; Flow's input runtime remains the sole gesture owner.
84
95
 
@@ -96,14 +107,14 @@ import {
96
107
  StatelyFlow,
97
108
  useFlow,
98
109
  useFlowSelector,
99
- } from "@statelyai/flow-react";
110
+ } from '@statelyai/flow-react';
100
111
 
101
112
  const graph = createGraph({
102
113
  nodes: [
103
- createNode({ id: "a", x: 0, y: 0, data: { label: "Start" } }),
104
- createNode({ id: "b", x: 240, y: 120, data: { label: "Done" } }),
114
+ createNode({ id: 'a', x: 0, y: 0, data: { label: 'Start' } }),
115
+ createNode({ id: 'b', x: 240, y: 120, data: { label: 'Done' } }),
105
116
  ],
106
- edges: [createEdge({ id: "a-b", sourceId: "a", targetId: "b" })],
117
+ edges: [createEdge({ id: 'a-b', sourceId: 'a', targetId: 'b' })],
107
118
  });
108
119
 
109
120
  function Flow() {
@@ -116,7 +127,7 @@ function Flow() {
116
127
  {
117
128
  selector: (context) => context.selection.size,
118
129
  onChange: (selectionSize) => {
119
- console.log("selection size", selectionSize);
130
+ console.log('selection size', selectionSize);
120
131
  },
121
132
  },
122
133
  ],
@@ -127,9 +138,9 @@ function Flow() {
127
138
  flow={flow}
128
139
  projections={[
129
140
  {
130
- projection: { id: "coverage", nodes: { a: "visited" } },
141
+ projection: { id: 'coverage', nodes: { a: 'visited' } },
131
142
  mapper: {
132
- getNodeAttrs: () => ({ color: "#16a34a", badge: "visited" }),
143
+ getNodeAttrs: () => ({ color: '#16a34a', badge: 'visited' }),
133
144
  },
134
145
  },
135
146
  ]}
@@ -140,7 +151,6 @@ function Flow() {
140
151
  );
141
152
  }
142
153
  ```
143
-
144
154
  ## Storybook
145
155
 
146
156
  ```bash
@@ -1,7 +1,7 @@
1
1
  import { Background, BackgroundProps, BackgroundVariant, CrossBackground, DotsBackground, LinesBackground } from "./backgrounds.js";
2
2
  import { FlowView, ResolvedFlowColorMode } from "@statelyai/flow-dom";
3
- import { ActiveGraphProjection, AnchorSpec, ConnectionEndedEvent, ConnectionState, CreateEdgeInput, CreateFlowOptions, EdgePathData, FlowColorMode, FlowCommitEvent, FlowConnection, FlowController, FlowEdge, FlowGraph, FlowInputMode, FlowKeymap, FlowNode, FlowProfile, FlowStoreContext, GraphEdgeAttrs, GraphNodeAttrs, LayoutRequestedEvent, Point, Rect, ResizeHandle, Viewport, VisualPort } from "@statelyai/flow";
4
- import { CSSProperties, ComponentPropsWithoutRef, HTMLAttributes, ReactNode, Ref, RefObject } from "react";
3
+ import { ActiveGraphProjection, AnchorSpec, ConnectionEndedEvent, ConnectionState, CreateEdgeInput, CreateFlowOptions, EdgePathData, FitViewExclusionSide, FlowColorMode, FlowCommitEvent, FlowConnection, FlowController, FlowEdge, FlowGraph, FlowInputMode, FlowKeymap, FlowNode, FlowProfile, FlowStoreContext, GraphEdgeAttrs, GraphNodeAttrs, LayoutRequestedEvent, Point, Rect, Viewport, VisualPort } from "@statelyai/flow";
4
+ import { CSSProperties, ComponentPropsWithoutRef, HTMLAttributes, ReactNode, Ref, RefCallback, RefObject } from "react";
5
5
  export * from "@statelyai/flow-dom";
6
6
  //#region src/createFlowInstance.d.ts
7
7
  type FlowSelector<T> = (context: FlowStoreContext) => T;
@@ -54,6 +54,13 @@ type FlowInstance<TNodeData = any, TEdgeData = any, TLabelData = any> = FlowCont
54
54
  fireImmediately?: boolean;
55
55
  }): () => void;
56
56
  };
57
+ /**
58
+ * Create the React adapter's imperative Flow instance.
59
+ *
60
+ * The instance owns its controller and all listeners registered through
61
+ * `listen` or creation options. `destroy()` removes those listeners and
62
+ * destroys the underlying controller.
63
+ */
57
64
  declare function createFlowInstance<TNodeData = any, TEdgeData = any, TLabelData = any>(options: CreateFlowInstanceOptions<TNodeData, TEdgeData, TLabelData>): FlowInstance<TNodeData, TEdgeData, TLabelData>;
58
65
  //#endregion
59
66
  //#region src/FlowInstanceContext.d.ts
@@ -67,9 +74,16 @@ declare function useOptionalFlowInstance(): FlowInstance | null;
67
74
  //#region src/hooks.d.ts
68
75
  type UseCreateFlowInstanceOptions<TNodeData = any, TEdgeData = any, TLabelData = any> = CreateFlowInstanceOptions<TNodeData, TEdgeData, TLabelData>;
69
76
  type UseFlowOptions<TNodeData = any, TEdgeData = any, TLabelData = any> = UseCreateFlowInstanceOptions<TNodeData, TEdgeData, TLabelData>;
77
+ /**
78
+ * Create one Flow instance for a component lifetime.
79
+ *
80
+ * Creation settings are initial-only; use `flow.configure()` for runtime
81
+ * changes. Event/listener props are rebound when their identities change.
82
+ */
70
83
  declare function useFlow<TNodeData = any, TEdgeData = any, TLabelData = any>(options: UseFlowOptions<TNodeData, TEdgeData, TLabelData>): FlowInstance<TNodeData, TEdgeData, TLabelData>;
71
84
  /** @deprecated Use `useFlow(options)`. */
72
85
  declare function useCreateFlowInstance<TNodeData = any, TEdgeData = any, TLabelData = any>(options: UseCreateFlowInstanceOptions<TNodeData, TEdgeData, TLabelData>): FlowInstance<TNodeData, TEdgeData, TLabelData>;
86
+ /** Subscribe to a derived slice of Flow context with optional equality. */
73
87
  declare function useFlowSelector<T>(selector: FlowSelector<T>, options?: {
74
88
  flow?: FlowInstance;
75
89
  isEqual?: FlowSelectorEquality<T>;
@@ -203,6 +217,10 @@ type StatelyFlowBaseProps<TNodeData = unknown, TEdgeData = unknown> = {
203
217
  renderEdge?: (props: RenderEdgeProps<TEdgeData>) => ReactNode;
204
218
  renderEdgeLabel?: (props: RenderEdgeLabelProps<TEdgeData>) => ReactNode;
205
219
  renderConnectionLine?: (props: RenderConnectionLineProps) => ReactNode;
220
+ /** Render a zoom-stable frame around established multi-selections. */
221
+ showSelectionBounds?: boolean;
222
+ /** Keep the same world point centered when the root container resizes. */
223
+ preserveViewportCenterOnResize?: boolean;
206
224
  /**
207
225
  * Only mount nodes/edges that intersect the viewport (plus an overscan
208
226
  * margin). Recommended for large graphs.
@@ -262,6 +280,12 @@ declare function ViewportPortal(props: {
262
280
  declare function PanePortal(props: {
263
281
  children: ReactNode;
264
282
  }): import("react").ReactPortal | null;
283
+ type UseFitViewExclusionOptions = {
284
+ side?: FitViewExclusionSide;
285
+ gap?: number;
286
+ };
287
+ /** Register an overlay element whose measured edge band fit commands avoid. */
288
+ declare function useFitViewExclusion(options?: UseFitViewExclusionOptions): RefCallback<HTMLElement>;
265
289
  //#endregion
266
290
  //#region src/FlowToolbar.d.ts
267
291
  /** Border-box size of an element, kept current via ResizeObserver. */
@@ -319,7 +343,7 @@ type MiniMapProps = {
319
343
  /**
320
344
  * Overview map. Deliberately thin: all math lives in core
321
345
  * (`getMinimapProjection`, `viewportCenteredOn`) and every interaction is a
322
- * plain store trigger — this component is exactly what you would write in
346
+ * controller viewport command — this component is exactly what you would write in
323
347
  * userland, kept here as a convenience.
324
348
  */
325
349
  declare function MiniMap(props: MiniMapProps): import("react").JSX.Element;
@@ -341,8 +365,7 @@ type ControlsProps = {
341
365
  children?: ReactNode;
342
366
  };
343
367
  /**
344
- * Zoom/fit toolbar. Deliberately thin: every button is one store trigger
345
- * (`zoomBy`, `fitView`) — replicate or extend it freely in userland.
368
+ * Zoom/fit toolbar backed by the controller's constrained viewport API.
346
369
  */
347
370
  declare function Controls(props: ControlsProps): import("react").JSX.Element;
348
371
  //#endregion
@@ -378,6 +401,11 @@ declare function EntityAnchor(props: EntityAnchorProps): import("react").JSX.Ele
378
401
  //#region src/Port.d.ts
379
402
  type PortProps = {
380
403
  name: string;
404
+ /**
405
+ * `inspect` makes a graph-owned port focusable/selectable without starting
406
+ * a connection. Defaults to `connect`.
407
+ */
408
+ interaction?: "connect" | "inspect";
381
409
  /**
382
410
  * Whether the DOM element reports its bounds back to the graph (default
383
411
  * true). Ports are unidirectional, one way or the other:
@@ -434,16 +462,6 @@ type DragHandleProps = HTMLAttributes<HTMLDivElement> & {
434
462
  */
435
463
  declare function DragHandle({ children, style, ...rest }: DragHandleProps): import("react").JSX.Element;
436
464
  //#endregion
437
- //#region src/NodeResizer.d.ts
438
- type NodeResizerProps = {
439
- handles?: ResizeHandle[];
440
- /** Defaults to node selection state. Constraints belong on `node.resize`. */
441
- isVisible?: boolean;
442
- className?: string;
443
- style?: CSSProperties;
444
- };
445
- declare function NodeResizer(props: NodeResizerProps): import("react").JSX.Element | null;
446
- //#endregion
447
465
  //#region src/baseStyles.d.ts
448
466
  /**
449
467
  * Minimal base stylesheet for the flow canvas. Intentionally tiny and
@@ -461,9 +479,10 @@ declare function NodeResizer(props: NodeResizerProps): import("react").JSX.Eleme
461
479
  * inline `pointer-events` on the same element — inline wins over this rule.
462
480
  * - `data-flow-selection-box` — the box-select marquee (styled here; geometry
463
481
  * is set inline by the renderer).
482
+ * - `data-flow-selection-bounds` — the established multi-selection frame.
464
483
  */
465
484
  declare const FLOW_BASE_STYLES_ID = "flow-base-styles";
466
- declare const FLOW_BASE_CSS = "\n[data-flow-root] {\n --flow-selection-color: #2563eb;\n --flow-selection-bg: rgba(37, 99, 235, 0.12);\n touch-action: none;\n}\n\n[data-flow-interactive-when-selected] {\n pointer-events: none;\n}\n[data-flow-selected=\"true\"] [data-flow-interactive-when-selected] {\n pointer-events: auto;\n}\n\n[data-flow-selection-box] {\n position: absolute;\n box-sizing: border-box;\n border: calc(1px / var(--flow-zoom, 1)) solid var(--flow-selection-color);\n border-radius: calc(2px / var(--flow-zoom, 1));\n background: var(--flow-selection-bg);\n pointer-events: none;\n}\n";
485
+ declare const FLOW_BASE_CSS = "\n[data-flow-root] {\n --flow-selection-color: #2563eb;\n --flow-selection-bg: rgba(37, 99, 235, 0.12);\n --flow-selection-bounds-outset: 1px;\n --flow-layer-edges: 0;\n --flow-layer-entities: 10;\n --flow-layer-labels: 20;\n --flow-layer-interaction: 30;\n --flow-layer-guides: 40;\n --flow-layer-chrome: 50;\n touch-action: none;\n}\n\n[data-flow-root]:focus {\n outline: var(--flow-focus-outline, none);\n}\n\n[data-flow-interactive-when-selected] {\n pointer-events: none;\n}\n[data-flow-selected=\"true\"] [data-flow-interactive-when-selected] {\n pointer-events: auto;\n}\n\n[data-flow-custom-node] :where(\n button,\n input,\n select,\n textarea,\n a[href],\n [role=\"button\"],\n [tabindex],\n [data-flow-drag-handle],\n [data-flow-port-name]\n) {\n pointer-events: auto;\n}\n\n[data-flow-selection-box] {\n position: absolute;\n box-sizing: border-box;\n border: calc(1px / var(--flow-zoom, 1)) solid var(--flow-selection-color);\n border-radius: 0;\n background: var(--flow-selection-bg);\n pointer-events: none;\n}\n\n[data-flow-selection-bounds] {\n position: absolute;\n box-sizing: border-box;\n outline: calc(1px / var(--flow-zoom, 1)) solid var(--flow-selection-color);\n outline-offset: calc(var(--flow-selection-bounds-outset) / var(--flow-zoom, 1));\n border-radius: 0;\n pointer-events: none;\n}\n";
467
486
  /** Inject the base stylesheet once per document (deduped by element id). */
468
487
  declare function useFlowBaseStyles(): void;
469
488
  //#endregion
@@ -480,4 +499,4 @@ declare function useFlowColorMode(): {
480
499
  };
481
500
  declare function useApplyFlowColorMode(element: HTMLElement | null): void;
482
501
  //#endregion
483
- export { Background, type BackgroundProps, type BackgroundVariant, Controls, type ControlsProps, type CreateFlowInstanceOptions, CrossBackground, DotsBackground, DragHandle, type DragHandleProps, EntityAnchor, type EntityAnchorAlign, type EntityAnchorPoint, type EntityAnchorProps, type EntityAnchorSide, FLOW_BASE_CSS, FLOW_BASE_STYLES_ID, FlowColorModeScope, type FlowColorModeScopeProps, type FlowEdgeBinding, type FlowEventListeners, type FlowInstance, FlowInstanceProvider, type FlowInstanceSettings, type FlowNodeBinding, type FlowPortBinding, type FlowSelector, type FlowSelectorEquality, type FlowStoreListener, FlowToolbar, type FlowToolbarProps, type FlowToolbarState, LinesBackground, MiniMap, type MiniMapProps, NodeResizer, type NodeResizerProps, PanePortal, Panel, type PanelPosition, type PanelProps, Port, type PortProps, type ReactActiveGraphProjection, type ReactGraphEdgeAttrs, type ReactGraphNodeAttrs, type RenderConnectionLineProps, type RenderEdgeLabelProps, type RenderEdgeProps, type RenderNodeProps, StatelyFlow, type StatelyFlowProps, type StatelyFlowRootProps, type StatelyFlowStyle, type UseCreateFlowInstanceOptions, type UseFlowInputOptions, type UseFlowOptions, type UseFlowToolbarOptions, ViewportPortal, createFlowInstance, useApplyFlowColorMode, useConnection, useCreateFlowInstance, useFlow, useFlowBaseStyles, useFlowColorMode, useFlowConnections, useFlowContext, useFlowEdge, useFlowEntity, useFlowInput, useFlowInstance, useFlowMeasurement, useFlowNode, useFlowPort, useFlowSelector, useFlowToolbar, useFlowValue, useFlowView, useFlowViewport, useKeyPress, useMeasuredSize, useNodesInitialized, useOptionalFlowInstance };
502
+ export { Background, type BackgroundProps, type BackgroundVariant, Controls, type ControlsProps, type CreateFlowInstanceOptions, CrossBackground, DotsBackground, DragHandle, type DragHandleProps, EntityAnchor, type EntityAnchorAlign, type EntityAnchorPoint, type EntityAnchorProps, type EntityAnchorSide, FLOW_BASE_CSS, FLOW_BASE_STYLES_ID, FlowColorModeScope, type FlowColorModeScopeProps, type FlowEdgeBinding, type FlowEventListeners, type FlowInstance, FlowInstanceProvider, type FlowInstanceSettings, type FlowNodeBinding, type FlowPortBinding, type FlowSelector, type FlowSelectorEquality, type FlowStoreListener, FlowToolbar, type FlowToolbarProps, type FlowToolbarState, LinesBackground, MiniMap, type MiniMapProps, PanePortal, Panel, type PanelPosition, type PanelProps, Port, type PortProps, type ReactActiveGraphProjection, type ReactGraphEdgeAttrs, type ReactGraphNodeAttrs, type RenderConnectionLineProps, type RenderEdgeLabelProps, type RenderEdgeProps, type RenderNodeProps, StatelyFlow, type StatelyFlowProps, type StatelyFlowRootProps, type StatelyFlowStyle, type UseCreateFlowInstanceOptions, type UseFitViewExclusionOptions, type UseFlowInputOptions, type UseFlowOptions, type UseFlowToolbarOptions, ViewportPortal, createFlowInstance, useApplyFlowColorMode, useConnection, useCreateFlowInstance, useFitViewExclusion, useFlow, useFlowBaseStyles, useFlowColorMode, useFlowConnections, useFlowContext, useFlowEdge, useFlowEntity, useFlowInput, useFlowInstance, useFlowMeasurement, useFlowNode, useFlowPort, useFlowSelector, useFlowToolbar, useFlowValue, useFlowView, useFlowViewport, useKeyPress, useMeasuredSize, useNodesInitialized, useOptionalFlowInstance };
package/dist/esm/index.js CHANGED
@@ -1,12 +1,19 @@
1
1
  import { a as FlowInstanceProvider, i as LinesBackground, n as CrossBackground, o as useFlowInstance, r as DotsBackground, s as useOptionalFlowInstance, t as Background } from "./backgrounds2.js";
2
2
  import { applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, clientToCanvas, computeRenderableIds, createEntityGeometryObserver, createFlowView, createMeasurementObserver, nodePositionStyle, renderableIdsEqual, resolveFlowColorMode, viewportStyle } from "@statelyai/flow-dom";
3
- import { CONNECTION_PREVIEW_EDGE_ID, areNodesMeasured, createConnectionPreviewGraph, createEntityChangeNotifier, createFlow, createFlowInputRuntime, getAnchorTransform, getAutoPanDelta, getMinimapProjection, getProjectedEdgeAttrs, getProjectedNodeAttrs, getToolbarPlacement, nodeDepths, pathToSVG, profileModesForContext, profilePoliciesForContext, viewportCenteredOn } from "@statelyai/flow";
4
- import { createContext, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
+ import { CONNECTION_PREVIEW_EDGE_ID, areNodesMeasured, createConnectionPreviewGraph, createEntityChangeNotifier, createFlow, createFlowInputRuntime, getAnchorTransform, getAutoPanDelta, getMinimapProjection, getProjectedEdgeAttrs, getProjectedNodeAttrs, getToolbarPlacement, markerToSVG, pathToSVG, profileModesForContext, profilePoliciesForContext, viewportCenteredOn } from "@statelyai/flow";
4
+ import { createContext, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector";
7
7
  import { createPortal } from "react-dom";
8
8
  export * from "@statelyai/flow-dom";
9
9
  //#region src/createFlowInstance.ts
10
+ /**
11
+ * Create the React adapter's imperative Flow instance.
12
+ *
13
+ * The instance owns its controller and all listeners registered through
14
+ * `listen` or creation options. `destroy()` removes those listeners and
15
+ * destroys the underlying controller.
16
+ */
10
17
  function createFlowInstance(options) {
11
18
  const { keymap, ...createFlowSettings } = options.settings ?? {};
12
19
  const flow = createFlow({
@@ -68,6 +75,12 @@ function createFlowInstance(options) {
68
75
  }
69
76
  //#endregion
70
77
  //#region src/hooks.ts
78
+ /**
79
+ * Create one Flow instance for a component lifetime.
80
+ *
81
+ * Creation settings are initial-only; use `flow.configure()` for runtime
82
+ * changes. Event/listener props are rebound when their identities change.
83
+ */
71
84
  function useFlow(options) {
72
85
  return useCreateFlowInstance(options);
73
86
  }
@@ -125,6 +138,7 @@ function useCreateFlowInstance(options) {
125
138
  }, [flow]);
126
139
  return flow;
127
140
  }
141
+ /** Subscribe to a derived slice of Flow context with optional equality. */
128
142
  function useFlowSelector(selector, options = {}) {
129
143
  const contextFlow = useOptionalFlowInstance();
130
144
  const flow = options.flow ?? contextFlow;
@@ -375,15 +389,27 @@ function useFlowView() {
375
389
  * inline `pointer-events` on the same element — inline wins over this rule.
376
390
  * - `data-flow-selection-box` — the box-select marquee (styled here; geometry
377
391
  * is set inline by the renderer).
392
+ * - `data-flow-selection-bounds` — the established multi-selection frame.
378
393
  */
379
394
  const FLOW_BASE_STYLES_ID = "flow-base-styles";
380
395
  const FLOW_BASE_CSS = `
381
396
  [data-flow-root] {
382
397
  --flow-selection-color: #2563eb;
383
398
  --flow-selection-bg: rgba(37, 99, 235, 0.12);
399
+ --flow-selection-bounds-outset: 1px;
400
+ --flow-layer-edges: 0;
401
+ --flow-layer-entities: 10;
402
+ --flow-layer-labels: 20;
403
+ --flow-layer-interaction: 30;
404
+ --flow-layer-guides: 40;
405
+ --flow-layer-chrome: 50;
384
406
  touch-action: none;
385
407
  }
386
408
 
409
+ [data-flow-root]:focus {
410
+ outline: var(--flow-focus-outline, none);
411
+ }
412
+
387
413
  [data-flow-interactive-when-selected] {
388
414
  pointer-events: none;
389
415
  }
@@ -391,14 +417,37 @@ const FLOW_BASE_CSS = `
391
417
  pointer-events: auto;
392
418
  }
393
419
 
420
+ [data-flow-custom-node] :where(
421
+ button,
422
+ input,
423
+ select,
424
+ textarea,
425
+ a[href],
426
+ [role="button"],
427
+ [tabindex],
428
+ [data-flow-drag-handle],
429
+ [data-flow-port-name]
430
+ ) {
431
+ pointer-events: auto;
432
+ }
433
+
394
434
  [data-flow-selection-box] {
395
435
  position: absolute;
396
436
  box-sizing: border-box;
397
437
  border: calc(1px / var(--flow-zoom, 1)) solid var(--flow-selection-color);
398
- border-radius: calc(2px / var(--flow-zoom, 1));
438
+ border-radius: 0;
399
439
  background: var(--flow-selection-bg);
400
440
  pointer-events: none;
401
441
  }
442
+
443
+ [data-flow-selection-bounds] {
444
+ position: absolute;
445
+ box-sizing: border-box;
446
+ outline: calc(1px / var(--flow-zoom, 1)) solid var(--flow-selection-color);
447
+ outline-offset: calc(var(--flow-selection-bounds-outset) / var(--flow-zoom, 1));
448
+ border-radius: 0;
449
+ pointer-events: none;
450
+ }
402
451
  `;
403
452
  /** Inject the base stylesheet once per document (deduped by element id). */
404
453
  function useFlowBaseStyles() {
@@ -421,6 +470,18 @@ function useNodeRenderContext() {
421
470
  return context;
422
471
  }
423
472
  //#endregion
473
+ //#region src/focus.ts
474
+ /** Whether a focused descendant owns its own graph focus identity. */
475
+ function descendantOwnsFlowFocus(event) {
476
+ const target = event.target;
477
+ return target instanceof Element && target.closest("[data-flow-port-interaction=\"inspect\"]") !== null;
478
+ }
479
+ function clearEntityFocusOnBlur(flow, entityId, event) {
480
+ const nextTarget = event.relatedTarget;
481
+ if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget)) return;
482
+ if (flow.getSnapshot().focusedEntityId === entityId) flow.internals.store.trigger.clearFocus();
483
+ }
484
+ //#endregion
424
485
  //#region src/LayerPortals.tsx
425
486
  const FlowLayerTargetsContext = createContext(null);
426
487
  function FlowLayerTargetsProvider(props) {
@@ -444,6 +505,43 @@ function PanePortal(props) {
444
505
  const target = useFlowLayerTargets().pane;
445
506
  return target ? createPortal(props.children, target) : null;
446
507
  }
508
+ /** Register an overlay element whose measured edge band fit commands avoid. */
509
+ function useFitViewExclusion(options = {}) {
510
+ const flow = useFlowInstance();
511
+ const root = useFlowLayerTargets().root;
512
+ const [element, setElement] = useState(null);
513
+ const ref = useCallback((next) => setElement(next), []);
514
+ useLayoutEffect(() => {
515
+ if (!root || !element) return;
516
+ element.setAttribute("data-flow-fit-exclusion", options.side ?? "nearest");
517
+ const unregister = flow.viewport.registerFitExclusion({
518
+ side: options.side,
519
+ gap: options.gap,
520
+ getRect: () => {
521
+ if (!element.isConnected || !root.isConnected) return null;
522
+ const rootRect = root.getBoundingClientRect();
523
+ const rect = element.getBoundingClientRect();
524
+ return {
525
+ x: rect.left - rootRect.left,
526
+ y: rect.top - rootRect.top,
527
+ width: rect.width,
528
+ height: rect.height
529
+ };
530
+ }
531
+ });
532
+ return () => {
533
+ unregister();
534
+ element.removeAttribute("data-flow-fit-exclusion");
535
+ };
536
+ }, [
537
+ element,
538
+ flow,
539
+ options.gap,
540
+ options.side,
541
+ root
542
+ ]);
543
+ return ref;
544
+ }
447
545
  //#endregion
448
546
  //#region src/StatelyFlow.tsx
449
547
  function stringArraysEqual(a, b) {
@@ -495,17 +593,24 @@ function OwnedStatelyFlow(props) {
495
593
  }
496
594
  function StatelyFlowView(props) {
497
595
  const containerRef = useRef(null);
596
+ const [rootLayer, setRootLayer] = useState(null);
498
597
  const setContainerRef = useCallback((element) => {
499
598
  containerRef.current = element;
599
+ setRootLayer(element);
500
600
  assignRef(props.rootRef, element);
501
601
  }, [props.rootRef]);
502
602
  const viewportRef = useRef(null);
503
603
  const [viewportLayer, setViewportLayer] = useState(null);
504
604
  const [paneLayer, setPaneLayer] = useState(null);
505
605
  const layerTargets = useMemo(() => ({
606
+ root: rootLayer,
506
607
  viewport: viewportLayer,
507
608
  pane: paneLayer
508
- }), [viewportLayer, paneLayer]);
609
+ }), [
610
+ rootLayer,
611
+ viewportLayer,
612
+ paneLayer
613
+ ]);
509
614
  const measurementObserver = useMemo(() => createMeasurementObserver(props.flow.internals.store, { container: () => containerRef.current }), [props.flow]);
510
615
  const geometryObserver = useMemo(() => createEntityGeometryObserver(props.flow.internals.store, { getEdgeDependents: props.flow.internals.engine.getEdgeSourceDependents }), [props.flow]);
511
616
  useLayoutEffect(() => () => {
@@ -519,8 +624,8 @@ function StatelyFlowView(props) {
519
624
  useLayoutEffect(() => {
520
625
  const element = containerRef.current;
521
626
  if (!element) return;
522
- return attachContainer(props.flow.internals.store, element);
523
- }, [props.flow]);
627
+ return attachContainer(props.flow.internals.store, element, { preserveViewportCenter: props.preserveViewportCenterOnResize });
628
+ }, [props.flow, props.preserveViewportCenterOnResize]);
524
629
  useFlowInput(props.flow, containerRef, {
525
630
  modes: props.inputModes,
526
631
  profile: props.settings?.store?.profile,
@@ -576,7 +681,10 @@ function StatelyFlowView(props) {
576
681
  ref: viewportRef,
577
682
  className: "stately-flow__viewport",
578
683
  "data-flow-viewport": true,
579
- style: viewportStyle(),
684
+ style: {
685
+ ...viewportStyle(),
686
+ isolation: "isolate"
687
+ },
580
688
  children: [
581
689
  /* @__PURE__ */ jsx(EdgeLayer, {
582
690
  geometryObserver,
@@ -598,6 +706,7 @@ function StatelyFlowView(props) {
598
706
  projections,
599
707
  onlyRenderVisible: props.onlyRenderVisible
600
708
  }),
709
+ props.showSelectionBounds ? /* @__PURE__ */ jsx(SelectionBoundsLayer, { geometryObserver }) : null,
601
710
  /* @__PURE__ */ jsx(SelectionBoxLayer, {}),
602
711
  /* @__PURE__ */ jsx("div", {
603
712
  ref: setViewportLayer,
@@ -607,6 +716,7 @@ function StatelyFlowView(props) {
607
716
  style: {
608
717
  position: "absolute",
609
718
  inset: 0,
719
+ zIndex: "var(--flow-layer-interaction, 30)",
610
720
  pointerEvents: "none"
611
721
  },
612
722
  children: props.layers?.viewport
@@ -621,6 +731,7 @@ function StatelyFlowView(props) {
621
731
  style: {
622
732
  position: "absolute",
623
733
  inset: 0,
734
+ zIndex: "var(--flow-layer-chrome, 50)",
624
735
  pointerEvents: "none"
625
736
  },
626
737
  children: props.layers?.pane
@@ -668,6 +779,7 @@ function SnapLinesLayer() {
668
779
  inset: 0,
669
780
  width: "100%",
670
781
  height: "100%",
782
+ zIndex: "var(--flow-layer-guides, 40)",
671
783
  overflow: "visible",
672
784
  pointerEvents: "none"
673
785
  },
@@ -782,16 +894,49 @@ function SelectionBoxLayer() {
782
894
  left: Math.min(a.x, b.x),
783
895
  top: Math.min(a.y, b.y),
784
896
  width: Math.abs(b.x - a.x),
785
- height: Math.abs(b.y - a.y)
897
+ height: Math.abs(b.y - a.y),
898
+ zIndex: "var(--flow-layer-interaction, 30)"
899
+ }
900
+ });
901
+ }
902
+ function SelectionBoundsLayer(props) {
903
+ const flow = useFlowInstance();
904
+ const selectedIds = useFlowSelector((context) => [...context.selection], { isEqual: stringArraysEqual });
905
+ const ref = useRef(null);
906
+ const bounds = selectedIds.length > 1 ? flow.getSelectionBounds() : null;
907
+ useLayoutEffect(() => {
908
+ const element = ref.current;
909
+ if (!element || selectedIds.length < 2) return;
910
+ return props.geometryObserver.observeBounds(element, {
911
+ ids: selectedIds,
912
+ getRect: () => flow.getSelectionBounds()
913
+ });
914
+ }, [
915
+ flow,
916
+ props.geometryObserver,
917
+ selectedIds
918
+ ]);
919
+ if (!bounds) return null;
920
+ return /* @__PURE__ */ jsx("div", {
921
+ ref,
922
+ "aria-hidden": "true",
923
+ className: "stately-flow__selection-bounds",
924
+ "data-flow-selection-bounds": "",
925
+ style: {
926
+ transform: `translate(${bounds.x}px, ${bounds.y}px)`,
927
+ width: bounds.width,
928
+ height: bounds.height,
929
+ zIndex: "var(--flow-layer-interaction, 30)"
786
930
  }
787
931
  });
788
932
  }
789
933
  function EdgeLayer(props) {
790
934
  const instance = useFlowInstance();
935
+ const markerPrefix = `stately-flow-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
791
936
  const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
792
- const edgeIds = useFlowSelector((context) => getOrderedEntities(instance, context).edges.map((edge) => edge.id), { isEqual: stringArraysEqual });
937
+ const edgeIds = useFlowSelector((_context) => getOrderedEntities(instance).edges.map((edge) => edge.id), { isEqual: stringArraysEqual });
793
938
  const renderEdge = props.renderEdge;
794
- return /* @__PURE__ */ jsx("svg", {
939
+ return /* @__PURE__ */ jsxs("svg", {
795
940
  "aria-label": "edges",
796
941
  className: "stately-flow__edges",
797
942
  "data-flow-canvas-layer": true,
@@ -800,23 +945,26 @@ function EdgeLayer(props) {
800
945
  inset: 0,
801
946
  width: "100%",
802
947
  height: "100%",
948
+ zIndex: "var(--flow-layer-edges, 0)",
803
949
  overflow: "visible",
804
950
  pointerEvents: "none"
805
951
  },
806
- children: /* @__PURE__ */ jsxs("g", { children: [edgeIds.map((id) => {
952
+ children: [/* @__PURE__ */ jsx(DefaultMarkerDefinitions, { prefix: markerPrefix }), /* @__PURE__ */ jsxs("g", { children: [edgeIds.map((id) => {
807
953
  if (renderableIds && !renderableIds.edgeIds.has(id)) return null;
808
954
  return /* @__PURE__ */ jsx(EdgeView, {
809
955
  flow: instance,
810
956
  geometryObserver: props.geometryObserver,
811
957
  id,
812
958
  projections: props.projections,
813
- renderEdge
959
+ renderEdge,
960
+ markerPrefix
814
961
  }, id);
815
962
  }), /* @__PURE__ */ jsx(ConnectionPreviewEdge, {
816
963
  flow: instance,
817
964
  renderEdge,
818
- renderConnectionLine: props.renderConnectionLine
819
- })] })
965
+ renderConnectionLine: props.renderConnectionLine,
966
+ markerPrefix
967
+ })] })]
820
968
  });
821
969
  }
822
970
  const EdgeView = memo(function EdgeView(props) {
@@ -860,23 +1008,23 @@ const EdgeView = memo(function EdgeView(props) {
860
1008
  "aria-label": props.flow.internals.engine.getAriaLabel(props.id),
861
1009
  "aria-selected": selected,
862
1010
  tabIndex: focusable ? 0 : void 0,
863
- onFocus: () => props.flow.internals.store.trigger.focus({ entityId: props.id })
1011
+ onFocus: (event) => {
1012
+ if (descendantOwnsFlowFocus(event)) return;
1013
+ props.flow.internals.store.trigger.focus({ entityId: props.id });
1014
+ },
1015
+ onBlur: (event) => clearEntityFocusOnBlur(props.flow, props.id, event)
864
1016
  };
865
1017
  if (props.renderEdge) return /* @__PURE__ */ jsxs("g", {
866
1018
  ref,
867
1019
  ...focusProps,
868
1020
  "data-entity-id": props.id,
869
1021
  "data-flow-selected": selected ? "true" : void 0,
1022
+ "data-flow-focused": focused ? "true" : void 0,
870
1023
  className: classNames$1("stately-flow__edge", attrs.className),
871
- style: {
872
- pointerEvents: "auto",
873
- outline: focused ? "2px solid #2563eb" : "none"
874
- },
1024
+ style: { pointerEvents: "auto" },
875
1025
  children: [/* @__PURE__ */ jsx(EdgeHitTarget, {
876
- flow: props.flow,
877
1026
  edge,
878
- pathData,
879
- selected
1027
+ pathData
880
1028
  }), props.renderEdge({
881
1029
  flow: props.flow,
882
1030
  edge,
@@ -892,18 +1040,19 @@ const EdgeView = memo(function EdgeView(props) {
892
1040
  ...focusProps,
893
1041
  "data-entity-id": props.id,
894
1042
  "data-flow-selected": selected ? "true" : void 0,
1043
+ "data-flow-focused": focused ? "true" : void 0,
895
1044
  className: classNames$1("stately-flow__edge", attrs.className),
896
1045
  style: {
897
1046
  pointerEvents: "auto",
898
- opacity: attrs.opacity,
899
- outline: focused ? "2px solid #2563eb" : "none"
1047
+ opacity: attrs.opacity
900
1048
  },
901
1049
  children: /* @__PURE__ */ jsx(DefaultEdge, {
902
1050
  flow: props.flow,
903
1051
  edge,
904
1052
  pathData,
905
1053
  attrs,
906
- selected
1054
+ selected,
1055
+ markerPrefix: props.markerPrefix
907
1056
  })
908
1057
  });
909
1058
  });
@@ -935,6 +1084,7 @@ function ConnectionPreviewEdge(props) {
935
1084
  pathData,
936
1085
  attrs: {},
937
1086
  selected: false,
1087
+ markerPrefix: props.markerPrefix,
938
1088
  preview: true
939
1089
  })
940
1090
  });
@@ -946,9 +1096,11 @@ function DefaultEdge(props) {
946
1096
  d,
947
1097
  "data-flow-edge-segment": index,
948
1098
  fill: "none",
949
- stroke: props.attrs.color ?? (props.preview ? props.edge.data?.isValid === false ? "#dc2626" : props.edge.data?.isValid === true ? "#16a34a" : "#0f766e" : props.selected ? "#2563eb" : "#94a3b8"),
950
- strokeDasharray: props.preview ? "6 6" : void 0,
951
- strokeWidth: props.attrs.strokeWidth ?? (props.selected ? 3 : 2),
1099
+ stroke: props.attrs.color ?? (props.preview ? props.edge.data?.isValid === false ? "#dc2626" : props.edge.data?.isValid === true ? "#16a34a" : "#0f766e" : props.selected ? "#2563eb" : props.edge.style?.stroke ?? "#94a3b8"),
1100
+ strokeDasharray: props.preview ? "6 6" : props.edge.style?.strokeDasharray,
1101
+ strokeWidth: props.attrs.strokeWidth ?? (props.selected ? 3 : props.edge.style?.strokeWidth ?? 2),
1102
+ markerStart: index === 0 ? markerToSVG(props.edge.style?.markerStart, props.markerPrefix) : void 0,
1103
+ markerEnd: index === props.pathData.segments.length - 1 ? markerToSVG(props.edge.style?.markerEnd, props.markerPrefix) : void 0,
952
1104
  style: props.attrs.style
953
1105
  }), props.preview ? null : /* @__PURE__ */ jsx("path", {
954
1106
  d,
@@ -967,8 +1119,54 @@ function DefaultEdge(props) {
967
1119
  selected: props.selected
968
1120
  })] });
969
1121
  }
1122
+ function DefaultMarkerDefinitions(props) {
1123
+ return /* @__PURE__ */ jsxs("defs", { children: [
1124
+ /* @__PURE__ */ jsx("marker", {
1125
+ id: `${props.prefix}-arrow`,
1126
+ viewBox: "0 0 10 10",
1127
+ refX: "9",
1128
+ refY: "5",
1129
+ markerWidth: "6",
1130
+ markerHeight: "6",
1131
+ orient: "auto-start-reverse",
1132
+ children: /* @__PURE__ */ jsx("path", {
1133
+ d: "M 1 1 L 9 5 L 1 9",
1134
+ fill: "none",
1135
+ stroke: "context-stroke"
1136
+ })
1137
+ }),
1138
+ /* @__PURE__ */ jsx("marker", {
1139
+ id: `${props.prefix}-arrow-closed`,
1140
+ viewBox: "0 0 10 10",
1141
+ refX: "9",
1142
+ refY: "5",
1143
+ markerWidth: "6",
1144
+ markerHeight: "6",
1145
+ orient: "auto-start-reverse",
1146
+ children: /* @__PURE__ */ jsx("path", {
1147
+ d: "M 1 1 L 9 5 L 1 9 Z",
1148
+ fill: "context-stroke"
1149
+ })
1150
+ }),
1151
+ /* @__PURE__ */ jsx("marker", {
1152
+ id: `${props.prefix}-circle`,
1153
+ viewBox: "0 0 10 10",
1154
+ refX: "5",
1155
+ refY: "5",
1156
+ markerWidth: "5",
1157
+ markerHeight: "5",
1158
+ orient: "auto",
1159
+ children: /* @__PURE__ */ jsx("circle", {
1160
+ cx: "5",
1161
+ cy: "5",
1162
+ r: "4",
1163
+ fill: "context-stroke"
1164
+ })
1165
+ })
1166
+ ] });
1167
+ }
970
1168
  function EdgeHitTarget(props) {
971
- return /* @__PURE__ */ jsxs(Fragment, { children: [props.pathData.segments.map((segment, index) => {
1169
+ return /* @__PURE__ */ jsx(Fragment, { children: props.pathData.segments.map((segment, index) => {
972
1170
  return /* @__PURE__ */ jsx("path", {
973
1171
  d: pathToSVG(segment),
974
1172
  "data-flow-edge-segment": index,
@@ -979,7 +1177,7 @@ function EdgeHitTarget(props) {
979
1177
  strokeWidth: 18,
980
1178
  pointerEvents: "stroke"
981
1179
  }, index);
982
- }), /* @__PURE__ */ jsx(EdgeEndpointHandles, { ...props })] });
1180
+ }) });
983
1181
  }
984
1182
  function EdgeEndpointHandles(props) {
985
1183
  if (!props.selected) return null;
@@ -1025,6 +1223,7 @@ function EdgeLabelLayer(props) {
1025
1223
  style: {
1026
1224
  position: "absolute",
1027
1225
  inset: 0,
1226
+ zIndex: "var(--flow-layer-labels, 20)",
1028
1227
  pointerEvents: "none"
1029
1228
  },
1030
1229
  children: useMemo(() => {
@@ -1185,7 +1384,7 @@ function NodeLayer(props) {
1185
1384
  const instance = useFlowInstance();
1186
1385
  const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
1187
1386
  const nodePaintEntries = useFlowSelector((context) => {
1188
- const paintOrder = new Map(getOrderedEntities(instance, context).nodes.map((node, orderIndex) => [node.id, orderIndex]));
1387
+ const paintOrder = new Map(getOrderedEntities(instance).nodes.map((node, orderIndex) => [node.id, orderIndex]));
1189
1388
  return context.graph.nodes.flatMap((node) => {
1190
1389
  const orderIndex = paintOrder.get(node.id);
1191
1390
  return orderIndex === void 0 ? [] : [{
@@ -1202,6 +1401,7 @@ function NodeLayer(props) {
1202
1401
  style: {
1203
1402
  position: "absolute",
1204
1403
  inset: 0,
1404
+ zIndex: "var(--flow-layer-entities, 10)",
1205
1405
  transformOrigin: "0 0",
1206
1406
  pointerEvents: "none"
1207
1407
  },
@@ -1219,33 +1419,8 @@ function NodeLayer(props) {
1219
1419
  })
1220
1420
  });
1221
1421
  }
1222
- function getOrderedEntities(flow, context) {
1223
- const maybeEngine = flow.internals.engine;
1224
- const visibleOrdered = maybeEngine.getVisibleEntities?.();
1225
- if (visibleOrdered) return visibleOrdered;
1226
- const ordered = maybeEngine.getEntityOrder?.();
1227
- if (ordered) return ordered;
1228
- const depths = nodeDepths(context.graph.nodes);
1229
- const nodeOrder = new Map(context.graph.nodes.map((node, index) => [node.id, index]));
1230
- const edgeOrder = new Map(context.graph.edges.map((edge, index) => [edge.id, index]));
1231
- return {
1232
- nodes: [...context.graph.nodes].sort((a, b) => {
1233
- const depthDelta = (depths.get(a.id) ?? 0) - (depths.get(b.id) ?? 0);
1234
- if (depthDelta !== 0) return depthDelta;
1235
- const zDelta = (a.zIndex ?? 0) - (b.zIndex ?? 0);
1236
- if (zDelta !== 0) return zDelta;
1237
- const selectedDelta = Number(context.selection.has(a.id)) - Number(context.selection.has(b.id));
1238
- if (selectedDelta !== 0) return selectedDelta;
1239
- return (nodeOrder.get(a.id) ?? 0) - (nodeOrder.get(b.id) ?? 0);
1240
- }),
1241
- edges: [...context.graph.edges].sort((a, b) => {
1242
- const depthDelta = Math.max(depths.get(a.sourceId) ?? 0, depths.get(a.targetId) ?? 0) - Math.max(depths.get(b.sourceId) ?? 0, depths.get(b.targetId) ?? 0);
1243
- if (depthDelta !== 0) return depthDelta;
1244
- const zDelta = (a.zIndex ?? 0) - (b.zIndex ?? 0);
1245
- if (zDelta !== 0) return zDelta;
1246
- return (edgeOrder.get(a.id) ?? 0) - (edgeOrder.get(b.id) ?? 0);
1247
- })
1248
- };
1422
+ function getOrderedEntities(flow) {
1423
+ return flow.internals.engine.getVisibleEntities();
1249
1424
  }
1250
1425
  function rectsShallowEqual(a, b) {
1251
1426
  return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
@@ -1336,6 +1511,8 @@ const NodeView = memo(function NodeView(props) {
1336
1511
  projections: props.projections
1337
1512
  });
1338
1513
  const focusable = props.flow.internals.engine.resolveEntityConfig(node).focusable;
1514
+ const groupGeometry = node.group ? props.flow.internals.engine.getGroupGeometry(node.id) : void 0;
1515
+ const groupHasInteractiveContent = Boolean(groupGeometry?.contentBounds.width && groupGeometry.contentBounds.height);
1339
1516
  return /* @__PURE__ */ jsx(NodeRenderContextProvider, {
1340
1517
  value: {
1341
1518
  node,
@@ -1345,35 +1522,45 @@ const NodeView = memo(function NodeView(props) {
1345
1522
  ref,
1346
1523
  "data-entity-id": props.id,
1347
1524
  "data-flow-selected": selected ? "true" : void 0,
1525
+ "data-flow-focused": focused ? "true" : void 0,
1348
1526
  className: classNames$1("stately-flow__node", attrs.className),
1349
1527
  role: "group",
1350
1528
  "aria-roledescription": "flow node",
1351
1529
  "aria-selected": selected,
1352
1530
  "aria-label": props.flow.internals.engine.getAriaLabel(props.id) + (selected ? ", selected" : ""),
1353
1531
  tabIndex: focusable ? 0 : void 0,
1354
- onFocus: () => props.flow.internals.store.trigger.focus({ entityId: props.id }),
1532
+ onFocus: (event) => {
1533
+ if (descendantOwnsFlowFocus(event)) return;
1534
+ props.flow.internals.store.trigger.focus({ entityId: props.id });
1535
+ },
1536
+ onBlur: (event) => clearEntityFocusOnBlur(props.flow, props.id, event),
1355
1537
  style: {
1356
1538
  ...nodePositionStyle(bounds, {
1357
1539
  autoSize: contentOwnsSize,
1358
1540
  dragging
1359
1541
  }),
1360
1542
  cursor: dragging ? "grabbing" : "grab",
1361
- pointerEvents: "auto",
1543
+ pointerEvents: node.group && groupHasInteractiveContent ? "none" : "auto",
1362
1544
  userSelect: "none",
1363
1545
  zIndex: props.orderIndex,
1364
- outline: focused ? "2px solid #2563eb" : "none",
1365
- outlineOffset: 2,
1366
1546
  opacity: attrs.opacity
1367
1547
  },
1368
- children: props.renderNode ? props.renderNode({
1369
- flow: props.flow,
1370
- node,
1371
- bounds,
1372
- attrs,
1373
- selected,
1374
- dragging,
1375
- hovered,
1376
- highlights
1548
+ children: props.renderNode ? /* @__PURE__ */ jsx("div", {
1549
+ "data-flow-custom-node": true,
1550
+ style: {
1551
+ display: "contents",
1552
+ pointerEvents: "none"
1553
+ },
1554
+ children: props.renderNode({
1555
+ flow: props.flow,
1556
+ node,
1557
+ bounds,
1558
+ attrs,
1559
+ selected,
1560
+ dragging,
1561
+ hovered,
1562
+ highlights
1563
+ })
1377
1564
  }) : node.group ? /* @__PURE__ */ jsx(DefaultGroupNode, {
1378
1565
  flow: props.flow,
1379
1566
  node,
@@ -1439,6 +1626,7 @@ function DefaultGroupNode(props) {
1439
1626
  color: "#0f172a",
1440
1627
  font: "600 13px/1.2 system-ui, sans-serif",
1441
1628
  cursor: props.dragging ? "grabbing" : "grab",
1629
+ pointerEvents: "auto",
1442
1630
  userSelect: "none",
1443
1631
  ...props.attrs.style
1444
1632
  },
@@ -1601,7 +1789,7 @@ function FlowToolbar(props) {
1601
1789
  /**
1602
1790
  * Overview map. Deliberately thin: all math lives in core
1603
1791
  * (`getMinimapProjection`, `viewportCenteredOn`) and every interaction is a
1604
- * plain store trigger — this component is exactly what you would write in
1792
+ * controller viewport command — this component is exactly what you would write in
1605
1793
  * userland, kept here as a convenience.
1606
1794
  */
1607
1795
  function MiniMap(props) {
@@ -1626,7 +1814,7 @@ function MiniMap(props) {
1626
1814
  x: event.clientX - rect.left,
1627
1815
  y: event.clientY - rect.top
1628
1816
  });
1629
- flow.internals.store.trigger.setViewport(viewportCenteredOn(canvasPoint, viewportSize, viewport.zoom));
1817
+ flow.viewport.set(viewportCenteredOn(canvasPoint, viewportSize, viewport.zoom));
1630
1818
  };
1631
1819
  return /* @__PURE__ */ jsxs("svg", {
1632
1820
  "aria-label": "minimap",
@@ -1749,8 +1937,7 @@ const buttonStyle = {
1749
1937
  cursor: "pointer"
1750
1938
  };
1751
1939
  /**
1752
- * Zoom/fit toolbar. Deliberately thin: every button is one store trigger
1753
- * (`zoomBy`, `fitView`) — replicate or extend it freely in userland.
1940
+ * Zoom/fit toolbar backed by the controller's constrained viewport API.
1754
1941
  */
1755
1942
  function Controls(props) {
1756
1943
  const flow = useFlowInstance();
@@ -1770,21 +1957,21 @@ function Controls(props) {
1770
1957
  type: "button",
1771
1958
  "aria-label": "zoom in",
1772
1959
  style: buttonStyle,
1773
- onClick: () => flow.internals.store.trigger.zoomBy({ factor: 1.2 }),
1960
+ onClick: () => flow.viewport.zoomBy(1.2),
1774
1961
  children: "+"
1775
1962
  }),
1776
1963
  /* @__PURE__ */ jsx("button", {
1777
1964
  type: "button",
1778
1965
  "aria-label": "zoom out",
1779
1966
  style: buttonStyle,
1780
- onClick: () => flow.internals.store.trigger.zoomBy({ factor: 1 / 1.2 }),
1967
+ onClick: () => flow.viewport.zoomBy(1 / 1.2),
1781
1968
  children: "−"
1782
1969
  }),
1783
1970
  /* @__PURE__ */ jsx("button", {
1784
1971
  type: "button",
1785
1972
  "aria-label": "fit view",
1786
1973
  style: buttonStyle,
1787
- onClick: () => flow.internals.store.trigger.fitView({ padding: .1 }),
1974
+ onClick: () => flow.viewport.fit({ padding: .1 }),
1788
1975
  children: "⛶"
1789
1976
  }),
1790
1977
  props.children
@@ -1891,6 +2078,11 @@ function Port(props) {
1891
2078
  const observerRef = useRef(null);
1892
2079
  const observer = useMeasurementObserver();
1893
2080
  const measured = props.measured ?? true;
2081
+ const interaction = props.interaction ?? "connect";
2082
+ const port = node.ports?.find((candidate) => candidate.name === props.name);
2083
+ const portId = `${node.id}:${props.name}`;
2084
+ const focusable = flow.internals.engine.resolveEntityConfig(node).focusable;
2085
+ const selected = useFlowSelector((context) => context.selection.has(portId), { flow });
1894
2086
  useLayoutEffect(() => {
1895
2087
  const element = ref.current;
1896
2088
  if (!element || !measured) return;
@@ -1926,8 +2118,20 @@ function Port(props) {
1926
2118
  className: props.className,
1927
2119
  "data-flow-port-name": props.name,
1928
2120
  "data-flow-node-id": node.id,
2121
+ "data-flow-port-id": portId,
2122
+ "data-flow-port-interaction": interaction,
2123
+ "data-flow-selected": selected ? "true" : void 0,
1929
2124
  "data-connection-candidate": candidate ?? void 0,
1930
- style: props.style,
2125
+ role: interaction === "inspect" ? "button" : void 0,
2126
+ "aria-label": interaction === "inspect" ? `${port?.label ?? props.name} port` : void 0,
2127
+ "aria-pressed": interaction === "inspect" ? selected : void 0,
2128
+ tabIndex: interaction === "inspect" && focusable ? 0 : void 0,
2129
+ onFocus: interaction === "inspect" && focusable ? () => flow.internals.store.trigger.focus({ entityId: portId }) : void 0,
2130
+ onBlur: interaction === "inspect" ? (event) => clearEntityFocusOnBlur(flow, portId, event) : void 0,
2131
+ style: {
2132
+ pointerEvents: "auto",
2133
+ ...props.style
2134
+ },
1931
2135
  children: props.children
1932
2136
  });
1933
2137
  }
@@ -1964,96 +2168,6 @@ function DragHandle({ children, style, ...rest }) {
1964
2168
  });
1965
2169
  }
1966
2170
  //#endregion
1967
- //#region src/NodeResizer.tsx
1968
- const DEFAULT_HANDLES = [
1969
- "top",
1970
- "right",
1971
- "bottom",
1972
- "left",
1973
- "top-left",
1974
- "top-right",
1975
- "bottom-left",
1976
- "bottom-right"
1977
- ];
1978
- const HANDLE_STYLE = {
1979
- top: {
1980
- left: "50%",
1981
- top: 0,
1982
- cursor: "ns-resize"
1983
- },
1984
- right: {
1985
- left: "100%",
1986
- top: "50%",
1987
- cursor: "ew-resize"
1988
- },
1989
- bottom: {
1990
- left: "50%",
1991
- top: "100%",
1992
- cursor: "ns-resize"
1993
- },
1994
- left: {
1995
- left: 0,
1996
- top: "50%",
1997
- cursor: "ew-resize"
1998
- },
1999
- "top-left": {
2000
- left: 0,
2001
- top: 0,
2002
- cursor: "nwse-resize"
2003
- },
2004
- "top-right": {
2005
- left: "100%",
2006
- top: 0,
2007
- cursor: "nesw-resize"
2008
- },
2009
- "bottom-left": {
2010
- left: 0,
2011
- top: "100%",
2012
- cursor: "nesw-resize"
2013
- },
2014
- "bottom-right": {
2015
- left: "100%",
2016
- top: "100%",
2017
- cursor: "nwse-resize"
2018
- }
2019
- };
2020
- function NodeResizer(props) {
2021
- const { node } = useNodeRenderContext();
2022
- const selected = useFlowSelector((context) => context.selection.has(node.id));
2023
- if (!(props.isVisible ?? selected)) return null;
2024
- return /* @__PURE__ */ jsx("div", {
2025
- "aria-hidden": "true",
2026
- className: props.className,
2027
- "data-flow-node-resizer": "",
2028
- style: {
2029
- position: "absolute",
2030
- inset: 0,
2031
- pointerEvents: "none",
2032
- ...props.style
2033
- },
2034
- children: (props.handles ?? DEFAULT_HANDLES).map((handle) => /* @__PURE__ */ jsx("div", {
2035
- "data-entity-id": node.id,
2036
- "data-flow-part": `resize-handle:${handle}`,
2037
- "data-flow-resize-handle": handle,
2038
- style: {
2039
- position: "absolute",
2040
- width: "calc(10px / var(--flow-zoom, 1))",
2041
- height: "calc(10px / var(--flow-zoom, 1))",
2042
- minWidth: 6,
2043
- minHeight: 6,
2044
- boxSizing: "border-box",
2045
- border: "calc(1px / var(--flow-zoom, 1)) solid #ffffff",
2046
- borderRadius: "calc(2px / var(--flow-zoom, 1))",
2047
- background: "var(--flow-selection-color, #2563eb)",
2048
- boxShadow: "0 0 0 calc(1px / var(--flow-zoom, 1)) rgba(15, 23, 42, 0.8)",
2049
- pointerEvents: "auto",
2050
- transform: "translate(-50%, -50%)",
2051
- ...HANDLE_STYLE[handle]
2052
- }
2053
- }, handle))
2054
- });
2055
- }
2056
- //#endregion
2057
2171
  //#region src/ColorMode.tsx
2058
2172
  function FlowColorModeScope(props) {
2059
2173
  const { colorMode, resolvedColorMode } = useFlowColorMode();
@@ -2098,4 +2212,4 @@ function useApplyFlowColorMode(element) {
2098
2212
  ]);
2099
2213
  }
2100
2214
  //#endregion
2101
- export { Background, Controls, CrossBackground, DotsBackground, DragHandle, EntityAnchor, FLOW_BASE_CSS, FLOW_BASE_STYLES_ID, FlowColorModeScope, FlowInstanceProvider, FlowToolbar, LinesBackground, MiniMap, NodeResizer, PanePortal, Panel, Port, StatelyFlow, ViewportPortal, createFlowInstance, useApplyFlowColorMode, useConnection, useCreateFlowInstance, useFlow, useFlowBaseStyles, useFlowColorMode, useFlowConnections, useFlowContext, useFlowEdge, useFlowEntity, useFlowInput, useFlowInstance, useFlowMeasurement, useFlowNode, useFlowPort, useFlowSelector, useFlowToolbar, useFlowValue, useFlowView, useFlowViewport, useKeyPress, useMeasuredSize, useNodesInitialized, useOptionalFlowInstance };
2215
+ export { Background, Controls, CrossBackground, DotsBackground, DragHandle, EntityAnchor, FLOW_BASE_CSS, FLOW_BASE_STYLES_ID, FlowColorModeScope, FlowInstanceProvider, FlowToolbar, LinesBackground, MiniMap, PanePortal, Panel, Port, StatelyFlow, ViewportPortal, createFlowInstance, useApplyFlowColorMode, useConnection, useCreateFlowInstance, useFitViewExclusion, useFlow, useFlowBaseStyles, useFlowColorMode, useFlowConnections, useFlowContext, useFlowEdge, useFlowEntity, useFlowInput, useFlowInstance, useFlowMeasurement, useFlowNode, useFlowPort, useFlowSelector, useFlowToolbar, useFlowValue, useFlowView, useFlowViewport, useKeyPress, useMeasuredSize, useNodesInitialized, useOptionalFlowInstance };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/flow-react",
3
- "version": "0.7.2",
3
+ "version": "0.9.2",
4
4
  "description": "Thin React adapter for @statelyai/flow with core and DOM re-exports.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -34,8 +34,8 @@
34
34
  "license": "MIT",
35
35
  "dependencies": {
36
36
  "use-sync-external-store": "^1.6.0",
37
- "@statelyai/flow": "0.6.0",
38
- "@statelyai/flow-dom": "3.2.1"
37
+ "@statelyai/flow": "0.8.2",
38
+ "@statelyai/flow-dom": "3.4.2"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "react": "^18.2.0 || ^19.0.0",