2
0
Эх сурвалжийг харах

feat: Support mermaid flowchart and sequence diagrams to excalidraw diagrams 🥳 (#6920)

* feat: integrate mermaidToExcalidraw

* create mermaid to excal dialog

* allow mermaid syntax and export in preview

* fix

* fix webpack config

* fix markdown error by using named export

* center preview

* set elements as selected when inserted onto canvas

* persist mermaid data to storage

* store canvas data in refs

* load mermaid lazily

* tweak design

* compute width, height correctly for arrows

* fix undefined vertex issue

* add mermaid icon in dropdown

* add a note in dialog

* reset preview when error

* show error in preview when error

* show mermaid error messgae react way

* design tweaks

* add example and docs link

* fix

* tweak design to remove scroll bar

* show a spinner unless mermaid loaded

* regenerate ids when needed via programmatic api, this makes sure for mermaid diagrams ids are regenerated

* tweak

* add option to transform viewport to scene coords in transform api

* make opts optional and use 100% zoom when inserting to canvas

* fix arrow bindings in safari and firefox

* fix elements insert position and viewport centering

* fix: Update start/end points by 0.5 so bindings don't overlap with start/end bound element coordinates.

* defer rendering the preview

* tweak text

* fix tests

* remove only

* make design responsive

* fix: show extra tools dropdown in mobile

* fix mobile css

* width auto

* upgrade mermaid-to-excalidraw

* don't pass appState in deps as its not used

* upgrade mermaid-to-excalidraw to fix firefox issue

* use types from mermaid-to-excalidraw

* upgrade mermaid-to-excalidraw

* use stable version of mermaid-to-excalidraw

* upgrade mermaid-to-excalidraw

* fix width of shapes toolbar for smaller screen size and also fix regression of mobile menu

* use i18n

* better api

* enable test coverage in ui

* Add tests

* use common utils to update and get text editor

* updgrade mermaid-to-excalidraw to support sequence diagrams

* fix test

* don't update arrow container height anytime in when redrawing text bounding box

* increase size limit

* increase size limit of vendor to 900kb

* use openDialog for mermaid

* upgrade mermaid-to-excalidraw

* update frame id post generation

* upgrade mermaid-to-excalidraw to add entity codes support

* update size limit

* upgrade mermaid-to-excalidraw package with frame api changes

* upgrade mermaid-to-excalidraw to remove directive and use config

* don't highlight mermaid tool and remove unused api setSelection

* stop using loading state to update text area

* move some styling to scss

* review fixes

* use modifiedTableIcon props and remove stale snap

* css

* dialog css

* fix snap

* use dialog border

* change mermaidToExcalidrawLib to state

* better styling of errors

* make modal bigger

* fix mobile

* update snaps

* fix icon color

* fix dark mode insert button color

* horizontally center spinner

* render canvas conditionally on loaded state

* rd tweaks

* tweak class names

* remove max height

* typo in example

* upgrade mermaid-to-excalidraw

* simplify error state

* fix height & overflow on vertical breakpoint

* fix lint

* show errors in overlay

* set textarea font family

* reduce opacity

* update snap

* upgrade to mermaid  0.1.2

---------

Co-authored-by: dwelle <[email protected]>
Aakansha Doshi 1 жил өмнө
parent
commit
e8def8da8d

+ 1 - 1
excalidraw-app/debug.ts

@@ -131,5 +131,5 @@ export class Debug {
     };
   };
 }
-
+//@ts-ignore
 window.debug = Debug;

+ 2 - 1
package.json

@@ -20,6 +20,7 @@
   },
   "dependencies": {
     "@braintree/sanitize-url": "6.0.2",
+    "@excalidraw/mermaid-to-excalidraw": "0.1.2",
     "@excalidraw/laser-pointer": "1.2.0",
     "@excalidraw/random-username": "1.0.0",
     "@radix-ui/react-popover": "1.0.3",
@@ -125,7 +126,7 @@
     "test": "yarn test:app",
     "test:coverage": "vitest --coverage",
     "test:coverage:watch": "vitest --coverage --watch",
-    "test:ui": "yarn test --ui",
+    "test:ui": "yarn test --ui --coverage.enabled=true",
     "autorelease": "node scripts/autorelease.js",
     "prerelease": "node scripts/prerelease.js",
     "build:preview": "yarn build && vite preview --port 5000",

+ 54 - 102
src/components/Actions.tsx

@@ -34,6 +34,7 @@ import {
   EmbedIcon,
   extraToolsIcon,
   frameToolIcon,
+  mermaidLogoIcon,
   laserPointerToolIcon,
 } from "./icons";
 import { KEYS } from "../keys";
@@ -223,7 +224,6 @@ export const ShapesSwitcher = ({
   app: AppClassProperties;
 }) => {
   const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
-  const device = useDevice();
 
   const frameToolSelected = activeTool.type === "frame";
   const laserToolSelected = activeTool.type === "laser";
@@ -273,111 +273,63 @@ export const ShapesSwitcher = ({
         );
       })}
       <div className="App-toolbar__divider" />
-      {/* TEMP HACK because dropdown doesn't work well inside mobile toolbar */}
-      {device.isMobile ? (
-        <>
-          <ToolButton
-            className={clsx("Shape", { fillable: false })}
-            type="radio"
+
+      <DropdownMenu open={isExtraToolsMenuOpen}>
+        <DropdownMenu.Trigger
+          className={clsx("App-toolbar__extra-tools-trigger", {
+            "App-toolbar__extra-tools-trigger--selected":
+              frameToolSelected ||
+              embeddableToolSelected ||
+              // in collab we're already highlighting the laser button
+              // outside toolbar, so let's not highlight extra-tools button
+              // on top of it
+              (laserToolSelected && !app.props.isCollaborating),
+          })}
+          onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
+          title={t("toolBar.extraTools")}
+        >
+          {extraToolsIcon}
+        </DropdownMenu.Trigger>
+        <DropdownMenu.Content
+          onClickOutside={() => setIsExtraToolsMenuOpen(false)}
+          onSelect={() => setIsExtraToolsMenuOpen(false)}
+          className="App-toolbar__extra-tools-dropdown"
+        >
+          <DropdownMenu.Item
+            onSelect={() => app.setActiveTool({ type: "frame" })}
             icon={frameToolIcon}
-            checked={activeTool.type === "frame"}
-            name="editor-current-shape"
-            title={`${capitalizeString(
-              t("toolBar.frame"),
-            )} — ${KEYS.F.toLocaleUpperCase()}`}
-            keyBindingLabel={KEYS.F.toLocaleUpperCase()}
-            aria-label={capitalizeString(t("toolBar.frame"))}
-            aria-keyshortcuts={KEYS.F.toLocaleUpperCase()}
-            data-testid={`toolbar-frame`}
-            onPointerDown={({ pointerType }) => {
-              if (!appState.penDetected && pointerType === "pen") {
-                app.togglePenMode(true);
-              }
-            }}
-            onChange={({ pointerType }) => {
-              trackEvent("toolbar", "frame", "ui");
-              app.setActiveTool({ type: "frame" });
-            }}
-            selected={activeTool.type === "frame"}
-          />
-          <ToolButton
-            className={clsx("Shape", { fillable: false })}
-            type="radio"
+            shortcut={KEYS.F.toLocaleUpperCase()}
+            data-testid="toolbar-frame"
+            selected={frameToolSelected}
+          >
+            {t("toolBar.frame")}
+          </DropdownMenu.Item>
+          <DropdownMenu.Item
+            onSelect={() => app.setActiveTool({ type: "embeddable" })}
             icon={EmbedIcon}
-            checked={activeTool.type === "embeddable"}
-            name="editor-current-shape"
-            title={capitalizeString(t("toolBar.embeddable"))}
-            aria-label={capitalizeString(t("toolBar.embeddable"))}
-            data-testid={`toolbar-embeddable`}
-            onPointerDown={({ pointerType }) => {
-              if (!appState.penDetected && pointerType === "pen") {
-                app.togglePenMode(true);
-              }
-            }}
-            onChange={({ pointerType }) => {
-              trackEvent("toolbar", "embeddable", "ui");
-              app.setActiveTool({ type: "embeddable" });
-            }}
-            selected={activeTool.type === "embeddable"}
-          />
-        </>
-      ) : (
-        <DropdownMenu open={isExtraToolsMenuOpen}>
-          <DropdownMenu.Trigger
-            className={clsx("App-toolbar__extra-tools-trigger", {
-              "App-toolbar__extra-tools-trigger--selected":
-                frameToolSelected ||
-                embeddableToolSelected ||
-                // in collab we're already highlighting the laser button
-                // outside toolbar, so let's not highlight extra-tools button
-                // on top of it
-                (laserToolSelected && !app.props.isCollaborating),
-            })}
-            onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
-            title={t("toolBar.extraTools")}
+            data-testid="toolbar-embeddable"
+            selected={embeddableToolSelected}
           >
-            {extraToolsIcon}
-          </DropdownMenu.Trigger>
-          <DropdownMenu.Content
-            onClickOutside={() => setIsExtraToolsMenuOpen(false)}
-            onSelect={() => setIsExtraToolsMenuOpen(false)}
-            className="App-toolbar__extra-tools-dropdown"
+            {t("toolBar.embeddable")}
+          </DropdownMenu.Item>
+          <DropdownMenu.Item
+            onSelect={() => app.setActiveTool({ type: "laser" })}
+            icon={laserPointerToolIcon}
+            data-testid="toolbar-laser"
+            selected={laserToolSelected}
+            shortcut={KEYS.K.toLocaleUpperCase()}
           >
-            <DropdownMenu.Item
-              onSelect={() => {
-                app.setActiveTool({ type: "frame" });
-              }}
-              icon={frameToolIcon}
-              shortcut={KEYS.F.toLocaleUpperCase()}
-              data-testid="toolbar-frame"
-              selected={frameToolSelected}
-            >
-              {t("toolBar.frame")}
-            </DropdownMenu.Item>
-            <DropdownMenu.Item
-              onSelect={() => {
-                app.setActiveTool({ type: "embeddable" });
-              }}
-              icon={EmbedIcon}
-              data-testid="toolbar-embeddable"
-              selected={embeddableToolSelected}
-            >
-              {t("toolBar.embeddable")}
-            </DropdownMenu.Item>
-            <DropdownMenu.Item
-              onSelect={() => {
-                app.setActiveTool({ type: "laser" });
-              }}
-              icon={laserPointerToolIcon}
-              data-testid="toolbar-laser"
-              selected={laserToolSelected}
-              shortcut={KEYS.K.toLocaleUpperCase()}
-            >
-              {t("toolBar.laser")}
-            </DropdownMenu.Item>
-          </DropdownMenu.Content>
-        </DropdownMenu>
-      )}
+            {t("toolBar.laser")}
+          </DropdownMenu.Item>
+          <DropdownMenu.Item
+            onSelect={() => app.setOpenDialog("mermaid")}
+            icon={mermaidLogoIcon}
+            data-testid="toolbar-embeddable"
+          >
+            {t("toolBar.mermaidToExcalidraw")}
+          </DropdownMenu.Item>
+        </DropdownMenu.Content>
+      </DropdownMenu>
     </>
   );
 };

+ 18 - 1
src/components/App.tsx

@@ -366,6 +366,7 @@ import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
 import { StaticCanvas, InteractiveCanvas } from "./canvases";
 import { Renderer } from "../scene/Renderer";
 import { ShapeCache } from "../scene/ShapeCache";
+import MermaidToExcalidraw from "./MermaidToExcalidraw";
 import { LaserToolOverlay } from "./LaserTool/LaserTool";
 import { LaserPathManager } from "./LaserTool/LaserPathManager";
 import {
@@ -1245,7 +1246,11 @@ class App extends React.Component<AppProps, AppState> {
                           isCollaborating={this.props.isCollaborating}
                         >
                           {this.props.children}
+                          {this.state.openDialog === "mermaid" && (
+                            <MermaidToExcalidraw />
+                          )}
                         </LayerUI>
+
                         <div className="excalidraw-textEditorContainer" />
                         <div className="excalidraw-contextMenuContainer" />
                         <div className="excalidraw-eye-dropper-container" />
@@ -2324,11 +2329,12 @@ class App extends React.Component<AppProps, AppState> {
     },
   );
 
-  private addElementsFromPasteOrLibrary = (opts: {
+  addElementsFromPasteOrLibrary = (opts: {
     elements: readonly ExcalidrawElement[];
     files: BinaryFiles | null;
     position: { clientX: number; clientY: number } | "cursor" | "center";
     retainSeed?: boolean;
+    fitToContent?: boolean;
   }) => {
     const elements = restoreElements(opts.elements, null, undefined);
     const [minX, minY, maxX, maxY] = getCommonBounds(elements);
@@ -2433,6 +2439,12 @@ class App extends React.Component<AppProps, AppState> {
       },
     );
     this.setActiveTool({ type: "selection" });
+
+    if (opts.fitToContent) {
+      this.scrollToContent(newElements, {
+        fitToContent: true,
+      });
+    }
   };
 
   // TODO rewrite this to paste both text & images at the same time if
@@ -3308,6 +3320,10 @@ class App extends React.Component<AppProps, AppState> {
     });
   };
 
+  setOpenDialog = (dialogType: AppState["openDialog"]) => {
+    this.setState({ openDialog: dialogType });
+  };
+
   private setCursor = (cursor: string) => {
     setCursor(this.interactiveCanvas, cursor);
   };
@@ -4258,6 +4274,7 @@ class App extends React.Component<AppProps, AppState> {
       scenePointer.x,
       scenePointer.y,
     );
+
     this.hitLinkElement = this.getElementLinkAtPosition(
       scenePointer,
       hitElement,

+ 221 - 0
src/components/MermaidToExcalidraw.scss

@@ -0,0 +1,221 @@
+@import "../css/variables.module";
+
+$verticalBreakpoint: 860px;
+
+.excalidraw {
+  .dialog-mermaid {
+    &-title {
+      margin-bottom: 5px;
+      margin-top: 2px;
+    }
+    &-desc {
+      font-size: 15px;
+      font-style: italic;
+      font-weight: 500;
+    }
+
+    .Modal__content .Island {
+      box-shadow: none;
+    }
+
+    @at-root .excalidraw:not(.excalidraw--mobile)#{&} {
+      padding: 1.25rem;
+
+      .Modal__content {
+        height: 100%;
+        max-height: 750px;
+
+        @media screen and (max-width: $verticalBreakpoint) {
+          height: auto;
+          // When vertical, we want the height to span whole viewport.
+          // This is also important for the children not to overflow the
+          // modal/viewport (for some reason).
+          max-height: 100%;
+        }
+
+        .Island {
+          height: 100%;
+          display: flex;
+          flex-direction: column;
+          flex: 1 1 auto;
+
+          .Dialog__content {
+            display: flex;
+            flex: 1 1 auto;
+          }
+        }
+      }
+    }
+  }
+
+  .dialog-mermaid-body {
+    width: 100%;
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    grid-template-rows: 1fr auto;
+    height: 100%;
+    column-gap: 4rem;
+
+    @media screen and (max-width: $verticalBreakpoint) {
+      flex-direction: column;
+      display: flex;
+      gap: 1rem;
+    }
+  }
+
+  .dialog-mermaid-panels {
+    display: grid;
+    width: 100%;
+    grid-template-columns: 1fr 1fr;
+    justify-content: space-between;
+    gap: 4rem;
+
+    grid-row: 1;
+    grid-column: 1 / 3;
+
+    @media screen and (max-width: $verticalBreakpoint) {
+      flex-direction: column;
+      display: flex;
+      gap: 1rem;
+    }
+
+    label {
+      font-size: 14px;
+      font-style: normal;
+      font-weight: 600;
+      margin-bottom: 4px;
+      margin-left: 4px;
+
+      @media screen and (max-width: $verticalBreakpoint) {
+        margin-top: 4px;
+      }
+    }
+
+    &-text {
+      display: flex;
+      flex-direction: column;
+
+      textarea {
+        width: 20rem;
+        height: 100%;
+        resize: none;
+        border-radius: var(--border-radius-lg);
+        border: 1px solid var(--dialog-border-color);
+        white-space: pre-wrap;
+        padding: 0.85rem;
+        box-sizing: border-box;
+        width: 100%;
+        font-family: monospace;
+
+        @media screen and (max-width: $verticalBreakpoint) {
+          width: auto;
+          height: 10rem;
+        }
+      }
+    }
+
+    &-preview-wrapper {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      padding: 0.85rem;
+      box-sizing: border-box;
+      width: 100%;
+      // acts as min-height
+      height: 200px;
+      flex-grow: 1;
+      position: relative;
+
+      background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==")
+        left center;
+      border-radius: var(--border-radius-lg);
+      border: 1px solid var(--dialog-border-color);
+
+      @media screen and (max-width: $verticalBreakpoint) {
+        // acts as min-height
+        height: 400px;
+        width: auto;
+      }
+
+      canvas {
+        max-width: 100%;
+        max-height: 100%;
+      }
+    }
+
+    &-preview-canvas-container {
+      display: flex;
+      width: 100%;
+      height: 100%;
+      align-items: center;
+      justify-content: center;
+      flex-grow: 1;
+    }
+
+    &-preview {
+      display: flex;
+      flex-direction: column;
+    }
+
+    .mermaid-error {
+      color: red;
+      font-weight: 800;
+      font-size: 30px;
+      word-break: break-word;
+      overflow: auto;
+      max-height: 100%;
+      height: 100%;
+      width: 100%;
+      text-align: center;
+      position: absolute;
+      z-index: 10;
+
+      p {
+        font-weight: 500;
+        font-family: Cascadia;
+        text-align: left;
+        white-space: pre-wrap;
+        font-size: 0.875rem;
+        padding: 0 10px;
+      }
+    }
+  }
+
+  .dialog-mermaid-buttons {
+    grid-column: 2;
+
+    .dialog-mermaid-insert {
+      &.excalidraw-button {
+        font-family: "Assistant";
+        font-weight: 600;
+        height: 2.5rem;
+        margin-top: 1em;
+        margin-bottom: 0.3em;
+        width: 7.5rem;
+        font-size: 12px;
+        color: $oc-white;
+        background-color: var(--color-primary);
+
+        &:hover {
+          background-color: var(--color-primary-darker);
+        }
+        &:active {
+          background-color: var(--color-primary-darkest);
+        }
+
+        @media screen and (max-width: $verticalBreakpoint) {
+          width: 100%;
+        }
+
+        @at-root .excalidraw.theme--dark#{&} {
+          color: var(--color-gray-100);
+        }
+      }
+
+      span {
+        padding-left: 0.5rem;
+        display: flex;
+      }
+    }
+  }
+}

+ 243 - 0
src/components/MermaidToExcalidraw.tsx

@@ -0,0 +1,243 @@
+import { useState, useRef, useEffect, useDeferredValue } from "react";
+import { BinaryFiles } from "../types";
+import { useApp } from "./App";
+import { Button } from "./Button";
+import { Dialog } from "./Dialog";
+import { DEFAULT_EXPORT_PADDING, DEFAULT_FONT_SIZE } from "../constants";
+import {
+  convertToExcalidrawElements,
+  exportToCanvas,
+} from "../packages/excalidraw/index";
+import { NonDeletedExcalidrawElement } from "../element/types";
+import { canvasToBlob } from "../data/blob";
+import { ArrowRightIcon } from "./icons";
+import Spinner from "./Spinner";
+import "./MermaidToExcalidraw.scss";
+
+import { MermaidToExcalidrawResult } from "@excalidraw/mermaid-to-excalidraw/dist/interfaces";
+import type { MermaidOptions } from "@excalidraw/mermaid-to-excalidraw";
+import { t } from "../i18n";
+import Trans from "./Trans";
+
+const LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW = "mermaid-to-excalidraw";
+const MERMAID_EXAMPLE =
+  "flowchart TD\n A[Christmas] -->|Get money| B(Go shopping)\n B --> C{Let me think}\n C -->|One| D[Laptop]\n C -->|Two| E[iPhone]\n C -->|Three| F[Car]";
+
+const saveMermaidDataToStorage = (data: string) => {
+  try {
+    localStorage.setItem(LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW, data);
+  } catch (error: any) {
+    // Unable to access window.localStorage
+    console.error(error);
+  }
+};
+
+const importMermaidDataFromStorage = () => {
+  try {
+    const data = localStorage.getItem(LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW);
+    if (data) {
+      return data;
+    }
+  } catch (error: any) {
+    // Unable to access localStorage
+    console.error(error);
+  }
+
+  return null;
+};
+
+const ErrorComp = ({ error }: { error: string }) => {
+  return (
+    <div data-testid="mermaid-error" className="mermaid-error">
+      Error! <p>{error}</p>
+    </div>
+  );
+};
+
+const MermaidToExcalidraw = () => {
+  const [mermaidToExcalidrawLib, setMermaidToExcalidrawLib] = useState<{
+    loaded: boolean;
+    api: {
+      parseMermaidToExcalidraw: (
+        defination: string,
+        options: MermaidOptions,
+      ) => Promise<MermaidToExcalidrawResult>;
+    } | null;
+  }>({ loaded: false, api: null });
+
+  const [text, setText] = useState("");
+  const deferredText = useDeferredValue(text.trim());
+  const [error, setError] = useState(null);
+
+  const canvasRef = useRef<HTMLDivElement>(null);
+  const data = useRef<{
+    elements: readonly NonDeletedExcalidrawElement[];
+    files: BinaryFiles | null;
+  }>({ elements: [], files: null });
+
+  const app = useApp();
+
+  const resetPreview = () => {
+    const canvasNode = canvasRef.current;
+
+    if (!canvasNode) {
+      return;
+    }
+    const parent = canvasNode.parentElement;
+    if (!parent) {
+      return;
+    }
+    parent.style.background = "";
+    setError(null);
+    canvasNode.replaceChildren();
+  };
+
+  useEffect(() => {
+    const loadMermaidToExcalidrawLib = async () => {
+      const api = await import(
+        /* webpackChunkName:"mermaid-to-excalidraw" */ "@excalidraw/mermaid-to-excalidraw"
+      );
+      setMermaidToExcalidrawLib({ loaded: true, api });
+    };
+    loadMermaidToExcalidrawLib();
+  }, []);
+
+  useEffect(() => {
+    const data = importMermaidDataFromStorage() || MERMAID_EXAMPLE;
+    setText(data);
+  }, []);
+
+  useEffect(() => {
+    const renderExcalidrawPreview = async () => {
+      const canvasNode = canvasRef.current;
+      const parent = canvasNode?.parentElement;
+      if (
+        !mermaidToExcalidrawLib.loaded ||
+        !canvasNode ||
+        !parent ||
+        !mermaidToExcalidrawLib.api
+      ) {
+        return;
+      }
+      if (!deferredText) {
+        resetPreview();
+        return;
+      }
+      try {
+        const { elements, files } =
+          await mermaidToExcalidrawLib.api.parseMermaidToExcalidraw(
+            deferredText,
+            {
+              fontSize: DEFAULT_FONT_SIZE,
+            },
+          );
+        setError(null);
+
+        data.current = {
+          elements: convertToExcalidrawElements(elements, {
+            regenerateIds: true,
+          }),
+          files,
+        };
+
+        const canvas = await exportToCanvas({
+          elements: data.current.elements,
+          files: data.current.files,
+          exportPadding: DEFAULT_EXPORT_PADDING,
+          maxWidthOrHeight:
+            Math.max(parent.offsetWidth, parent.offsetHeight) *
+            window.devicePixelRatio,
+        });
+        // if converting to blob fails, there's some problem that will
+        // likely prevent preview and export (e.g. canvas too big)
+        await canvasToBlob(canvas);
+        parent.style.background = "var(--default-bg-color)";
+        canvasNode.replaceChildren(canvas);
+      } catch (e: any) {
+        parent.style.background = "var(--default-bg-color)";
+        if (deferredText) {
+          setError(e.message);
+        }
+      }
+    };
+    renderExcalidrawPreview();
+  }, [deferredText, mermaidToExcalidrawLib]);
+
+  const onClose = () => {
+    app.setOpenDialog(null);
+    saveMermaidDataToStorage(text);
+  };
+
+  const onSelect = () => {
+    const { elements: newElements, files } = data.current;
+    app.addElementsFromPasteOrLibrary({
+      elements: newElements,
+      files,
+      position: "center",
+      fitToContent: true,
+    });
+    onClose();
+  };
+
+  return (
+    <Dialog
+      className="dialog-mermaid"
+      onCloseRequest={onClose}
+      size={1200}
+      title={
+        <>
+          <p className="dialog-mermaid-title">{t("mermaid.title")}</p>
+          <span className="dialog-mermaid-desc">
+            <Trans
+              i18nKey="mermaid.description"
+              flowchartLink={(el) => (
+                <a href="https://mermaid.js.org/syntax/flowchart.html">{el}</a>
+              )}
+              sequenceLink={(el) => (
+                <a href="https://mermaid.js.org/syntax/sequenceDiagram.html">
+                  {el}
+                </a>
+              )}
+            />
+            <br />
+          </span>
+        </>
+      }
+    >
+      <div className="dialog-mermaid-body">
+        <div className="dialog-mermaid-panels">
+          <div className="dialog-mermaid-panels-text">
+            <label>{t("mermaid.syntax")}</label>
+
+            <textarea
+              onChange={(event) => setText(event.target.value)}
+              value={text}
+            />
+          </div>
+          <div className="dialog-mermaid-panels-preview">
+            <label>{t("mermaid.preview")}</label>
+            <div className="dialog-mermaid-panels-preview-wrapper">
+              {error && <ErrorComp error={error} />}
+              {mermaidToExcalidrawLib.loaded ? (
+                <div
+                  ref={canvasRef}
+                  style={{ opacity: error ? "0.15" : 1 }}
+                  className="dialog-mermaid-panels-preview-canvas-container"
+                />
+              ) : (
+                <Spinner size="2rem" />
+              )}
+            </div>
+          </div>
+        </div>
+        <div className="dialog-mermaid-buttons">
+          <Button className="dialog-mermaid-insert" onSelect={onSelect}>
+            {t("mermaid.button")}
+            <span>{ArrowRightIcon}</span>
+          </Button>
+        </div>
+      </div>
+    </Dialog>
+  );
+};
+export default MermaidToExcalidraw;

+ 9 - 0
src/components/ToolIcon.scss

@@ -160,6 +160,15 @@
       width: var(--lg-button-size);
       height: var(--lg-button-size);
 
+      @media screen and (max-width: 450px) {
+        width: 1.8rem;
+        height: 1.8rem;
+      }
+      @media screen and (max-width: 379px) {
+        width: 1.5rem;
+        height: 1.5rem;
+      }
+
       svg {
         width: var(--lg-icon-size);
         height: var(--lg-icon-size);

+ 5 - 0
src/components/Toolbar.scss

@@ -16,6 +16,10 @@
       align-self: center;
       background-color: var(--default-border-color);
       margin: 0 0.25rem;
+
+      @include isMobile {
+        margin: 0;
+      }
     }
   }
 
@@ -41,5 +45,6 @@
     margin-top: 0.375rem;
     right: 0;
     min-width: 11.875rem;
+    z-index: 1;
   }
 }

+ 0 - 2
src/components/dropdownMenu/DropdownMenu.scss

@@ -7,8 +7,6 @@
     margin-top: 0.25rem;
 
     &--mobile {
-      bottom: 55px;
-      top: auto;
       left: 0;
       width: 100%;
       row-gap: 0.75rem;

+ 16 - 0
src/components/icons.tsx

@@ -1654,6 +1654,22 @@ export const frameToolIcon = createIcon(
   tablerIconProps,
 );
 
+export const mermaidLogoIcon = createIcon(
+  <path
+    fill="currentColor"
+    d="M407.48,111.18C335.587,108.103 269.573,152.338 245.08,220C220.587,152.338 154.573,108.103 82.68,111.18C80.285,168.229 107.577,222.632 154.74,254.82C178.908,271.419 193.35,298.951 193.27,328.27L193.27,379.13L296.9,379.13L296.9,328.27C296.816,298.953 311.255,271.42 335.42,254.82C382.596,222.644 409.892,168.233 407.48,111.18Z"
+  />,
+);
+
+export const ArrowRightIcon = createIcon(
+  <g strokeWidth="1.25">
+    <path d="M4.16602 10H15.8327" />
+    <path d="M12.5 13.3333L15.8333 10" />
+    <path d="M12.5 6.66666L15.8333 9.99999" />
+  </g>,
+  modifiedTablerIconProps,
+);
+
 export const laserPointerToolIcon = createIcon(
   <g
     fill="none"

+ 9 - 2
src/css/styles.scss

@@ -280,6 +280,11 @@
     align-items: center;
     justify-content: space-between;
     padding: 8px;
+
+    .dropdown-menu--mobile {
+      bottom: 55px;
+      top: auto;
+    }
   }
 
   .App-mobile-menu {
@@ -592,6 +597,8 @@
     background-color: var(--island-bg-color);
 
     .ToolIcon__icon {
+      width: 2rem;
+      height: 2rem;
       border-radius: 0;
     }
 
@@ -601,8 +608,8 @@
   }
 
   .App-toolbar--mobile {
-    overflow-x: auto;
-    max-width: 90vw;
+    overflow: visible;
+    max-width: 98vw;
 
     .ToolIcon__keybinding {
       display: none;

+ 4 - 1056
src/data/__snapshots__/transform.test.ts.snap

@@ -1,140 +1,5 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
-exports[`Test Transform > Test Frames > should transform frames and update frame ids when regenerated 1`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "boundElements": null,
-  "fillStyle": "solid",
-  "frameId": "id33",
-  "groupIds": [],
-  "height": 100,
-  "id": Any<String>,
-  "isDeleted": false,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "type": "rectangle",
-  "updated": 1,
-  "version": 1,
-  "versionNonce": Any<Number>,
-  "width": 100,
-  "x": 10,
-  "y": 10,
-}
-`;
-
-exports[`Test Transform > Test Frames > should transform frames and update frame ids when regenerated 2`] = `
-{
-  "angle": 0,
-  "backgroundColor": "#fff3bf",
-  "boundElements": [
-    {
-      "id": "id34",
-      "type": "text",
-    },
-  ],
-  "fillStyle": "solid",
-  "frameId": "id33",
-  "groupIds": [],
-  "height": 96,
-  "id": Any<String>,
-  "isDeleted": false,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "type": "diamond",
-  "updated": 1,
-  "version": 3,
-  "versionNonce": Any<Number>,
-  "width": 340,
-  "x": 120,
-  "y": 20,
-}
-`;
-
-exports[`Test Transform > Test Frames > should transform frames and update frame ids when regenerated 3`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "boundElements": null,
-  "fillStyle": "solid",
-  "frameId": null,
-  "groupIds": [],
-  "height": 126,
-  "id": Any<String>,
-  "isDeleted": false,
-  "link": null,
-  "locked": false,
-  "name": "My frame",
-  "opacity": 100,
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "type": "frame",
-  "updated": 1,
-  "version": 1,
-  "versionNonce": Any<Number>,
-  "width": 470,
-  "x": 0,
-  "y": 0,
-}
-`;
-
-exports[`Test Transform > Test Frames > should transform frames and update frame ids when regenerated 4`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "baseline": 0,
-  "boundElements": null,
-  "containerId": "2",
-  "fillStyle": "solid",
-  "fontFamily": 1,
-  "fontSize": 30,
-  "frameId": "id33",
-  "groupIds": [],
-  "height": 37.5,
-  "id": Any<String>,
-  "isDeleted": false,
-  "lineHeight": 1.25,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "originalText": "HELLO EXCALIDRAW",
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#099268",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "text": "HELLO EXCALIDRAW",
-  "textAlign": "center",
-  "type": "text",
-  "updated": 1,
-  "version": 2,
-  "versionNonce": Any<Number>,
-  "verticalAlign": "middle",
-  "width": 160,
-  "x": 210,
-  "y": 49.25,
-}
-`;
-
 exports[`Test Transform > Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 1`] = `
 {
   "angle": 0,
@@ -917,141 +782,6 @@ exports[`Test Transform > should not allow duplicate ids 1`] = `
 }
 `;
 
-exports[`Test Transform > should transform frames and update frame ids when regenerated 1`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "boundElements": null,
-  "fillStyle": "solid",
-  "frameId": "id33",
-  "groupIds": [],
-  "height": 100,
-  "id": Any<String>,
-  "isDeleted": false,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "type": "rectangle",
-  "updated": 1,
-  "version": 1,
-  "versionNonce": Any<Number>,
-  "width": 100,
-  "x": 10,
-  "y": 10,
-}
-`;
-
-exports[`Test Transform > should transform frames and update frame ids when regenerated 2`] = `
-{
-  "angle": 0,
-  "backgroundColor": "#fff3bf",
-  "boundElements": [
-    {
-      "id": "id34",
-      "type": "text",
-    },
-  ],
-  "fillStyle": "solid",
-  "frameId": "id33",
-  "groupIds": [],
-  "height": 96,
-  "id": Any<String>,
-  "isDeleted": false,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "type": "diamond",
-  "updated": 1,
-  "version": 3,
-  "versionNonce": Any<Number>,
-  "width": 340,
-  "x": 120,
-  "y": 20,
-}
-`;
-
-exports[`Test Transform > should transform frames and update frame ids when regenerated 3`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "boundElements": null,
-  "fillStyle": "solid",
-  "frameId": null,
-  "groupIds": [],
-  "height": 126,
-  "id": Any<String>,
-  "isDeleted": false,
-  "link": null,
-  "locked": false,
-  "name": "My frame",
-  "opacity": 100,
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "type": "frame",
-  "updated": 1,
-  "version": 1,
-  "versionNonce": Any<Number>,
-  "width": 470,
-  "x": 0,
-  "y": 0,
-}
-`;
-
-exports[`Test Transform > should transform frames and update frame ids when regenerated 4`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "baseline": 0,
-  "boundElements": null,
-  "containerId": "2",
-  "fillStyle": "solid",
-  "fontFamily": 1,
-  "fontSize": 30,
-  "frameId": "id33",
-  "groupIds": [],
-  "height": 37.5,
-  "id": Any<String>,
-  "isDeleted": false,
-  "lineHeight": 1.25,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "originalText": "HELLO EXCALIDRAW",
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#099268",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "text": "HELLO EXCALIDRAW",
-  "textAlign": "center",
-  "type": "text",
-  "updated": 1,
-  "version": 2,
-  "versionNonce": Any<Number>,
-  "verticalAlign": "middle",
-  "width": 160,
-  "x": 210,
-  "y": 49.25,
-}
-`;
-
 exports[`Test Transform > should transform linear elements 1`] = `
 {
   "angle": 0,
@@ -1605,7 +1335,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
   "fillStyle": "solid",
   "frameId": null,
   "groupIds": [],
-  "height": 130,
+  "height": 0,
   "id": Any<String>,
   "isDeleted": false,
   "lastCommittedPoint": null,
@@ -1632,7 +1362,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
   "strokeWidth": 2,
   "type": "arrow",
   "updated": 1,
-  "version": 2,
+  "version": 1,
   "versionNonce": Any<Number>,
   "width": 100,
   "x": 100,
@@ -1655,7 +1385,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
   "fillStyle": "solid",
   "frameId": null,
   "groupIds": [],
-  "height": 130,
+  "height": 0,
   "id": Any<String>,
   "isDeleted": false,
   "lastCommittedPoint": null,
@@ -1682,7 +1412,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
   "strokeWidth": 2,
   "type": "arrow",
   "updated": 1,
-  "version": 2,
+  "version": 1,
   "versionNonce": Any<Number>,
   "width": 100,
   "x": 100,
@@ -2300,785 +2030,3 @@ CONTAINER",
   "y": 522.5735931288071,
 }
 `;
-
-exports[`Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 1`] = `
-{
-  "angle": 0,
-  "backgroundColor": "#d8f5a2",
-  "boundElements": [
-    {
-      "id": "id43",
-      "type": "arrow",
-    },
-    {
-      "id": "id44",
-      "type": "arrow",
-    },
-  ],
-  "fillStyle": "solid",
-  "frameId": null,
-  "groupIds": [],
-  "height": 300,
-  "id": Any<String>,
-  "isDeleted": false,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#66a80f",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "type": "ellipse",
-  "updated": 1,
-  "version": 3,
-  "versionNonce": Any<Number>,
-  "width": 300,
-  "x": 630,
-  "y": 316,
-}
-`;
-
-exports[`Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 2`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "boundElements": [
-    {
-      "id": "id44",
-      "type": "arrow",
-    },
-  ],
-  "fillStyle": "solid",
-  "frameId": null,
-  "groupIds": [],
-  "height": 100,
-  "id": Any<String>,
-  "isDeleted": false,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#9c36b5",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "type": "diamond",
-  "updated": 1,
-  "version": 2,
-  "versionNonce": Any<Number>,
-  "width": 140,
-  "x": 96,
-  "y": 400,
-}
-`;
-
-exports[`Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 3`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "boundElements": null,
-  "endArrowhead": "arrow",
-  "endBinding": {
-    "elementId": "ellipse-1",
-    "focus": -0.008153707962747813,
-    "gap": 1,
-  },
-  "fillStyle": "solid",
-  "frameId": null,
-  "groupIds": [],
-  "height": 35,
-  "id": Any<String>,
-  "isDeleted": false,
-  "lastCommittedPoint": null,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "points": [
-    [
-      0.5,
-      0.5,
-    ],
-    [
-      394.5,
-      34.5,
-    ],
-  ],
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "startArrowhead": null,
-  "startBinding": {
-    "elementId": "id45",
-    "focus": -0.08139534883720931,
-    "gap": 1,
-  },
-  "strokeColor": "#1864ab",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "type": "arrow",
-  "updated": 1,
-  "version": 3,
-  "versionNonce": Any<Number>,
-  "width": 395,
-  "x": 247,
-  "y": 420,
-}
-`;
-
-exports[`Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 4`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "boundElements": null,
-  "endArrowhead": "arrow",
-  "endBinding": {
-    "elementId": "ellipse-1",
-    "focus": 0.10666666666666667,
-    "gap": 3.834326468444573,
-  },
-  "fillStyle": "solid",
-  "frameId": null,
-  "groupIds": [],
-  "height": 0,
-  "id": Any<String>,
-  "isDeleted": false,
-  "lastCommittedPoint": null,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "points": [
-    [
-      0.5,
-      0,
-    ],
-    [
-      399.5,
-      0,
-    ],
-  ],
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "startArrowhead": null,
-  "startBinding": {
-    "elementId": "diamond-1",
-    "focus": 0,
-    "gap": 1,
-  },
-  "strokeColor": "#e67700",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "type": "arrow",
-  "updated": 1,
-  "version": 3,
-  "versionNonce": Any<Number>,
-  "width": 400,
-  "x": 227,
-  "y": 450,
-}
-`;
-
-exports[`Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 5`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "boundElements": [
-    {
-      "id": "id43",
-      "type": "arrow",
-    },
-  ],
-  "fillStyle": "solid",
-  "frameId": null,
-  "groupIds": [],
-  "height": 300,
-  "id": Any<String>,
-  "isDeleted": false,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "type": "rectangle",
-  "updated": 1,
-  "version": 2,
-  "versionNonce": Any<Number>,
-  "width": 300,
-  "x": -53,
-  "y": 270,
-}
-`;
-
-exports[`Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 1`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "baseline": 0,
-  "boundElements": [
-    {
-      "id": "id46",
-      "type": "arrow",
-    },
-  ],
-  "containerId": null,
-  "fillStyle": "solid",
-  "fontFamily": 1,
-  "fontSize": 20,
-  "frameId": null,
-  "groupIds": [],
-  "height": 25,
-  "id": Any<String>,
-  "isDeleted": false,
-  "lineHeight": 1.25,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "originalText": "HEYYYYY",
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#c2255c",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "text": "HEYYYYY",
-  "textAlign": "left",
-  "type": "text",
-  "updated": 1,
-  "version": 2,
-  "versionNonce": Any<Number>,
-  "verticalAlign": "top",
-  "width": 70,
-  "x": 185,
-  "y": 226.5,
-}
-`;
-
-exports[`Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 2`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "baseline": 0,
-  "boundElements": [
-    {
-      "id": "id46",
-      "type": "arrow",
-    },
-  ],
-  "containerId": null,
-  "fillStyle": "solid",
-  "fontFamily": 1,
-  "fontSize": 20,
-  "frameId": null,
-  "groupIds": [],
-  "height": 25,
-  "id": Any<String>,
-  "isDeleted": false,
-  "lineHeight": 1.25,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "originalText": "Whats up ?",
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "text": "Whats up ?",
-  "textAlign": "left",
-  "type": "text",
-  "updated": 1,
-  "version": 2,
-  "versionNonce": Any<Number>,
-  "verticalAlign": "top",
-  "width": 100,
-  "x": 560,
-  "y": 226.5,
-}
-`;
-
-exports[`Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 3`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "boundElements": [
-    {
-      "id": "id47",
-      "type": "text",
-    },
-  ],
-  "endArrowhead": "arrow",
-  "endBinding": {
-    "elementId": "text-2",
-    "focus": 0,
-    "gap": 205,
-  },
-  "fillStyle": "solid",
-  "frameId": null,
-  "groupIds": [],
-  "height": 0,
-  "id": Any<String>,
-  "isDeleted": false,
-  "lastCommittedPoint": null,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "points": [
-    [
-      0.5,
-      0,
-    ],
-    [
-      99.5,
-      0,
-    ],
-  ],
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "startArrowhead": null,
-  "startBinding": {
-    "elementId": "text-1",
-    "focus": 0,
-    "gap": 1,
-  },
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "type": "arrow",
-  "updated": 1,
-  "version": 3,
-  "versionNonce": Any<Number>,
-  "width": 100,
-  "x": 255,
-  "y": 239,
-}
-`;
-
-exports[`Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 4`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "baseline": 0,
-  "boundElements": null,
-  "containerId": "id46",
-  "fillStyle": "solid",
-  "fontFamily": 1,
-  "fontSize": 20,
-  "frameId": null,
-  "groupIds": [],
-  "height": 25,
-  "id": Any<String>,
-  "isDeleted": false,
-  "lineHeight": 1.25,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "originalText": "HELLO WORLD!!",
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "text": "HELLO WORLD!!",
-  "textAlign": "center",
-  "type": "text",
-  "updated": 1,
-  "version": 2,
-  "versionNonce": Any<Number>,
-  "verticalAlign": "middle",
-  "width": 130,
-  "x": 240,
-  "y": 226.5,
-}
-`;
-
-exports[`Test arrow bindings > should bind arrows to shapes when start / end provided without ids 1`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "boundElements": [
-    {
-      "id": "id36",
-      "type": "text",
-    },
-  ],
-  "endArrowhead": "arrow",
-  "endBinding": {
-    "elementId": "id38",
-    "focus": 0,
-    "gap": 1,
-  },
-  "fillStyle": "solid",
-  "frameId": null,
-  "groupIds": [],
-  "height": 0,
-  "id": Any<String>,
-  "isDeleted": false,
-  "lastCommittedPoint": null,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "points": [
-    [
-      0.5,
-      0,
-    ],
-    [
-      99.5,
-      0,
-    ],
-  ],
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "startArrowhead": null,
-  "startBinding": {
-    "elementId": "id37",
-    "focus": 0,
-    "gap": 1,
-  },
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "type": "arrow",
-  "updated": 1,
-  "version": 3,
-  "versionNonce": Any<Number>,
-  "width": 100,
-  "x": 255,
-  "y": 239,
-}
-`;
-
-exports[`Test arrow bindings > should bind arrows to shapes when start / end provided without ids 2`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "baseline": 0,
-  "boundElements": null,
-  "containerId": "id35",
-  "fillStyle": "solid",
-  "fontFamily": 1,
-  "fontSize": 20,
-  "frameId": null,
-  "groupIds": [],
-  "height": 25,
-  "id": Any<String>,
-  "isDeleted": false,
-  "lineHeight": 1.25,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "originalText": "HELLO WORLD!!",
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "text": "HELLO WORLD!!",
-  "textAlign": "center",
-  "type": "text",
-  "updated": 1,
-  "version": 2,
-  "versionNonce": Any<Number>,
-  "verticalAlign": "middle",
-  "width": 130,
-  "x": 240,
-  "y": 226.5,
-}
-`;
-
-exports[`Test arrow bindings > should bind arrows to shapes when start / end provided without ids 3`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "boundElements": [
-    {
-      "id": "id35",
-      "type": "arrow",
-    },
-  ],
-  "fillStyle": "solid",
-  "frameId": null,
-  "groupIds": [],
-  "height": 100,
-  "id": Any<String>,
-  "isDeleted": false,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "type": "rectangle",
-  "updated": 1,
-  "version": 2,
-  "versionNonce": Any<Number>,
-  "width": 100,
-  "x": 155,
-  "y": 189,
-}
-`;
-
-exports[`Test arrow bindings > should bind arrows to shapes when start / end provided without ids 4`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "boundElements": [
-    {
-      "id": "id35",
-      "type": "arrow",
-    },
-  ],
-  "fillStyle": "solid",
-  "frameId": null,
-  "groupIds": [],
-  "height": 100,
-  "id": Any<String>,
-  "isDeleted": false,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "type": "ellipse",
-  "updated": 1,
-  "version": 2,
-  "versionNonce": Any<Number>,
-  "width": 100,
-  "x": 355,
-  "y": 189,
-}
-`;
-
-exports[`Test arrow bindings > should bind arrows to text when start / end provided without ids 1`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "boundElements": [
-    {
-      "id": "id40",
-      "type": "text",
-    },
-  ],
-  "endArrowhead": "arrow",
-  "endBinding": {
-    "elementId": "id42",
-    "focus": 0,
-    "gap": 1,
-  },
-  "fillStyle": "solid",
-  "frameId": null,
-  "groupIds": [],
-  "height": 0,
-  "id": Any<String>,
-  "isDeleted": false,
-  "lastCommittedPoint": null,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "points": [
-    [
-      0.5,
-      0,
-    ],
-    [
-      99.5,
-      0,
-    ],
-  ],
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "startArrowhead": null,
-  "startBinding": {
-    "elementId": "id41",
-    "focus": 0,
-    "gap": 1,
-  },
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "type": "arrow",
-  "updated": 1,
-  "version": 3,
-  "versionNonce": Any<Number>,
-  "width": 100,
-  "x": 255,
-  "y": 239,
-}
-`;
-
-exports[`Test arrow bindings > should bind arrows to text when start / end provided without ids 2`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "baseline": 0,
-  "boundElements": null,
-  "containerId": "id39",
-  "fillStyle": "solid",
-  "fontFamily": 1,
-  "fontSize": 20,
-  "frameId": null,
-  "groupIds": [],
-  "height": 25,
-  "id": Any<String>,
-  "isDeleted": false,
-  "lineHeight": 1.25,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "originalText": "HELLO WORLD!!",
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "text": "HELLO WORLD!!",
-  "textAlign": "center",
-  "type": "text",
-  "updated": 1,
-  "version": 2,
-  "versionNonce": Any<Number>,
-  "verticalAlign": "middle",
-  "width": 130,
-  "x": 240,
-  "y": 226.5,
-}
-`;
-
-exports[`Test arrow bindings > should bind arrows to text when start / end provided without ids 3`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "baseline": 0,
-  "boundElements": [
-    {
-      "id": "id39",
-      "type": "arrow",
-    },
-  ],
-  "containerId": null,
-  "fillStyle": "solid",
-  "fontFamily": 1,
-  "fontSize": 20,
-  "frameId": null,
-  "groupIds": [],
-  "height": 25,
-  "id": Any<String>,
-  "isDeleted": false,
-  "lineHeight": 1.25,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "originalText": "HEYYYYY",
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "text": "HEYYYYY",
-  "textAlign": "left",
-  "type": "text",
-  "updated": 1,
-  "version": 2,
-  "versionNonce": Any<Number>,
-  "verticalAlign": "top",
-  "width": 70,
-  "x": 185,
-  "y": 226.5,
-}
-`;
-
-exports[`Test arrow bindings > should bind arrows to text when start / end provided without ids 4`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "baseline": 0,
-  "boundElements": [
-    {
-      "id": "id39",
-      "type": "arrow",
-    },
-  ],
-  "containerId": null,
-  "fillStyle": "solid",
-  "fontFamily": 1,
-  "fontSize": 20,
-  "frameId": null,
-  "groupIds": [],
-  "height": 25,
-  "id": Any<String>,
-  "isDeleted": false,
-  "lineHeight": 1.25,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "originalText": "WHATS UP ?",
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "text": "WHATS UP ?",
-  "textAlign": "left",
-  "type": "text",
-  "updated": 1,
-  "version": 2,
-  "versionNonce": Any<Number>,
-  "verticalAlign": "top",
-  "width": 100,
-  "x": 355,
-  "y": 226.5,
-}
-`;
-
-exports[`should not allow duplicate ids 1`] = `
-{
-  "angle": 0,
-  "backgroundColor": "transparent",
-  "boundElements": null,
-  "fillStyle": "solid",
-  "frameId": null,
-  "groupIds": [],
-  "height": 200,
-  "id": "rect-1",
-  "isDeleted": false,
-  "link": null,
-  "locked": false,
-  "opacity": 100,
-  "roughness": 1,
-  "roundness": null,
-  "seed": Any<Number>,
-  "strokeColor": "#1e1e1e",
-  "strokeStyle": "solid",
-  "strokeWidth": 2,
-  "type": "rectangle",
-  "updated": 1,
-  "version": 1,
-  "versionNonce": Any<Number>,
-  "width": 100,
-  "x": 300,
-  "y": 100,
-}
-`;

+ 1 - 1
src/element/textElement.ts

@@ -91,7 +91,7 @@ export const redrawTextBoundingBox = (
     );
     const maxContainerWidth = getBoundTextMaxWidth(container);
 
-    if (metrics.height > maxContainerHeight) {
+    if (!isArrowElement(container) && metrics.height > maxContainerHeight) {
       const nextHeight = computeContainerDimensionForBoundText(
         metrics.height,
         container.type,

+ 39 - 42
src/element/textWysiwyg.test.tsx

@@ -18,7 +18,7 @@ import {
 import { API } from "../tests/helpers/api";
 import { mutateElement } from "./mutateElement";
 import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
-import { getTextEditor } from "../tests/queries/dom";
+import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
 
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@@ -26,10 +26,7 @@ ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 const tab = "    ";
 const mouse = new Pointer("mouse");
 
-const updateTextEditor = (editor: HTMLTextAreaElement, value: string) => {
-  fireEvent.change(editor, { target: { value } });
-  editor.dispatchEvent(new Event("input"));
-};
+const textEditorSelector = ".excalidraw-textEditorContainer > textarea";
 
 describe("textWysiwyg", () => {
   describe("start text editing", () => {
@@ -195,7 +192,7 @@ describe("textWysiwyg", () => {
 
       mouse.clickAt(text.x + 50, text.y + 50);
 
-      const editor = await getTextEditor(false);
+      const editor = await getTextEditor(textEditorSelector, false);
 
       expect(editor).not.toBe(null);
       expect(h.state.editingElement?.id).toBe(text.id);
@@ -217,7 +214,7 @@ describe("textWysiwyg", () => {
 
       mouse.doubleClickAt(text.x + 50, text.y + 50);
 
-      const editor = await getTextEditor(false);
+      const editor = await getTextEditor(textEditorSelector, false);
 
       expect(editor).not.toBe(null);
       expect(h.state.editingElement?.id).toBe(text.id);
@@ -258,7 +255,7 @@ describe("textWysiwyg", () => {
       textElement = UI.createElement("text");
 
       mouse.clickOn(textElement);
-      textarea = await getTextEditor(true);
+      textarea = await getTextEditor(textEditorSelector, true);
     });
 
     afterAll(() => {
@@ -468,7 +465,7 @@ describe("textWysiwyg", () => {
       UI.clickTool("text");
       mouse.clickAt(750, 300);
 
-      textarea = await getTextEditor(true);
+      textarea = await getTextEditor(textEditorSelector, true);
       updateTextEditor(
         textarea,
         "Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
@@ -520,7 +517,7 @@ describe("textWysiwyg", () => {
         { id: text.id, type: "text" },
       ]);
       mouse.down();
-      const editor = await getTextEditor(true);
+      const editor = await getTextEditor(textEditorSelector, true);
 
       updateTextEditor(editor, "Hello World!");
 
@@ -548,7 +545,7 @@ describe("textWysiwyg", () => {
       ]);
       expect(text.angle).toBe(rectangle.angle);
       mouse.down();
-      const editor = await getTextEditor(true);
+      const editor = await getTextEditor(textEditorSelector, true);
 
       updateTextEditor(editor, "Hello World!");
 
@@ -575,7 +572,7 @@ describe("textWysiwyg", () => {
       API.setSelectedElements([diamond]);
       Keyboard.keyPress(KEYS.ENTER);
 
-      const editor = await getTextEditor(true);
+      const editor = await getTextEditor(textEditorSelector, true);
 
       await new Promise((r) => setTimeout(r, 0));
       const value = new Array(1000).fill("1").join("\n");
@@ -610,7 +607,7 @@ describe("textWysiwyg", () => {
       expect(text.type).toBe("text");
       expect(text.containerId).toBe(null);
       mouse.down();
-      let editor = await getTextEditor(true);
+      let editor = await getTextEditor(textEditorSelector, true);
       await new Promise((r) => setTimeout(r, 0));
       editor.blur();
 
@@ -625,7 +622,7 @@ describe("textWysiwyg", () => {
       expect(text.containerId).toBe(rectangle.id);
 
       mouse.down();
-      editor = await getTextEditor(true);
+      editor = await getTextEditor(textEditorSelector, true);
 
       updateTextEditor(editor, "Hello World!");
       await new Promise((r) => setTimeout(r, 0));
@@ -647,7 +644,7 @@ describe("textWysiwyg", () => {
       const text = h.elements[1] as ExcalidrawTextElementWithContainer;
       expect(text.type).toBe("text");
       expect(text.containerId).toBe(rectangle.id);
-      const editor = await getTextEditor(true);
+      const editor = await getTextEditor(textEditorSelector, true);
 
       await new Promise((r) => setTimeout(r, 0));
 
@@ -682,7 +679,7 @@ describe("textWysiwyg", () => {
         { id: text.id, type: "text" },
       ]);
       mouse.down();
-      const editor = await getTextEditor(true);
+      const editor = await getTextEditor(textEditorSelector, true);
       updateTextEditor(editor, "Hello World!");
 
       await new Promise((r) => setTimeout(r, 0));
@@ -707,7 +704,7 @@ describe("textWysiwyg", () => {
         freedraw.y + freedraw.height / 2,
       );
 
-      const editor = await getTextEditor(true);
+      const editor = await getTextEditor(textEditorSelector, true);
       updateTextEditor(editor, "Hello World!");
       fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
 
@@ -741,7 +738,7 @@ describe("textWysiwyg", () => {
       expect(text.type).toBe("text");
       expect(text.containerId).toBe(null);
       mouse.down();
-      const editor = await getTextEditor(true);
+      const editor = await getTextEditor(textEditorSelector, true);
 
       updateTextEditor(editor, "Hello World!");
 
@@ -756,7 +753,7 @@ describe("textWysiwyg", () => {
 
       UI.clickTool("text");
       mouse.clickAt(20, 30);
-      const editor = await getTextEditor(true);
+      const editor = await getTextEditor(textEditorSelector, true);
 
       updateTextEditor(
         editor,
@@ -801,7 +798,7 @@ describe("textWysiwyg", () => {
       mouse.down();
 
       const text = h.elements[1] as ExcalidrawTextElementWithContainer;
-      let editor = await getTextEditor(true);
+      let editor = await getTextEditor(textEditorSelector, true);
 
       await new Promise((r) => setTimeout(r, 0));
       updateTextEditor(editor, "Hello World!");
@@ -814,7 +811,7 @@ describe("textWysiwyg", () => {
         rectangle.y + rectangle.height / 2,
       );
       mouse.down();
-      editor = await getTextEditor(true);
+      editor = await getTextEditor(textEditorSelector, true);
 
       editor.select();
       fireEvent.click(screen.getByTitle(/code/i));
@@ -847,7 +844,7 @@ describe("textWysiwyg", () => {
 
       Keyboard.keyDown(KEYS.ENTER);
       let text = h.elements[1] as ExcalidrawTextElementWithContainer;
-      let editor = await getTextEditor(true);
+      let editor = await getTextEditor(textEditorSelector, true);
 
       updateTextEditor(editor, "Hello World!");
 
@@ -868,7 +865,7 @@ describe("textWysiwyg", () => {
       mouse.select(rectangle);
       Keyboard.keyPress(KEYS.ENTER);
 
-      editor = await getTextEditor(true);
+      editor = await getTextEditor(textEditorSelector, true);
       updateTextEditor(editor, "Hello");
 
       await new Promise((r) => setTimeout(r, 0));
@@ -897,7 +894,7 @@ describe("textWysiwyg", () => {
       const text = h.elements[1] as ExcalidrawTextElementWithContainer;
       expect(text.containerId).toBe(rectangle.id);
 
-      const editor = await getTextEditor(true);
+      const editor = await getTextEditor(textEditorSelector, true);
 
       await new Promise((r) => setTimeout(r, 0));
 
@@ -934,7 +931,7 @@ describe("textWysiwyg", () => {
       // Bind first text
       const text = h.elements[1] as ExcalidrawTextElementWithContainer;
       expect(text.containerId).toBe(rectangle.id);
-      const editor = await getTextEditor(true);
+      const editor = await getTextEditor(textEditorSelector, true);
       await new Promise((r) => setTimeout(r, 0));
       updateTextEditor(editor, "Hello World!");
       editor.blur();
@@ -955,7 +952,7 @@ describe("textWysiwyg", () => {
     it("should respect text alignment when resizing", async () => {
       Keyboard.keyPress(KEYS.ENTER);
 
-      let editor = await getTextEditor(true);
+      let editor = await getTextEditor(textEditorSelector, true);
       await new Promise((r) => setTimeout(r, 0));
       updateTextEditor(editor, "Hello");
       editor.blur();
@@ -972,7 +969,7 @@ describe("textWysiwyg", () => {
       mouse.select(rectangle);
       Keyboard.keyPress(KEYS.ENTER);
 
-      editor = await getTextEditor(true);
+      editor = await getTextEditor(textEditorSelector, true);
 
       editor.select();
 
@@ -995,7 +992,7 @@ describe("textWysiwyg", () => {
 
       mouse.select(rectangle);
       Keyboard.keyPress(KEYS.ENTER);
-      editor = await getTextEditor(true);
+      editor = await getTextEditor(textEditorSelector, true);
 
       editor.select();
 
@@ -1033,7 +1030,7 @@ describe("textWysiwyg", () => {
       expect(text.type).toBe("text");
       expect(text.containerId).toBe(rectangle.id);
       mouse.down();
-      const editor = await getTextEditor(true);
+      const editor = await getTextEditor(textEditorSelector, true);
 
       updateTextEditor(editor, "Hello World!");
 
@@ -1048,7 +1045,7 @@ describe("textWysiwyg", () => {
     it("should scale font size correctly when resizing using shift", async () => {
       Keyboard.keyPress(KEYS.ENTER);
 
-      const editor = await getTextEditor(true);
+      const editor = await getTextEditor(textEditorSelector, true);
       await new Promise((r) => setTimeout(r, 0));
       updateTextEditor(editor, "Hello");
       editor.blur();
@@ -1068,7 +1065,7 @@ describe("textWysiwyg", () => {
     it("should bind text correctly when container duplicated with alt-drag", async () => {
       Keyboard.keyPress(KEYS.ENTER);
 
-      const editor = await getTextEditor(true);
+      const editor = await getTextEditor(textEditorSelector, true);
       await new Promise((r) => setTimeout(r, 0));
       updateTextEditor(editor, "Hello");
       editor.blur();
@@ -1100,7 +1097,7 @@ describe("textWysiwyg", () => {
 
     it("undo should work", async () => {
       Keyboard.keyPress(KEYS.ENTER);
-      const editor = await getTextEditor(true);
+      const editor = await getTextEditor(textEditorSelector, true);
       await new Promise((r) => setTimeout(r, 0));
       updateTextEditor(editor, "Hello");
       editor.blur();
@@ -1137,7 +1134,7 @@ describe("textWysiwyg", () => {
 
     it("should not allow bound text with only whitespaces", async () => {
       Keyboard.keyPress(KEYS.ENTER);
-      const editor = await getTextEditor(true);
+      const editor = await getTextEditor(textEditorSelector, true);
       await new Promise((r) => setTimeout(r, 0));
 
       updateTextEditor(editor, "   ");
@@ -1192,7 +1189,7 @@ describe("textWysiwyg", () => {
     it("should reset the container height cache when resizing", async () => {
       Keyboard.keyPress(KEYS.ENTER);
       expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
-      let editor = await getTextEditor(true);
+      let editor = await getTextEditor(textEditorSelector, true);
       await new Promise((r) => setTimeout(r, 0));
       updateTextEditor(editor, "Hello");
       editor.blur();
@@ -1204,7 +1201,7 @@ describe("textWysiwyg", () => {
       mouse.select(rectangle);
       Keyboard.keyPress(KEYS.ENTER);
 
-      editor = await getTextEditor(true);
+      editor = await getTextEditor(textEditorSelector, true);
 
       await new Promise((r) => setTimeout(r, 0));
       editor.blur();
@@ -1220,7 +1217,7 @@ describe("textWysiwyg", () => {
       Keyboard.keyPress(KEYS.ENTER);
       expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
 
-      const editor = await getTextEditor(true);
+      const editor = await getTextEditor(textEditorSelector, true);
       updateTextEditor(editor, "Hello World!");
       editor.blur();
 
@@ -1245,7 +1242,7 @@ describe("textWysiwyg", () => {
       Keyboard.keyPress(KEYS.ENTER);
       expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
 
-      const editor = await getTextEditor(true);
+      const editor = await getTextEditor(textEditorSelector, true);
       updateTextEditor(editor, "Hello World!");
       editor.blur();
       expect(
@@ -1277,12 +1274,12 @@ describe("textWysiwyg", () => {
 
       beforeEach(async () => {
         Keyboard.keyPress(KEYS.ENTER);
-        editor = await getTextEditor(true);
+        editor = await getTextEditor(textEditorSelector, true);
         updateTextEditor(editor, "Hello");
         editor.blur();
         mouse.select(rectangle);
         Keyboard.keyPress(KEYS.ENTER);
-        editor = await getTextEditor(true);
+        editor = await getTextEditor(textEditorSelector, true);
         editor.select();
       });
 
@@ -1393,7 +1390,7 @@ describe("textWysiwyg", () => {
     it("should wrap text in a container when wrap text in container triggered from context menu", async () => {
       UI.clickTool("text");
       mouse.clickAt(20, 30);
-      const editor = await getTextEditor(true);
+      const editor = await getTextEditor(textEditorSelector, true);
 
       updateTextEditor(
         editor,
@@ -1481,7 +1478,7 @@ describe("textWysiwyg", () => {
       // Bind first text
       let text = h.elements[1] as ExcalidrawTextElementWithContainer;
       expect(text.containerId).toBe(rectangle.id);
-      let editor = await getTextEditor(true);
+      let editor = await getTextEditor(textEditorSelector, true);
       await new Promise((r) => setTimeout(r, 0));
       updateTextEditor(editor, "Hello!");
       expect(
@@ -1506,7 +1503,7 @@ describe("textWysiwyg", () => {
         rectangle.x + rectangle.width / 2,
         rectangle.y + rectangle.height / 2,
       );
-      editor = await getTextEditor(true);
+      editor = await getTextEditor(textEditorSelector, true);
       await new Promise((r) => setTimeout(r, 0));
       updateTextEditor(editor, "Excalidraw");
       editor.blur();

+ 9 - 1
src/locales/en.json

@@ -242,7 +242,8 @@
     "embeddable": "Web Embed",
     "laser": "Laser pointer",
     "hand": "Hand (panning tool)",
-    "extraTools": "More tools"
+    "extraTools": "More tools",
+    "mermaidToExcalidraw": "Mermaid to Excalidraw"
   },
   "headings": {
     "canvasActions": "Canvas actions",
@@ -501,5 +502,12 @@
         "description": "Loading external drawing will <bold>replace your existing content</bold>.<br></br>You can back up your drawing first by using one of the options below."
       }
     }
+  },
+  "mermaid": {
+    "title": "Mermaid to Excalidraw",
+    "button": "Insert",
+    "description": "Currently only <flowchartLink>Flowcharts</flowchartLink> and <sequenceLink>Sequence Diagrams</sequenceLink> are supported. The other types will be rendered as image in Excalidraw.",
+    "syntax": "Mermaid Syntax",
+    "preview": "Preview"
   }
 }

+ 2 - 2
src/packages/excalidraw/.size-limit.json

@@ -1,7 +1,7 @@
 [
   {
     "path": "dist/excalidraw.production.min.js",
-    "limit": "320 kB"
+    "limit": "325 kB"
   },
   {
     "path": "dist/excalidraw-assets/locales",
@@ -11,6 +11,6 @@
   {
     "path": "dist/excalidraw-assets/vendor-*.js",
     "name": "dist/excalidraw-assets/vendor*.js",
-    "limit": "30 kB"
+    "limit": "900 kB"
   }
 ]

+ 8 - 0
src/packages/excalidraw/webpack.dev.config.js

@@ -41,6 +41,14 @@ module.exports = {
           "sass-loader",
         ],
       },
+      // So that type module works with webpack
+      // https://github.com/webpack/webpack/issues/11467#issuecomment-691873586
+      {
+        test: /\.m?js/,
+        resolve: {
+          fullySpecified: false,
+        },
+      },
       {
         test: /\.(ts|tsx|js|jsx|mjs)$/,
         exclude:

+ 8 - 0
src/packages/excalidraw/webpack.prod.config.js

@@ -44,6 +44,14 @@ module.exports = {
           "sass-loader",
         ],
       },
+      // So that type module works with webpack
+      // https://github.com/webpack/webpack/issues/11467#issuecomment-691873586
+      {
+        test: /\.m?js/,
+        resolve: {
+          fullySpecified: false,
+        },
+      },
       {
         test: /\.(ts|tsx|js|jsx|mjs)$/,
         exclude:

+ 167 - 0
src/tests/MermaidToExcalidraw.test.tsx

@@ -0,0 +1,167 @@
+import { act, fireEvent, render } from "./test-utils";
+import { Excalidraw } from "../packages/excalidraw/index";
+import React from "react";
+import { expect, vi } from "vitest";
+import * as MermaidToExcalidraw from "@excalidraw/mermaid-to-excalidraw";
+import { getTextEditor, updateTextEditor } from "./queries/dom";
+
+vi.mock("@excalidraw/mermaid-to-excalidraw", async (importActual) => {
+  const module = (await importActual()) as any;
+
+  return {
+    __esModule: true,
+    ...module,
+  };
+});
+const parseMermaidToExcalidrawSpy = vi.spyOn(
+  MermaidToExcalidraw,
+  "parseMermaidToExcalidraw",
+);
+
+parseMermaidToExcalidrawSpy.mockImplementation(
+  async (
+    definition: string,
+    options?: MermaidToExcalidraw.MermaidOptions | undefined,
+  ) => {
+    const firstLine = definition.split("\n")[0];
+    return new Promise((resolve, reject) => {
+      if (firstLine === "flowchart TD") {
+        resolve({
+          elements: [
+            {
+              id: "Start",
+              type: "rectangle",
+              groupIds: [],
+              x: 0,
+              y: 0,
+              width: 69.703125,
+              height: 44,
+              strokeWidth: 2,
+              label: {
+                groupIds: [],
+                text: "Start",
+                fontSize: 20,
+              },
+              link: null,
+            },
+            {
+              id: "Stop",
+              type: "rectangle",
+              groupIds: [],
+              x: 2.7109375,
+              y: 94,
+              width: 64.28125,
+              height: 44,
+              strokeWidth: 2,
+              label: {
+                groupIds: [],
+                text: "Stop",
+                fontSize: 20,
+              },
+              link: null,
+            },
+            {
+              id: "Start_Stop",
+              type: "arrow",
+              groupIds: [],
+              x: 34.852,
+              y: 44,
+              strokeWidth: 2,
+              points: [
+                [0, 0],
+                [0, 50],
+              ],
+              roundness: {
+                type: 2,
+              },
+              start: {
+                id: "Start",
+              },
+              end: {
+                id: "Stop",
+              },
+            },
+          ],
+        });
+      } else {
+        reject(new Error("ERROR"));
+      }
+    });
+  },
+);
+
+vi.spyOn(React, "useRef").mockReturnValue({
+  current: {
+    parseMermaidToExcalidraw: parseMermaidToExcalidrawSpy,
+  },
+});
+
+describe("Test <MermaidToExcalidraw/>", () => {
+  beforeEach(async () => {
+    await render(
+      <Excalidraw
+        initialData={{
+          appState: {
+            openDialog: "mermaid",
+          },
+        }}
+      />,
+    );
+  });
+
+  it("should open mermaid popup when active tool is mermaid", async () => {
+    const dialog = document.querySelector(".dialog-mermaid")!;
+
+    expect(dialog.outerHTML).toMatchSnapshot();
+  });
+
+  it("should close the popup and set the tool to selection when close button clicked", () => {
+    const dialog = document.querySelector(".dialog-mermaid")!;
+    const closeBtn = dialog.querySelector(".Dialog__close")!;
+    fireEvent.click(closeBtn);
+    expect(document.querySelector(".dialog-mermaid")).toBe(null);
+    expect(window.h.state.activeTool).toStrictEqual({
+      customType: null,
+      lastActiveTool: null,
+      locked: false,
+      type: "selection",
+    });
+  });
+
+  it("should show error in preview when mermaid library throws error", async () => {
+    const dialog = document.querySelector(".dialog-mermaid")!;
+    const selector = ".dialog-mermaid-panels-text textarea";
+    let editor = await getTextEditor(selector, false);
+
+    expect(dialog.querySelector('[data-testid="mermaid-error"]')).toBeNull();
+
+    expect(editor.textContent).toMatchInlineSnapshot(`
+      "flowchart TD
+       A[Christmas] -->|Get money| B(Go shopping)
+       B --> C{Let me think}
+       C -->|One| D[Laptop]
+       C -->|Two| E[iPhone]
+       C -->|Three| F[Car]"
+    `);
+
+    await act(async () => {
+      updateTextEditor(editor, "flowchart TD1");
+      await new Promise((cb) => setTimeout(cb, 0));
+    });
+    editor = await getTextEditor(selector, false);
+
+    expect(editor.textContent).toBe("flowchart TD1");
+    expect(dialog.querySelector('[data-testid="mermaid-error"]'))
+      .toMatchInlineSnapshot(`
+        <div
+          class="mermaid-error"
+          data-testid="mermaid-error"
+        >
+          Error! 
+          <p>
+            ERROR
+          </p>
+        </div>
+      `);
+  });
+});

+ 10 - 0
src/tests/__snapshots__/MermaidToExcalidraw.test.tsx.snap

@@ -0,0 +1,10 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Test <MermaidToExcalidraw/> > should open mermaid popup when active tool is mermaid 1`] = `
+"<div class=\\"Modal Dialog dialog-mermaid\\" role=\\"dialog\\" aria-modal=\\"true\\" aria-labelledby=\\"dialog-title\\" data-prevent-outside-click=\\"true\\"><div class=\\"Modal__background\\"></div><div class=\\"Modal__content\\" style=\\"--max-width: 1200px;\\" tabindex=\\"0\\"><div class=\\"Island\\"><h2 id=\\"test-id-dialog-title\\" class=\\"Dialog__title\\"><span class=\\"Dialog__titleContent\\"><p class=\\"dialog-mermaid-title\\">Mermaid to Excalidraw</p><span class=\\"dialog-mermaid-desc\\">Currently only <a href=\\"https://mermaid.js.org/syntax/flowchart.html\\">Flowcharts</a> and <a href=\\"https://mermaid.js.org/syntax/sequenceDiagram.html\\">Sequence Diagrams</a> are supported. The other types will be rendered as image in Excalidraw.<br></span></span></h2><button class=\\"Dialog__close\\" title=\\"Close\\" aria-label=\\"Close\\"><svg aria-hidden=\\"true\\" focusable=\\"false\\" role=\\"img\\" viewBox=\\"0 0 20 20\\" class=\\"\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"><g clip-path=\\"url(#a)\\" stroke=\\"currentColor\\" stroke-width=\\"1.25\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"><path d=\\"M15 5 5 15M5 5l10 10\\"></path></g><defs><clipPath id=\\"a\\"><path fill=\\"#fff\\" d=\\"M0 0h20v20H0z\\"></path></clipPath></defs></svg></button><div class=\\"Dialog__content\\"><div class=\\"dialog-mermaid-body\\"><div class=\\"dialog-mermaid-panels\\"><div class=\\"dialog-mermaid-panels-text\\"><label>Mermaid Syntax</label><textarea>flowchart TD
+ A[Christmas] --&gt;|Get money| B(Go shopping)
+ B --&gt; C{Let me think}
+ C --&gt;|One| D[Laptop]
+ C --&gt;|Two| E[iPhone]
+ C --&gt;|Three| F[Car]</textarea></div><div class=\\"dialog-mermaid-panels-preview\\"><label>Preview</label><div class=\\"dialog-mermaid-panels-preview-wrapper\\"><div style=\\"opacity: 1;\\" class=\\"dialog-mermaid-panels-preview-canvas-container\\"></div></div></div></div><div class=\\"dialog-mermaid-buttons\\"><button type=\\"button\\" class=\\"excalidraw-button dialog-mermaid-insert\\">Insert<span><svg aria-hidden=\\"true\\" focusable=\\"false\\" role=\\"img\\" viewBox=\\"0 0 20 20\\" class=\\"\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"><g stroke-width=\\"1.25\\"><path d=\\"M4.16602 10H15.8327\\"></path><path d=\\"M12.5 13.3333L15.8333 10\\"></path><path d=\\"M12.5 6.66666L15.8333 9.99999\\"></path></g></svg></span></button></div></div></div></div></div></div>"
+`;

+ 4 - 4
src/tests/helpers/ui.ts

@@ -468,16 +468,16 @@ export class UI {
   static async editText<
     T extends ExcalidrawTextElement | ExcalidrawTextContainer,
   >(element: T, text: string) {
-    const openedEditor = document.querySelector<HTMLTextAreaElement>(
-      ".excalidraw-textEditorContainer > textarea",
-    );
+    const textEditorSelector = ".excalidraw-textEditorContainer > textarea";
+    const openedEditor =
+      document.querySelector<HTMLTextAreaElement>(textEditorSelector);
 
     if (!openedEditor) {
       mouse.select(element);
       Keyboard.keyPress(KEYS.ENTER);
     }
 
-    const editor = await getTextEditor();
+    const editor = await getTextEditor(textEditorSelector);
     if (!editor) {
       throw new Error("Can't find wysiwyg text editor in the dom");
     }

+ 4 - 4
src/tests/linearElementEditor.test.tsx

@@ -273,7 +273,7 @@ describe("Test Linear Elements", () => {
 
       // drag line from midpoint
       drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
-      expect(renderInteractiveScene).toHaveBeenCalledTimes(14);
+      expect(renderInteractiveScene).toHaveBeenCalledTimes(13);
       expect(renderStaticScene).toHaveBeenCalledTimes(6);
 
       expect(line.points.length).toEqual(3);
@@ -416,7 +416,7 @@ describe("Test Linear Elements", () => {
           lastSegmentMidpoint[1] + delta,
         ]);
 
-        expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
+        expect(renderInteractiveScene).toHaveBeenCalledTimes(19);
         expect(renderStaticScene).toHaveBeenCalledTimes(9);
 
         expect(line.points.length).toEqual(5);
@@ -519,7 +519,7 @@ describe("Test Linear Elements", () => {
         // delete 3rd point
         deletePoint(points[2]);
         expect(line.points.length).toEqual(3);
-        expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
+        expect(renderInteractiveScene).toHaveBeenCalledTimes(20);
         expect(renderStaticScene).toHaveBeenCalledTimes(9);
 
         const newMidPoints = LinearElementEditor.getEditorMidPoints(
@@ -566,7 +566,7 @@ describe("Test Linear Elements", () => {
           lastSegmentMidpoint[0] + delta,
           lastSegmentMidpoint[1] + delta,
         ]);
-        expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
+        expect(renderInteractiveScene).toHaveBeenCalledTimes(19);
         expect(renderStaticScene).toHaveBeenCalledTimes(9);
         expect(line.points.length).toEqual(5);
 

+ 11 - 5
src/tests/queries/dom.ts

@@ -1,13 +1,19 @@
 import { waitFor } from "@testing-library/dom";
+import { fireEvent } from "@testing-library/react";
 
-export const getTextEditor = async (waitForEditor = true) => {
-  const query = () =>
-    document.querySelector(
-      ".excalidraw-textEditorContainer > textarea",
-    ) as HTMLTextAreaElement;
+export const getTextEditor = async (selector: string, waitForEditor = true) => {
+  const query = () => document.querySelector(selector) as HTMLTextAreaElement;
   if (waitForEditor) {
     await waitFor(() => expect(query()).not.toBe(null));
     return query();
   }
   return query();
 };
+
+export const updateTextEditor = (
+  editor: HTMLTextAreaElement,
+  value: string,
+) => {
+  fireEvent.change(editor, { target: { value } });
+  editor.dispatchEvent(new Event("input"));
+};

+ 5 - 1
src/types.ts

@@ -241,7 +241,7 @@ export type AppState = {
   openMenu: "canvas" | "shape" | null;
   openPopup: "canvasBackground" | "elementBackground" | "elementStroke" | null;
   openSidebar: { name: SidebarName; tab?: SidebarTabName } | null;
-  openDialog: "imageExport" | "help" | "jsonExport" | null;
+  openDialog: "imageExport" | "help" | "jsonExport" | "mermaid" | null;
   /**
    * Reflects user preference for whether the default sidebar should be docked.
    *
@@ -537,8 +537,12 @@ export type AppClassProperties = {
   onInsertElements: App["onInsertElements"];
   onExportImage: App["onExportImage"];
   lastViewportPosition: App["lastViewportPosition"];
+  scrollToContent: App["scrollToContent"];
+  addFiles: App["addFiles"];
+  addElementsFromPasteOrLibrary: App["addElementsFromPasteOrLibrary"];
   togglePenMode: App["togglePenMode"];
   setActiveTool: App["setActiveTool"];
+  setOpenDialog: App["setOpenDialog"];
 };
 
 export type PointerDownState = Readonly<{

+ 1 - 1
vitest.config.ts

@@ -6,7 +6,7 @@ export default defineConfig({
     globals: true,
     environment: "jsdom",
     coverage: {
-      reporter: ["text", "json-summary", "json"],
+      reporter: ["text", "json-summary", "json", "html"],
       lines: 70,
       branches: 70,
       functions: 68,

+ 728 - 2
yarn.lock

@@ -1321,6 +1321,11 @@
   resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz#6110f918d273fe2af8ea1c4398a88774bb9fc12f"
   integrity sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==
 
+"@braintree/sanitize-url@^6.0.2":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783"
+  integrity sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==
+
 "@esbuild/[email protected]":
   version "0.17.19"
   resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd"
@@ -1578,6 +1583,20 @@
   resolved "https://registry.yarnpkg.com/@excalidraw/laser-pointer/-/laser-pointer-1.2.0.tgz#cd34ea7d24b11743c726488cc1fcb28c161cacba"
   integrity sha512-WjFFwLk9ahmKRKku7U0jqYpeM3fe9ZS1K43pfwPREHk4/FYU3iKDKVeS8m4tEAASnRlBt3hhLCBQLBF2uvgOnw==
 
+"@excalidraw/[email protected]":
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/@excalidraw/markdown-to-text/-/markdown-to-text-0.1.2.tgz#1703705e7da608cf478f17bfe96fb295f55a23eb"
+  integrity sha512-1nDXBNAojfi3oSFwJswKREkFm5wrSjqay81QlyRv2pkITG/XYB5v+oChENVBQLcxQwX4IUATWvXM5BcaNhPiIg==
+
+"@excalidraw/[email protected]":
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/@excalidraw/mermaid-to-excalidraw/-/mermaid-to-excalidraw-0.1.2.tgz#be7b412536fc00b7986ccdccba8e7c33592aa004"
+  integrity sha512-LFk+cLGhXlvRTaf0f6ClCFIZFRsbZPb1ke2cytr5/JlnOefnXQQHgWITafskjcIO2c34KXFGO0HjgYPNFLUknw==
+  dependencies:
+    "@excalidraw/markdown-to-text" "0.1.2"
+    mermaid "10.2.3"
+    nanoid "4.0.2"
+
 "@excalidraw/[email protected]":
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/@excalidraw/prettier-config/-/prettier-config-1.0.2.tgz#b7c061c99cee2f78b9ca470ea1fbd602683bba65"
@@ -2556,6 +2575,13 @@
   resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.0.tgz#23509ebc1fa32f1b4d50d6a66c4032d5b8eaabdc"
   integrity sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==
 
+"@types/debug@^4.0.0":
+  version "4.1.8"
+  resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.8.tgz#cef723a5d0a90990313faec2d1e22aee5eecb317"
+  integrity sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==
+  dependencies:
+    "@types/ms" "*"
+
 "@types/[email protected]":
   version "0.0.39"
   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
@@ -2628,6 +2654,18 @@
   resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
   integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==
 
+"@types/mdast@^3.0.0":
+  version "3.0.12"
+  resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.12.tgz#beeb511b977c875a5b0cc92eab6fcac2f0895514"
+  integrity sha512-DT+iNIRNX884cx0/Q1ja7NyUPpZuv0KPyL5rGNxm1WC1OtHstl7n4Jb7nk+xacNShQMbczJjt8uFzznpp6kYBg==
+  dependencies:
+    "@types/unist" "^2"
+
+"@types/ms@*":
+  version "0.7.31"
+  resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
+  integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
+
 "@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0":
   version "18.15.11"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f"
@@ -2738,6 +2776,11 @@
   resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311"
   integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==
 
+"@types/unist@^2", "@types/unist@^2.0.0":
+  version "2.0.7"
+  resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.7.tgz#5b06ad6894b236a1d2bd6b2f07850ca5c59cf4d6"
+  integrity sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==
+
 "@types/yargs-parser@*":
   version "21.0.0"
   resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
@@ -3431,6 +3474,11 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
+character-entities@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22"
+  integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==
+
 check-error@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
@@ -3534,6 +3582,11 @@ combined-stream@^1.0.8:
   dependencies:
     delayed-stream "~1.0.0"
 
+commander@7:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
 commander@^2.20.0:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@@ -3601,6 +3654,20 @@ corser@^2.0.1:
   resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87"
   integrity sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==
 
+cose-base@^1.0.0:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-1.0.3.tgz#650334b41b869578a543358b80cda7e0abe0a60a"
+  integrity sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==
+  dependencies:
+    layout-base "^1.0.0"
+
+cose-base@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-2.2.0.tgz#1c395c35b6e10bb83f9769ca8b817d614add5c01"
+  integrity sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==
+  dependencies:
+    layout-base "^2.0.0"
+
 cosmiconfig@^7.0.0, cosmiconfig@^7.0.1:
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6"
@@ -3669,6 +3736,280 @@ csstype@^3.0.2:
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
   integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
 
+cytoscape-cose-bilkent@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz#762fa121df9930ffeb51a495d87917c570ac209b"
+  integrity sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==
+  dependencies:
+    cose-base "^1.0.0"
+
+cytoscape-fcose@^2.1.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz#e4d6f6490df4fab58ae9cea9e5c3ab8d7472f471"
+  integrity sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==
+  dependencies:
+    cose-base "^2.2.0"
+
+cytoscape@^3.23.0:
+  version "3.26.0"
+  resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.26.0.tgz#b4c6961445fd51e1fd3cca83c3ffe924d9a8abc9"
+  integrity sha512-IV+crL+KBcrCnVVUCZW+zRRRFUZQcrtdOPXki+o4CFUWLdAEYvuZLcBSJC9EBK++suamERKzeY7roq2hdovV3w==
+  dependencies:
+    heap "^0.2.6"
+    lodash "^4.17.21"
+
+"d3-array@2 - 3", "[email protected] - 3", "[email protected] - 3", d3-array@3, d3-array@^3.2.0:
+  version "3.2.4"
+  resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
+  integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
+  dependencies:
+    internmap "1 - 2"
+
+d3-axis@3:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322"
+  integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==
+
+d3-brush@3:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c"
+  integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-drag "2 - 3"
+    d3-interpolate "1 - 3"
+    d3-selection "3"
+    d3-transition "3"
+
+d3-chord@3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966"
+  integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==
+  dependencies:
+    d3-path "1 - 3"
+
+"d3-color@1 - 3", d3-color@3:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
+  integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
+
+d3-contour@4:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-4.0.2.tgz#bb92063bc8c5663acb2422f99c73cbb6c6ae3bcc"
+  integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==
+  dependencies:
+    d3-array "^3.2.0"
+
+d3-delaunay@6:
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b"
+  integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==
+  dependencies:
+    delaunator "5"
+
+"d3-dispatch@1 - 3", d3-dispatch@3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
+  integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
+
+"d3-drag@2 - 3", d3-drag@3:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
+  integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-selection "3"
+
+"d3-dsv@1 - 3", d3-dsv@3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73"
+  integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==
+  dependencies:
+    commander "7"
+    iconv-lite "0.6"
+    rw "1"
+
+"d3-ease@1 - 3", d3-ease@3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
+  integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
+
+d3-fetch@3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22"
+  integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==
+  dependencies:
+    d3-dsv "1 - 3"
+
+d3-force@3:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4"
+  integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-quadtree "1 - 3"
+    d3-timer "1 - 3"
+
+"d3-format@1 - 3", d3-format@3:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
+  integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
+
+d3-geo@3:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.0.tgz#74fd54e1f4cebd5185ac2039217a98d39b0a4c0e"
+  integrity sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==
+  dependencies:
+    d3-array "2.5.0 - 3"
+
+d3-hierarchy@3:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6"
+  integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
+
+"d3-interpolate@1 - 3", "[email protected] - 3", d3-interpolate@3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
+  integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
+  dependencies:
+    d3-color "1 - 3"
+
+"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
+  integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
+
+d3-polygon@3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398"
+  integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==
+
+"d3-quadtree@1 - 3", d3-quadtree@3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f"
+  integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==
+
+d3-random@3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4"
+  integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==
+
+d3-scale-chromatic@3:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a"
+  integrity sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==
+  dependencies:
+    d3-color "1 - 3"
+    d3-interpolate "1 - 3"
+
+d3-scale@4:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
+  integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
+  dependencies:
+    d3-array "2.10.0 - 3"
+    d3-format "1 - 3"
+    d3-interpolate "1.2.0 - 3"
+    d3-time "2.1.1 - 3"
+    d3-time-format "2 - 4"
+
+"d3-selection@2 - 3", d3-selection@3:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
+  integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
+
+d3-shape@3:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
+  integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
+  dependencies:
+    d3-path "^3.1.0"
+
+"d3-time-format@2 - 4", d3-time-format@4:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
+  integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
+  dependencies:
+    d3-time "1 - 3"
+
+"d3-time@1 - 3", "[email protected] - 3", d3-time@3:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
+  integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
+  dependencies:
+    d3-array "2 - 3"
+
+"d3-timer@1 - 3", d3-timer@3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
+  integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
+
+"d3-transition@2 - 3", d3-transition@3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
+  integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
+  dependencies:
+    d3-color "1 - 3"
+    d3-dispatch "1 - 3"
+    d3-ease "1 - 3"
+    d3-interpolate "1 - 3"
+    d3-timer "1 - 3"
+
+d3-zoom@3:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3"
+  integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-drag "2 - 3"
+    d3-interpolate "1 - 3"
+    d3-selection "2 - 3"
+    d3-transition "2 - 3"
+
+d3@^7.4.0, d3@^7.8.2:
+  version "7.8.5"
+  resolved "https://registry.yarnpkg.com/d3/-/d3-7.8.5.tgz#fde4b760d4486cdb6f0cc8e2cbff318af844635c"
+  integrity sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==
+  dependencies:
+    d3-array "3"
+    d3-axis "3"
+    d3-brush "3"
+    d3-chord "3"
+    d3-color "3"
+    d3-contour "4"
+    d3-delaunay "6"
+    d3-dispatch "3"
+    d3-drag "3"
+    d3-dsv "3"
+    d3-ease "3"
+    d3-fetch "3"
+    d3-force "3"
+    d3-format "3"
+    d3-geo "3"
+    d3-hierarchy "3"
+    d3-interpolate "3"
+    d3-path "3"
+    d3-polygon "3"
+    d3-quadtree "3"
+    d3-random "3"
+    d3-scale "4"
+    d3-scale-chromatic "3"
+    d3-selection "3"
+    d3-shape "3"
+    d3-time "3"
+    d3-time-format "4"
+    d3-timer "3"
+    d3-transition "3"
+    d3-zoom "3"
+
[email protected]:
+  version "7.0.10"
+  resolved "https://registry.yarnpkg.com/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz#19800d4be674379a3cd8c86a8216a2ac6827cadc"
+  integrity sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==
+  dependencies:
+    d3 "^7.8.2"
+    lodash-es "^4.17.21"
+
 damerau-levenshtein@^1.0.8:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
@@ -3683,7 +4024,12 @@ data-urls@^4.0.0:
     whatwg-mimetype "^3.0.0"
     whatwg-url "^12.0.0"
 
-debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.3, debug@^4.3.4:
+dayjs@^1.11.7:
+  version "1.11.9"
+  resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a"
+  integrity sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==
+
+debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.3, debug@^4.3.4:
   version "4.3.4"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
   integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -3709,6 +4055,13 @@ decimal.js@^10.4.3:
   resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
   integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==
 
+decode-named-character-reference@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e"
+  integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==
+  dependencies:
+    character-entities "^2.0.0"
+
 decode-uri-component@^0.2.0:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9"
@@ -3769,11 +4122,23 @@ define-properties@^1.1.3, define-properties@^1.1.4:
     has-property-descriptors "^1.0.0"
     object-keys "^1.1.1"
 
+delaunator@5:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.0.tgz#60f052b28bd91c9b4566850ebf7756efe821d81b"
+  integrity sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==
+  dependencies:
+    robust-predicates "^3.0.0"
+
 delayed-stream@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
   integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
 
+dequal@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
+  integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
+
 detect-node-es@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
@@ -3789,6 +4154,11 @@ diff-sequences@^29.4.3:
   resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2"
   integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==
 
+diff@^5.0.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
+  integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==
+
 dir-glob@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@@ -3834,6 +4204,11 @@ domexception@^4.0.0:
   dependencies:
     webidl-conversions "^7.0.0"
 
[email protected]:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.3.tgz#4b115d15a091ddc96f232bcef668550a2f6f1430"
+  integrity sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ==
+
 [email protected]:
   version "16.0.1"
   resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d"
@@ -3856,6 +4231,11 @@ electron-to-chromium@^1.4.284:
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.359.tgz#5c4d13cb08032469fcd6bd36457915caa211356b"
   integrity sha512-OoVcngKCIuNXtZnsYoqlCvr0Cf3NIPzDIgwUfI9bdTFjXCrr79lI0kwQstLPZ7WhCezLlGksZk/BFAzoXC7GDw==
 
+elkjs@^0.8.2:
+  version "0.8.2"
+  resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e"
+  integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==
+
 emoji-regex@^8.0.0:
   version "8.0.0"
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
@@ -4752,6 +5132,11 @@ he@^1.2.0:
   resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
   integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
 
+heap@^0.2.6:
+  version "0.2.7"
+  resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.7.tgz#1e6adf711d3f27ce35a81fe3b7bd576c2260a8fc"
+  integrity sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==
+
 html-encoding-sniffer@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9"
@@ -4831,7 +5216,7 @@ [email protected]:
   dependencies:
     "@babel/runtime" "^7.14.6"
 
[email protected]:
+[email protected], [email protected]:
   version "0.6.3"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
   integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
@@ -4927,6 +5312,11 @@ internal-slot@^1.0.3, internal-slot@^1.0.4, internal-slot@^1.0.5:
     has "^1.0.3"
     side-channel "^1.0.4"
 
+"internmap@1 - 2":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
+  integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
+
 invariant@^2.2.4:
   version "2.2.4"
   resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
@@ -5399,6 +5789,16 @@ jsonpointer@^5.0.0:
     array-includes "^3.1.5"
     object.assign "^4.1.3"
 
+khroma@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/khroma/-/khroma-2.0.0.tgz#7577de98aed9f36c7a474c4d453d94c0d6c6588b"
+  integrity sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==
+
+kleur@^4.0.3:
+  version "4.1.5"
+  resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
+  integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
+
 language-subtag-registry@~0.3.2:
   version "0.3.22"
   resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d"
@@ -5411,6 +5811,16 @@ language-tags@=1.0.5:
   dependencies:
     language-subtag-registry "~0.3.2"
 
+layout-base@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-1.0.2.tgz#1291e296883c322a9dd4c5dd82063721b53e26e2"
+  integrity sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==
+
+layout-base@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-2.0.1.tgz#d0337913586c90f9c2c075292069f5c2da5dd285"
+  integrity sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==
+
 leven@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
@@ -5487,6 +5897,11 @@ localforage@^1.8.1:
   dependencies:
     lie "3.1.1"
 
+lodash-es@^4.17.21:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
+  integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+
 lodash.camelcase@^4.3.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
@@ -5608,6 +6023,31 @@ make-dir@^4.0.0:
   dependencies:
     semver "^7.5.3"
 
+mdast-util-from-markdown@^1.3.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0"
+  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"
+
+mdast-util-to-string@^3.1.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz#66f7bb6324756741c5f47a53557f0cbf16b6f789"
+  integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==
+  dependencies:
+    "@types/mdast" "^3.0.0"
+
 merge-stream@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@@ -5618,6 +6058,223 @@ merge2@^1.3.0, merge2@^1.4.1:
   resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
   integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
 
[email protected]:
+  version "10.2.3"
+  resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.2.3.tgz#789d3b582c5da8c69aa4a7c0e2b826562c8c8b12"
+  integrity sha512-cMVE5s9PlQvOwfORkyVpr5beMsLdInrycAosdr+tpZ0WFjG4RJ/bUHST7aTgHNJbujHkdBRAm+N50P3puQOfPw==
+  dependencies:
+    "@braintree/sanitize-url" "^6.0.2"
+    cytoscape "^3.23.0"
+    cytoscape-cose-bilkent "^4.1.0"
+    cytoscape-fcose "^2.1.0"
+    d3 "^7.4.0"
+    dagre-d3-es "7.0.10"
+    dayjs "^1.11.7"
+    dompurify "3.0.3"
+    elkjs "^0.8.2"
+    khroma "^2.0.0"
+    lodash-es "^4.17.21"
+    mdast-util-from-markdown "^1.3.0"
+    non-layered-tidy-tree-layout "^2.0.2"
+    stylis "^4.1.3"
+    ts-dedent "^2.2.0"
+    uuid "^9.0.0"
+    web-worker "^1.2.0"
+
+micromark-core-commonmark@^1.0.1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8"
+  integrity sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==
+  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"
+
+micromark-factory-destination@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz#eb815957d83e6d44479b3df640f010edad667b9f"
+  integrity sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==
+  dependencies:
+    micromark-util-character "^1.0.0"
+    micromark-util-symbol "^1.0.0"
+    micromark-util-types "^1.0.0"
+
+micromark-factory-label@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz#cc95d5478269085cfa2a7282b3de26eb2e2dec68"
+  integrity sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==
+  dependencies:
+    micromark-util-character "^1.0.0"
+    micromark-util-symbol "^1.0.0"
+    micromark-util-types "^1.0.0"
+    uvu "^0.5.0"
+
+micromark-factory-space@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf"
+  integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==
+  dependencies:
+    micromark-util-character "^1.0.0"
+    micromark-util-types "^1.0.0"
+
+micromark-factory-title@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz#dd0fe951d7a0ac71bdc5ee13e5d1465ad7f50ea1"
+  integrity sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==
+  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"
+
+micromark-factory-whitespace@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz#798fb7489f4c8abafa7ca77eed6b5745853c9705"
+  integrity sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==
+  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"
+
+micromark-util-character@^1.0.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc"
+  integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==
+  dependencies:
+    micromark-util-symbol "^1.0.0"
+    micromark-util-types "^1.0.0"
+
+micromark-util-chunked@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz#37a24d33333c8c69a74ba12a14651fd9ea8a368b"
+  integrity sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==
+  dependencies:
+    micromark-util-symbol "^1.0.0"
+
+micromark-util-classify-character@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz#6a7f8c8838e8a120c8e3c4f2ae97a2bff9190e9d"
+  integrity sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==
+  dependencies:
+    micromark-util-character "^1.0.0"
+    micromark-util-symbol "^1.0.0"
+    micromark-util-types "^1.0.0"
+
+micromark-util-combine-extensions@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz#192e2b3d6567660a85f735e54d8ea6e3952dbe84"
+  integrity sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==
+  dependencies:
+    micromark-util-chunked "^1.0.0"
+    micromark-util-types "^1.0.0"
+
+micromark-util-decode-numeric-character-reference@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz#b1e6e17009b1f20bc652a521309c5f22c85eb1c6"
+  integrity sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==
+  dependencies:
+    micromark-util-symbol "^1.0.0"
+
+micromark-util-decode-string@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz#dc12b078cba7a3ff690d0203f95b5d5537f2809c"
+  integrity sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==
+  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"
+
+micromark-util-encode@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz#92e4f565fd4ccb19e0dcae1afab9a173bbeb19a5"
+  integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==
+
+micromark-util-html-tag-name@^1.0.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz#48fd7a25826f29d2f71479d3b4e83e94829b3588"
+  integrity sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==
+
+micromark-util-normalize-identifier@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz#7a73f824eb9f10d442b4d7f120fecb9b38ebf8b7"
+  integrity sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==
+  dependencies:
+    micromark-util-symbol "^1.0.0"
+
+micromark-util-resolve-all@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz#4652a591ee8c8fa06714c9b54cd6c8e693671188"
+  integrity sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==
+  dependencies:
+    micromark-util-types "^1.0.0"
+
+micromark-util-sanitize-uri@^1.0.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz#613f738e4400c6eedbc53590c67b197e30d7f90d"
+  integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==
+  dependencies:
+    micromark-util-character "^1.0.0"
+    micromark-util-encode "^1.0.0"
+    micromark-util-symbol "^1.0.0"
+
+micromark-util-subtokenize@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz#941c74f93a93eaf687b9054aeb94642b0e92edb1"
+  integrity sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==
+  dependencies:
+    micromark-util-chunked "^1.0.0"
+    micromark-util-symbol "^1.0.0"
+    micromark-util-types "^1.0.0"
+    uvu "^0.5.0"
+
+micromark-util-symbol@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142"
+  integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==
+
+micromark-util-types@^1.0.0, micromark-util-types@^1.0.1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283"
+  integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==
+
+micromark@^3.0.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.2.0.tgz#1af9fef3f995ea1ea4ac9c7e2f19c48fd5c006e9"
+  integrity sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==
+  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"
+
 micromatch@^4.0.4:
   version "4.0.5"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
@@ -5696,6 +6353,11 @@ moo-color@^1.0.2:
   dependencies:
     color-name "^1.1.4"
 
+mri@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
+  integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
+
 mrmime@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27"
@@ -5729,6 +6391,11 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25"
   integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==
 
[email protected]:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-4.0.2.tgz#140b3c5003959adbebf521c170f282c5e7f9fb9e"
+  integrity sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==
+
 nanoid@^3.3.6:
   version "3.3.6"
   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
@@ -5754,6 +6421,11 @@ node-releases@^2.0.8:
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f"
   integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==
 
+non-layered-tidy-tree-layout@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz#57d35d13c356643fc296a55fb11ac15e74da7804"
+  integrity sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==
+
 normalize-path@^3.0.0, normalize-path@~3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
@@ -6428,6 +7100,11 @@ rimraf@^3.0.2:
   dependencies:
     glob "^7.1.3"
 
+robust-predicates@^3.0.0:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771"
+  integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==
+
 rollup-plugin-terser@^7.0.0:
   version "7.0.2"
   resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d"
@@ -6481,6 +7158,11 @@ run-parallel@^1.1.9:
   dependencies:
     queue-microtask "^1.2.2"
 
+rw@1:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
+  integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==
+
 rxjs@^7.5.5:
   version "7.8.0"
   resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4"
@@ -6488,6 +7170,13 @@ rxjs@^7.5.5:
   dependencies:
     tslib "^2.1.0"
 
+sade@^1.7.3:
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701"
+  integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==
+  dependencies:
+    mri "^1.1.0"
+
 safari-14-idb-fix@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz#450fc049b996ec7f3fd9ca2f89d32e0761583440"
@@ -6866,6 +7555,11 @@ strip-literal@^1.0.1:
   dependencies:
     acorn "^8.8.2"
 
+stylis@^4.1.3:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.0.tgz#abe305a669fc3d8777e10eefcfc73ad861c5588c"
+  integrity sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==
+
 supports-color@^5.3.0:
   version "5.5.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@@ -7028,6 +7722,11 @@ tr46@^4.1.1:
   dependencies:
     punycode "^2.3.0"
 
+ts-dedent@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5"
+  integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==
+
 tsconfig-paths@^3.14.1:
   version "3.14.2"
   resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088"
@@ -7169,6 +7868,13 @@ unique-string@^2.0.0:
   dependencies:
     crypto-random-string "^2.0.0"
 
+unist-util-stringify-position@^3.0.0:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d"
+  integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==
+  dependencies:
+    "@types/unist" "^2.0.0"
+
 universalify@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
@@ -7237,6 +7943,21 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
   integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
 
+uuid@^9.0.0:
+  version "9.0.0"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
+  integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
+
+uvu@^0.5.0:
+  version "0.5.6"
+  resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df"
+  integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==
+  dependencies:
+    dequal "^2.0.0"
+    diff "^5.0.0"
+    kleur "^4.0.3"
+    sade "^1.7.3"
+
 v8-compile-cache@^2.0.3:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
@@ -7422,6 +8143,11 @@ w3c-xmlserializer@^4.0.0:
   dependencies:
     xml-name-validator "^4.0.0"
 
+web-worker@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.2.0.tgz#5d85a04a7fbc1e7db58f66595d7a3ac7c9c180da"
+  integrity sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==
+
 webidl-conversions@^4.0.2:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"