diff --git a/.env.development b/.env.development index d67b137d1..bee5d8944 100644 --- a/.env.development +++ b/.env.development @@ -10,6 +10,9 @@ VITE_APP_WS_SERVER_URL=http://localhost:3002 # set this only if using the collaboration workflow we use on excalidraw.com VITE_APP_PORTAL_URL= +VITE_APP_PLUS_LP=https://plus.excalidraw.com +VITE_APP_PLUS_APP=https://app.excalidraw.com + VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}' # put these in your .env.local, or make sure you don't commit! diff --git a/.env.production b/.env.production index b0570f2a0..19df4b96e 100644 --- a/.env.production +++ b/.env.production @@ -5,11 +5,14 @@ VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries VITE_APP_PORTAL_URL=https://portal.excalidraw.com + +VITE_APP_PLUS_LP=https://plus.excalidraw.com +VITE_APP_PLUS_APP=https://app.excalidraw.com + # Fill to set socket server URL used for collaboration. # Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow VITE_APP_WS_SERVER_URL= VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}' -VITE_APP_PLUS_APP=https://app.excalidraw.com VITE_APP_DISABLE_TRACKING= diff --git a/.github/assets/logo.png b/.github/assets/logo.png deleted file mode 100644 index d9b8953eb..000000000 Binary files a/.github/assets/logo.png and /dev/null differ diff --git a/README.md b/README.md index 48529165e..3a3d1c4d6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ - - Excalidraw + + Excalidraw diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/props/render-props.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/props/render-props.mdx index 2cec0daef..5256704c2 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/props/render-props.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/api/props/render-props.mdx @@ -80,7 +80,7 @@ The `` component takes these props (all are optional except `children`) | --- | --- | --- | | `children` | `React.ReactNode` | Content you want to render inside the `sidebar`. | | `onClose` | `function` | Invoked when the component is closed (by user, or the editor). No need to act on this event, as the editor manages the sidebar open state on its own. | -| `onDock` | `function` | Invoked when the user toggles the `dock` button. The callback recieves a `boolean` parameter `isDocked` which indicates whether the sidebar is `docked` | +| `onDock` | `function` | Invoked when the user toggles the `dock` button. The callback receives a `boolean` parameter `isDocked` which indicates whether the sidebar is `docked` | | `docked` | `boolean` | Indicates whether the sidebar is`docked`. By default, the sidebar is `undocked`. If passed, the docking becomes controlled, and you are responsible for updating the `docked` state by listening on `onDock` callback. To decide the breakpoint for docking you can use [UIOptions.dockedSidebarBreakpoint](/docs/@excalidraw/excalidraw/api/props/ui-options#dockedsidebarbreakpoint) for more info on docking. | | `dockable` | `boolean` | Indicates whether to show the `dock` button so that user can `dock` the sidebar. If `false`, you can still dock programmatically by passing `docked` as `true`. | diff --git a/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/api.mdx b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/api.mdx new file mode 100644 index 000000000..a88bda5ef --- /dev/null +++ b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/api.mdx @@ -0,0 +1,155 @@ +# API + +At the moment the mermaid-to-excalidraw works in two steps. First, you call `parseMermaidToExcalidraw(mermaidSyntax)` on the mermaid diagram definition string, which resolves with elements in a skeleton format — a simplified excalidraw JSON format (docs coming soon). You then pass them to `convertToExcalidrawElements(elements)` to get the fully qualified excalidraw elements you can render in the editor. + +The need for these two steps is due to the [@excalidraw/excalidraw](/docs/@excalidraw/excalidraw/installation) being a **UMD** build so we currently cannot import the `convertToExcalidrawElements()` util alone, until we support a tree-shakeable **ESM** build. + +## parseMermaidToExcalidraw + +This API receives the mermaid syntax as the input, and resolves to skeleton Excalidraw elements. You will need to call `convertToExcalidraw` API to convert them to fully qualified elements that you can render in Excalidraw. + +** Example ** + +```ts +import { parseMermaidToExcalidraw } from "@excalidraw/mermaid-to-excalidraw"; +import { convertToExcalidrawElements} from "@excalidraw/excalidraw" +try { + const { elements, files } = await parseMermaid(mermaidSyntax: string, { + fontSize: number, + }); + const excalidrawElements = convertToExcalidrawElements(elements); + // Render elements and files on Excalidraw +} catch (e) { + // Parse error, displaying error message to users +} +``` + +## Supported Diagram Types + +Currently only [flowcharts](https://mermaid.js.org/syntax/flowchart.html) are supported. Oother diagram types will be rendered as an image in Excalidraw. + +### Flowchart + +#### Excalidraw Regular Shapes + +**Rectangles**, **Circle**, **Diamond**, and **Arrows** are fully supported in Excalidraw + +``` +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] + ``` + + + + +``` +flowchart LR + id1((Hello from Circle)) +``` + + + + +#### Subgraphs + +Subgraphs are grouped diagrams hence they are also supported in Excalidraw + +``` +flowchart TB + c1-->a2 + subgraph one + a1-->a2 + end + subgraph two + b1-->b2 + end + subgraph three + c1-->c2 + end +``` + + + +#### Unsupported shapes fallback to Rectangle + +**Subroutine**, **Cylindrical**, **Asymmetric**, **Hexagon**, **Parallelogram**, **Trapezoid** are not supported in Excalidraw hence they fallback to the closest shape **Rectangle** + +``` +flowchart LR + id1[[Subroutine fallback to Rectangle]] + id2[(Cylindrical fallback to Rectangle)] + id3>Asymmetric fallback to Rectangle] + id4{{Hexagon fallback to Rectangle}} + id5[/Parallelogram fallback to Rectangle /] + id6[/Trapezoid fallback to Rectangle\] +``` +The above shapes are not supported in Excalidraw hence they fallback to Rectangle + + + +#### Markdown fallback to Regular text + +Since we don't support wysiwyg text editor yet, hence [Markdown Strings](https://mermaid.js.org/syntax/flowchart.html#markdown-strings) will fallback to regular text. + +``` +flowchart LR + A("`Hello **World**`") --> B("`Whats **up** ?`") +``` + + +#### Basic FontAwesome fallback to text + +The [FontAwesome](https://mermaid.js.org/syntax/flowchart.html#basic-support-for-fontawesome) icons aren't support so they won't be rendered in Excalidraw + +``` +flowchart TD + B["fab:fa-twitter for peace"] + B-->C[fa:fa-ban forbidden] + B-->E(A fa:fa-camera-retro perhaps?) +``` + + + + +#### Cross Arrow head fallback to Bar Arrow head + +``` +flowchart LR + Start x--x Stop +``` + + + +## Unsupported Diagram Types + +Currently only [flowcharts](https://mermaid.js.org/syntax/flowchart.html) are supported. All other diagram types will be rendered as an image in Excalidraw. + +``` +erDiagram + CUSTOMER ||--o{ ORDER : places + ORDER ||--|{ LINE-ITEM : contains + CUSTOMER }|..|{ DELIVERY-ADDRESS : uses +``` + + + +``` +gitGraph + commit + commit + branch develop + checkout develop + commit + commit + checkout main + merge develop + commit + commit + +``` + + diff --git a/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/codebase.mdx b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/codebase.mdx new file mode 100644 index 000000000..02a48ee54 --- /dev/null +++ b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/codebase.mdx @@ -0,0 +1,7 @@ +# Codebase + +The Codebase is divided into 2 Sections + +* [How Parser Works under the hood](/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser) - If you are interested in understanding and deep diving into inner workings of the Parser, then make sure to checkout this section. + +* [Adding a new diagram type](/docs/@excalidraw/mermaid-to-excalidraw/codebase/new-diagram-type) - If you want to help us make the mermaid to Excalidraw Parser more powerful, you will find all information in this section to do so. diff --git a/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/new-diagram-type.mdx b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/new-diagram-type.mdx new file mode 100644 index 000000000..c59dfaba1 --- /dev/null +++ b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/new-diagram-type.mdx @@ -0,0 +1,54 @@ +# Adding a new Diagram Type + +To add a new diagram type to the parser you can follow the below steps :point_down: + +All the code for the parser resides in [`src`](https://github.com/excalidraw/mermaid-to-excalidraw/tree/master/src) folder and for playground resides in [`playground`](https://github.com/excalidraw/mermaid-to-excalidraw/tree/master/playground) folder. + +lets run the playground server in local :point_down: + +```bash +yarn start +``` + +This will start the playground server in port `1234` and open it in browser so you start playing with the playground. + +## Update Supported Diagram Types + +First step is to add the new diagram type in [`SUPPORTED_DIAGRAM_TYPES`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/constants.ts#L2). + +Once this is done the new diagram type won't be rendered as an image but start throwing error since we don't support parsing the data yet. + +## Writing the Diagram Parser + +Once the diagram type is added in previous step. Next step is to write the parser to parse the mermaid diagram. + +For this create a file named `{{diagramType}}.ts` in [`src/parser`](https://github.com/excalidraw/mermaid-to-excalidraw/tree/master/src/parser) and write a function `parseMermaid{{diagramType}}Diagram` similar to how we [`parseMermaidFlowChartDiagram`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/parser/flowchart.ts#L256) for parsing flowchart diagram. + +The main aim of the parser is :point_down: + +1. Determine how elements are connected in the diagram and thus finding arrow and text bindings. + +For this you might have to dig in to the parser `diagram.parser.yy` and which attributes to parse for the new diagram. + +2. Determine the position and dimensions of each element, for this would be using the `svg` + +Once the parser is ready, lets start using it. + +Add the diagram type in switch case in [`parseMermaid`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/parseMermaid.ts#L97) and call the parser for the same. + +## Writing the Excalidraw Skeleton Convertor + +With the completion of previous step, we have all the data, now we need to transform it so to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133) format. + +Similar to [`FlowChartToExcalidrawSkeletonConverter`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/converter/types/flowchart.ts#L24), you have to write the `{{diagramType}}ToExcalidrawSkeletonConverter` which parses the data received in previous step and returns the [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133). + +Thats it, you have added the new diagram type 🥳, now lets test it out! + +## Updating the Playground + +1. Create a file named `{{diagramType}}.ts` in [`playround/testcases/`](https://github.com/excalidraw/mermaid-to-excalidraw/tree/master/playground/testcases). For reference you can check [`flowchart.ts`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/playground/testcases/flowchart.ts). + +2. Incase the new diagram type added is present in [`unsupported.ts`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/playground/testcases/unsupported.ts) then remove it from there. + +3. Verify if the test cases are running fine in playground. + diff --git a/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/flowchart.mdx b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/flowchart.mdx new file mode 100644 index 000000000..b8d122df3 --- /dev/null +++ b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/flowchart.mdx @@ -0,0 +1,177 @@ +# Flowchart Parser + +As mentioned in the previous section, we use [getDiagramFromText](https://github.com/mermaid-js/mermaid/blob/00d06c7282a701849793680c1e97da1cfdfcce62/packages/mermaid/src/Diagram.ts#L80) to parse the full defination and get the [Diagram](https://github.com/mermaid-js/mermaid/blob/00d06c7282a701849793680c1e97da1cfdfcce62/packages/mermaid/src/Diagram.ts#L15) json from it. + +In this section we will be diving into how the [flowchart parser](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/parser/flowchart.ts#L256) works behind the scenes. + +![image](https://github.com/excalidraw/excalidraw/assets/11256141/2a097bbb-64bf-49d6-bf7f-21172bdb538d) + +We use `diagram.parser.yy` attribute to parse the data. If you want to know more about how the `diagram.parse.yy` attribute looks like, you can check it [here](https://github.com/mermaid-js/mermaid/blob/00d06c7282a701849793680c1e97da1cfdfcce62/packages/mermaid/src/diagrams/flowchart/flowDb.js#L768), however for scope of flowchart we are using **3** APIs from this parser to compute `vertices`, `edges` and `clusters` as we need these data to transform to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133C13-L133C38). + + +For computing `vertices` and `edge`s lets consider the below svg generated by mermaid + +![image](https://github.com/excalidraw/excalidraw/assets/11256141/d7013305-0b90-4fa0-a66e-b4f4604ad0b2) + + +## Computing the vertices + +We use `getVertices` API from `diagram.parse.yy` to get the vertices for a given flowchart. + +Considering the same example this is the response from the API + +```js +{ + "start": { + "id": "start", + "labelType": "text", + "domId": "flowchart-start-1414", + "styles": [], + "classes": [], + "text": "start", + "props": {} + }, + "stop": { + "id": "stop", + "labelType": "text", + "domId": "flowchart-stop-1415", + "styles": [], + "classes": [], + "text": "stop", + "props": {} + } +} +``` +The dimensions and position is missing in this response and we need that to transform to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133C13-L133C38), for this we have our own parser [`parseVertex`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/parseMermaid.ts#L178) which takes the above response and uses the `svg` together to compute position, dimensions and cleans up the response. + + The final output from `parseVertex` looks like :point_down: + +```js +{ + "start": { + "id": "start", + "labelType": "text", + "text": "start", + "x": 0, + "y": 0, + "width": 67.796875, + "height": 44, + "containerStyle": {}, + "labelStyle": {} + }, + "stop": { + "id": "stop", + "labelType": "text", + "text": "stop", + "x": 117.796875, + "y": 0, + "width": 62.3828125, + "height": 44, + "containerStyle": {}, + "labelStyle": {} + } +} +``` + + +## Computing the edges + +The lines and arrows are considered as `edges` in mermaid as shown in the above diagram. +We use `getEdges` API from `diagram.parse.yy` to get the edges for a given flowchart. +Considering the same example this is the response from the API + +```js +[ + { + "start": "start", + "end": "stop", + "type": "arrow_point", + "text": "", + "labelType": "text", + "stroke": "normal", + "length": 1 + } +] +``` + +Similarly here the dimensions and position is missing and we compute that from the svg. The [`parseEdge`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/parseMermaid.ts#L245) takes the above response along with `svg` and computes the position, dimensions and cleans up the response. + + The final output from `parseEdge` looks like :point_down: + +```js +[ + { + "start": "start", + "end": "stop", + "type": "arrow_point", + "text": "", + "labelType": "text", + "stroke": "normal", + "startX": 67.797, + "startY": 22, + "endX": 117.797, + "endY": 22, + "reflectionPoints": [ + { + "x": 67.797, + "y": 22 + }, + { + "x": 117.797, + "y": 22 + } + ] + } +] +``` +## Computing the Subgraphs + +`Subgraphs` is collection of elements grouped together. The Subgraphs map to `grouping` elements in Excalidraw. + +Lets consider the below example :point_down: + +![image](https://github.com/excalidraw/excalidraw/assets/11256141/5243ce4c-beaa-43d2-812a-0577b0a574d7) + +We use `getSubgraphs` API to get the subgraph data for a given flowchart. +Considering the same example this is the response from the API + +```js +[ + { + "id": "one", + "nodes": [ + "flowchart-a2-1399", + "flowchart-a1-1400" + ], + "title": "one", + "classes": [], + "labelType": "text" + } +] +``` + +For position and dimensions we use the svg to compute. The [`parseSubgraph`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/parseMermaid.ts#L139) takes the above response along with `svg` and computes the position, dimensions and cleans up the response. + + +```js +[ + { + "id": "one", + "nodes": [ + "flowchart-a2-1399", + "flowchart-a1-1400" + ], + "title": "one", + "labelType": "text", + "nodeIds": [ + "a2", + "a1" + ], + "x": 75.4921875, + "y": 0, + "width": 121.25, + "height": 188, + "text": "one" + } +] +``` \ No newline at end of file diff --git a/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/parser.mdx b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/parser.mdx new file mode 100644 index 000000000..4f4af4dec --- /dev/null +++ b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/parser.mdx @@ -0,0 +1,65 @@ +# How the Parser works under the hood ? + +[This](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/index.ts) is the entry point of the library. + + +`parseMermaidToExcalidraw` function is the only function exposed which receives mermaid syntax as the input, parses the mermaid syntax and resolves to Excalidraw Skeleton. + +Lets look at the high level overview at how the parse works :point_down: + +![image](https://github.com/excalidraw/excalidraw/assets/11256141/8e060de7-b867-44ad-864b-0c1b24466b67) + +Lets dive deeper into individual section now to understand better. + +## Parsing Mermaid diagram + +One of the dependencies of the library is [`mermaid`](https://www.npmjs.com/package/mermaid) library. +We need the mermaid diagram in some extractable format so we can parse it to Excalidraw Elements. + +Parsing is broken into two steps +1. [`Rendering Mermaid to Svg`](/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser#rendering-mermaid-to-svg) - This helps in determining the position and dimensions of each element in the diagram + +2. [`Parsing the mermaid syntax`](/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser#parsing-the-mermaid-syntax) - We also need to know how elements are connected which isn't possible with svg alone hence we also parse the mermaid syntax which helps in determining the connections and bindings between elements in the diagram. + +### Rendering Mermaid to Svg + +![image](https://github.com/excalidraw/excalidraw/assets/11256141/2c24cf7b-f096-4c12-88db-55520de27558) + +The [`mermaid`](https://www.npmjs.com/package/mermaid) library provides the API `mermaid.render` API which gives the output of the diagram in `svg`. + + +If the diagram isn't supported, this svg is converted to `dataURL` and can be rendered as an image in Excalidraw. + + +### Parsing the mermaid syntax + +For this we first need to process the options along with mermaid defination for diagram provided by the user. + +[`processMermaidTextWithOptions`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/parseMermaid.ts#L13) takes the mermaid defination and options and returns the full defination including the mermaid [directives ](https://mermaid.js.org/config/directives.html). + +![image](https://github.com/excalidraw/excalidraw/assets/11256141/3a4825d8-9704-468c-a02f-7e507f4d5b7a) + +Next we use mermaid api [getDiagramFromText](https://github.com/mermaid-js/mermaid/blob/00d06c7282a701849793680c1e97da1cfdfcce62/packages/mermaid/src/Diagram.ts#L80) to parse the full defination and get the [Diagram](https://github.com/mermaid-js/mermaid/blob/00d06c7282a701849793680c1e97da1cfdfcce62/packages/mermaid/src/Diagram.ts#L15) json from it. + +```js +const diagram = await mermaid.mermaidAPI.getDiagramFromText(fullDefinition); +``` + +Next we write our own parser to parse this diagram. + +### Parsing the Diagram + +For each diagram type, we need a parser as the result of every diagram would differ and dependinng on the data we have to write the parser. Since currently we support flowchart, so here is the [`parseMermaidFlowChartDiagram`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/parser/flowchart.ts#L256) to parse the flowchart diagram. + +If you want to understand how flowchart parser works, you can navigate to [Flowchart Parser](http://localhost:3003/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/flowchart). + +## Converting to ExcalidrawElementSkeleton + +Now we have all the data, we just need to transform it to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133C13-L133C38) API so it can be rendered in Excalidraw. + +For this we have `converters` which takes the parsed mermaid data and gives back the Excalidraw Skeleton. +For Unsupported types, we have already mentioned above that we convert it to `dataURL` and return the ExcalidrawImageSkeleton. + +For supported types, currently only flowchart, we have [flowchartConverter](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/converter/types/flowchart.ts#L24) which parses the data and converts to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133C13-L133C38). + +![image](https://github.com/excalidraw/excalidraw/assets/11256141/00226e9d-043d-4a08-989a-3ad9d2a574f1) \ No newline at end of file diff --git a/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/development.mdx b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/development.mdx new file mode 100644 index 000000000..818465a3c --- /dev/null +++ b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/development.mdx @@ -0,0 +1,60 @@ +# Development + +This page relates to developing the `@excalidraw/mermaid-to-excalidraw` package itself. + +## Setting up in Local + +To set up the library in local, follow the below steps 👇🏼 + +### Clone the Repository + +Go to [@excalidraw/mermaid-to-excalidraw](https://github.com/excalidraw/mermaid-to-excalidraw) and clone the repository to your local. + + +```bash +git clone git@github.com:excalidraw/mermaid-to-excalidraw.git +``` + +### Install the dependencies + +Using `npm` + +```bash +npm install @excalidraw/mermaid-to-excalidraw +``` + +Using `yarn` + +```bash +yarn add @excalidraw/mermaid-to-excalidraw +``` + +### Run the playground server + +```bash +yarn start +``` + +This will start the playground server in port `1234` and you start playing with the playground. + +## Creating a test release + +We will soon simplify creating release via commenting on GitHub PR similar till then you can create a release by following the below steps + +1. Create the build + +```bash +yarn build +``` + +This will create the dist folder which we need to publish next. + +2. Publish the library + +Update the package name and version in [package.json](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/package.json) and run the below command to publish it + +```bash +yarn publish +``` + +And thats all your test release is successfully created 🎉 diff --git a/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/installation.mdx b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/installation.mdx new file mode 100644 index 000000000..1860fdf63 --- /dev/null +++ b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/installation.mdx @@ -0,0 +1,42 @@ +# Installation + +`@excalidraw/mermaid-to-excalidraw` is published to npm. This library is used in [excalidraw](https://excalidraw.com) to transform mermaid syntax to Excalidraw diagrams. + +Using `npm` + +```bash +npm install @excalidraw/mermaid-to-excalidraw +``` + +Using `yarn` + +```bash +yarn add @excalidraw/mermaid-to-excalidraw +``` + +## Usage + +Once the library is installed, its ready to use. + +```js +import { parseMermaidToExcalidraw } from "@excalidraw/mermaid-to-excalidraw"; +import { convertToExcalidrawElements} from "@excalidraw/excalidraw" + +try { + const { elements, files } = await parseMermaid(diagramDefinition, { + fontSize: DEFAULT_FONT_SIZE, + }); + // currently the elements returned from the parser are in a "skeleton" format + // which we need to convert to fully qualified excalidraw elements first + const excalidrawElements = convertToExcalidrawElements(elements); + + // Render elements and files on Excalidraw +} catch (e) { + // Error handling +} +``` + +## Playground + + Try it out [here](https://mermaid-to-excalidraw.vercel.app) + diff --git a/dev-docs/docs/codebase/json-schema.mdx b/dev-docs/docs/codebase/json-schema.mdx new file mode 100644 index 000000000..75916c5f8 --- /dev/null +++ b/dev-docs/docs/codebase/json-schema.mdx @@ -0,0 +1,75 @@ +# JSON Schema + +The Excalidraw data format uses plaintext JSON. + +## Excalidraw files + +When saving an Excalidraw scene locally to a file, the JSON file (`.excalidraw`) is using the below format. + +### Attributes + +| Attribute | Description | Value | +| --- | --- | --- | +| `type` | The type of the Excalidraw schema | `"excalidraw"` | +| `version` | The version of the Excalidraw schema | number | +| `source` | The source URL of the Excalidraw application | `"https://excalidraw.com"` | +| `elements` | An array of objects representing excalidraw elements on canvas | Array containing excalidraw element objects | +| `appState` | Additional application state/configuration | Object containing application state properties | +| `files` | Data for excalidraw `image` elements | Object containing image data | + +### JSON Schema example + +```json +{ + // schema information + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + + // elements on canvas + "elements": [ + // example element + { + "id": "pologsyG-tAraPgiN9xP9b", + "type": "rectangle", + "x": 928, + "y": 319, + "width": 134, + "height": 90 + /* ...other element properties */ + } + /* other elements */ + ], + + // editor state (canvas config, preferences, ...) + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + + // files data for "image" elements, using format `{ [fileId]: fileData }` + "files": { + // example of an image data object + "3cebd7720911620a3938ce77243696149da03861": { + "mimeType": "image/png", + "id": "3cebd7720911620a3938c.77243626149da03861", + "dataURL": "data:image/png;base64,iVBORWOKGgoAAAANSUhEUgA=", + "created": 1690295874454, + "lastRetrieved": 1690295874454 + } + /* ...other image data objects */ + } +} +``` + +## Excalidraw clipboard format + +When copying selected excalidraw elements to clipboard, the JSON schema is similar to `.excalidraw` format, except it differs in attributes. + +### Attributes + +| Attribute | Description | Example Value | +| --- | --- | --- | +| `type` | The type of the Excalidraw document. | "excalidraw/clipboard" | +| `elements` | An array of objects representing excalidraw elements on canvas. | Array containing excalidraw element objects (see example below) | +| `files` | Data for excalidraw `image` elements. | Object containing image data | diff --git a/dev-docs/docs/introduction/get-started.mdx b/dev-docs/docs/introduction/get-started.mdx index f122c9a77..c5d9ab08e 100644 --- a/dev-docs/docs/introduction/get-started.mdx +++ b/dev-docs/docs/introduction/get-started.mdx @@ -14,3 +14,4 @@ These docs are focused on developers, and structured in the following way: - [Introduction](/docs/) — development setup and introduction. - [@excalidraw/excalidraw](/docs/@excalidraw/excalidraw/installation) — docs for the npm package to help you integrate Excalidraw into your own app. - Editor — IN PROGRESS. Docs describing the internals of the Excalidraw editor to help in contributing to the codebase. +- [@excalidraw/mermaid-to-excalidraw](/docs/@excalidraw/mermaid-to-excalidraw/installation) - Docs for the mermaid to excalidraw parser diff --git a/dev-docs/docusaurus.config.js b/dev-docs/docusaurus.config.js index 390c619af..706101f66 100644 --- a/dev-docs/docusaurus.config.js +++ b/dev-docs/docusaurus.config.js @@ -10,7 +10,7 @@ const config = { baseUrl: "/", onBrokenLinks: "throw", onBrokenMarkdownLinks: "warn", - favicon: "img/favicon.ico", + favicon: "img/favicon.png", organizationName: "Excalidraw", // Usually your GitHub org/user name. projectName: "excalidraw", // Usually your repo name. @@ -123,7 +123,7 @@ const config = { prism: { theme: require("prism-react-renderer/themes/dracula"), }, - image: "img/og-image.png", + image: "img/og-image-2.png", docs: { sidebar: { hideable: true, diff --git a/dev-docs/sidebars.js b/dev-docs/sidebars.js index 00879c12f..4ff33746b 100644 --- a/dev-docs/sidebars.js +++ b/dev-docs/sidebars.js @@ -23,7 +23,7 @@ const sidebars = { }, items: ["introduction/development", "introduction/contributing"], }, - + { type: "category", label: "Codebase", items: ["codebase/json-schema"] }, { type: "category", label: "@excalidraw/excalidraw", @@ -92,6 +92,40 @@ const sidebars = { "@excalidraw/excalidraw/development", ], }, + { + type: "category", + label: "@excalidraw/mermaid-to-excalidraw", + link: { + type: "doc", + id: "@excalidraw/mermaid-to-excalidraw/installation", + }, + items: [ + "@excalidraw/mermaid-to-excalidraw/api", + "@excalidraw/mermaid-to-excalidraw/development", + { + type: "category", + label: "Codebase", + link: { + type: "doc", + id: "@excalidraw/mermaid-to-excalidraw/codebase/codebase", + }, + items: [ + { + type: "category", + label: "How Parser works under the hood?", + link: { + type: "doc", + id: "@excalidraw/mermaid-to-excalidraw/codebase/parser/parser", + }, + items: [ + "@excalidraw/mermaid-to-excalidraw/codebase/parser/flowchart", + ], + }, + "@excalidraw/mermaid-to-excalidraw/codebase/new-diagram-type", + ], + }, + ], + }, ], }; diff --git a/dev-docs/src/theme/ReactLiveScope/index.js b/dev-docs/src/theme/ReactLiveScope/index.js index e5263e1db..7464db22f 100644 --- a/dev-docs/src/theme/ReactLiveScope/index.js +++ b/dev-docs/src/theme/ReactLiveScope/index.js @@ -25,6 +25,7 @@ const ExcalidrawScope = { exportToCanvas: ExcalidrawComp.exportToCanvas, initialData, useI18n: ExcalidrawComp.useI18n, + convertToExcalidrawElements: ExcalidrawComp.convertToExcalidrawElements, }; export default ExcalidrawScope; diff --git a/dev-docs/static/img/favicon.ico b/dev-docs/static/img/favicon.ico index 518c8962e..e40a39d8d 100644 Binary files a/dev-docs/static/img/favicon.ico and b/dev-docs/static/img/favicon.ico differ diff --git a/dev-docs/static/img/favicon.png b/dev-docs/static/img/favicon.png new file mode 100644 index 000000000..041545cbd Binary files /dev/null and b/dev-docs/static/img/favicon.png differ diff --git a/dev-docs/static/img/logo.svg b/dev-docs/static/img/logo.svg index 8770e525d..ee996762e 100644 --- a/dev-docs/static/img/logo.svg +++ b/dev-docs/static/img/logo.svg @@ -1,4 +1,7 @@ - - - + + + + + + diff --git a/dev-docs/static/img/og-image-2.png b/dev-docs/static/img/og-image-2.png new file mode 100644 index 000000000..9c341b9e1 Binary files /dev/null and b/dev-docs/static/img/og-image-2.png differ diff --git a/dev-docs/static/img/og-image-sm.png b/dev-docs/static/img/og-image-sm.png deleted file mode 100644 index 5e88dba05..000000000 Binary files a/dev-docs/static/img/og-image-sm.png and /dev/null differ diff --git a/index.html b/index.html index cbdc7706c..b1e0f2abc 100644 --- a/index.html +++ b/index.html @@ -20,7 +20,7 @@ name="description" content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them." /> - + @@ -35,7 +35,7 @@ property="og:description" content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them." /> - + @@ -51,7 +51,7 @@ /> @@ -99,7 +99,9 @@ <% } %> - + + + diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png new file mode 100644 index 000000000..34c7c2fc3 Binary files /dev/null and b/public/android-chrome-192x192.png differ diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png new file mode 100644 index 000000000..24ea4de79 Binary files /dev/null and b/public/android-chrome-512x512.png differ diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index 40af4de2f..bcbbeaf98 100644 Binary files a/public/apple-touch-icon.png and b/public/apple-touch-icon.png differ diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png new file mode 100644 index 000000000..eead57364 Binary files /dev/null and b/public/favicon-16x16.png differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 000000000..041545cbd Binary files /dev/null and b/public/favicon-32x32.png differ diff --git a/public/favicon.ico b/public/favicon.ico index 518c8962e..e40a39d8d 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 000000000..ee996762e --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/logo-180x180.png b/public/logo-180x180.png deleted file mode 100644 index 9aa9b103b..000000000 Binary files a/public/logo-180x180.png and /dev/null differ diff --git a/public/manifest.json b/public/manifest.json index 85dc878f2..64f7e5bac 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -4,8 +4,8 @@ "description": "Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them.", "icons": [ { - "src": "logo-180x180.png", - "sizes": "180x180", + "src": "android-chrome-192x192.png", + "sizes": "192x192", "type": "image/png" }, { diff --git a/public/maskable_icon_x192.png b/public/maskable_icon_x192.png new file mode 100644 index 000000000..321f0bb76 Binary files /dev/null and b/public/maskable_icon_x192.png differ diff --git a/public/maskable_icon_x512.png b/public/maskable_icon_x512.png new file mode 100644 index 000000000..0a620acf7 Binary files /dev/null and b/public/maskable_icon_x512.png differ diff --git a/public/og-fb-v1.png b/public/og-fb-v1.png deleted file mode 100644 index b626ee60a..000000000 Binary files a/public/og-fb-v1.png and /dev/null differ diff --git a/public/og-general-v1.png b/public/og-general-v1.png deleted file mode 100644 index 0a6fbb343..000000000 Binary files a/public/og-general-v1.png and /dev/null differ diff --git a/public/og-image-2.png b/public/og-image-2.png new file mode 100644 index 000000000..4fe1c83cd Binary files /dev/null and b/public/og-image-2.png differ diff --git a/public/og-image-sm.png b/public/og-image-sm.png deleted file mode 100644 index 5e88dba05..000000000 Binary files a/public/og-image-sm.png and /dev/null differ diff --git a/public/og-image.png b/public/og-image.png deleted file mode 100644 index 8f05f7bab..000000000 Binary files a/public/og-image.png and /dev/null differ diff --git a/public/og-twitter-v1.png b/public/og-twitter-v1.png deleted file mode 100644 index e7834c334..000000000 Binary files a/public/og-twitter-v1.png and /dev/null differ diff --git a/src/actions/actionProperties.tsx b/src/actions/actionProperties.tsx index e0d61a271..1a05bc7cb 100644 --- a/src/actions/actionProperties.tsx +++ b/src/actions/actionProperties.tsx @@ -95,7 +95,7 @@ import { register } from "./register"; const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1; -const changeProperty = ( +export const changeProperty = ( elements: readonly ExcalidrawElement[], appState: AppState, callback: (element: ExcalidrawElement) => ExcalidrawElement, @@ -118,7 +118,7 @@ const changeProperty = ( }); }; -const getFormValue = function ( +export const getFormValue = function ( elements: readonly ExcalidrawElement[], appState: AppState, getAttribute: (element: ExcalidrawElement) => T, diff --git a/src/components/App.tsx b/src/components/App.tsx index 321da1d8d..b7d93ffbc 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -6518,7 +6518,7 @@ class App extends React.Component { } nextElements = updateFrameMembershipOfSelectedElements( - this.scene.getElementsIncludingDeleted(), + nextElements, this.state, this, ); diff --git a/src/components/ExcalidrawLogo.scss b/src/components/ExcalidrawLogo.scss new file mode 100644 index 000000000..e59e8a90c --- /dev/null +++ b/src/components/ExcalidrawLogo.scss @@ -0,0 +1,73 @@ +.excalidraw { + .ExcalidrawLogo { + --logo-icon--xs: 2rem; + --logo-text--xs: 1.5rem; + + --logo-icon--small: 2.5rem; + --logo-text--small: 1.75rem; + + --logo-icon--normal: 3rem; + --logo-text--normal: 2.2rem; + + --logo-icon--large: 90px; + --logo-text--large: 65px; + + display: flex; + align-items: center; + + svg { + flex: 0 0 auto; + } + + .ExcalidrawLogo-icon { + width: auto; + color: var(--color-logo-icon); + } + + .ExcalidrawLogo-text { + margin-left: 0.75rem; + width: auto; + color: var(--color-logo-text); + } + + &.is-xs { + .ExcalidrawLogo-icon { + height: var(--logo-icon--xs); + } + + .ExcalidrawLogo-text { + height: var(--logo-text--xs); + } + } + + &.is-small { + .ExcalidrawLogo-icon { + height: var(--logo-icon--small); + } + + .ExcalidrawLogo-text { + height: var(--logo-text--small); + } + } + + &.is-normal { + .ExcalidrawLogo-icon { + height: var(--logo-icon--normal); + } + + .ExcalidrawLogo-text { + height: var(--logo-text--normal); + } + } + + &.is-large { + .ExcalidrawLogo-icon { + height: var(--logo-icon--large); + } + + .ExcalidrawLogo-text { + height: var(--logo-text--large); + } + } + } +} diff --git a/src/components/ExcalidrawLogo.tsx b/src/components/ExcalidrawLogo.tsx new file mode 100644 index 000000000..01d07fc50 --- /dev/null +++ b/src/components/ExcalidrawLogo.tsx @@ -0,0 +1,69 @@ +import "./ExcalidrawLogo.scss"; + +const LogoIcon = () => ( + + + +); + +const LogoText = () => ( + + + + + + +); + +type LogoSize = "xs" | "small" | "normal" | "large" | "custom"; + +interface LogoProps { + size?: LogoSize; + withText?: boolean; + style?: React.CSSProperties; + /** + * If true, the logo will not be wrapped in a Link component. + * The link prop will be ignored as well. + * It will merely be a plain div. + */ + isNotLink?: boolean; +} + +export const ExcalidrawLogo = ({ + style, + size = "small", + withText, +}: LogoProps) => { + return ( +
+ + {withText && } +
+ ); +}; diff --git a/src/components/canvases/StaticCanvas.tsx b/src/components/canvases/StaticCanvas.tsx index f32133f35..dfdf8b51e 100644 --- a/src/components/canvases/StaticCanvas.tsx +++ b/src/components/canvases/StaticCanvas.tsx @@ -37,10 +37,25 @@ const StaticCanvas = (props: StaticCanvasProps) => { canvas.classList.add("excalidraw__canvas", "static"); } - canvas.style.width = `${props.appState.width}px`; - canvas.style.height = `${props.appState.height}px`; - canvas.width = props.appState.width * props.scale; - canvas.height = props.appState.height * props.scale; + const widthString = `${props.appState.width}px`; + const heightString = `${props.appState.height}px`; + if (canvas.style.width !== widthString) { + canvas.style.width = widthString; + } + if (canvas.style.height !== heightString) { + canvas.style.height = heightString; + } + + const scaledWidth = props.appState.width * props.scale; + const scaledHeight = props.appState.height * props.scale; + // setting width/height resets the canvas even if dimensions not changed, + // which would cause flicker when we skip frame (due to throttling) + if (canvas.width !== scaledWidth) { + canvas.width = scaledWidth; + } + if (canvas.height !== scaledHeight) { + canvas.height = scaledHeight; + } renderStaticScene( { diff --git a/src/components/icons.tsx b/src/components/icons.tsx index e7e73d7e7..aefa43b16 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -255,14 +255,12 @@ export const WelcomeScreenTopToolbarArrow = createIcon( // custom export const ExcalLogo = createIcon( - , - { width: 26, height: 62, fill: "none" }, + { width: 40, height: 40, fill: "none" }, ); // custom diff --git a/src/components/welcome-screen/WelcomeScreen.Center.tsx b/src/components/welcome-screen/WelcomeScreen.Center.tsx index 2d43761e6..46a51e3fe 100644 --- a/src/components/welcome-screen/WelcomeScreen.Center.tsx +++ b/src/components/welcome-screen/WelcomeScreen.Center.tsx @@ -3,8 +3,9 @@ import { getShortcutFromShortcutName } from "../../actions/shortcuts"; import { t, useI18n } from "../../i18n"; import { useDevice, useExcalidrawActionManager } from "../App"; import { useTunnels } from "../../context/tunnels"; -import { ExcalLogo, HelpIcon, LoadIcon, usersIcon } from "../icons"; +import { HelpIcon, LoadIcon, usersIcon } from "../icons"; import { useUIAppState } from "../../context/ui-appState"; +import { ExcalidrawLogo } from "../ExcalidrawLogo"; const WelcomeScreenMenuItemContent = ({ icon, @@ -109,7 +110,7 @@ Center.displayName = "Center"; const Logo = ({ children }: { children?: React.ReactNode }) => { return (
- {children || <>{ExcalLogo} Excalidraw} + {children || }
); }; diff --git a/src/components/welcome-screen/WelcomeScreen.scss b/src/components/welcome-screen/WelcomeScreen.scss index f856d4594..a1b88d8bd 100644 --- a/src/components/welcome-screen/WelcomeScreen.scss +++ b/src/components/welcome-screen/WelcomeScreen.scss @@ -10,6 +10,13 @@ pointer-events: none; color: var(--color-gray-40); + + a { + --color: var(--color-primary); + color: var(--color); + text-decoration: none; + margin-bottom: -6px; + } } &.theme--dark { @@ -136,11 +143,6 @@ align-items: center; column-gap: 0.75rem; font-size: 2.25rem; - - svg { - width: 1.625rem; - height: auto; - } } .welcome-screen-center__heading { diff --git a/src/css/theme.scss b/src/css/theme.scss index 643f9346d..1db22313a 100644 --- a/src/css/theme.scss +++ b/src/css/theme.scss @@ -126,6 +126,9 @@ --color-success: #268029; --color-success-lighter: #cafccc; + --color-logo-icon: var(--color-primary); + --color-logo-text: #190064; + --border-radius-md: 0.375rem; --border-radius-lg: 0.5rem; @@ -219,5 +222,7 @@ --color-muted-background-darker: var(--color-gray-20); --color-promo: #d297ff; + + --color-logo-text: #e2dfff; } } diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx index a2301b964..c855de357 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/src/element/textWysiwyg.test.tsx @@ -1509,4 +1509,30 @@ describe("textWysiwyg", () => { expect(text.text).toBe("Excalidraw"); }); }); + + it("should bump the version of labelled arrow when label updated", async () => { + await render(); + const arrow = UI.createElement("arrow", { + width: 300, + height: 0, + }); + + mouse.select(arrow); + Keyboard.keyPress(KEYS.ENTER); + let editor = getTextEditor(); + await new Promise((r) => setTimeout(r, 0)); + updateTextEditor(editor, "Hello"); + editor.blur(); + + const { version } = arrow; + + mouse.select(arrow); + Keyboard.keyPress(KEYS.ENTER); + editor = getTextEditor(); + await new Promise((r) => setTimeout(r, 0)); + updateTextEditor(editor, "Hello\nworld!"); + editor.blur(); + + expect(arrow.version).toEqual(version + 1); + }); }); diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx index b60fdeed2..d7fed6981 100644 --- a/src/element/textWysiwyg.tsx +++ b/src/element/textWysiwyg.tsx @@ -20,7 +20,7 @@ import { ExcalidrawTextContainer, } from "./types"; import { AppState } from "../types"; -import { mutateElement } from "./mutateElement"; +import { bumpVersion, mutateElement } from "./mutateElement"; import { getBoundTextElementId, getContainerElement, @@ -541,6 +541,9 @@ export const textWysiwyg = ({ id: element.id, }), }); + } else if (isArrowElement(container)) { + // updating an arrow label may change bounds, prevent stale cache: + bumpVersion(container); } } else { mutateElement(container, { diff --git a/src/excalidraw-app/components/AppMainMenu.tsx b/src/excalidraw-app/components/AppMainMenu.tsx index 7b562f8b7..6e12d7811 100644 --- a/src/excalidraw-app/components/AppMainMenu.tsx +++ b/src/excalidraw-app/components/AppMainMenu.tsx @@ -26,7 +26,9 @@ export const AppMainMenu: React.FC<{ Excalidraw+ diff --git a/src/excalidraw-app/components/AppWelcomeScreen.tsx b/src/excalidraw-app/components/AppWelcomeScreen.tsx index 2dff7d270..699c3ba88 100644 --- a/src/excalidraw-app/components/AppWelcomeScreen.tsx +++ b/src/excalidraw-app/components/AppWelcomeScreen.tsx @@ -56,7 +56,9 @@ export const AppWelcomeScreen: React.FC<{ )} {!isExcalidrawPlusSignedUser && ( diff --git a/src/excalidraw-app/components/ExportToExcalidrawPlus.tsx b/src/excalidraw-app/components/ExportToExcalidrawPlus.tsx index 8e6342d3a..42b7a7d4f 100644 --- a/src/excalidraw-app/components/ExportToExcalidrawPlus.tsx +++ b/src/excalidraw-app/components/ExportToExcalidrawPlus.tsx @@ -7,7 +7,6 @@ import { FileId, NonDeletedExcalidrawElement } from "../../element/types"; import { AppState, BinaryFileData, BinaryFiles } from "../../types"; import { nanoid } from "nanoid"; import { useI18n } from "../../i18n"; -import { excalidrawPlusIcon } from "./icons"; import { encryptData, generateEncryptionKey } from "../../data/encryption"; import { isInitializedImageElement } from "../../element/typeChecks"; import { FILE_UPLOAD_MAX_BYTES } from "../app_constants"; @@ -15,6 +14,7 @@ import { encodeFilesForUpload } from "../data/FileManager"; import { MIME_TYPES } from "../../constants"; import { trackEvent } from "../../analytics"; import { getFrame } from "../../utils"; +import { ExcalidrawLogo } from "../../components/ExcalidrawLogo"; export const exportToExcalidrawPlus = async ( elements: readonly NonDeletedExcalidrawElement[], @@ -69,7 +69,9 @@ export const exportToExcalidrawPlus = async ( } window.open( - `https://plus.excalidraw.com/import?excalidraw=${id},${encryptionKey}`, + `${ + import.meta.env.VITE_APP_PLUS_APP + }/import?excalidraw=${id},${encryptionKey}`, ); }; @@ -82,7 +84,15 @@ export const ExportToExcalidrawPlus: React.FC<{ const { t } = useI18n(); return ( -
{excalidrawPlusIcon}
+
+ +

Excalidraw+

{t("exportDialog.excalidrawplus_description")} diff --git a/src/excalidraw-app/components/icons.tsx b/src/excalidraw-app/components/icons.tsx deleted file mode 100644 index 5e701ce8a..000000000 --- a/src/excalidraw-app/components/icons.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { createIcon } from "../../components/icons"; - -export const excalidrawPlusIcon = createIcon( - <> - - - - , - { width: 89, height: 131, style: { transform: "translateX(4px)" } }, -); diff --git a/src/frame.test.tsx b/src/frame.test.tsx index e9562a6f3..85417ddf3 100644 --- a/src/frame.test.tsx +++ b/src/frame.test.tsx @@ -1,9 +1,10 @@ +import { ExcalidrawElement } from "./element/types"; import { convertToExcalidrawElements, Excalidraw, } from "./packages/excalidraw/index"; import { API } from "./tests/helpers/api"; -import { Pointer } from "./tests/helpers/ui"; +import { Keyboard, Pointer } from "./tests/helpers/ui"; import { render } from "./tests/test-utils"; const { h } = window; @@ -28,83 +29,301 @@ describe("adding elements to frames", () => { }, []); }; - describe("resizing frame over elements", () => { - const testElements = async ( - containerType: "arrow" | "rectangle", - initialOrder: ElementType[], - expectedOrder: ElementType[], - ) => { - await render(); + function resizeFrameOverElement( + frame: ExcalidrawElement, + element: ExcalidrawElement, + ) { + mouse.clickAt(0, 0); + mouse.downAt(frame.x + frame.width, frame.y + frame.height); + mouse.moveTo( + element.x + element.width + 50, + element.y + element.height + 50, + ); + mouse.up(); + } - const frame = API.createElement({ type: "frame", x: 0, y: 0 }); + function dragElementIntoFrame( + frame: ExcalidrawElement, + element: ExcalidrawElement, + ) { + mouse.clickAt(element.x, element.y); + mouse.downAt(element.x + element.width / 2, element.y + element.height / 2); + mouse.moveTo(frame.x + frame.width / 2, frame.y + frame.height / 2); + mouse.up(); + } - h.elements = reorderElements( - [ - frame, - ...convertToExcalidrawElements([ - { - type: containerType, - x: 100, - y: 100, - height: 10, - label: { text: "xx" }, - }, - ]), - ], - initialOrder, - ); + function selectElementAndDuplicate( + element: ExcalidrawElement, + moveTo: [number, number] = [element.x + 25, element.y + 25], + ) { + const [x, y] = [ + element.x + element.width / 2, + element.y + element.height / 2, + ]; - assertOrder(h.elements, initialOrder); - - expect(h.elements[1].frameId).toBe(null); - expect(h.elements[2].frameId).toBe(null); - - const container = h.elements[1]; - - mouse.clickAt(0, 0); - mouse.downAt(frame.x + frame.width, frame.y + frame.height); - mouse.moveTo( - container.x + container.width + 100, - container.y + container.height + 100, - ); + Keyboard.withModifierKeys({ alt: true }, () => { + mouse.downAt(x, y); + mouse.moveTo(moveTo[0], moveTo[1]); mouse.up(); - assertOrder(h.elements, expectedOrder); + }); + } - expect(h.elements[0].frameId).toBe(frame.id); - expect(h.elements[1].frameId).toBe(frame.id); - }; + function expectEqualIds(expected: ExcalidrawElement[]) { + expect(h.elements.map((x) => x.id)).toEqual(expected.map((x) => x.id)); + } - it("resizing over text containers / labelled arrows", async () => { - await testElements( + let frame: ExcalidrawElement; + let rect1: ExcalidrawElement; + let rect2: ExcalidrawElement; + let rect3: ExcalidrawElement; + let rect4: ExcalidrawElement; + let text: ExcalidrawElement; + let arrow: ExcalidrawElement; + + beforeEach(async () => { + await render(); + + frame = API.createElement({ id: "id0", type: "frame", x: 0, width: 150 }); + rect1 = API.createElement({ + id: "id1", + type: "rectangle", + x: -1000, + }); + rect2 = API.createElement({ + id: "id2", + type: "rectangle", + x: 200, + width: 50, + }); + rect3 = API.createElement({ + id: "id3", + type: "rectangle", + x: 400, + width: 50, + }); + rect4 = API.createElement({ + id: "id4", + type: "rectangle", + x: 1000, + width: 50, + }); + text = API.createElement({ + id: "id5", + type: "text", + x: 100, + }); + arrow = API.createElement({ + id: "id6", + type: "arrow", + x: 100, + boundElements: [{ id: text.id, type: "text" }], + }); + }); + + const commonTestCases = async ( + func: typeof resizeFrameOverElement | typeof dragElementIntoFrame, + ) => { + describe("when frame is in a layer below", async () => { + it("should add an element", async () => { + h.elements = [frame, rect2]; + + func(frame, rect2); + + expect(h.elements[0].frameId).toBe(frame.id); + expectEqualIds([rect2, frame]); + }); + + it("should add elements", async () => { + h.elements = [frame, rect2, rect3]; + + func(frame, rect2); + func(frame, rect3); + + expect(rect2.frameId).toBe(frame.id); + expect(rect3.frameId).toBe(frame.id); + expectEqualIds([rect2, rect3, frame]); + }); + + it("should add elements when there are other other elements in between", async () => { + h.elements = [frame, rect1, rect2, rect4, rect3]; + + func(frame, rect2); + func(frame, rect3); + + expect(rect2.frameId).toBe(frame.id); + expect(rect3.frameId).toBe(frame.id); + expectEqualIds([rect2, rect3, frame, rect1, rect4]); + }); + + it("should add elements when there are other elements in between and the order is reversed", async () => { + h.elements = [frame, rect3, rect4, rect2, rect1]; + + func(frame, rect2); + func(frame, rect3); + + expect(rect2.frameId).toBe(frame.id); + expect(rect3.frameId).toBe(frame.id); + expectEqualIds([rect2, rect3, frame, rect4, rect1]); + }); + }); + + describe("when frame is in a layer above", async () => { + it("should add an element", async () => { + h.elements = [rect2, frame]; + + func(frame, rect2); + + expect(h.elements[0].frameId).toBe(frame.id); + expectEqualIds([rect2, frame]); + }); + + it("should add elements", async () => { + h.elements = [rect2, rect3, frame]; + + func(frame, rect2); + func(frame, rect3); + + expect(rect2.frameId).toBe(frame.id); + expect(rect3.frameId).toBe(frame.id); + expectEqualIds([rect3, rect2, frame]); + }); + + it("should add elements when there are other other elements in between", async () => { + h.elements = [rect1, rect2, rect4, rect3, frame]; + + func(frame, rect2); + func(frame, rect3); + + expect(rect2.frameId).toBe(frame.id); + expect(rect3.frameId).toBe(frame.id); + expectEqualIds([rect1, rect4, rect3, rect2, frame]); + }); + + it("should add elements when there are other elements in between and the order is reversed", async () => { + h.elements = [rect3, rect4, rect2, rect1, frame]; + + func(frame, rect2); + func(frame, rect3); + + expect(rect2.frameId).toBe(frame.id); + expect(rect3.frameId).toBe(frame.id); + expectEqualIds([rect4, rect1, rect3, rect2, frame]); + }); + }); + + describe("when frame is in an inner layer", async () => { + it("should add elements", async () => { + h.elements = [rect2, frame, rect3]; + + func(frame, rect2); + func(frame, rect3); + + expect(rect2.frameId).toBe(frame.id); + expect(rect3.frameId).toBe(frame.id); + expectEqualIds([rect2, rect3, frame]); + }); + + it("should add elements when there are other other elements in between", async () => { + h.elements = [rect2, rect1, frame, rect4, rect3]; + + func(frame, rect2); + func(frame, rect3); + + expect(rect2.frameId).toBe(frame.id); + expect(rect3.frameId).toBe(frame.id); + expectEqualIds([rect1, rect2, rect3, frame, rect4]); + }); + + it("should add elements when there are other elements in between and the order is reversed", async () => { + h.elements = [rect3, rect4, frame, rect2, rect1]; + + func(frame, rect2); + func(frame, rect3); + + expect(rect2.frameId).toBe(frame.id); + expect(rect3.frameId).toBe(frame.id); + expectEqualIds([rect4, rect3, rect2, frame, rect1]); + }); + }); + }; + + const resizingTest = async ( + containerType: "arrow" | "rectangle", + initialOrder: ElementType[], + expectedOrder: ElementType[], + ) => { + await render(); + + const frame = API.createElement({ type: "frame", x: 0, y: 0 }); + + h.elements = reorderElements( + [ + frame, + ...convertToExcalidrawElements([ + { + type: containerType, + x: 100, + y: 100, + height: 10, + label: { text: "xx" }, + }, + ]), + ], + initialOrder, + ); + + assertOrder(h.elements, initialOrder); + + expect(h.elements[1].frameId).toBe(null); + expect(h.elements[2].frameId).toBe(null); + + const container = h.elements[1]; + + resizeFrameOverElement(frame, container); + assertOrder(h.elements, expectedOrder); + + expect(h.elements[0].frameId).toBe(frame.id); + expect(h.elements[1].frameId).toBe(frame.id); + }; + + describe("resizing frame over elements", async () => { + await commonTestCases(resizeFrameOverElement); + + it("resizing over text containers and labelled arrows", async () => { + await resizingTest( "rectangle", ["frame", "rectangle", "text"], ["rectangle", "text", "frame"], ); - await testElements( + await resizingTest( "rectangle", ["frame", "text", "rectangle"], ["rectangle", "text", "frame"], ); - await testElements( + await resizingTest( "rectangle", ["rectangle", "text", "frame"], ["rectangle", "text", "frame"], ); - await testElements( + await resizingTest( "rectangle", ["text", "rectangle", "frame"], - ["text", "rectangle", "frame"], + ["rectangle", "text", "frame"], ); - - await testElements( + await resizingTest( "arrow", ["frame", "arrow", "text"], ["arrow", "text", "frame"], ); - await testElements( + await resizingTest( "arrow", ["text", "arrow", "frame"], - ["text", "arrow", "frame"], + ["arrow", "text", "frame"], + ); + await resizingTest( + "arrow", + ["frame", "arrow", "text"], + ["arrow", "text", "frame"], ); // FIXME failing in tests (it fails to add elements to frame for some @@ -118,11 +337,104 @@ describe("adding elements to frames", () => { // ["arrow", "text", "frame"], // ["arrow", "text", "frame"], // ); - // await testElements( - // "arrow", - // ["frame", "text", "arrow"], - // ["text", "arrow", "frame"], - // ); + }); + + it("should add arrow bound with text when frame is in a layer below", async () => { + h.elements = [frame, arrow, text]; + + resizeFrameOverElement(frame, arrow); + + expect(arrow.frameId).toBe(frame.id); + expect(text.frameId).toBe(frame.id); + expectEqualIds([arrow, text, frame]); + }); + + it("should add arrow bound with text when frame is in a layer above", async () => { + h.elements = [arrow, text, frame]; + + resizeFrameOverElement(frame, arrow); + + expect(arrow.frameId).toBe(frame.id); + expect(text.frameId).toBe(frame.id); + expectEqualIds([arrow, text, frame]); + }); + + it("should add arrow bound with text when frame is in an inner layer", async () => { + h.elements = [arrow, frame, text]; + + resizeFrameOverElement(frame, arrow); + + expect(arrow.frameId).toBe(frame.id); + expect(text.frameId).toBe(frame.id); + expectEqualIds([arrow, text, frame]); + }); + }); + + describe("resizing frame over elements but downwards", async () => { + it("should add elements when frame is in a layer below", async () => { + h.elements = [frame, rect1, rect2, rect3, rect4]; + + resizeFrameOverElement(frame, rect4); + resizeFrameOverElement(frame, rect3); + + expect(rect2.frameId).toBe(frame.id); + expect(rect3.frameId).toBe(frame.id); + expectEqualIds([rect2, rect3, frame, rect4, rect1]); + }); + + it("should add elements when frame is in a layer above", async () => { + h.elements = [rect1, rect2, rect3, rect4, frame]; + + resizeFrameOverElement(frame, rect4); + resizeFrameOverElement(frame, rect3); + + expect(rect2.frameId).toBe(frame.id); + expect(rect3.frameId).toBe(frame.id); + expectEqualIds([rect1, rect2, rect3, frame, rect4]); + }); + + it("should add elements when frame is in an inner layer", async () => { + h.elements = [rect1, rect2, frame, rect3, rect4]; + + resizeFrameOverElement(frame, rect4); + resizeFrameOverElement(frame, rect3); + + expect(rect2.frameId).toBe(frame.id); + expect(rect3.frameId).toBe(frame.id); + expectEqualIds([rect1, rect2, rect3, frame, rect4]); + }); + }); + + describe("dragging elements into the frame", async () => { + await commonTestCases(dragElementIntoFrame); + + it("should drag element inside, duplicate it and keep it in frame", () => { + h.elements = [frame, rect2]; + + dragElementIntoFrame(frame, rect2); + + const rect2_copy = { ...rect2, id: `${rect2.id}_copy` }; + + selectElementAndDuplicate(rect2); + + expect(rect2_copy.frameId).toBe(frame.id); + expect(rect2.frameId).toBe(frame.id); + expectEqualIds([rect2_copy, rect2, frame]); + }); + + it("should drag element inside, duplicate it and remove it from frame", () => { + h.elements = [frame, rect2]; + + dragElementIntoFrame(frame, rect2); + + const rect2_copy = { ...rect2, id: `${rect2.id}_copy` }; + + // move the rect2 outside the frame + selectElementAndDuplicate(rect2, [-1000, -1000]); + + expect(rect2_copy.frameId).toBe(frame.id); + expect(rect2.frameId).toBe(null); + expectEqualIds([rect2_copy, frame, rect2]); }); }); }); diff --git a/src/frame.ts b/src/frame.ts index d5599157c..e366ff430 100644 --- a/src/frame.ts +++ b/src/frame.ts @@ -468,14 +468,39 @@ export const addElementsToFrame = ( } } - let nextElements = allElements.slice(); + const allElementsIndex = allElements.reduce( + (acc: Record, element, index) => { + acc[element.id] = index; + return acc; + }, + {}, + ); + + const frameIndex = allElementsIndex[frame.id]; + // need to be calculated before the mutation below occurs + const leftFrameBoundaryIndex = findIndex( + allElements, + (e) => e.frameId === frame.id, + ); + + const existingFrameChildren = allElements.filter( + (element) => element.frameId === frame.id, + ); + + const addedFrameChildren_left: ExcalidrawElement[] = []; + const addedFrameChildren_right: ExcalidrawElement[] = []; - const frameBoundary = findIndex(nextElements, (e) => e.frameId === frame.id); for (const element of omitGroupsContainingFrames( allElements, _elementsToAdd, )) { if (element.frameId !== frame.id && !isFrameElement(element)) { + if (allElementsIndex[element.id] > frameIndex) { + addedFrameChildren_right.push(element); + } else { + addedFrameChildren_left.push(element); + } + mutateElement( element, { @@ -483,28 +508,35 @@ export const addElementsToFrame = ( }, false, ); - - const frameIndex = findIndex(nextElements, (e) => e.id === frame.id); - const elementIndex = findIndex(nextElements, (e) => e.id === element.id); - - if (elementIndex < frameBoundary) { - nextElements = [ - ...nextElements.slice(0, elementIndex), - ...nextElements.slice(elementIndex + 1, frameBoundary), - element, - ...nextElements.slice(frameBoundary), - ]; - } else if (elementIndex > frameIndex) { - nextElements = [ - ...nextElements.slice(0, frameIndex), - element, - ...nextElements.slice(frameIndex, elementIndex), - ...nextElements.slice(elementIndex + 1), - ]; - } } } + const frameElement = allElements[frameIndex]; + const nextFrameChildren = addedFrameChildren_left + .concat(existingFrameChildren) + .concat(addedFrameChildren_right); + + const nextFrameChildrenMap = nextFrameChildren.reduce( + (acc: Record, element) => { + acc[element.id] = true; + return acc; + }, + {}, + ); + + const nextOtherElements_left = allElements + .slice(0, leftFrameBoundaryIndex >= 0 ? leftFrameBoundaryIndex : frameIndex) + .filter((element) => !nextFrameChildrenMap[element.id]); + + const nextOtherElement_right = allElements + .slice(frameIndex + 1) + .filter((element) => !nextFrameChildrenMap[element.id]); + + const nextElements = nextOtherElements_left + .concat(nextFrameChildren) + .concat([frameElement]) + .concat(nextOtherElement_right); + return nextElements; }; @@ -518,6 +550,7 @@ export const removeElementsFromFrame = ( for (const element of elementsToRemove) { if (element.frameId) { _elementsToRemove.push(element); + const boundTextElement = getBoundTextElement(element); if (boundTextElement) { _elementsToRemove.push(boundTextElement); @@ -566,7 +599,7 @@ export const replaceAllElementsInFrame = ( ); }; -/** does not mutate elements, but return new ones */ +/** does not mutate elements, but returns new ones */ export const updateFrameMembershipOfSelectedElements = ( allElements: ExcalidrawElementsIncludingDeleted, appState: AppState, diff --git a/src/packages/excalidraw/.size-limit.json b/src/packages/excalidraw/.size-limit.json index e25386354..1b953f30b 100644 --- a/src/packages/excalidraw/.size-limit.json +++ b/src/packages/excalidraw/.size-limit.json @@ -1,7 +1,7 @@ [ { "path": "dist/excalidraw.production.min.js", - "limit": "291 kB" + "limit": "305 kB" }, { "path": "dist/excalidraw-assets/locales", diff --git a/src/packages/excalidraw/package.json b/src/packages/excalidraw/package.json index c449498f0..5e2e3583d 100644 --- a/src/packages/excalidraw/package.json +++ b/src/packages/excalidraw/package.json @@ -52,7 +52,7 @@ "@babel/preset-env": "7.18.6", "@babel/preset-react": "7.18.6", "@babel/preset-typescript": "7.18.6", - "@size-limit/preset-big-lib": "8.2.6", + "@size-limit/preset-big-lib": "9.0.0", "autoprefixer": "10.4.7", "babel-loader": "8.2.5", "babel-plugin-transform-class-properties": "6.24.1", @@ -63,7 +63,7 @@ "mini-css-extract-plugin": "2.6.1", "postcss-loader": "7.0.1", "sass-loader": "13.0.2", - "size-limit": "8.2.4", + "size-limit": "9.0.0", "style-loader": "3.3.3", "terser-webpack-plugin": "5.3.3", "ts-loader": "9.3.1", diff --git a/src/packages/excalidraw/yarn.lock b/src/packages/excalidraw/yarn.lock index 91789d834..293e596e8 100644 --- a/src/packages/excalidraw/yarn.lock +++ b/src/packages/excalidraw/yarn.lock @@ -1112,38 +1112,37 @@ dependencies: debug "^4.1.1" -"@size-limit/file@8.2.6": - version "8.2.6" - resolved "https://registry.yarnpkg.com/@size-limit/file/-/file-8.2.6.tgz#0e17045a0fa8009fc787c85e3c09f611316f908c" - integrity sha512-B7ayjxiJsbtXdIIWazJkB5gezi5WBMecdHTFPMDhI3NwEML1RVvUjAkrb1mPAAkIpt2LVHPnhdCUHjqDdjugwg== +"@size-limit/file@9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@size-limit/file/-/file-9.0.0.tgz#eed5415f5bcc8407979e47ffa49ffaf12d2d2378" + integrity sha512-oM2UaH2FRq4q22k+R+P6xCpzET10T94LFdSjb9svVu/vOD7NaB9LGcG6se8TW1BExXiyXO4GEhLsBt3uMKM3qA== dependencies: - semver "7.5.3" + semver "7.5.4" -"@size-limit/preset-big-lib@8.2.6": - version "8.2.6" - resolved "https://registry.yarnpkg.com/@size-limit/preset-big-lib/-/preset-big-lib-8.2.6.tgz#fbff51e7a03fc36b6b3d9103cbe5b3909e35a83e" - integrity sha512-63a+yos0QNMVCfx1OWnxBrdQVTlBVGzW5fDXwpWq/hKfP3B89XXHYGeL2Z2f8IXSVeGkAHXnDcTZyIPRaXffVg== +"@size-limit/preset-big-lib@9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@size-limit/preset-big-lib/-/preset-big-lib-9.0.0.tgz#ddcf30e7646b66ecc0f8a1a6498a5eda6d82876d" + integrity sha512-wc+VNLXjn0z11s1IWevo8+utP7uZGPVDNNe5cNyMFYHv7/pwJtgsd8w2onEkbK1h8x1oJfWlcqFNKAnvD1Bylw== dependencies: - "@size-limit/file" "8.2.6" - "@size-limit/time" "8.2.6" - "@size-limit/webpack" "8.2.6" - size-limit "8.2.6" + "@size-limit/file" "9.0.0" + "@size-limit/time" "9.0.0" + "@size-limit/webpack" "9.0.0" + size-limit "9.0.0" -"@size-limit/time@8.2.6": - version "8.2.6" - resolved "https://registry.yarnpkg.com/@size-limit/time/-/time-8.2.6.tgz#5d1912bcfc6437f6f59804737ad0538b25c207ed" - integrity sha512-fUEPvz7Uq6+oUQxSYbNlJt3tTgQBl1VY21USi/B7ebdnVKLnUx1JyPI9v7imN6XEkB2VpJtnYgjFeLgNrirzMA== +"@size-limit/time@9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@size-limit/time/-/time-9.0.0.tgz#44ba75b3cba30736b133dbb3fd740f894a642c87" + integrity sha512-//Yba5fRkYqpBZ6MFtjDTSjCpQonDMqkwofpe0G1hMd/5l/3PZXVLDCAU2BW3nQFqTkpeyytFG6Y3jxUqSddiw== dependencies: estimo "^2.3.6" - react "^17.0.2" -"@size-limit/webpack@8.2.6": - version "8.2.6" - resolved "https://registry.yarnpkg.com/@size-limit/webpack/-/webpack-8.2.6.tgz#3a3c98293b80f7c5fb6e8499199ae6f94f05b463" - integrity sha512-y2sB66m5sJxIjZ8SEAzpWbiw3/+bnQHDHfk9cSbV5ChKklq02AlYg8BS5KxGWmMpdyUo4TzpjSCP9oEudY+hxQ== +"@size-limit/webpack@9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@size-limit/webpack/-/webpack-9.0.0.tgz#4514851d3607490e228bf22bc95286643f64a490" + integrity sha512-0YwdvmBj9rS4bXE/PY9vSdc5lCiQXmT0794EsG7yvlDMWyrWa/dsgcRok/w0MoZstfuLaS6lv03VI5UJRFU/lg== dependencies: nanoid "^3.3.6" - webpack "^5.88.0" + webpack "^5.88.2" "@types/body-parser@*": version "1.19.2" @@ -1694,9 +1693,9 @@ ansi-styles@^4.1.0: color-convert "^2.0.1" anymatch@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" @@ -2553,9 +2552,9 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-glob@^3.2.9: - version "3.2.12" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" - integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -2672,9 +2671,9 @@ fs.realpath@^1.0.0: integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== function-bind@^1.1.1: version "1.1.1" @@ -2993,7 +2992,7 @@ is-docker@^2.0.0, is-docker@^2.1.1: is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.3" @@ -3105,7 +3104,7 @@ klona@^2.0.4, klona@^2.0.5: resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc" integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ== -lilconfig@^2.0.6, lilconfig@^2.1.0: +lilconfig@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== @@ -3146,7 +3145,7 @@ lodash@^4.17.20, lodash@^4.17.4: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -loose-envify@^1.0.0, loose-envify@^1.1.0: +loose-envify@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -3360,11 +3359,6 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - object-inspect@^1.9.0: version "1.12.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" @@ -3519,16 +3513,16 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" - integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== - -picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^2.2.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" + integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== + pkg-dir@4.2.0, pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -3685,14 +3679,6 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" -react@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - readable-stream@^2.0.1: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" @@ -3927,10 +3913,10 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@7.5.3: - version "7.5.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e" - integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ== +semver@7.5.4, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" @@ -3939,13 +3925,6 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== - dependencies: - lru-cache "^6.0.0" - send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -4054,22 +4033,10 @@ sirv@^1.0.7: mime "^2.3.1" totalist "^1.0.0" -size-limit@8.2.4: - version "8.2.4" - resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-8.2.4.tgz#0ab0df7cbc89007d544a50b451f5fb4d110694ca" - integrity sha512-Un16nSreD1v2CYwSorattiJcHuAWqXvg4TsGgzpjnoByqQwsSfCIEQHuaD14HNStzredR8cdsO9oGH91ibypTA== - dependencies: - bytes-iec "^3.1.1" - chokidar "^3.5.3" - globby "^11.1.0" - lilconfig "^2.0.6" - nanospinner "^1.1.0" - picocolors "^1.0.0" - -size-limit@8.2.6: - version "8.2.6" - resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-8.2.6.tgz#e41dbc74a4d7fc13be72551b6ef31ea50007d18d" - integrity sha512-zpznim/tX/NegjoQuRKgWTF4XiB0cn2qt90uJzxYNTFAqexk4b94DOAkBD3TwhC6c3kw2r0KcnA5upziVMZqDg== +size-limit@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-9.0.0.tgz#203c47303462a8351976eb26175acea5f4e80447" + integrity sha512-DrA7o2DeRN3s+vwCA9nn7Ck9Y4pn9t0GNUwQRpKqBtBmNkl6LA2s/NlNCdtKHrEkRTeYA1ZQ65mnYveo9rUqgA== dependencies: bytes-iec "^3.1.1" chokidar "^3.5.3" @@ -4556,7 +4523,7 @@ webpack@5.76.0: watchpack "^2.4.0" webpack-sources "^3.2.3" -webpack@^5.88.0: +webpack@^5.88.2: version "5.88.2" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e" integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ== diff --git a/src/tests/__snapshots__/MobileMenu.test.tsx.snap b/src/tests/__snapshots__/MobileMenu.test.tsx.snap index d85989e68..ad0c9f0f1 100644 --- a/src/tests/__snapshots__/MobileMenu.test.tsx.snap +++ b/src/tests/__snapshots__/MobileMenu.test.tsx.snap @@ -7,28 +7,45 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u