@eventcatalog/core 3.10.2 → 3.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.
Files changed (41) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-K7EPHS7S.js → chunk-G7GG3HEB.js} +1 -1
  6. package/dist/{chunk-DGE5ITBR.js → chunk-K44BXVHU.js} +1 -1
  7. package/dist/{chunk-IJKRHWWO.js → chunk-LXOS3MXQ.js} +1 -1
  8. package/dist/{chunk-QWSUFHCT.js → chunk-VUBZ6A7B.js} +1 -1
  9. package/dist/{chunk-AL67CV2N.js → chunk-WVKLG26T.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +2 -1
  13. package/dist/eventcatalog.js +6 -5
  14. package/dist/generate.cjs +1 -1
  15. package/dist/generate.js +3 -3
  16. package/dist/utils/cli-logger.cjs +1 -1
  17. package/dist/utils/cli-logger.js +2 -2
  18. package/eventcatalog/integrations/eventcatalog-features.ts +13 -0
  19. package/eventcatalog/src/components/MDX/Design/Design.astro +10 -2
  20. package/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro +10 -2
  21. package/eventcatalog/src/components/MDX/Flow/Flow.astro +10 -2
  22. package/eventcatalog/src/components/MDX/NodeGraph/Edges/AnimatedMessageEdge.tsx +13 -0
  23. package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModeContent.tsx +294 -0
  24. package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModeNodeActions.tsx +92 -0
  25. package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModePlaceholder.tsx +26 -0
  26. package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/utils.ts +163 -0
  27. package/eventcatalog/src/components/MDX/NodeGraph/FocusModeModal.tsx +99 -0
  28. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +10 -2
  29. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +166 -43
  30. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Entity.tsx +4 -1
  31. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/MessageContextMenu.tsx +4 -1
  32. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Service.tsx +4 -1
  33. package/eventcatalog/src/components/MDX/NodeGraph/VisualizerDropdownContent.tsx +91 -2
  34. package/eventcatalog/src/enterprise/visualizer-layout/reset.ts +45 -0
  35. package/eventcatalog/src/enterprise/visualizer-layout/save.ts +57 -0
  36. package/eventcatalog/src/layouts/Footer.astro +4 -1
  37. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +3 -1
  38. package/eventcatalog/src/utils/feature.ts +2 -0
  39. package/eventcatalog/src/utils/node-graphs/layout-persistence.ts +81 -0
  40. package/eventcatalog/tailwind.config.mjs +10 -0
  41. package/package.json +1 -1
@@ -37,7 +37,7 @@ var import_axios = __toESM(require("axios"), 1);
37
37
  var import_os = __toESM(require("os"), 1);
38
38
 
39
39
  // package.json
40
- var version = "3.10.2";
40
+ var version = "3.12.0";
41
41
 
42
42
  // src/constants.ts
43
43
  var VERSION = version;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "../chunk-DGE5ITBR.js";
4
- import "../chunk-K7EPHS7S.js";
3
+ } from "../chunk-K44BXVHU.js";
4
+ import "../chunk-G7GG3HEB.js";
5
5
  export {
6
6
  raiseEvent
7
7
  };
@@ -106,7 +106,7 @@ var import_axios = __toESM(require("axios"), 1);
106
106
  var import_os = __toESM(require("os"), 1);
107
107
 
108
108
  // package.json
109
- var version = "3.10.2";
109
+ var version = "3.12.0";
110
110
 
111
111
  // src/constants.ts
112
112
  var VERSION = version;
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  log_build_default
3
- } from "../chunk-QWSUFHCT.js";
4
- import "../chunk-DGE5ITBR.js";
3
+ } from "../chunk-VUBZ6A7B.js";
4
+ import "../chunk-K44BXVHU.js";
5
5
  import "../chunk-4UVFXLPI.js";
6
- import "../chunk-K7EPHS7S.js";
6
+ import "../chunk-G7GG3HEB.js";
7
7
  import "../chunk-UPONRQSN.js";
8
8
  export {
9
9
  log_build_default as default
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "3.10.2";
2
+ var version = "3.12.0";
3
3
 
4
4
  // src/constants.ts
5
5
  var VERSION = version;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-K7EPHS7S.js";
3
+ } from "./chunk-G7GG3HEB.js";
4
4
 
5
5
  // src/analytics/analytics.js
6
6
  import axios from "axios";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  logger
3
- } from "./chunk-AL67CV2N.js";
3
+ } from "./chunk-WVKLG26T.js";
4
4
  import {
5
5
  cleanup,
6
6
  getEventCatalogConfigFile
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "./chunk-DGE5ITBR.js";
3
+ } from "./chunk-K44BXVHU.js";
4
4
  import {
5
5
  countResources,
6
6
  serializeCounts
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-K7EPHS7S.js";
3
+ } from "./chunk-G7GG3HEB.js";
4
4
 
5
5
  // src/utils/cli-logger.ts
6
6
  import pc from "picocolors";
@@ -25,7 +25,7 @@ __export(constants_exports, {
25
25
  module.exports = __toCommonJS(constants_exports);
26
26
 
27
27
  // package.json
28
- var version = "3.10.2";
28
+ var version = "3.12.0";
29
29
 
30
30
  // src/constants.ts
31
31
  var VERSION = version;
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-K7EPHS7S.js";
3
+ } from "./chunk-G7GG3HEB.js";
4
4
  export {
5
5
  VERSION
6
6
  };
@@ -109,7 +109,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
109
109
  var import_picocolors = __toESM(require("picocolors"), 1);
110
110
 
111
111
  // package.json
112
- var version = "3.10.2";
112
+ var version = "3.12.0";
113
113
 
114
114
  // src/constants.ts
115
115
  var VERSION = version;
@@ -812,6 +812,7 @@ program.command("dev").description("Run development server of EventCatalog").opt
812
812
  ENABLE_EMBED: canEmbedPages || isEventCatalogScale,
813
813
  EVENTCATALOG_STARTER: isEventCatalogStarter,
814
814
  EVENTCATALOG_SCALE: isEventCatalogScale,
815
+ EVENTCATALOG_DEV_MODE: "true",
815
816
  NODE_NO_WARNINGS: "1"
816
817
  }
817
818
  }
@@ -6,8 +6,8 @@ import {
6
6
  } from "./chunk-PLNJC7NZ.js";
7
7
  import {
8
8
  log_build_default
9
- } from "./chunk-QWSUFHCT.js";
10
- import "./chunk-DGE5ITBR.js";
9
+ } from "./chunk-VUBZ6A7B.js";
10
+ import "./chunk-K44BXVHU.js";
11
11
  import "./chunk-4UVFXLPI.js";
12
12
  import {
13
13
  runMigrations
@@ -22,13 +22,13 @@ import {
22
22
  } from "./chunk-5VBIXL6C.js";
23
23
  import {
24
24
  generate
25
- } from "./chunk-IJKRHWWO.js";
25
+ } from "./chunk-LXOS3MXQ.js";
26
26
  import {
27
27
  logger
28
- } from "./chunk-AL67CV2N.js";
28
+ } from "./chunk-WVKLG26T.js";
29
29
  import {
30
30
  VERSION
31
- } from "./chunk-K7EPHS7S.js";
31
+ } from "./chunk-G7GG3HEB.js";
32
32
  import "./chunk-UPONRQSN.js";
33
33
 
34
34
  // src/eventcatalog.ts
@@ -165,6 +165,7 @@ program.command("dev").description("Run development server of EventCatalog").opt
165
165
  ENABLE_EMBED: canEmbedPages || isEventCatalogScale,
166
166
  EVENTCATALOG_STARTER: isEventCatalogStarter,
167
167
  EVENTCATALOG_SCALE: isEventCatalogScale,
168
+ EVENTCATALOG_DEV_MODE: "true",
168
169
  NODE_NO_WARNINGS: "1"
169
170
  }
170
171
  }
package/dist/generate.cjs CHANGED
@@ -73,7 +73,7 @@ var getEventCatalogConfigFile = async (projectDirectory) => {
73
73
  var import_picocolors = __toESM(require("picocolors"), 1);
74
74
 
75
75
  // package.json
76
- var version = "3.10.2";
76
+ var version = "3.12.0";
77
77
 
78
78
  // src/constants.ts
79
79
  var VERSION = version;
package/dist/generate.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  generate
3
- } from "./chunk-IJKRHWWO.js";
4
- import "./chunk-AL67CV2N.js";
5
- import "./chunk-K7EPHS7S.js";
3
+ } from "./chunk-LXOS3MXQ.js";
4
+ import "./chunk-WVKLG26T.js";
5
+ import "./chunk-G7GG3HEB.js";
6
6
  import "./chunk-UPONRQSN.js";
7
7
  export {
8
8
  generate
@@ -36,7 +36,7 @@ module.exports = __toCommonJS(cli_logger_exports);
36
36
  var import_picocolors = __toESM(require("picocolors"), 1);
37
37
 
38
38
  // package.json
39
- var version = "3.10.2";
39
+ var version = "3.12.0";
40
40
 
41
41
  // src/constants.ts
42
42
  var VERSION = version;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  logger
3
- } from "../chunk-AL67CV2N.js";
4
- import "../chunk-K7EPHS7S.js";
3
+ } from "../chunk-WVKLG26T.js";
4
+ import "../chunk-G7GG3HEB.js";
5
5
  export {
6
6
  logger
7
7
  };
@@ -6,6 +6,7 @@ import {
6
6
  isEventCatalogScaleEnabled,
7
7
  isEventCatalogStarterEnabled,
8
8
  isEventCatalogMCPEnabled,
9
+ isDevMode,
9
10
  } from '../src/utils/feature';
10
11
 
11
12
  const catalogDirectory = process.env.CATALOG_DIR || process.cwd();
@@ -72,6 +73,18 @@ export default function eventCatalogIntegration(): AstroIntegration {
72
73
  entrypoint: path.join(catalogDirectory, 'src/enterprise/plans/index.astro'),
73
74
  });
74
75
  }
76
+
77
+ // Dev-only routes for visualizer layout persistence
78
+ if (isDevMode()) {
79
+ params.injectRoute({
80
+ pattern: '/api/dev/visualizer-layout/save',
81
+ entrypoint: path.join(catalogDirectory, 'src/enterprise/visualizer-layout/save.ts'),
82
+ });
83
+ params.injectRoute({
84
+ pattern: '/api/dev/visualizer-layout/reset',
85
+ entrypoint: path.join(catalogDirectory, 'src/enterprise/visualizer-layout/reset.ts'),
86
+ });
87
+ }
75
88
  },
76
89
  },
77
90
  };
@@ -5,7 +5,8 @@ import { getAbsoluteFilePathForAstroFile } from '@utils/files';
5
5
  import Admonition from '@components/MDX/Admonition';
6
6
  import NodeGraph from '../NodeGraph/NodeGraph';
7
7
 
8
- import { isVisualiserEnabled, isEventCatalogChatEnabled } from '@utils/feature';
8
+ import { isVisualiserEnabled, isEventCatalogChatEnabled, isDevMode } from '@utils/feature';
9
+ import { loadSavedLayout, applyLayoutToNodes, buildResourceKey } from '@utils/node-graphs/layout-persistence';
9
10
 
10
11
  const isChatEnabled = isEventCatalogChatEnabled();
11
12
 
@@ -21,6 +22,11 @@ try {
21
22
  } catch (error) {
22
23
  console.error(`Error reading design file: ${error}`);
23
24
  }
25
+
26
+ // Load and apply saved layout if it exists
27
+ const resourceKey = design ? buildResourceKey('designs', design.id || id) : '';
28
+ const savedLayout = resourceKey ? await loadSavedLayout(resourceKey) : null;
29
+ const nodesWithLayout = design ? applyLayoutToNodes(design.nodes || [], savedLayout) : ([] as any[]);
24
30
  ---
25
31
 
26
32
  {
@@ -51,7 +57,7 @@ try {
51
57
  <NodeGraph
52
58
  id={id}
53
59
  title={title ?? design.name}
54
- nodes={design.nodes || []}
60
+ nodes={nodesWithLayout}
55
61
  edges={design.edges || []}
56
62
  hrefLabel={isVisualiserEnabled() ? 'View in visualizer' : undefined}
57
63
  href={isVisualiserEnabled() ? `/visualiser/designs/${design.id}` : undefined}
@@ -60,6 +66,8 @@ try {
60
66
  client:only="react"
61
67
  showSearch={search}
62
68
  isChatEnabled={isChatEnabled}
69
+ isDevMode={isDevMode()}
70
+ resourceKey={resourceKey}
63
71
  />
64
72
  </div>
65
73
  </div>
@@ -5,7 +5,8 @@ import Admonition from '@components/MDX/Admonition';
5
5
  import NodeGraph from '../NodeGraph/NodeGraph';
6
6
  import { getVersionFromCollection } from '@utils/collections/versions';
7
7
  import { getServices } from '@utils/collections/services';
8
- import { isEventCatalogChatEnabled } from '@utils/feature';
8
+ import { isEventCatalogChatEnabled, isDevMode } from '@utils/feature';
9
+ import { loadSavedLayout, applyLayoutToNodes, buildResourceKey } from '@utils/node-graphs/layout-persistence';
9
10
 
10
11
  const isChatEnabled = isEventCatalogChatEnabled();
11
12
 
@@ -38,6 +39,11 @@ const { nodes, edges } = await getNodesAndEdges({
38
39
  ...(entities ? { entities } : {}), // Pass entities if provided
39
40
  type: collection,
40
41
  });
42
+
43
+ // Load and apply saved layout if it exists
44
+ const resourceKey = buildResourceKey(collection, resourceId, resource?.data?.version);
45
+ const savedLayout = await loadSavedLayout(resourceKey);
46
+ const nodesWithLayout = applyLayoutToNodes(nodes as any[], savedLayout);
41
47
  ---
42
48
 
43
49
  {
@@ -66,7 +72,7 @@ const { nodes, edges } = await getNodesAndEdges({
66
72
  <div>
67
73
  <NodeGraph
68
74
  id={id}
69
- nodes={nodes}
75
+ nodes={nodesWithLayout}
70
76
  edges={edges}
71
77
  linkTo={'visualiser'}
72
78
  mode="simple"
@@ -75,6 +81,8 @@ const { nodes, edges } = await getNodesAndEdges({
75
81
  client:only="react"
76
82
  portalId={`${id}-entity-map-portal`}
77
83
  isChatEnabled={isChatEnabled}
84
+ isDevMode={isDevMode()}
85
+ resourceKey={resourceKey}
78
86
  />
79
87
  </div>
80
88
 
@@ -4,7 +4,8 @@ import { getNodesAndEdges } from '@utils/node-graphs/flows-node-graph';
4
4
  import Admonition from '@components/MDX/Admonition';
5
5
  import NodeGraph from '../NodeGraph/NodeGraph';
6
6
  import { getVersionFromCollection } from '@utils/collections/versions';
7
- import { isVisualiserEnabled, isEventCatalogChatEnabled } from '@utils/feature';
7
+ import { isVisualiserEnabled, isEventCatalogChatEnabled, isDevMode } from '@utils/feature';
8
+ import { loadSavedLayout, applyLayoutToNodes, buildResourceKey } from '@utils/node-graphs/layout-persistence';
8
9
 
9
10
  const isChatEnabled = isEventCatalogChatEnabled();
10
11
 
@@ -22,6 +23,11 @@ const { nodes, edges } = await getNodesAndEdges({
22
23
  version: flow.data.version,
23
24
  mode: mode,
24
25
  });
26
+
27
+ // Load and apply saved layout if it exists
28
+ const resourceKey = buildResourceKey('flows', id, flow.data.version);
29
+ const savedLayout = await loadSavedLayout(resourceKey);
30
+ const nodesWithLayout = applyLayoutToNodes(nodes, savedLayout);
25
31
  ---
26
32
 
27
33
  {
@@ -49,7 +55,7 @@ const { nodes, edges } = await getNodesAndEdges({
49
55
  <div>
50
56
  <NodeGraph
51
57
  id={id}
52
- nodes={nodes}
58
+ nodes={nodesWithLayout}
53
59
  edges={edges}
54
60
  hrefLabel={'View in visualizer'}
55
61
  href={isVisualiserEnabled() ? `/visualiser/flows/${id}/${version}` : undefined}
@@ -60,6 +66,8 @@ const { nodes, edges } = await getNodesAndEdges({
60
66
  showFlowWalkthrough={walkthrough}
61
67
  showSearch={search}
62
68
  isChatEnabled={isChatEnabled}
69
+ isDevMode={isDevMode()}
70
+ resourceKey={resourceKey}
63
71
  />
64
72
  </div>
65
73
 
@@ -81,6 +81,19 @@ const AnimatedMessageEdge = ({
81
81
  {/* <g className={`z-30 ${opacity === 1 ? 'opacity-100' : 'opacity-10'}`}>
82
82
  </g> */}
83
83
  <g>
84
+ {/* Background rect for label */}
85
+ {label && (
86
+ <rect
87
+ x={labelX - 30}
88
+ y={labelY - lines.length * 6}
89
+ width={60}
90
+ height={lines.length * 14}
91
+ fill="white"
92
+ fillOpacity={0.3}
93
+ rx={4}
94
+ ry={4}
95
+ />
96
+ )}
84
97
  {/* Text element */}
85
98
  <text x={labelX} y={labelY} textAnchor="middle" dominantBaseline="middle" fontSize="10px" pointerEvents="none">
86
99
  {lines.map((line, i) => (
@@ -0,0 +1,294 @@
1
+ import React, { useMemo, useCallback, useEffect, useState, useRef } from 'react';
2
+ import {
3
+ ReactFlow,
4
+ Background,
5
+ Controls,
6
+ useReactFlow,
7
+ useNodesState,
8
+ useEdgesState,
9
+ type Node,
10
+ type Edge,
11
+ type NodeTypes,
12
+ type EdgeTypes,
13
+ } from '@xyflow/react';
14
+ import { getConnectedNodes, getNodeDisplayInfo } from './utils';
15
+ import FocusModeNodeActions from './FocusModeNodeActions';
16
+ import FocusModePlaceholder from './FocusModePlaceholder';
17
+
18
+ interface FocusModeContentProps {
19
+ centerNodeId: string;
20
+ nodes: Node[];
21
+ edges: Edge[];
22
+ nodeTypes: NodeTypes;
23
+ edgeTypes: EdgeTypes;
24
+ onSwitchCenter: (newCenterNodeId: string, direction: 'left' | 'right') => void;
25
+ }
26
+
27
+ const HORIZONTAL_SPACING = 450;
28
+ const VERTICAL_SPACING = 200;
29
+ const SLIDE_DURATION = 300;
30
+
31
+ const FocusModeContent: React.FC<FocusModeContentProps> = ({
32
+ centerNodeId,
33
+ nodes: allNodes,
34
+ edges: allEdges,
35
+ nodeTypes,
36
+ edgeTypes,
37
+ onSwitchCenter,
38
+ }) => {
39
+ const { fitView } = useReactFlow();
40
+ const [isAnimating, setIsAnimating] = useState(false);
41
+ const [needsFitView, setNeedsFitView] = useState(false);
42
+ const [hoveredEdgeId, setHoveredEdgeId] = useState<string | null>(null);
43
+ const reactFlowInitialized = useRef(false);
44
+ const animationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
45
+
46
+ // Cleanup timeout on unmount
47
+ useEffect(() => {
48
+ return () => {
49
+ if (animationTimeoutRef.current) {
50
+ clearTimeout(animationTimeoutRef.current);
51
+ }
52
+ };
53
+ }, []);
54
+
55
+ // Calculate focused nodes and edges with positions
56
+ const calculateFocusedGraph = useCallback(
57
+ (centerId: string) => {
58
+ const centerNode = allNodes.find((n) => n.id === centerId);
59
+ if (!centerNode) {
60
+ return { nodes: [], edges: [] };
61
+ }
62
+
63
+ const { leftNodes, rightNodes } = getConnectedNodes(centerId, allNodes, allEdges);
64
+ const centerNodeInfo = getNodeDisplayInfo(centerNode);
65
+ const positionedNodes: Node[] = [];
66
+
67
+ // Center node at origin
68
+ positionedNodes.push({
69
+ ...centerNode,
70
+ position: { x: 0, y: 0 },
71
+ style: { ...centerNode.style, opacity: 1 },
72
+ data: { ...centerNode.data, isFocusCenter: true },
73
+ });
74
+
75
+ // Left nodes (incoming)
76
+ leftNodes.forEach((node, index) => {
77
+ const yOffset = (index - (leftNodes.length - 1) / 2) * VERTICAL_SPACING;
78
+ positionedNodes.push({
79
+ ...node,
80
+ position: { x: -HORIZONTAL_SPACING, y: yOffset },
81
+ style: { ...node.style, opacity: 1 },
82
+ });
83
+ });
84
+
85
+ // Right nodes (outgoing)
86
+ rightNodes.forEach((node, index) => {
87
+ const yOffset = (index - (rightNodes.length - 1) / 2) * VERTICAL_SPACING;
88
+ positionedNodes.push({
89
+ ...node,
90
+ position: { x: HORIZONTAL_SPACING, y: yOffset },
91
+ style: { ...node.style, opacity: 1 },
92
+ });
93
+ });
94
+
95
+ // Add placeholder nodes if no connections exist
96
+ if (leftNodes.length === 0) {
97
+ positionedNodes.push({
98
+ id: '__placeholder-left__',
99
+ type: 'placeholder',
100
+ position: { x: -HORIZONTAL_SPACING, y: 0 },
101
+ data: { label: `No inputs found for "${centerNodeInfo.name}" in this diagram`, side: 'left' },
102
+ draggable: false,
103
+ selectable: false,
104
+ } as Node);
105
+ }
106
+
107
+ if (rightNodes.length === 0) {
108
+ positionedNodes.push({
109
+ id: '__placeholder-right__',
110
+ type: 'placeholder',
111
+ position: { x: HORIZONTAL_SPACING, y: 0 },
112
+ data: { label: `No outputs found for "${centerNodeInfo.name}" in this diagram`, side: 'right' },
113
+ draggable: false,
114
+ selectable: false,
115
+ } as Node);
116
+ }
117
+
118
+ // Filter edges - only show edges connected to center node
119
+ const focusedNodeIds = new Set(positionedNodes.map((n) => n.id));
120
+ const filteredEdges = allEdges
121
+ .filter((edge) => {
122
+ // Only include edges where center node is either source or target
123
+ const connectsToCenter = edge.source === centerId || edge.target === centerId;
124
+ // And the other end is in our focused nodes
125
+ const otherEndInFocus = focusedNodeIds.has(edge.source) && focusedNodeIds.has(edge.target);
126
+ return connectsToCenter && otherEndInFocus;
127
+ })
128
+ .map((edge) => ({
129
+ ...edge,
130
+ style: { ...edge.style, opacity: 1 },
131
+ labelStyle: { ...edge.labelStyle, opacity: 1 },
132
+ data: { ...edge.data, opacity: 1, animated: false },
133
+ animated: false,
134
+ }));
135
+
136
+ return { nodes: positionedNodes, edges: filteredEdges };
137
+ },
138
+ [allNodes, allEdges]
139
+ );
140
+
141
+ // Initial graph
142
+ const initialGraph = useMemo(() => calculateFocusedGraph(centerNodeId), [centerNodeId, calculateFocusedGraph]);
143
+
144
+ const [displayNodes, setDisplayNodes] = useNodesState(initialGraph.nodes);
145
+ const [displayEdges, setDisplayEdges] = useEdgesState(initialGraph.edges);
146
+
147
+ // Update when centerNodeId changes externally
148
+ useEffect(() => {
149
+ const { nodes, edges } = calculateFocusedGraph(centerNodeId);
150
+ setDisplayNodes(nodes);
151
+ setDisplayEdges(edges);
152
+ setNeedsFitView(true);
153
+ }, [centerNodeId, calculateFocusedGraph, setDisplayNodes, setDisplayEdges]);
154
+
155
+ // FitView when needed - triggered after nodes are updated
156
+ useEffect(() => {
157
+ if (needsFitView && reactFlowInitialized.current) {
158
+ // Wait for nodes to be fully rendered, then fit view with animation
159
+ const timer = setTimeout(() => {
160
+ fitView({ padding: 0.2, duration: 400 });
161
+ setNeedsFitView(false);
162
+ }, 150);
163
+ return () => clearTimeout(timer);
164
+ }
165
+ }, [needsFitView, displayNodes, fitView]);
166
+
167
+ // Handle ReactFlow initialization
168
+ const handleInit = useCallback(() => {
169
+ reactFlowInitialized.current = true;
170
+ // Wait for nodes to be fully rendered, then fit view with animation
171
+ setTimeout(() => {
172
+ fitView({ padding: 0.2, duration: 400 });
173
+ }, 150);
174
+ }, [fitView]);
175
+
176
+ // Handle switching to a new center node with animation
177
+ const handleSwitchNode = useCallback(
178
+ (nodeId: string, direction: 'left' | 'right') => {
179
+ if (nodeId === centerNodeId || isAnimating) return;
180
+
181
+ setIsAnimating(true);
182
+
183
+ // Animate: clicked node slides to center, current center hides
184
+ setDisplayNodes((currentNodes) =>
185
+ currentNodes.map((node) => {
186
+ if (node.id === nodeId) {
187
+ // Clicked node slides to center
188
+ return {
189
+ ...node,
190
+ position: { x: 0, y: 0 },
191
+ style: { ...node.style, transition: `all ${SLIDE_DURATION}ms ease-out` },
192
+ };
193
+ }
194
+ if (node.id === centerNodeId) {
195
+ // Current center node hides
196
+ return {
197
+ ...node,
198
+ style: { ...node.style, opacity: 0, transition: `opacity ${SLIDE_DURATION}ms ease-out` },
199
+ };
200
+ }
201
+ return node;
202
+ })
203
+ );
204
+
205
+ // After slide completes, switch to new center
206
+ animationTimeoutRef.current = setTimeout(() => {
207
+ onSwitchCenter(nodeId, direction);
208
+ setIsAnimating(false);
209
+ }, SLIDE_DURATION);
210
+ },
211
+ [centerNodeId, isAnimating, setDisplayNodes, onSwitchCenter]
212
+ );
213
+
214
+ // Handle node click with animation
215
+ const handleNodeClick = useCallback(
216
+ (_: React.MouseEvent, clickedNode: Node) => {
217
+ if (clickedNode.id === centerNodeId || isAnimating) return;
218
+ const direction = (clickedNode.position?.x ?? 0) < 0 ? 'left' : 'right';
219
+ handleSwitchNode(clickedNode.id, direction);
220
+ },
221
+ [centerNodeId, isAnimating, handleSwitchNode]
222
+ );
223
+
224
+ // Handle edge hover for animation
225
+ const handleEdgeMouseEnter = useCallback((_: React.MouseEvent, edge: Edge) => {
226
+ setHoveredEdgeId(edge.id);
227
+ }, []);
228
+
229
+ const handleEdgeMouseLeave = useCallback(() => {
230
+ setHoveredEdgeId(null);
231
+ }, []);
232
+
233
+ // Apply hover animation to edges - use ReactFlow's built-in animated property
234
+ const edgesWithHover = useMemo(() => {
235
+ return displayEdges.map((edge) => {
236
+ if (edge.id === hoveredEdgeId) {
237
+ return {
238
+ ...edge,
239
+ animated: true,
240
+ };
241
+ }
242
+ return edge;
243
+ });
244
+ }, [displayEdges, hoveredEdgeId]);
245
+
246
+ // Merge nodeTypes with placeholder
247
+ const mergedNodeTypes = useMemo(
248
+ () => ({
249
+ ...nodeTypes,
250
+ placeholder: FocusModePlaceholder,
251
+ }),
252
+ [nodeTypes]
253
+ );
254
+
255
+ if (displayNodes.length === 0) {
256
+ return <div className="flex items-center justify-center h-full text-[rgb(var(--ec-page-text-muted))]">Node not found</div>;
257
+ }
258
+
259
+ return (
260
+ <div className="h-full w-full focus-mode-container">
261
+ <ReactFlow
262
+ nodes={displayNodes}
263
+ edges={edgesWithHover}
264
+ nodeTypes={mergedNodeTypes}
265
+ edgeTypes={edgeTypes}
266
+ onNodeClick={handleNodeClick}
267
+ onEdgeMouseEnter={handleEdgeMouseEnter}
268
+ onEdgeMouseLeave={handleEdgeMouseLeave}
269
+ onInit={handleInit}
270
+ proOptions={{ hideAttribution: true }}
271
+ nodesDraggable={true}
272
+ nodesConnectable={false}
273
+ elementsSelectable={true}
274
+ panOnDrag={true}
275
+ zoomOnScroll={true}
276
+ minZoom={0.3}
277
+ maxZoom={2}
278
+ >
279
+ <Background color="rgb(var(--ec-page-border))" gap={20} />
280
+ <Controls showInteractive={false} />
281
+ {displayNodes.map((node, index) => (
282
+ <FocusModeNodeActions
283
+ key={`actions-${node.id}-${index}`}
284
+ node={node}
285
+ isCenter={node.id === centerNodeId}
286
+ onSwitch={handleSwitchNode}
287
+ />
288
+ ))}
289
+ </ReactFlow>
290
+ </div>
291
+ );
292
+ };
293
+
294
+ export default FocusModeContent;