@statelyai/flow-react 0.9.4 → 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,
@@ -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"
@@ -1627,7 +1627,7 @@ function DefaultGroupNode(props) {
1627
1627
  height: rect.height
1628
1628
  });
1629
1629
  const dropTarget = props.highlights.some((entry) => entry.kind === "drop-target");
1630
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", { style: {
1630
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", { style: {
1631
1631
  position: "absolute",
1632
1632
  ...relative(geometry.childArea),
1633
1633
  boxSizing: "border-box",
@@ -1812,35 +1812,115 @@ function FlowToolbar(props) {
1812
1812
  }
1813
1813
  //#endregion
1814
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
+ }
1815
1838
  /**
1816
- * Overview map. Deliberately thin: all math lives in core
1817
- * (`getMinimapProjection`, `viewportCenteredOn`) and every interaction is a
1818
- * controller viewport command — this component is exactly what you would write in
1819
- * 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.
1820
1841
  */
1821
1842
  function MiniMap(props) {
1822
1843
  const flow = useFlowInstance();
1823
1844
  const width = props.width ?? 200;
1824
1845
  const height = props.height ?? 140;
1825
1846
  const nodes = useFlowSelector((context) => context.graph.nodes);
1826
- const viewport = useFlowSelector((context) => context.viewport);
1847
+ const measurements = useFlowSelector((context) => context.measurements);
1827
1848
  const viewportSize = useFlowSelector((context) => context.viewportSize);
1828
- const draggingRef = useRef(false);
1829
- const projection = getMinimapProjection({
1830
- contentBounds: flow.getGraphBounds(),
1831
- 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,
1832
1871
  viewportSize,
1833
1872
  width,
1834
1873
  height,
1835
1874
  padding: props.padding
1836
- });
1837
- 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) => {
1838
1908
  const rect = event.currentTarget.getBoundingClientRect();
1839
- const canvasPoint = projection.toCanvas({
1909
+ return activeProjection.toCanvas({
1840
1910
  x: event.clientX - rect.left,
1841
1911
  y: event.clientY - rect.top
1842
1912
  });
1843
- 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));
1844
1924
  };
1845
1925
  return /* @__PURE__ */ jsxs("svg", {
1846
1926
  "aria-label": "minimap",
@@ -1848,19 +1928,84 @@ function MiniMap(props) {
1848
1928
  height,
1849
1929
  className: props.className,
1850
1930
  onPointerDown: (event) => {
1851
- draggingRef.current = true;
1852
- 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);
1853
1986
  try {
1854
- event.currentTarget.setPointerCapture(event.pointerId);
1987
+ element.setPointerCapture(event.pointerId);
1855
1988
  } catch {}
1989
+ if (!grabbedLens) navigate(event, session);
1856
1990
  },
1857
1991
  onPointerMove: (event) => {
1858
- 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);
1859
1998
  },
1860
1999
  onPointerUp: (event) => {
1861
- draggingRef.current = false;
1862
- if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
2000
+ if (sessionRef.current?.pointerId === event.pointerId) endSessionRef.current?.();
1863
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?.(),
1864
2009
  style: {
1865
2010
  position: "absolute",
1866
2011
  right: 12,
@@ -1873,21 +2018,27 @@ function MiniMap(props) {
1873
2018
  pointerEvents: "auto",
1874
2019
  ...props.style
1875
2020
  },
1876
- children: [nodes.map((node) => {
1877
- const rect = projection.rectToMinimap(flow.internals.engine.visualBounds(node.id));
1878
- return props.renderNode?.(node, rect) ?? /* @__PURE__ */ jsx("rect", {
1879
- x: rect.x,
1880
- y: rect.y,
1881
- width: Math.max(rect.width, 1),
1882
- height: Math.max(rect.height, 1),
1883
- rx: 1,
1884
- fill: "#94a3b8"
1885
- }, 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
+ })
1886
2035
  }), /* @__PURE__ */ jsx("rect", {
1887
- x: projection.viewportRect.x,
1888
- y: projection.viewportRect.y,
1889
- width: projection.viewportRect.width,
1890
- 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,
1891
2042
  fill: "rgba(37, 99, 235, 0.08)",
1892
2043
  stroke: "#2563eb",
1893
2044
  strokeWidth: 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/flow-react",
3
- "version": "0.9.4",
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.1",
38
- "@statelyai/flow-dom": "3.4.4"
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",