From ff7fd0f0d5819b36d89e84c990923d0807b02c17 Mon Sep 17 00:00:00 2001
From: zhichaona <1255628593@qq.com>
Date: Mon, 24 Mar 2025 14:11:58 +0800
Subject: [PATCH] Add excl draw module 2 (#7658)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* init exceldraw module

* excalidraw demo

* i18n fixed bug

* exceldraw change to excalidraw

* lang setting

---------

Co-authored-by: 杨顺强 <978987373@qq.com>
Co-authored-by: First <first@FirstdeMacBook-Pro.local>
---
 frontend/config/webpack.config.js             |    6 +
 frontend/config/webpack.entry.js              |    1 +
 frontend/package-lock.json                    | 1294 ++++++++++++++++-
 frontend/package.json                         |    1 +
 .../dirent-grid-view/dirent-grid-view.js      |   15 +-
 .../dirent-list-view/dirent-list-view.js      |   12 +-
 .../dirent-list-view/dirent-none-view.js      |    8 +-
 .../toolbar/dir-operation-toolbar.js          |   12 +-
 frontend/src/excalidraw-editor.js             |   11 +
 .../src/pages/excalidraw-editor/constants.js  |   15 +
 .../src/pages/excalidraw-editor/editor-api.js |   26 +
 .../src/pages/excalidraw-editor/index.css     |    8 +
 frontend/src/pages/excalidraw-editor/index.js |   86 ++
 .../pages/excalidraw-editor/simple-editor.js  |   75 +
 frontend/src/utils/constants.js               |    1 +
 frontend/src/utils/text-translation.js        |    4 +
 seahub/base/context_processors.py             |    3 +-
 seahub/settings.py                            |    6 +
 seahub/templates/base_for_react.html          |    1 +
 .../templates/excalidraw_file_view_react.html |   19 +
 seahub/utils/__init__.py                      |    1 +
 seahub/utils/file_types.py                    |    1 +
 seahub/views/file.py                          |   23 +-
 23 files changed, 1597 insertions(+), 32 deletions(-)
 create mode 100644 frontend/src/excalidraw-editor.js
 create mode 100644 frontend/src/pages/excalidraw-editor/constants.js
 create mode 100644 frontend/src/pages/excalidraw-editor/editor-api.js
 create mode 100644 frontend/src/pages/excalidraw-editor/index.css
 create mode 100644 frontend/src/pages/excalidraw-editor/index.js
 create mode 100644 frontend/src/pages/excalidraw-editor/simple-editor.js
 create mode 100644 seahub/templates/excalidraw_file_view_react.html

diff --git a/frontend/config/webpack.config.js b/frontend/config/webpack.config.js
index 8b00510c30..f2efe52733 100644
--- a/frontend/config/webpack.config.js
+++ b/frontend/config/webpack.config.js
@@ -627,6 +627,12 @@ module.exports = function (webpackEnv) {
             // Make sure to add the new loader(s) before the "file" loader.
           ],
         },
+        {
+          test: /\.m?js$/,
+          resolve: {
+            fullySpecified: false
+          }
+        }
       ].filter(Boolean),
     },
     plugins: [
diff --git a/frontend/config/webpack.entry.js b/frontend/config/webpack.entry.js
index 9831b0908b..55ff95fbbb 100644
--- a/frontend/config/webpack.entry.js
+++ b/frontend/config/webpack.entry.js
@@ -2,6 +2,7 @@ const paths = require('./paths');
 
 const entryFiles = {
   tldrawEditor: '/tldrawEditor.js',
+  excalidrawEditor: '/excalidraw-editor.js',
   markdownEditor: '/index.js',
   plainMarkdownEditor: '/pages/plain-markdown-editor/index.js',
   TCAccept: '/tc-accept.js',
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 6451c8b985..6f10b0fc6a 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -13,6 +13,7 @@
         "@codemirror/view": "^6.34.1",
         "@emoji-mart/data": "^1.2.1",
         "@emoji-mart/react": "^1.1.1",
+        "@excalidraw/excalidraw": "^0.18.0",
         "@gatsbyjs/reach-router": "2.0.1",
         "@seafile/react-image-lightbox": "4.0.2",
         "@seafile/resumablejs": "1.1.16",
@@ -2260,6 +2261,11 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@braintree/sanitize-url": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz",
+      "integrity": "sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg=="
+    },
     "node_modules/@codemirror/autocomplete": {
       "version": "6.18.4",
       "resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.18.4.tgz",
@@ -3270,6 +3276,109 @@
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
+    "node_modules/@excalidraw/excalidraw": {
+      "version": "0.18.0",
+      "resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.18.0.tgz",
+      "integrity": "sha512-QkIiS+5qdy8lmDWTKsuy0sK/fen/LRDtbhm2lc2xcFcqhv2/zdg95bYnl+wnwwXGHo7kEmP65BSiMHE7PJ3Zpw==",
+      "dependencies": {
+        "@braintree/sanitize-url": "6.0.2",
+        "@excalidraw/laser-pointer": "1.3.1",
+        "@excalidraw/mermaid-to-excalidraw": "1.1.2",
+        "@excalidraw/random-username": "1.1.0",
+        "@radix-ui/react-popover": "1.1.6",
+        "@radix-ui/react-tabs": "1.0.2",
+        "browser-fs-access": "0.29.1",
+        "canvas-roundrect-polyfill": "0.0.1",
+        "clsx": "1.1.1",
+        "cross-env": "7.0.3",
+        "es6-promise-pool": "2.5.0",
+        "fractional-indexing": "3.2.0",
+        "fuzzy": "0.1.3",
+        "image-blob-reduce": "3.0.1",
+        "jotai": "2.11.0",
+        "jotai-scope": "0.7.2",
+        "lodash.debounce": "4.0.8",
+        "lodash.throttle": "4.1.1",
+        "nanoid": "3.3.3",
+        "open-color": "1.9.1",
+        "pako": "2.0.3",
+        "perfect-freehand": "1.2.0",
+        "pica": "7.1.1",
+        "png-chunk-text": "1.0.0",
+        "png-chunks-encode": "1.0.0",
+        "png-chunks-extract": "1.0.0",
+        "points-on-curve": "1.0.1",
+        "pwacompat": "2.0.17",
+        "roughjs": "4.6.4",
+        "sass": "1.51.0",
+        "tunnel-rat": "0.1.2"
+      },
+      "peerDependencies": {
+        "react": "^17.0.2 || ^18.2.0 || ^19.0.0",
+        "react-dom": "^17.0.2 || ^18.2.0 || ^19.0.0"
+      }
+    },
+    "node_modules/@excalidraw/excalidraw/node_modules/nanoid": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
+      "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/@excalidraw/excalidraw/node_modules/pako": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.3.tgz",
+      "integrity": "sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw=="
+    },
+    "node_modules/@excalidraw/laser-pointer": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@excalidraw/laser-pointer/-/laser-pointer-1.3.1.tgz",
+      "integrity": "sha512-psA1z1N2qeAfsORdXc9JmD2y4CmDwmuMRxnNdJHZexIcPwaNEyIpNcelw+QkL9rz9tosaN9krXuKaRqYpRAR6g=="
+    },
+    "node_modules/@excalidraw/markdown-to-text": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/@excalidraw/markdown-to-text/-/markdown-to-text-0.1.2.tgz",
+      "integrity": "sha512-1nDXBNAojfi3oSFwJswKREkFm5wrSjqay81QlyRv2pkITG/XYB5v+oChENVBQLcxQwX4IUATWvXM5BcaNhPiIg=="
+    },
+    "node_modules/@excalidraw/mermaid-to-excalidraw": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@excalidraw/mermaid-to-excalidraw/-/mermaid-to-excalidraw-1.1.2.tgz",
+      "integrity": "sha512-hAFv/TTIsOdoy0dL5v+oBd297SQ+Z88gZ5u99fCIFuEMHfQuPgLhU/ztKhFSTs7fISwVo6fizny/5oQRR3d4tQ==",
+      "dependencies": {
+        "@excalidraw/markdown-to-text": "0.1.2",
+        "mermaid": "10.9.3",
+        "nanoid": "4.0.2"
+      }
+    },
+    "node_modules/@excalidraw/mermaid-to-excalidraw/node_modules/nanoid": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz",
+      "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "bin": {
+        "nanoid": "bin/nanoid.js"
+      },
+      "engines": {
+        "node": "^14 || ^16 || >=18"
+      }
+    },
+    "node_modules/@excalidraw/random-username": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@excalidraw/random-username/-/random-username-1.1.0.tgz",
+      "integrity": "sha512-nULYsQxkWHnbmHvcs+efMkJ4/9TtvNyFeLyHdeGxW0zHs6P+jYVqcRff9A6Vq9w9JXeDRnRh2VKvTtS19GW2qA==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/@floating-ui/core": {
       "version": "1.6.9",
       "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
@@ -4920,6 +5029,189 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-tabs": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.2.tgz",
+      "integrity": "sha512-gOUwh+HbjCuL0UCo8kZ+kdUEG8QtpdO4sMQduJ34ZEz0r4922g9REOBM+vIsfwtGxSug4Yb1msJMJYN2Bk8TpQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/primitive": "1.0.0",
+        "@radix-ui/react-context": "1.0.0",
+        "@radix-ui/react-direction": "1.0.0",
+        "@radix-ui/react-id": "1.0.0",
+        "@radix-ui/react-presence": "1.0.0",
+        "@radix-ui/react-primitive": "1.0.1",
+        "@radix-ui/react-roving-focus": "1.0.2",
+        "@radix-ui/react-use-controllable-state": "1.0.0"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0",
+        "react-dom": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz",
+      "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10"
+      }
+    },
+    "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-collection": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.1.tgz",
+      "integrity": "sha512-uuiFbs+YCKjn3X1DTSx9G7BHApu4GHbi3kgiwsnFUbOKCrwejAJv4eE4Vc8C0Oaxt9T0aV4ox0WCOdx+39Xo+g==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-compose-refs": "1.0.0",
+        "@radix-ui/react-context": "1.0.0",
+        "@radix-ui/react-primitive": "1.0.1",
+        "@radix-ui/react-slot": "1.0.1"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0",
+        "react-dom": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-compose-refs": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz",
+      "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz",
+      "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-direction": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.0.tgz",
+      "integrity": "sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-id": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz",
+      "integrity": "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-use-layout-effect": "1.0.0"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz",
+      "integrity": "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-compose-refs": "1.0.0",
+        "@radix-ui/react-use-layout-effect": "1.0.0"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0",
+        "react-dom": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz",
+      "integrity": "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-slot": "1.0.1"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0",
+        "react-dom": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.2.tgz",
+      "integrity": "sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/primitive": "1.0.0",
+        "@radix-ui/react-collection": "1.0.1",
+        "@radix-ui/react-compose-refs": "1.0.0",
+        "@radix-ui/react-context": "1.0.0",
+        "@radix-ui/react-direction": "1.0.0",
+        "@radix-ui/react-id": "1.0.0",
+        "@radix-ui/react-primitive": "1.0.1",
+        "@radix-ui/react-use-callback-ref": "1.0.0",
+        "@radix-ui/react-use-controllable-state": "1.0.0"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0",
+        "react-dom": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz",
+      "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-compose-refs": "1.0.0"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-callback-ref": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz",
+      "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-controllable-state": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz",
+      "integrity": "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/react-use-callback-ref": "1.0.0"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
+    "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-layout-effect": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz",
+      "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
     "node_modules/@radix-ui/react-toast": {
       "version": "1.2.6",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
@@ -6372,6 +6664,24 @@
       "resolved": "https://registry.npmjs.org/@types/core-js/-/core-js-2.5.8.tgz",
       "integrity": "sha512-VgnAj6tIAhJhZdJ8/IpxdatM8G4OD3VWGlp6xIxUGENZlpbob9Ty4VVdC1FIEp0aK6DBscDDjyzy5FB60TuNqg=="
     },
+    "node_modules/@types/d3-scale": {
+      "version": "4.0.9",
+      "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+      "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+      "dependencies": {
+        "@types/d3-time": "*"
+      }
+    },
+    "node_modules/@types/d3-scale-chromatic": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+      "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="
+    },
+    "node_modules/@types/d3-time": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+      "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="
+    },
     "node_modules/@types/debug": {
       "version": "4.1.12",
       "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -7852,7 +8162,6 @@
       "version": "3.1.3",
       "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
       "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
-      "dev": true,
       "license": "ISC",
       "dependencies": {
         "normalize-path": "^3.0.0",
@@ -8687,7 +8996,6 @@
       "version": "2.3.0",
       "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
       "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8"
@@ -8812,7 +9120,6 @@
       "version": "3.0.3",
       "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz",
       "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "fill-range": "^7.1.1"
@@ -8828,6 +9135,11 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/browser-fs-access": {
+      "version": "0.29.1",
+      "resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.29.1.tgz",
+      "integrity": "sha512-LSvVX5e21LRrXqVMhqtAwj5xPgDb+fXAIH80NsnCQ9xuZPs2xWsOREi24RKgZa1XOiQRbcmVrv87+ulOKsgjxw=="
+    },
     "node_modules/browser-process-hrtime": {
       "version": "1.0.0",
       "resolved": "https://registry.npmmirror.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
@@ -9225,6 +9537,11 @@
       ],
       "license": "CC-BY-4.0"
     },
+    "node_modules/canvas-roundrect-polyfill": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/canvas-roundrect-polyfill/-/canvas-roundrect-polyfill-0.0.1.tgz",
+      "integrity": "sha512-yWq+R3U3jE+coOeEb3a3GgE2j/0MMiDKM/QpLb6h9ihf5fGY9UXtvK9o4vNqjWXoZz7/3EaSVU3IX53TvFFUOw=="
+    },
     "node_modules/canvas-size": {
       "version": "1.2.6",
       "resolved": "https://registry.npmjs.org/canvas-size/-/canvas-size-1.2.6.tgz",
@@ -9338,7 +9655,6 @@
       "version": "3.6.0",
       "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
       "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "anymatch": "~3.1.2",
@@ -9363,7 +9679,6 @@
       "version": "5.1.2",
       "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
       "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
-      "dev": true,
       "license": "ISC",
       "dependencies": {
         "is-glob": "^4.0.1"
@@ -9509,6 +9824,14 @@
         "node": ">=0.8"
       }
     },
+    "node_modules/clsx": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz",
+      "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/co": {
       "version": "4.6.0",
       "resolved": "https://registry.npmmirror.com/co/-/co-4.6.0.tgz",
@@ -9961,6 +10284,14 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/cose-base": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
+      "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==",
+      "dependencies": {
+        "layout-base": "^1.0.0"
+      }
+    },
     "node_modules/cosmiconfig": {
       "version": "7.1.0",
       "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
@@ -9977,6 +10308,14 @@
         "node": ">=10"
       }
     },
+    "node_modules/crc-32": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-0.3.0.tgz",
+      "integrity": "sha512-kucVIjOmMc1f0tv53BJ/5WIX+MGLcKuoBhnGqQrgKJNqLByb/sVMWfW/Aw6hw0jgcqjJ2pi9E5y32zOIpaUlsA==",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/create-ecdh": {
       "version": "4.0.4",
       "resolved": "https://registry.npmmirror.com/create-ecdh/-/create-ecdh-4.0.4.tgz",
@@ -10039,11 +10378,27 @@
       "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
       "license": "MIT"
     },
+    "node_modules/cross-env": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+      "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+      "dependencies": {
+        "cross-spawn": "^7.0.1"
+      },
+      "bin": {
+        "cross-env": "src/bin/cross-env.js",
+        "cross-env-shell": "src/bin/cross-env-shell.js"
+      },
+      "engines": {
+        "node": ">=10.14",
+        "npm": ">=6",
+        "yarn": ">=1"
+      }
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.6",
       "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
       "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "path-key": "^3.1.0",
@@ -10534,6 +10889,25 @@
       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
       "license": "MIT"
     },
+    "node_modules/cytoscape": {
+      "version": "3.31.1",
+      "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.31.1.tgz",
+      "integrity": "sha512-Hx5Mtb1+hnmAKaZZ/7zL1Y5HTFYOjdDswZy/jD+1WINRU8KVi1B7+vlHdsTwY+VCFucTreoyu1RDzQJ9u0d2Hw==",
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/cytoscape-cose-bilkent": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz",
+      "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==",
+      "dependencies": {
+        "cose-base": "^1.0.0"
+      },
+      "peerDependencies": {
+        "cytoscape": "^3.2.0"
+      }
+    },
     "node_modules/d3": {
       "version": "7.9.0",
       "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
@@ -10803,6 +11177,41 @@
         "node": ">=12"
       }
     },
+    "node_modules/d3-sankey": {
+      "version": "0.12.3",
+      "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
+      "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
+      "dependencies": {
+        "d3-array": "1 - 2",
+        "d3-shape": "^1.2.0"
+      }
+    },
+    "node_modules/d3-sankey/node_modules/d3-array": {
+      "version": "2.12.1",
+      "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
+      "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
+      "dependencies": {
+        "internmap": "^1.0.0"
+      }
+    },
+    "node_modules/d3-sankey/node_modules/d3-path": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
+      "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
+    },
+    "node_modules/d3-sankey/node_modules/d3-shape": {
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
+      "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
+      "dependencies": {
+        "d3-path": "1"
+      }
+    },
+    "node_modules/d3-sankey/node_modules/internmap": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
+      "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="
+    },
     "node_modules/d3-scale": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
@@ -10912,6 +11321,15 @@
         "node": ">=12"
       }
     },
+    "node_modules/dagre-d3-es": {
+      "version": "7.0.10",
+      "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz",
+      "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==",
+      "dependencies": {
+        "d3": "^7.8.2",
+        "lodash-es": "^4.17.21"
+      }
+    },
     "node_modules/damerau-levenshtein": {
       "version": "1.0.8",
       "resolved": "https://registry.npmmirror.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -11272,6 +11690,14 @@
       "dev": true,
       "license": "Apache-2.0"
     },
+    "node_modules/diff": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+      "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
+      "engines": {
+        "node": ">=0.3.1"
+      }
+    },
     "node_modules/diff-sequences": {
       "version": "29.6.3",
       "resolved": "https://registry.npmmirror.com/diff-sequences/-/diff-sequences-29.6.3.tgz",
@@ -11493,6 +11919,11 @@
         "url": "https://github.com/fb55/domhandler?sponsor=1"
       }
     },
+    "node_modules/dompurify": {
+      "version": "3.1.6",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz",
+      "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ=="
+    },
     "node_modules/domready": {
       "version": "1.0.8",
       "resolved": "https://registry.npmmirror.com/domready/-/domready-1.0.8.tgz",
@@ -11690,6 +12121,11 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/elkjs": {
+      "version": "0.9.3",
+      "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz",
+      "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ=="
+    },
     "node_modules/elliptic": {
       "version": "6.6.1",
       "resolved": "https://registry.npmmirror.com/elliptic/-/elliptic-6.6.1.tgz",
@@ -11985,6 +12421,14 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/es6-promise-pool": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/es6-promise-pool/-/es6-promise-pool-2.5.0.tgz",
+      "integrity": "sha512-VHErXfzR/6r/+yyzPKeBvO0lgjfC5cbDCQWjWwMZWSb6YU39TGIl51OUmCfWCq4ylMdJSB8zkz2vIuIeIxXApA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/escalade": {
       "version": "3.2.0",
       "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
@@ -13280,7 +13724,6 @@
       "version": "7.1.1",
       "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
       "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "to-regex-range": "^5.0.1"
@@ -13615,6 +14058,14 @@
         "url": "https://github.com/sponsors/rawify"
       }
     },
+    "node_modules/fractional-indexing": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/fractional-indexing/-/fractional-indexing-3.2.0.tgz",
+      "integrity": "sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ==",
+      "engines": {
+        "node": "^14.13.1 || >=16.0.0"
+      }
+    },
     "node_modules/fractional-indexing-jittered": {
       "version": "0.9.1",
       "resolved": "https://registry.npmjs.org/fractional-indexing-jittered/-/fractional-indexing-jittered-0.9.1.tgz",
@@ -13675,7 +14126,6 @@
       "version": "2.3.3",
       "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
       "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
-      "dev": true,
       "hasInstallScript": true,
       "license": "MIT",
       "optional": true,
@@ -13724,6 +14174,14 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/fuzzy": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz",
+      "integrity": "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==",
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
     "node_modules/gensync": {
       "version": "1.0.0-beta.2",
       "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -13991,6 +14449,11 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/glur": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/glur/-/glur-1.1.2.tgz",
+      "integrity": "sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA=="
+    },
     "node_modules/gopd": {
       "version": "1.2.0",
       "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
@@ -14033,6 +14496,11 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/hachure-fill": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
+      "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="
+    },
     "node_modules/handle-thing": {
       "version": "2.0.1",
       "resolved": "https://registry.npmmirror.com/handle-thing/-/handle-thing-2.0.1.tgz",
@@ -15310,6 +15778,14 @@
         "node": ">= 4"
       }
     },
+    "node_modules/image-blob-reduce": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/image-blob-reduce/-/image-blob-reduce-3.0.1.tgz",
+      "integrity": "sha512-/VmmWgIryG/wcn4TVrV7cC4mlfUC/oyiKIfSg5eVM3Ten/c1c34RJhMYKCWTnoSMHSqXLt3tsrBR4Q2HInvN+Q==",
+      "dependencies": {
+        "pica": "^7.1.0"
+      }
+    },
     "node_modules/image-size": {
       "version": "0.5.5",
       "resolved": "https://registry.npmmirror.com/image-size/-/image-size-0.5.5.tgz",
@@ -15332,6 +15808,12 @@
         "url": "https://opencollective.com/immer"
       }
     },
+    "node_modules/immutable": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
+      "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
+      "license": "MIT"
+    },
     "node_modules/import-fresh": {
       "version": "3.3.0",
       "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -15554,7 +16036,6 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz",
       "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "binary-extensions": "^2.0.0"
@@ -15716,7 +16197,6 @@
       "version": "2.1.1",
       "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
       "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
@@ -15782,7 +16262,6 @@
       "version": "4.0.3",
       "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
       "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "is-extglob": "^2.1.1"
@@ -15837,7 +16316,6 @@
       "version": "7.0.0",
       "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
       "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=0.12.0"
@@ -16117,7 +16595,6 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
       "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
-      "dev": true,
       "license": "ISC"
     },
     "node_modules/isobject": {
@@ -17775,6 +18252,35 @@
         "jiti": "bin/jiti.js"
       }
     },
+    "node_modules/jotai": {
+      "version": "2.11.0",
+      "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.11.0.tgz",
+      "integrity": "sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ==",
+      "engines": {
+        "node": ">=12.20.0"
+      },
+      "peerDependencies": {
+        "@types/react": ">=17.0.0",
+        "react": ">=17.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/jotai-scope": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/jotai-scope/-/jotai-scope-0.7.2.tgz",
+      "integrity": "sha512-Gwed97f3dDObrO43++2lRcgOqw4O2sdr4JCjP/7eHK1oPACDJ7xKHGScpJX9XaflU+KBHXF+VhwECnzcaQiShg==",
+      "peerDependencies": {
+        "jotai": ">=2.9.2",
+        "react": ">=17.0.0"
+      }
+    },
     "node_modules/js-base64": {
       "version": "2.6.4",
       "resolved": "https://registry.npmmirror.com/js-base64/-/js-base64-2.6.4.tgz",
@@ -18017,6 +18523,11 @@
         "json-buffer": "3.0.1"
       }
     },
+    "node_modules/khroma": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz",
+      "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
+    },
     "node_modules/kind-of": {
       "version": "6.0.3",
       "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz",
@@ -18077,6 +18588,11 @@
         "shell-quote": "^1.8.1"
       }
     },
+    "node_modules/layout-base": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
+      "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="
+    },
     "node_modules/leven": {
       "version": "3.1.0",
       "resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz",
@@ -18177,7 +18693,6 @@
       "version": "4.0.8",
       "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
       "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/lodash.defaults": {
@@ -18772,6 +19287,519 @@
         "node": ">= 8"
       }
     },
+    "node_modules/mermaid": {
+      "version": "10.9.3",
+      "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.3.tgz",
+      "integrity": "sha512-V80X1isSEvAewIL3xhmz/rVmc27CVljcsbWxkxlWJWY/1kQa4XOABqpDl2qQLGKzpKm6WbTfUEKImBlUfFYArw==",
+      "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 <3.1.7",
+        "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"
+      }
+    },
+    "node_modules/mermaid/node_modules/@types/mdast": {
+      "version": "3.0.15",
+      "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz",
+      "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==",
+      "dependencies": {
+        "@types/unist": "^2"
+      }
+    },
+    "node_modules/mermaid/node_modules/@types/unist": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+      "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="
+    },
+    "node_modules/mermaid/node_modules/dayjs": {
+      "version": "1.11.13",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
+      "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
+    },
+    "node_modules/mermaid/node_modules/mdast-util-from-markdown": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz",
+      "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==",
+      "dependencies": {
+        "@types/mdast": "^3.0.0",
+        "@types/unist": "^2.0.0",
+        "decode-named-character-reference": "^1.0.0",
+        "mdast-util-to-string": "^3.1.0",
+        "micromark": "^3.0.0",
+        "micromark-util-decode-numeric-character-reference": "^1.0.0",
+        "micromark-util-decode-string": "^1.0.0",
+        "micromark-util-normalize-identifier": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0",
+        "unist-util-stringify-position": "^3.0.0",
+        "uvu": "^0.5.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified"
+      }
+    },
+    "node_modules/mermaid/node_modules/mdast-util-to-string": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz",
+      "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==",
+      "dependencies": {
+        "@types/mdast": "^3.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified"
+      }
+    },
+    "node_modules/mermaid/node_modules/micromark": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz",
+      "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "@types/debug": "^4.0.0",
+        "debug": "^4.0.0",
+        "decode-named-character-reference": "^1.0.0",
+        "micromark-core-commonmark": "^1.0.1",
+        "micromark-factory-space": "^1.0.0",
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-chunked": "^1.0.0",
+        "micromark-util-combine-extensions": "^1.0.0",
+        "micromark-util-decode-numeric-character-reference": "^1.0.0",
+        "micromark-util-encode": "^1.0.0",
+        "micromark-util-normalize-identifier": "^1.0.0",
+        "micromark-util-resolve-all": "^1.0.0",
+        "micromark-util-sanitize-uri": "^1.0.0",
+        "micromark-util-subtokenize": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.1",
+        "uvu": "^0.5.0"
+      }
+    },
+    "node_modules/mermaid/node_modules/micromark-core-commonmark": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz",
+      "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "decode-named-character-reference": "^1.0.0",
+        "micromark-factory-destination": "^1.0.0",
+        "micromark-factory-label": "^1.0.0",
+        "micromark-factory-space": "^1.0.0",
+        "micromark-factory-title": "^1.0.0",
+        "micromark-factory-whitespace": "^1.0.0",
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-chunked": "^1.0.0",
+        "micromark-util-classify-character": "^1.0.0",
+        "micromark-util-html-tag-name": "^1.0.0",
+        "micromark-util-normalize-identifier": "^1.0.0",
+        "micromark-util-resolve-all": "^1.0.0",
+        "micromark-util-subtokenize": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.1",
+        "uvu": "^0.5.0"
+      }
+    },
+    "node_modules/mermaid/node_modules/micromark-factory-destination": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz",
+      "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "node_modules/mermaid/node_modules/micromark-factory-label": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz",
+      "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0",
+        "uvu": "^0.5.0"
+      }
+    },
+    "node_modules/mermaid/node_modules/micromark-factory-space": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz",
+      "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "node_modules/mermaid/node_modules/micromark-factory-title": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz",
+      "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-factory-space": "^1.0.0",
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "node_modules/mermaid/node_modules/micromark-factory-whitespace": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz",
+      "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-factory-space": "^1.0.0",
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "node_modules/mermaid/node_modules/micromark-util-character": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz",
+      "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "node_modules/mermaid/node_modules/micromark-util-chunked": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz",
+      "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-symbol": "^1.0.0"
+      }
+    },
+    "node_modules/mermaid/node_modules/micromark-util-classify-character": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz",
+      "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "node_modules/mermaid/node_modules/micromark-util-combine-extensions": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz",
+      "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-chunked": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "node_modules/mermaid/node_modules/micromark-util-decode-numeric-character-reference": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz",
+      "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-symbol": "^1.0.0"
+      }
+    },
+    "node_modules/mermaid/node_modules/micromark-util-decode-string": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz",
+      "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "decode-named-character-reference": "^1.0.0",
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-decode-numeric-character-reference": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0"
+      }
+    },
+    "node_modules/mermaid/node_modules/micromark-util-encode": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz",
+      "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ]
+    },
+    "node_modules/mermaid/node_modules/micromark-util-html-tag-name": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz",
+      "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ]
+    },
+    "node_modules/mermaid/node_modules/micromark-util-normalize-identifier": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz",
+      "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-symbol": "^1.0.0"
+      }
+    },
+    "node_modules/mermaid/node_modules/micromark-util-resolve-all": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz",
+      "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "node_modules/mermaid/node_modules/micromark-util-sanitize-uri": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz",
+      "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-encode": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0"
+      }
+    },
+    "node_modules/mermaid/node_modules/micromark-util-subtokenize": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz",
+      "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-chunked": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0",
+        "uvu": "^0.5.0"
+      }
+    },
+    "node_modules/mermaid/node_modules/micromark-util-symbol": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz",
+      "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ]
+    },
+    "node_modules/mermaid/node_modules/micromark-util-types": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz",
+      "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ]
+    },
+    "node_modules/mermaid/node_modules/unist-util-stringify-position": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz",
+      "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==",
+      "dependencies": {
+        "@types/unist": "^2.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified"
+      }
+    },
     "node_modules/methods": {
       "version": "1.1.2",
       "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz",
@@ -19611,6 +20639,14 @@
         "mpd-to-m3u8-json": "bin/parse.js"
       }
     },
+    "node_modules/mri": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
+      "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/ms": {
       "version": "2.1.3",
       "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
@@ -19631,6 +20667,15 @@
         "multicast-dns": "cli.js"
       }
     },
+    "node_modules/multimath": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/multimath/-/multimath-2.0.0.tgz",
+      "integrity": "sha512-toRx66cAMJ+Ccz7pMIg38xSIrtnbozk0dchXezwQDMgQmbGpfxjtv68H+L00iFL8hxDaVjrmwAFSb3I6bg8Q2g==",
+      "dependencies": {
+        "glur": "^1.1.2",
+        "object-assign": "^4.1.1"
+      }
+    },
     "node_modules/mux.js": {
       "version": "6.0.1",
       "resolved": "https://registry.npmmirror.com/mux.js/-/mux.js-6.0.1.tgz",
@@ -19886,11 +20931,15 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/non-layered-tidy-tree-layout": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz",
+      "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw=="
+    },
     "node_modules/normalize-path": {
       "version": "3.0.0",
       "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
       "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
@@ -20277,6 +21326,11 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/open-color": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/open-color/-/open-color-1.9.1.tgz",
+      "integrity": "sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw=="
+    },
     "node_modules/optionator": {
       "version": "0.9.4",
       "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz",
@@ -20488,6 +21542,11 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/path-data-parser": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
+      "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="
+    },
     "node_modules/path-exists": {
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
@@ -20512,7 +21571,6 @@
       "version": "3.1.1",
       "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
       "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8"
@@ -20581,12 +21639,29 @@
         "node": ">=0.12"
       }
     },
+    "node_modules/perfect-freehand": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.2.0.tgz",
+      "integrity": "sha512-h/0ikF1M3phW7CwpZ5MMvKnfpHficWoOEyr//KVNTxV4F6deRK1eYMtHyBKEAKFK0aXIEUK9oBvlF6PNXMDsAw=="
+    },
     "node_modules/performance-now": {
       "version": "2.1.0",
       "resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz",
       "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
       "license": "MIT"
     },
+    "node_modules/pica": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/pica/-/pica-7.1.1.tgz",
+      "integrity": "sha512-WY73tMvNzXWEld2LicT9Y260L43isrZ85tPuqRyvtkljSDLmnNFQmZICt4xUJMVulmcc6L9O7jbBrtx3DOz/YQ==",
+      "dependencies": {
+        "glur": "^1.1.2",
+        "inherits": "^2.0.3",
+        "multimath": "^2.0.0",
+        "object-assign": "^4.1.1",
+        "webworkify": "^1.5.0"
+      }
+    },
     "node_modules/picocolors": {
       "version": "1.1.1",
       "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
@@ -20597,7 +21672,6 @@
       "version": "2.3.1",
       "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
       "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8.6"
@@ -20714,6 +21788,47 @@
         "node": ">=4"
       }
     },
+    "node_modules/png-chunk-text": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/png-chunk-text/-/png-chunk-text-1.0.0.tgz",
+      "integrity": "sha512-DEROKU3SkkLGWNMzru3xPVgxyd48UGuMSZvioErCure6yhOc/pRH2ZV+SEn7nmaf7WNf3NdIpH+UTrRdKyq9Lw=="
+    },
+    "node_modules/png-chunks-encode": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/png-chunks-encode/-/png-chunks-encode-1.0.0.tgz",
+      "integrity": "sha512-J1jcHgbQRsIIgx5wxW9UmCymV3wwn4qCCJl6KYgEU/yHCh/L2Mwq/nMOkRPtmV79TLxRZj5w3tH69pvygFkDqA==",
+      "dependencies": {
+        "crc-32": "^0.3.0",
+        "sliced": "^1.0.1"
+      }
+    },
+    "node_modules/png-chunks-extract": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/png-chunks-extract/-/png-chunks-extract-1.0.0.tgz",
+      "integrity": "sha512-ZiVwF5EJ0DNZyzAqld8BP1qyJBaGOFaq9zl579qfbkcmOwWLLO4I9L8i2O4j3HkI6/35i0nKG2n+dZplxiT89Q==",
+      "dependencies": {
+        "crc-32": "^0.3.0"
+      }
+    },
+    "node_modules/points-on-curve": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-1.0.1.tgz",
+      "integrity": "sha512-3nmX4/LIiyuwGLwuUrfhTlDeQFlAhi7lyK/zcRNGhalwapDWgAGR82bUpmn2mA03vII3fvNCG8jAONzKXwpxAg=="
+    },
+    "node_modules/points-on-path": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz",
+      "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==",
+      "dependencies": {
+        "path-data-parser": "0.1.0",
+        "points-on-curve": "0.2.0"
+      }
+    },
+    "node_modules/points-on-path/node_modules/points-on-curve": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
+      "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="
+    },
     "node_modules/posix-character-classes": {
       "version": "0.1.1",
       "resolved": "https://registry.npmmirror.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
@@ -22521,6 +23636,11 @@
         "node": ">=6"
       }
     },
+    "node_modules/pwacompat": {
+      "version": "2.0.17",
+      "resolved": "https://registry.npmjs.org/pwacompat/-/pwacompat-2.0.17.tgz",
+      "integrity": "sha512-6Du7IZdIy7cHiv7AhtDy4X2QRM8IAD5DII69mt5qWibC2d15ZU8DmBG1WdZKekG11cChSu4zkSUGPF9sweOl6w=="
+    },
     "node_modules/q": {
       "version": "1.5.1",
       "resolved": "https://registry.npmmirror.com/q/-/q-1.5.1.tgz",
@@ -23345,7 +24465,6 @@
       "version": "3.6.0",
       "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
       "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "picomatch": "^2.2.1"
@@ -25005,6 +26124,22 @@
         "randombytes": "^2.1.0"
       }
     },
+    "node_modules/roughjs": {
+      "version": "4.6.4",
+      "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.4.tgz",
+      "integrity": "sha512-s6EZ0BntezkFYMf/9mGn7M8XGIoaav9QQBCnJROWB3brUWQ683Q2LbRD/hq0Z3bAJ/9NVpU/5LpiTWvQMyLDhw==",
+      "dependencies": {
+        "hachure-fill": "^0.5.2",
+        "path-data-parser": "^0.1.0",
+        "points-on-curve": "^0.2.0",
+        "points-on-path": "^0.2.1"
+      }
+    },
+    "node_modules/roughjs/node_modules/points-on-curve": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
+      "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="
+    },
     "node_modules/rrweb-cssom": {
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
@@ -25049,6 +26184,17 @@
       "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
       "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
     },
+    "node_modules/sade": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
+      "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
+      "dependencies": {
+        "mri": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/safe-array-concat": {
       "version": "1.1.3",
       "resolved": "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@@ -25151,6 +26297,23 @@
       "dev": true,
       "license": "CC0-1.0"
     },
+    "node_modules/sass": {
+      "version": "1.51.0",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.51.0.tgz",
+      "integrity": "sha512-haGdpTgywJTvHC2b91GSq+clTKGbtkkZmVAb82jZQN/wTy6qs8DdFm2lhEQbEwrY0QDRgSQ3xDurqM977C3noA==",
+      "license": "MIT",
+      "dependencies": {
+        "chokidar": ">=3.0.0 <4.0.0",
+        "immutable": "^4.0.0",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      },
+      "bin": {
+        "sass": "sass.js"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/sass-loader": {
       "version": "12.6.0",
       "resolved": "https://registry.npmmirror.com/sass-loader/-/sass-loader-12.6.0.tgz",
@@ -25722,7 +26885,6 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
       "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "shebang-regex": "^3.0.0"
@@ -25735,7 +26897,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
       "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8"
@@ -25913,6 +27074,11 @@
       "integrity": "sha512-qs3NZ1INIS+H+yeo7cD9pDfwYV/jqRh1JG9S9zYrNudkoUQg7OL7ziXqRKu+InFjUIDoP2o6HIkLYMh1pcWgyQ==",
       "license": "MIT"
     },
+    "node_modules/sliced": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
+      "integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA=="
+    },
     "node_modules/slugid": {
       "version": "3.2.0",
       "resolved": "https://registry.npmmirror.com/slugid/-/slugid-3.2.0.tgz",
@@ -27983,7 +29149,6 @@
       "version": "5.0.1",
       "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
       "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "is-number": "^7.0.0"
@@ -28139,6 +29304,14 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/ts-dedent": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
+      "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
+      "engines": {
+        "node": ">=6.10"
+      }
+    },
     "node_modules/ts-interface-checker": {
       "version": "0.1.13",
       "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@@ -28233,6 +29406,14 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/tunnel-rat": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
+      "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==",
+      "dependencies": {
+        "zustand": "^4.3.2"
+      }
+    },
     "node_modules/tween-functions": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz",
@@ -28995,6 +30176,14 @@
         }
       }
     },
+    "node_modules/use-sync-external-store": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
+      "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
     "node_modules/util": {
       "version": "0.12.5",
       "resolved": "https://registry.npmmirror.com/util/-/util-0.12.5.tgz",
@@ -29061,6 +30250,31 @@
         "uuid": "dist/bin/uuid"
       }
     },
+    "node_modules/uvu": {
+      "version": "0.5.6",
+      "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",
+      "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==",
+      "dependencies": {
+        "dequal": "^2.0.0",
+        "diff": "^5.0.0",
+        "kleur": "^4.0.3",
+        "sade": "^1.7.3"
+      },
+      "bin": {
+        "uvu": "bin.js"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/uvu/node_modules/kleur": {
+      "version": "4.1.5",
+      "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+      "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/v8-to-istanbul": {
       "version": "8.1.1",
       "resolved": "https://registry.npmmirror.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz",
@@ -29282,6 +30496,11 @@
       "dev": true,
       "license": "Apache-2.0"
     },
+    "node_modules/web-worker": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",
+      "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw=="
+    },
     "node_modules/webidl-conversions": {
       "version": "6.1.0",
       "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
@@ -29580,6 +30799,11 @@
         "node": ">=0.8.0"
       }
     },
+    "node_modules/webworkify": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/webworkify/-/webworkify-1.5.0.tgz",
+      "integrity": "sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g=="
+    },
     "node_modules/whatwg-encoding": {
       "version": "1.0.5",
       "resolved": "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz",
@@ -29635,7 +30859,6 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
       "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
-      "dev": true,
       "license": "ISC",
       "dependencies": {
         "isexe": "^2.0.0"
@@ -30288,6 +31511,33 @@
         "babel-runtime": "6.x"
       }
     },
+    "node_modules/zustand": {
+      "version": "4.5.6",
+      "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz",
+      "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==",
+      "dependencies": {
+        "use-sync-external-store": "^1.2.2"
+      },
+      "engines": {
+        "node": ">=12.7.0"
+      },
+      "peerDependencies": {
+        "@types/react": ">=16.8",
+        "immer": ">=9.0.6",
+        "react": ">=16.8"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "immer": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/zwitch": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 11c3110509..90f2e72974 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -8,6 +8,7 @@
     "@codemirror/view": "^6.34.1",
     "@emoji-mart/data": "^1.2.1",
     "@emoji-mart/react": "^1.1.1",
+    "@excalidraw/excalidraw": "^0.18.0",
     "@gatsbyjs/reach-router": "2.0.1",
     "@seafile/react-image-lightbox": "4.0.2",
     "@seafile/resumablejs": "1.1.16",
diff --git a/frontend/src/components/dirent-grid-view/dirent-grid-view.js b/frontend/src/components/dirent-grid-view/dirent-grid-view.js
index f97d862a49..20bfc860df 100644
--- a/frontend/src/components/dirent-grid-view/dirent-grid-view.js
+++ b/frontend/src/components/dirent-grid-view/dirent-grid-view.js
@@ -1,6 +1,6 @@
 import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
-import { siteRoot, username, enableSeadoc, thumbnailDefaultSize, thumbnailSizeForOriginal, gettext, fileServerRoot, enableWhiteboard, useGoFileserver } from '../../utils/constants';
+import { siteRoot, username, enableSeadoc, thumbnailDefaultSize, thumbnailSizeForOriginal, gettext, fileServerRoot, enableWhiteboard, useGoFileserver, enableExcalidraw } from '../../utils/constants';
 import { Utils } from '../../utils/utils';
 import { seafileAPI } from '../../utils/seafile-api';
 import URLDecorator from '../../utils/url-decorator';
@@ -412,6 +412,9 @@ class DirentGridView extends React.Component {
       case 'New Whiteboard File':
         this.onCreateFileToggle('.draw');
         break;
+      case 'New Excalidraw File':
+        this.onCreateFileToggle('.exdraw');
+        break;
       case 'New SeaDoc File':
         this.onCreateFileToggle('.sdoc');
         break;
@@ -734,13 +737,15 @@ class DirentGridView extends React.Component {
     if (!['admin', 'rw'].includes(this.props.userPerm)) return;
 
     const {
-      NEW_FOLDER, NEW_FILE,
+      NEW_FOLDER,
+      NEW_FILE,
       NEW_MARKDOWN_FILE,
       NEW_EXCEL_FILE,
       NEW_POWERPOINT_FILE,
       NEW_WORD_FILE,
       NEW_SEADOC_FILE,
-      NEW_TLDRAW_FILE
+      NEW_TLDRAW_FILE,
+      NEW_EXCALIDRAW_FILE
     } = TextTranslation;
 
     let direntsContainerMenuList = [
@@ -763,6 +768,10 @@ class DirentGridView extends React.Component {
       direntsContainerMenuList.push(NEW_TLDRAW_FILE);
     }
 
+    if (enableExcalidraw) {
+      direntsContainerMenuList.push(NEW_EXCALIDRAW_FILE);
+    }
+
     if (selectedDirentList.length === 0) {
       if (!hasCustomPermission('create')) return;
       this.handleContextClick(event, DIRENT_GRID_CONTAINER_MENU_ID, direntsContainerMenuList);
diff --git a/frontend/src/components/dirent-list-view/dirent-list-view.js b/frontend/src/components/dirent-list-view/dirent-list-view.js
index d8eb302f59..d25614a283 100644
--- a/frontend/src/components/dirent-list-view/dirent-list-view.js
+++ b/frontend/src/components/dirent-list-view/dirent-list-view.js
@@ -1,7 +1,7 @@
 import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import classnames from 'classnames';
-import { siteRoot, gettext, username, enableSeadoc, thumbnailSizeForOriginal, thumbnailDefaultSize, fileServerRoot, enableWhiteboard, useGoFileserver } from '../../utils/constants';
+import { siteRoot, gettext, username, enableSeadoc, thumbnailSizeForOriginal, thumbnailDefaultSize, fileServerRoot, enableWhiteboard, useGoFileserver, enableExcalidraw } from '../../utils/constants';
 import { Utils } from '../../utils/utils';
 import TextTranslation from '../../utils/text-translation';
 import URLDecorator from '../../utils/url-decorator';
@@ -431,7 +431,8 @@ class DirentListView extends React.Component {
       NEW_POWERPOINT_FILE,
       NEW_WORD_FILE,
       NEW_SEADOC_FILE,
-      NEW_TLDRAW_FILE
+      NEW_TLDRAW_FILE,
+      NEW_EXCALIDRAW_FILE,
     } = TextTranslation;
 
     const direntsContainerMenuList = [
@@ -452,6 +453,10 @@ class DirentListView extends React.Component {
       direntsContainerMenuList.push(NEW_TLDRAW_FILE);
     }
 
+    if (enableExcalidraw) {
+      direntsContainerMenuList.push(NEW_EXCALIDRAW_FILE);
+    }
+
     if (this.props.selectedDirentList.length === 0) {
       let id = 'dirent-container-menu';
 
@@ -529,6 +534,9 @@ class DirentListView extends React.Component {
       case 'New Whiteboard File':
         this.onCreateFileToggle('.draw');
         break;
+      case 'New Excalidraw File':
+        this.onCreateFileToggle('.exdraw');
+        break;
       case 'New SeaDoc File':
         this.onCreateFileToggle('.sdoc');
         break;
diff --git a/frontend/src/components/dirent-list-view/dirent-none-view.js b/frontend/src/components/dirent-list-view/dirent-none-view.js
index 75658ca759..c09dd2161c 100644
--- a/frontend/src/components/dirent-list-view/dirent-none-view.js
+++ b/frontend/src/components/dirent-list-view/dirent-none-view.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { enableSeadoc, gettext, enableWhiteboard } from '../../utils/constants';
+import { enableSeadoc, gettext, enableWhiteboard, enableExcalidraw } from '../../utils/constants';
 import Loading from '../loading';
 import ModalPortal from '../modal-portal';
 import CreateFile from '../../components/dialog/create-file-dialog';
@@ -74,7 +74,8 @@ class DirentNoneView extends React.Component {
       NEW_POWERPOINT_FILE,
       NEW_WORD_FILE,
       NEW_SEADOC_FILE,
-      NEW_TLDRAW_FILE
+      NEW_TLDRAW_FILE,
+      NEW_EXCALIDRAW_FILE
     } = TextTranslation;
     const direntsContainerMenuList = [
       NEW_FOLDER, NEW_FILE, 'Divider',
@@ -92,6 +93,9 @@ class DirentNoneView extends React.Component {
     if (enableWhiteboard) {
       direntsContainerMenuList.push(NEW_TLDRAW_FILE);
     }
+    if (enableExcalidraw) {
+      direntsContainerMenuList.push(NEW_EXCALIDRAW_FILE);
+    }
     let id = 'dirent-container-menu';
     if (isCustomPermission) {
       const { create: canCreate } = customPermission.permission;
diff --git a/frontend/src/components/toolbar/dir-operation-toolbar.js b/frontend/src/components/toolbar/dir-operation-toolbar.js
index f8d9eea040..6bc70a9dad 100644
--- a/frontend/src/components/toolbar/dir-operation-toolbar.js
+++ b/frontend/src/components/toolbar/dir-operation-toolbar.js
@@ -2,7 +2,7 @@ import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
 import { Utils } from '../../utils/utils';
-import { enableSeadoc, enableWhiteboard, gettext } from '../../utils/constants';
+import { enableExcalidraw, enableSeadoc, enableWhiteboard, gettext } from '../../utils/constants';
 import ModalPortal from '../modal-portal';
 import CreateFolder from '../../components/dialog/create-folder-dialog';
 import CreateFile from '../../components/dialog/create-file-dialog';
@@ -115,6 +115,13 @@ class DirOperationToolbar extends React.Component {
     });
   };
 
+  onCreateExcalidrawToggle = () => {
+    this.setState({
+      isCreateFileDialogShow: !this.state.isCreateFileDialogShow,
+      fileType: '.exdraw'
+    });
+  };
+
   onCreateSeaDocToggle = () => {
     this.setState({
       isCreateFileDialogShow: !this.state.isCreateFileDialogShow,
@@ -266,6 +273,9 @@ class DirOperationToolbar extends React.Component {
         if (enableWhiteboard) {
           newSubOpList.push({ 'text': gettext('New Whiteboard File'), 'onClick': this.onCreateTldrawToggle });
         }
+        if (enableExcalidraw) {
+          newSubOpList.push({ 'text': gettext('New Excalidraw File'), 'onClick': this.onCreateExcalidrawToggle });
+        }
         opList.push({
           'icon': 'new',
           'text': gettext('New'),
diff --git a/frontend/src/excalidraw-editor.js b/frontend/src/excalidraw-editor.js
new file mode 100644
index 0000000000..9019d8385c
--- /dev/null
+++ b/frontend/src/excalidraw-editor.js
@@ -0,0 +1,11 @@
+import React, { Suspense } from 'react';
+import { createRoot } from 'react-dom/client';
+import Loading from './components/loading';
+import ExcaliEditor from './pages/excalidraw-editor';
+
+const root = createRoot(document.getElementById('wrapper'));
+root.render(
+  <Suspense fallback={<Loading />}>
+    <ExcaliEditor />
+  </Suspense>
+);
diff --git a/frontend/src/pages/excalidraw-editor/constants.js b/frontend/src/pages/excalidraw-editor/constants.js
new file mode 100644
index 0000000000..8407f1f02b
--- /dev/null
+++ b/frontend/src/pages/excalidraw-editor/constants.js
@@ -0,0 +1,15 @@
+export const SAVE_INTERVAL_TIME = 3 * 60 * 1000;
+
+export const langList = {
+  'zh-cn': 'zh-CN',
+  'en': 'en',
+  'zh-tw': 'zh-TW',
+  'ru': 'ru-RU',
+  'it': 'it-IT',
+  'fr': 'fr-FR',
+  'es-ms': 'en',
+  'es-ar': 'en',
+  'es': 'es-ES',
+  'de': 'de-DE',
+  'cs': 'cs-CZ',
+};
diff --git a/frontend/src/pages/excalidraw-editor/editor-api.js b/frontend/src/pages/excalidraw-editor/editor-api.js
new file mode 100644
index 0000000000..f2524cca2f
--- /dev/null
+++ b/frontend/src/pages/excalidraw-editor/editor-api.js
@@ -0,0 +1,26 @@
+import { seafileAPI } from '../../utils/seafile-api';
+import { Utils } from '../../utils/utils';
+
+const { repoID, filePath, fileName } = window.app.pageOptions;
+let dirPath = Utils.getDirName(filePath);
+
+class EditorApi {
+
+  saveContent(content) {
+    return seafileAPI.getUpdateLink(repoID, dirPath).then((res) => {
+      const uploadLink = res.data;
+      return seafileAPI.updateFile(uploadLink, filePath, fileName, content);
+    });
+  }
+
+  getFileContent = () => {
+    return seafileAPI.getFileDownloadLink(repoID, filePath).then(res => {
+      const downLoadUrl = res.data;
+      return seafileAPI.getFileContent(downLoadUrl);
+    });
+  };
+}
+
+const editorApi = new EditorApi();
+
+export default editorApi;
diff --git a/frontend/src/pages/excalidraw-editor/index.css b/frontend/src/pages/excalidraw-editor/index.css
new file mode 100644
index 0000000000..7141caaa9b
--- /dev/null
+++ b/frontend/src/pages/excalidraw-editor/index.css
@@ -0,0 +1,8 @@
+.dropdown-menu {
+  display: block !important;
+}
+
+.excalidraw .dropdown-menu {
+  --bs-dropdown-padding-x: auto;
+  border: unset;
+}
\ No newline at end of file
diff --git a/frontend/src/pages/excalidraw-editor/index.js b/frontend/src/pages/excalidraw-editor/index.js
new file mode 100644
index 0000000000..10c0f457e2
--- /dev/null
+++ b/frontend/src/pages/excalidraw-editor/index.js
@@ -0,0 +1,86 @@
+import React, { useCallback, useState, useEffect, useRef } from 'react';
+import SimpleEditor from './simple-editor';
+import editorApi from './editor-api';
+import isHotkey from 'is-hotkey';
+import { gettext } from '../../utils/constants';
+import toaster from '../../components/toast';
+import { SAVE_INTERVAL_TIME } from './constants';
+
+import './index.css';
+
+const ExcaliEditor = () => {
+  const [fileContent, setFileContent] = useState(null);
+  const editorRef = useRef(null);
+  const isChangedRef = useRef(false);
+  const [isFetching, setIsFetching] = useState(true);
+
+  useEffect(() => {
+    editorApi.getFileContent().then(res => {
+      if (res.data?.appState?.collaborators && !Array.isArray(res.data.appState.collaborators)) {
+        // collaborators.forEach is not a function
+        res.data['appState']['collaborators'] = [];
+      }
+      setFileContent(res.data);
+      setIsFetching(false);
+    });
+  }, []);
+
+  const saveSceneContent = useCallback(async () => {
+    if (isChangedRef.current) {
+      try {
+        await editorApi.saveContent(JSON.stringify(editorRef.current));
+        isChangedRef.current = false;
+        toaster.success(gettext('Successfully saved'), { duration: 2 });
+      } catch {
+        toaster.danger(gettext('Failed to save'), { duration: 2 });
+      }
+    }
+  }, []);
+
+  useEffect(() => {
+    const handleHotkeySave = (event) => {
+      if (isHotkey('mod+s', event)) {
+        event.preventDefault();
+      }
+    };
+
+    document.addEventListener('keydown', handleHotkeySave, true);
+    return () => {
+      document.removeEventListener('keydown', handleHotkeySave, true);
+    };
+  }, [saveSceneContent]);
+
+  useEffect(() => {
+    const saveInterval = setInterval(() => {
+      if (isChangedRef.current) {
+        editorApi.saveContent(JSON.stringify(editorRef.current)).then(res => {
+          isChangedRef.current = false;
+        });
+      }
+    }, SAVE_INTERVAL_TIME);
+
+    return () => {
+      clearInterval(saveInterval);
+    };
+  }, [saveSceneContent]);
+
+  const onSaveContent = useCallback(() => {
+    saveSceneContent();
+  }, [saveSceneContent]);
+
+  const onChangeContent = useCallback((elements) => {
+    editorRef.current = { elements };
+    isChangedRef.current = true;
+  }, []);
+
+  return (
+    <SimpleEditor
+      isFetching={isFetching}
+      sceneContent={fileContent}
+      onSaveContent={onSaveContent}
+      onChangeContent={onChangeContent}
+    />
+  );
+};
+
+export default ExcaliEditor;
diff --git a/frontend/src/pages/excalidraw-editor/simple-editor.js b/frontend/src/pages/excalidraw-editor/simple-editor.js
new file mode 100644
index 0000000000..ce6a0e5c03
--- /dev/null
+++ b/frontend/src/pages/excalidraw-editor/simple-editor.js
@@ -0,0 +1,75 @@
+import React, { useEffect, useState } from 'react';
+import { Excalidraw, MainMenu } from '@excalidraw/excalidraw';
+import isHotkey from 'is-hotkey';
+import CodeMirrorLoading from '../../components/code-mirror-loading';
+import { langList } from './constants';
+
+import '@excalidraw/excalidraw/index.css';
+
+const SimpleEditor = ({
+  sceneContent = null,
+  onChangeContent,
+  onSaveContent,
+  isFetching
+}) => {
+  const [excalidrawAPI, setExcalidrawAPI] = useState(null);
+  const UIOptions = {
+    canvasActions: {
+      saveToActiveFile: false,
+      LoadScene: false
+    },
+    tools: { image: false },
+  };
+
+  const handleChange = () => {
+    const elements = excalidrawAPI.getSceneElements();
+    onChangeContent(elements);
+  };
+
+  useEffect(() => {
+    const handleHotkeySave = (event) => {
+      if (isHotkey('mod+s', event)) {
+        event.preventDefault();
+        onSaveContent(excalidrawAPI.getSceneElements());
+      }
+    };
+
+    document.addEventListener('keydown', handleHotkeySave, true);
+    return () => {
+      document.removeEventListener('keydown', handleHotkeySave, true);
+    };
+  }, [excalidrawAPI, onSaveContent]);
+
+  if (isFetching) {
+    return (
+      <div className='excali-container'>
+        <CodeMirrorLoading />
+      </div>
+    );
+  }
+
+  return (
+    <>
+      <div className='excali-container' style={{ height: '100vh', width: '100vw' }}>
+        <Excalidraw
+          initialData={sceneContent}
+          excalidrawAPI={(api) => setExcalidrawAPI(api)}
+          onChange={handleChange}
+          UIOptions={UIOptions}
+          langCode={langList[window.app.config.lang] || 'en'}
+        >
+          <MainMenu>
+            <MainMenu.DefaultItems.Export />
+            <MainMenu.DefaultItems.SaveAsImage />
+            <MainMenu.DefaultItems.Help />
+            <MainMenu.DefaultItems.ClearCanvas />
+            <MainMenu.DefaultItems.ToggleTheme />
+            <MainMenu.DefaultItems.ChangeCanvasBackground />
+          </MainMenu>
+        </Excalidraw>
+      </div>
+    </>
+  );
+};
+
+export default SimpleEditor;
diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js
index 21cf592ee5..c9e410a565 100644
--- a/frontend/src/utils/constants.js
+++ b/frontend/src/utils/constants.js
@@ -99,6 +99,7 @@ export const enableSSOToThirdpartWebsite = window.app.pageOptions.enableSSOToThi
 export const enableSeadoc = window.app.pageOptions.enableSeadoc;
 export const enableSeafileAI = window.app.pageOptions.enableSeafileAI;
 export const enableWhiteboard = window.app.pageOptions.enableWhiteboard;
+export const enableExcalidraw = window.app.pageOptions.enableExcalidraw;
 export const enableMultipleOfficeSuite = window.app.pageOptions.enableMultipleOfficeSuite;
 
 export const curNoteMsg = window.app.pageOptions.curNoteMsg;
diff --git a/frontend/src/utils/text-translation.js b/frontend/src/utils/text-translation.js
index 1c33771115..6beeca70f6 100644
--- a/frontend/src/utils/text-translation.js
+++ b/frontend/src/utils/text-translation.js
@@ -31,6 +31,10 @@ const TextTranslation = {
     key: 'New Whiteboard File',
     value: gettext('New Whiteboard File')
   },
+  NEW_EXCALIDRAW_FILE: {
+    key: 'New Excalidraw File',
+    value: gettext('New Excalidraw File')
+  },
   NEW_SEADOC_FILE: {
     key: 'New SeaDoc File',
     value: gettext('New SeaDoc File')
diff --git a/seahub/base/context_processors.py b/seahub/base/context_processors.py
index 8bfbaa00a4..75889591e0 100644
--- a/seahub/base/context_processors.py
+++ b/seahub/base/context_processors.py
@@ -25,7 +25,7 @@ from seahub.settings import SEAFILE_VERSION, SITE_DESCRIPTION, \
     LOGIN_BG_IMAGE_PATH, THUMBNAIL_DEFAULT_SIZE, \
     CUSTOM_LOGIN_BG_PATH, ENABLE_SHARE_LINK_REPORT_ABUSE, \
     PRIVACY_POLICY_LINK, TERMS_OF_SERVICE_LINK, ENABLE_SEADOC, THUMBNAIL_SIZE_FOR_GRID, \
-    FILE_SERVER_ROOT, ENABLE_WHITEBOARD, ENABLE_SEAFILE_AI
+    FILE_SERVER_ROOT, ENABLE_WHITEBOARD, ENABLE_SEAFILE_AI, ENABLE_EXCALIDRAW
 
 from seahub.organizations.models import OrgAdminSettings
 from seahub.organizations.settings import ORG_ENABLE_ADMIN_CUSTOM_LOGO
@@ -181,6 +181,7 @@ def base(request):
         'enable_seadoc': ENABLE_SEADOC,
         'enable_seafile_ai': ENABLE_SEAFILE_AI,
         'enable_whiteboard': ENABLE_WHITEBOARD,
+        'enable_excalidraw': ENABLE_EXCALIDRAW,
     }
 
     if request.user.is_staff:
diff --git a/seahub/settings.py b/seahub/settings.py
index e51f61d125..3ea7552e94 100644
--- a/seahub/settings.py
+++ b/seahub/settings.py
@@ -961,6 +961,12 @@ FILE_CONVERTER_SERVER_URL = 'http://127.0.0.1:8888'
 
 ENABLE_WHITEBOARD = False
 
+##########################
+# Settings for excalidraw    #
+##########################
+
+ENABLE_EXCALIDRAW = False
+
 ############################
 # Settings for Seahub Priv #
 ############################
diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html
index bc869a68c5..6f2ab3b828 100644
--- a/seahub/templates/base_for_react.html
+++ b/seahub/templates/base_for_react.html
@@ -159,6 +159,7 @@
             officeWebAppEditFileExtension: {% if office_web_app_edit_file_extension %} {{office_web_app_edit_file_extension|safe}} {% else %} [] {% endif %},
             enableSeadoc: {% if enable_seadoc %} true {% else %} false {% endif %},
             enableWhiteboard: {% if enable_whiteboard %} true {% else %} false {% endif %},
+            enableExcalidraw: {% if enable_excalidraw %} true {% else %} false {% endif %},
             isOrgContext: {% if org is not None %} true {% else %} false {% endif %},
             enableMetadataManagement: {% if enable_metadata_management %} true {% else %} false {% endif %},
             isMultiTenacy: {% if multi_tenancy %} true {% else %} false {% endif %},
diff --git a/seahub/templates/excalidraw_file_view_react.html b/seahub/templates/excalidraw_file_view_react.html
new file mode 100644
index 0000000000..17805eb736
--- /dev/null
+++ b/seahub/templates/excalidraw_file_view_react.html
@@ -0,0 +1,19 @@
+{% extends 'file_view_react.html' %}
+{% load render_bundle from webpack_loader %}
+
+
+{% block extra_style %}
+{% render_bundle 'excalidrawEditor' 'css' %}
+{% endblock %}
+
+{% block extra_data %}
+docPath: '{{ path|escapejs }}',
+docName: '{{ filename|escapejs }}',
+docUuid: '{{ file_uuid }}',
+lang: '{{ lang }}',
+rawPath: '{{ raw_path|escapejs }}',
+{% endblock %}
+
+{% block render_bundle %}
+{% render_bundle 'excalidrawEditor' 'js' %}
+{% endblock %}
diff --git a/seahub/utils/__init__.py b/seahub/utils/__init__.py
index 6683aa4ffc..b0250d89c6 100644
--- a/seahub/utils/__init__.py
+++ b/seahub/utils/__init__.py
@@ -144,6 +144,7 @@ PREVIEW_FILEEXT = {
     XMIND: ('xmind',),
     SEADOC: ('sdoc',),
     TLDRAW: ('draw',),
+    EXCALIDRAW: ('exdraw',),
 }
 
 def get_non_sdoc_file_exts():
diff --git a/seahub/utils/file_types.py b/seahub/utils/file_types.py
index 5b15fd48c2..405dc16640 100644
--- a/seahub/utils/file_types.py
+++ b/seahub/utils/file_types.py
@@ -11,6 +11,7 @@ SPREADSHEET = 'SpreadSheet'
 XMIND = 'XMind'
 SEADOC = 'SDoc'
 TLDRAW = 'Tldraw'
+EXCALIDRAW = 'Excalidraw'
 
 
 MARKDOWN_SUPPORT_CONVERT_TYPES = ['sdoc']
diff --git a/seahub/views/file.py b/seahub/views/file.py
index b18acb651b..24225b81a5 100644
--- a/seahub/views/file.py
+++ b/seahub/views/file.py
@@ -61,7 +61,7 @@ from seahub.utils import render_error, is_org_context, \
 from seahub.utils.ip import get_remote_ip
 from seahub.utils.file_types import (IMAGE, PDF, SVG,
                                      DOCUMENT, SPREADSHEET, AUDIO,
-                                     MARKDOWN, TEXT, VIDEO, XMIND, SEADOC, TLDRAW)
+                                     MARKDOWN, TEXT, VIDEO, XMIND, SEADOC, TLDRAW, EXCALIDRAW)
 from seahub.utils.timeutils import timestamp_to_isoformat_timestr
 from seahub.utils.star import is_file_starred
 from seahub.utils.http import json_response, \
@@ -850,6 +850,27 @@ def view_lib_file(request, repo_id, path):
         return_dict['raw_path'] = raw_path
 
 
+        can_edit_file = True
+        if parse_repo_perm(permission).can_edit_on_web is False:
+            can_edit_file = False
+        elif is_locked and not locked_by_me:
+            can_edit_file = False
+        return_dict['can_edit_file'] = can_edit_file
+
+        return render(request, template, return_dict)
+    
+    if filetype == EXCALIDRAW:
+
+        return_dict['protocol'] = request.is_secure() and 'https' or 'http'
+        return_dict['domain'] = get_current_site(request).domain
+        return_dict['serviceUrl'] = get_service_url().rstrip('/')
+        return_dict['language_code'] = get_language()
+        return_dict['share_link_expire_days_Default'] = SHARE_LINK_EXPIRE_DAYS_DEFAULT
+        return_dict['share_link_expire_days_min'] = SHARE_LINK_EXPIRE_DAYS_MIN
+        return_dict['share_link_expire_days_max'] = SHARE_LINK_EXPIRE_DAYS_MAX
+        return_dict['raw_path'] = raw_path
+
+
         can_edit_file = True
         if parse_repo_perm(permission).can_edit_on_web is False:
             can_edit_file = False