@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 +55 -45
- package/dist/esm/index.d.ts +36 -17
- package/dist/esm/index.js +284 -170
- 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
|
|
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
|
|
31
|
-
|
|
|
32
|
-
| `createFlowInstance(options)`
|
|
33
|
-
| `useFlow(options)`
|
|
34
|
-
| `useCreateFlowInstance(options)`
|
|
35
|
-
| `useFlowSelector(selector, options?)`
|
|
36
|
-
| `useFlowValue(selector, isEqual?)`
|
|
37
|
-
| `useFlowContext()`
|
|
38
|
-
| `useConnection()`
|
|
39
|
-
| `useFlowConnections(query)`
|
|
40
|
-
| `useFlowView()`
|
|
41
|
-
| `useKeyPress(keys)`
|
|
42
|
-
| `useNodesInitialized()`
|
|
43
|
-
| `useFlowNode(id, selector?, isEqual?)`
|
|
44
|
-
| `useFlowEdge(id, selector?, isEqual?)`
|
|
45
|
-
| `useFlowPort(id, selector?, isEqual?)`
|
|
46
|
-
| `useFlowViewport()`
|
|
47
|
-
| `useFlowMeasurement(id)`
|
|
48
|
-
| `FlowInstanceProvider`
|
|
49
|
-
| `useFlowInstance()`
|
|
50
|
-
| `useOptionalFlowInstance()`
|
|
51
|
-
| `StatelyFlow`
|
|
52
|
-
| `createEntityGeometryObserver(store)`
|
|
53
|
-
| `PanePortal` / `ViewportPortal`
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
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
|
-
|
|
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
|
|
110
|
+
} from '@statelyai/flow-react';
|
|
100
111
|
|
|
101
112
|
const graph = createGraph({
|
|
102
113
|
nodes: [
|
|
103
|
-
createNode({ id:
|
|
104
|
-
createNode({ id:
|
|
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:
|
|
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(
|
|
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:
|
|
141
|
+
projection: { id: 'coverage', nodes: { a: 'visited' } },
|
|
131
142
|
mapper: {
|
|
132
|
-
getNodeAttrs: () => ({ color:
|
|
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
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -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,
|
|
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
|
-
*
|
|
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
|
|
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(
|
|
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,
|
|
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,
|
|
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:
|
|
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
|
-
}), [
|
|
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:
|
|
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((
|
|
937
|
+
const edgeIds = useFlowSelector((_context) => getOrderedEntities(instance).edges.map((edge) => edge.id), { isEqual: stringArraysEqual });
|
|
793
938
|
const renderEdge = props.renderEdge;
|
|
794
|
-
return /* @__PURE__ */
|
|
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: () =>
|
|
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" :
|
|
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__ */
|
|
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
|
-
})
|
|
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
|
|
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
|
|
1223
|
-
|
|
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: () =>
|
|
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 ?
|
|
1369
|
-
flow:
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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
|
-
*
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
38
|
-
"@statelyai/flow-dom": "3.2
|
|
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",
|