@statelyai/flow-react 0.8.0 → 0.9.3
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 +7 -3
- package/dist/esm/index.d.ts +24 -16
- package/dist/esm/index.js +195 -163
- package/package.json +7 -4
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
|
|
|
@@ -53,9 +53,9 @@ 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 connection port or a graph-owned readonly inspection port |
|
|
56
57
|
| `RenderEdgeLabelProps` | Edge, path, bounds, interaction state, and projected attrs passed to `renderEdgeLabel` |
|
|
57
58
|
| `Panel` | Positioned overlay panel for controls and custom UI |
|
|
58
|
-
| `NodeResizer` | Resize handles for custom node renderers; constraints live on `node.resize` |
|
|
59
59
|
| `ReactActiveGraphProjection` | React-typed active projection for `<StatelyFlow projections={...} />` or controller-owned projections |
|
|
60
60
|
| `ReactGraphNodeAttrs` / `ReactGraphEdgeAttrs` | React-typed visual attrs exposed to custom node/edge renderers |
|
|
61
61
|
| `FlowColorModeScope` | Applies the current flow color mode as DOM data attributes and `color-scheme` |
|
|
@@ -75,7 +75,11 @@ Color mode is store data, not engine behavior. `@statelyai/flow-dom` exports sma
|
|
|
75
75
|
|
|
76
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
77
|
|
|
78
|
-
Set `showSelectionBounds` to render a zoom-stable, square frame around established multi-selections. Its geometry follows drag
|
|
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.
|
|
79
83
|
|
|
80
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.
|
|
81
85
|
|
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,
|
|
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
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
|
|
@@ -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>;
|
|
@@ -329,7 +343,7 @@ type MiniMapProps = {
|
|
|
329
343
|
/**
|
|
330
344
|
* Overview map. Deliberately thin: all math lives in core
|
|
331
345
|
* (`getMinimapProjection`, `viewportCenteredOn`) and every interaction is a
|
|
332
|
-
*
|
|
346
|
+
* controller viewport command — this component is exactly what you would write in
|
|
333
347
|
* userland, kept here as a convenience.
|
|
334
348
|
*/
|
|
335
349
|
declare function MiniMap(props: MiniMapProps): import("react").JSX.Element;
|
|
@@ -351,8 +365,7 @@ type ControlsProps = {
|
|
|
351
365
|
children?: ReactNode;
|
|
352
366
|
};
|
|
353
367
|
/**
|
|
354
|
-
* Zoom/fit toolbar
|
|
355
|
-
* (`zoomBy`, `fitView`) — replicate or extend it freely in userland.
|
|
368
|
+
* Zoom/fit toolbar backed by the controller's constrained viewport API.
|
|
356
369
|
*/
|
|
357
370
|
declare function Controls(props: ControlsProps): import("react").JSX.Element;
|
|
358
371
|
//#endregion
|
|
@@ -388,6 +401,11 @@ declare function EntityAnchor(props: EntityAnchorProps): import("react").JSX.Ele
|
|
|
388
401
|
//#region src/Port.d.ts
|
|
389
402
|
type PortProps = {
|
|
390
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";
|
|
391
409
|
/**
|
|
392
410
|
* Whether the DOM element reports its bounds back to the graph (default
|
|
393
411
|
* true). Ports are unidirectional, one way or the other:
|
|
@@ -444,16 +462,6 @@ type DragHandleProps = HTMLAttributes<HTMLDivElement> & {
|
|
|
444
462
|
*/
|
|
445
463
|
declare function DragHandle({ children, style, ...rest }: DragHandleProps): import("react").JSX.Element;
|
|
446
464
|
//#endregion
|
|
447
|
-
//#region src/NodeResizer.d.ts
|
|
448
|
-
type NodeResizerProps = {
|
|
449
|
-
handles?: ResizeHandle[];
|
|
450
|
-
/** Defaults to node selection state. Constraints belong on `node.resize`. */
|
|
451
|
-
isVisible?: boolean;
|
|
452
|
-
className?: string;
|
|
453
|
-
style?: CSSProperties;
|
|
454
|
-
};
|
|
455
|
-
declare function NodeResizer(props: NodeResizerProps): import("react").JSX.Element | null;
|
|
456
|
-
//#endregion
|
|
457
465
|
//#region src/baseStyles.d.ts
|
|
458
466
|
/**
|
|
459
467
|
* Minimal base stylesheet for the flow canvas. Intentionally tiny and
|
|
@@ -474,7 +482,7 @@ declare function NodeResizer(props: NodeResizerProps): import("react").JSX.Eleme
|
|
|
474
482
|
* - `data-flow-selection-bounds` — the established multi-selection frame.
|
|
475
483
|
*/
|
|
476
484
|
declare const FLOW_BASE_STYLES_ID = "flow-base-styles";
|
|
477
|
-
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-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";
|
|
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";
|
|
478
486
|
/** Inject the base stylesheet once per document (deduped by element id). */
|
|
479
487
|
declare function useFlowBaseStyles(): void;
|
|
480
488
|
//#endregion
|
|
@@ -491,4 +499,4 @@ declare function useFlowColorMode(): {
|
|
|
491
499
|
};
|
|
492
500
|
declare function useApplyFlowColorMode(element: HTMLElement | null): void;
|
|
493
501
|
//#endregion
|
|
494
|
-
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;
|
|
@@ -403,6 +417,20 @@ const FLOW_BASE_CSS = `
|
|
|
403
417
|
pointer-events: auto;
|
|
404
418
|
}
|
|
405
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
|
+
|
|
406
434
|
[data-flow-selection-box] {
|
|
407
435
|
position: absolute;
|
|
408
436
|
box-sizing: border-box;
|
|
@@ -442,6 +470,18 @@ function useNodeRenderContext() {
|
|
|
442
470
|
return context;
|
|
443
471
|
}
|
|
444
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
|
|
445
485
|
//#region src/LayerPortals.tsx
|
|
446
486
|
const FlowLayerTargetsContext = createContext(null);
|
|
447
487
|
function FlowLayerTargetsProvider(props) {
|
|
@@ -517,17 +557,35 @@ function nodePaintEntriesEqual(a, b) {
|
|
|
517
557
|
function classNames$1(...values) {
|
|
518
558
|
return values.filter(Boolean).join(" ");
|
|
519
559
|
}
|
|
520
|
-
function clearEntityFocusOnBlur(flow, entityId, event) {
|
|
521
|
-
const nextTarget = event.relatedTarget;
|
|
522
|
-
if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget)) return;
|
|
523
|
-
if (flow.getSnapshot().focusedEntityId === entityId) flow.internals.store.trigger.clearFocus();
|
|
524
|
-
}
|
|
525
560
|
function assignRef(ref, value) {
|
|
526
561
|
if (typeof ref === "function") ref(value);
|
|
527
562
|
else if (ref) ref.current = value;
|
|
528
563
|
}
|
|
564
|
+
/**
|
|
565
|
+
* `dragRevision` changes only for transient drag geometry. Topology, paint
|
|
566
|
+
* order, and viewport membership stay stable during that transition, so
|
|
567
|
+
* list-level selectors can retain their prior value while per-entity DOM
|
|
568
|
+
* observers update only the moved cohort.
|
|
569
|
+
*/
|
|
570
|
+
function useDragStableFlowSelector(selector, isEqual) {
|
|
571
|
+
const cached = useRef(null);
|
|
572
|
+
return useFlowSelector((context) => {
|
|
573
|
+
if (cached.current && context.canvasState === "dragging" && cached.current.canvasState === "dragging" && context.dragRevision !== cached.current.dragRevision) {
|
|
574
|
+
cached.current.canvasState = context.canvasState;
|
|
575
|
+
cached.current.dragRevision = context.dragRevision;
|
|
576
|
+
return cached.current.value;
|
|
577
|
+
}
|
|
578
|
+
const value = selector(context);
|
|
579
|
+
cached.current = {
|
|
580
|
+
canvasState: context.canvasState,
|
|
581
|
+
dragRevision: context.dragRevision,
|
|
582
|
+
value
|
|
583
|
+
};
|
|
584
|
+
return value;
|
|
585
|
+
}, { isEqual });
|
|
586
|
+
}
|
|
529
587
|
function useRenderableIds(instance, enabled) {
|
|
530
|
-
return
|
|
588
|
+
return useDragStableFlowSelector((context) => computeRenderableIds(instance.internals.engine, context, enabled), renderableIdsEqual);
|
|
531
589
|
}
|
|
532
590
|
function useViewportTransform(ref, instance) {
|
|
533
591
|
useLayoutEffect(() => {
|
|
@@ -897,10 +955,11 @@ function SelectionBoundsLayer(props) {
|
|
|
897
955
|
}
|
|
898
956
|
function EdgeLayer(props) {
|
|
899
957
|
const instance = useFlowInstance();
|
|
958
|
+
const markerPrefix = `stately-flow-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
900
959
|
const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
|
|
901
|
-
const edgeIds =
|
|
960
|
+
const edgeIds = useDragStableFlowSelector((_context) => getOrderedEntities(instance).edges.map((edge) => edge.id), stringArraysEqual);
|
|
902
961
|
const renderEdge = props.renderEdge;
|
|
903
|
-
return /* @__PURE__ */
|
|
962
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
904
963
|
"aria-label": "edges",
|
|
905
964
|
className: "stately-flow__edges",
|
|
906
965
|
"data-flow-canvas-layer": true,
|
|
@@ -913,20 +972,22 @@ function EdgeLayer(props) {
|
|
|
913
972
|
overflow: "visible",
|
|
914
973
|
pointerEvents: "none"
|
|
915
974
|
},
|
|
916
|
-
children: /* @__PURE__ */ jsxs("g", { children: [edgeIds.map((id) => {
|
|
975
|
+
children: [/* @__PURE__ */ jsx(DefaultMarkerDefinitions, { prefix: markerPrefix }), /* @__PURE__ */ jsxs("g", { children: [edgeIds.map((id) => {
|
|
917
976
|
if (renderableIds && !renderableIds.edgeIds.has(id)) return null;
|
|
918
977
|
return /* @__PURE__ */ jsx(EdgeView, {
|
|
919
978
|
flow: instance,
|
|
920
979
|
geometryObserver: props.geometryObserver,
|
|
921
980
|
id,
|
|
922
981
|
projections: props.projections,
|
|
923
|
-
renderEdge
|
|
982
|
+
renderEdge,
|
|
983
|
+
markerPrefix
|
|
924
984
|
}, id);
|
|
925
985
|
}), /* @__PURE__ */ jsx(ConnectionPreviewEdge, {
|
|
926
986
|
flow: instance,
|
|
927
987
|
renderEdge,
|
|
928
|
-
renderConnectionLine: props.renderConnectionLine
|
|
929
|
-
|
|
988
|
+
renderConnectionLine: props.renderConnectionLine,
|
|
989
|
+
markerPrefix
|
|
990
|
+
})] })]
|
|
930
991
|
});
|
|
931
992
|
}
|
|
932
993
|
const EdgeView = memo(function EdgeView(props) {
|
|
@@ -941,7 +1002,8 @@ const EdgeView = memo(function EdgeView(props) {
|
|
|
941
1002
|
hovered: context.hoveredEntityIds.has(props.id),
|
|
942
1003
|
highlights: props.flow.internals.engine.getHighlights(props.id)
|
|
943
1004
|
}), { isEqual: (a, b) => {
|
|
944
|
-
if (!(a.selected === b.selected && a.focused === b.focused && a.hovered === b.hovered && a.highlights === b.highlights)
|
|
1005
|
+
if (!(a.selected === b.selected && a.focused === b.focused && a.hovered === b.hovered && a.highlights === b.highlights)) return false;
|
|
1006
|
+
if (a.geometryActive !== b.geometryActive) return b.geometryActive ? edgeRenderContentEqual(a.edge, b.edge) : false;
|
|
945
1007
|
if (a.geometryActive) return edgeRenderContentEqual(a.edge, b.edge);
|
|
946
1008
|
return a.graph === b.graph && a.edge === b.edge && a.pathData === b.pathData;
|
|
947
1009
|
} });
|
|
@@ -970,7 +1032,10 @@ const EdgeView = memo(function EdgeView(props) {
|
|
|
970
1032
|
"aria-label": props.flow.internals.engine.getAriaLabel(props.id),
|
|
971
1033
|
"aria-selected": selected,
|
|
972
1034
|
tabIndex: focusable ? 0 : void 0,
|
|
973
|
-
onFocus: () =>
|
|
1035
|
+
onFocus: (event) => {
|
|
1036
|
+
if (descendantOwnsFlowFocus(event)) return;
|
|
1037
|
+
props.flow.internals.store.trigger.focus({ entityId: props.id });
|
|
1038
|
+
},
|
|
974
1039
|
onBlur: (event) => clearEntityFocusOnBlur(props.flow, props.id, event)
|
|
975
1040
|
};
|
|
976
1041
|
if (props.renderEdge) return /* @__PURE__ */ jsxs("g", {
|
|
@@ -1010,7 +1075,8 @@ const EdgeView = memo(function EdgeView(props) {
|
|
|
1010
1075
|
edge,
|
|
1011
1076
|
pathData,
|
|
1012
1077
|
attrs,
|
|
1013
|
-
selected
|
|
1078
|
+
selected,
|
|
1079
|
+
markerPrefix: props.markerPrefix
|
|
1014
1080
|
})
|
|
1015
1081
|
});
|
|
1016
1082
|
});
|
|
@@ -1042,6 +1108,7 @@ function ConnectionPreviewEdge(props) {
|
|
|
1042
1108
|
pathData,
|
|
1043
1109
|
attrs: {},
|
|
1044
1110
|
selected: false,
|
|
1111
|
+
markerPrefix: props.markerPrefix,
|
|
1045
1112
|
preview: true
|
|
1046
1113
|
})
|
|
1047
1114
|
});
|
|
@@ -1053,9 +1120,11 @@ function DefaultEdge(props) {
|
|
|
1053
1120
|
d,
|
|
1054
1121
|
"data-flow-edge-segment": index,
|
|
1055
1122
|
fill: "none",
|
|
1056
|
-
stroke: props.attrs.color ?? (props.preview ? props.edge.data?.isValid === false ? "#dc2626" : props.edge.data?.isValid === true ? "#16a34a" : "#0f766e" : props.selected ? "#2563eb" : "#94a3b8"),
|
|
1057
|
-
strokeDasharray: props.preview ? "6 6" :
|
|
1058
|
-
strokeWidth: props.attrs.strokeWidth ?? (props.selected ? 3 : 2),
|
|
1123
|
+
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"),
|
|
1124
|
+
strokeDasharray: props.preview ? "6 6" : props.edge.style?.strokeDasharray,
|
|
1125
|
+
strokeWidth: props.attrs.strokeWidth ?? (props.selected ? 3 : props.edge.style?.strokeWidth ?? 2),
|
|
1126
|
+
markerStart: index === 0 ? markerToSVG(props.edge.style?.markerStart, props.markerPrefix) : void 0,
|
|
1127
|
+
markerEnd: index === props.pathData.segments.length - 1 ? markerToSVG(props.edge.style?.markerEnd, props.markerPrefix) : void 0,
|
|
1059
1128
|
style: props.attrs.style
|
|
1060
1129
|
}), props.preview ? null : /* @__PURE__ */ jsx("path", {
|
|
1061
1130
|
d,
|
|
@@ -1074,6 +1143,52 @@ function DefaultEdge(props) {
|
|
|
1074
1143
|
selected: props.selected
|
|
1075
1144
|
})] });
|
|
1076
1145
|
}
|
|
1146
|
+
function DefaultMarkerDefinitions(props) {
|
|
1147
|
+
return /* @__PURE__ */ jsxs("defs", { children: [
|
|
1148
|
+
/* @__PURE__ */ jsx("marker", {
|
|
1149
|
+
id: `${props.prefix}-arrow`,
|
|
1150
|
+
viewBox: "0 0 10 10",
|
|
1151
|
+
refX: "9",
|
|
1152
|
+
refY: "5",
|
|
1153
|
+
markerWidth: "6",
|
|
1154
|
+
markerHeight: "6",
|
|
1155
|
+
orient: "auto-start-reverse",
|
|
1156
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1157
|
+
d: "M 1 1 L 9 5 L 1 9",
|
|
1158
|
+
fill: "none",
|
|
1159
|
+
stroke: "context-stroke"
|
|
1160
|
+
})
|
|
1161
|
+
}),
|
|
1162
|
+
/* @__PURE__ */ jsx("marker", {
|
|
1163
|
+
id: `${props.prefix}-arrow-closed`,
|
|
1164
|
+
viewBox: "0 0 10 10",
|
|
1165
|
+
refX: "9",
|
|
1166
|
+
refY: "5",
|
|
1167
|
+
markerWidth: "6",
|
|
1168
|
+
markerHeight: "6",
|
|
1169
|
+
orient: "auto-start-reverse",
|
|
1170
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1171
|
+
d: "M 1 1 L 9 5 L 1 9 Z",
|
|
1172
|
+
fill: "context-stroke"
|
|
1173
|
+
})
|
|
1174
|
+
}),
|
|
1175
|
+
/* @__PURE__ */ jsx("marker", {
|
|
1176
|
+
id: `${props.prefix}-circle`,
|
|
1177
|
+
viewBox: "0 0 10 10",
|
|
1178
|
+
refX: "5",
|
|
1179
|
+
refY: "5",
|
|
1180
|
+
markerWidth: "5",
|
|
1181
|
+
markerHeight: "5",
|
|
1182
|
+
orient: "auto",
|
|
1183
|
+
children: /* @__PURE__ */ jsx("circle", {
|
|
1184
|
+
cx: "5",
|
|
1185
|
+
cy: "5",
|
|
1186
|
+
r: "4",
|
|
1187
|
+
fill: "context-stroke"
|
|
1188
|
+
})
|
|
1189
|
+
})
|
|
1190
|
+
] });
|
|
1191
|
+
}
|
|
1077
1192
|
function EdgeHitTarget(props) {
|
|
1078
1193
|
return /* @__PURE__ */ jsx(Fragment, { children: props.pathData.segments.map((segment, index) => {
|
|
1079
1194
|
return /* @__PURE__ */ jsx("path", {
|
|
@@ -1167,7 +1282,8 @@ const EdgeLabelItem = memo(function EdgeLabelItem(props) {
|
|
|
1167
1282
|
hovered: context.hoveredEntityIds.has(props.id),
|
|
1168
1283
|
highlights: props.flow.internals.engine.getHighlights(props.id)
|
|
1169
1284
|
}), { isEqual: (a, b) => {
|
|
1170
|
-
if (!(a.selected === b.selected && a.dragging === b.dragging && a.hovered === b.hovered && a.highlights === b.highlights)
|
|
1285
|
+
if (!(a.selected === b.selected && a.dragging === b.dragging && a.hovered === b.hovered && a.highlights === b.highlights)) return false;
|
|
1286
|
+
if (a.geometryActive !== b.geometryActive) return b.geometryActive ? edgeRenderContentEqual(a.edge, b.edge) : false;
|
|
1171
1287
|
if (a.geometryActive) return edgeRenderContentEqual(a.edge, b.edge);
|
|
1172
1288
|
return a.graph === b.graph && a.edge === b.edge && a.pathData === b.pathData;
|
|
1173
1289
|
} });
|
|
@@ -1292,16 +1408,17 @@ function projectedEdgeLabelBounds(pathData, label) {
|
|
|
1292
1408
|
function NodeLayer(props) {
|
|
1293
1409
|
const instance = useFlowInstance();
|
|
1294
1410
|
const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
|
|
1295
|
-
const nodePaintEntries =
|
|
1296
|
-
const paintOrder = new Map(getOrderedEntities(instance,
|
|
1411
|
+
const nodePaintEntries = useDragStableFlowSelector((context) => {
|
|
1412
|
+
const paintOrder = new Map(getOrderedEntities(instance, { elevateSelected: false }).nodes.map((node, orderIndex) => [node.id, orderIndex]));
|
|
1413
|
+
const selectedBoost = paintOrder.size;
|
|
1297
1414
|
return context.graph.nodes.flatMap((node) => {
|
|
1298
1415
|
const orderIndex = paintOrder.get(node.id);
|
|
1299
1416
|
return orderIndex === void 0 ? [] : [{
|
|
1300
1417
|
id: node.id,
|
|
1301
|
-
orderIndex
|
|
1418
|
+
orderIndex: orderIndex + (context.selection.has(node.id) ? selectedBoost : 0)
|
|
1302
1419
|
}];
|
|
1303
1420
|
});
|
|
1304
|
-
},
|
|
1421
|
+
}, nodePaintEntriesEqual);
|
|
1305
1422
|
const observer = useMeasurementObserver();
|
|
1306
1423
|
return /* @__PURE__ */ jsx("div", {
|
|
1307
1424
|
"aria-label": "nodes",
|
|
@@ -1328,33 +1445,8 @@ function NodeLayer(props) {
|
|
|
1328
1445
|
})
|
|
1329
1446
|
});
|
|
1330
1447
|
}
|
|
1331
|
-
function getOrderedEntities(flow,
|
|
1332
|
-
|
|
1333
|
-
const visibleOrdered = maybeEngine.getVisibleEntities?.();
|
|
1334
|
-
if (visibleOrdered) return visibleOrdered;
|
|
1335
|
-
const ordered = maybeEngine.getEntityOrder?.();
|
|
1336
|
-
if (ordered) return ordered;
|
|
1337
|
-
const depths = nodeDepths(context.graph.nodes);
|
|
1338
|
-
const nodeOrder = new Map(context.graph.nodes.map((node, index) => [node.id, index]));
|
|
1339
|
-
const edgeOrder = new Map(context.graph.edges.map((edge, index) => [edge.id, index]));
|
|
1340
|
-
return {
|
|
1341
|
-
nodes: [...context.graph.nodes].sort((a, b) => {
|
|
1342
|
-
const depthDelta = (depths.get(a.id) ?? 0) - (depths.get(b.id) ?? 0);
|
|
1343
|
-
if (depthDelta !== 0) return depthDelta;
|
|
1344
|
-
const zDelta = (a.zIndex ?? 0) - (b.zIndex ?? 0);
|
|
1345
|
-
if (zDelta !== 0) return zDelta;
|
|
1346
|
-
const selectedDelta = Number(context.selection.has(a.id)) - Number(context.selection.has(b.id));
|
|
1347
|
-
if (selectedDelta !== 0) return selectedDelta;
|
|
1348
|
-
return (nodeOrder.get(a.id) ?? 0) - (nodeOrder.get(b.id) ?? 0);
|
|
1349
|
-
}),
|
|
1350
|
-
edges: [...context.graph.edges].sort((a, b) => {
|
|
1351
|
-
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);
|
|
1352
|
-
if (depthDelta !== 0) return depthDelta;
|
|
1353
|
-
const zDelta = (a.zIndex ?? 0) - (b.zIndex ?? 0);
|
|
1354
|
-
if (zDelta !== 0) return zDelta;
|
|
1355
|
-
return (edgeOrder.get(a.id) ?? 0) - (edgeOrder.get(b.id) ?? 0);
|
|
1356
|
-
})
|
|
1357
|
-
};
|
|
1448
|
+
function getOrderedEntities(flow, options = {}) {
|
|
1449
|
+
return flow.internals.engine.getVisibleEntities(options);
|
|
1358
1450
|
}
|
|
1359
1451
|
function rectsShallowEqual(a, b) {
|
|
1360
1452
|
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
|
|
@@ -1403,7 +1495,8 @@ const NodeView = memo(function NodeView(props) {
|
|
|
1403
1495
|
bounds: context.resizingEntityId === props.id && context.resizeCorners ? props.flow.internals.engine.getResizeBounds() ?? props.flow.internals.engine.visualBounds(props.id) : props.flow.internals.engine.visualBounds(props.id)
|
|
1404
1496
|
};
|
|
1405
1497
|
}, { isEqual: (a, b) => {
|
|
1406
|
-
if (!(a.hidden === b.hidden && a.selected === b.selected && a.dragging === b.dragging && a.focused === b.focused && a.hovered === b.hovered && a.highlights === b.highlights)
|
|
1498
|
+
if (!(a.hidden === b.hidden && a.selected === b.selected && a.dragging === b.dragging && a.focused === b.focused && a.hovered === b.hovered && a.highlights === b.highlights)) return false;
|
|
1499
|
+
if (a.geometryActive !== b.geometryActive) return b.geometryActive ? nodeRenderContentEqual(a.node, b.node) : false;
|
|
1407
1500
|
if (a.geometryActive) return nodeRenderContentEqual(a.node, b.node);
|
|
1408
1501
|
return a.graph === b.graph && a.node === b.node && rectsShallowEqual(a.bounds, b.bounds);
|
|
1409
1502
|
} });
|
|
@@ -1445,6 +1538,8 @@ const NodeView = memo(function NodeView(props) {
|
|
|
1445
1538
|
projections: props.projections
|
|
1446
1539
|
});
|
|
1447
1540
|
const focusable = props.flow.internals.engine.resolveEntityConfig(node).focusable;
|
|
1541
|
+
const groupGeometry = node.group ? props.flow.internals.engine.getGroupGeometry(node.id) : void 0;
|
|
1542
|
+
const groupHasInteractiveContent = Boolean(groupGeometry?.contentBounds.width && groupGeometry.contentBounds.height);
|
|
1448
1543
|
return /* @__PURE__ */ jsx(NodeRenderContextProvider, {
|
|
1449
1544
|
value: {
|
|
1450
1545
|
node,
|
|
@@ -1461,7 +1556,10 @@ const NodeView = memo(function NodeView(props) {
|
|
|
1461
1556
|
"aria-selected": selected,
|
|
1462
1557
|
"aria-label": props.flow.internals.engine.getAriaLabel(props.id) + (selected ? ", selected" : ""),
|
|
1463
1558
|
tabIndex: focusable ? 0 : void 0,
|
|
1464
|
-
onFocus: () =>
|
|
1559
|
+
onFocus: (event) => {
|
|
1560
|
+
if (descendantOwnsFlowFocus(event)) return;
|
|
1561
|
+
props.flow.internals.store.trigger.focus({ entityId: props.id });
|
|
1562
|
+
},
|
|
1465
1563
|
onBlur: (event) => clearEntityFocusOnBlur(props.flow, props.id, event),
|
|
1466
1564
|
style: {
|
|
1467
1565
|
...nodePositionStyle(bounds, {
|
|
@@ -1469,20 +1567,27 @@ const NodeView = memo(function NodeView(props) {
|
|
|
1469
1567
|
dragging
|
|
1470
1568
|
}),
|
|
1471
1569
|
cursor: dragging ? "grabbing" : "grab",
|
|
1472
|
-
pointerEvents: "auto",
|
|
1570
|
+
pointerEvents: node.group?.type === "derived" || node.group && groupHasInteractiveContent ? "none" : "auto",
|
|
1473
1571
|
userSelect: "none",
|
|
1474
1572
|
zIndex: props.orderIndex,
|
|
1475
1573
|
opacity: attrs.opacity
|
|
1476
1574
|
},
|
|
1477
|
-
children: props.renderNode ?
|
|
1478
|
-
flow:
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1575
|
+
children: props.renderNode ? /* @__PURE__ */ jsx("div", {
|
|
1576
|
+
"data-flow-custom-node": true,
|
|
1577
|
+
style: {
|
|
1578
|
+
display: "contents",
|
|
1579
|
+
pointerEvents: "none"
|
|
1580
|
+
},
|
|
1581
|
+
children: props.renderNode({
|
|
1582
|
+
flow: props.flow,
|
|
1583
|
+
node,
|
|
1584
|
+
bounds,
|
|
1585
|
+
attrs,
|
|
1586
|
+
selected,
|
|
1587
|
+
dragging,
|
|
1588
|
+
hovered,
|
|
1589
|
+
highlights
|
|
1590
|
+
})
|
|
1486
1591
|
}) : node.group ? /* @__PURE__ */ jsx(DefaultGroupNode, {
|
|
1487
1592
|
flow: props.flow,
|
|
1488
1593
|
node,
|
|
@@ -1548,6 +1653,7 @@ function DefaultGroupNode(props) {
|
|
|
1548
1653
|
color: "#0f172a",
|
|
1549
1654
|
font: "600 13px/1.2 system-ui, sans-serif",
|
|
1550
1655
|
cursor: props.dragging ? "grabbing" : "grab",
|
|
1656
|
+
pointerEvents: "auto",
|
|
1551
1657
|
userSelect: "none",
|
|
1552
1658
|
...props.attrs.style
|
|
1553
1659
|
},
|
|
@@ -1710,7 +1816,7 @@ function FlowToolbar(props) {
|
|
|
1710
1816
|
/**
|
|
1711
1817
|
* Overview map. Deliberately thin: all math lives in core
|
|
1712
1818
|
* (`getMinimapProjection`, `viewportCenteredOn`) and every interaction is a
|
|
1713
|
-
*
|
|
1819
|
+
* controller viewport command — this component is exactly what you would write in
|
|
1714
1820
|
* userland, kept here as a convenience.
|
|
1715
1821
|
*/
|
|
1716
1822
|
function MiniMap(props) {
|
|
@@ -1735,7 +1841,7 @@ function MiniMap(props) {
|
|
|
1735
1841
|
x: event.clientX - rect.left,
|
|
1736
1842
|
y: event.clientY - rect.top
|
|
1737
1843
|
});
|
|
1738
|
-
flow.
|
|
1844
|
+
flow.viewport.set(viewportCenteredOn(canvasPoint, viewportSize, viewport.zoom));
|
|
1739
1845
|
};
|
|
1740
1846
|
return /* @__PURE__ */ jsxs("svg", {
|
|
1741
1847
|
"aria-label": "minimap",
|
|
@@ -1858,8 +1964,7 @@ const buttonStyle = {
|
|
|
1858
1964
|
cursor: "pointer"
|
|
1859
1965
|
};
|
|
1860
1966
|
/**
|
|
1861
|
-
* Zoom/fit toolbar
|
|
1862
|
-
* (`zoomBy`, `fitView`) — replicate or extend it freely in userland.
|
|
1967
|
+
* Zoom/fit toolbar backed by the controller's constrained viewport API.
|
|
1863
1968
|
*/
|
|
1864
1969
|
function Controls(props) {
|
|
1865
1970
|
const flow = useFlowInstance();
|
|
@@ -1879,21 +1984,21 @@ function Controls(props) {
|
|
|
1879
1984
|
type: "button",
|
|
1880
1985
|
"aria-label": "zoom in",
|
|
1881
1986
|
style: buttonStyle,
|
|
1882
|
-
onClick: () => flow.
|
|
1987
|
+
onClick: () => flow.viewport.zoomBy(1.2),
|
|
1883
1988
|
children: "+"
|
|
1884
1989
|
}),
|
|
1885
1990
|
/* @__PURE__ */ jsx("button", {
|
|
1886
1991
|
type: "button",
|
|
1887
1992
|
"aria-label": "zoom out",
|
|
1888
1993
|
style: buttonStyle,
|
|
1889
|
-
onClick: () => flow.
|
|
1994
|
+
onClick: () => flow.viewport.zoomBy(1 / 1.2),
|
|
1890
1995
|
children: "−"
|
|
1891
1996
|
}),
|
|
1892
1997
|
/* @__PURE__ */ jsx("button", {
|
|
1893
1998
|
type: "button",
|
|
1894
1999
|
"aria-label": "fit view",
|
|
1895
2000
|
style: buttonStyle,
|
|
1896
|
-
onClick: () => flow.
|
|
2001
|
+
onClick: () => flow.viewport.fit({ padding: .1 }),
|
|
1897
2002
|
children: "⛶"
|
|
1898
2003
|
}),
|
|
1899
2004
|
props.children
|
|
@@ -2000,6 +2105,11 @@ function Port(props) {
|
|
|
2000
2105
|
const observerRef = useRef(null);
|
|
2001
2106
|
const observer = useMeasurementObserver();
|
|
2002
2107
|
const measured = props.measured ?? true;
|
|
2108
|
+
const interaction = props.interaction ?? "connect";
|
|
2109
|
+
const port = node.ports?.find((candidate) => candidate.name === props.name);
|
|
2110
|
+
const portId = `${node.id}:${props.name}`;
|
|
2111
|
+
const focusable = flow.internals.engine.resolveEntityConfig(node).focusable;
|
|
2112
|
+
const selected = useFlowSelector((context) => context.selection.has(portId), { flow });
|
|
2003
2113
|
useLayoutEffect(() => {
|
|
2004
2114
|
const element = ref.current;
|
|
2005
2115
|
if (!element || !measured) return;
|
|
@@ -2035,8 +2145,20 @@ function Port(props) {
|
|
|
2035
2145
|
className: props.className,
|
|
2036
2146
|
"data-flow-port-name": props.name,
|
|
2037
2147
|
"data-flow-node-id": node.id,
|
|
2148
|
+
"data-flow-port-id": portId,
|
|
2149
|
+
"data-flow-port-interaction": interaction,
|
|
2150
|
+
"data-flow-selected": selected ? "true" : void 0,
|
|
2038
2151
|
"data-connection-candidate": candidate ?? void 0,
|
|
2039
|
-
|
|
2152
|
+
role: interaction === "inspect" ? "button" : void 0,
|
|
2153
|
+
"aria-label": interaction === "inspect" ? `${port?.label ?? props.name} port` : void 0,
|
|
2154
|
+
"aria-pressed": interaction === "inspect" ? selected : void 0,
|
|
2155
|
+
tabIndex: interaction === "inspect" && focusable ? 0 : void 0,
|
|
2156
|
+
onFocus: interaction === "inspect" && focusable ? () => flow.internals.store.trigger.focus({ entityId: portId }) : void 0,
|
|
2157
|
+
onBlur: interaction === "inspect" ? (event) => clearEntityFocusOnBlur(flow, portId, event) : void 0,
|
|
2158
|
+
style: {
|
|
2159
|
+
pointerEvents: "auto",
|
|
2160
|
+
...props.style
|
|
2161
|
+
},
|
|
2040
2162
|
children: props.children
|
|
2041
2163
|
});
|
|
2042
2164
|
}
|
|
@@ -2073,96 +2195,6 @@ function DragHandle({ children, style, ...rest }) {
|
|
|
2073
2195
|
});
|
|
2074
2196
|
}
|
|
2075
2197
|
//#endregion
|
|
2076
|
-
//#region src/NodeResizer.tsx
|
|
2077
|
-
const DEFAULT_HANDLES = [
|
|
2078
|
-
"top",
|
|
2079
|
-
"right",
|
|
2080
|
-
"bottom",
|
|
2081
|
-
"left",
|
|
2082
|
-
"top-left",
|
|
2083
|
-
"top-right",
|
|
2084
|
-
"bottom-left",
|
|
2085
|
-
"bottom-right"
|
|
2086
|
-
];
|
|
2087
|
-
const HANDLE_STYLE = {
|
|
2088
|
-
top: {
|
|
2089
|
-
left: "50%",
|
|
2090
|
-
top: 0,
|
|
2091
|
-
cursor: "ns-resize"
|
|
2092
|
-
},
|
|
2093
|
-
right: {
|
|
2094
|
-
left: "100%",
|
|
2095
|
-
top: "50%",
|
|
2096
|
-
cursor: "ew-resize"
|
|
2097
|
-
},
|
|
2098
|
-
bottom: {
|
|
2099
|
-
left: "50%",
|
|
2100
|
-
top: "100%",
|
|
2101
|
-
cursor: "ns-resize"
|
|
2102
|
-
},
|
|
2103
|
-
left: {
|
|
2104
|
-
left: 0,
|
|
2105
|
-
top: "50%",
|
|
2106
|
-
cursor: "ew-resize"
|
|
2107
|
-
},
|
|
2108
|
-
"top-left": {
|
|
2109
|
-
left: 0,
|
|
2110
|
-
top: 0,
|
|
2111
|
-
cursor: "nwse-resize"
|
|
2112
|
-
},
|
|
2113
|
-
"top-right": {
|
|
2114
|
-
left: "100%",
|
|
2115
|
-
top: 0,
|
|
2116
|
-
cursor: "nesw-resize"
|
|
2117
|
-
},
|
|
2118
|
-
"bottom-left": {
|
|
2119
|
-
left: 0,
|
|
2120
|
-
top: "100%",
|
|
2121
|
-
cursor: "nesw-resize"
|
|
2122
|
-
},
|
|
2123
|
-
"bottom-right": {
|
|
2124
|
-
left: "100%",
|
|
2125
|
-
top: "100%",
|
|
2126
|
-
cursor: "nwse-resize"
|
|
2127
|
-
}
|
|
2128
|
-
};
|
|
2129
|
-
function NodeResizer(props) {
|
|
2130
|
-
const { node } = useNodeRenderContext();
|
|
2131
|
-
const selected = useFlowSelector((context) => context.selection.has(node.id));
|
|
2132
|
-
if (!(props.isVisible ?? selected)) return null;
|
|
2133
|
-
return /* @__PURE__ */ jsx("div", {
|
|
2134
|
-
"aria-hidden": "true",
|
|
2135
|
-
className: props.className,
|
|
2136
|
-
"data-flow-node-resizer": "",
|
|
2137
|
-
style: {
|
|
2138
|
-
position: "absolute",
|
|
2139
|
-
inset: 0,
|
|
2140
|
-
pointerEvents: "none",
|
|
2141
|
-
...props.style
|
|
2142
|
-
},
|
|
2143
|
-
children: (props.handles ?? DEFAULT_HANDLES).map((handle) => /* @__PURE__ */ jsx("div", {
|
|
2144
|
-
"data-entity-id": node.id,
|
|
2145
|
-
"data-flow-part": `resize-handle:${handle}`,
|
|
2146
|
-
"data-flow-resize-handle": handle,
|
|
2147
|
-
style: {
|
|
2148
|
-
position: "absolute",
|
|
2149
|
-
width: "calc(10px / var(--flow-zoom, 1))",
|
|
2150
|
-
height: "calc(10px / var(--flow-zoom, 1))",
|
|
2151
|
-
minWidth: 6,
|
|
2152
|
-
minHeight: 6,
|
|
2153
|
-
boxSizing: "border-box",
|
|
2154
|
-
border: "calc(1px / var(--flow-zoom, 1)) solid #ffffff",
|
|
2155
|
-
borderRadius: "calc(2px / var(--flow-zoom, 1))",
|
|
2156
|
-
background: "var(--flow-selection-color, #2563eb)",
|
|
2157
|
-
boxShadow: "0 0 0 calc(1px / var(--flow-zoom, 1)) rgba(15, 23, 42, 0.8)",
|
|
2158
|
-
pointerEvents: "auto",
|
|
2159
|
-
transform: "translate(-50%, -50%)",
|
|
2160
|
-
...HANDLE_STYLE[handle]
|
|
2161
|
-
}
|
|
2162
|
-
}, handle))
|
|
2163
|
-
});
|
|
2164
|
-
}
|
|
2165
|
-
//#endregion
|
|
2166
2198
|
//#region src/ColorMode.tsx
|
|
2167
2199
|
function FlowColorModeScope(props) {
|
|
2168
2200
|
const { colorMode, resolvedColorMode } = useFlowColorMode();
|
|
@@ -2207,4 +2239,4 @@ function useApplyFlowColorMode(element) {
|
|
|
2207
2239
|
]);
|
|
2208
2240
|
}
|
|
2209
2241
|
//#endregion
|
|
2210
|
-
export { Background, Controls, CrossBackground, DotsBackground, DragHandle, EntityAnchor, FLOW_BASE_CSS, FLOW_BASE_STYLES_ID, FlowColorModeScope, FlowInstanceProvider, FlowToolbar, LinesBackground, MiniMap,
|
|
2242
|
+
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.3",
|
|
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.3
|
|
37
|
+
"@statelyai/flow": "0.9.0",
|
|
38
|
+
"@statelyai/flow-dom": "3.4.3"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"react": "^18.2.0 || ^19.0.0",
|
|
@@ -83,7 +83,10 @@
|
|
|
83
83
|
"typecheck": "tsc --noEmit",
|
|
84
84
|
"test:browser": "vitest run --config vitest.browser.config.mjs",
|
|
85
85
|
"test:e2e": "playwright test",
|
|
86
|
-
"bench:browser": "playwright test --config playwright.performance.config.ts",
|
|
86
|
+
"bench:browser": "playwright test --config playwright.performance.config.ts && node ../../scripts/report-browser-benchmarks.mjs",
|
|
87
|
+
"bench:react": "playwright test --config playwright.performance.config.ts --grep StatelyFlow && node ../../scripts/report-browser-benchmarks.mjs",
|
|
88
|
+
"bench:interaction": "playwright test --config playwright.performance.config.ts --grep 'viewport pan' && node ../../scripts/report-browser-benchmarks.mjs",
|
|
89
|
+
"bench:comparison": "playwright test --config playwright.performance.config.ts --grep 'StatelyFlow|React Flow' && node ../../scripts/report-browser-benchmarks.mjs",
|
|
87
90
|
"storybook": "storybook dev -p 6008",
|
|
88
91
|
"storybook:build": "storybook build"
|
|
89
92
|
}
|