@statelyai/flow-react 0.10.1 → 0.11.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 +55 -6
- package/dist/esm/index.d.ts +63 -13
- package/dist/esm/index.js +206 -89
- package/package.json +3 -3
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 and animated layout sessions, `@statelyai/flow-dom` batches node rectangles, default group child-area and header 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 group headers keep projected styles on a separate element from imperative geometry, preserving projected transforms during drag.
|
|
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, default group child-area and header 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 group headers keep projected styles on a separate element from imperative geometry, preserving projected transforms during drag. Use `EdgePath` and `EdgeEndpoint` in custom edge renderers to receive the same imperative path and endpoint updates. The components declare Flow's geometry attributes for you. Existing custom paths whose `d` exactly matches a routed segment are also bound automatically, including duplicate hit strokes; unrelated decoration paths are left alone. 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
|
|
|
@@ -53,7 +53,10 @@ DOM measurement is one-directional. Nodes, ports, and edge labels report their r
|
|
|
53
53
|
| `PanePortal` / `ViewportPortal` | Render custom content into fixed screen-coordinate or transformed canvas-coordinate layers |
|
|
54
54
|
| `useFitViewExclusion()` | Register an overlay element whose measured edge band fit and center commands avoid |
|
|
55
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
|
|
56
|
+
| `Port` | Render a measured or graph-owned port with native div props and a forwarded ref |
|
|
57
|
+
| `EdgePath` | Render routed SVG segments with imperative geometry updates |
|
|
58
|
+
| `EdgeEndpoint` | Render a reconnection circle with an optional canvas-space endpoint offset |
|
|
59
|
+
| `RenderConnectionLineProps` | Connection state, routed geometry, validity, port centers, and directions passed to `renderConnectionLine` |
|
|
57
60
|
| `RenderEdgeLabelProps` | Edge, path, bounds, interaction state, and projected attrs passed to `renderEdgeLabel` |
|
|
58
61
|
| `Panel` | Positioned overlay panel for controls and custom UI |
|
|
59
62
|
| `ReactActiveGraphProjection` | React-typed active projection for `<StatelyFlow projections={...} />` or controller-owned projections |
|
|
@@ -67,11 +70,27 @@ DOM measurement is one-directional. Nodes, ports, and edge labels report their r
|
|
|
67
70
|
|
|
68
71
|
Color mode is store data, not engine behavior. `@statelyai/flow-dom` exports small helpers for resolving and applying `light`, `dark`, and `system`, while React exposes hooks/components that make those values easy to use with CSS variables.
|
|
69
72
|
|
|
70
|
-
`initialGraph` seeds a graph that the adapter-owned flow instance owns. For caller ownership, create a controller and pass `flow={flow}`. There is no controlled `graph` prop; observe `flow.on('commit', ...)`
|
|
73
|
+
`initialGraph` seeds a graph that the adapter-owned flow instance owns. For caller ownership, create a controller and pass `flow={flow}`. There is no controlled `graph` prop; observe `flow.on('commit', ...)` for persistence and synchronization. Adapter-owned instances also accept `onCommit`. Runtime `settings`, `inputModes`, and `projections` update the existing flow instance. Changes to `inputModes` affect new gestures without cancelling one in progress. Removing connect mode clears a pending click connection. If `projections` is omitted, `StatelyFlow` subscribes to the instance's `flow.projections` lifecycle.
|
|
74
|
+
|
|
75
|
+
<!-- events and listeners lifecycle from src/hooks.ts and src/StatelyFlow.tsx -->
|
|
76
|
+
|
|
77
|
+
`events` and selector `listeners` work with both adapter-owned and supplied instances. The adapter keeps callbacks current and removes its subscriptions when the view unmounts or the instance changes. Callbacks registered when creating a supplied instance remain owned by that instance. Each separately registered callback still runs.
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<StatelyFlow
|
|
81
|
+
flow={flow}
|
|
82
|
+
events={{
|
|
83
|
+
connectionEnded: (event) => console.log(event.status),
|
|
84
|
+
commit: () => console.log('graph changed'),
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
See [Keep application state in XState Store](EXTERNAL_STATE.md) for a live Flow graph with separate application state and saved documents.
|
|
71
90
|
|
|
72
91
|
<!-- StatelyFlow renderEdgeLabel and layers props from src/StatelyFlow.tsx plus portal exports from src/LayerPortals.tsx -->
|
|
73
92
|
|
|
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.
|
|
93
|
+
`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. Empty pane layers use `display: none` and become visible when content mounts. 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
94
|
|
|
76
95
|
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
96
|
|
|
@@ -81,6 +100,36 @@ The default edge renderer honors `edge.style.markerStart` and `markerEnd` values
|
|
|
81
100
|
|
|
82
101
|
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.
|
|
83
102
|
|
|
103
|
+
<!-- Port props from src/Port.tsx and custom edge geometry from src/EdgePrimitives.tsx -->
|
|
104
|
+
|
|
105
|
+
`Port` accepts native div props, including `aria-*`, `className`, `style`, and event handlers. Its forwarded ref points to the measured div. Flow composes focus handlers with yours and retains its identity attributes.
|
|
106
|
+
|
|
107
|
+
Render custom edge SVG content with `EdgePath` and `EdgeEndpoint`. `EdgePath` accepts native path props except `d` and paints every routed segment. `EdgeEndpoint` accepts native circle props except `cx`, `cy`, `r`, and `offset`. Its `radius` defaults to `10` canvas units. Its `offset` is a canvas-space `{ x, y }` displacement that follows drag and layout geometry without React commits. `end` selects the source or target endpoint, and the ref points to the circle.
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { EdgeEndpoint, EdgePath, StatelyFlow } from '@statelyai/flow-react';
|
|
111
|
+
|
|
112
|
+
<StatelyFlow
|
|
113
|
+
flow={flow}
|
|
114
|
+
renderEdge={({ edge, pathData }) => (
|
|
115
|
+
<>
|
|
116
|
+
<EdgePath pathData={pathData} stroke="currentColor" />
|
|
117
|
+
<EdgeEndpoint
|
|
118
|
+
edgeId={edge.id}
|
|
119
|
+
pathData={pathData}
|
|
120
|
+
end="target"
|
|
121
|
+
radius={10}
|
|
122
|
+
offset={{ x: -6, y: 0 }}
|
|
123
|
+
/>
|
|
124
|
+
</>
|
|
125
|
+
)}
|
|
126
|
+
/>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
<!-- connection preview lifecycle and render props from src/StatelyFlow.tsx -->
|
|
130
|
+
|
|
131
|
+
`renderConnectionLine` renders SVG content in a separate host above nodes. The host exists only during an active preview and is inert to pointer and keyboard input. The callback receives `connection`, `pathData`, `isValid`, `sourcePoint`, `targetPoint`, `sourceDirection`, and `targetDirection`. Points are in canvas coordinates. The source point is the gesture source port center, with the starting point as fallback. The target point is the hovered port center, with the pointer point as fallback. Directions use port sides when present, then routed directions. The gesture origin remains the source even when the resulting edge direction is reversed.
|
|
132
|
+
|
|
84
133
|
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.
|
|
85
134
|
|
|
86
135
|
For controlled reconnect gestures, `reconnectMode({ commit: false, preview: false })` keeps Flow's input lifecycle while suppressing the adapter's projected preview edge, so an external domain store can render its own temporary geometry without duplicate paths.
|
|
@@ -89,11 +138,11 @@ For controlled reconnect gestures, `reconnectMode({ commit: false, preview: fals
|
|
|
89
138
|
|
|
90
139
|
<!-- namespaced selector roles and entity state attributes from src/StatelyFlow.tsx -->
|
|
91
140
|
|
|
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.
|
|
141
|
+
`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. When Flow clears entity focus, the adapter blurs the directly focused node, edge, or inspection port. Child controls and unrelated focused elements retain browser focus. 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.
|
|
93
142
|
|
|
94
143
|
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.
|
|
95
144
|
|
|
96
|
-
Keyboard handling is scoped to the `StatelyFlow` root by default so multiple flows on one page do not all receive Delete/undo/arrow keys. Pass `keyboardTarget="window"` to restore global handling. Pass `settings={{ keymap }}` to rebind built-in shortcuts. Double-click zoom is enabled in the default input modes; Shift double-click zooms out.
|
|
145
|
+
Keyboard handling is scoped to the `StatelyFlow` root by default so multiple flows on one page do not all receive Delete/undo/arrow keys. Pass `keyboardTarget="window"` to restore global handling. Pass `settings={{ keymap }}` to rebind built-in shortcuts. Double-click zoom is enabled in the default input modes; Shift double-click zooms out. Compose `keyboardMode` and `keyboardNavMode` to configure activation, selection, and Escape behavior. See [core input policies](../flow/README.md#input-policies).
|
|
97
146
|
|
|
98
147
|
Alignment and equal-spacing guides stay a constant screen size across zoom and expose `--flow-snap-line-color` and `--flow-spacing-guide-color` for theming. Equal-spacing guides include their rounded canvas distance.
|
|
99
148
|
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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, FitViewExclusionSide, FlowColorMode, FlowCommitEvent, FlowConnection, FlowController, FlowEdge, FlowGraph, FlowInputMode, FlowKeymap, FlowNode, FlowProfile, FlowStoreContext, GraphEdgeAttrs, GraphNodeAttrs, LayoutRequestedEvent, Point, Rect, ResolvedFlowAppearance, Viewport, VisualPort } from "@statelyai/flow";
|
|
3
|
+
import { ActiveGraphProjection, AnchorSpec, ConnectionEndedEvent, ConnectionState, CreateEdgeInput, CreateFlowOptions, Direction, EdgePathData, FitViewExclusionSide, FlowColorMode, FlowCommitEvent, FlowConnection, FlowController, FlowEdge, FlowGraph, FlowInputMode, FlowKeymap, FlowNode, FlowProfile, FlowStoreContext, GraphEdgeAttrs, GraphNodeAttrs, LayoutRequestedEvent, Point, Rect, ResolvedFlowAppearance, Viewport, VisualPort } from "@statelyai/flow";
|
|
4
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
|
|
@@ -78,7 +78,7 @@ type UseFlowOptions<TNodeData = any, TEdgeData = any, TLabelData = any> = UseCre
|
|
|
78
78
|
* Create one Flow instance for a component lifetime.
|
|
79
79
|
*
|
|
80
80
|
* Creation settings are initial-only; use `flow.configure()` for runtime
|
|
81
|
-
* changes. Event
|
|
81
|
+
* changes. Event callbacks stay current; selector listeners rebind on change.
|
|
82
82
|
*/
|
|
83
83
|
declare function useFlow<TNodeData = any, TEdgeData = any, TLabelData = any>(options: UseFlowOptions<TNodeData, TEdgeData, TLabelData>): FlowInstance<TNodeData, TEdgeData, TLabelData>;
|
|
84
84
|
/** @deprecated Use `useFlow(options)`. */
|
|
@@ -140,9 +140,8 @@ type UseFlowInputOptions = {
|
|
|
140
140
|
* Input modes, or a resolver from store context. Defaults to the store
|
|
141
141
|
* profile's input modes (when a profile is given) and otherwise to the
|
|
142
142
|
* core default set, which follows the store's `context.mode`
|
|
143
|
-
* (interactive / readonly).
|
|
144
|
-
*
|
|
145
|
-
* context-aware resolver.
|
|
143
|
+
* (interactive / readonly). Changes apply to new gestures without cancelling
|
|
144
|
+
* a gesture already in progress.
|
|
146
145
|
*/
|
|
147
146
|
modes?: FlowInputMode[] | ((context: FlowStoreContext) => FlowInputMode[]);
|
|
148
147
|
/** Store profile whose per-mode `input.modes` drive the runtime. */
|
|
@@ -201,6 +200,12 @@ type RenderConnectionLineProps = {
|
|
|
201
200
|
connection: ConnectionState;
|
|
202
201
|
pathData: EdgePathData;
|
|
203
202
|
isValid: boolean;
|
|
203
|
+
/** Current gesture source port center, or its starting canvas point. */
|
|
204
|
+
sourcePoint: Point;
|
|
205
|
+
/** Hovered target port center, or the current pointer canvas point. */
|
|
206
|
+
targetPoint: Point;
|
|
207
|
+
sourceDirection: Direction;
|
|
208
|
+
targetDirection: Direction;
|
|
204
209
|
};
|
|
205
210
|
type StatelyFlowStyle = CSSProperties | (CSSProperties & {
|
|
206
211
|
[customProperty: `--${string}`]: string | number | undefined;
|
|
@@ -235,7 +240,8 @@ type StatelyFlowBaseProps<TNodeData = unknown, TEdgeData = unknown> = {
|
|
|
235
240
|
*/
|
|
236
241
|
onlyRenderVisible?: boolean;
|
|
237
242
|
/**
|
|
238
|
-
* Override the input modes driving gestures
|
|
243
|
+
* Override the input modes driving gestures. Updates apply to new gestures
|
|
244
|
+
* without cancelling the current one. Defaults to
|
|
239
245
|
* the store profile's input modes when `settings.store.profile` is set,
|
|
240
246
|
* otherwise to the core defaults. Example — custom connection validity:
|
|
241
247
|
*
|
|
@@ -405,7 +411,7 @@ type EntityAnchorProps = EntityAnchorBaseProps & ({
|
|
|
405
411
|
declare function EntityAnchor(props: EntityAnchorProps): import("react").JSX.Element | null;
|
|
406
412
|
//#endregion
|
|
407
413
|
//#region src/Port.d.ts
|
|
408
|
-
type PortProps = {
|
|
414
|
+
type PortProps = ComponentPropsWithoutRef<"div"> & {
|
|
409
415
|
name: string;
|
|
410
416
|
/**
|
|
411
417
|
* `inspect` makes a graph-owned port focusable/selectable without starting
|
|
@@ -427,9 +433,6 @@ type PortProps = {
|
|
|
427
433
|
* feedback loop and runs away.
|
|
428
434
|
*/
|
|
429
435
|
measured?: boolean;
|
|
430
|
-
className?: string;
|
|
431
|
-
style?: CSSProperties;
|
|
432
|
-
children?: ReactNode;
|
|
433
436
|
};
|
|
434
437
|
/**
|
|
435
438
|
* A connection port. Purely declarative: it renders the
|
|
@@ -441,7 +444,29 @@ type PortProps = {
|
|
|
441
444
|
* observe results via `flow.on("connectionEnded", …)` or the store's
|
|
442
445
|
* `connection` state.
|
|
443
446
|
*/
|
|
444
|
-
declare
|
|
447
|
+
declare const Port: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
|
|
448
|
+
name: string;
|
|
449
|
+
/**
|
|
450
|
+
* `inspect` makes a graph-owned port focusable/selectable without starting
|
|
451
|
+
* a connection. Defaults to `connect`.
|
|
452
|
+
*/
|
|
453
|
+
interaction?: "connect" | "inspect";
|
|
454
|
+
/**
|
|
455
|
+
* Whether the DOM element reports its bounds back to the graph (default
|
|
456
|
+
* true). Ports are unidirectional, one way or the other:
|
|
457
|
+
*
|
|
458
|
+
* - DOM-owned (default): position the element with your own CSS (offsets,
|
|
459
|
+
* percentages, flex…) — NEVER from `port.x`/`port.y` — and the graph's
|
|
460
|
+
* port geometry follows the DOM.
|
|
461
|
+
* - Graph-owned (`measured={false}`): position the element FROM
|
|
462
|
+
* `port.x`/`port.y` (layout results, "virtual" ports) and nothing is
|
|
463
|
+
* written back.
|
|
464
|
+
*
|
|
465
|
+
* Mixing the two (positioning from `port.x` while measuring) creates a
|
|
466
|
+
* feedback loop and runs away.
|
|
467
|
+
*/
|
|
468
|
+
measured?: boolean;
|
|
469
|
+
} & import("react").RefAttributes<HTMLDivElement>>;
|
|
445
470
|
//#endregion
|
|
446
471
|
//#region src/DragHandle.d.ts
|
|
447
472
|
type DragHandleProps = HTMLAttributes<HTMLDivElement> & {
|
|
@@ -488,7 +513,7 @@ declare function DragHandle({ children, style, ...rest }: DragHandleProps): impo
|
|
|
488
513
|
* - `data-flow-selection-bounds` — the established multi-selection frame.
|
|
489
514
|
*/
|
|
490
515
|
declare const FLOW_BASE_STYLES_ID = "flow-base-styles";
|
|
491
|
-
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";
|
|
516
|
+
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-layer=\"pane\"]:empty {\n display: none;\n}\n\n[data-flow-connection-preview],\n[data-flow-connection-preview] * {\n pointer-events: none !important;\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";
|
|
492
517
|
/** Inject the base stylesheet once per document (deduped by element id). */
|
|
493
518
|
declare function useFlowBaseStyles(): void;
|
|
494
519
|
//#endregion
|
|
@@ -505,4 +530,29 @@ declare function useFlowColorMode(): {
|
|
|
505
530
|
};
|
|
506
531
|
declare function useApplyFlowColorMode(element: HTMLElement | null): void;
|
|
507
532
|
//#endregion
|
|
508
|
-
|
|
533
|
+
//#region src/EdgePrimitives.d.ts
|
|
534
|
+
type EdgePathProps = Omit<ComponentPropsWithoutRef<"path">, "d"> & {
|
|
535
|
+
pathData: EdgePathData;
|
|
536
|
+
};
|
|
537
|
+
/** Paint routed segments while Flow updates their geometry during gestures. */
|
|
538
|
+
declare function EdgePath({ pathData, ...props }: EdgePathProps): import("react").JSX.Element;
|
|
539
|
+
type EdgeEndpointProps = Omit<ComponentPropsWithoutRef<"circle">, "cx" | "cy" | "r" | "offset"> & {
|
|
540
|
+
edgeId: string;
|
|
541
|
+
pathData: EdgePathData;
|
|
542
|
+
end: "source" | "target";
|
|
543
|
+
/** Reconnection hit radius in canvas units. Defaults to 10. */
|
|
544
|
+
radius?: number;
|
|
545
|
+
/** Reconnection hit offset from the route endpoint in canvas units. */
|
|
546
|
+
offset?: Point;
|
|
547
|
+
};
|
|
548
|
+
declare const EdgeEndpoint: import("react").ForwardRefExoticComponent<Omit<Omit<import("react").SVGProps<SVGCircleElement>, "ref">, "cx" | "cy" | "offset" | "r"> & {
|
|
549
|
+
edgeId: string;
|
|
550
|
+
pathData: EdgePathData;
|
|
551
|
+
end: "source" | "target";
|
|
552
|
+
/** Reconnection hit radius in canvas units. Defaults to 10. */
|
|
553
|
+
radius?: number;
|
|
554
|
+
/** Reconnection hit offset from the route endpoint in canvas units. */
|
|
555
|
+
offset?: Point;
|
|
556
|
+
} & import("react").RefAttributes<SVGCircleElement>>;
|
|
557
|
+
//#endregion
|
|
558
|
+
export { Background, type BackgroundProps, type BackgroundVariant, Controls, type ControlsProps, type CreateFlowInstanceOptions, CrossBackground, DotsBackground, DragHandle, type DragHandleProps, EdgeEndpoint, type EdgeEndpointProps, EdgePath, type EdgePathProps, 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 ReactAppearanceRenderers, 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,7 +1,7 @@
|
|
|
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, observeAppearanceBounds, renderableIdsEqual, resolveFlowColorMode, viewportStyle } from "@statelyai/flow-dom";
|
|
3
|
-
import { CONNECTION_PREVIEW_EDGE_ID, appearanceMeasurementId, areNodesMeasured, createConnectionPreviewGraph, createEntityChangeNotifier, createFlow, createFlowInputRuntime, getAnchorTransform, getAutoPanDelta, getMinimapProjection, getMinimapViewportCenter, getProjectedEdgeAttrs, getProjectedNodeAttrs, getToolbarPlacement, getVisibleBounds, markerToSVG, pathToSVG, profileModesForContext, profilePoliciesForContext, viewportCenteredOn } from "@statelyai/flow";
|
|
4
|
-
import { Fragment, createContext, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { CONNECTION_PREVIEW_EDGE_ID, appearanceMeasurementId, areNodesMeasured, createConnectionPreviewGraph, createEntityChangeNotifier, createFlow, createFlowInputRuntime, defaultModesForContext, getAnchorTransform, getAutoPanDelta, getMinimapProjection, getMinimapViewportCenter, getProjectedEdgeAttrs, getProjectedNodeAttrs, getToolbarPlacement, getVisibleBounds, markerToSVG, pathToSVG, profileModesForContext, profilePoliciesForContext, viewportCenteredOn } from "@statelyai/flow";
|
|
4
|
+
import { Fragment, createContext, forwardRef, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
5
5
|
import { Fragment as Fragment$1, 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";
|
|
@@ -79,7 +79,7 @@ function createFlowInstance(options) {
|
|
|
79
79
|
* Create one Flow instance for a component lifetime.
|
|
80
80
|
*
|
|
81
81
|
* Creation settings are initial-only; use `flow.configure()` for runtime
|
|
82
|
-
* changes. Event
|
|
82
|
+
* changes. Event callbacks stay current; selector listeners rebind on change.
|
|
83
83
|
*/
|
|
84
84
|
function useFlow(options) {
|
|
85
85
|
return useCreateFlowInstance(options);
|
|
@@ -87,56 +87,66 @@ function useFlow(options) {
|
|
|
87
87
|
/** @deprecated Use `useFlow(options)`. */
|
|
88
88
|
function useCreateFlowInstance(options) {
|
|
89
89
|
const instanceRef = useRef(null);
|
|
90
|
-
const listenersRef = useRef(options.listeners);
|
|
91
|
-
const eventsRef = useRef(options.events);
|
|
92
90
|
const lifecycleRef = useRef(0);
|
|
93
|
-
listenersRef.current = options.listeners;
|
|
94
|
-
eventsRef.current = options.events;
|
|
95
91
|
if (!instanceRef.current) instanceRef.current = createFlowInstance({
|
|
96
92
|
initialGraph: options.initialGraph,
|
|
97
93
|
settings: options.settings
|
|
98
94
|
});
|
|
99
95
|
const flow = instanceRef.current;
|
|
96
|
+
useFlowSubscriptions(flow, options);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
const lifecycle = lifecycleRef;
|
|
99
|
+
const generation = ++lifecycle.current;
|
|
100
|
+
return () => {
|
|
101
|
+
queueMicrotask(() => {
|
|
102
|
+
if (lifecycle.current !== generation) return;
|
|
103
|
+
lifecycle.current++;
|
|
104
|
+
flow.destroy();
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
}, [flow]);
|
|
108
|
+
return flow;
|
|
109
|
+
}
|
|
110
|
+
function useFlowSubscriptions(flow, options) {
|
|
111
|
+
const callbacks = useRef(options);
|
|
112
|
+
useLayoutEffect(() => {
|
|
113
|
+
callbacks.current = options;
|
|
114
|
+
});
|
|
115
|
+
const hasConnectionEnded = Boolean(options.events?.connectionEnded);
|
|
116
|
+
const hasLayoutRequested = Boolean(options.events?.layoutRequested);
|
|
117
|
+
const hasCommit = Boolean(options.events?.commit || options.onCommit);
|
|
100
118
|
useEffect(() => {
|
|
101
119
|
const cleanup = [];
|
|
102
|
-
if (
|
|
103
|
-
const subscription = flow.on("connectionEnded",
|
|
104
|
-
cleanup.push(() => subscription.unsubscribe());
|
|
105
|
-
}
|
|
106
|
-
if (eventsRef.current?.layoutRequested) {
|
|
107
|
-
const subscription = flow.on("layoutRequested", eventsRef.current.layoutRequested);
|
|
120
|
+
if (hasConnectionEnded) {
|
|
121
|
+
const subscription = flow.on("connectionEnded", (event) => callbacks.current.events?.connectionEnded?.(event));
|
|
108
122
|
cleanup.push(() => subscription.unsubscribe());
|
|
109
123
|
}
|
|
110
|
-
if (
|
|
111
|
-
const subscription = flow.on("
|
|
124
|
+
if (hasLayoutRequested) {
|
|
125
|
+
const subscription = flow.on("layoutRequested", (event) => callbacks.current.events?.layoutRequested?.(event));
|
|
112
126
|
cleanup.push(() => subscription.unsubscribe());
|
|
113
127
|
}
|
|
114
|
-
if (
|
|
115
|
-
const subscription = flow.on("commit",
|
|
128
|
+
if (hasCommit) {
|
|
129
|
+
const subscription = flow.on("commit", (event) => {
|
|
130
|
+
callbacks.current.events?.commit?.(event);
|
|
131
|
+
callbacks.current.onCommit?.(event);
|
|
132
|
+
});
|
|
116
133
|
cleanup.push(() => subscription.unsubscribe());
|
|
117
134
|
}
|
|
118
|
-
for (const listener of listenersRef.current ?? []) cleanup.push(flow.listen(listener.selector, listener.onChange, listener));
|
|
119
135
|
return () => {
|
|
120
|
-
for (const
|
|
136
|
+
for (const unsubscribe of cleanup) unsubscribe();
|
|
121
137
|
};
|
|
122
138
|
}, [
|
|
123
139
|
flow,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
140
|
+
hasConnectionEnded,
|
|
141
|
+
hasLayoutRequested,
|
|
142
|
+
hasCommit
|
|
127
143
|
]);
|
|
128
144
|
useEffect(() => {
|
|
129
|
-
const
|
|
130
|
-
const generation = ++lifecycle.current;
|
|
145
|
+
const cleanup = options.listeners?.map((listener) => flow.listen(listener.selector, listener.onChange, listener));
|
|
131
146
|
return () => {
|
|
132
|
-
|
|
133
|
-
if (lifecycle.current !== generation) return;
|
|
134
|
-
lifecycle.current++;
|
|
135
|
-
flow.destroy();
|
|
136
|
-
});
|
|
147
|
+
for (const unsubscribe of cleanup ?? []) unsubscribe();
|
|
137
148
|
};
|
|
138
|
-
}, [flow]);
|
|
139
|
-
return flow;
|
|
149
|
+
}, [flow, options.listeners]);
|
|
140
150
|
}
|
|
141
151
|
/** Subscribe to a derived slice of Flow context with optional equality. */
|
|
142
152
|
function useFlowSelector(selector, options = {}) {
|
|
@@ -303,6 +313,9 @@ function getDerivedFields(context, id, props) {
|
|
|
303
313
|
*/
|
|
304
314
|
function useFlowInput(flow, containerRef, options = {}) {
|
|
305
315
|
const { modes, profile, keyboardTarget, keymap } = options;
|
|
316
|
+
const modesRef = useRef(modes);
|
|
317
|
+
const runtimeRef = useRef(null);
|
|
318
|
+
modesRef.current = modes;
|
|
306
319
|
const effectiveKeymap = keymap ?? flow.inputSettings?.keymap;
|
|
307
320
|
const keymapKey = effectiveKeymap ? JSON.stringify(effectiveKeymap) : void 0;
|
|
308
321
|
const keymapRef = useRef(effectiveKeymap);
|
|
@@ -314,25 +327,36 @@ function useFlowInput(flow, containerRef, options = {}) {
|
|
|
314
327
|
const runtime = createFlowInputRuntime({
|
|
315
328
|
store: flow.internals.store,
|
|
316
329
|
engine: flow.internals.engine,
|
|
317
|
-
modes:
|
|
330
|
+
modes: (context) => {
|
|
331
|
+
const activeModes = modesRef.current;
|
|
332
|
+
if (activeModes) return typeof activeModes === "function" ? activeModes(context) : activeModes;
|
|
333
|
+
return profile ? profileModesForContext(profile)(context) : defaultModesForContext(context, activeKeymap);
|
|
334
|
+
},
|
|
318
335
|
policies: profile ? profilePoliciesForContext(profile) : void 0,
|
|
319
336
|
...activeKeymap ? { keymap: activeKeymap } : {}
|
|
320
337
|
});
|
|
321
|
-
|
|
338
|
+
runtimeRef.current = runtime;
|
|
339
|
+
const detach = attachInput({
|
|
322
340
|
container,
|
|
323
341
|
store: flow.internals.store,
|
|
324
342
|
runtime,
|
|
325
343
|
keyboardTarget,
|
|
326
344
|
autoPan: getAutoPanDelta
|
|
327
345
|
});
|
|
346
|
+
return () => {
|
|
347
|
+
runtimeRef.current = null;
|
|
348
|
+
detach();
|
|
349
|
+
};
|
|
328
350
|
}, [
|
|
329
351
|
flow,
|
|
330
352
|
containerRef,
|
|
331
|
-
modes,
|
|
332
353
|
profile,
|
|
333
354
|
keyboardTarget,
|
|
334
355
|
keymapKey
|
|
335
356
|
]);
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
runtimeRef.current?.reconcileModes();
|
|
359
|
+
}, [modes]);
|
|
336
360
|
}
|
|
337
361
|
//#endregion
|
|
338
362
|
//#region src/MeasurementContext.tsx
|
|
@@ -431,6 +455,15 @@ const FLOW_BASE_CSS = `
|
|
|
431
455
|
outline: var(--flow-focus-outline, none);
|
|
432
456
|
}
|
|
433
457
|
|
|
458
|
+
[data-flow-layer="pane"]:empty {
|
|
459
|
+
display: none;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
[data-flow-connection-preview],
|
|
463
|
+
[data-flow-connection-preview] * {
|
|
464
|
+
pointer-events: none !important;
|
|
465
|
+
}
|
|
466
|
+
|
|
434
467
|
[data-flow-interactive-when-selected] {
|
|
435
468
|
pointer-events: none;
|
|
436
469
|
}
|
|
@@ -502,6 +535,14 @@ function clearEntityFocusOnBlur(flow, entityId, event) {
|
|
|
502
535
|
if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget)) return;
|
|
503
536
|
if (flow.getSnapshot().focusedEntityId === entityId) flow.internals.store.trigger.clearFocus();
|
|
504
537
|
}
|
|
538
|
+
function blurClearedEntityFocus(root, previousId, nextId) {
|
|
539
|
+
if (!previousId || nextId !== null) return;
|
|
540
|
+
const active = root.ownerDocument.activeElement;
|
|
541
|
+
const view = root.ownerDocument.defaultView;
|
|
542
|
+
if (!view || !(active instanceof view.HTMLElement || active instanceof view.SVGElement) || !root.contains(active)) return;
|
|
543
|
+
if (!active.matches(".stately-flow__node, .stately-flow__edge, [data-flow-port-interaction=\"inspect\"]")) return;
|
|
544
|
+
if ((active.getAttribute("data-flow-port-id") ?? active.getAttribute("data-entity-id")) === previousId) active.blur();
|
|
545
|
+
}
|
|
505
546
|
//#endregion
|
|
506
547
|
//#region src/LayerPortals.tsx
|
|
507
548
|
const FlowLayerTargetsContext = createContext(null);
|
|
@@ -625,10 +666,7 @@ function StatelyFlow(props) {
|
|
|
625
666
|
function OwnedStatelyFlow(props) {
|
|
626
667
|
const flow = useFlow({
|
|
627
668
|
initialGraph: props.initialGraph,
|
|
628
|
-
settings: props.settings
|
|
629
|
-
listeners: props.listeners,
|
|
630
|
-
events: props.events,
|
|
631
|
-
onCommit: props.onCommit
|
|
669
|
+
settings: props.settings
|
|
632
670
|
});
|
|
633
671
|
return /* @__PURE__ */ jsx(StatelyFlowView, {
|
|
634
672
|
...props,
|
|
@@ -636,6 +674,7 @@ function OwnedStatelyFlow(props) {
|
|
|
636
674
|
});
|
|
637
675
|
}
|
|
638
676
|
function StatelyFlowView(props) {
|
|
677
|
+
useFlowSubscriptions(props.flow, props);
|
|
639
678
|
const containerRef = useRef(null);
|
|
640
679
|
const [rootLayer, setRootLayer] = useState(null);
|
|
641
680
|
const setContainerRef = useCallback((element) => {
|
|
@@ -661,6 +700,9 @@ function StatelyFlowView(props) {
|
|
|
661
700
|
measurementObserver.disconnect();
|
|
662
701
|
geometryObserver.disconnect();
|
|
663
702
|
}, [geometryObserver, measurementObserver]);
|
|
703
|
+
useLayoutEffect(() => props.flow.subscribe((context) => context.focusedEntityId, (nextId, previousId) => {
|
|
704
|
+
if (containerRef.current) blurClearedEntityFocus(containerRef.current, previousId, nextId);
|
|
705
|
+
}), [props.flow]);
|
|
664
706
|
useViewportTransform(viewportRef, props.flow);
|
|
665
707
|
useFlowBaseStyles();
|
|
666
708
|
const projections = useActiveProjections(props.flow, props.projections);
|
|
@@ -733,7 +775,6 @@ function StatelyFlowView(props) {
|
|
|
733
775
|
/* @__PURE__ */ jsx(EdgeLayer, {
|
|
734
776
|
geometryObserver,
|
|
735
777
|
renderEdge: props.renderEdge,
|
|
736
|
-
renderConnectionLine: props.renderConnectionLine,
|
|
737
778
|
projections,
|
|
738
779
|
onlyRenderVisible: props.onlyRenderVisible
|
|
739
780
|
}),
|
|
@@ -744,6 +785,11 @@ function StatelyFlowView(props) {
|
|
|
744
785
|
projections,
|
|
745
786
|
onlyRenderVisible: props.onlyRenderVisible
|
|
746
787
|
}),
|
|
788
|
+
/* @__PURE__ */ jsx(ConnectionPreviewEdge, {
|
|
789
|
+
flow: props.flow,
|
|
790
|
+
renderEdge: props.renderEdge,
|
|
791
|
+
renderConnectionLine: props.renderConnectionLine
|
|
792
|
+
}),
|
|
747
793
|
/* @__PURE__ */ jsx(SnapLinesLayer, {}),
|
|
748
794
|
/* @__PURE__ */ jsx(EdgeLabelLayer, {
|
|
749
795
|
geometryObserver,
|
|
@@ -995,7 +1041,7 @@ function EdgeLayer(props) {
|
|
|
995
1041
|
overflow: "visible",
|
|
996
1042
|
pointerEvents: "none"
|
|
997
1043
|
},
|
|
998
|
-
children: [/* @__PURE__ */ jsx(DefaultMarkerDefinitions, { prefix: markerPrefix }), /* @__PURE__ */
|
|
1044
|
+
children: [/* @__PURE__ */ jsx(DefaultMarkerDefinitions, { prefix: markerPrefix }), /* @__PURE__ */ jsx("g", { children: edgeIds.map((id) => {
|
|
999
1045
|
if (renderableIds && !renderableIds.edgeIds.has(id)) return null;
|
|
1000
1046
|
return /* @__PURE__ */ jsx(EdgeView, {
|
|
1001
1047
|
flow: instance,
|
|
@@ -1005,12 +1051,7 @@ function EdgeLayer(props) {
|
|
|
1005
1051
|
renderEdge,
|
|
1006
1052
|
markerPrefix
|
|
1007
1053
|
}, id);
|
|
1008
|
-
})
|
|
1009
|
-
flow: instance,
|
|
1010
|
-
renderEdge,
|
|
1011
|
-
renderConnectionLine: props.renderConnectionLine,
|
|
1012
|
-
markerPrefix
|
|
1013
|
-
})] })]
|
|
1054
|
+
}) })]
|
|
1014
1055
|
});
|
|
1015
1056
|
}
|
|
1016
1057
|
const EdgeView = memo(function EdgeView(props) {
|
|
@@ -1106,34 +1147,59 @@ const EdgeView = memo(function EdgeView(props) {
|
|
|
1106
1147
|
});
|
|
1107
1148
|
/** The in-progress connection gesture's preview edge (renders only mid-gesture). */
|
|
1108
1149
|
function ConnectionPreviewEdge(props) {
|
|
1109
|
-
const
|
|
1150
|
+
const markerPrefix = `stately-flow-preview-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
1151
|
+
const connection = useFlowSelector((context) => context.connection?.active ? context : null)?.connection;
|
|
1110
1152
|
if (!connection?.active || connection.preview === false) return null;
|
|
1111
1153
|
const previewGraph = createConnectionPreviewGraph(props.flow.getSnapshot().graph, connection);
|
|
1112
1154
|
const pathData = props.flow.getAllEdgePaths({ graph: previewGraph }).get(CONNECTION_PREVIEW_EDGE_ID);
|
|
1113
1155
|
const edge = previewGraph.edges.find((candidate) => candidate.id === CONNECTION_PREVIEW_EDGE_ID);
|
|
1114
1156
|
if (!edge || !pathData) return null;
|
|
1115
|
-
return /* @__PURE__ */ jsx("
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1157
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1158
|
+
ref: (element) => {
|
|
1159
|
+
element?.setAttribute("inert", "");
|
|
1160
|
+
},
|
|
1161
|
+
"aria-hidden": "true",
|
|
1162
|
+
className: "stately-flow__connection-preview",
|
|
1163
|
+
"data-flow-connection-preview": "",
|
|
1164
|
+
"data-flow-canvas-layer": "",
|
|
1165
|
+
style: {
|
|
1166
|
+
position: "absolute",
|
|
1167
|
+
inset: 0,
|
|
1168
|
+
zIndex: "var(--flow-layer-interaction, 30)",
|
|
1169
|
+
pointerEvents: "none"
|
|
1170
|
+
},
|
|
1171
|
+
children: /* @__PURE__ */ jsxs("svg", {
|
|
1172
|
+
style: {
|
|
1173
|
+
width: "100%",
|
|
1174
|
+
height: "100%",
|
|
1175
|
+
overflow: "visible",
|
|
1176
|
+
pointerEvents: "none"
|
|
1177
|
+
},
|
|
1178
|
+
children: [/* @__PURE__ */ jsx(DefaultMarkerDefinitions, { prefix: markerPrefix }), props.renderConnectionLine ? props.renderConnectionLine({
|
|
1179
|
+
connection,
|
|
1180
|
+
pathData,
|
|
1181
|
+
isValid: connection.isValid,
|
|
1182
|
+
sourcePoint: connection.sourcePort ? props.flow.internals.engine.getPortPosition(connection.sourceId, connection.sourcePort) ?? connection.startPosition : connection.startPosition,
|
|
1183
|
+
targetPoint: connection.targetId && connection.targetPort ? props.flow.internals.engine.getPortPosition(connection.targetId, connection.targetPort) ?? connection.pointerPosition : connection.pointerPosition,
|
|
1184
|
+
sourceDirection: props.flow.getNode(connection.sourceId)?.ports?.find((port) => port.name === connection.sourcePort)?.side ?? pathData.segments[0]?.sourceDirection ?? "right",
|
|
1185
|
+
targetDirection: (connection.targetId ? props.flow.getNode(connection.targetId)?.ports?.find((port) => port.name === connection.targetPort)?.side : void 0) ?? pathData.segments.at(-1)?.targetDirection ?? "left"
|
|
1186
|
+
}) : props.renderEdge ? props.renderEdge({
|
|
1187
|
+
flow: props.flow,
|
|
1188
|
+
edge,
|
|
1189
|
+
pathData,
|
|
1190
|
+
attrs: {},
|
|
1191
|
+
selected: false,
|
|
1192
|
+
hovered: false,
|
|
1193
|
+
highlights: []
|
|
1194
|
+
}) : /* @__PURE__ */ jsx(DefaultEdge, {
|
|
1195
|
+
flow: props.flow,
|
|
1196
|
+
edge,
|
|
1197
|
+
pathData,
|
|
1198
|
+
attrs: {},
|
|
1199
|
+
selected: false,
|
|
1200
|
+
markerPrefix,
|
|
1201
|
+
preview: true
|
|
1202
|
+
})]
|
|
1137
1203
|
})
|
|
1138
1204
|
});
|
|
1139
1205
|
}
|
|
@@ -2329,17 +2395,30 @@ function classNames(...values) {
|
|
|
2329
2395
|
* observe results via `flow.on("connectionEnded", …)` or the store's
|
|
2330
2396
|
* `connection` state.
|
|
2331
2397
|
*/
|
|
2332
|
-
function Port(props) {
|
|
2398
|
+
const Port = forwardRef(function Port(props, forwardedRef) {
|
|
2399
|
+
const { name, interaction: interactionProp, measured: measuredProp, onFocus, onBlur, ...nativeProps } = props;
|
|
2333
2400
|
const flow = useFlowInstance();
|
|
2334
2401
|
const { node } = useNodeRenderContext();
|
|
2335
2402
|
const ref = useRef(null);
|
|
2403
|
+
const forwardedCleanup = useRef(void 0);
|
|
2404
|
+
const setRef = useCallback((element) => {
|
|
2405
|
+
const cleanup = forwardedCleanup.current;
|
|
2406
|
+
forwardedCleanup.current = void 0;
|
|
2407
|
+
cleanup?.();
|
|
2408
|
+
ref.current = element;
|
|
2409
|
+
if (typeof forwardedRef === "function") {
|
|
2410
|
+
if (!element && cleanup) return;
|
|
2411
|
+
const dispose = forwardedRef(element);
|
|
2412
|
+
if (typeof dispose === "function") forwardedCleanup.current = dispose;
|
|
2413
|
+
} else if (forwardedRef) forwardedRef.current = element;
|
|
2414
|
+
}, [forwardedRef]);
|
|
2336
2415
|
const observerRef = useRef(null);
|
|
2337
2416
|
const observer = useMeasurementObserver();
|
|
2338
|
-
const measured =
|
|
2339
|
-
const interaction =
|
|
2340
|
-
const permitted = interaction !== "connect" || !flow.internals.engine.resolveAppearance(node).registered || flow.internals.engine.isPortAllowed(node,
|
|
2341
|
-
const port = node.ports?.find((candidate) => candidate.name ===
|
|
2342
|
-
const portId = `${node.id}:${
|
|
2417
|
+
const measured = measuredProp ?? true;
|
|
2418
|
+
const interaction = interactionProp ?? "connect";
|
|
2419
|
+
const permitted = interaction !== "connect" || !flow.internals.engine.resolveAppearance(node).registered || flow.internals.engine.isPortAllowed(node, name);
|
|
2420
|
+
const port = node.ports?.find((candidate) => candidate.name === name);
|
|
2421
|
+
const portId = `${node.id}:${name}`;
|
|
2343
2422
|
const focusable = flow.internals.engine.resolveEntityConfig(node).focusable;
|
|
2344
2423
|
const selected = useFlowSelector((context) => context.selection.has(portId), { flow });
|
|
2345
2424
|
useLayoutEffect(() => {
|
|
@@ -2348,7 +2427,7 @@ function Port(props) {
|
|
|
2348
2427
|
observerRef.current = observer;
|
|
2349
2428
|
observer.observe(element, {
|
|
2350
2429
|
nodeId: node.id,
|
|
2351
|
-
portName:
|
|
2430
|
+
portName: name
|
|
2352
2431
|
});
|
|
2353
2432
|
return () => {
|
|
2354
2433
|
observerRef.current = null;
|
|
@@ -2357,7 +2436,7 @@ function Port(props) {
|
|
|
2357
2436
|
}, [
|
|
2358
2437
|
observer,
|
|
2359
2438
|
node.id,
|
|
2360
|
-
|
|
2439
|
+
name,
|
|
2361
2440
|
measured,
|
|
2362
2441
|
permitted
|
|
2363
2442
|
]);
|
|
@@ -2370,32 +2449,38 @@ function Port(props) {
|
|
|
2370
2449
|
const candidate = useFlowSelector((context) => {
|
|
2371
2450
|
const connection = context.connection;
|
|
2372
2451
|
if (!connection?.active) return null;
|
|
2373
|
-
if (connection.targetId !== node.id || connection.targetPort !==
|
|
2452
|
+
if (connection.targetId !== node.id || connection.targetPort !== name) return null;
|
|
2374
2453
|
return connection.isValid === false ? "invalid" : "valid";
|
|
2375
2454
|
}, { flow });
|
|
2376
2455
|
if (!permitted) return null;
|
|
2377
2456
|
return /* @__PURE__ */ jsx("div", {
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
"
|
|
2457
|
+
role: interaction === "inspect" ? "button" : void 0,
|
|
2458
|
+
"aria-label": interaction === "inspect" ? `${port?.label ?? name} port` : void 0,
|
|
2459
|
+
"aria-pressed": interaction === "inspect" ? selected : void 0,
|
|
2460
|
+
tabIndex: interaction === "inspect" && focusable ? 0 : void 0,
|
|
2461
|
+
...nativeProps,
|
|
2462
|
+
ref: setRef,
|
|
2463
|
+
"data-flow-port-name": name,
|
|
2381
2464
|
"data-flow-node-id": node.id,
|
|
2382
2465
|
"data-flow-port-id": portId,
|
|
2383
2466
|
"data-flow-port-interaction": interaction,
|
|
2384
2467
|
"data-flow-selected": selected ? "true" : void 0,
|
|
2385
2468
|
"data-connection-candidate": candidate ?? void 0,
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2469
|
+
onFocus: (event) => {
|
|
2470
|
+
if (interaction === "inspect" && focusable) flow.internals.store.trigger.focus({ entityId: portId });
|
|
2471
|
+
onFocus?.(event);
|
|
2472
|
+
},
|
|
2473
|
+
onBlur: (event) => {
|
|
2474
|
+
if (interaction === "inspect") clearEntityFocusOnBlur(flow, portId, event);
|
|
2475
|
+
onBlur?.(event);
|
|
2476
|
+
},
|
|
2392
2477
|
style: {
|
|
2393
2478
|
pointerEvents: "auto",
|
|
2394
2479
|
...props.style
|
|
2395
2480
|
},
|
|
2396
2481
|
children: props.children
|
|
2397
2482
|
});
|
|
2398
|
-
}
|
|
2483
|
+
});
|
|
2399
2484
|
//#endregion
|
|
2400
2485
|
//#region src/DragHandle.tsx
|
|
2401
2486
|
/**
|
|
@@ -2473,4 +2558,36 @@ function useApplyFlowColorMode(element) {
|
|
|
2473
2558
|
]);
|
|
2474
2559
|
}
|
|
2475
2560
|
//#endregion
|
|
2476
|
-
|
|
2561
|
+
//#region src/EdgePrimitives.tsx
|
|
2562
|
+
/** Paint routed segments while Flow updates their geometry during gestures. */
|
|
2563
|
+
function EdgePath({ pathData, ...props }) {
|
|
2564
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: pathData.segments.map((segment, index) => /* @__PURE__ */ jsx("path", {
|
|
2565
|
+
fill: "none",
|
|
2566
|
+
...props,
|
|
2567
|
+
d: pathToSVG(segment),
|
|
2568
|
+
"data-flow-edge-segment": index
|
|
2569
|
+
}, index)) });
|
|
2570
|
+
}
|
|
2571
|
+
const EdgeEndpoint = forwardRef(function EdgeEndpoint({ edgeId, pathData, end, radius = 10, offset = {
|
|
2572
|
+
x: 0,
|
|
2573
|
+
y: 0
|
|
2574
|
+
}, ...props }, ref) {
|
|
2575
|
+
const point = end === "source" ? pathData.segments[0]?.points[0] : pathData.segments.at(-1)?.points.at(-1);
|
|
2576
|
+
if (!point) return null;
|
|
2577
|
+
return /* @__PURE__ */ jsx("circle", {
|
|
2578
|
+
fill: "transparent",
|
|
2579
|
+
pointerEvents: "all",
|
|
2580
|
+
...props,
|
|
2581
|
+
ref,
|
|
2582
|
+
"data-entity-id": edgeId,
|
|
2583
|
+
"data-flow-part": `edge-endpoint:${end}`,
|
|
2584
|
+
"data-flow-edge-endpoint": end,
|
|
2585
|
+
"data-flow-edge-endpoint-offset-x": offset.x,
|
|
2586
|
+
"data-flow-edge-endpoint-offset-y": offset.y,
|
|
2587
|
+
cx: point.x + offset.x,
|
|
2588
|
+
cy: point.y + offset.y,
|
|
2589
|
+
r: radius
|
|
2590
|
+
});
|
|
2591
|
+
});
|
|
2592
|
+
//#endregion
|
|
2593
|
+
export { Background, Controls, CrossBackground, DotsBackground, DragHandle, EdgeEndpoint, EdgePath, 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.
|
|
3
|
+
"version": "0.11.0",
|
|
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.
|
|
38
|
-
"@statelyai/flow-dom": "3.5.
|
|
37
|
+
"@statelyai/flow": "0.13.0",
|
|
38
|
+
"@statelyai/flow-dom": "3.5.2"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"react": "^18.2.0 || ^19.0.0",
|