@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 CHANGED
@@ -17,7 +17,7 @@ The React package owns React lifecycle, context, selector subscriptions, and a c
17
17
 
18
18
  <!-- transient entity rendering split between src/StatelyFlow.tsx and flow-dom src/entity-geometry.ts -->
19
19
 
20
- Optimization is part of the architecture, not a later cleanup pass. Components subscribe only to durable content and interaction state. During drag, resize, and animated layout sessions, `@statelyai/flow-dom` batches node rectangles, edge paths, endpoint handles, 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.
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, resize, 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.
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
 
@@ -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, ResizeHandle, Viewport, VisualPort } from "@statelyai/flow";
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
- * plain store trigger — this component is exactly what you would write in
346
+ * controller viewport command — this component is exactly what you would write in
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. Deliberately thin: every button is one store trigger
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, NodeResizer, type NodeResizerProps, PanePortal, Panel, type PanelPosition, type PanelProps, Port, type PortProps, type ReactActiveGraphProjection, type ReactGraphEdgeAttrs, type ReactGraphNodeAttrs, type RenderConnectionLineProps, type RenderEdgeLabelProps, type RenderEdgeProps, type RenderNodeProps, StatelyFlow, type StatelyFlowProps, type StatelyFlowRootProps, type StatelyFlowStyle, type UseCreateFlowInstanceOptions, type 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 };
502
+ export { Background, type BackgroundProps, type BackgroundVariant, Controls, type ControlsProps, type CreateFlowInstanceOptions, CrossBackground, DotsBackground, DragHandle, type DragHandleProps, EntityAnchor, type EntityAnchorAlign, type EntityAnchorPoint, type EntityAnchorProps, type EntityAnchorSide, FLOW_BASE_CSS, FLOW_BASE_STYLES_ID, FlowColorModeScope, type FlowColorModeScopeProps, type FlowEdgeBinding, type FlowEventListeners, type FlowInstance, FlowInstanceProvider, type FlowInstanceSettings, type FlowNodeBinding, type FlowPortBinding, type FlowSelector, type FlowSelectorEquality, type FlowStoreListener, FlowToolbar, type FlowToolbarProps, type FlowToolbarState, LinesBackground, MiniMap, type MiniMapProps, PanePortal, Panel, type PanelPosition, type PanelProps, Port, type PortProps, type ReactActiveGraphProjection, type ReactGraphEdgeAttrs, type ReactGraphNodeAttrs, type RenderConnectionLineProps, type RenderEdgeLabelProps, type RenderEdgeProps, type RenderNodeProps, StatelyFlow, type StatelyFlowProps, type StatelyFlowRootProps, type StatelyFlowStyle, type UseCreateFlowInstanceOptions, type UseFitViewExclusionOptions, type UseFlowInputOptions, type UseFlowOptions, type UseFlowToolbarOptions, ViewportPortal, createFlowInstance, useApplyFlowColorMode, useConnection, useCreateFlowInstance, useFitViewExclusion, useFlow, useFlowBaseStyles, useFlowColorMode, useFlowConnections, useFlowContext, useFlowEdge, useFlowEntity, useFlowInput, useFlowInstance, useFlowMeasurement, useFlowNode, useFlowPort, useFlowSelector, useFlowToolbar, useFlowValue, useFlowView, useFlowViewport, useKeyPress, useMeasuredSize, useNodesInitialized, useOptionalFlowInstance };
package/dist/esm/index.js CHANGED
@@ -1,12 +1,19 @@
1
1
  import { a as FlowInstanceProvider, i as LinesBackground, n as CrossBackground, o as useFlowInstance, r as DotsBackground, s as useOptionalFlowInstance, t as Background } from "./backgrounds2.js";
2
2
  import { applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, clientToCanvas, computeRenderableIds, createEntityGeometryObserver, createFlowView, createMeasurementObserver, nodePositionStyle, renderableIdsEqual, resolveFlowColorMode, viewportStyle } from "@statelyai/flow-dom";
3
- import { CONNECTION_PREVIEW_EDGE_ID, areNodesMeasured, createConnectionPreviewGraph, createEntityChangeNotifier, createFlow, createFlowInputRuntime, getAnchorTransform, getAutoPanDelta, getMinimapProjection, getProjectedEdgeAttrs, getProjectedNodeAttrs, getToolbarPlacement, nodeDepths, pathToSVG, profileModesForContext, profilePoliciesForContext, viewportCenteredOn } from "@statelyai/flow";
4
- import { createContext, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
+ import { CONNECTION_PREVIEW_EDGE_ID, areNodesMeasured, createConnectionPreviewGraph, createEntityChangeNotifier, createFlow, createFlowInputRuntime, getAnchorTransform, getAutoPanDelta, getMinimapProjection, getProjectedEdgeAttrs, getProjectedNodeAttrs, getToolbarPlacement, markerToSVG, pathToSVG, profileModesForContext, profilePoliciesForContext, viewportCenteredOn } from "@statelyai/flow";
4
+ import { createContext, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector";
7
7
  import { createPortal } from "react-dom";
8
8
  export * from "@statelyai/flow-dom";
9
9
  //#region src/createFlowInstance.ts
10
+ /**
11
+ * Create the React adapter's imperative Flow instance.
12
+ *
13
+ * The instance owns its controller and all listeners registered through
14
+ * `listen` or creation options. `destroy()` removes those listeners and
15
+ * destroys the underlying controller.
16
+ */
10
17
  function createFlowInstance(options) {
11
18
  const { keymap, ...createFlowSettings } = options.settings ?? {};
12
19
  const flow = createFlow({
@@ -68,6 +75,12 @@ function createFlowInstance(options) {
68
75
  }
69
76
  //#endregion
70
77
  //#region src/hooks.ts
78
+ /**
79
+ * Create one Flow instance for a component lifetime.
80
+ *
81
+ * Creation settings are initial-only; use `flow.configure()` for runtime
82
+ * changes. Event/listener props are rebound when their identities change.
83
+ */
71
84
  function useFlow(options) {
72
85
  return useCreateFlowInstance(options);
73
86
  }
@@ -125,6 +138,7 @@ function useCreateFlowInstance(options) {
125
138
  }, [flow]);
126
139
  return flow;
127
140
  }
141
+ /** Subscribe to a derived slice of Flow context with optional equality. */
128
142
  function useFlowSelector(selector, options = {}) {
129
143
  const contextFlow = useOptionalFlowInstance();
130
144
  const flow = options.flow ?? contextFlow;
@@ -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 useFlowSelector((context) => computeRenderableIds(instance.internals.engine, context, enabled), { isEqual: renderableIdsEqual });
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 = useFlowSelector((context) => getOrderedEntities(instance, context).edges.map((edge) => edge.id), { isEqual: stringArraysEqual });
960
+ const edgeIds = useDragStableFlowSelector((_context) => getOrderedEntities(instance).edges.map((edge) => edge.id), stringArraysEqual);
902
961
  const renderEdge = props.renderEdge;
903
- return /* @__PURE__ */ jsx("svg", {
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) || a.geometryActive !== b.geometryActive) return false;
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: () => props.flow.internals.store.trigger.focus({ entityId: props.id }),
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" : void 0,
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) || a.geometryActive !== b.geometryActive) return false;
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 = useFlowSelector((context) => {
1296
- const paintOrder = new Map(getOrderedEntities(instance, context).nodes.map((node, orderIndex) => [node.id, orderIndex]));
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
- }, { isEqual: nodePaintEntriesEqual });
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, context) {
1332
- const maybeEngine = flow.internals.engine;
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) || a.geometryActive !== b.geometryActive) return false;
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: () => props.flow.internals.store.trigger.focus({ entityId: props.id }),
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 ? props.renderNode({
1478
- flow: props.flow,
1479
- node,
1480
- bounds,
1481
- attrs,
1482
- selected,
1483
- dragging,
1484
- hovered,
1485
- highlights
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
- * plain store trigger — this component is exactly what you would write in
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.internals.store.trigger.setViewport(viewportCenteredOn(canvasPoint, viewportSize, viewport.zoom));
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. Deliberately thin: every button is one store trigger
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.internals.store.trigger.zoomBy({ factor: 1.2 }),
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.internals.store.trigger.zoomBy({ factor: 1 / 1.2 }),
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.internals.store.trigger.fitView({ padding: .1 }),
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
- style: props.style,
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, NodeResizer, 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 };
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.8.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.7.0",
38
- "@statelyai/flow-dom": "3.3.0"
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
  }