@canmingir/link 1.2.61 → 1.2.63

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canmingir/link",
3
- "version": "1.2.61",
3
+ "version": "1.2.63",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,21 +1,32 @@
1
1
  import { Box, alpha } from "@mui/material";
2
- import React, { useMemo, useState } from "react";
2
+ import React, { forwardRef, useMemo, useState } from "react";
3
3
  import { assertLinkedGraph, buildTreeFromLinked } from "../utils/flowUtils";
4
4
 
5
5
  import FlowNode from "./FlowNode";
6
6
  import { useGraphOperations } from "../hooks/useGraphOperations";
7
7
 
8
- export const Flow = ({
9
- data,
10
- variant = "simple",
11
- style,
12
- plugin,
13
- editable = false,
14
- onChange,
15
- height,
16
- initialZoom,
17
- centered = false,
18
- }) => {
8
+ export const Flow = forwardRef(function Flow(
9
+ {
10
+ data,
11
+ variant = "simple",
12
+ style,
13
+ plugin,
14
+ editable = false,
15
+ onChange,
16
+ height,
17
+ initialZoom,
18
+ centered = false,
19
+ minZoom,
20
+ maxZoom,
21
+ fitViewPadding,
22
+ fitViewMaxZoom,
23
+ fitViewOnMount,
24
+ fitViewOnResize,
25
+ fitViewOnNodesChange,
26
+ onInit,
27
+ },
28
+ ref,
29
+ ) {
19
30
  const [floatingNodes, setFloatingNodes] = useState([]);
20
31
 
21
32
  const { nodesById, roots } = useMemo(() => assertLinkedGraph(data), [data]);
@@ -79,6 +90,7 @@ export const Flow = ({
79
90
  }}
80
91
  >
81
92
  <FlowNode
93
+ ref={ref}
82
94
  node={treeData}
83
95
  variant={variant}
84
96
  style={style}
@@ -92,9 +104,17 @@ export const Flow = ({
92
104
  height={height}
93
105
  initialZoom={initialZoom}
94
106
  centered={centered}
107
+ minZoom={minZoom}
108
+ maxZoom={maxZoom}
109
+ fitViewPadding={fitViewPadding}
110
+ fitViewMaxZoom={fitViewMaxZoom}
111
+ fitViewOnMount={fitViewOnMount}
112
+ fitViewOnResize={fitViewOnResize}
113
+ fitViewOnNodesChange={fitViewOnNodesChange}
114
+ onInit={onInit}
95
115
  />
96
116
  </Box>
97
117
  );
98
- };
118
+ });
99
119
 
100
120
  export default Flow;
@@ -1,20 +1,31 @@
1
- import React, { useMemo } from "react";
1
+ import React, { forwardRef, useMemo } from "react";
2
2
 
3
3
  import FlowBoardItem from "./FlowBoardItem";
4
4
  import FlowViewport from "./FlowViewport";
5
5
  import { SelectionProvider } from "../selection/SelectionContext";
6
6
  import { getBaseStyleForVariant } from "../styles";
7
7
 
8
- export const FlowBoard = ({
9
- flows = [],
10
- variant = "simple",
11
- style,
12
- plugin,
13
- initialZoom = 1,
14
- height = "100vh",
15
- gap = 480,
16
- onFlowPositionChange,
17
- }) => {
8
+ export const FlowBoard = forwardRef(function FlowBoard(
9
+ {
10
+ flows = [],
11
+ variant = "simple",
12
+ style,
13
+ plugin,
14
+ initialZoom = 1,
15
+ height = "100vh",
16
+ gap = 480,
17
+ onFlowPositionChange,
18
+ minZoom,
19
+ maxZoom,
20
+ fitViewPadding,
21
+ fitViewMaxZoom,
22
+ fitViewOnMount,
23
+ fitViewOnResize,
24
+ fitViewOnNodesChange,
25
+ onInit,
26
+ },
27
+ ref,
28
+ ) {
18
29
  const baseStyle = getBaseStyleForVariant(variant);
19
30
  const selectionColor = baseStyle.selectionColor ?? "#64748b";
20
31
 
@@ -44,6 +55,7 @@ export const FlowBoard = ({
44
55
  return (
45
56
  <SelectionProvider>
46
57
  <FlowViewport
58
+ ref={ref}
47
59
  selectionColor={selectionColor}
48
60
  nodesById={mergedNodesById}
49
61
  variant={variant}
@@ -51,6 +63,14 @@ export const FlowBoard = ({
51
63
  plugin={plugin}
52
64
  height={height}
53
65
  initialZoom={initialZoom}
66
+ minZoom={minZoom}
67
+ maxZoom={maxZoom}
68
+ fitViewPadding={fitViewPadding}
69
+ fitViewMaxZoom={fitViewMaxZoom}
70
+ fitViewOnMount={fitViewOnMount}
71
+ fitViewOnResize={fitViewOnResize}
72
+ fitViewOnNodesChange={fitViewOnNodesChange}
73
+ onInit={onInit}
54
74
  >
55
75
  {positionedFlows.map(({ flow, id, position }) => (
56
76
  <FlowBoardItem
@@ -73,6 +93,6 @@ export const FlowBoard = ({
73
93
  </FlowViewport>
74
94
  </SelectionProvider>
75
95
  );
76
- };
96
+ });
77
97
 
78
98
  export default FlowBoard;
@@ -1,26 +1,37 @@
1
1
  import FlowNodeView from "../nodes/FlowNodeView";
2
2
  import FlowViewport from "./FlowViewport";
3
- import React from "react";
3
+ import React, { forwardRef } from "react";
4
4
  import { SelectionProvider } from "../selection/SelectionContext";
5
5
  import { getBaseStyleForVariant } from "../styles";
6
6
 
7
- const FlowNode = ({
8
- isRoot = false,
9
- onAddNode,
10
- variant,
11
- nodesById,
12
- onPaste,
13
- onCut,
14
- onConnect,
15
- floatingNodes,
16
- style,
17
- plugin,
18
- node,
19
- height,
20
- initialZoom,
21
- centered,
22
- ...props
23
- }) => {
7
+ const FlowNode = forwardRef(function FlowNode(
8
+ {
9
+ isRoot = false,
10
+ onAddNode,
11
+ variant,
12
+ nodesById,
13
+ onPaste,
14
+ onCut,
15
+ onConnect,
16
+ floatingNodes,
17
+ style,
18
+ plugin,
19
+ node,
20
+ height,
21
+ initialZoom,
22
+ centered,
23
+ minZoom,
24
+ maxZoom,
25
+ fitViewPadding,
26
+ fitViewMaxZoom,
27
+ fitViewOnMount,
28
+ fitViewOnResize,
29
+ fitViewOnNodesChange,
30
+ onInit,
31
+ ...props
32
+ },
33
+ ref,
34
+ ) {
24
35
  if (!isRoot) {
25
36
  if (!node) return null;
26
37
  return (
@@ -42,6 +53,7 @@ const FlowNode = ({
42
53
  return (
43
54
  <SelectionProvider>
44
55
  <FlowViewport
56
+ ref={ref}
45
57
  selectionColor={selectionColor}
46
58
  nodesById={nodesById}
47
59
  onPaste={onPaste}
@@ -54,6 +66,14 @@ const FlowNode = ({
54
66
  height={height}
55
67
  initialZoom={initialZoom}
56
68
  centered={centered}
69
+ minZoom={minZoom}
70
+ maxZoom={maxZoom}
71
+ fitViewPadding={fitViewPadding}
72
+ fitViewMaxZoom={fitViewMaxZoom}
73
+ fitViewOnMount={fitViewOnMount}
74
+ fitViewOnResize={fitViewOnResize}
75
+ fitViewOnNodesChange={fitViewOnNodesChange}
76
+ onInit={onInit}
57
77
  >
58
78
  {node && (
59
79
  <FlowNodeView
@@ -69,6 +89,6 @@ const FlowNode = ({
69
89
  </FlowViewport>
70
90
  </SelectionProvider>
71
91
  );
72
- };
92
+ });
73
93
 
74
94
  export default FlowNode;
@@ -1,28 +1,55 @@
1
- import React, { useEffect, useRef, useState } from "react";
1
+ import {
2
+ DEFAULT_FIT_VIEW_PADDING,
3
+ DEFAULT_MAX_ZOOM,
4
+ DEFAULT_MIN_ZOOM,
5
+ clampZoomValue,
6
+ computeFitViewState,
7
+ } from "../utils/viewportUtils";
8
+ import React, {
9
+ forwardRef,
10
+ useCallback,
11
+ useEffect,
12
+ useImperativeHandle,
13
+ useRef,
14
+ useState,
15
+ } from "react";
2
16
 
3
17
  import { Box } from "@mui/material";
4
18
  import FloatingGraph from "../graph/FloatingGraph";
5
19
  import SelectionOverlay from "../selection/SelectionOverlay";
6
20
  import { useSelection } from "../selection/SelectionContext";
7
21
 
8
- const FlowViewport = ({
9
- children,
10
- selectionColor = "#64748b",
11
- nodesById,
12
- onPaste,
13
- onCut,
14
- onConnect,
15
- floatingNodes = [],
16
- variant,
17
- style,
18
- plugin,
19
- height = "100vh",
20
- initialZoom = 1,
21
- centered = false,
22
- sx = {},
23
- ...rest
24
- }) => {
25
- const clampZoom = (zoom) => Math.min(2.5, Math.max(0.25, zoom));
22
+ const FlowViewport = forwardRef(function FlowViewport(
23
+ {
24
+ children,
25
+ selectionColor = "#64748b",
26
+ nodesById,
27
+ onPaste,
28
+ onCut,
29
+ onConnect,
30
+ floatingNodes = [],
31
+ variant,
32
+ style,
33
+ plugin,
34
+ height = "100vh",
35
+ initialZoom = 1,
36
+ centered = false,
37
+ minZoom = DEFAULT_MIN_ZOOM,
38
+ maxZoom = DEFAULT_MAX_ZOOM,
39
+ fitViewPadding = DEFAULT_FIT_VIEW_PADDING,
40
+ fitViewMaxZoom = 1,
41
+ fitViewOnMount = false,
42
+ fitViewOnResize = false,
43
+ fitViewOnNodesChange = false,
44
+ onInit,
45
+ sx = {},
46
+ ...rest
47
+ },
48
+ ref,
49
+ ) {
50
+ const clampZoom = (z) => clampZoomValue(z, minZoom, maxZoom);
51
+
52
+ const usesFitView = fitViewOnMount || fitViewOnResize || fitViewOnNodesChange;
26
53
 
27
54
  const [offset, setOffset] = useState({ x: 0, y: 0 });
28
55
  const [zoom, setZoom] = useState(initialZoom);
@@ -46,7 +73,7 @@ const FlowViewport = ({
46
73
  } = useSelection();
47
74
 
48
75
  useEffect(() => {
49
- if (centered) return;
76
+ if (centered || usesFitView) return;
50
77
 
51
78
  const container = containerRef.current;
52
79
  const inner = innerRef.current;
@@ -67,7 +94,123 @@ const FlowViewport = ({
67
94
  observer.observe(container);
68
95
 
69
96
  return () => observer.disconnect();
70
- }, [centered]);
97
+ }, [centered, usesFitView]);
98
+
99
+ const runFitView = useCallback(
100
+ (options = {}) => {
101
+ const fit = computeFitViewState(containerRef.current, {
102
+ zoom,
103
+ offset,
104
+ padding: options.padding ?? fitViewPadding,
105
+ minZoom: options.minZoom ?? minZoom,
106
+ maxZoom: options.maxZoom ?? fitViewMaxZoom,
107
+ });
108
+ if (!fit) return;
109
+ setZoom(fit.zoom);
110
+ setOffset(fit.offset);
111
+ },
112
+ [zoom, offset, fitViewPadding, minZoom, fitViewMaxZoom],
113
+ );
114
+
115
+ const zoomIn = useCallback(
116
+ (step = 1.2) => setZoom((z) => clampZoom(z * step)),
117
+ [minZoom, maxZoom],
118
+ );
119
+
120
+ const zoomOut = useCallback(
121
+ (step = 1.2) => setZoom((z) => clampZoom(z / step)),
122
+ [minZoom, maxZoom],
123
+ );
124
+
125
+ const setZoomPublic = useCallback(
126
+ (z) => setZoom(clampZoom(z)),
127
+ [minZoom, maxZoom],
128
+ );
129
+
130
+ const setCenter = useCallback(
131
+ (x, y, options = {}) => {
132
+ const targetZoom = clampZoom(options.zoom ?? zoom);
133
+ setZoom(targetZoom);
134
+ setOffset({ x: -targetZoom * x, y: -targetZoom * y });
135
+ },
136
+ [zoom, minZoom, maxZoom],
137
+ );
138
+
139
+ const getZoom = useCallback(() => zoom, [zoom]);
140
+
141
+ const runFitViewRef = useRef(runFitView);
142
+ useEffect(() => {
143
+ runFitViewRef.current = runFitView;
144
+ }, [runFitView]);
145
+
146
+ useImperativeHandle(
147
+ ref,
148
+ () => ({
149
+ fitView: runFitView,
150
+ zoomIn,
151
+ zoomOut,
152
+ setZoom: setZoomPublic,
153
+ setCenter,
154
+ getZoom,
155
+ }),
156
+ [runFitView, zoomIn, zoomOut, setZoomPublic, setCenter, getZoom],
157
+ );
158
+
159
+ useEffect(() => {
160
+ if (!fitViewOnMount) return;
161
+ runFitView();
162
+ // eslint-disable-next-line react-hooks/exhaustive-deps
163
+ }, []);
164
+
165
+ useEffect(() => {
166
+ if (!fitViewOnResize) return;
167
+
168
+ const container = containerRef.current;
169
+ if (!container) return;
170
+
171
+ let frame = null;
172
+ let isFirstCallback = true;
173
+ const observer = new ResizeObserver(() => {
174
+ if (isFirstCallback) {
175
+ isFirstCallback = false;
176
+ return;
177
+ }
178
+ if (frame) cancelAnimationFrame(frame);
179
+ frame = requestAnimationFrame(() => runFitViewRef.current());
180
+ });
181
+
182
+ observer.observe(container);
183
+
184
+ return () => {
185
+ if (frame) cancelAnimationFrame(frame);
186
+ observer.disconnect();
187
+ };
188
+ }, [fitViewOnResize]);
189
+
190
+ const nodeCount = nodesById ? Object.keys(nodesById).length : 0;
191
+ const previousNodeCountRef = useRef(nodeCount);
192
+
193
+ useEffect(() => {
194
+ if (!fitViewOnNodesChange) return;
195
+
196
+ if (previousNodeCountRef.current !== nodeCount) {
197
+ previousNodeCountRef.current = nodeCount;
198
+ const frame = requestAnimationFrame(() => runFitViewRef.current());
199
+ return () => cancelAnimationFrame(frame);
200
+ }
201
+ }, [fitViewOnNodesChange, nodeCount]);
202
+
203
+ useEffect(() => {
204
+ onInit?.({
205
+ fitView: runFitView,
206
+ zoomIn,
207
+ zoomOut,
208
+ setZoom: setZoomPublic,
209
+ setCenter,
210
+ getZoom,
211
+ });
212
+ // eslint-disable-next-line react-hooks/exhaustive-deps
213
+ }, []);
71
214
 
72
215
  useEffect(() => {
73
216
  const handleMouseMove = (e) => {
@@ -313,11 +456,19 @@ const FlowViewport = ({
313
456
  height: height,
314
457
  display: "flex",
315
458
  alignItems: "center",
316
- justifyContent: centered || shouldCenter ? "center" : "flex-start",
459
+ justifyContent:
460
+ centered || (!usesFitView && shouldCenter)
461
+ ? "center"
462
+ : "flex-start",
317
463
  transition: isDragging ? "none" : "transform 0.1s ease-out",
318
464
  pointerEvents: "auto",
319
465
  position: "relative",
320
- pl: centered ? 0 : variant === "horizontal" ? 4 : 0,
466
+ pl:
467
+ centered || (!usesFitView && shouldCenter)
468
+ ? 0
469
+ : variant === "horizontal"
470
+ ? 4
471
+ : 0,
321
472
  }}
322
473
  >
323
474
  {children}
@@ -340,6 +491,6 @@ const FlowViewport = ({
340
491
  </Box>
341
492
  </Box>
342
493
  );
343
- };
494
+ });
344
495
 
345
496
  export default FlowViewport;
@@ -1,8 +1,8 @@
1
- import React from "react";
2
-
3
1
  import { Box, Card, Stack, Typography } from "@mui/material";
4
2
  import { alpha, useTheme } from "@mui/material/styles";
5
3
 
4
+ import React from "react";
5
+
6
6
  const STYLES = {
7
7
  width: "100%",
8
8
  display: "-webkit-box",
@@ -158,7 +158,7 @@ export function SideStripeCard({
158
158
  const theme = useTheme();
159
159
 
160
160
  return (
161
- <Box sx={{ cursor: "pointer" }}>
161
+ <Box sx={{ cursor: "inherit" }}>
162
162
  <Card
163
163
  sx={{
164
164
  display: "flex",
@@ -315,7 +315,7 @@ export function HeaderCard({
315
315
  left: 0,
316
316
  background: `linear-gradient(135deg, ${alpha(
317
317
  theme.palette.secondary.light,
318
- 0.2
318
+ 0.2,
319
319
  )}, ${alpha(theme.palette.primary.main, 0.3)})`,
320
320
  borderRadius: "8px 8px 0 0",
321
321
  transition: "height 0.3s ease-in-out",
@@ -369,7 +369,7 @@ export function AvatarRoleCard({
369
369
  left: 0,
370
370
  background: `linear-gradient(135deg, ${alpha(
371
371
  theme.palette.secondary.light,
372
- 0.2
372
+ 0.2,
373
373
  )}, ${alpha(theme.palette.primary.main, 0.3)})`,
374
374
  borderRadius: "8px 8px 0 0",
375
375
  transition: "height 0.3s ease-in-out",
@@ -385,8 +385,9 @@ export function AvatarRoleCard({
385
385
  direction="row"
386
386
  sx={{
387
387
  justifyContent: "space-between",
388
- alignItems: "flex-start"
389
- }}>
388
+ alignItems: "flex-start",
389
+ }}
390
+ >
390
391
  {leftContent}
391
392
  {topRightContent}
392
393
  </Stack>
@@ -1,8 +1,8 @@
1
- import { Iconify } from "@canmingir/link/platform/components";
2
-
3
1
  import React, { useEffect, useState } from "react";
4
2
  import { alpha, styled } from "@mui/material/styles";
5
3
 
4
+ import { Iconify } from "@canmingir/link/platform/components";
5
+
6
6
  const ANIMATION_DELAY_MS = 200;
7
7
 
8
8
  const MainContainer = styled("div", {
@@ -33,7 +33,7 @@ const MainContainer = styled("div", {
33
33
  border: `1px solid ${borderColor}`,
34
34
  backdropFilter: "blur(10px)",
35
35
  transition: "all 0.4s cubic-bezier(0.4, 0, 0.2, 1)",
36
- cursor: "pointer",
36
+ cursor: "inherit",
37
37
  position: "relative",
38
38
  overflow: "hidden",
39
39
  minWidth,
@@ -0,0 +1,81 @@
1
+ export const DEFAULT_MIN_ZOOM = 0.25;
2
+ export const DEFAULT_MAX_ZOOM = 2.5;
3
+ export const DEFAULT_FIT_VIEW_PADDING = 40;
4
+
5
+ export const clampZoomValue = (zoom, minZoom, maxZoom) =>
6
+ Math.min(maxZoom, Math.max(minZoom, zoom));
7
+
8
+ export const computeFitTransform = ({
9
+ bounds,
10
+ containerWidth,
11
+ containerHeight,
12
+ padding = DEFAULT_FIT_VIEW_PADDING,
13
+ minZoom = DEFAULT_MIN_ZOOM,
14
+ maxZoom = DEFAULT_MAX_ZOOM,
15
+ }) => {
16
+ const availableWidth = Math.max(0, containerWidth - 2 * padding);
17
+ const availableHeight = Math.max(0, containerHeight - 2 * padding);
18
+
19
+ const scaleX = bounds.width > 0 ? availableWidth / bounds.width : Infinity;
20
+ const scaleY = bounds.height > 0 ? availableHeight / bounds.height : Infinity;
21
+ const rawZoom = Math.min(scaleX, scaleY);
22
+
23
+ const zoom = clampZoomValue(rawZoom, minZoom, maxZoom);
24
+
25
+ const centerX = bounds.x + bounds.width / 2;
26
+ const centerY = bounds.y + bounds.height / 2;
27
+
28
+ return {
29
+ zoom,
30
+ offset: {
31
+ x: -zoom * centerX,
32
+ y: -zoom * centerY,
33
+ },
34
+ };
35
+ };
36
+
37
+ export const computeContentBounds = (containerEl, { zoom, offset }) => {
38
+ if (!containerEl) return null;
39
+
40
+ const nodeEls = containerEl.querySelectorAll("[data-node-id]");
41
+ if (nodeEls.length === 0) return null;
42
+
43
+ const containerRect = containerEl.getBoundingClientRect();
44
+ const originX = containerRect.left + containerRect.width / 2;
45
+ const originY = containerRect.top + containerRect.height / 2;
46
+
47
+ const toContentX = (screenX) => (screenX - originX - offset.x) / zoom;
48
+ const toContentY = (screenY) => (screenY - originY - offset.y) / zoom;
49
+
50
+ let minX = Infinity;
51
+ let minY = Infinity;
52
+ let maxX = -Infinity;
53
+ let maxY = -Infinity;
54
+
55
+ nodeEls.forEach((el) => {
56
+ const rect = el.getBoundingClientRect();
57
+ minX = Math.min(minX, toContentX(rect.left));
58
+ maxX = Math.max(maxX, toContentX(rect.right));
59
+ minY = Math.min(minY, toContentY(rect.top));
60
+ maxY = Math.max(maxY, toContentY(rect.bottom));
61
+ });
62
+
63
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
64
+ };
65
+
66
+ export const computeFitViewState = (
67
+ containerEl,
68
+ { zoom, offset, padding, minZoom, maxZoom },
69
+ ) => {
70
+ const bounds = computeContentBounds(containerEl, { zoom, offset });
71
+ if (!bounds) return null;
72
+
73
+ return computeFitTransform({
74
+ bounds,
75
+ containerWidth: containerEl.clientWidth,
76
+ containerHeight: containerEl.clientHeight,
77
+ padding,
78
+ minZoom,
79
+ maxZoom,
80
+ });
81
+ };
@@ -0,0 +1,173 @@
1
+ import { Button, Stack } from "@mui/material";
2
+ import React, { useRef, useState } from "react";
3
+
4
+ import { Flow } from "../lib/Flow";
5
+
6
+ const treeToLinked = (tree) => {
7
+ if (!tree) return { nodes: {}, roots: [] };
8
+
9
+ const nodes = {};
10
+ const roots = [];
11
+
12
+ const walk = (node, parentId = null) => {
13
+ if (!node || !node.id) return;
14
+
15
+ const { children, ...rest } = node;
16
+ if (!nodes[node.id]) {
17
+ nodes[node.id] = { ...rest, id: node.id };
18
+ }
19
+
20
+ const kids = Array.isArray(children) ? children : [];
21
+ const nextIds = kids.map((c) => c.id).filter(Boolean);
22
+
23
+ if (nextIds.length === 1) {
24
+ nodes[node.id].next = nextIds[0];
25
+ } else if (nextIds.length > 1) {
26
+ nodes[node.id].next = nextIds;
27
+ }
28
+
29
+ if (parentId) {
30
+ nodes[node.id].previous = parentId;
31
+ } else if (!roots.includes(node.id)) {
32
+ roots.push(node.id);
33
+ }
34
+
35
+ kids.forEach((child) => walk(child, node.id));
36
+ };
37
+
38
+ walk(tree);
39
+ return { nodes, roots };
40
+ };
41
+
42
+ export default {
43
+ title: "Components/FlowFitView",
44
+ component: Flow,
45
+ parameters: {
46
+ layout: "fullscreen",
47
+ },
48
+ };
49
+
50
+ const wideTree = {
51
+ id: "root",
52
+ label: "Root",
53
+ children: [
54
+ {
55
+ id: "branch1",
56
+ label: "Branch 1",
57
+ children: [
58
+ { id: "leaf1-1", label: "Leaf 1.1", children: [] },
59
+ { id: "leaf1-2", label: "Leaf 1.2", children: [] },
60
+ ],
61
+ },
62
+ {
63
+ id: "branch2",
64
+ label: "Branch 2",
65
+ children: [
66
+ { id: "leaf2-1", label: "Leaf 2.1", children: [] },
67
+ { id: "leaf2-2", label: "Leaf 2.2", children: [] },
68
+ { id: "leaf2-3", label: "Leaf 2.3", children: [] },
69
+ ],
70
+ },
71
+ {
72
+ id: "branch3",
73
+ label: "Branch 3",
74
+ children: [{ id: "leaf3-1", label: "Leaf 3.1", children: [] }],
75
+ },
76
+ ],
77
+ };
78
+
79
+ export const FitViewDemo = {
80
+ render: () => {
81
+ const flowRef = useRef(null);
82
+ const data = treeToLinked(wideTree);
83
+
84
+ return (
85
+ <Stack sx={{ height: "100vh" }}>
86
+ <Stack direction="row" spacing={1} sx={{ p: 1 }}>
87
+ <Button
88
+ variant="contained"
89
+ onClick={() => flowRef.current?.fitView()}
90
+ >
91
+ Fit View
92
+ </Button>
93
+ <Button variant="outlined" onClick={() => flowRef.current?.zoomIn()}>
94
+ Zoom In
95
+ </Button>
96
+ <Button
97
+ variant="outlined"
98
+ onClick={() => flowRef.current?.zoomOut()}
99
+ >
100
+ Zoom Out
101
+ </Button>
102
+ </Stack>
103
+ <Flow
104
+ ref={flowRef}
105
+ data={data}
106
+ variant="simple"
107
+ height="calc(100vh - 56px)"
108
+ minZoom={0.25}
109
+ maxZoom={2.5}
110
+ fitViewPadding={60}
111
+ fitViewMaxZoom={1}
112
+ fitViewOnMount
113
+ />
114
+ </Stack>
115
+ );
116
+ },
117
+ };
118
+
119
+ export const FitViewOnNodesChangeDemo = {
120
+ render: () => {
121
+ const flowRef = useRef(null);
122
+ const [tree, setTree] = useState(wideTree);
123
+ const data = treeToLinked(tree);
124
+
125
+ const addLeafToBranch1 = () => {
126
+ setTree((prev) => {
127
+ const branch1 = prev.children.find((c) => c.id === "branch1");
128
+ const nextIndex = branch1.children.length + 1;
129
+ const newLeaf = {
130
+ id: `leaf1-${nextIndex}-${Date.now()}`,
131
+ label: `Leaf 1.${nextIndex}`,
132
+ children: [],
133
+ };
134
+ return {
135
+ ...prev,
136
+ children: prev.children.map((c) =>
137
+ c.id === "branch1"
138
+ ? { ...c, children: [...c.children, newLeaf] }
139
+ : c,
140
+ ),
141
+ };
142
+ });
143
+ };
144
+
145
+ return (
146
+ <Stack sx={{ height: "100vh" }}>
147
+ <Stack direction="row" spacing={1} sx={{ p: 1 }}>
148
+ <Button variant="contained" onClick={addLeafToBranch1}>
149
+ Add Node
150
+ </Button>
151
+ <Button
152
+ variant="outlined"
153
+ onClick={() => flowRef.current?.fitView()}
154
+ >
155
+ Fit View
156
+ </Button>
157
+ </Stack>
158
+ <Flow
159
+ ref={flowRef}
160
+ data={data}
161
+ variant="simple"
162
+ height="calc(100vh - 56px)"
163
+ minZoom={0.25}
164
+ maxZoom={2.5}
165
+ fitViewPadding={60}
166
+ fitViewMaxZoom={1}
167
+ fitViewOnMount
168
+ fitViewOnNodesChange
169
+ />
170
+ </Stack>
171
+ );
172
+ },
173
+ };