@statelyai/flow-react 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -239,6 +239,14 @@ type StatelyFlowBaseProps<TNodeData = unknown, TEdgeData = unknown> = {
239
239
  * margin). Recommended for large graphs.
240
240
  */
241
241
  onlyRenderVisible?: boolean;
242
+ /**
243
+ * The zoom that zoom-dependent work follows, when it should lag the live
244
+ * zoom. While the live zoom differs (a zoom gesture), each frame only
245
+ * changes the viewport transform: `--flow-zoom` and `--flow-inverse-zoom`
246
+ * keep this zoom's values and `onlyRenderVisible` keeps its last cull.
247
+ * Both catch up when this prop changes. Omit to follow the live zoom.
248
+ */
249
+ settledZoom?: number;
242
250
  /**
243
251
  * Override the input modes driving gestures. Updates apply to new gestures
244
252
  * without cancelling the current one. Defaults to
@@ -351,7 +359,10 @@ type MiniMapProps = {
351
359
  padding?: number;
352
360
  className?: string;
353
361
  style?: CSSProperties;
354
- /** Render one node marker; defaults to a plain `<rect>`. */
362
+ /**
363
+ * Render one node marker; defaults to a plain `<rect>`. Pass a stable
364
+ * function so viewport changes skip re-rendering the markers.
365
+ */
355
366
  renderNode?: (node: FlowNode, rect: Rect) => ReactNode;
356
367
  };
357
368
  /**
package/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
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
- import { applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, clientToCanvas, computeRenderableIds, createEntityGeometryObserver, createFlowView, createMeasurementObserver, nodePositionStyle, observeAppearanceBounds, renderableIdsEqual, resolveFlowColorMode, viewportStyle } from "@statelyai/flow-dom";
2
+ import { applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, clientToCanvas, computeSettledRenderableIds, createEntityGeometryObserver, createFlowView, createMeasurementObserver, nodePositionStyle, observeAppearanceBounds, renderableIdsEqual, resolveFlowColorMode, viewportStyle, writeZoomVariables } from "@statelyai/flow-dom";
3
3
  import { CONNECTION_PREVIEW_EDGE_ID, appearanceMeasurementId, areNodesMeasured, createConnectionPreviewGraph, createEntityChangeNotifier, createFlow, createFlowInputRuntime, defaultModesForContext, getAnchorTransform, getAutoPanDelta, getMinimapProjection, getMinimapViewportCenter, getProjectedEdgeAttrs, getProjectedNodeAttrs, getToolbarPlacement, getVisibleBounds, markerToSVG, pathToSVG, profileModesForContext, profilePoliciesForContext, viewportCenteredOn } from "@statelyai/flow";
4
4
  import { Fragment, createContext, forwardRef, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
5
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -646,15 +646,30 @@ function useDragStableFlowSelector(selector, isEqual) {
646
646
  return value;
647
647
  }, { isEqual });
648
648
  }
649
- function useRenderableIds(instance, enabled) {
650
- return useDragStableFlowSelector((context) => computeRenderableIds(instance.internals.engine, context, enabled), renderableIdsEqual);
649
+ function useRenderableIds(instance, enabled, settledZoom) {
650
+ const lastCull = useRef(null);
651
+ return useDragStableFlowSelector((context) => {
652
+ lastCull.current = computeSettledRenderableIds(instance.internals.engine, context, enabled, settledZoom, lastCull.current);
653
+ return lastCull.current.value;
654
+ }, renderableIdsEqual);
651
655
  }
652
- function useViewportTransform(ref, instance) {
656
+ function useViewportTransform(ref, instance, settledZoom) {
657
+ const settledZoomRef = useRef(settledZoom);
658
+ settledZoomRef.current = settledZoom;
653
659
  useLayoutEffect(() => {
654
660
  const el = ref.current;
655
661
  if (!el) return;
656
- return attachViewportTransform(instance.internals.store, el);
662
+ return attachViewportTransform(instance.internals.store, el, { getVariableZoom: () => settledZoomRef.current });
657
663
  }, [ref, instance]);
664
+ useLayoutEffect(() => {
665
+ const el = ref.current;
666
+ if (!el) return;
667
+ writeZoomVariables(el, settledZoom ?? instance.getContext().viewport.zoom);
668
+ }, [
669
+ ref,
670
+ instance,
671
+ settledZoom
672
+ ]);
658
673
  }
659
674
  function StatelyFlow(props) {
660
675
  if (props.flow) return /* @__PURE__ */ jsx(StatelyFlowView, {
@@ -703,7 +718,7 @@ function StatelyFlowView(props) {
703
718
  useLayoutEffect(() => props.flow.subscribe((context) => context.focusedEntityId, (nextId, previousId) => {
704
719
  if (containerRef.current) blurClearedEntityFocus(containerRef.current, previousId, nextId);
705
720
  }), [props.flow]);
706
- useViewportTransform(viewportRef, props.flow);
721
+ useViewportTransform(viewportRef, props.flow, props.settledZoom);
707
722
  useFlowBaseStyles();
708
723
  const projections = useActiveProjections(props.flow, props.projections);
709
724
  const { className: rootClassName, style: rootStyle, tabIndex: rootTabIndex, ...rootProps } = props.rootProps ?? {};
@@ -776,14 +791,16 @@ function StatelyFlowView(props) {
776
791
  geometryObserver,
777
792
  renderEdge: props.renderEdge,
778
793
  projections,
779
- onlyRenderVisible: props.onlyRenderVisible
794
+ onlyRenderVisible: props.onlyRenderVisible,
795
+ settledZoom: props.settledZoom
780
796
  }),
781
797
  /* @__PURE__ */ jsx(NodeLayer, {
782
798
  geometryObserver,
783
799
  appearanceRenderers: props.appearanceRenderers,
784
800
  renderNode: props.renderNode,
785
801
  projections,
786
- onlyRenderVisible: props.onlyRenderVisible
802
+ onlyRenderVisible: props.onlyRenderVisible,
803
+ settledZoom: props.settledZoom
787
804
  }),
788
805
  /* @__PURE__ */ jsx(ConnectionPreviewEdge, {
789
806
  flow: props.flow,
@@ -796,7 +813,8 @@ function StatelyFlowView(props) {
796
813
  appearanceRenderers: props.appearanceRenderers,
797
814
  renderEdgeLabel: props.renderEdgeLabel,
798
815
  projections,
799
- onlyRenderVisible: props.onlyRenderVisible
816
+ onlyRenderVisible: props.onlyRenderVisible,
817
+ settledZoom: props.settledZoom
800
818
  }),
801
819
  props.showSelectionBounds ? /* @__PURE__ */ jsx(SelectionBoundsLayer, { geometryObserver }) : null,
802
820
  /* @__PURE__ */ jsx(SelectionBoxLayer, {}),
@@ -1025,7 +1043,7 @@ function SelectionBoundsLayer(props) {
1025
1043
  function EdgeLayer(props) {
1026
1044
  const instance = useFlowInstance();
1027
1045
  const markerPrefix = `stately-flow-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
1028
- const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
1046
+ const renderableIds = useRenderableIds(instance, props.onlyRenderVisible, props.settledZoom);
1029
1047
  const edgeIds = useDragStableFlowSelector((_context) => getOrderedEntities(instance).edges.map((edge) => edge.id), stringArraysEqual);
1030
1048
  const renderEdge = props.renderEdge;
1031
1049
  return /* @__PURE__ */ jsxs("svg", {
@@ -1070,7 +1088,7 @@ const EdgeView = memo(function EdgeView(props) {
1070
1088
  if (!(a.configRevision === b.configRevision && a.selected === b.selected && a.focused === b.focused && a.hovered === b.hovered && a.highlights === b.highlights)) return false;
1071
1089
  if (a.geometryActive !== b.geometryActive) return b.geometryActive ? edgeRenderContentEqual(a.edge, b.edge) : false;
1072
1090
  if (a.geometryActive) return edgeRenderContentEqual(a.edge, b.edge);
1073
- return a.graph === b.graph && a.edge === b.edge && a.pathData === b.pathData;
1091
+ return (a.graph === b.graph || !props.projections?.length) && a.edge === b.edge && a.pathData === b.pathData;
1074
1092
  } });
1075
1093
  useLayoutEffect(() => {
1076
1094
  const element = ref.current;
@@ -1094,8 +1112,7 @@ const EdgeView = memo(function EdgeView(props) {
1094
1112
  const focusProps = {
1095
1113
  role: "group",
1096
1114
  "aria-roledescription": "flow edge",
1097
- "aria-label": props.flow.internals.engine.getAriaLabel(props.id),
1098
- "aria-selected": selected,
1115
+ "aria-label": props.flow.internals.engine.getAriaLabel(props.id) + (selected ? ", selected" : ""),
1099
1116
  tabIndex: focusable ? 0 : void 0,
1100
1117
  onFocus: (event) => {
1101
1118
  if (descendantOwnsFlowFocus(event)) return;
@@ -1327,7 +1344,7 @@ function EdgeEndpointHandles(props) {
1327
1344
  }
1328
1345
  function EdgeLabelLayer(props) {
1329
1346
  const instance = useFlowInstance();
1330
- const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
1347
+ const renderableIds = useRenderableIds(instance, props.onlyRenderVisible, props.settledZoom);
1331
1348
  const renderEdgeLabel = props.renderEdgeLabel;
1332
1349
  const graphEdgeIds = useFlowSelector((context) => context.graph.edges.map((edge) => edge.id), { isEqual: stringArraysEqual });
1333
1350
  const durableLabelIds = useFlowSelector((context) => context.graph.edges.filter((edge) => edge.label).map((edge) => edge.id), { isEqual: stringArraysEqual });
@@ -1377,7 +1394,7 @@ const EdgeLabelItem = memo(function EdgeLabelItem(props) {
1377
1394
  if (!(a.configRevision === b.configRevision && a.selected === b.selected && a.dragging === b.dragging && a.hovered === b.hovered && a.highlights === b.highlights)) return false;
1378
1395
  if (a.geometryActive !== b.geometryActive) return b.geometryActive ? edgeRenderContentEqual(a.edge, b.edge) : false;
1379
1396
  if (a.geometryActive) return edgeRenderContentEqual(a.edge, b.edge);
1380
- return a.graph === b.graph && a.edge === b.edge && a.pathData === b.pathData;
1397
+ return (a.graph === b.graph || !props.projections?.length) && a.edge === b.edge && a.pathData === b.pathData;
1381
1398
  } });
1382
1399
  if (!edge || !pathData) return null;
1383
1400
  const attrs = getProjectedEdgeAttrs({
@@ -1505,7 +1522,7 @@ function projectedEdgeLabelBounds(pathData, label) {
1505
1522
  }
1506
1523
  function NodeLayer(props) {
1507
1524
  const instance = useFlowInstance();
1508
- const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
1525
+ const renderableIds = useRenderableIds(instance, props.onlyRenderVisible, props.settledZoom);
1509
1526
  const nodePaintEntries = useDragStableFlowSelector((context) => {
1510
1527
  const paintZIndices = instance.internals.engine.getPaintZIndices().nodes;
1511
1528
  return context.graph.nodes.flatMap((node) => {
@@ -1597,7 +1614,7 @@ const NodeView = memo(function NodeView(props) {
1597
1614
  if (!(a.configRevision === b.configRevision && 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;
1598
1615
  if (a.geometryActive !== b.geometryActive) return b.geometryActive ? nodeRenderContentEqual(a.node, b.node) : false;
1599
1616
  if (a.geometryActive) return nodeRenderContentEqual(a.node, b.node);
1600
- return a.graph === b.graph && a.node === b.node && rectsShallowEqual(a.bounds, b.bounds);
1617
+ return (a.graph === b.graph || !props.projections?.length && !b.node?.group) && a.node === b.node && rectsShallowEqual(a.bounds, b.bounds);
1601
1618
  } });
1602
1619
  const appearance = node ? props.flow.internals.engine.resolveAppearance(node) : void 0;
1603
1620
  useAppearanceMeasurement(ref, props.id, appearance, props.observer);
@@ -1614,7 +1631,8 @@ const NodeView = memo(function NodeView(props) {
1614
1631
  props.id,
1615
1632
  props.observer,
1616
1633
  props.flow,
1617
- hidden
1634
+ hidden,
1635
+ node?.group?.type
1618
1636
  ]);
1619
1637
  useLayoutEffect(() => {
1620
1638
  const element = ref.current;
@@ -1656,7 +1674,6 @@ const NodeView = memo(function NodeView(props) {
1656
1674
  className: classNames$1("stately-flow__node", attrs.className),
1657
1675
  role: "group",
1658
1676
  "aria-roledescription": "flow node",
1659
- "aria-selected": selected,
1660
1677
  "aria-label": props.flow.internals.engine.getAriaLabel(props.id) + (selected ? ", selected" : ""),
1661
1678
  tabIndex: focusable ? 0 : void 0,
1662
1679
  onFocus: (event) => {
@@ -1982,6 +1999,20 @@ function projectionTransform(from, to) {
1982
1999
  });
1983
2000
  return `translate(${toOrigin.x - fromOrigin.x * scale} ${toOrigin.y - fromOrigin.y * scale}) scale(${scale})`;
1984
2001
  }
2002
+ const MiniMapNodes = memo(function MiniMapNodes(props) {
2003
+ return props.nodes.map((node, index) => {
2004
+ const rect = props.rects[index];
2005
+ return /* @__PURE__ */ jsx(Fragment, { children: props.renderNode?.(node, rect) ?? /* @__PURE__ */ jsx("rect", {
2006
+ "data-flow-minimap-node": node.id,
2007
+ x: rect.x,
2008
+ y: rect.y,
2009
+ width: Math.max(rect.width, 1),
2010
+ height: Math.max(rect.height, 1),
2011
+ rx: 1,
2012
+ fill: "#94a3b8"
2013
+ }) }, node.id);
2014
+ });
2015
+ });
1985
2016
  /**
1986
2017
  * Overview map. Projection and navigation math live in core; this adapter owns
1987
2018
  * only React rendering and the browser pointer session.
@@ -2167,17 +2198,10 @@ function MiniMap(props) {
2167
2198
  },
2168
2199
  children: [/* @__PURE__ */ jsx("g", {
2169
2200
  ref: markerGroupRef,
2170
- children: nodes.map((node, index) => {
2171
- const rect = projectedNodes[index];
2172
- return /* @__PURE__ */ jsx(Fragment, { children: props.renderNode?.(node, rect) ?? /* @__PURE__ */ jsx("rect", {
2173
- "data-flow-minimap-node": node.id,
2174
- x: rect.x,
2175
- y: rect.y,
2176
- width: Math.max(rect.width, 1),
2177
- height: Math.max(rect.height, 1),
2178
- rx: 1,
2179
- fill: "#94a3b8"
2180
- }) }, node.id);
2201
+ children: /* @__PURE__ */ jsx(MiniMapNodes, {
2202
+ nodes,
2203
+ rects: projectedNodes,
2204
+ renderNode: props.renderNode
2181
2205
  })
2182
2206
  }), /* @__PURE__ */ jsx("rect", {
2183
2207
  ref: viewportRectRef,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/flow-react",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Thin React adapter for @statelyai/flow with core and DOM re-exports.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -34,8 +34,8 @@
34
34
  "license": "MIT",
35
35
  "dependencies": {
36
36
  "use-sync-external-store": "^1.6.0",
37
- "@statelyai/flow": "0.13.0",
38
- "@statelyai/flow-dom": "3.5.2"
37
+ "@statelyai/flow": "0.13.2",
38
+ "@statelyai/flow-dom": "3.6.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "react": "^18.2.0 || ^19.0.0",