diff --git a/.changeset/wise-snails-nail.md b/.changeset/wise-snails-nail.md new file mode 100644 index 0000000..61eaab2 --- /dev/null +++ b/.changeset/wise-snails-nail.md @@ -0,0 +1,5 @@ +--- +"@react-unforget/babel-plugin": patch +--- + +Expose mermaid dependency graph generator API diff --git a/apps/docs/components/CodeEditorAndPreview.tsx b/apps/docs/components/CodeEditorAndPreview.tsx index e972f77..6f4463e 100644 --- a/apps/docs/components/CodeEditorAndPreview.tsx +++ b/apps/docs/components/CodeEditorAndPreview.tsx @@ -5,7 +5,7 @@ import { useSandpack, } from "@codesandbox/sandpack-react"; import { memo, useCallback, useEffect, useRef } from "react"; -import { twMerge } from "tailwind-merge"; +import { cn } from "utils/cn"; type CodeEditorAndPreviewProps = { readOnly?: boolean; @@ -98,7 +98,7 @@ export const CodeEditorAndPreview = memo(
); diff --git a/apps/docs/components/DependencyGraphViewer.tsx b/apps/docs/components/DependencyGraphViewer.tsx new file mode 100644 index 0000000..4e178db --- /dev/null +++ b/apps/docs/components/DependencyGraphViewer.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; + +interface DependencyGraphViewerProps { + mermaidSyntax: string; +} + +function createNodeFromHTML(svgString: string) { + var div = document.createElement("div"); + div.innerHTML = svgString.trim(); + + return div.firstElementChild as SVGElement; +} + +type MermaidDefault = (typeof import("mermaid"))["default"]; +type SvgPanZoomDefault = typeof import("svg-pan-zoom"); + +const DependencyGraphViewer = ({ + mermaidSyntax, +}: DependencyGraphViewerProps) => { + const container = useRef(null); + + const [modules, setModules] = useState< + [MermaidDefault, SvgPanZoomDefault] | null + >(null); + + useEffect(() => { + Promise.all([import("mermaid"), import("svg-pan-zoom")]).then( + ([mermaidModule, svgPanZoomModule]) => { + mermaidModule.default.initialize({ + startOnLoad: true, + theme: "dark", + }); + + setModules([mermaidModule.default, svgPanZoomModule.default]); + }, + ); + }, []); + + useEffect(() => { + if (mermaidSyntax && container.current && modules) { + const [mermaid, svgPanZoom] = modules; + (async () => { + const { svg } = await mermaid.render("mermaidGraph", mermaidSyntax); + + while (container.current.hasChildNodes()) { + container.current.removeChild(container.current.lastChild); + } + + const svgNode = createNodeFromHTML(svg); + svgNode.setAttribute("height", "100%"); + container.current.appendChild(svgNode); + + svgPanZoom(svgNode, { + controlIconsEnabled: true, + }); + })(); + } + }, [mermaidSyntax, modules]); + + return ( +
+

+ Below is a visual representation of the dependency graph that React + Unforget's compiler sees to ultimately optimize your code. +

+
+
+ ); +}; + +export default DependencyGraphViewer; diff --git a/apps/docs/components/DynamicDependencyGraphViewer.tsx b/apps/docs/components/DynamicDependencyGraphViewer.tsx new file mode 100644 index 0000000..ba26c5a --- /dev/null +++ b/apps/docs/components/DynamicDependencyGraphViewer.tsx @@ -0,0 +1,14 @@ +import dynamic from "next/dynamic"; + +const DynamicDependencyGraphViewer = dynamic( + () => import("./DependencyGraphViewer"), + { + loading: () => ( +
+ +
+ ), + }, +); + +export { DynamicDependencyGraphViewer }; diff --git a/apps/docs/components/DynamicLiveCodeSandpack.tsx b/apps/docs/components/DynamicLiveCodeSandpack.tsx index 332804a..8c9e8a3 100644 --- a/apps/docs/components/DynamicLiveCodeSandpack.tsx +++ b/apps/docs/components/DynamicLiveCodeSandpack.tsx @@ -7,12 +7,12 @@ const DynamicLiveCodeSandpack = dynamic(() => import("./LiveCodeSandpack"), { - {" "} +
} after={
- {" "} +
} /> diff --git a/apps/docs/components/LiveCodeSandpack.tsx b/apps/docs/components/LiveCodeSandpack.tsx index b5b0f30..7ecd946 100644 --- a/apps/docs/components/LiveCodeSandpack.tsx +++ b/apps/docs/components/LiveCodeSandpack.tsx @@ -1,6 +1,8 @@ import { transform } from "@babel/standalone"; import { SandpackProvider } from "@codesandbox/sandpack-react"; -import reactUnforgetBabelPlugin from "@react-unforget/babel-plugin"; +import reactUnforgetBabelPlugin, { + mermaidGraphFromComponent, +} from "@react-unforget/babel-plugin"; // @ts-ignore import jsxBabelPlugin from "@babel/plugin-syntax-jsx"; @@ -8,6 +10,7 @@ import { useTheme } from "nextra-theme-docs"; import { useCallback, useEffect, useMemo, useState } from "react"; import { BeforeAfterCodeLayout } from "./BeforeAfterCodeLayout"; import { CodeEditorAndPreview } from "./CodeEditorAndPreview"; +import { DynamicDependencyGraphViewer } from "./DynamicDependencyGraphViewer"; const indexFileContent = ` import { createRoot } from "react-dom/client"; @@ -31,19 +34,34 @@ export interface LiveCodeProps { previewClassName?: string; } -function transformCode(content: string) { - const result = transform(content, { - plugins: [jsxBabelPlugin, reactUnforgetBabelPlugin], - }); - - return result?.code ?? ""; -} - function LiveCodeSandpack({ children, previewClassName }: LiveCodeProps) { const [beforeCode, setBeforeCode] = useState(children); const [afterCode, setAfterCode] = useState(defaultLoadingCode); + const [mermaidSyntax, setMermaidSyntax] = useState(null); + const [viewDependencyGraph, setViewDependencyGraph] = useState(false); + const handleToggleDependencyGraph = useCallback(() => { + setViewDependencyGraph((prev) => !prev); + }, []); const { theme } = useTheme(); + const transformCode = useCallback((content: string) => { + const result = transform(content, { + plugins: [ + jsxBabelPlugin, + [ + reactUnforgetBabelPlugin, + { + _debug_onComponentAnalysisFinished: (component: any) => { + setMermaidSyntax(mermaidGraphFromComponent(component)); + }, + }, + ], + ], + }); + + return result?.code ?? ""; + }, []); + const handleCodeChange = useCallback((newCode: string) => { setBeforeCode(newCode); }, []); @@ -83,39 +101,56 @@ function LiveCodeSandpack({ children, previewClassName }: LiveCodeProps) { ); return ( - - - - } - after={ - - - - } - /> +
+ + + + } + after={ + + + + } + /> +
+ + Click here to {viewDependencyGraph ? "hide" : "view"} the dependency + graph + +
+ {viewDependencyGraph && ( + + )} +
+
+
); } diff --git a/apps/docs/components/OldAndNewCodeReveal.tsx b/apps/docs/components/OldAndNewCodeReveal.tsx new file mode 100644 index 0000000..b11f0a2 --- /dev/null +++ b/apps/docs/components/OldAndNewCodeReveal.tsx @@ -0,0 +1,79 @@ +"use client"; +import React from "react"; +import { + TextRevealCard, + TextRevealCardDescription, + TextRevealCardTitle, +} from "./ui/text-reveal-card"; +import { encode } from "html-entities"; + +const oldCode = `const UglyComponent = memo(() => { + const data = useData(); + const filteredData = useMemo(() => { + const items = []; + for (let i = 0; i < 1000000000; i++) { + items.push(data[i]); + } + }, [data]); + + const someComplexJsx = useMemo(() => ( + <> + + {/* ... */} + + ), [dependency1, dependency2, filteredData]); + + return
{someComplexJsx}
; +}); +`; + +/* +const NiceComponent = () => { + const data = useData(); + const filteredData = []; + + for (let i = 0; i < 1000000000; i++) { + filteredData.push(data[i]); + } + + return ( +
+ + {/* ... *} +
+ ); +} +*/ +const newCode = `const NiceComponent = () => { + const data = useData(); + const filteredData = []; + + for (let i = 0; i < 1000000000; i++) { + filteredData.push(data[i]); + } + + + return ( + <div> + <DependentComponent1 data={filteredData} /> + {/* ... */} + </div> + ); +}\n\n +`; + +export const OldAndNewCodeReveal = () => { + return ( +
+ + + You don't need all that clutter! + + + 👋 Wave goodbye to clutter! Shed the excess and use React Unforget + instead. + + +
+ ); +}; diff --git a/apps/docs/components/ui/text-reveal-card.tsx b/apps/docs/components/ui/text-reveal-card.tsx new file mode 100644 index 0000000..b981544 --- /dev/null +++ b/apps/docs/components/ui/text-reveal-card.tsx @@ -0,0 +1,177 @@ +"use client"; +import React, { useEffect, useRef, useState, memo } from "react"; +import { motion } from "framer-motion"; +import { twMerge } from "tailwind-merge"; +import { cn } from "@/utils/cn"; + +export const TextRevealCard = ({ + text, + revealText, + children, + className, +}: { + text: string; + revealText: string; + children?: React.ReactNode; + className?: string; +}) => { + const [widthPercentage, setWidthPercentage] = useState(0); + const cardRef = useRef(null); + const [left, setLeft] = useState(0); + const [localWidth, setLocalWidth] = useState(0); + const [isMouseOver, setIsMouseOver] = useState(false); + + useEffect(() => { + if (cardRef.current) { + const { left, width: localWidth } = + cardRef.current.getBoundingClientRect(); + setLeft(left); + setLocalWidth(localWidth); + } + }, []); + + function mouseMoveHandler(event: any) { + event.preventDefault(); + + const { clientX } = event; + if (cardRef.current) { + const relativeX = clientX - left; + setWidthPercentage((relativeX / localWidth) * 100); + } + } + + function mouseLeaveHandler() { + setIsMouseOver(false); + setWidthPercentage(0); + } + + function mouseEnterHandler() { + setIsMouseOver(true); + } + + const rotateDeg = (widthPercentage - 50) * 0.1; + return ( +
+ {children} + +
+ 0 ? 1 : 0, + clipPath: `inset(0 ${100 - widthPercentage}% 0 0)`, + } + : { + clipPath: `inset(0 ${100 - widthPercentage}% 0 0)`, + } + } + transition={isMouseOver ? { duration: 0 } : { duration: 0.4 }} + className="absolute bg-[#1d1c20] z-20 will-change-transform" + > +
+        
+         0 ? 1 : 0,
+          }}
+          transition={isMouseOver ? { duration: 0 } : { duration: 0.4 }}
+          className="h-40 w-[8px] bg-gradient-to-b from-transparent via-neutral-800 to-transparent absolute z-50 will-change-transform"
+        >
+
+        
+
+          
+        
+
+
+ ); +}; + +export const TextRevealCardTitle = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) => { + return ( +

+ {children} +

+ ); +}; + +export const TextRevealCardDescription = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) => { + return ( +

{children}

+ ); +}; + +const Stars = () => { + const randomMove = () => Math.random() * 4 - 2; + const randomOpacity = () => Math.random(); + const random = () => Math.random(); + return ( +
+ {[...Array(140)].map((_, i) => ( + + ))} +
+ ); +}; + +export const MemoizedStars = memo(Stars); diff --git a/apps/docs/package.json b/apps/docs/package.json index d9ef0e4..f7396f7 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -15,12 +15,17 @@ "@react-unforget/babel-plugin": "0.1.0-dev.10", "@react-unforget/runtime": "0.1.0-dev.6", "@vercel/analytics": "^1.2.2", + "clsx": "^2.1.0", + "framer-motion": "^11.0.8", + "html-entities": "^2.5.2", + "mermaid": "^10.9.0", "next": "^14.0.4", "nextra": "^2.13.3", "nextra-theme-docs": "^2.13.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.0.1", + "svg-pan-zoom": "bumbu/svg-pan-zoom", "tailwind-merge": "^2.2.1" }, "devDependencies": { diff --git a/apps/docs/pages/index.mdx b/apps/docs/pages/index.mdx index 5b60f6b..ad670a5 100644 --- a/apps/docs/pages/index.mdx +++ b/apps/docs/pages/index.mdx @@ -3,10 +3,15 @@ title: "Home" --- import { DynamicLiveCodeSandpack } from "@components/DynamicLiveCodeSandpack"; +import { OldAndNewCodeReveal } from "@components/OldAndNewCodeReveal"; # React Unforget -React Unforget is a project inspired by the principles of React Forget, designed to streamline and optimize React component rendering. The mission of React Unforget is to enhance the performance of React applications as well as developer experience by automating the memoization process, making it unnecessary to manually use `useMemo` or `useCallback`. +React Unforget is a project inspired by the principles of React Forget, designed to streamline and optimize React component rendering. The mission of React Unforget is to enhance the performance of React applications as well as developer experience by automating the memoization process, making it unnecessary to manually use `useMemo`, `useCallback` or `React.memo`. + + + +## See it in action {`import { useState } from "react"; @@ -14,22 +19,22 @@ React Unforget is a project inspired by the principles of React Forget, designed export default function CounterWithMutationTracking() { const [state, setState] = useState(0); - let text = "The number is: "; - - if (state % 2 === 0) { - text += "even"; - } else { - text += "odd"; - } - - return ( -
- -
- Count: {state} {text} -
-
- ); +let text = "The number is: "; + +if (state % 2 === 0) { +text += "even"; +} else { +text += "odd"; +} + +return ( +
+ +
+Count: {state} {text} +
+
+); } `} diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json index 541b1e3..7be6fb9 100644 --- a/apps/docs/tsconfig.json +++ b/apps/docs/tsconfig.json @@ -16,7 +16,8 @@ "module": "esnext", "resolveJsonModule": true, "paths": { - "@components/*": ["components/*"] + "@components/*": ["components/*"], + "@/*": ["*"] } }, "include": [ @@ -25,7 +26,8 @@ "**/*.tsx", "**/*.js", "**/*.jsx", - "**/*.mdx" -, "next.config.mjs" ], + "**/*.mdx", + "next.config.mjs" + ], "exclude": ["node_modules"] } diff --git a/apps/docs/utils/cn.ts b/apps/docs/utils/cn.ts new file mode 100644 index 0000000..cec6ac9 --- /dev/null +++ b/apps/docs/utils/cn.ts @@ -0,0 +1,6 @@ +import { ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/packages/babel-plugin/package.json b/packages/babel-plugin/package.json index 545213a..3a3e21b 100644 --- a/packages/babel-plugin/package.json +++ b/packages/babel-plugin/package.json @@ -16,10 +16,10 @@ "require": "./dist/index.cjs", "types": "./dist/index.d.ts" }, - "./testing": { - "import": "./dist/testing.js", - "require": "./dist/testing.cjs", - "types": "./dist/testing.d.ts" + "./dist/utils/testing": { + "import": "./dist/utils/testing.js", + "require": "./dist/utils/testing.cjs", + "types": "./dist/utils/testing.d.ts" } }, "scripts": { diff --git a/packages/babel-plugin/src/index.ts b/packages/babel-plugin/src/index.ts index 752e552..7645f24 100644 --- a/packages/babel-plugin/src/index.ts +++ b/packages/babel-plugin/src/index.ts @@ -1,10 +1,11 @@ import type * as babel from "@babel/core"; -import { visitProgram } from "~/visit-program"; -import { findComponents } from "~/utils/path-tools/find-components"; import type { Options } from "~/models/TransformationContext"; import { makeTransformationContext } from "~/models/TransformationContext"; +import { findComponents } from "~/utils/path-tools/find-components"; +import { visitProgram } from "~/visit-program"; +export { mermaidGraphFromComponent } from "~/utils/misc/mermaid-graph-from-component"; -export { visitProgram, findComponents }; +export { findComponents, visitProgram }; export default function unforgetBabelPlugin( _: object, @@ -14,6 +15,8 @@ export default function unforgetBabelPlugin( throwOnFailure: options.throwOnFailure ?? false, skipComponents: options.skipComponents ?? [], skipComponentsWithMutation: options.skipComponentsWithMutation ?? false, + _debug_onComponentAnalysisFinished: + options._debug_onComponentAnalysisFinished, }); return { visitor: { diff --git a/packages/babel-plugin/src/models/TransformationContext.ts b/packages/babel-plugin/src/models/TransformationContext.ts index 7513551..29a48e5 100644 --- a/packages/babel-plugin/src/models/TransformationContext.ts +++ b/packages/babel-plugin/src/models/TransformationContext.ts @@ -1,18 +1,23 @@ +import type { Component } from "./Component"; + export interface Options { throwOnFailure?: boolean; skipComponents?: string[]; skipComponentsWithMutation?: boolean; + _debug_onComponentAnalysisFinished?: (component: Component) => void; } export function makeTransformationContext({ throwOnFailure = true, skipComponents = [], skipComponentsWithMutation = false, + _debug_onComponentAnalysisFinished, }: Options = {}) { return { throwOnFailure, skipComponents, skipComponentsWithMutation, + _debug_onComponentAnalysisFinished, shouldSkipComponent(componentName: string) { return skipComponents.includes(componentName); }, diff --git a/packages/babel-plugin/src/tests/__snapshots__/dependency-graph.test.ts.snap b/packages/babel-plugin/src/tests/__snapshots__/dependency-graph.test.ts.snap index 27eade5..a8023fe 100644 --- a/packages/babel-plugin/src/tests/__snapshots__/dependency-graph.test.ts.snap +++ b/packages/babel-plugin/src/tests/__snapshots__/dependency-graph.test.ts.snap @@ -6,7 +6,7 @@ exports[`dependency graph fixture_dependency_graph 1`] = ` classDef blockSegment fill:#5352ed,stroke:#333,stroke-width:2px; classDef rootChild stroke:#ff0000,stroke-width:2px; classDef returnNetwork stroke:#2ecc71,stroke-width:2px; -classDef componentVariable fill:#2ecc71,stroke:#333,stroke-width:1px; +classDef componentVariable fill:#1289A7,stroke:#333,stroke-width:1px; node1["
function Test(_props) {
   let a = _props.a;
   let b = _props.b;
diff --git a/packages/babel-plugin/src/utils/misc/mermaid-graph-from-component.ts b/packages/babel-plugin/src/utils/misc/mermaid-graph-from-component.ts
index ed720d8..a6a9272 100644
--- a/packages/babel-plugin/src/utils/misc/mermaid-graph-from-component.ts
+++ b/packages/babel-plugin/src/utils/misc/mermaid-graph-from-component.ts
@@ -19,7 +19,7 @@ function printMermaidGraph(root: ComponentSegment): string {
     "classDef blockSegment fill:#5352ed,stroke:#333,stroke-width:2px;",
     "classDef rootChild stroke:#ff0000,stroke-width:2px;",
     "classDef returnNetwork stroke:#2ecc71,stroke-width:2px;",
-    "classDef componentVariable fill:#2ecc71,stroke:#333,stroke-width:1px;",
+    "classDef componentVariable fill:#1289A7,stroke:#333,stroke-width:1px;",
   ];
   let nodeIdCounter = 0;
   let subgraphCounter = 0;
diff --git a/packages/babel-plugin/src/utils/testing.ts b/packages/babel-plugin/src/utils/testing.ts
index 2ccc3a3..6b7ec35 100644
--- a/packages/babel-plugin/src/utils/testing.ts
+++ b/packages/babel-plugin/src/utils/testing.ts
@@ -4,7 +4,6 @@ import * as babel from "@babel/core";
 import * as generateBase from "@babel/generator";
 import traverse from "@babel/traverse";
 import type * as t from "@babel/types";
-import { mermaidGraphFromComponent } from "~/utils/misc/mermaid-graph-from-component";
 import babelPlugin from "../index";
 
 const babelTransformOptions = {
@@ -67,5 +66,3 @@ export function parseFixture(fixturePath: string) {
 export function generate(path: babel.types.Node) {
   return generateBase.default(path).code;
 }
-
-export { mermaidGraphFromComponent };
diff --git a/packages/babel-plugin/src/visit-program.ts b/packages/babel-plugin/src/visit-program.ts
index 07678f0..4d7d920 100644
--- a/packages/babel-plugin/src/visit-program.ts
+++ b/packages/babel-plugin/src/visit-program.ts
@@ -19,6 +19,8 @@ export function visitProgram(
   components.forEach((component) => {
     try {
       component.analyze();
+      context?._debug_onComponentAnalysisFinished?.(component);
+
       component.applyTransformation();
     } catch (e) {
       if (!context || context?.throwOnFailure) {
diff --git a/yarn.lock b/yarn.lock
index db72462..6cdfd27 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1443,6 +1443,18 @@
   dependencies:
     "@jridgewell/trace-mapping" "0.3.9"
 
+"@emotion/is-prop-valid@^0.8.2":
+  version "0.8.8"
+  resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
+  integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==
+  dependencies:
+    "@emotion/memoize" "0.7.4"
+
+"@emotion/memoize@0.7.4":
+  version "0.7.4"
+  resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
+  integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
+
 "@esbuild/aix-ppc64@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f"
@@ -2181,15 +2193,6 @@
   resolved "https://registry.yarnpkg.com/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz#c06dac2d011f36d61259aa1c6df4f0d5e28bc55e"
   integrity sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==
 
-"@react-unforget/babel-plugin@file:packages/babel-plugin":
-  version "0.1.0-dev.8"
-  dependencies:
-    "@babel/helper-validator-identifier" "^7.22.20"
-    "@babel/parser" "^7.24.0"
-    "@babel/preset-react" "^7.23.3"
-    "@babel/traverse" "^7.24.0"
-    "@babel/types" "^7.24.0"
-
 "@rollup/rollup-android-arm-eabi@4.12.0":
   version "4.12.0"
   resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz#38c3abd1955a3c21d492af6b1a1dca4bb1d894d6"
@@ -3643,7 +3646,7 @@ clone@^1.0.2:
   resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
   integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
 
-clsx@^2.0.0:
+clsx@^2.0.0, clsx@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
   integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==
@@ -5267,6 +5270,15 @@ fraction.js@^4.3.7:
   resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
   integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
 
+framer-motion@^11.0.8:
+  version "11.0.8"
+  resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.0.8.tgz#8f97a18cbad5858d85b53bc325c40a03d0a5c203"
+  integrity sha512-1KSGNuqe1qZkS/SWQlDnqK2VCVzRVEoval379j0FiUBJAZoqgwyvqFkfvJbgW2IPFo4wX16K+M0k5jO23lCIjA==
+  dependencies:
+    tslib "^2.4.0"
+  optionalDependencies:
+    "@emotion/is-prop-valid" "^0.8.2"
+
 fs-extra@^7.0.1:
   version "7.0.1"
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
@@ -7256,6 +7268,32 @@ mermaid@^10.2.2:
     uuid "^9.0.0"
     web-worker "^1.2.0"
 
+mermaid@^10.9.0:
+  version "10.9.0"
+  resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.9.0.tgz#4d1272fbe434bd8f3c2c150554dc8a23a9bf9361"
+  integrity sha512-swZju0hFox/B/qoLKK0rOxxgh8Cf7rJSfAUc1u8fezVihYMvrJAS45GzAxTVf4Q+xn9uMgitBcmWk7nWGXOs/g==
+  dependencies:
+    "@braintree/sanitize-url" "^6.0.1"
+    "@types/d3-scale" "^4.0.3"
+    "@types/d3-scale-chromatic" "^3.0.0"
+    cytoscape "^3.28.1"
+    cytoscape-cose-bilkent "^4.1.0"
+    d3 "^7.4.0"
+    d3-sankey "^0.12.3"
+    dagre-d3-es "7.0.10"
+    dayjs "^1.11.7"
+    dompurify "^3.0.5"
+    elkjs "^0.9.0"
+    katex "^0.16.9"
+    khroma "^2.0.0"
+    lodash-es "^4.17.21"
+    mdast-util-from-markdown "^1.3.0"
+    non-layered-tidy-tree-layout "^2.0.2"
+    stylis "^4.1.3"
+    ts-dedent "^2.2.0"
+    uuid "^9.0.0"
+    web-worker "^1.2.0"
+
 micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8"
@@ -9344,6 +9382,10 @@ supports-preserve-symlinks-flag@^1.0.0:
   resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
   integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
 
+svg-pan-zoom@bumbu/svg-pan-zoom:
+  version "3.6.2"
+  resolved "https://codeload.github.com/bumbu/svg-pan-zoom/tar.gz/aaa68d186abab5d782191b66d2582592fe5d3c13"
+
 symbol-tree@^3.2.4:
   version "3.2.4"
   resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"