@statelyai/flow-react 0.9.3 → 0.9.5

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.
@@ -341,10 +341,8 @@ type MiniMapProps = {
341
341
  renderNode?: (node: FlowNode, rect: Rect) => ReactNode;
342
342
  };
343
343
  /**
344
- * Overview map. Deliberately thin: all math lives in core
345
- * (`getMinimapProjection`, `viewportCenteredOn`) and every interaction is a
346
- * controller viewport command — this component is exactly what you would write in
347
- * userland, kept here as a convenience.
344
+ * Overview map. Projection and navigation math live in core; this adapter owns
345
+ * only React rendering and the browser pointer session.
348
346
  */
349
347
  declare function MiniMap(props: MiniMapProps): import("react").JSX.Element;
350
348
  //#endregion
package/dist/esm/index.js CHANGED
@@ -1,8 +1,8 @@
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, markerToSVG, pathToSVG, profileModesForContext, profilePoliciesForContext, viewportCenteredOn } from "@statelyai/flow";
4
- import { createContext, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { CONNECTION_PREVIEW_EDGE_ID, areNodesMeasured, createConnectionPreviewGraph, createEntityChangeNotifier, createFlow, createFlowInputRuntime, getAnchorTransform, getAutoPanDelta, getMinimapProjection, getMinimapViewportCenter, getProjectedEdgeAttrs, getProjectedNodeAttrs, getToolbarPlacement, getVisibleBounds, markerToSVG, pathToSVG, profileModesForContext, profilePoliciesForContext, viewportCenteredOn } from "@statelyai/flow";
4
+ import { Fragment, createContext, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
+ import { Fragment as Fragment$1, 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";
@@ -323,7 +323,7 @@ function useFlowInput(flow, containerRef, options = {}) {
323
323
  store: flow.internals.store,
324
324
  runtime,
325
325
  keyboardTarget,
326
- autoPan: (context, screenPoint) => getAutoPanDelta(context, screenPoint, flow.internals.engine)
326
+ autoPan: getAutoPanDelta
327
327
  });
328
328
  }, [
329
329
  flow,
@@ -551,7 +551,7 @@ function stringArraysEqual(a, b) {
551
551
  }
552
552
  function nodePaintEntriesEqual(a, b) {
553
553
  if (a.length !== b.length) return false;
554
- for (let index = 0; index < a.length; index++) if (a[index]?.id !== b[index]?.id || a[index]?.orderIndex !== b[index]?.orderIndex) return false;
554
+ for (let index = 0; index < a.length; index++) if (a[index]?.id !== b[index]?.id || a[index]?.zIndex !== b[index]?.zIndex) return false;
555
555
  return true;
556
556
  }
557
557
  function classNames$1(...values) {
@@ -1114,7 +1114,7 @@ function ConnectionPreviewEdge(props) {
1114
1114
  });
1115
1115
  }
1116
1116
  function DefaultEdge(props) {
1117
- return /* @__PURE__ */ jsxs(Fragment, { children: [props.pathData.segments.map((segment, index) => {
1117
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [props.pathData.segments.map((segment, index) => {
1118
1118
  const d = pathToSVG(segment);
1119
1119
  return /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("path", {
1120
1120
  d,
@@ -1190,7 +1190,7 @@ function DefaultMarkerDefinitions(props) {
1190
1190
  ] });
1191
1191
  }
1192
1192
  function EdgeHitTarget(props) {
1193
- return /* @__PURE__ */ jsx(Fragment, { children: props.pathData.segments.map((segment, index) => {
1193
+ return /* @__PURE__ */ jsx(Fragment$1, { children: props.pathData.segments.map((segment, index) => {
1194
1194
  return /* @__PURE__ */ jsx("path", {
1195
1195
  d: pathToSVG(segment),
1196
1196
  "data-flow-edge-segment": index,
@@ -1211,7 +1211,7 @@ function EdgeEndpointHandles(props) {
1211
1211
  if (!first || !last) return null;
1212
1212
  const radius = 6 / props.flow.getContext().viewport.zoom;
1213
1213
  const strokeWidth = 2 / props.flow.getContext().viewport.zoom;
1214
- return /* @__PURE__ */ jsx(Fragment, { children: [{
1214
+ return /* @__PURE__ */ jsx(Fragment$1, { children: [{
1215
1215
  end: "source",
1216
1216
  point: first,
1217
1217
  cursor: "crosshair"
@@ -1409,13 +1409,12 @@ function NodeLayer(props) {
1409
1409
  const instance = useFlowInstance();
1410
1410
  const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
1411
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;
1412
+ const paintZIndices = instance.internals.engine.getPaintZIndices().nodes;
1414
1413
  return context.graph.nodes.flatMap((node) => {
1415
- const orderIndex = paintOrder.get(node.id);
1416
- return orderIndex === void 0 ? [] : [{
1414
+ const zIndex = paintZIndices.get(node.id);
1415
+ return zIndex === void 0 ? [] : [{
1417
1416
  id: node.id,
1418
- orderIndex: orderIndex + (context.selection.has(node.id) ? selectedBoost : 0)
1417
+ zIndex
1419
1418
  }];
1420
1419
  });
1421
1420
  }, nodePaintEntriesEqual);
@@ -1431,13 +1430,13 @@ function NodeLayer(props) {
1431
1430
  transformOrigin: "0 0",
1432
1431
  pointerEvents: "none"
1433
1432
  },
1434
- children: nodePaintEntries.map(({ id, orderIndex }) => {
1433
+ children: nodePaintEntries.map(({ id, zIndex }) => {
1435
1434
  if (renderableIds && !renderableIds.nodeIds.has(id)) return null;
1436
1435
  return /* @__PURE__ */ jsx(NodeView, {
1437
1436
  flow: instance,
1438
1437
  geometryObserver: props.geometryObserver,
1439
1438
  id,
1440
- orderIndex,
1439
+ zIndex,
1441
1440
  observer,
1442
1441
  projections: props.projections,
1443
1442
  renderNode: props.renderNode
@@ -1569,7 +1568,7 @@ const NodeView = memo(function NodeView(props) {
1569
1568
  cursor: dragging ? "grabbing" : "grab",
1570
1569
  pointerEvents: node.group?.type === "derived" || node.group && groupHasInteractiveContent ? "none" : "auto",
1571
1570
  userSelect: "none",
1572
- zIndex: props.orderIndex,
1571
+ zIndex: props.zIndex,
1573
1572
  opacity: attrs.opacity
1574
1573
  },
1575
1574
  children: props.renderNode ? /* @__PURE__ */ jsx("div", {
@@ -1628,7 +1627,7 @@ function DefaultGroupNode(props) {
1628
1627
  height: rect.height
1629
1628
  });
1630
1629
  const dropTarget = props.highlights.some((entry) => entry.kind === "drop-target");
1631
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", { style: {
1630
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", { style: {
1632
1631
  position: "absolute",
1633
1632
  ...relative(geometry.childArea),
1634
1633
  boxSizing: "border-box",
@@ -1813,35 +1812,115 @@ function FlowToolbar(props) {
1813
1812
  }
1814
1813
  //#endregion
1815
1814
  //#region src/MiniMap.tsx
1815
+ const DRAG_JITTER_THRESHOLD_SQ = 4;
1816
+ function containsPoint(rect, point) {
1817
+ return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
1818
+ }
1819
+ function setRect(element, rect) {
1820
+ if (!element) return;
1821
+ element.setAttribute("x", String(rect.x));
1822
+ element.setAttribute("y", String(rect.y));
1823
+ element.setAttribute("width", String(rect.width));
1824
+ element.setAttribute("height", String(rect.height));
1825
+ }
1826
+ function projectionTransform(from, to) {
1827
+ const scale = to.scale / from.scale;
1828
+ const fromOrigin = from.toMinimap({
1829
+ x: 0,
1830
+ y: 0
1831
+ });
1832
+ const toOrigin = to.toMinimap({
1833
+ x: 0,
1834
+ y: 0
1835
+ });
1836
+ return `translate(${toOrigin.x - fromOrigin.x * scale} ${toOrigin.y - fromOrigin.y * scale}) scale(${scale})`;
1837
+ }
1816
1838
  /**
1817
- * Overview map. Deliberately thin: all math lives in core
1818
- * (`getMinimapProjection`, `viewportCenteredOn`) and every interaction is a
1819
- * controller viewport command — this component is exactly what you would write in
1820
- * userland, kept here as a convenience.
1839
+ * Overview map. Projection and navigation math live in core; this adapter owns
1840
+ * only React rendering and the browser pointer session.
1821
1841
  */
1822
1842
  function MiniMap(props) {
1823
1843
  const flow = useFlowInstance();
1824
1844
  const width = props.width ?? 200;
1825
1845
  const height = props.height ?? 140;
1826
1846
  const nodes = useFlowSelector((context) => context.graph.nodes);
1827
- const viewport = useFlowSelector((context) => context.viewport);
1847
+ const measurements = useFlowSelector((context) => context.measurements);
1828
1848
  const viewportSize = useFlowSelector((context) => context.viewportSize);
1829
- const draggingRef = useRef(false);
1830
- const projection = getMinimapProjection({
1831
- contentBounds: flow.getGraphBounds(),
1832
- viewport,
1849
+ const markerGroupRef = useRef(null);
1850
+ const viewportRectRef = useRef(null);
1851
+ const sessionRef = useRef(null);
1852
+ const endSessionRef = useRef(null);
1853
+ const paintViewportRef = useRef(() => {});
1854
+ const contentBounds = useMemo(() => {
1855
+ return flow.getGraphBounds();
1856
+ }, [
1857
+ flow,
1858
+ nodes,
1859
+ measurements
1860
+ ]);
1861
+ const nodeBounds = useMemo(() => {
1862
+ return nodes.map((node) => flow.internals.engine.visualBounds(node.id));
1863
+ }, [
1864
+ flow,
1865
+ nodes,
1866
+ measurements
1867
+ ]);
1868
+ const baseProjection = useMemo(() => getMinimapProjection({
1869
+ contentBounds,
1870
+ viewport: flow.getSnapshot().viewport,
1833
1871
  viewportSize,
1834
1872
  width,
1835
1873
  height,
1836
1874
  padding: props.padding
1837
- });
1838
- const jumpTo = (event) => {
1875
+ }), [
1876
+ contentBounds,
1877
+ flow,
1878
+ height,
1879
+ props.padding,
1880
+ viewportSize,
1881
+ width
1882
+ ]);
1883
+ const projectedNodes = useMemo(() => nodeBounds.map((bounds) => baseProjection.rectToMinimap(bounds)), [baseProjection, nodeBounds]);
1884
+ const initialViewport = flow.getSnapshot().viewport;
1885
+ const viewportRect = baseProjection.rectToMinimap(getVisibleBounds(initialViewport, viewportSize));
1886
+ paintViewportRef.current = (nextViewport) => {
1887
+ const projection = sessionRef.current?.projection ?? getMinimapProjection({
1888
+ contentBounds,
1889
+ viewport: nextViewport,
1890
+ viewportSize,
1891
+ width,
1892
+ height,
1893
+ padding: props.padding
1894
+ });
1895
+ markerGroupRef.current?.setAttribute("transform", projectionTransform(baseProjection, projection));
1896
+ setRect(viewportRectRef.current, projection.rectToMinimap(getVisibleBounds(nextViewport, viewportSize)));
1897
+ };
1898
+ useLayoutEffect(() => {
1899
+ paintViewportRef.current(flow.getSnapshot().viewport);
1900
+ }, [
1901
+ baseProjection,
1902
+ flow,
1903
+ nodeBounds
1904
+ ]);
1905
+ useEffect(() => flow.listen((context) => context.viewport, (nextViewport) => paintViewportRef.current(nextViewport)), [flow]);
1906
+ useEffect(() => () => endSessionRef.current?.(), []);
1907
+ const pointFromEvent = (event, activeProjection) => {
1839
1908
  const rect = event.currentTarget.getBoundingClientRect();
1840
- const canvasPoint = projection.toCanvas({
1909
+ return activeProjection.toCanvas({
1841
1910
  x: event.clientX - rect.left,
1842
1911
  y: event.clientY - rect.top
1843
1912
  });
1844
- flow.viewport.set(viewportCenteredOn(canvasPoint, viewportSize, viewport.zoom));
1913
+ };
1914
+ const navigate = (event, session) => {
1915
+ const viewport = flow.getSnapshot().viewport;
1916
+ const visibleBounds = getVisibleBounds(viewport, viewportSize);
1917
+ const center = getMinimapViewportCenter({
1918
+ point: pointFromEvent(event, session.projection),
1919
+ grabOffset: session.grabOffset,
1920
+ contentBounds,
1921
+ viewportBounds: visibleBounds
1922
+ });
1923
+ flow.viewport.set(viewportCenteredOn(center, viewportSize, viewport.zoom));
1845
1924
  };
1846
1925
  return /* @__PURE__ */ jsxs("svg", {
1847
1926
  "aria-label": "minimap",
@@ -1849,19 +1928,84 @@ function MiniMap(props) {
1849
1928
  height,
1850
1929
  className: props.className,
1851
1930
  onPointerDown: (event) => {
1852
- draggingRef.current = true;
1853
- jumpTo(event);
1931
+ if (event.button !== 0 || nodes.length === 0) return;
1932
+ endSessionRef.current?.();
1933
+ const viewport = flow.getSnapshot().viewport;
1934
+ const currentProjection = getMinimapProjection({
1935
+ contentBounds,
1936
+ viewport,
1937
+ viewportSize,
1938
+ width,
1939
+ height,
1940
+ padding: props.padding
1941
+ });
1942
+ const pointerPoint = pointFromEvent(event, currentProjection);
1943
+ const visibleBounds = getVisibleBounds(viewport, viewportSize);
1944
+ const viewportCenter = {
1945
+ x: visibleBounds.x + visibleBounds.width / 2,
1946
+ y: visibleBounds.y + visibleBounds.height / 2
1947
+ };
1948
+ const grabbedLens = containsPoint(visibleBounds, pointerPoint);
1949
+ const session = {
1950
+ pointerId: event.pointerId,
1951
+ originClient: {
1952
+ x: event.clientX,
1953
+ y: event.clientY
1954
+ },
1955
+ grabOffset: grabbedLens ? {
1956
+ x: pointerPoint.x - viewportCenter.x,
1957
+ y: pointerPoint.y - viewportCenter.y
1958
+ } : {
1959
+ x: 0,
1960
+ y: 0
1961
+ },
1962
+ projection: currentProjection
1963
+ };
1964
+ sessionRef.current = session;
1965
+ const element = event.currentTarget;
1966
+ const body = element.ownerDocument.body;
1967
+ const finish = (pointerId) => {
1968
+ if (pointerId !== void 0 && pointerId !== session.pointerId) return;
1969
+ if (sessionRef.current?.pointerId !== session.pointerId) return;
1970
+ sessionRef.current = null;
1971
+ endSessionRef.current = null;
1972
+ body.removeEventListener("pointerup", onPointerEnd);
1973
+ body.removeEventListener("pointercancel", onPointerEnd);
1974
+ body.removeEventListener("contextmenu", onContextMenu, true);
1975
+ try {
1976
+ if (element.hasPointerCapture?.(session.pointerId)) element.releasePointerCapture(session.pointerId);
1977
+ } catch {}
1978
+ paintViewportRef.current(flow.getSnapshot().viewport);
1979
+ };
1980
+ const onPointerEnd = (pointerEvent) => finish(pointerEvent.pointerId);
1981
+ const onContextMenu = () => finish();
1982
+ endSessionRef.current = () => finish();
1983
+ body.addEventListener("pointerup", onPointerEnd);
1984
+ body.addEventListener("pointercancel", onPointerEnd);
1985
+ body.addEventListener("contextmenu", onContextMenu, true);
1854
1986
  try {
1855
- event.currentTarget.setPointerCapture(event.pointerId);
1987
+ element.setPointerCapture(event.pointerId);
1856
1988
  } catch {}
1989
+ if (!grabbedLens) navigate(event, session);
1857
1990
  },
1858
1991
  onPointerMove: (event) => {
1859
- if (draggingRef.current) jumpTo(event);
1992
+ const session = sessionRef.current;
1993
+ if (!session || event.pointerId !== session.pointerId) return;
1994
+ const dx = event.clientX - session.originClient.x;
1995
+ const dy = event.clientY - session.originClient.y;
1996
+ if (dx * dx + dy * dy <= DRAG_JITTER_THRESHOLD_SQ) return;
1997
+ navigate(event, session);
1860
1998
  },
1861
1999
  onPointerUp: (event) => {
1862
- draggingRef.current = false;
1863
- if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
2000
+ if (sessionRef.current?.pointerId === event.pointerId) endSessionRef.current?.();
1864
2001
  },
2002
+ onPointerCancel: (event) => {
2003
+ if (sessionRef.current?.pointerId === event.pointerId) endSessionRef.current?.();
2004
+ },
2005
+ onLostPointerCapture: (event) => {
2006
+ if (sessionRef.current?.pointerId === event.pointerId) endSessionRef.current?.();
2007
+ },
2008
+ onContextMenu: () => endSessionRef.current?.(),
1865
2009
  style: {
1866
2010
  position: "absolute",
1867
2011
  right: 12,
@@ -1874,21 +2018,27 @@ function MiniMap(props) {
1874
2018
  pointerEvents: "auto",
1875
2019
  ...props.style
1876
2020
  },
1877
- children: [nodes.map((node) => {
1878
- const rect = projection.rectToMinimap(flow.internals.engine.visualBounds(node.id));
1879
- return props.renderNode?.(node, rect) ?? /* @__PURE__ */ jsx("rect", {
1880
- x: rect.x,
1881
- y: rect.y,
1882
- width: Math.max(rect.width, 1),
1883
- height: Math.max(rect.height, 1),
1884
- rx: 1,
1885
- fill: "#94a3b8"
1886
- }, node.id);
2021
+ children: [/* @__PURE__ */ jsx("g", {
2022
+ ref: markerGroupRef,
2023
+ children: nodes.map((node, index) => {
2024
+ const rect = projectedNodes[index];
2025
+ return /* @__PURE__ */ jsx(Fragment, { children: props.renderNode?.(node, rect) ?? /* @__PURE__ */ jsx("rect", {
2026
+ "data-flow-minimap-node": node.id,
2027
+ x: rect.x,
2028
+ y: rect.y,
2029
+ width: Math.max(rect.width, 1),
2030
+ height: Math.max(rect.height, 1),
2031
+ rx: 1,
2032
+ fill: "#94a3b8"
2033
+ }) }, node.id);
2034
+ })
1887
2035
  }), /* @__PURE__ */ jsx("rect", {
1888
- x: projection.viewportRect.x,
1889
- y: projection.viewportRect.y,
1890
- width: projection.viewportRect.width,
1891
- height: projection.viewportRect.height,
2036
+ ref: viewportRectRef,
2037
+ "data-flow-minimap-viewport": "",
2038
+ x: viewportRect.x,
2039
+ y: viewportRect.y,
2040
+ width: viewportRect.width,
2041
+ height: viewportRect.height,
1892
2042
  fill: "rgba(37, 99, 235, 0.08)",
1893
2043
  stroke: "#2563eb",
1894
2044
  strokeWidth: 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/flow-react",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
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.9.0",
38
- "@statelyai/flow-dom": "3.4.3"
37
+ "@statelyai/flow": "0.10.0",
38
+ "@statelyai/flow-dom": "3.4.5"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "react": "^18.2.0 || ^19.0.0",