@statelyai/flow-react 0.11.1 → 0.12.1

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;
@@ -1326,7 +1344,7 @@ function EdgeEndpointHandles(props) {
1326
1344
  }
1327
1345
  function EdgeLabelLayer(props) {
1328
1346
  const instance = useFlowInstance();
1329
- const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
1347
+ const renderableIds = useRenderableIds(instance, props.onlyRenderVisible, props.settledZoom);
1330
1348
  const renderEdgeLabel = props.renderEdgeLabel;
1331
1349
  const graphEdgeIds = useFlowSelector((context) => context.graph.edges.map((edge) => edge.id), { isEqual: stringArraysEqual });
1332
1350
  const durableLabelIds = useFlowSelector((context) => context.graph.edges.filter((edge) => edge.label).map((edge) => edge.id), { isEqual: stringArraysEqual });
@@ -1376,7 +1394,7 @@ const EdgeLabelItem = memo(function EdgeLabelItem(props) {
1376
1394
  if (!(a.configRevision === b.configRevision && a.selected === b.selected && a.dragging === b.dragging && a.hovered === b.hovered && a.highlights === b.highlights)) return false;
1377
1395
  if (a.geometryActive !== b.geometryActive) return b.geometryActive ? edgeRenderContentEqual(a.edge, b.edge) : false;
1378
1396
  if (a.geometryActive) return edgeRenderContentEqual(a.edge, b.edge);
1379
- 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;
1380
1398
  } });
1381
1399
  if (!edge || !pathData) return null;
1382
1400
  const attrs = getProjectedEdgeAttrs({
@@ -1504,7 +1522,7 @@ function projectedEdgeLabelBounds(pathData, label) {
1504
1522
  }
1505
1523
  function NodeLayer(props) {
1506
1524
  const instance = useFlowInstance();
1507
- const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
1525
+ const renderableIds = useRenderableIds(instance, props.onlyRenderVisible, props.settledZoom);
1508
1526
  const nodePaintEntries = useDragStableFlowSelector((context) => {
1509
1527
  const paintZIndices = instance.internals.engine.getPaintZIndices().nodes;
1510
1528
  return context.graph.nodes.flatMap((node) => {
@@ -1596,7 +1614,7 @@ const NodeView = memo(function NodeView(props) {
1596
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;
1597
1615
  if (a.geometryActive !== b.geometryActive) return b.geometryActive ? nodeRenderContentEqual(a.node, b.node) : false;
1598
1616
  if (a.geometryActive) return nodeRenderContentEqual(a.node, b.node);
1599
- 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);
1600
1618
  } });
1601
1619
  const appearance = node ? props.flow.internals.engine.resolveAppearance(node) : void 0;
1602
1620
  useAppearanceMeasurement(ref, props.id, appearance, props.observer);
@@ -1981,6 +1999,20 @@ function projectionTransform(from, to) {
1981
1999
  });
1982
2000
  return `translate(${toOrigin.x - fromOrigin.x * scale} ${toOrigin.y - fromOrigin.y * scale}) scale(${scale})`;
1983
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
+ });
1984
2016
  /**
1985
2017
  * Overview map. Projection and navigation math live in core; this adapter owns
1986
2018
  * only React rendering and the browser pointer session.
@@ -2166,17 +2198,10 @@ function MiniMap(props) {
2166
2198
  },
2167
2199
  children: [/* @__PURE__ */ jsx("g", {
2168
2200
  ref: markerGroupRef,
2169
- children: nodes.map((node, index) => {
2170
- const rect = projectedNodes[index];
2171
- return /* @__PURE__ */ jsx(Fragment, { children: props.renderNode?.(node, rect) ?? /* @__PURE__ */ jsx("rect", {
2172
- "data-flow-minimap-node": node.id,
2173
- x: rect.x,
2174
- y: rect.y,
2175
- width: Math.max(rect.width, 1),
2176
- height: Math.max(rect.height, 1),
2177
- rx: 1,
2178
- fill: "#94a3b8"
2179
- }) }, node.id);
2201
+ children: /* @__PURE__ */ jsx(MiniMapNodes, {
2202
+ nodes,
2203
+ rects: projectedNodes,
2204
+ renderNode: props.renderNode
2180
2205
  })
2181
2206
  }), /* @__PURE__ */ jsx("rect", {
2182
2207
  ref: viewportRectRef,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/flow-react",
3
- "version": "0.11.1",
3
+ "version": "0.12.1",
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.1",
38
- "@statelyai/flow-dom": "3.5.3"
37
+ "@statelyai/flow": "0.13.3",
38
+ "@statelyai/flow-dom": "3.6.1"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "react": "^18.2.0 || ^19.0.0",