Pārlūkot izejas kodu

Merge remote-tracking branch 'origin/release' into danieljgeiger-mathjax-maint-stage

Daniel J. Geiger 1 gadu atpakaļ
vecāks
revīzija
bff220e0f5
100 mainītis faili ar 3152 papildinājumiem un 1484 dzēšanām
  1. 1 1
      .dockerignore
  2. 0 3
      .env.development
  3. 2 5
      .env.production
  4. 1 1
      .eslintignore
  5. 1 1
      .github/workflows/autorelease-excalidraw.yml
  6. 1 1
      .github/workflows/autorelease-preview.yml
  7. 1 1
      .github/workflows/lint.yml
  8. 2 2
      .github/workflows/locales-coverage.yml
  9. 5 7
      .github/workflows/size-limit.yml
  10. 1 1
      .github/workflows/test-coverage-pr.yml
  11. 1 1
      .github/workflows/test.yml
  12. 2 4
      .gitignore
  13. 2 2
      README.md
  14. 2 2
      crowdin.yml
  15. 1 1
      dev-docs/docs/@excalidraw/excalidraw/api/children-components/main-menu.mdx
  16. 1 1
      dev-docs/docs/@excalidraw/excalidraw/api/constants.mdx
  17. 6 6
      dev-docs/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton.mdx
  18. 18 18
      dev-docs/docs/@excalidraw/excalidraw/api/props/excalidraw-api.mdx
  19. 5 5
      dev-docs/docs/@excalidraw/excalidraw/api/props/initialdata.mdx
  20. 15 15
      dev-docs/docs/@excalidraw/excalidraw/api/props/props.mdx
  21. 2 2
      dev-docs/docs/@excalidraw/excalidraw/api/props/render-props.mdx
  22. 4 4
      dev-docs/docs/@excalidraw/excalidraw/api/props/ui-options.mdx
  23. 12 12
      dev-docs/docs/@excalidraw/excalidraw/api/utils/export.mdx
  24. 11 11
      dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx
  25. 24 24
      dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md
  26. 1 1
      dev-docs/docs/@excalidraw/excalidraw/customizing-styles.mdx
  27. 1 1
      dev-docs/docs/@excalidraw/excalidraw/development.mdx
  28. 1 1
      dev-docs/docs/@excalidraw/excalidraw/faq.mdx
  29. 81 24
      dev-docs/docs/@excalidraw/excalidraw/integration.mdx
  30. 2 2
      dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/new-diagram-type.mdx
  31. 2 2
      dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/flowchart.mdx
  32. 2 2
      dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/parser.mdx
  33. 0 9
      dev-docs/docs/introduction/contributing.mdx
  34. 1 4
      dev-docs/docusaurus.config.js
  35. 4 0
      dev-docs/vercel.json
  36. 15 6
      examples/excalidraw/components/App.scss
  37. 166 130
      examples/excalidraw/components/App.tsx
  38. 13 15
      examples/excalidraw/components/CustomFooter.tsx
  39. 27 0
      examples/excalidraw/components/MobileFooter.tsx
  40. 0 0
      examples/excalidraw/components/sidebar/ExampleSidebar.scss
  41. 2 1
      examples/excalidraw/components/sidebar/ExampleSidebar.tsx
  42. 2 2
      examples/excalidraw/initialData.tsx
  43. 13 0
      examples/excalidraw/package.json
  44. 3 0
      examples/excalidraw/tsconfig.json
  45. 146 0
      examples/excalidraw/utils.ts
  46. 36 0
      examples/excalidraw/with-nextjs/.gitignore
  47. 36 0
      examples/excalidraw/with-nextjs/README.md
  48. 12 0
      examples/excalidraw/with-nextjs/next.config.js
  49. 25 0
      examples/excalidraw/with-nextjs/package.json
  50. 0 0
      examples/excalidraw/with-nextjs/public/images/doremon.png
  51. 0 0
      examples/excalidraw/with-nextjs/public/images/excalibot.png
  52. 0 0
      examples/excalidraw/with-nextjs/public/images/pika.jpeg
  53. 0 0
      examples/excalidraw/with-nextjs/public/images/rocket.jpeg
  54. BIN
      examples/excalidraw/with-nextjs/src/app/favicon.ico
  55. 11 0
      examples/excalidraw/with-nextjs/src/app/layout.tsx
  56. 23 0
      examples/excalidraw/with-nextjs/src/app/page.tsx
  57. 15 0
      examples/excalidraw/with-nextjs/src/common.scss
  58. 22 0
      examples/excalidraw/with-nextjs/src/excalidrawWrapper.tsx
  59. 22 0
      examples/excalidraw/with-nextjs/src/pages/excalidraw-in-pages.tsx
  60. 28 0
      examples/excalidraw/with-nextjs/tsconfig.json
  61. 3 0
      examples/excalidraw/with-nextjs/vercel.json
  62. 252 0
      examples/excalidraw/with-nextjs/yarn.lock
  63. 7 4
      examples/excalidraw/with-script-in-browser/index.html
  64. 28 0
      examples/excalidraw/with-script-in-browser/index.tsx
  65. 19 0
      examples/excalidraw/with-script-in-browser/package.json
  66. BIN
      examples/excalidraw/with-script-in-browser/public/images/doremon.png
  67. BIN
      examples/excalidraw/with-script-in-browser/public/images/excalibot.png
  68. BIN
      examples/excalidraw/with-script-in-browser/public/images/pika.jpeg
  69. BIN
      examples/excalidraw/with-script-in-browser/public/images/rocket.jpeg
  70. 4 0
      examples/excalidraw/with-script-in-browser/vercel.json
  71. 11 0
      examples/excalidraw/with-script-in-browser/vite.config.mts
  72. 313 0
      examples/excalidraw/yarn.lock
  73. 900 0
      excalidraw-app/App.tsx
  74. 6 6
      excalidraw-app/CustomStats.tsx
  75. 8 2
      excalidraw-app/app_constants.ts
  76. 0 0
      excalidraw-app/bug-issue-template.js
  77. 192 106
      excalidraw-app/collab/Collab.tsx
  78. 54 21
      excalidraw-app/collab/Portal.tsx
  79. 14 14
      excalidraw-app/collab/RoomDialog.tsx
  80. 4 4
      excalidraw-app/collab/reconciliation.ts
  81. 1 1
      excalidraw-app/components/AppFooter.tsx
  82. 4 4
      excalidraw-app/components/AppMainMenu.tsx
  83. 6 6
      excalidraw-app/components/AppWelcomeScreen.tsx
  84. 3 3
      excalidraw-app/components/EncryptedIcon.tsx
  85. 22 12
      excalidraw-app/components/ExportToExcalidrawPlus.tsx
  86. 2 2
      excalidraw-app/components/GitHubCorner.tsx
  87. 3 3
      excalidraw-app/components/LanguageList.tsx
  88. 2 2
      excalidraw-app/components/TopErrorBoundary.tsx
  89. 6 6
      excalidraw-app/data/FileManager.ts
  90. 12 5
      excalidraw-app/data/LocalData.ts
  91. 19 12
      excalidraw-app/data/firebase.ts
  92. 38 50
      excalidraw-app/data/index.ts
  93. 5 5
      excalidraw-app/data/localStorage.ts
  94. 3 0
      excalidraw-app/global.d.ts
  95. 2 2
      excalidraw-app/index.html
  96. 14 873
      excalidraw-app/index.tsx
  97. 40 0
      excalidraw-app/package.json
  98. 23 6
      excalidraw-app/share/ShareDialog.scss
  99. 290 0
      excalidraw-app/share/ShareDialog.tsx
  100. 9 4
      excalidraw-app/tests/LanguageList.test.tsx

+ 1 - 1
.dockerignore

@@ -6,6 +6,6 @@
 !.prettierrc
 !.prettierrc
 !package.json
 !package.json
 !public/
 !public/
-!src/
+!packages/
 !tsconfig.json
 !tsconfig.json
 !yarn.lock
 !yarn.lock

+ 0 - 3
.env.development

@@ -7,9 +7,6 @@ VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfu
 # collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room)
 # collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room)
 VITE_APP_WS_SERVER_URL=http://localhost:3002
 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_LP=https://plus.excalidraw.com
 VITE_APP_PLUS_APP=https://app.excalidraw.com
 VITE_APP_PLUS_APP=https://app.excalidraw.com
 
 

+ 2 - 5
.env.production

@@ -4,16 +4,13 @@ VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
 VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
 VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
 VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
 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_LP=https://plus.excalidraw.com
 VITE_APP_PLUS_APP=https://app.excalidraw.com
 VITE_APP_PLUS_APP=https://app.excalidraw.com
 
 
 VITE_APP_AI_BACKEND=https://oss-ai.excalidraw.com
 VITE_APP_AI_BACKEND=https://oss-ai.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=
+# socket server URL used for collaboration
+VITE_APP_WS_SERVER_URL=https://oss-collab.excalidraw.com
 
 
 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_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"}'
 
 

+ 1 - 1
.eslintignore

@@ -5,4 +5,4 @@ package-lock.json
 firebase/
 firebase/
 dist/
 dist/
 public/workbox
 public/workbox
-src/packages/excalidraw/types
+packages/excalidraw/types

+ 1 - 1
.github/workflows/autorelease-excalidraw.yml

@@ -23,5 +23,5 @@ jobs:
           NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
           NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
       - name: Auto release
       - name: Auto release
         run: |
         run: |
-          yarn add @actions/core
+          yarn add @actions/core -W
           yarn autorelease
           yarn autorelease

+ 1 - 1
.github/workflows/autorelease-preview.yml

@@ -44,7 +44,7 @@ jobs:
       - name: Auto release preview
       - name: Auto release preview
         id: "autorelease"
         id: "autorelease"
         run: |
         run: |
-          yarn add @actions/core
+          yarn add @actions/core -W
           yarn autorelease preview ${{ github.event.issue.number }}
           yarn autorelease preview ${{ github.event.issue.number }}
       - name: Post comment post release
       - name: Post comment post release
         if: always()
         if: always()

+ 1 - 1
.github/workflows/lint.yml

@@ -16,7 +16,7 @@ jobs:
 
 
       - name: Install and lint
       - name: Install and lint
         run: |
         run: |
-          yarn --frozen-lockfile
+          yarn install
           yarn test:other
           yarn test:other
           yarn test:code
           yarn test:code
           yarn test:typecheck
           yarn test:typecheck

+ 2 - 2
.github/workflows/locales-coverage.yml

@@ -22,11 +22,11 @@ jobs:
       - name: Create report file
       - name: Create report file
         run: |
         run: |
           yarn locales-coverage
           yarn locales-coverage
-          FILE_CHANGED=$(git diff src/locales/percentages.json)
+          FILE_CHANGED=$(git diff packages/excalidraw/locales/percentages.json)
           if [ ! -z "${FILE_CHANGED}" ]; then
           if [ ! -z "${FILE_CHANGED}" ]; then
             git config --global user.name 'Excalidraw Bot'
             git config --global user.name 'Excalidraw Bot'
             git config --global user.email '[email protected]'
             git config --global user.email '[email protected]'
-            git add src/locales/percentages.json
+            git add packages/excalidraw/locales/percentages.json
             git commit -am "Auto commit: Calculate translation coverage"
             git commit -am "Auto commit: Calculate translation coverage"
             git push
             git push
           fi
           fi

+ 5 - 7
.github/workflows/size-limit.yml

@@ -15,16 +15,14 @@ jobs:
         uses: actions/setup-node@v3
         uses: actions/setup-node@v3
         with:
         with:
           node-version: 18.x
           node-version: 18.x
-      - name: Install
-        run: yarn --frozen-lockfile
-      - name: Install in src/packages/excalidraw
-        run: yarn --frozen-lockfile
-        working-directory: src/packages/excalidraw
+      - name: Install in packages/excalidraw
+        run: yarn
+        working-directory: packages/excalidraw
         env:
         env:
           CI: true
           CI: true
       - uses: andresz1/size-limit-action@v1
       - uses: andresz1/size-limit-action@v1
         with:
         with:
           github_token: ${{ secrets.GITHUB_TOKEN }}
           github_token: ${{ secrets.GITHUB_TOKEN }}
-          build_script: build:umd
+          build_script: build:esm
           skip_step: install
           skip_step: install
-          directory: src/packages/excalidraw
+          directory: packages/excalidraw

+ 1 - 1
.github/workflows/test-coverage-pr.yml

@@ -16,7 +16,7 @@ jobs:
         with:
         with:
           node-version: "18.x"
           node-version: "18.x"
       - name: "Install Deps"
       - name: "Install Deps"
-        run: yarn --frozen-lockfile
+        run: yarn install
       - name: "Test Coverage"
       - name: "Test Coverage"
         run: yarn test:coverage
         run: yarn test:coverage
       - name: "Report Coverage"
       - name: "Report Coverage"

+ 1 - 1
.github/workflows/test.yml

@@ -13,5 +13,5 @@ jobs:
           node-version: 18.x
           node-version: 18.x
       - name: Install and test
       - name: Install and test
         run: |
         run: |
-          yarn --frozen-lockfile
+          yarn install
           yarn test:app
           yarn test:app

+ 2 - 4
.gitignore

@@ -21,10 +21,8 @@ npm-debug.log*
 package-lock.json
 package-lock.json
 yarn-debug.log*
 yarn-debug.log*
 yarn-error.log*
 yarn-error.log*
-src/packages/excalidraw/types
-src/packages/excalidraw/example/public/bundle.js
-src/packages/excalidraw/example/public/excalidraw-assets-dev
-src/packages/excalidraw/example/public/excalidraw.development.js
+packages/excalidraw/types
 coverage
 coverage
 dev-dist
 dev-dist
 html
 html
+examples/**/bundle.*

+ 2 - 2
README.md

@@ -85,7 +85,7 @@ We'll be adding these features as drop-in plugins for the npm package in the fut
 
 
 ## Quick start
 ## Quick start
 
 
-Install the [Excalidraw npm package](https://www.npmjs.com/package/@excalidraw/excalidraw):
+**Note:** following instructions are for installing the Excalidraw [npm package](https://www.npmjs.com/package/@excalidraw/excalidraw) when integrating Excalidraw into your own app. To run the repository locally for development, please refer to our [Development Guide](https://docs.excalidraw.com/docs/introduction/development).
 
 
 ```
 ```
 npm install react react-dom @excalidraw/excalidraw
 npm install react react-dom @excalidraw/excalidraw
@@ -97,7 +97,7 @@ or via yarn
 yarn add react react-dom @excalidraw/excalidraw
 yarn add react react-dom @excalidraw/excalidraw
 ```
 ```
 
 
-Don't forget to check out our [Documentation](https://docs.excalidraw.com)!
+Check out our [documentation](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/installation) for more details!
 
 
 ## Contributing
 ## Contributing
 
 

+ 2 - 2
crowdin.yml

@@ -1,3 +1,3 @@
 files:
 files:
-  - source: /src/locales/en.json
-    translation: /src/locales/%locale%.json
+  - source: /packages/excalidraw/locales/en.json
+    translation: /packages/excalidraw/locales/%locale%.json

+ 1 - 1
dev-docs/docs/@excalidraw/excalidraw/api/children-components/main-menu.mdx

@@ -133,7 +133,7 @@ function App() {
 }
 }
 ```
 ```
 
 
-Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/src/components/mainMenu/DefaultItems.tsx) of the default items.
+Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/components/mainMenu/DefaultItems.tsx) of the default items.
 
 
 ### MainMenu.Group
 ### MainMenu.Group
 
 

+ 1 - 1
dev-docs/docs/@excalidraw/excalidraw/api/constants.mdx

@@ -37,7 +37,7 @@ Defaults to `THEME.LIGHT` unless passed in `initialData.appState.theme`
 
 
 ### MIME_TYPES
 ### MIME_TYPES
 
 
-[`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L101) contains all the mime types supported by `Excalidraw`.
+[`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L101) contains all the mime types supported by `Excalidraw`.
 
 
 **How to use **
 **How to use **
 
 

+ 6 - 6
dev-docs/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton.mdx

@@ -2,9 +2,9 @@
 
 
 We support a simplified API to make it easier to generate Excalidraw elements programmatically. This API is in beta and subject to change before stable. You can check the [PR](https://github.com/excalidraw/excalidraw/pull/6546) for more details.
 We support a simplified API to make it easier to generate Excalidraw elements programmatically. This API is in beta and subject to change before stable. You can check the [PR](https://github.com/excalidraw/excalidraw/pull/6546) for more details.
 
 
-For this purpose we introduced a new type [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133). This is the simplified version of [`ExcalidrawElement`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L134) type with the minimum possible attributes so that creating elements programmatically is much easier (especially for cases like binding arrows or creating text containers).
+For this purpose we introduced a new type [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133). This is the simplified version of [`ExcalidrawElement`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L134) type with the minimum possible attributes so that creating elements programmatically is much easier (especially for cases like binding arrows or creating text containers).
 
 
-The [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133) can be converted to fully qualified Excalidraw elements by using [`convertToExcalidrawElements`](/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton#converttoexcalidrawelements).
+The [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133) can be converted to fully qualified Excalidraw elements by using [`convertToExcalidrawElements`](/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton#converttoexcalidrawelements).
 
 
 ## convertToExcalidrawElements
 ## convertToExcalidrawElements
 
 
@@ -19,7 +19,7 @@ convertToExcalidrawElements(
 
 
 | Name | Type | Default | Description |
 | Name | Type | Default | Description |
 | --- | --- | --- | --- |
 | --- | --- | --- | --- |
-| `elements` | [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L137) |  | The Excalidraw element Skeleton which needs to be converted to Excalidraw elements. |
+| `elements` | [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L137) |  | The Excalidraw element Skeleton which needs to be converted to Excalidraw elements. |
 | `opts` | `{ regenerateIds: boolean }` | ` {regenerateIds: true}` | By default `id` will be regenerated for all the elements irrespective of whether you pass the `id` so if you don't want the ids to regenerated, you can set this attribute to `false`. |
 | `opts` | `{ regenerateIds: boolean }` | ` {regenerateIds: true}` | By default `id` will be regenerated for all the elements irrespective of whether you pass the `id` so if you don't want the ids to regenerated, you can set this attribute to `false`. |
 
 
 **_How to use_**
 **_How to use_**
@@ -71,7 +71,7 @@ function App() {
 }
 }
 ```
 ```
 
 
-You can pass additional [`properties`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L27) as well to decorate the shapes.
+You can pass additional [`properties`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L27) as well to decorate the shapes.
 
 
 :::info
 :::info
 
 
@@ -192,7 +192,7 @@ convertToExcalidrawElements([
 
 
 ### Text Containers
 ### Text Containers
 
 
-In addition to `type`, `x` and `y` properties, [`label`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L124C7-L130C59) property is required for text containers. The `text` property in `label` is required, rest of the attributes are optional.
+In addition to `type`, `x` and `y` properties, [`label`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L124C7-L130C59) property is required for text containers. The `text` property in `label` is required, rest of the attributes are optional.
 
 
 If you don't provide the dimensions of container, we calculate it based of the label dimensions.
 If you don't provide the dimensions of container, we calculate it based of the label dimensions.
 
 
@@ -326,7 +326,7 @@ convertToExcalidrawElements([
 
 
 ### Arrow bindings
 ### Arrow bindings
 
 
-To bind arrow to a shape you need to specify its [`start`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L86) and [`end`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L54) properties. You need to pass either `type` or `id` property in `start` and `end` properties, rest of the attributes are optional
+To bind arrow to a shape you need to specify its [`start`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L86) and [`end`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L54) properties. You need to pass either `type` or `id` property in `start` and `end` properties, rest of the attributes are optional
 
 
 ```js
 ```js
 convertToExcalidrawElements([
 convertToExcalidrawElements([

+ 18 - 18
dev-docs/docs/@excalidraw/excalidraw/api/props/excalidraw-api.mdx

@@ -2,7 +2,7 @@
 
 
 <pre>
 <pre>
   (api:{" "}
   (api:{" "}
-  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L616">
+  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L616">
     ExcalidrawAPI
     ExcalidrawAPI
   </a>
   </a>
   ) => void;
   ) => void;
@@ -17,7 +17,7 @@ export default function App() {
 }
 }
 ```
 ```
 
 
-You can use this prop when you want to access some [Excalidraw APIs](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L616). We expose the below APIs :point_down:
+You can use this prop when you want to access some [Excalidraw APIs](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L616). We expose the below APIs :point_down:
 
 
 | API | Signature | Usage |
 | API | Signature | Usage |
 | --- | --- | --- |
 | --- | --- | --- |
@@ -37,7 +37,7 @@ You can use this prop when you want to access some [Excalidraw APIs](https://git
 | [setActiveTool](#setactivetool) | `function` | This API can be used to set the active tool |
 | [setActiveTool](#setactivetool) | `function` | This API can be used to set the active tool |
 | [setCursor](#setcursor) | `function` | This API can be used to set customise the mouse cursor on the canvas |
 | [setCursor](#setcursor) | `function` | This API can be used to set customise the mouse cursor on the canvas |
 | [resetCursor](#resetcursor) | `function` | This API can be used to reset to default mouse cursor on the canvas |
 | [resetCursor](#resetcursor) | `function` | This API can be used to reset to default mouse cursor on the canvas |
-| [toggleMenu](#togglemenu) | `function` | Toggles specific menus on/off |
+| [toggleSidebar](#toggleSidebar) | `function` | Toggles specific sidebar on/off |
 | [onChange](#onChange) | `function` | Subscribes to change events |
 | [onChange](#onChange) | `function` | Subscribes to change events |
 | [onPointerDown](#onPointerDown) | `function` | Subscribes to `pointerdown` events |
 | [onPointerDown](#onPointerDown) | `function` | Subscribes to `pointerdown` events |
 | [onPointerUp](#onPointerUp) | `function` | Subscribes to `pointerup` events |
 | [onPointerUp](#onPointerUp) | `function` | Subscribes to `pointerup` events |
@@ -52,7 +52,7 @@ Additionally `ready` and `readyPromise` from the API have been discontinued. The
 
 
 <pre>
 <pre>
   (scene:{" "}
   (scene:{" "}
-  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L339">
+  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L339">
     sceneData
     sceneData
   </a>
   </a>
   ) => void
   ) => void
@@ -62,9 +62,9 @@ You can use this function to update the scene with the sceneData. It accepts the
 
 
 | Name | Type | Description |
 | Name | Type | Description |
 | --- | --- | --- |
 | --- | --- | --- |
-| `elements` | [`ImportedDataState["elements"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L38) | The `elements` to be updated in the scene |
-| `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L39) | The `appState` to be updated in the scene. |
-| `collaborators` | <code>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L37">Collaborator></a></code> | The list of collaborators to be updated in the scene. |
+| `elements` | [`ImportedDataState["elements"]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L38) | The `elements` to be updated in the scene |
+| `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L39) | The `appState` to be updated in the scene. |
+| `collaborators` | <code>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L37">Collaborator></a></code> | The list of collaborators to be updated in the scene. |
 | `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
 | `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
 
 
 ```jsx live
 ```jsx live
@@ -125,13 +125,13 @@ function App() {
 
 
 <pre>
 <pre>
   (opts: &#123; <br /> libraryItems:{" "}
   (opts: &#123; <br /> libraryItems:{" "}
-  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L249">
+  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L249">
     LibraryItemsSource
     LibraryItemsSource
   </a>
   </a>
   ;<br /> merge?: boolean; <br /> prompt?: boolean;
   ;<br /> merge?: boolean; <br /> prompt?: boolean;
   <br /> openLibraryMenu?: boolean;
   <br /> openLibraryMenu?: boolean;
   <br /> defaultStatus?: "unpublished" | "published"; <br /> &#125;) => Promise&lt;
   <br /> defaultStatus?: "unpublished" | "published"; <br /> &#125;) => Promise&lt;
-  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L246">
+  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L246">
     LibraryItems
     LibraryItems
   </a>
   </a>
   &gt;
   &gt;
@@ -141,7 +141,7 @@ You can use this function to update the library. It accepts the below attributes
 
 
 | Name | Type | Default | Description |
 | Name | Type | Default | Description |
 | --- | --- | --- | --- |
 | --- | --- | --- | --- |
-| `libraryItems` | [LibraryItemsSource](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L249) | \_ | The `libraryItems` to be replaced/merged with current library |
+| `libraryItems` | [LibraryItemsSource](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L249) | \_ | The `libraryItems` to be replaced/merged with current library |
 | `merge` | boolean | `false` | Whether to merge with existing library items. |
 | `merge` | boolean | `false` | Whether to merge with existing library items. |
 | `prompt` | boolean | `false` | Whether to prompt user for confirmation. |
 | `prompt` | boolean | `false` | Whether to prompt user for confirmation. |
 | `openLibraryMenu` | boolean | `false` | Keep the library menu open after library is updated. |
 | `openLibraryMenu` | boolean | `false` | Keep the library menu open after library is updated. |
@@ -189,7 +189,7 @@ function App() {
       </button>
       </button>
       <Excalidraw
       <Excalidraw
         ref={(api) => setExcalidrawAPI(api)}
         ref={(api) => setExcalidrawAPI(api)}
-        // initial data retrieved from https://github.com/excalidraw/excalidraw/blob/master/dev-docs/src/initialData.js
+        // initial data retrieved from https://github.com/excalidraw/excalidraw/blob/master/dev-docs/packages/excalidraw/initialData.js
         initialData={{
         initialData={{
           libraryItems: initialData.libraryItems,
           libraryItems: initialData.libraryItems,
           appState: { openSidebar: "library" },
           appState: { openSidebar: "library" },
@@ -204,7 +204,7 @@ function App() {
 
 
 <pre>
 <pre>
   (files:{" "}
   (files:{" "}
-  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59">
+  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L59">
     BinaryFileData
     BinaryFileData
   </a>
   </a>
   ) => void
   ) => void
@@ -224,7 +224,7 @@ Resets the scene. If `resetLoadingState` is passed as true then it will also for
 
 
 <pre>
 <pre>
   () =>{" "}
   () =>{" "}
-  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
+  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L115">
     ExcalidrawElement[]
     ExcalidrawElement[]
   </a>
   </a>
 </pre>
 </pre>
@@ -235,7 +235,7 @@ Returns all the elements including the deleted in the scene.
 
 
 <pre>
 <pre>
   () => NonDeleted&#60;
   () => NonDeleted&#60;
-  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
+  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L115">
     ExcalidrawElement
     ExcalidrawElement
   </a>
   </a>
   []&#62;
   []&#62;
@@ -247,7 +247,7 @@ Returns all the elements excluding the deleted in the scene
 
 
 <pre>
 <pre>
   () =>{" "}
   () =>{" "}
-  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
+  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">
     AppState
     AppState
   </a>
   </a>
 </pre>
 </pre>
@@ -288,7 +288,7 @@ Scroll the nearest element out of the elements supplied to the center of the vie
 
 
 | Attribute | type | default | Description |
 | Attribute | type | default | Description |
 | --- | --- | --- | --- |
 | --- | --- | --- | --- |
-| target | [ExcalidrawElement](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115) &#124; [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115) | All scene elements | The element(s) to scroll to. |
+| target | [ExcalidrawElement](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L115) &#124; [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L115) | All scene elements | The element(s) to scroll to. |
 | opts.fitToContent | boolean | false | Whether to fit the elements to viewport by automatically changing zoom as needed. Note that the zoom range is between 10%-100%. |
 | opts.fitToContent | boolean | false | Whether to fit the elements to viewport by automatically changing zoom as needed. Note that the zoom range is between 10%-100%. |
 | opts.fitToViewport | boolean | false | Similar to fitToContent but the zoom range is not limited. If elements are smaller than the viewport, zoom will go above 100%. |
 | opts.fitToViewport | boolean | false | Similar to fitToContent but the zoom range is not limited. If elements are smaller than the viewport, zoom will go above 100%. |
 | opts.viewportZoomFactor | number | 0.7 | when fitToViewport=true, how much screen should the content cover, between 0.1 (10%) and 1 (100%) |
 | opts.viewportZoomFactor | number | 0.7 | when fitToViewport=true, how much screen should the content cover, between 0.1 (10%) and 1 (100%) |
@@ -336,7 +336,7 @@ The unique id of the excalidraw component. This can be used to identify the exca
 
 
 <pre>
 <pre>
   () =>{" "}
   () =>{" "}
-  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L82">
+  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L82">
     files
     files
   </a>
   </a>
 </pre>
 </pre>
@@ -364,7 +364,7 @@ This API has the below signature. It sets the `tool` passed in param as the acti
 
 
 | Name | Type | Default | Description |
 | Name | Type | Default | Description |
 | --- | --- | --- | --- |
 | --- | --- | --- | --- |
-| `type` | [ToolType](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L91) | `selection` | The tool type which should be set as active tool. When setting `image` as active tool, the insertion onto canvas when using image tool is disabled by default, so you can enable it by setting `insertOnCanvasDirectly` to `true` |
+| `type` | [ToolType](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L91) | `selection` | The tool type which should be set as active tool. When setting `image` as active tool, the insertion onto canvas when using image tool is disabled by default, so you can enable it by setting `insertOnCanvasDirectly` to `true` |
 | `locked` | `boolean` | `false` | Indicates whether the the active tool should be locked. It behaves the same way when using the `lock` tool in the editor interface |
 | `locked` | `boolean` | `false` | Indicates whether the the active tool should be locked. It behaves the same way when using the `lock` tool in the editor interface |
 
 
 ## setCursor
 ## setCursor

+ 5 - 5
dev-docs/docs/@excalidraw/excalidraw/api/props/initialdata.mdx

@@ -1,18 +1,18 @@
 # initialData
 # initialData
 
 
 <pre>
 <pre>
-&#123; elements?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a> &#125;
+&#123; elements?: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a> &#125;
 </pre>
 </pre>
 
 
 This helps to load Excalidraw with `initialData`. It must be an object or a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) which resolves to an object containing the below optional fields.
 This helps to load Excalidraw with `initialData`. It must be an object or a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) which resolves to an object containing the below optional fields.
 
 
 | Name | Type | Description |
 | Name | Type | Description |
 | --- | --- | --- |
 | --- | --- | --- |
-| `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) | The `elements` with which `Excalidraw` should be mounted. |
-| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) | The `AppState` with which `Excalidraw` should be mounted. |
+| `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) | The `elements` with which `Excalidraw` should be mounted. |
+| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) | The `AppState` with which `Excalidraw` should be mounted. |
 | `scrollToContent` | `boolean` | This attribute indicates whether to `scroll` to the nearest element to center once `Excalidraw` is mounted. By default, it will not scroll the nearest element to the center. Make sure you pass `initialData.appState.scrollX` and `initialData.appState.scrollY` when `scrollToContent` is false so that scroll positions are retained |
 | `scrollToContent` | `boolean` | This attribute indicates whether to `scroll` to the nearest element to center once `Excalidraw` is mounted. By default, it will not scroll the nearest element to the center. Make sure you pass `initialData.appState.scrollX` and `initialData.appState.scrollY` when `scrollToContent` is false so that scroll positions are retained |
-| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L247) &#124; Promise&lt;[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)&gt; | This library items with which `Excalidraw` should be mounted. |
-| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L82) | The `files` added to the scene. |
+| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L247) &#124; Promise&lt;[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L200)&gt; | This library items with which `Excalidraw` should be mounted. |
+| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L82) | The `files` added to the scene. |
 
 
 You might want to use this when you want to load excalidraw with some initial elements and app state.
 You might want to use this when you want to load excalidraw with some initial elements and app state.
 
 

+ 15 - 15
dev-docs/docs/@excalidraw/excalidraw/api/props/props.mdx

@@ -23,7 +23,7 @@ All `props` are _optional_.
 | [`libraryReturnUrl`](#libraryreturnurl) | `string` | _ | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to |
 | [`libraryReturnUrl`](#libraryreturnurl) | `string` | _ | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to |
 | [`theme`](#theme) | `"light"` &#124; `"dark"` | `"light"` | The theme of the Excalidraw component |
 | [`theme`](#theme) | `"light"` &#124; `"dark"` | `"light"` | The theme of the Excalidraw component |
 | [`name`](#name) | `string` |  | Name of the drawing |
 | [`name`](#name) | `string` |  | Name of the drawing |
-| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](#canvasactions) |
+| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](/docs/@excalidraw/excalidraw/api/props/ui-options#canvasactions) |
 | [`detectScroll`](#detectscroll) | `boolean` | `true` | Indicates whether to update the offsets when nearest ancestor is scrolled. |
 | [`detectScroll`](#detectscroll) | `boolean` | `true` | Indicates whether to update the offsets when nearest ancestor is scrolled. |
 | [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
 | [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
 | [`autoFocus`](#autofocus) | `boolean` | `false` | indicates whether to focus the Excalidraw component on page load |
 | [`autoFocus`](#autofocus) | `boolean` | `false` | indicates whether to focus the Excalidraw component on page load |
@@ -33,7 +33,7 @@ All `props` are _optional_.
 
 
 ### Storing custom data on Excalidraw elements
 ### Storing custom data on Excalidraw elements
 
 
-Beyond attributes that Excalidraw elements already support, you can store `custom` data on each `element` in a `customData` object. The type of the attribute is [`Record<string, any>`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L66) and is optional.
+Beyond attributes that Excalidraw elements already support, you can store `custom` data on each `element` in a `customData` object. The type of the attribute is [`Record<string, any>`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L66) and is optional.
 
 
 You can use this to add any extra information you need to keep track of.
 You can use this to add any extra information you need to keep track of.
 
 
@@ -59,11 +59,11 @@ Every time component updates, this callback if passed will get triggered and has
 (excalidrawElements, appState, files) => void;
 (excalidrawElements, appState, files) => void;
 ```
 ```
 
 
-1. `excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) in the scene.
+1. `excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) in the scene.
 
 
-2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) of the scene.
+2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) of the scene.
 
 
-3. `files`: The [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) which are added to the scene.
+3. `files`: The [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L64) which are added to the scene.
 
 
 Here you can try saving the data to your backend or local storage for example.
 Here you can try saving the data to your backend or local storage for example.
 
 
@@ -79,14 +79,14 @@ This callback is triggered when mouse pointer is updated.
 
 
 2.`button`: The position of the button. This will be one of `["down", "up"]`
 2.`button`: The position of the button. This will be one of `["down", "up"]`
 
 
-3.`pointersMap`: [`pointers`](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L131) map of the scene
+3.`pointersMap`: [`pointers`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L131) map of the scene
 
 
 ```js
 ```js
 (exportedElements, appState, canvas) => void
 (exportedElements, appState, canvas) => void
 ```
 ```
 
 
-1. `exportedElements`: An array of [non deleted elements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L87) which needs to be exported.
-2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) of the scene.
+1. `exportedElements`: An array of [non deleted elements](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L87) which needs to be exported.
+2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) of the scene.
 3. `canvas`: The `HTMLCanvasElement` of the scene.
 3. `canvas`: The `HTMLCanvasElement` of the scene.
 
 
 ### onPointerDown
 ### onPointerDown
@@ -96,11 +96,11 @@ This prop if passed will be triggered on pointer down events and has the below s
 
 
 <pre>
 <pre>
 (activeTool:{" "}
 (activeTool:{" "}
-  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L115">
+  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L115">
     {" "}
     {" "}
     AppState["activeTool"]
     AppState["activeTool"]
   </a>
   </a>
-  , pointerDownState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L424">
+  , pointerDownState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L424">
     PointerDownState
     PointerDownState
   </a>) => void
   </a>) => void
 </pre>
 </pre>
@@ -119,7 +119,7 @@ This callback is triggered if passed when something is pasted into the scene. Yo
 
 
 <pre>
 <pre>
   (data:{" "}
   (data:{" "}
-  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/clipboard.ts#L18">
+  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/clipboard.ts#L18">
     ClipboardData
     ClipboardData
   </a>
   </a>
   , event: ClipboardEvent &#124; null) => boolean
   , event: ClipboardEvent &#124; null) => boolean
@@ -135,7 +135,7 @@ This callback if supplied will get triggered when the library is updated and has
 
 
 <pre>
 <pre>
   (items:{" "}
   (items:{" "}
-  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">
+  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L200">
     LibraryItems
     LibraryItems
   </a>
   </a>
   ) => void | Promise&lt;any&gt;
   ) => void | Promise&lt;any&gt;
@@ -149,7 +149,7 @@ This prop if passed will be triggered when clicked on `link`. To handle the redi
 
 
 <pre>
 <pre>
   (element:{" "}
   (element:{" "}
-  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
+  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">
     ExcalidrawElement
     ExcalidrawElement
   </a>
   </a>
   , event: CustomEvent&lt;&#123; nativeEvent: MouseEvent }&gt;) => void
   , event: CustomEvent&lt;&#123; nativeEvent: MouseEvent }&gt;) => void
@@ -182,7 +182,7 @@ const onLinkOpen: ExcalidrawProps["onLinkOpen"] = useCallback(
 
 
 ### langCode
 ### langCode
 
 
-Determines the `language` of the UI. It should be one of the [available language codes](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L14). Defaults to `en` (English). We also export default language and supported languages which you can import as shown below.
+Determines the `language` of the UI. It should be one of the [available language codes](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L14). Defaults to `en` (English). We also export default language and supported languages which you can import as shown below.
 
 
 ```js
 ```js
 import { defaultLang, languages } from "@excalidraw/excalidraw";
 import { defaultLang, languages } from "@excalidraw/excalidraw";
@@ -191,7 +191,7 @@ import { defaultLang, languages } from "@excalidraw/excalidraw";
 | name | type |
 | name | type |
 | --- | --- |
 | --- | --- |
 | `defaultLang` | `string` |
 | `defaultLang` | `string` |
-| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) |
+| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L15) |
 
 
 ### viewModeEnabled
 ### viewModeEnabled
 
 

+ 2 - 2
dev-docs/docs/@excalidraw/excalidraw/api/props/render-props.mdx

@@ -4,7 +4,7 @@
 
 
 <pre>
 <pre>
   (isMobile: boolean, appState:
   (isMobile: boolean, appState:
-  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
+  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">
     AppState
     AppState
   </a>) => JSX | null
   </a>) => JSX | null
 </pre>
 </pre>
@@ -66,7 +66,7 @@ function App() {
 
 
 <pre>
 <pre>
   (element: NonDeleted&lt;ExcalidrawEmbeddableElement&gt;, appState:{" "}
   (element: NonDeleted&lt;ExcalidrawEmbeddableElement&gt;, appState:{" "}
-  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
+  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">
     AppState
     AppState
   </a>
   </a>
   ) => JSX.Element | null
   ) => JSX.Element | null

+ 4 - 4
dev-docs/docs/@excalidraw/excalidraw/api/props/ui-options.mdx

@@ -4,7 +4,7 @@ This prop can be used to customise UI of Excalidraw. Currently we support custom
 
 
 <pre>
 <pre>
   &#123;
   &#123;
-  <br /> canvasActions?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L372">
+  <br /> canvasActions?: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L372">
     CanvasActions
     CanvasActions
   </a>, <br /> dockedSidebarBreakpoint?: number, <br /> welcomeScreen?: boolean <br />
   </a>, <br /> dockedSidebarBreakpoint?: number, <br /> welcomeScreen?: boolean <br />
 
 
@@ -55,7 +55,7 @@ If `UIOptions.canvasActions.export` is `false` the export button will not be ren
 
 
 ## dockedSidebarBreakpoint
 ## dockedSidebarBreakpoint
 
 
-This prop indicates at what point should we break to a docked, permanent sidebar. If not passed it defaults to [`MQ_RIGHT_SIDEBAR_MAX_WIDTH_PORTRAIT`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L161).  
+This prop indicates at what point should we break to a docked, permanent sidebar. If not passed it defaults to [`MQ_RIGHT_SIDEBAR_MAX_WIDTH_PORTRAIT`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L161).  
 If the _width_ of the _excalidraw_ container exceeds _dockedSidebarBreakpoint_, the sidebar will be `dockable` and the button to `dock` the sidebar will be shown  
 If the _width_ of the _excalidraw_ container exceeds _dockedSidebarBreakpoint_, the sidebar will be `dockable` and the button to `dock` the sidebar will be shown  
 If user choses to `dock` the sidebar, it will push the right part of the UI towards the left, making space for the sidebar as shown below.
 If user choses to `dock` the sidebar, it will push the right part of the UI towards the left, making space for the sidebar as shown below.
 
 
@@ -73,9 +73,9 @@ function App() {
 
 
 ## tools
 ## tools
 
 
-This `prop ` controls the visibility of the tools in the editor.
+This `prop` controls the visibility of the tools in the editor.
 Currently you can control the visibility of `image` tool via this prop.
 Currently you can control the visibility of `image` tool via this prop.
 
 
 | Prop | Type | Default | Description |
 | Prop | Type | Default | Description |
 | --- | --- | --- | --- |
 | --- | --- | --- | --- |
-| image | boolean | true | Decides whether `image` tool should be visible.
+| image | boolean | true | Decides whether `image` tool should be visible.

+ 12 - 12
dev-docs/docs/@excalidraw/excalidraw/api/utils/export.mdx

@@ -20,16 +20,16 @@ exportToCanvas(&#123;<br/>&nbsp;
   getDimensions,<br/>&nbsp;
   getDimensions,<br/>&nbsp;
   files,<br/>&nbsp;
   files,<br/>&nbsp;
   exportPadding?: number;<br/>
   exportPadding?: number;<br/>
-&#125;: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">ExportOpts</a>
+&#125;: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L21">ExportOpts</a>
 </pre>
 </pre>
 
 
 | Name | Type | Default | Description |
 | Name | Type | Default | Description |
 | --- | --- | --- | --- |
 | --- | --- | --- | --- |
-| `elements` | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) |  | The elements to be exported to canvas. |
-| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L23) | [Default App State](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L17) | The app state of the scene. |
+| `elements` | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) |  | The elements to be exported to canvas. |
+| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L23) | [Default App State](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/appState.ts#L17) | The app state of the scene. |
 | [`getDimensions`](#getdimensions) | `function` | _ | A function which returns the `width`, `height`, and optionally `scale` (defaults to  `1`), with which canvas is to be exported. |
 | [`getDimensions`](#getdimensions) | `function` | _ | A function which returns the `width`, `height`, and optionally `scale` (defaults to  `1`), with which canvas is to be exported. |
 | `maxWidthOrHeight` | `number` | _ | The maximum `width` or `height` of the exported image. If provided, `getDimensions` is ignored. |
 | `maxWidthOrHeight` | `number` | _ | The maximum `width` or `height` of the exported image. If provided, `getDimensions` is ignored. |
-| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59) | _ | The files added to the scene. |
+| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L59) | _ | The files added to the scene. |
 | `exportPadding` | `number` | `10` | The `padding` to be added on canvas. |
 | `exportPadding` | `number` | `10` | The `padding` to be added on canvas. |
 
 
 
 
@@ -105,7 +105,7 @@ function App() {
 
 
 <pre>
 <pre>
 exportToBlob(<br/>&nbsp;
 exportToBlob(<br/>&nbsp;
-  opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">ExportOpts</a> & &#123;<br/>&nbsp;
+  opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L14">ExportOpts</a> & &#123;<br/>&nbsp;
   mimeType?: string,<br/>&nbsp;
   mimeType?: string,<br/>&nbsp;
   quality?: number,<br/>&nbsp;
   quality?: number,<br/>&nbsp;
   exportPadding?: number;<br/>
   exportPadding?: number;<br/>
@@ -134,16 +134,16 @@ Returns a promise which resolves with a [blob](https://developer.mozilla.org/en-
 <pre>
 <pre>
 exportToSvg(&#123;<br/>&nbsp;
 exportToSvg(&#123;<br/>&nbsp;
   elements:&nbsp; 
   elements:&nbsp; 
-    <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
+    <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">
       ExcalidrawElement[]
       ExcalidrawElement[]
     </a>,<br/>&nbsp;
     </a>,<br/>&nbsp;
   appState:
   appState:
-    <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95"> AppState
+    <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95"> AppState
     </a>,<br/>&nbsp;
     </a>,<br/>&nbsp;
   exportPadding: number,<br/>&nbsp;
   exportPadding: number,<br/>&nbsp;
   metadata: string,<br/>&nbsp;
   metadata: string,<br/>&nbsp;
   files:&nbsp;
   files:&nbsp;
-  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59">
+  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L59">
       BinaryFiles
       BinaryFiles
     </a>,<br/>
     </a>,<br/>
 &#125;);
 &#125;);
@@ -151,10 +151,10 @@ exportToSvg(&#123;<br/>&nbsp;
 
 
 | Name | Type | Default | Description |
 | Name | Type | Default | Description |
 | --- | --- | --- | --- |
 | --- | --- | --- | --- |
-| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) |  | The elements to exported as `svg `|
-| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L11) | The `appState` of the scene |
+| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) |  | The elements to exported as `svg `|
+| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/appState.ts#L11) | The `appState` of the scene |
 | exportPadding | number | 10 | The `padding` to be added on canvas |
 | exportPadding | number | 10 | The `padding` to be added on canvas |
-| files | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) | undefined | The `files` added to the scene. |
+| files | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L64) | undefined | The `files` added to the scene. |
 
 
 This function returns a promise which resolves to `svg` of the exported drawing.
 This function returns a promise which resolves to `svg` of the exported drawing.
 
 
@@ -164,7 +164,7 @@ This function returns a promise which resolves to `svg` of the exported drawing.
 
 
 <pre>
 <pre>
 exportToClipboard(<br/>&nbsp;
 exportToClipboard(<br/>&nbsp;
-  opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">ExportOpts</a> & &#123;<br/>&nbsp;
+  opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L21">ExportOpts</a> & &#123;<br/>&nbsp;
   mimeType?: string,<br/>&nbsp;
   mimeType?: string,<br/>&nbsp;
   quality?: number;<br/>&nbsp;
   quality?: number;<br/>&nbsp;
   type: 'png' | 'svg' |'json'<br/>
   type: 'png' | 'svg' |'json'<br/>

+ 11 - 11
dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx

@@ -8,7 +8,7 @@ id: "restore"
 **_Signature_**
 **_Signature_**
 
 
 <pre>
 <pre>
-restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState["appState"]</a>,<br/>&nbsp; localAppState: Partial&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>
+restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L34">ImportedDataState["appState"]</a>,<br/>&nbsp; localAppState: Partial&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a>> | null): <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a>
 </pre>
 </pre>
 
 
 **_How to use_**
 **_How to use_**
@@ -17,7 +17,7 @@ restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob
 import { restoreAppState } from "@excalidraw/excalidraw";
 import { restoreAppState } from "@excalidraw/excalidraw";
 ```
 ```
 
 
-This function will make sure all the `keys` have appropriate `values` in [appState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) and if any key is missing, it will be set to its `default` value.
+This function will make sure all the `keys` have appropriate `values` in [appState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) and if any key is missing, it will be set to its `default` value.
 
 
 When `localAppState` is supplied, it's used in place of values that are missing (`undefined`) in `appState` instead of the defaults.  
 When `localAppState` is supplied, it's used in place of values that are missing (`undefined`) in `appState` instead of the defaults.  
 Use this as a way to not override user's defaults if you persist them.
 Use this as a way to not override user's defaults if you persist them.
@@ -29,16 +29,16 @@ You can pass `null` / `undefined` if not applicable.
 
 
 <pre>
 <pre>
 restoreElements(
 restoreElements(
-  elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>&nbsp;
-  localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>&nbsp;
+  elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>&nbsp;
+  localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>&nbsp;
   opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean }<br/>
   opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean }<br/>
 )
 )
 </pre>
 </pre>
 
 
 | Prop | Type | Description |
 | Prop | Type | Description |
 | ---- | ---- | ---- |
 | ---- | ---- | ---- |
-| `elements` | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a> | The `elements` to be restored |
-| [`localElements`](#localelements) | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> &#124; null &#124; undefined |  When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. |
+| `elements` | <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ImportedDataState["elements"]</a> | The `elements` to be restored |
+| [`localElements`](#localelements) | <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> &#124; null &#124; undefined |  When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. |
 | [`opts`](#opts) | `Object` | The extra optional parameter to configure restored elements
 | [`opts`](#opts) | `Object` | The extra optional parameter to configure restored elements
 
 
 #### localElements
 #### localElements
@@ -70,15 +70,15 @@ Parameter `refreshDimensions` indicates whether we should also `recalculate` tex
 
 
 <pre>
 <pre>
 restore(
 restore(
-  data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>&nbsp;
-  localAppState: Partial&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>&nbsp;
-  localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a><br/>
+  data: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L34">ImportedDataState</a>,<br/>&nbsp;
+  localAppState: Partial&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a>> | null | undefined,<br/>&nbsp;
+  localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L4">DataState</a><br/>
   opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean }<br/>
   opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean }<br/>
 
 
 )
 )
 </pre>
 </pre>
 
 
-See [`restoreAppState()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreAppState) about `localAppState`, and [`restoreElements()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreElements) about `localElements`.
+See [`restoreAppState()`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/excalidraw/README.md#restoreAppState) about `localAppState`, and [`restoreElements()`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/excalidraw/README.md#restoreElements) about `localElements`.
 
 
 **_How to use_**
 **_How to use_**
 
 
@@ -93,7 +93,7 @@ This function makes sure elements and state is set to appropriate values and set
 **_Signature_**
 **_Signature_**
 
 
 <pre>
 <pre>
-restoreLibraryItems(libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState["libraryItems"]</a>,<br/>&nbsp;
+restoreLibraryItems(libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L34">ImportedDataState["libraryItems"]</a>,<br/>&nbsp;
 defaultStatus: "published" | "unpublished")
 defaultStatus: "published" | "unpublished")
 </pre>
 </pre>
 
 

+ 24 - 24
dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md

@@ -8,7 +8,7 @@ These are pure Javascript functions exported from the @excalidraw/excalidraw [`@
 
 
 ### serializeAsJSON
 ### serializeAsJSON
 
 
-Takes the scene elements and state and returns a JSON string. `Deleted` elements as well as most properties from `AppState` are removed from the resulting JSON. (see [`serializeAsJSON()`](https://github.com/excalidraw/excalidraw/blob/master/src/data/json.ts#L42) source for details).
+Takes the scene elements and state and returns a JSON string. `Deleted` elements as well as most properties from `AppState` are removed from the resulting JSON. (see [`serializeAsJSON()`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/json.ts#L42) source for details).
 
 
 If you want to overwrite the `source` field in the `JSON` string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value.
 If you want to overwrite the `source` field in the `JSON` string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value.
 
 
@@ -16,8 +16,8 @@ If you want to overwrite the `source` field in the `JSON` string, you can set `w
 
 
 <pre>
 <pre>
 serializeAsJSON(&#123;<br/>&nbsp;
 serializeAsJSON(&#123;<br/>&nbsp;
-  elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>&nbsp;
-  appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>,<br/>
+  elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>&nbsp;
+  appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a>,<br/>
 }): string
 }): string
 </pre>
 </pre>
 
 
@@ -37,7 +37,7 @@ If you want to overwrite the source field in the JSON string, you can set `windo
 
 
 <pre>
 <pre>
 serializeLibraryAsJSON(
 serializeLibraryAsJSON(
-  libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems[]</a>)
+  libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L200">LibraryItems[]</a>)
 </pre>
 </pre>
 
 
 **How to use**
 **How to use**
@@ -53,7 +53,7 @@ Returns `true` if element is invisibly small (e.g. width & height are zero).
 **_Signature_**
 **_Signature_**
 
 
 <pre>
 <pre>
-isInvisiblySmallElement(element:  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement</a>): boolean
+isInvisiblySmallElement(element:  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement</a>): boolean
 </pre>
 </pre>
 
 
 **How to use**
 **How to use**
@@ -80,10 +80,10 @@ excalidrawAPI.updateScene(scene);
 <pre>
 <pre>
 loadFromBlob(<br/>&nbsp;
 loadFromBlob(<br/>&nbsp;
   blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,<br/>&nbsp;
   blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,<br/>&nbsp;
-  localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a> | null,<br/>&nbsp;
-  localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>&nbsp;
+  localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a> | null,<br/>&nbsp;
+  localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>&nbsp;
   fileHandle?: FileSystemHandle | null <br/>
   fileHandle?: FileSystemHandle | null <br/>
-) => Promise&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/restore.ts#L61">RestoredDataState</a>>
+) => Promise&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/restore.ts#L61">RestoredDataState</a>>
 </pre>
 </pre>
 
 
 ### loadLibraryFromBlob
 ### loadLibraryFromBlob
@@ -130,10 +130,10 @@ if (contents.type === MIME_TYPES.excalidraw) {
 <pre>
 <pre>
 loadSceneOrLibraryFromBlob(<br/>&nbsp;
 loadSceneOrLibraryFromBlob(<br/>&nbsp;
   blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,<br/>&nbsp;
   blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,<br/>&nbsp;
-  localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a> | null,<br/>&nbsp;
-  localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>&nbsp;
+  localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a> | null,<br/>&nbsp;
+  localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>&nbsp;
   fileHandle?: FileSystemHandle | null<br/>
   fileHandle?: FileSystemHandle | null<br/>
-) => Promise&lt;&#123; type: string, data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/restore.ts#L53">RestoredDataState</a> | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L33">ImportedLibraryState</a>}>
+) => Promise&lt;&#123; type: string, data: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/restore.ts#L53">RestoredDataState</a> | <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L33">ImportedLibraryState</a>}>
 </pre>
 </pre>
 
 
 ### getFreeDrawSvgPath
 ### getFreeDrawSvgPath
@@ -149,7 +149,7 @@ import { getFreeDrawSvgPath } from "@excalidraw/excalidraw";
 **Signature**
 **Signature**
 
 
 <pre>
 <pre>
-getFreeDrawSvgPath(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L182">ExcalidrawFreeDrawElement</a>)
+getFreeDrawSvgPath(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L182">ExcalidrawFreeDrawElement</a>)
 </pre>
 </pre>
 
 
 ### isLinearElement
 ### isLinearElement
@@ -165,7 +165,7 @@ import { isLinearElement } from "@excalidraw/excalidraw";
 **Signature**
 **Signature**
 
 
 <pre>
 <pre>
-isLinearElement(elementType?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L80">ExcalidrawElement</a>): boolean
+isLinearElement(elementType?: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L80">ExcalidrawElement</a>): boolean
 </pre>
 </pre>
 
 
 ### getNonDeletedElements
 ### getNonDeletedElements
@@ -181,7 +181,7 @@ import { getNonDeletedElements } from "@excalidraw/excalidraw";
 **Signature**
 **Signature**
 
 
 <pre>
 <pre>
-getNonDeletedElements(elements:<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114"> readonly ExcalidrawElement[]</a>): as readonly <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L125">NonDeletedExcalidrawElement[]</a>
+getNonDeletedElements(elements:<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114"> readonly ExcalidrawElement[]</a>): as readonly <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L125">NonDeletedExcalidrawElement[]</a>
 </pre>
 </pre>
 
 
 ### mergeLibraryItems
 ### mergeLibraryItems
@@ -196,9 +196,9 @@ import { mergeLibraryItems } from "@excalidraw/excalidraw";
 
 
 <pre>
 <pre>
 mergeLibraryItems(<br/>&nbsp;
 mergeLibraryItems(<br/>&nbsp;
-  localItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L250">LibraryItems</a>,<br/>&nbsp;
-  otherItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a><br/>
-): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L250">LibraryItems</a>
+  localItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L250">LibraryItems</a>,<br/>&nbsp;
+  otherItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L200">LibraryItems</a><br/>
+): <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L250">LibraryItems</a>
 </pre>
 </pre>
 
 
 ### parseLibraryTokensFromUrl
 ### parseLibraryTokensFromUrl
@@ -239,8 +239,8 @@ export const App = () => {
 
 
 <pre>
 <pre>
 useHandleLibrary(opts: &#123;<br/>&nbsp;
 useHandleLibrary(opts: &#123;<br/>&nbsp;
-  excalidrawAPI: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L494">ExcalidrawAPI</a>,<br/>&nbsp;
-  getInitialLibraryItems?: () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L253">LibraryItemsSource</a><br/>
+  excalidrawAPI: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L494">ExcalidrawAPI</a>,<br/>&nbsp;
+  getInitialLibraryItems?: () => <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L253">LibraryItemsSource</a><br/>
 });
 });
 </pre>
 </pre>
 
 
@@ -253,7 +253,7 @@ This function returns the current `scene` version.
 **_Signature_**
 **_Signature_**
 
 
 <pre>
 <pre>
-getSceneVersion(elements:  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>)
+getSceneVersion(elements:  <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a>)
 </pre>
 </pre>
 
 
 **How to use**
 **How to use**
@@ -274,7 +274,7 @@ import { sceneCoordsToViewportCoords } from "@excalidraw/excalidraw";
 
 
 <pre>
 <pre>
 sceneCoordsToViewportCoords(&#123; sceneX: number, sceneY: number },<br/>&nbsp;
 sceneCoordsToViewportCoords(&#123; sceneX: number, sceneY: number },<br/>&nbsp;
-  appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a><br/>): &#123; x: number, y: number }
+  appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a><br/>): &#123; x: number, y: number }
 </pre>
 </pre>
 
 
 ### viewportCoordsToSceneCoords
 ### viewportCoordsToSceneCoords
@@ -289,7 +289,7 @@ import { viewportCoordsToSceneCoords } from "@excalidraw/excalidraw";
 
 
 <pre>
 <pre>
 viewportCoordsToSceneCoords(&#123; clientX: number, clientY: number },<br/>&nbsp;
 viewportCoordsToSceneCoords(&#123; clientX: number, clientY: number },<br/>&nbsp;
-  appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a><br/>): &#123;x: number, y: number}
+  appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a><br/>): &#123;x: number, y: number}
 </pre>
 </pre>
 
 
 ### useDevice
 ### useDevice
@@ -350,8 +350,8 @@ To help with localization, we export the following.
 | name | type |
 | name | type |
 | --- | --- |
 | --- | --- |
 | `defaultLang` | `string` |
 | `defaultLang` | `string` |
-| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) |
-| `useI18n` | [`() => { langCode, t }`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) |
+| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L15) |
+| `useI18n` | [`() => { langCode, t }`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L15) |
 
 
 ```js
 ```js
 import { defaultLang, languages, useI18n } from "@excalidraw/excalidraw";
 import { defaultLang, languages, useI18n } from "@excalidraw/excalidraw";

+ 1 - 1
dev-docs/docs/@excalidraw/excalidraw/customizing-styles.mdx

@@ -21,7 +21,7 @@ Most notably, you can customize the primary colors, by overriding these variable
 - `--color-primary-light`
 - `--color-primary-light`
 - `--color-primary-contrast-offset` — a slightly darker (in light mode), or lighter (in dark mode) `--color-primary` color to fix contrast issues (see [Chubb illusion](https://en.wikipedia.org/wiki/Chubb_illusion)). It will fall back to `--color-primary` if not present.
 - `--color-primary-contrast-offset` — a slightly darker (in light mode), or lighter (in dark mode) `--color-primary` color to fix contrast issues (see [Chubb illusion](https://en.wikipedia.org/wiki/Chubb_illusion)). It will fall back to `--color-primary` if not present.
 
 
-For a complete list of variables, check [theme.scss](https://github.com/excalidraw/excalidraw/blob/master/src/css/theme.scss), though most of them will not make sense to override.
+For a complete list of variables, check [theme.scss](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/css/theme.scss), though most of them will not make sense to override.
 
 
 ```css showLineNumbers
 ```css showLineNumbers
 .custom-styles .excalidraw {
 .custom-styles .excalidraw {

+ 1 - 1
dev-docs/docs/@excalidraw/excalidraw/development.mdx

@@ -13,7 +13,7 @@ To start the example app using the `@excalidraw/excalidraw` package, follow the
 1. Install the dependencies
 1. Install the dependencies
 
 
    ```bash
    ```bash
-   cd src/packages/excalidraw && yarn
+   cd packages/excalidraw && yarn
    ```
    ```
 
 
 2. Start the example app
 2. Start the example app

+ 1 - 1
dev-docs/docs/@excalidraw/excalidraw/faq.mdx

@@ -39,7 +39,7 @@ Since Vite removes env variables by default, you can update the vite config to e
 
 
 ```
 ```
  define: {
  define: {
-    "process.env.IS_PREACT": process.env.IS_PREACT,
+    "process.env.IS_PREACT": JSON.stringify("true"),
   },
   },
 ```
 ```
 
 

+ 81 - 24
dev-docs/docs/@excalidraw/excalidraw/integration.mdx

@@ -32,15 +32,9 @@ function App() {
 
 
 ### Next.js
 ### Next.js
 
 
-Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
+Since Excalidraw doesn't support `server side rendering` so it should be rendered only on `client`. The way to achieve this in next.js is using `next.js dynamic import`.
 
 
-Here are two ways on how you can render **Excalidraw** on **Next.js**.
-
-
-
-1. Using **Next.js Dynamic** import [Recommended].
-
-Since Excalidraw doesn't support server side rendering so you can also use `dynamic import` to render by setting `ssr` to `false`.
+If you want to only import `Excalidraw` component you can do :point_down:
 
 
 ```jsx showLineNumbers
 ```jsx showLineNumbers
 import dynamic from "next/dynamic";
 import dynamic from "next/dynamic";
@@ -55,25 +49,88 @@ export default function App() {
 }
 }
 ```
 ```
 
 
-Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-dynamic-k8yjq2).
+However the above component only works for named component exports. If you want to import some util / constant or something else apart from Excalidraw, then this approach will not work. Instead you can write a wrapper over Excalidraw and import the wrapper dynamically.
+
+If you are using `pages router` then importing the wrapper dynamically would work, where as if you are using `app router` then you will have to also add `useClient` directive on top of the file in addition to dynamically importing the wrapper as shown :point_down:
+
+<Tabs>
+  <TabItem value="Excalidraw Wrapper" label="Excalidraw Wrapper" >
+
+  ```jsx showLineNumbers
+  "use client";
+  import { Excalidraw. convertToExcalidrawElements } from "@excalidraw/excalidraw";
+
+  import "@excalidraw/excalidraw/index.css";
+
+  const ExcalidrawWrapper: React.FC = () => {
+    console.info(convertToExcalidrawElements([{
+      type: "rectangle",
+      id: "rect-1",
+      width: 186.47265625,
+      height: 141.9765625,
+    },]));
+    return (
+      <div style={{height:"500px", width:"500px"}}  
+        <Excalidraw />
+      </div> 
+    );
+  };
+  export default ExcalidrawWrapper;
+  ```
+
+  </TabItem>
 
 
+  <TabItem value="pages" label="Pages router">
 
 
-2. Importing Excalidraw once **client** is rendered.
+  ```jsx showLineNumbers
+  import dynamic from "next/dynamic";
+  
+  // Since client components get prerenderd on server as well hence importing 
+  // the excalidraw stuff dynamically with ssr false
 
 
-```jsx showLineNumbers
-import { useState, useEffect } from "react";
-export default function App() {
-  const [Excalidraw, setExcalidraw] = useState(null);
-  useEffect(() => {
-    import("@excalidraw/excalidraw").then((comp) =>
-      setExcalidraw(comp.Excalidraw),
+  const ExcalidrawWrapper = dynamic(
+    async () => (await import("../excalidrawWrapper")).default,
+    {
+      ssr: false,
+    },
+  );
+
+  export default function Page() {
+    return (
+      <ExcalidrawWrapper />      
     );
     );
-  }, []);
-  return <>{Excalidraw && <Excalidraw />}</>;
-}
-```
+  }
+  ```
+  </TabItem>
+
+  <TabItem value="app" label="App router">
+
+  ```jsx showLineNumbers
+  import dynamic from "next/dynamic";
+
+  // Since client components get prerenderd on server as well hence importing 
+  // the excalidraw stuff dynamically with ssr false
+
+  const ExcalidrawWrapper = dynamic(
+    async () => (await import("../excalidrawWrapper")).default,
+    {
+      ssr: false,
+    },
+  );
+
+  export default function Page() {
+    return (
+      <ExcalidrawWrapper />
+    );
+  }
+  ```
+
+  </TabItem>
+</Tabs>
+
+
+Here is a [source code](https://github.com/excalidraw/excalidraw/tree/master/examples/excalidraw/with-nextjs) for the example with app and pages router. You you can try it out [here](https://excalidraw-package-example-with-nextjs-gh6smrdnq-excalidraw.vercel.app/).
 
 
-Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-5xb3d)
 
 
 The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm)
 The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm)
 
 
@@ -93,7 +150,7 @@ Since Vite removes env variables by default, you can update the vite config to e
 
 
 ```
 ```
  define: {
  define: {
-    "process.env.IS_PREACT": process.env.IS_PREACT,
+    "process.env.IS_PREACT": JSON.stringify("true"),
   },
   },
 ```
 ```
 ::: 
 ::: 
@@ -148,7 +205,7 @@ import TabItem from "@theme/TabItem";
       <h1>Excalidraw Embed Example</h1>
       <h1>Excalidraw Embed Example</h1>
       <div id="app"></div>
       <div id="app"></div>
     </div>
     </div>
-    <script type="text/javascript" src="src/index.js"></script>
+    <script type="text/javascript" src="packages/excalidraw/index.js"></script>
   </body>
   </body>
 </html>
 </html>
 ```
 ```

+ 2 - 2
dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/new-diagram-type.mdx

@@ -38,9 +38,9 @@ Add the diagram type in switch case in [`parseMermaid`](https://github.com/excal
 
 
 ## Writing the Excalidraw Skeleton Convertor
 ## 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.
+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/packages/excalidraw/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).
+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/packages/excalidraw/data/transform.ts#L133).
 
 
 Thats it, you have added the new diagram type 🥳, now lets test it out!
 Thats it, you have added the new diagram type 🥳, now lets test it out!
 
 

+ 2 - 2
dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/flowchart.mdx

@@ -6,7 +6,7 @@ In this section we will be diving into how the [flowchart parser](https://github
 
 
 ![image](https://github.com/excalidraw/excalidraw/assets/11256141/2a097bbb-64bf-49d6-bf7f-21172bdb538d)
 ![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).
+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/packages/excalidraw/data/transform.ts#L133C13-L133C38).
 
 
 
 
 For computing `vertices` and `edge`s lets consider the below svg generated by mermaid
 For computing `vertices` and `edge`s lets consider the below svg generated by mermaid
@@ -42,7 +42,7 @@ Considering the same example this is the response from the API
 	}
 	}
 }
 }
 ```
 ```
-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 dimensions and position is missing in this response and we need that to transform to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/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:
  The final output from `parseVertex` looks like :point_down:
 
 

+ 2 - 2
dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/parser.mdx

@@ -55,11 +55,11 @@ If you want to understand how flowchart parser works, you can navigate to [Flowc
 
 
 ## Converting to ExcalidrawElementSkeleton
 ## 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.
+Now we have all the data, we just need to transform it to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/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 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 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).
+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/packages/excalidraw/data/transform.ts#L133C13-L133C38).
 
 
 ![image](https://github.com/excalidraw/excalidraw/assets/11256141/00226e9d-043d-4a08-989a-3ad9d2a574f1)
 ![image](https://github.com/excalidraw/excalidraw/assets/11256141/00226e9d-043d-4a08-989a-3ad9d2a574f1)

+ 0 - 9
dev-docs/docs/introduction/contributing.mdx

@@ -52,15 +52,6 @@ Make sure the title starts with a semantic prefix:
 - **chore**: Other changes that don't modify src or test files
 - **chore**: Other changes that don't modify src or test files
 - **revert**: Reverts a previous commit
 - **revert**: Reverts a previous commit
 
 
-### Changelog
-
-Add a brief description of your pull request to the changelog located here: [changelog](https://github.com/excalidraw/excalidraw/blob/master/CHANGELOG.md)
-
-Notes:
-
-- Make sure to prepend to the section corresponding with the semantic prefix you selected in the title
-- Link to your pull request - this will require updating the CHANGELOG _after_ creating the pull request
-
 ### Testing
 ### Testing
 
 
 Once you submit your pull request it will automatically be tested. Be sure to check the results of the test and fix any issues that arise.
 Once you submit your pull request it will automatically be tested. Be sure to check the results of the test and fix any issues that arise.

+ 1 - 4
dev-docs/docusaurus.config.js

@@ -41,10 +41,7 @@ const config = {
           showLastUpdateTime: true,
           showLastUpdateTime: true,
         },
         },
         theme: {
         theme: {
-          customCss: [
-            require.resolve("./src/css/custom.scss"),
-            require.resolve("../src/packages/excalidraw/example/App.scss"),
-          ],
+          customCss: [require.resolve("./src/css/custom.scss")],
         },
         },
       }),
       }),
     ],
     ],

+ 4 - 0
dev-docs/vercel.json

@@ -0,0 +1,4 @@
+{
+  "outputDirectory": "build",
+  "installCommand": "yarn install"
+}

+ 15 - 6
src/packages/excalidraw/example/App.scss → examples/excalidraw/components/App.scss

@@ -15,14 +15,23 @@
       border-radius: 50%;
       border-radius: 50%;
     }
     }
   }
   }
+  .app-title {
+    margin-block-start: 0.83em;
+    margin-block-end: 0.83em;
+  }
 }
 }
 
 
-.button-wrapper button {
-  z-index: 1;
-  height: 40px;
-  max-width: 200px;
-  margin: 10px;
-  padding: 5px;
+.button-wrapper {
+  input[type="checkbox"] {
+    margin: 5px;
+  }
+  button {
+    z-index: 1;
+    height: 40px;
+    max-width: 200px;
+    margin: 10px;
+    padding: 5px;
+  }
 }
 }
 
 
 .excalidraw .App-menu_top .buttonList {
 .excalidraw .App-menu_top .buttonList {

+ 166 - 130
src/packages/excalidraw/example/App.tsx → examples/excalidraw/components/App.tsx

@@ -1,23 +1,31 @@
-import { useEffect, useState, useRef, useCallback } from "react";
-
+import React, {
+  useEffect,
+  useState,
+  useRef,
+  useCallback,
+  Children,
+  cloneElement,
+} from "react";
 import ExampleSidebar from "./sidebar/ExampleSidebar";
 import ExampleSidebar from "./sidebar/ExampleSidebar";
 
 
-import type * as TExcalidraw from "../index";
+import type * as TExcalidraw from "@excalidraw/excalidraw";
 
 
-import "./App.scss";
-import initialData from "./initialData";
 import { nanoid } from "nanoid";
 import { nanoid } from "nanoid";
+
 import {
 import {
   resolvablePromise,
   resolvablePromise,
   ResolvablePromise,
   ResolvablePromise,
+  distance2d,
+  fileOpen,
   withBatchedUpdates,
   withBatchedUpdates,
   withBatchedUpdatesThrottled,
   withBatchedUpdatesThrottled,
-} from "../../../utils";
-import { EVENT, ROUNDNESS } from "../../../constants";
-import { distance2d } from "../../../math";
-import { fileOpen } from "../../../data/filesystem";
-import { loadSceneOrLibraryFromBlob } from "../../utils";
-import {
+} from "../utils";
+
+import CustomFooter from "./CustomFooter";
+import MobileFooter from "./MobileFooter";
+import initialData from "../initialData";
+
+import type {
   AppState,
   AppState,
   BinaryFileData,
   BinaryFileData,
   ExcalidrawImperativeAPI,
   ExcalidrawImperativeAPI,
@@ -25,18 +33,14 @@ import {
   Gesture,
   Gesture,
   LibraryItems,
   LibraryItems,
   PointerDownState as ExcalidrawPointerDownState,
   PointerDownState as ExcalidrawPointerDownState,
-} from "../../../types";
-import { NonDeletedExcalidrawElement, Theme } from "../../../element/types";
-import { ImportedLibraryData } from "../../../data/types";
-import CustomFooter from "./CustomFooter";
-import MobileFooter from "./MobileFooter";
-import { KEYS } from "../../../keys";
+} from "@excalidraw/excalidraw/dist/excalidraw/types";
+import type {
+  NonDeletedExcalidrawElement,
+  Theme,
+} from "@excalidraw/excalidraw/dist/excalidraw/element/types";
+import type { ImportedLibraryData } from "@excalidraw/excalidraw/dist/excalidraw/data/types";
 
 
-declare global {
-  interface Window {
-    ExcalidrawLib: typeof TExcalidraw;
-  }
-}
+import "./App.scss";
 
 
 type Comment = {
 type Comment = {
   x: number;
   x: number;
@@ -57,29 +61,6 @@ type PointerDownState = {
   };
   };
 };
 };
 
 
-// This is so that we use the bundled excalidraw.development.js file instead
-// of the actual source code
-const {
-  exportToCanvas,
-  exportToSvg,
-  exportToBlob,
-  exportToClipboard,
-  Excalidraw,
-  useHandleLibrary,
-  MIME_TYPES,
-  sceneCoordsToViewportCoords,
-  viewportCoordsToSceneCoords,
-  restoreElements,
-  Sidebar,
-  Footer,
-  WelcomeScreen,
-  MainMenu,
-  LiveCollaborationTrigger,
-  convertToExcalidrawElements,
-  TTDDialog,
-  TTDDialogTrigger,
-} = window.ExcalidrawLib;
-
 const COMMENT_ICON_DIMENSION = 32;
 const COMMENT_ICON_DIMENSION = 32;
 const COMMENT_INPUT_HEIGHT = 50;
 const COMMENT_INPUT_HEIGHT = 50;
 const COMMENT_INPUT_WIDTH = 150;
 const COMMENT_INPUT_WIDTH = 150;
@@ -88,9 +69,38 @@ export interface AppProps {
   appTitle: string;
   appTitle: string;
   useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void;
   useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void;
   customArgs?: any[];
   customArgs?: any[];
+  children: React.ReactNode;
+  excalidrawLib: typeof TExcalidraw;
 }
 }
 
 
-export default function App({ appTitle, useCustom, customArgs }: AppProps) {
+export default function App({
+  appTitle,
+  useCustom,
+  customArgs,
+  children,
+  excalidrawLib,
+}: AppProps) {
+  const {
+    exportToCanvas,
+    exportToSvg,
+    exportToBlob,
+    exportToClipboard,
+    useHandleLibrary,
+    MIME_TYPES,
+    sceneCoordsToViewportCoords,
+    viewportCoordsToSceneCoords,
+    restoreElements,
+    Sidebar,
+    Footer,
+    WelcomeScreen,
+    MainMenu,
+    LiveCollaborationTrigger,
+    convertToExcalidrawElements,
+    TTDDialog,
+    TTDDialogTrigger,
+    ROUNDNESS,
+    loadSceneOrLibraryFromBlob,
+  } = excalidrawLib;
   const appRef = useRef<any>(null);
   const appRef = useRef<any>(null);
   const [viewModeEnabled, setViewModeEnabled] = useState(false);
   const [viewModeEnabled, setViewModeEnabled] = useState(false);
   const [zenModeEnabled, setZenModeEnabled] = useState(false);
   const [zenModeEnabled, setZenModeEnabled] = useState(false);
@@ -152,8 +162,105 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
       };
       };
     };
     };
     fetchData();
     fetchData();
-  }, [excalidrawAPI]);
+  }, [excalidrawAPI, convertToExcalidrawElements, MIME_TYPES]);
 
 
+  const renderExcalidraw = (children: React.ReactNode) => {
+    const Excalidraw: any = Children.toArray(children).find(
+      (child) =>
+        React.isValidElement(child) &&
+        typeof child.type !== "string" &&
+        //@ts-ignore
+        child.type.displayName === "Excalidraw",
+    );
+    if (!Excalidraw) {
+      return;
+    }
+    const newElement = cloneElement(
+      Excalidraw,
+      {
+        excalidrawAPI: (api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api),
+        initialData: initialStatePromiseRef.current.promise,
+        onChange: (
+          elements: NonDeletedExcalidrawElement[],
+          state: AppState,
+        ) => {
+          console.info("Elements :", elements, "State : ", state);
+        },
+        onPointerUpdate: (payload: {
+          pointer: { x: number; y: number };
+          button: "down" | "up";
+          pointersMap: Gesture["pointers"];
+        }) => setPointerData(payload),
+        viewModeEnabled,
+        zenModeEnabled,
+        gridModeEnabled,
+        theme,
+        name: "Custom name of drawing",
+        UIOptions: {
+          canvasActions: {
+            loadScene: false,
+          },
+          tools: { image: !disableImageTool },
+        },
+        renderTopRightUI,
+        onLinkOpen,
+        onPointerDown,
+        onScrollChange: rerenderCommentIcons,
+        validateEmbeddable: true,
+      },
+      <>
+        {excalidrawAPI && (
+          <Footer>
+            <CustomFooter
+              excalidrawAPI={excalidrawAPI}
+              excalidrawLib={excalidrawLib}
+            />
+          </Footer>
+        )}
+        <WelcomeScreen />
+        <Sidebar name="custom">
+          <Sidebar.Tabs>
+            <Sidebar.Header />
+            <Sidebar.Tab tab="one">Tab one!</Sidebar.Tab>
+            <Sidebar.Tab tab="two">Tab two!</Sidebar.Tab>
+            <Sidebar.TabTriggers>
+              <Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger>
+              <Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger>
+            </Sidebar.TabTriggers>
+          </Sidebar.Tabs>
+        </Sidebar>
+        <Sidebar.Trigger
+          name="custom"
+          tab="one"
+          style={{
+            position: "absolute",
+            left: "50%",
+            transform: "translateX(-50%)",
+            bottom: "20px",
+            zIndex: 9999999999999999,
+          }}
+        >
+          Toggle Custom Sidebar
+        </Sidebar.Trigger>
+        {renderMenu()}
+        {excalidrawAPI && (
+          <TTDDialogTrigger icon={<span>😀</span>}>
+            Text to diagram
+          </TTDDialogTrigger>
+        )}
+        <TTDDialog
+          onTextSubmit={async (_) => {
+            console.info("submit");
+            // sleep for 2s
+            await new Promise((resolve) => setTimeout(resolve, 2000));
+            throw new Error("error, go away now");
+            // return "dummy";
+          }}
+        />
+      </>,
+    );
+    return newElement;
+  };
   const renderTopRightUI = (isMobile: boolean) => {
   const renderTopRightUI = (isMobile: boolean) => {
     return (
     return (
       <>
       <>
@@ -337,8 +444,8 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
     pointerDownState: PointerDownState,
     pointerDownState: PointerDownState,
   ) => {
   ) => {
     return withBatchedUpdates((event) => {
     return withBatchedUpdates((event) => {
-      window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.onMove);
-      window.removeEventListener(EVENT.POINTER_UP, pointerDownState.onUp);
+      window.removeEventListener("pointermove", pointerDownState.onMove);
+      window.removeEventListener("pointerup", pointerDownState.onUp);
       excalidrawAPI?.setActiveTool({ type: "selection" });
       excalidrawAPI?.setActiveTool({ type: "selection" });
       const distance = distance2d(
       const distance = distance2d(
         pointerDownState.x,
         pointerDownState.x,
@@ -402,8 +509,8 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
               onPointerMoveFromPointerDownHandler(pointerDownState);
               onPointerMoveFromPointerDownHandler(pointerDownState);
             const onPointerUp =
             const onPointerUp =
               onPointerUpFromPointerDownHandler(pointerDownState);
               onPointerUpFromPointerDownHandler(pointerDownState);
-            window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
-            window.addEventListener(EVENT.POINTER_UP, onPointerUp);
+            window.addEventListener("pointermove", onPointerMove);
+            window.addEventListener("pointerup", onPointerUp);
 
 
             pointerDownState.onMove = onPointerMove;
             pointerDownState.onMove = onPointerMove;
             pointerDownState.onUp = onPointerUp;
             pointerDownState.onUp = onPointerUp;
@@ -495,7 +602,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
         }}
         }}
         onBlur={saveComment}
         onBlur={saveComment}
         onKeyDown={(event) => {
         onKeyDown={(event) => {
-          if (!event.shiftKey && event.key === KEYS.ENTER) {
+          if (!event.shiftKey && event.key === "Enter") {
             event.preventDefault();
             event.preventDefault();
             saveComment();
             saveComment();
           }
           }
@@ -528,7 +635,12 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
         </MainMenu.ItemCustom>
         </MainMenu.ItemCustom>
         <MainMenu.DefaultItems.Help />
         <MainMenu.DefaultItems.Help />
 
 
-        {excalidrawAPI && <MobileFooter excalidrawAPI={excalidrawAPI} />}
+        {excalidrawAPI && (
+          <MobileFooter
+            excalidrawLib={excalidrawLib}
+            excalidrawAPI={excalidrawAPI}
+          />
+        )}
       </MainMenu>
       </MainMenu>
     );
     );
   };
   };
@@ -677,83 +789,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
           </div>
           </div>
         </div>
         </div>
         <div className="excalidraw-wrapper">
         <div className="excalidraw-wrapper">
-          <Excalidraw
-            excalidrawAPI={(api: ExcalidrawImperativeAPI) =>
-              setExcalidrawAPI(api)
-            }
-            initialData={initialStatePromiseRef.current.promise}
-            onChange={(elements, state) => {
-              // console.info("Elements :", elements, "State : ", state);
-            }}
-            onPointerUpdate={(payload: {
-              pointer: { x: number; y: number };
-              button: "down" | "up";
-              pointersMap: Gesture["pointers"];
-            }) => setPointerData(payload)}
-            viewModeEnabled={viewModeEnabled}
-            zenModeEnabled={zenModeEnabled}
-            gridModeEnabled={gridModeEnabled}
-            theme={theme}
-            name="Custom name of drawing"
-            UIOptions={{
-              canvasActions: {
-                loadScene: false,
-              },
-              tools: { image: !disableImageTool },
-            }}
-            renderTopRightUI={renderTopRightUI}
-            onLinkOpen={onLinkOpen}
-            onPointerDown={onPointerDown}
-            onScrollChange={rerenderCommentIcons}
-            // allow all urls
-            validateEmbeddable={true}
-          >
-            {excalidrawAPI && (
-              <Footer>
-                <CustomFooter excalidrawAPI={excalidrawAPI} />
-              </Footer>
-            )}
-            <WelcomeScreen />
-            <Sidebar name="custom">
-              <Sidebar.Tabs>
-                <Sidebar.Header />
-                <Sidebar.Tab tab="one">Tab one!</Sidebar.Tab>
-                <Sidebar.Tab tab="two">Tab two!</Sidebar.Tab>
-                <Sidebar.TabTriggers>
-                  <Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger>
-                  <Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger>
-                </Sidebar.TabTriggers>
-              </Sidebar.Tabs>
-            </Sidebar>
-            <Sidebar.Trigger
-              name="custom"
-              tab="one"
-              style={{
-                position: "absolute",
-                left: "50%",
-                transform: "translateX(-50%)",
-                bottom: "20px",
-                zIndex: 9999999999999999,
-              }}
-            >
-              Toggle Custom Sidebar
-            </Sidebar.Trigger>
-            {renderMenu()}
-            {excalidrawAPI && (
-              <TTDDialogTrigger icon={<span>😀</span>}>
-                Text to diagram
-              </TTDDialogTrigger>
-            )}
-            <TTDDialog
-              onTextSubmit={async (_) => {
-                console.info("submit");
-                // sleep for 2s
-                await new Promise((resolve) => setTimeout(resolve, 2000));
-                throw new Error("error, go away now");
-                // return "dummy";
-              }}
-            />
-          </Excalidraw>
+          {renderExcalidraw(children)}
           {Object.keys(commentIcons || []).length > 0 && renderCommentIcons()}
           {Object.keys(commentIcons || []).length > 0 && renderCommentIcons()}
           {comment && renderComment()}
           {comment && renderComment()}
         </div>
         </div>

+ 13 - 15
src/packages/excalidraw/example/CustomFooter.tsx → examples/excalidraw/components/CustomFooter.tsx

@@ -1,6 +1,5 @@
-import { ExcalidrawImperativeAPI } from "../../../types";
-import { MIME_TYPES } from "../entry";
-import { Button } from "../../../components/Button";
+import type * as TExcalidraw from "@excalidraw/excalidraw";
+import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
 
 
 const COMMENT_SVG = (
 const COMMENT_SVG = (
   <svg
   <svg
@@ -18,24 +17,28 @@ const COMMENT_SVG = (
     <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
     <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
   </svg>
   </svg>
 );
 );
+
 const CustomFooter = ({
 const CustomFooter = ({
   excalidrawAPI,
   excalidrawAPI,
+  excalidrawLib,
 }: {
 }: {
   excalidrawAPI: ExcalidrawImperativeAPI;
   excalidrawAPI: ExcalidrawImperativeAPI;
+  excalidrawLib: typeof TExcalidraw;
 }) => {
 }) => {
+  const { Button, MIME_TYPES } = excalidrawLib;
+
   return (
   return (
     <>
     <>
       <Button
       <Button
         onSelect={() => alert("General Kenobi!")}
         onSelect={() => alert("General Kenobi!")}
-        className="you are a bold one"
-        style={{ marginLeft: "1rem" }}
+        style={{ marginLeft: "1rem", width: "auto" }}
         title="Hello there!"
         title="Hello there!"
       >
       >
-        {COMMENT_SVG}
+        Hit me
       </Button>
       </Button>
-      <button
+      <Button
         className="custom-element"
         className="custom-element"
-        onClick={() => {
+        onSelect={() => {
           excalidrawAPI?.setActiveTool({
           excalidrawAPI?.setActiveTool({
             type: "custom",
             type: "custom",
             customType: "comment",
             customType: "comment",
@@ -58,15 +61,10 @@ const CustomFooter = ({
           )}`;
           )}`;
           excalidrawAPI?.setCursor(`url(${url}), auto`);
           excalidrawAPI?.setCursor(`url(${url}), auto`);
         }}
         }}
+        title="Comments!"
       >
       >
         {COMMENT_SVG}
         {COMMENT_SVG}
-      </button>
-      <button
-        className="custom-footer"
-        onClick={() => alert("This is dummy footer")}
-      >
-        custom footer
-      </button>
+      </Button>
     </>
     </>
   );
   );
 };
 };

+ 27 - 0
examples/excalidraw/components/MobileFooter.tsx

@@ -0,0 +1,27 @@
+import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
+import CustomFooter from "./CustomFooter";
+import type * as TExcalidraw from "@excalidraw/excalidraw";
+
+const MobileFooter = ({
+  excalidrawAPI,
+  excalidrawLib,
+}: {
+  excalidrawAPI: ExcalidrawImperativeAPI;
+  excalidrawLib: typeof TExcalidraw;
+}) => {
+  const { useDevice, Footer } = excalidrawLib;
+
+  const device = useDevice();
+  if (device.editor.isMobile) {
+    return (
+      <Footer>
+        <CustomFooter
+          excalidrawAPI={excalidrawAPI}
+          excalidrawLib={excalidrawLib}
+        />
+      </Footer>
+    );
+  }
+  return null;
+};
+export default MobileFooter;

+ 0 - 0
src/packages/excalidraw/example/sidebar/ExampleSidebar.scss → examples/excalidraw/components/sidebar/ExampleSidebar.scss


+ 2 - 1
src/packages/excalidraw/example/sidebar/ExampleSidebar.tsx → examples/excalidraw/components/sidebar/ExampleSidebar.tsx

@@ -1,5 +1,6 @@
-import React, { useState } from "react";
+import { useState } from "react";
 import "./ExampleSidebar.scss";
 import "./ExampleSidebar.scss";
+
 export default function Sidebar({ children }: { children: React.ReactNode }) {
 export default function Sidebar({ children }: { children: React.ReactNode }) {
   const [open, setOpen] = useState(false);
   const [open, setOpen] = useState(false);
 
 

+ 2 - 2
src/packages/excalidraw/example/initialData.tsx → examples/excalidraw/initialData.tsx

@@ -1,5 +1,5 @@
-import { ExcalidrawElementSkeleton } from "../../../data/transform";
-import { FileId } from "../../../element/types";
+import type { ExcalidrawElementSkeleton } from "@excalidraw/excalidraw/data/transform";
+import type { FileId } from "@excalidraw/excalidraw/element/types";
 
 
 const elements: ExcalidrawElementSkeleton[] = [
 const elements: ExcalidrawElementSkeleton[] = [
   {
   {

+ 13 - 0
examples/excalidraw/package.json

@@ -0,0 +1,13 @@
+{
+  "name": "examples",
+  "version": "1.0.0",
+  "private": true,
+  "dependencies": {
+    "react": "18.2.0",
+    "react-dom": "18.2.0",
+    "@excalidraw/excalidraw": "*"
+  },
+  "devDependencies": {
+    "typescript": "^5"
+  }
+}

+ 3 - 0
examples/excalidraw/tsconfig.json

@@ -0,0 +1,3 @@
+{
+  "extends": "../../tsconfig"
+}

+ 146 - 0
examples/excalidraw/utils.ts

@@ -0,0 +1,146 @@
+import { unstable_batchedUpdates } from "react-dom";
+import { fileOpen as _fileOpen } from "browser-fs-access";
+import type { MIME_TYPES } from "@excalidraw/excalidraw";
+import { AbortError } from "../../packages/excalidraw/errors";
+
+type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
+
+const INPUT_CHANGE_INTERVAL_MS = 500;
+
+export type ResolvablePromise<T> = Promise<T> & {
+  resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void;
+  reject: (error: Error) => void;
+};
+export const resolvablePromise = <T>() => {
+  let resolve!: any;
+  let reject!: any;
+  const promise = new Promise((_resolve, _reject) => {
+    resolve = _resolve;
+    reject = _reject;
+  });
+  (promise as any).resolve = resolve;
+  (promise as any).reject = reject;
+  return promise as ResolvablePromise<T>;
+};
+
+export const distance2d = (x1: number, y1: number, x2: number, y2: number) => {
+  const xd = x2 - x1;
+  const yd = y2 - y1;
+  return Math.hypot(xd, yd);
+};
+
+export const fileOpen = <M extends boolean | undefined = false>(opts: {
+  extensions?: FILE_EXTENSION[];
+  description: string;
+  multiple?: M;
+}): Promise<M extends false | undefined ? File : File[]> => {
+  // an unsafe TS hack, alas not much we can do AFAIK
+  type RetType = M extends false | undefined ? File : File[];
+
+  const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {
+    mimeTypes.push(MIME_TYPES[type]);
+
+    return mimeTypes;
+  }, [] as string[]);
+
+  const extensions = opts.extensions?.reduce((acc, ext) => {
+    if (ext === "jpg") {
+      return acc.concat(".jpg", ".jpeg");
+    }
+    return acc.concat(`.${ext}`);
+  }, [] as string[]);
+
+  return _fileOpen({
+    description: opts.description,
+    extensions,
+    mimeTypes,
+    multiple: opts.multiple ?? false,
+    legacySetup: (resolve, reject, input) => {
+      const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS);
+      const focusHandler = () => {
+        checkForFile();
+        document.addEventListener("keyup", scheduleRejection);
+        document.addEventListener("pointerup", scheduleRejection);
+        scheduleRejection();
+      };
+      const checkForFile = () => {
+        // this hack might not work when expecting multiple files
+        if (input.files?.length) {
+          const ret = opts.multiple ? [...input.files] : input.files[0];
+          resolve(ret as RetType);
+        }
+      };
+      requestAnimationFrame(() => {
+        window.addEventListener("focus", focusHandler);
+      });
+      const interval = window.setInterval(() => {
+        checkForFile();
+      }, INPUT_CHANGE_INTERVAL_MS);
+      return (rejectPromise) => {
+        clearInterval(interval);
+        scheduleRejection.cancel();
+        window.removeEventListener("focus", focusHandler);
+        document.removeEventListener("keyup", scheduleRejection);
+        document.removeEventListener("pointerup", scheduleRejection);
+        if (rejectPromise) {
+          // so that something is shown in console if we need to debug this
+          console.warn("Opening the file was canceled (legacy-fs).");
+          rejectPromise(new AbortError());
+        }
+      };
+    },
+  }) as Promise<RetType>;
+};
+
+export const debounce = <T extends any[]>(
+  fn: (...args: T) => void,
+  timeout: number,
+) => {
+  let handle = 0;
+  let lastArgs: T | null = null;
+  const ret = (...args: T) => {
+    lastArgs = args;
+    clearTimeout(handle);
+    handle = window.setTimeout(() => {
+      lastArgs = null;
+      fn(...args);
+    }, timeout);
+  };
+  ret.flush = () => {
+    clearTimeout(handle);
+    if (lastArgs) {
+      const _lastArgs = lastArgs;
+      lastArgs = null;
+      fn(..._lastArgs);
+    }
+  };
+  ret.cancel = () => {
+    lastArgs = null;
+    clearTimeout(handle);
+  };
+  return ret;
+};
+
+export const withBatchedUpdates = <
+  TFunction extends ((event: any) => void) | (() => void),
+>(
+  func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
+) =>
+  ((event) => {
+    unstable_batchedUpdates(func as TFunction, event);
+  }) as TFunction;
+
+/**
+ * barches React state updates and throttles the calls to a single call per
+ * animation frame
+ */
+export const withBatchedUpdatesThrottled = <
+  TFunction extends ((event: any) => void) | (() => void),
+>(
+  func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
+) => {
+  // @ts-ignore
+  return throttleRAF<Parameters<TFunction>>(((event) => {
+    unstable_batchedUpdates(func, event);
+  }) as TFunction);
+};

+ 36 - 0
examples/excalidraw/with-nextjs/.gitignore

@@ -0,0 +1,36 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts

+ 36 - 0
examples/excalidraw/with-nextjs/README.md

@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3005) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

+ 12 - 0
examples/excalidraw/with-nextjs/next.config.js

@@ -0,0 +1,12 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+  distDir: "build",
+  typescript: {
+    // The ts config doesn't work with `jsx: preserve" and if updated to `react-jsx` it gets ovewritten by next js throwing ts errors hence I am ignoring build errors until this is fixed.
+    ignoreBuildErrors: true,
+  },
+  // This is needed as in pages router the code for importing types throws error as its outside next js app
+  transpilePackages: ["../"],
+};
+
+module.exports = nextConfig;

+ 25 - 0
examples/excalidraw/with-nextjs/package.json

@@ -0,0 +1,25 @@
+{
+  "name": "with-nextjs",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm",
+    "dev": "yarn build:workspace && next dev -p 3005",
+    "build": "yarn build:workspace && next build",
+    "start": "next start -p 3006",
+    "lint": "next lint"
+  },
+  "dependencies": {
+    "@excalidraw/excalidraw": "*",
+    "next": "14.1",
+    "react": "^18",
+    "react-dom": "^18"
+  },
+  "devDependencies": {
+    "@types/node": "^20",
+    "@types/react": "^18",
+    "@types/react-dom": "^18",
+    "path2d-polyfill": "2.0.1",
+    "typescript": "^5"
+  }
+}

+ 0 - 0
src/packages/excalidraw/example/public/images/doremon.png → examples/excalidraw/with-nextjs/public/images/doremon.png


+ 0 - 0
src/packages/excalidraw/example/public/images/excalibot.png → examples/excalidraw/with-nextjs/public/images/excalibot.png


+ 0 - 0
src/packages/excalidraw/example/public/images/pika.jpeg → examples/excalidraw/with-nextjs/public/images/pika.jpeg


+ 0 - 0
src/packages/excalidraw/example/public/images/rocket.jpeg → examples/excalidraw/with-nextjs/public/images/rocket.jpeg


BIN
examples/excalidraw/with-nextjs/src/app/favicon.ico


+ 11 - 0
examples/excalidraw/with-nextjs/src/app/layout.tsx

@@ -0,0 +1,11 @@
+export default function RootLayout({
+  children,
+}: {
+  children: React.ReactNode;
+}) {
+  return (
+    <html lang="en">
+      <body>{children}</body>
+    </html>
+  );
+}

+ 23 - 0
examples/excalidraw/with-nextjs/src/app/page.tsx

@@ -0,0 +1,23 @@
+import dynamic from "next/dynamic";
+import "../common.scss";
+
+// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
+// with ssr false
+const ExcalidrawWithClientOnly = dynamic(
+  async () => (await import("../excalidrawWrapper")).default,
+  {
+    ssr: false,
+  },
+);
+
+export default function Page() {
+  return (
+    <>
+      <a href="/excalidraw-in-pages">Switch to Pages router</a>
+      <h1 className="page-title">App Router</h1>
+
+      {/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */}
+      <ExcalidrawWithClientOnly />
+    </>
+  );
+}

+ 15 - 0
examples/excalidraw/with-nextjs/src/common.scss

@@ -0,0 +1,15 @@
+* {
+  box-sizing: border-box;
+  font-family: sans-serif;
+}
+
+a {
+  color: #1c7ed6;
+  font-size: 20px;
+  text-decoration: none;
+  font-weight: 550;
+}
+
+.page-title {
+  text-align: center;
+}

+ 22 - 0
examples/excalidraw/with-nextjs/src/excalidrawWrapper.tsx

@@ -0,0 +1,22 @@
+"use client";
+import * as excalidrawLib from "@excalidraw/excalidraw";
+import { Excalidraw } from "@excalidraw/excalidraw";
+import App from "../../components/App";
+
+import "@excalidraw/excalidraw/index.css";
+
+const ExcalidrawWrapper: React.FC = () => {
+  return (
+    <>
+      <App
+        appTitle={"Excalidraw with Nextjs Example"}
+        useCustom={(api: any, args?: any[]) => {}}
+        excalidrawLib={excalidrawLib}
+      >
+        <Excalidraw />
+      </App>
+    </>
+  );
+};
+
+export default ExcalidrawWrapper;

+ 22 - 0
examples/excalidraw/with-nextjs/src/pages/excalidraw-in-pages.tsx

@@ -0,0 +1,22 @@
+import dynamic from "next/dynamic";
+import "../common.scss";
+
+// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
+// with ssr false
+const Excalidraw = dynamic(
+  async () => (await import("../excalidrawWrapper")).default,
+  {
+    ssr: false,
+  },
+);
+
+export default function Page() {
+  return (
+    <>
+      <a href="/">Switch to App router</a>
+      <h1 className="page-title">Pages Router</h1>
+      {/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */}
+      <Excalidraw />
+    </>
+  );
+}

+ 28 - 0
examples/excalidraw/with-nextjs/tsconfig.json

@@ -0,0 +1,28 @@
+{
+  "compilerOptions": {
+    "target": "es5",
+    "lib": ["dom", "dom.iterable", "esnext"],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "strict": true,
+    "noEmit": true,
+    "esModuleInterop": true,
+    "module": "esnext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "jsx": "preserve",
+    "incremental": true,
+    "plugins": [
+      {
+        "name": "next"
+      }
+    ],
+    "paths": {
+      "@/*": ["./src/*"]
+    },
+    "forceConsistentCasingInFileNames": true
+  },
+  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "build/types/**/*.ts"],
+  "exclude": ["node_modules"]
+}

+ 3 - 0
examples/excalidraw/with-nextjs/vercel.json

@@ -0,0 +1,3 @@
+{
+  "outputDirectory": "build"
+}

+ 252 - 0
examples/excalidraw/with-nextjs/yarn.lock

@@ -0,0 +1,252 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@excalidraw/excalidraw@workspace:^":
+  version "0.17.2"
+  resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.17.2.tgz#9a636a1e6bb3c88c5883347d3a7e75e9cce8ab96"
+  integrity sha512-7pqUWD8+mPjDhF4XxG3gw4rvE2JGaLW3Vss5UZfTbITPxAtFaGEc1K081bncitnaYhUwN9ENJE0i87QB3poDwQ==
+
+"@next/[email protected]":
+  version "14.0.4"
+  resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.4.tgz#d5cda0c4a862d70ae760e58c0cd96a8899a2e49a"
+  integrity sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ==
+
+"@next/[email protected]":
+  version "14.0.4"
+  resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz#27b1854c2cd04eb1d5e75081a1a792ad91526618"
+  integrity sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg==
+
+"@next/[email protected]":
+  version "14.0.4"
+  resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz#9940c449e757d0ee50bb9e792d2600cc08a3eb3b"
+  integrity sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==
+
+"@next/[email protected]":
+  version "14.0.4"
+  resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz#0eafd27c8587f68ace7b4fa80695711a8434de21"
+  integrity sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==
+
+"@next/[email protected]":
+  version "14.0.4"
+  resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz#2b0072adb213f36dada5394ea67d6e82069ae7dd"
+  integrity sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==
+
+"@next/[email protected]":
+  version "14.0.4"
+  resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz#68c67d20ebc8e3f6ced6ff23a4ba2a679dbcec32"
+  integrity sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==
+
+"@next/[email protected]":
+  version "14.0.4"
+  resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz#67cd81b42fb2caf313f7992fcf6d978af55a1247"
+  integrity sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==
+
+"@next/[email protected]":
+  version "14.0.4"
+  resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz#be06585906b195d755ceda28f33c633e1443f1a3"
+  integrity sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==
+
+"@next/[email protected]":
+  version "14.0.4"
+  resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz#e76cabefa9f2d891599c3d85928475bd8d3f6600"
+  integrity sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==
+
+"@next/[email protected]":
+  version "14.0.4"
+  resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz#e74892f1a9ccf41d3bf5979ad6d3d77c07b9cba1"
+  integrity sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==
+
+"@swc/[email protected]":
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d"
+  integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==
+  dependencies:
+    tslib "^2.4.0"
+
+"@types/node@^20":
+  version "20.11.0"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.0.tgz#8e0b99e70c0c1ade1a86c4a282f7b7ef87c9552f"
+  integrity sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==
+  dependencies:
+    undici-types "~5.26.4"
+
+"@types/prop-types@*":
+  version "15.7.11"
+  resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563"
+  integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==
+
+"@types/react-dom@^18":
+  version "18.2.18"
+  resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd"
+  integrity sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==
+  dependencies:
+    "@types/react" "*"
+
+"@types/react@*", "@types/react@^18":
+  version "18.2.47"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.47.tgz#85074b27ab563df01fbc3f68dc64bf7050b0af40"
+  integrity sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==
+  dependencies:
+    "@types/prop-types" "*"
+    "@types/scheduler" "*"
+    csstype "^3.0.2"
+
+"@types/scheduler@*":
+  version "0.16.8"
+  resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff"
+  integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==
+
[email protected]:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
+  integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
+  dependencies:
+    streamsearch "^1.1.0"
+
+caniuse-lite@^1.0.30001406:
+  version "1.0.30001576"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz#893be772cf8ee6056d6c1e2d07df365b9ec0a5c4"
+  integrity sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==
+
[email protected]:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
+  integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
+
+csstype@^3.0.2:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
+  integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
+
+glob-to-regexp@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
+  integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
+
+graceful-fs@^4.1.2, graceful-fs@^4.2.11:
+  version "4.2.11"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
+  integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
+"js-tokens@^3.0.0 || ^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+loose-envify@^1.1.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==
+  dependencies:
+    js-tokens "^3.0.0 || ^4.0.0"
+
+nanoid@^3.3.6:
+  version "3.3.7"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
+  integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
+
[email protected]:
+  version "14.0.4"
+  resolved "https://registry.yarnpkg.com/next/-/next-14.0.4.tgz#bf00b6f835b20d10a5057838fa2dfced1d0d84dc"
+  integrity sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==
+  dependencies:
+    "@next/env" "14.0.4"
+    "@swc/helpers" "0.5.2"
+    busboy "1.6.0"
+    caniuse-lite "^1.0.30001406"
+    graceful-fs "^4.2.11"
+    postcss "8.4.31"
+    styled-jsx "5.1.1"
+    watchpack "2.4.0"
+  optionalDependencies:
+    "@next/swc-darwin-arm64" "14.0.4"
+    "@next/swc-darwin-x64" "14.0.4"
+    "@next/swc-linux-arm64-gnu" "14.0.4"
+    "@next/swc-linux-arm64-musl" "14.0.4"
+    "@next/swc-linux-x64-gnu" "14.0.4"
+    "@next/swc-linux-x64-musl" "14.0.4"
+    "@next/swc-win32-arm64-msvc" "14.0.4"
+    "@next/swc-win32-ia32-msvc" "14.0.4"
+    "@next/swc-win32-x64-msvc" "14.0.4"
+
[email protected]:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz#24c554a738f42700d6961992bf5f1049672f2391"
+  integrity sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==
+
+picocolors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
[email protected]:
+  version "8.4.31"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
+  integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
+  dependencies:
+    nanoid "^3.3.6"
+    picocolors "^1.0.0"
+    source-map-js "^1.0.2"
+
+react-dom@^18:
+  version "18.2.0"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
+  integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
+  dependencies:
+    loose-envify "^1.1.0"
+    scheduler "^0.23.0"
+
+react@^18:
+  version "18.2.0"
+  resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
+  integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
+  dependencies:
+    loose-envify "^1.1.0"
+
+scheduler@^0.23.0:
+  version "0.23.0"
+  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
+  integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
+  dependencies:
+    loose-envify "^1.1.0"
+
+source-map-js@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
+  integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+
+streamsearch@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
+  integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
+
[email protected]:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f"
+  integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==
+  dependencies:
+    client-only "0.0.1"
+
+tslib@^2.4.0:
+  version "2.6.2"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
+  integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
+
+typescript@^5:
+  version "5.3.3"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
+  integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
+
+undici-types@~5.26.4:
+  version "5.26.5"
+  resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
+  integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
+
[email protected]:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
+  integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
+  dependencies:
+    glob-to-regexp "^0.4.1"
+    graceful-fs "^4.1.2"

+ 7 - 4
src/packages/excalidraw/example/public/index.html → examples/excalidraw/with-script-in-browser/index.html

@@ -12,18 +12,21 @@
     <script>
     <script>
       window.name = "codesandbox";
       window.name = "codesandbox";
     </script>
     </script>
+    <link rel="stylesheet" href="/dist/browser/dev/index.css" />
   </head>
   </head>
 
 
   <body>
   <body>
     <noscript> You need to enable JavaScript to run this app. </noscript>
     <noscript> You need to enable JavaScript to run this app. </noscript>
     <div id="root"></div>
     <div id="root"></div>
-    <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
-    <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
 
 
     <!-- This is so that we use the bundled excalidraw.development.js file instead
     <!-- This is so that we use the bundled excalidraw.development.js file instead
     of the actual source code -->
     of the actual source code -->
-    <script src="./excalidraw.development.js"></script>
+    <script type="module">
+      import * as ExcalidrawLib from "@excalidraw/excalidraw";
 
 
-    <script src="./bundle.js"></script>
+      console.log(ExcalidrawLib);
+      window.ExcalidrawLib = ExcalidrawLib;
+    </script>
+    <script type="module" src="index.tsx"></script>
   </body>
   </body>
 </html>
 </html>

+ 28 - 0
examples/excalidraw/with-script-in-browser/index.tsx

@@ -0,0 +1,28 @@
+import App from "../components/App";
+import React, { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+
+import type * as TExcalidraw from "@excalidraw/excalidraw";
+
+import "@excalidraw/excalidraw/index.css";
+
+declare global {
+  interface Window {
+    ExcalidrawLib: typeof TExcalidraw;
+  }
+}
+
+const rootElement = document.getElementById("root")!;
+const root = createRoot(rootElement);
+const { Excalidraw } = window.ExcalidrawLib;
+root.render(
+  <StrictMode>
+    <App
+      appTitle={"Excalidraw Example"}
+      useCustom={(api: any, args?: any[]) => {}}
+      excalidrawLib={window.ExcalidrawLib}
+    >
+      <Excalidraw />
+    </App>
+  </StrictMode>,
+);

+ 19 - 0
examples/excalidraw/with-script-in-browser/package.json

@@ -0,0 +1,19 @@
+{
+  "name": "with-script-in-browser",
+  "version": "1.0.0",
+  "private": true,
+  "dependencies": {
+    "react": "18.2.0",
+    "react-dom": "18.2.0",
+    "@excalidraw/excalidraw": "*"
+  },
+  "devDependencies": {
+    "vite": "5.0.12",
+    "typescript": "^5"
+  },
+  "scripts": {
+    "start": "yarn workspace @excalidraw/excalidraw run build:esm && vite",
+    "build": "yarn workspace @excalidraw/excalidraw run build:esm && vite build",
+    "build:preview": "yarn build && vite preview --port 5002"
+  }
+}

BIN
examples/excalidraw/with-script-in-browser/public/images/doremon.png


BIN
examples/excalidraw/with-script-in-browser/public/images/excalibot.png


BIN
examples/excalidraw/with-script-in-browser/public/images/pika.jpeg


BIN
examples/excalidraw/with-script-in-browser/public/images/rocket.jpeg


+ 4 - 0
examples/excalidraw/with-script-in-browser/vercel.json

@@ -0,0 +1,4 @@
+{
+  "outputDirectory": "dist",
+  "installCommand": "yarn install"
+}

+ 11 - 0
examples/excalidraw/with-script-in-browser/vite.config.mts

@@ -0,0 +1,11 @@
+import { defineConfig } from "vite";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  server: {
+    port: 3001,
+    // open the browser
+    open: true,
+  },
+  publicDir: "public",
+});

+ 313 - 0
examples/excalidraw/yarn.lock

@@ -0,0 +1,313 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz#2acd20be6d4f0458bc8c784103495ff24f13b1d3"
+  integrity sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz#b45d000017385c9051a4f03e17078abb935be220"
+  integrity sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.11.tgz#f46f55414e1c3614ac682b29977792131238164c"
+  integrity sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.11.tgz#bfc01e91740b82011ef503c48f548950824922b2"
+  integrity sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz#533fb7f5a08c37121d82c66198263dcc1bed29bf"
+  integrity sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz#62f3819eff7e4ddc656b7c6815a31cf9a1e7d98e"
+  integrity sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz#d478b4195aa3ca44160272dab85ef8baf4175b4a"
+  integrity sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz#7bdcc1917409178257ca6a1a27fe06e797ec18a2"
+  integrity sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz#58ad4ff11685fcc735d7ff4ca759ab18fcfe4545"
+  integrity sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz#ce82246d873b5534d34de1e5c1b33026f35e60e3"
+  integrity sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz#cbae1f313209affc74b80f4390c4c35c6ab83fa4"
+  integrity sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz#5f32aead1c3ec8f4cccdb7ed08b166224d4e9121"
+  integrity sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz#38eecf1cbb8c36a616261de858b3c10d03419af9"
+  integrity sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz#9c5725a94e6ec15b93195e5a6afb821628afd912"
+  integrity sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz#2dc4486d474a2a62bbe5870522a9a600e2acb916"
+  integrity sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz#4ad8567df48f7dd4c71ec5b1753b6f37561a65a8"
+  integrity sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz#b7390c4d5184f203ebe7ddaedf073df82a658766"
+  integrity sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz#d633c09492a1721377f3bccedb2d821b911e813d"
+  integrity sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz#17388c76e2f01125bf831a68c03a7ffccb65d1a2"
+  integrity sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz#e320636f00bb9f4fdf3a80e548cb743370d41767"
+  integrity sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz#c778b45a496e90b6fc373e2a2bb072f1441fe0ee"
+  integrity sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz#481a65fee2e5cce74ec44823e6b09ecedcc5194c"
+  integrity sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==
+
+"@esbuild/[email protected]":
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz#a5d300008960bb39677c46bf16f53ec70d8dee04"
+  integrity sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==
+
+"@rollup/[email protected]":
+  version "4.9.5"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz#b752b6c88a14ccfcbdf3f48c577ccc3a7f0e66b9"
+  integrity sha512-idWaG8xeSRCfRq9KpRysDHJ/rEHBEXcHuJ82XY0yYFIWnLMjZv9vF/7DOq8djQ2n3Lk6+3qfSH8AqlmHlmi1MA==
+
+"@rollup/[email protected]":
+  version "4.9.5"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.5.tgz#33757c3a448b9ef77b6f6292d8b0ec45c87e9c1a"
+  integrity sha512-f14d7uhAMtsCGjAYwZGv6TwuS3IFaM4ZnGMUn3aCBgkcHAYErhV1Ad97WzBvS2o0aaDv4mVz+syiN0ElMyfBPg==
+
+"@rollup/[email protected]":
+  version "4.9.5"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.5.tgz#5234ba62665a3f443143bc8bcea9df2cc58f55fb"
+  integrity sha512-ndoXeLx455FffL68OIUrVr89Xu1WLzAG4n65R8roDlCoYiQcGGg6MALvs2Ap9zs7AHg8mpHtMpwC8jBBjZrT/w==
+
+"@rollup/[email protected]":
+  version "4.9.5"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.5.tgz#981256c054d3247b83313724938d606798a919d1"
+  integrity sha512-UmElV1OY2m/1KEEqTlIjieKfVwRg0Zwg4PLgNf0s3glAHXBN99KLpw5A5lrSYCa1Kp63czTpVll2MAqbZYIHoA==
+
+"@rollup/[email protected]":
+  version "4.9.5"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.5.tgz#120678a5a2b3a283a548dbb4d337f9187a793560"
+  integrity sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g==
+
+"@rollup/[email protected]":
+  version "4.9.5"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.5.tgz#c99d857e2372ece544b6f60b85058ad259f64114"
+  integrity sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA==
+
+"@rollup/[email protected]":
+  version "4.9.5"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.5.tgz#3064060f568a5718c2a06858cd6e6d24f2ff8632"
+  integrity sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ==
+
+"@rollup/[email protected]":
+  version "4.9.5"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.5.tgz#987d30b5d2b992fff07d055015991a57ff55fbad"
+  integrity sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA==
+
+"@rollup/[email protected]":
+  version "4.9.5"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz#85946ee4d068bd12197aeeec2c6f679c94978a49"
+  integrity sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==
+
+"@rollup/[email protected]":
+  version "4.9.5"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.5.tgz#fe0b20f9749a60eb1df43d20effa96c756ddcbd4"
+  integrity sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg==
+
+"@rollup/[email protected]":
+  version "4.9.5"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.5.tgz#422661ef0e16699a234465d15b2c1089ef963b2a"
+  integrity sha512-aHSsMnUw+0UETB0Hlv7B/ZHOGY5bQdwMKJSzGfDfvyhnpmVxLMGnQPGNE9wgqkLUs3+gbG1Qx02S2LLfJ5GaRQ==
+
+"@rollup/[email protected]":
+  version "4.9.5"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.5.tgz#7b73a145891c202fbcc08759248983667a035d85"
+  integrity sha512-AiqiLkb9KSf7Lj/o1U3SEP9Zn+5NuVKgFdRIZkvd4N0+bYrTOovVd0+LmYCPQGbocT4kvFyK+LXCDiXPBF3fyA==
+
+"@rollup/[email protected]":
+  version "4.9.5"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.5.tgz#10491ccf4f63c814d4149e0316541476ea603602"
+  integrity sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ==
+
+"@types/[email protected]":
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
+  integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
+
+esbuild@^0.19.3:
+  version "0.19.11"
+  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.11.tgz#4a02dca031e768b5556606e1b468fe72e3325d96"
+  integrity sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==
+  optionalDependencies:
+    "@esbuild/aix-ppc64" "0.19.11"
+    "@esbuild/android-arm" "0.19.11"
+    "@esbuild/android-arm64" "0.19.11"
+    "@esbuild/android-x64" "0.19.11"
+    "@esbuild/darwin-arm64" "0.19.11"
+    "@esbuild/darwin-x64" "0.19.11"
+    "@esbuild/freebsd-arm64" "0.19.11"
+    "@esbuild/freebsd-x64" "0.19.11"
+    "@esbuild/linux-arm" "0.19.11"
+    "@esbuild/linux-arm64" "0.19.11"
+    "@esbuild/linux-ia32" "0.19.11"
+    "@esbuild/linux-loong64" "0.19.11"
+    "@esbuild/linux-mips64el" "0.19.11"
+    "@esbuild/linux-ppc64" "0.19.11"
+    "@esbuild/linux-riscv64" "0.19.11"
+    "@esbuild/linux-s390x" "0.19.11"
+    "@esbuild/linux-x64" "0.19.11"
+    "@esbuild/netbsd-x64" "0.19.11"
+    "@esbuild/openbsd-x64" "0.19.11"
+    "@esbuild/sunos-x64" "0.19.11"
+    "@esbuild/win32-arm64" "0.19.11"
+    "@esbuild/win32-ia32" "0.19.11"
+    "@esbuild/win32-x64" "0.19.11"
+
+fsevents@~2.3.2, fsevents@~2.3.3:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+  integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+"js-tokens@^3.0.0 || ^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+loose-envify@^1.1.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==
+  dependencies:
+    js-tokens "^3.0.0 || ^4.0.0"
+
+nanoid@^3.3.7:
+  version "3.3.7"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
+  integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
+
+picocolors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+postcss@^8.4.32:
+  version "8.4.33"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742"
+  integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==
+  dependencies:
+    nanoid "^3.3.7"
+    picocolors "^1.0.0"
+    source-map-js "^1.0.2"
+
[email protected]:
+  version "18.2.0"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
+  integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
+  dependencies:
+    loose-envify "^1.1.0"
+    scheduler "^0.23.0"
+
[email protected]:
+  version "18.2.0"
+  resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
+  integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
+  dependencies:
+    loose-envify "^1.1.0"
+
+rollup@^4.2.0:
+  version "4.9.5"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.9.5.tgz#62999462c90f4c8b5d7c38fc7161e63b29101b05"
+  integrity sha512-E4vQW0H/mbNMw2yLSqJyjtkHY9dslf/p0zuT1xehNRqUTBOFMqEjguDvqhXr7N7r/4ttb2jr4T41d3dncmIgbQ==
+  dependencies:
+    "@types/estree" "1.0.5"
+  optionalDependencies:
+    "@rollup/rollup-android-arm-eabi" "4.9.5"
+    "@rollup/rollup-android-arm64" "4.9.5"
+    "@rollup/rollup-darwin-arm64" "4.9.5"
+    "@rollup/rollup-darwin-x64" "4.9.5"
+    "@rollup/rollup-linux-arm-gnueabihf" "4.9.5"
+    "@rollup/rollup-linux-arm64-gnu" "4.9.5"
+    "@rollup/rollup-linux-arm64-musl" "4.9.5"
+    "@rollup/rollup-linux-riscv64-gnu" "4.9.5"
+    "@rollup/rollup-linux-x64-gnu" "4.9.5"
+    "@rollup/rollup-linux-x64-musl" "4.9.5"
+    "@rollup/rollup-win32-arm64-msvc" "4.9.5"
+    "@rollup/rollup-win32-ia32-msvc" "4.9.5"
+    "@rollup/rollup-win32-x64-msvc" "4.9.5"
+    fsevents "~2.3.2"
+
+scheduler@^0.23.0:
+  version "0.23.0"
+  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
+  integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
+  dependencies:
+    loose-envify "^1.1.0"
+
+source-map-js@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
+  integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+
[email protected]:
+  version "5.0.6"
+  resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.6.tgz#f9e13503a4c5ccd67312c67803dec921f3bdea7c"
+  integrity sha512-MD3joyAEBtV7QZPl2JVVUai6zHms3YOmLR+BpMzLlX2Yzjfcc4gTgNi09d/Rua3F4EtC8zdwPU8eQYyib4vVMQ==
+  dependencies:
+    esbuild "^0.19.3"
+    postcss "^8.4.32"
+    rollup "^4.2.0"
+  optionalDependencies:
+    fsevents "~2.3.3"

+ 900 - 0
excalidraw-app/App.tsx

@@ -0,0 +1,900 @@
+import polyfill from "../packages/excalidraw/polyfill";
+import LanguageDetector from "i18next-browser-languagedetector";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { trackEvent } from "../packages/excalidraw/analytics";
+import { getDefaultAppState } from "../packages/excalidraw/appState";
+import { ErrorDialog } from "../packages/excalidraw/components/ErrorDialog";
+import { TopErrorBoundary } from "./components/TopErrorBoundary";
+import { useMathSubtype } from "../packages/excalidraw/element/subtypes/mathjax";
+import {
+  APP_NAME,
+  EVENT,
+  THEME,
+  TITLE_TIMEOUT,
+  VERSION_TIMEOUT,
+} from "../packages/excalidraw/constants";
+import { loadFromBlob } from "../packages/excalidraw/data/blob";
+import {
+  ExcalidrawElement,
+  FileId,
+  NonDeletedExcalidrawElement,
+  Theme,
+} from "../packages/excalidraw/element/types";
+import { useCallbackRefState } from "../packages/excalidraw/hooks/useCallbackRefState";
+import { t } from "../packages/excalidraw/i18n";
+import {
+  Excalidraw,
+  defaultLang,
+  LiveCollaborationTrigger,
+  TTDDialog,
+  TTDDialogTrigger,
+} from "../packages/excalidraw/index";
+import {
+  AppState,
+  LibraryItems,
+  ExcalidrawImperativeAPI,
+  BinaryFiles,
+  ExcalidrawInitialDataState,
+  UIAppState,
+} from "../packages/excalidraw/types";
+import {
+  debounce,
+  getVersion,
+  getFrame,
+  isTestEnv,
+  preventUnload,
+  ResolvablePromise,
+  resolvablePromise,
+  isRunningInIframe,
+} from "../packages/excalidraw/utils";
+import {
+  FIREBASE_STORAGE_PREFIXES,
+  STORAGE_KEYS,
+  SYNC_BROWSER_TABS_TIMEOUT,
+} from "./app_constants";
+import Collab, {
+  CollabAPI,
+  collabAPIAtom,
+  isCollaboratingAtom,
+  isOfflineAtom,
+} from "./collab/Collab";
+import {
+  exportToBackend,
+  getCollaborationLinkData,
+  isCollaborationLink,
+  loadScene,
+} from "./data";
+import {
+  getLibraryItemsFromStorage,
+  importFromLocalStorage,
+  importUsernameFromLocalStorage,
+} from "./data/localStorage";
+import CustomStats from "./CustomStats";
+import {
+  restore,
+  restoreAppState,
+  RestoredDataState,
+} from "../packages/excalidraw/data/restore";
+import {
+  ExportToExcalidrawPlus,
+  exportToExcalidrawPlus,
+} from "./components/ExportToExcalidrawPlus";
+import { updateStaleImageStatuses } from "./data/FileManager";
+import { newElementWith } from "../packages/excalidraw/element/mutateElement";
+import { isInitializedImageElement } from "../packages/excalidraw/element/typeChecks";
+import { loadFilesFromFirebase } from "./data/firebase";
+import { LocalData } from "./data/LocalData";
+import { isBrowserStorageStateNewer } from "./data/tabSync";
+import clsx from "clsx";
+import { reconcileElements } from "./collab/reconciliation";
+import {
+  parseLibraryTokensFromUrl,
+  useHandleLibrary,
+} from "../packages/excalidraw/data/library";
+import { AppMainMenu } from "./components/AppMainMenu";
+import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
+import { AppFooter } from "./components/AppFooter";
+import { atom, Provider, useAtom, useAtomValue } from "jotai";
+import { useAtomWithInitialValue } from "../packages/excalidraw/jotai";
+import { appJotaiStore } from "./app-jotai";
+
+import "./index.scss";
+import { ResolutionType } from "../packages/excalidraw/utility-types";
+import { ShareableLinkDialog } from "../packages/excalidraw/components/ShareableLinkDialog";
+import { openConfirmModal } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
+import { OverwriteConfirmDialog } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
+import Trans from "../packages/excalidraw/components/Trans";
+import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
+
+polyfill();
+
+window.EXCALIDRAW_THROTTLE_RENDER = true;
+
+let isSelfEmbedding = false;
+
+if (window.self !== window.top) {
+  try {
+    const parentUrl = new URL(document.referrer);
+    const currentUrl = new URL(window.location.href);
+    if (parentUrl.origin === currentUrl.origin) {
+      isSelfEmbedding = true;
+    }
+  } catch (error) {
+    // ignore
+  }
+}
+
+const languageDetector = new LanguageDetector();
+languageDetector.init({
+  languageUtils: {},
+});
+
+const shareableLinkConfirmDialog = {
+  title: t("overwriteConfirm.modal.shareableLink.title"),
+  description: (
+    <Trans
+      i18nKey="overwriteConfirm.modal.shareableLink.description"
+      bold={(text) => <strong>{text}</strong>}
+      br={() => <br />}
+    />
+  ),
+  actionLabel: t("overwriteConfirm.modal.shareableLink.button"),
+  color: "danger",
+} as const;
+
+const initializeScene = async (opts: {
+  collabAPI: CollabAPI | null;
+  excalidrawAPI: ExcalidrawImperativeAPI;
+}): Promise<
+  { scene: ExcalidrawInitialDataState | null } & (
+    | { isExternalScene: true; id: string; key: string }
+    | { isExternalScene: false; id?: null; key?: null }
+  )
+> => {
+  const searchParams = new URLSearchParams(window.location.search);
+  const id = searchParams.get("id");
+  const jsonBackendMatch = window.location.hash.match(
+    /^#json=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/,
+  );
+  const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/);
+
+  const localDataState = importFromLocalStorage();
+
+  let scene: RestoredDataState & {
+    scrollToContent?: boolean;
+  } = await loadScene(null, null, localDataState);
+
+  let roomLinkData = getCollaborationLinkData(window.location.href);
+  const isExternalScene = !!(id || jsonBackendMatch || roomLinkData);
+  if (isExternalScene) {
+    if (
+      // don't prompt if scene is empty
+      !scene.elements.length ||
+      // don't prompt for collab scenes because we don't override local storage
+      roomLinkData ||
+      // otherwise, prompt whether user wants to override current scene
+      (await openConfirmModal(shareableLinkConfirmDialog))
+    ) {
+      if (jsonBackendMatch) {
+        scene = await loadScene(
+          jsonBackendMatch[1],
+          jsonBackendMatch[2],
+          localDataState,
+        );
+      }
+      scene.scrollToContent = true;
+      if (!roomLinkData) {
+        window.history.replaceState({}, APP_NAME, window.location.origin);
+      }
+    } else {
+      // https://github.com/excalidraw/excalidraw/issues/1919
+      if (document.hidden) {
+        return new Promise((resolve, reject) => {
+          window.addEventListener(
+            "focus",
+            () => initializeScene(opts).then(resolve).catch(reject),
+            {
+              once: true,
+            },
+          );
+        });
+      }
+
+      roomLinkData = null;
+      window.history.replaceState({}, APP_NAME, window.location.origin);
+    }
+  } else if (externalUrlMatch) {
+    window.history.replaceState({}, APP_NAME, window.location.origin);
+
+    const url = externalUrlMatch[1];
+    try {
+      const request = await fetch(window.decodeURIComponent(url));
+      const data = await loadFromBlob(await request.blob(), null, null);
+      if (
+        !scene.elements.length ||
+        (await openConfirmModal(shareableLinkConfirmDialog))
+      ) {
+        return { scene: data, isExternalScene };
+      }
+    } catch (error: any) {
+      return {
+        scene: {
+          appState: {
+            errorMessage: t("alerts.invalidSceneUrl"),
+          },
+        },
+        isExternalScene,
+      };
+    }
+  }
+
+  if (roomLinkData && opts.collabAPI) {
+    const { excalidrawAPI } = opts;
+
+    const scene = await opts.collabAPI.startCollaboration(roomLinkData);
+
+    return {
+      // when collaborating, the state may have already been updated at this
+      // point (we may have received updates from other clients), so reconcile
+      // elements and appState with existing state
+      scene: {
+        ...scene,
+        appState: {
+          ...restoreAppState(
+            {
+              ...scene?.appState,
+              theme: localDataState?.appState?.theme || scene?.appState?.theme,
+            },
+            excalidrawAPI.getAppState(),
+          ),
+          // necessary if we're invoking from a hashchange handler which doesn't
+          // go through App.initializeScene() that resets this flag
+          isLoading: false,
+        },
+        elements: reconcileElements(
+          scene?.elements || [],
+          excalidrawAPI.getSceneElementsIncludingDeleted(),
+          excalidrawAPI.getAppState(),
+        ),
+      },
+      isExternalScene: true,
+      id: roomLinkData.roomId,
+      key: roomLinkData.roomKey,
+    };
+  } else if (scene) {
+    return isExternalScene && jsonBackendMatch
+      ? {
+          scene,
+          isExternalScene,
+          id: jsonBackendMatch[1],
+          key: jsonBackendMatch[2],
+        }
+      : { scene, isExternalScene: false };
+  }
+  return { scene: null, isExternalScene: false };
+};
+
+const detectedLangCode = languageDetector.detect() || defaultLang.code;
+export const appLangCodeAtom = atom(
+  Array.isArray(detectedLangCode) ? detectedLangCode[0] : detectedLangCode,
+);
+
+const ExcalidrawWrapper = () => {
+  const [errorMessage, setErrorMessage] = useState("");
+  const [langCode, setLangCode] = useAtom(appLangCodeAtom);
+  const isCollabDisabled = isRunningInIframe();
+
+  // initial state
+  // ---------------------------------------------------------------------------
+
+  const initialStatePromiseRef = useRef<{
+    promise: ResolvablePromise<ExcalidrawInitialDataState | null>;
+  }>({ promise: null! });
+  if (!initialStatePromiseRef.current.promise) {
+    initialStatePromiseRef.current.promise =
+      resolvablePromise<ExcalidrawInitialDataState | null>();
+  }
+
+  useEffect(() => {
+    trackEvent("load", "frame", getFrame());
+    // Delayed so that the app has a time to load the latest SW
+    setTimeout(() => {
+      trackEvent("load", "version", getVersion());
+    }, VERSION_TIMEOUT);
+  }, []);
+
+  const [excalidrawAPI, excalidrawRefCallback] =
+    useCallbackRefState<ExcalidrawImperativeAPI>();
+
+  useMathSubtype(excalidrawAPI);
+
+  const [, setShareDialogState] = useAtom(shareDialogStateAtom);
+  const [collabAPI] = useAtom(collabAPIAtom);
+  const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
+    return isCollaborationLink(window.location.href);
+  });
+
+  useHandleLibrary({
+    excalidrawAPI,
+    getInitialLibraryItems: getLibraryItemsFromStorage,
+  });
+
+  useEffect(() => {
+    if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) {
+      return;
+    }
+
+    const loadImages = (
+      data: ResolutionType<typeof initializeScene>,
+      isInitialLoad = false,
+    ) => {
+      if (!data.scene) {
+        return;
+      }
+      if (collabAPI?.isCollaborating()) {
+        if (data.scene.elements) {
+          collabAPI
+            .fetchImageFilesFromFirebase({
+              elements: data.scene.elements,
+              forceFetchFiles: true,
+            })
+            .then(({ loadedFiles, erroredFiles }) => {
+              excalidrawAPI.addFiles(loadedFiles);
+              updateStaleImageStatuses({
+                excalidrawAPI,
+                erroredFiles,
+                elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
+              });
+            });
+        }
+      } else {
+        const fileIds =
+          data.scene.elements?.reduce((acc, element) => {
+            if (isInitializedImageElement(element)) {
+              return acc.concat(element.fileId);
+            }
+            return acc;
+          }, [] as FileId[]) || [];
+
+        if (data.isExternalScene) {
+          loadFilesFromFirebase(
+            `${FIREBASE_STORAGE_PREFIXES.shareLinkFiles}/${data.id}`,
+            data.key,
+            fileIds,
+          ).then(({ loadedFiles, erroredFiles }) => {
+            excalidrawAPI.addFiles(loadedFiles);
+            updateStaleImageStatuses({
+              excalidrawAPI,
+              erroredFiles,
+              elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
+            });
+          });
+        } else if (isInitialLoad) {
+          if (fileIds.length) {
+            LocalData.fileStorage
+              .getFiles(fileIds)
+              .then(({ loadedFiles, erroredFiles }) => {
+                if (loadedFiles.length) {
+                  excalidrawAPI.addFiles(loadedFiles);
+                }
+                updateStaleImageStatuses({
+                  excalidrawAPI,
+                  erroredFiles,
+                  elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
+                });
+              });
+          }
+          // on fresh load, clear unused files from IDB (from previous
+          // session)
+          LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds });
+        }
+      }
+    };
+
+    initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => {
+      loadImages(data, /* isInitialLoad */ true);
+      initialStatePromiseRef.current.promise.resolve(data.scene);
+    });
+
+    const onHashChange = async (event: HashChangeEvent) => {
+      event.preventDefault();
+      const libraryUrlTokens = parseLibraryTokensFromUrl();
+      if (!libraryUrlTokens) {
+        if (
+          collabAPI?.isCollaborating() &&
+          !isCollaborationLink(window.location.href)
+        ) {
+          collabAPI.stopCollaboration(false);
+        }
+        excalidrawAPI.updateScene({ appState: { isLoading: true } });
+
+        initializeScene({ collabAPI, excalidrawAPI }).then((data) => {
+          loadImages(data);
+          if (data.scene) {
+            excalidrawAPI.updateScene({
+              ...data.scene,
+              ...restore(data.scene, null, null, { repairBindings: true }),
+              commitToHistory: true,
+            });
+          }
+        });
+      }
+    };
+
+    const titleTimeout = setTimeout(
+      () => (document.title = APP_NAME),
+      TITLE_TIMEOUT,
+    );
+
+    const syncData = debounce(() => {
+      if (isTestEnv()) {
+        return;
+      }
+      if (
+        !document.hidden &&
+        ((collabAPI && !collabAPI.isCollaborating()) || isCollabDisabled)
+      ) {
+        // don't sync if local state is newer or identical to browser state
+        if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
+          const localDataState = importFromLocalStorage();
+          const username = importUsernameFromLocalStorage();
+          let langCode = languageDetector.detect() || defaultLang.code;
+          if (Array.isArray(langCode)) {
+            langCode = langCode[0];
+          }
+          setLangCode(langCode);
+          excalidrawAPI.updateScene({
+            ...localDataState,
+          });
+          excalidrawAPI.updateLibrary({
+            libraryItems: getLibraryItemsFromStorage(),
+          });
+          collabAPI?.setUsername(username || "");
+        }
+
+        if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) {
+          const elements = excalidrawAPI.getSceneElementsIncludingDeleted();
+          const currFiles = excalidrawAPI.getFiles();
+          const fileIds =
+            elements?.reduce((acc, element) => {
+              if (
+                isInitializedImageElement(element) &&
+                // only load and update images that aren't already loaded
+                !currFiles[element.fileId]
+              ) {
+                return acc.concat(element.fileId);
+              }
+              return acc;
+            }, [] as FileId[]) || [];
+          if (fileIds.length) {
+            LocalData.fileStorage
+              .getFiles(fileIds)
+              .then(({ loadedFiles, erroredFiles }) => {
+                if (loadedFiles.length) {
+                  excalidrawAPI.addFiles(loadedFiles);
+                }
+                updateStaleImageStatuses({
+                  excalidrawAPI,
+                  erroredFiles,
+                  elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
+                });
+              });
+          }
+        }
+      }
+    }, SYNC_BROWSER_TABS_TIMEOUT);
+
+    const onUnload = () => {
+      LocalData.flushSave();
+    };
+
+    const visibilityChange = (event: FocusEvent | Event) => {
+      if (event.type === EVENT.BLUR || document.hidden) {
+        LocalData.flushSave();
+      }
+      if (
+        event.type === EVENT.VISIBILITY_CHANGE ||
+        event.type === EVENT.FOCUS
+      ) {
+        syncData();
+      }
+    };
+
+    window.addEventListener(EVENT.HASHCHANGE, onHashChange, false);
+    window.addEventListener(EVENT.UNLOAD, onUnload, false);
+    window.addEventListener(EVENT.BLUR, visibilityChange, false);
+    document.addEventListener(EVENT.VISIBILITY_CHANGE, visibilityChange, false);
+    window.addEventListener(EVENT.FOCUS, visibilityChange, false);
+    return () => {
+      window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false);
+      window.removeEventListener(EVENT.UNLOAD, onUnload, false);
+      window.removeEventListener(EVENT.BLUR, visibilityChange, false);
+      window.removeEventListener(EVENT.FOCUS, visibilityChange, false);
+      document.removeEventListener(
+        EVENT.VISIBILITY_CHANGE,
+        visibilityChange,
+        false,
+      );
+      clearTimeout(titleTimeout);
+    };
+  }, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode]);
+
+  useEffect(() => {
+    const unloadHandler = (event: BeforeUnloadEvent) => {
+      LocalData.flushSave();
+
+      if (
+        excalidrawAPI &&
+        LocalData.fileStorage.shouldPreventUnload(
+          excalidrawAPI.getSceneElements(),
+        )
+      ) {
+        preventUnload(event);
+      }
+    };
+    window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
+    return () => {
+      window.removeEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
+    };
+  }, [excalidrawAPI]);
+
+  useEffect(() => {
+    languageDetector.cacheUserLanguage(langCode);
+  }, [langCode]);
+
+  const [theme, setTheme] = useState<Theme>(
+    () =>
+      (localStorage.getItem(
+        STORAGE_KEYS.LOCAL_STORAGE_THEME,
+      ) as Theme | null) ||
+      // FIXME migration from old LS scheme. Can be removed later. #5660
+      importFromLocalStorage().appState?.theme ||
+      THEME.LIGHT,
+  );
+
+  useEffect(() => {
+    localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_THEME, theme);
+    // currently only used for body styling during init (see public/index.html),
+    // but may change in the future
+    document.documentElement.classList.toggle("dark", theme === THEME.DARK);
+  }, [theme]);
+
+  const onChange = (
+    elements: readonly ExcalidrawElement[],
+    appState: AppState,
+    files: BinaryFiles,
+  ) => {
+    if (collabAPI?.isCollaborating()) {
+      collabAPI.syncElements(elements);
+    }
+
+    setTheme(appState.theme);
+
+    // this check is redundant, but since this is a hot path, it's best
+    // not to evaludate the nested expression every time
+    if (!LocalData.isSavePaused()) {
+      LocalData.save(elements, appState, files, () => {
+        if (excalidrawAPI) {
+          let didChange = false;
+
+          const elements = excalidrawAPI
+            .getSceneElementsIncludingDeleted()
+            .map((element) => {
+              if (
+                LocalData.fileStorage.shouldUpdateImageElementStatus(element)
+              ) {
+                const newElement = newElementWith(element, { status: "saved" });
+                if (newElement !== element) {
+                  didChange = true;
+                }
+                return newElement;
+              }
+              return element;
+            });
+
+          if (didChange) {
+            excalidrawAPI.updateScene({
+              elements,
+            });
+          }
+        }
+      });
+    }
+  };
+
+  const [latestShareableLink, setLatestShareableLink] = useState<string | null>(
+    null,
+  );
+
+  const onExportToBackend = async (
+    exportedElements: readonly NonDeletedExcalidrawElement[],
+    appState: Partial<AppState>,
+    files: BinaryFiles,
+  ) => {
+    if (exportedElements.length === 0) {
+      throw new Error(t("alerts.cannotExportEmptyCanvas"));
+    }
+    try {
+      const { url, errorMessage } = await exportToBackend(
+        exportedElements,
+        {
+          ...appState,
+          viewBackgroundColor: appState.exportBackground
+            ? appState.viewBackgroundColor
+            : getDefaultAppState().viewBackgroundColor,
+        },
+        files,
+      );
+
+      if (errorMessage) {
+        throw new Error(errorMessage);
+      }
+
+      if (url) {
+        setLatestShareableLink(url);
+      }
+    } catch (error: any) {
+      if (error.name !== "AbortError") {
+        const { width, height } = appState;
+        console.error(error, {
+          width,
+          height,
+          devicePixelRatio: window.devicePixelRatio,
+        });
+        throw new Error(error.message);
+      }
+    }
+  };
+
+  const renderCustomStats = (
+    elements: readonly NonDeletedExcalidrawElement[],
+    appState: UIAppState,
+  ) => {
+    return (
+      <CustomStats
+        setToast={(message) => excalidrawAPI!.setToast({ message })}
+        appState={appState}
+        elements={elements}
+      />
+    );
+  };
+
+  const onLibraryChange = async (items: LibraryItems) => {
+    if (!items.length) {
+      localStorage.removeItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
+      return;
+    }
+    const serializedItems = JSON.stringify(items);
+    localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
+  };
+
+  const isOffline = useAtomValue(isOfflineAtom);
+
+  const onCollabDialogOpen = useCallback(
+    () => setShareDialogState({ isOpen: true, type: "collaborationOnly" }),
+    [setShareDialogState],
+  );
+
+  // browsers generally prevent infinite self-embedding, there are
+  // cases where it still happens, and while we disallow self-embedding
+  // by not whitelisting our own origin, this serves as an additional guard
+  if (isSelfEmbedding) {
+    return (
+      <div
+        style={{
+          display: "flex",
+          alignItems: "center",
+          justifyContent: "center",
+          textAlign: "center",
+          height: "100%",
+        }}
+      >
+        <h1>I'm not a pretzel!</h1>
+      </div>
+    );
+  }
+
+  return (
+    <div
+      style={{ height: "100%" }}
+      className={clsx("excalidraw-app", {
+        "is-collaborating": isCollaborating,
+      })}
+    >
+      <Excalidraw
+        excalidrawAPI={excalidrawRefCallback}
+        onChange={onChange}
+        initialData={initialStatePromiseRef.current.promise}
+        isCollaborating={isCollaborating}
+        onPointerUpdate={collabAPI?.onPointerUpdate}
+        UIOptions={{
+          canvasActions: {
+            toggleTheme: true,
+            export: {
+              onExportToBackend,
+              renderCustomUI: (elements, appState, files) => {
+                return (
+                  <ExportToExcalidrawPlus
+                    elements={elements}
+                    appState={appState}
+                    files={files}
+                    onError={(error) => {
+                      excalidrawAPI?.updateScene({
+                        appState: {
+                          errorMessage: error.message,
+                        },
+                      });
+                    }}
+                    onSuccess={() => {
+                      excalidrawAPI?.updateScene({
+                        appState: { openDialog: null },
+                      });
+                    }}
+                  />
+                );
+              },
+            },
+          },
+        }}
+        langCode={langCode}
+        renderCustomStats={renderCustomStats}
+        detectScroll={false}
+        handleKeyboardGlobally={true}
+        onLibraryChange={onLibraryChange}
+        autoFocus={true}
+        theme={theme}
+        renderTopRightUI={(isMobile) => {
+          if (isMobile || !collabAPI || isCollabDisabled) {
+            return null;
+          }
+          return (
+            <LiveCollaborationTrigger
+              isCollaborating={isCollaborating}
+              onSelect={() =>
+                setShareDialogState({ isOpen: true, type: "share" })
+              }
+            />
+          );
+        }}
+      >
+        <AppMainMenu
+          onCollabDialogOpen={onCollabDialogOpen}
+          isCollaborating={isCollaborating}
+          isCollabEnabled={!isCollabDisabled}
+        />
+        <AppWelcomeScreen
+          onCollabDialogOpen={onCollabDialogOpen}
+          isCollabEnabled={!isCollabDisabled}
+        />
+        <OverwriteConfirmDialog>
+          <OverwriteConfirmDialog.Actions.ExportToImage />
+          <OverwriteConfirmDialog.Actions.SaveToDisk />
+          {excalidrawAPI && (
+            <OverwriteConfirmDialog.Action
+              title={t("overwriteConfirm.action.excalidrawPlus.title")}
+              actionLabel={t("overwriteConfirm.action.excalidrawPlus.button")}
+              onClick={() => {
+                exportToExcalidrawPlus(
+                  excalidrawAPI.getSceneElements(),
+                  excalidrawAPI.getAppState(),
+                  excalidrawAPI.getFiles(),
+                );
+              }}
+            >
+              {t("overwriteConfirm.action.excalidrawPlus.description")}
+            </OverwriteConfirmDialog.Action>
+          )}
+        </OverwriteConfirmDialog>
+        <AppFooter />
+        <TTDDialog
+          onTextSubmit={async (input) => {
+            try {
+              const response = await fetch(
+                `${
+                  import.meta.env.VITE_APP_AI_BACKEND
+                }/v1/ai/text-to-diagram/generate`,
+                {
+                  method: "POST",
+                  headers: {
+                    Accept: "application/json",
+                    "Content-Type": "application/json",
+                  },
+                  body: JSON.stringify({ prompt: input }),
+                },
+              );
+
+              const rateLimit = response.headers.has("X-Ratelimit-Limit")
+                ? parseInt(response.headers.get("X-Ratelimit-Limit") || "0", 10)
+                : undefined;
+
+              const rateLimitRemaining = response.headers.has(
+                "X-Ratelimit-Remaining",
+              )
+                ? parseInt(
+                    response.headers.get("X-Ratelimit-Remaining") || "0",
+                    10,
+                  )
+                : undefined;
+
+              const json = await response.json();
+
+              if (!response.ok) {
+                if (response.status === 429) {
+                  return {
+                    rateLimit,
+                    rateLimitRemaining,
+                    error: new Error(
+                      "Too many requests today, please try again tomorrow!",
+                    ),
+                  };
+                }
+
+                throw new Error(json.message || "Generation failed...");
+              }
+
+              const generatedResponse = json.generatedResponse;
+              if (!generatedResponse) {
+                throw new Error("Generation failed...");
+              }
+
+              return { generatedResponse, rateLimit, rateLimitRemaining };
+            } catch (err: any) {
+              throw new Error("Request failed");
+            }
+          }}
+        />
+        <TTDDialogTrigger />
+        {isCollaborating && isOffline && (
+          <div className="collab-offline-warning">
+            {t("alerts.collabOfflineWarning")}
+          </div>
+        )}
+        {latestShareableLink && (
+          <ShareableLinkDialog
+            link={latestShareableLink}
+            onCloseRequest={() => setLatestShareableLink(null)}
+            setErrorMessage={setErrorMessage}
+          />
+        )}
+        {excalidrawAPI && !isCollabDisabled && (
+          <Collab excalidrawAPI={excalidrawAPI} />
+        )}
+
+        <ShareDialog
+          collabAPI={collabAPI}
+          onExportToBackend={async () => {
+            if (excalidrawAPI) {
+              try {
+                await onExportToBackend(
+                  excalidrawAPI.getSceneElements(),
+                  excalidrawAPI.getAppState(),
+                  excalidrawAPI.getFiles(),
+                );
+              } catch (error: any) {
+                setErrorMessage(error.message);
+              }
+            }
+          }}
+        />
+
+        {errorMessage && (
+          <ErrorDialog onClose={() => setErrorMessage("")}>
+            {errorMessage}
+          </ErrorDialog>
+        )}
+      </Excalidraw>
+    </div>
+  );
+};
+
+const ExcalidrawApp = () => {
+  return (
+    <TopErrorBoundary>
+      <Provider unstable_createStore={() => appJotaiStore}>
+        <ExcalidrawWrapper />
+      </Provider>
+    </TopErrorBoundary>
+  );
+};
+
+export default ExcalidrawApp;

+ 6 - 6
excalidraw-app/CustomStats.tsx

@@ -1,14 +1,14 @@
 import { useEffect, useState } from "react";
 import { useEffect, useState } from "react";
-import { debounce, getVersion, nFormatter } from "../src/utils";
+import { debounce, getVersion, nFormatter } from "../packages/excalidraw/utils";
 import {
 import {
   getElementsStorageSize,
   getElementsStorageSize,
   getTotalStorageSize,
   getTotalStorageSize,
 } from "./data/localStorage";
 } from "./data/localStorage";
-import { DEFAULT_VERSION } from "../src/constants";
-import { t } from "../src/i18n";
-import { copyTextToSystemClipboard } from "../src/clipboard";
-import { NonDeletedExcalidrawElement } from "../src/element/types";
-import { UIAppState } from "../src/types";
+import { DEFAULT_VERSION } from "../packages/excalidraw/constants";
+import { t } from "../packages/excalidraw/i18n";
+import { copyTextToSystemClipboard } from "../packages/excalidraw/clipboard";
+import { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types";
+import { UIAppState } from "../packages/excalidraw/types";
 
 
 type StorageSizes = { scene: number; total: number };
 type StorageSizes = { scene: number; total: number };
 
 

+ 8 - 2
excalidraw-app/app_constants.ts

@@ -15,11 +15,17 @@ export const FILE_CACHE_MAX_AGE_SEC = 31536000;
 export const WS_EVENTS = {
 export const WS_EVENTS = {
   SERVER_VOLATILE: "server-volatile-broadcast",
   SERVER_VOLATILE: "server-volatile-broadcast",
   SERVER: "server-broadcast",
   SERVER: "server-broadcast",
-};
+  USER_FOLLOW_CHANGE: "user-follow",
+  USER_FOLLOW_ROOM_CHANGE: "user-follow-room-change",
+} as const;
 
 
-export enum WS_SCENE_EVENT_TYPES {
+export enum WS_SUBTYPES {
+  INVALID_RESPONSE = "INVALID_RESPONSE",
   INIT = "SCENE_INIT",
   INIT = "SCENE_INIT",
   UPDATE = "SCENE_UPDATE",
   UPDATE = "SCENE_UPDATE",
+  MOUSE_LOCATION = "MOUSE_LOCATION",
+  IDLE_STATUS = "IDLE_STATUS",
+  USER_VISIBLE_SCENE_BOUNDS = "USER_VISIBLE_SCENE_BOUNDS",
 }
 }
 
 
 export const FIREBASE_STORAGE_PREFIXES = {
 export const FIREBASE_STORAGE_PREFIXES = {

+ 0 - 0
src/bug-issue-template.js → excalidraw-app/bug-issue-template.js


+ 192 - 106
excalidraw-app/collab/Collab.tsx

@@ -1,36 +1,41 @@
 import throttle from "lodash.throttle";
 import throttle from "lodash.throttle";
 import { PureComponent } from "react";
 import { PureComponent } from "react";
-import { ExcalidrawImperativeAPI } from "../../src/types";
-import { ErrorDialog } from "../../src/components/ErrorDialog";
-import { APP_NAME, ENV, EVENT } from "../../src/constants";
-import { ImportedDataState } from "../../src/data/types";
+import {
+  ExcalidrawImperativeAPI,
+  SocketId,
+} from "../../packages/excalidraw/types";
+import { ErrorDialog } from "../../packages/excalidraw/components/ErrorDialog";
+import { APP_NAME, ENV, EVENT } from "../../packages/excalidraw/constants";
+import { ImportedDataState } from "../../packages/excalidraw/data/types";
 import {
 import {
   ExcalidrawElement,
   ExcalidrawElement,
   InitializedExcalidrawImageElement,
   InitializedExcalidrawImageElement,
-} from "../../src/element/types";
+} from "../../packages/excalidraw/element/types";
 import {
 import {
   getSceneVersion,
   getSceneVersion,
   restoreElements,
   restoreElements,
-} from "../../src/packages/excalidraw/index";
-import { Collaborator, Gesture } from "../../src/types";
+  zoomToFitBounds,
+} from "../../packages/excalidraw/index";
+import { Collaborator, Gesture } from "../../packages/excalidraw/types";
 import {
 import {
+  assertNever,
   preventUnload,
   preventUnload,
   resolvablePromise,
   resolvablePromise,
-  withBatchedUpdates,
-} from "../../src/utils";
+  throttleRAF,
+} from "../../packages/excalidraw/utils";
 import {
 import {
   CURSOR_SYNC_TIMEOUT,
   CURSOR_SYNC_TIMEOUT,
   FILE_UPLOAD_MAX_BYTES,
   FILE_UPLOAD_MAX_BYTES,
   FIREBASE_STORAGE_PREFIXES,
   FIREBASE_STORAGE_PREFIXES,
   INITIAL_SCENE_UPDATE_TIMEOUT,
   INITIAL_SCENE_UPDATE_TIMEOUT,
   LOAD_IMAGES_TIMEOUT,
   LOAD_IMAGES_TIMEOUT,
-  WS_SCENE_EVENT_TYPES,
+  WS_SUBTYPES,
   SYNC_FULL_SCENE_INTERVAL_MS,
   SYNC_FULL_SCENE_INTERVAL_MS,
+  WS_EVENTS,
 } from "../app_constants";
 } from "../app_constants";
 import {
 import {
   generateCollaborationLinkData,
   generateCollaborationLinkData,
   getCollaborationLink,
   getCollaborationLink,
-  getCollabServer,
   getSyncableElements,
   getSyncableElements,
   SocketUpdateDataSource,
   SocketUpdateDataSource,
   SyncableExcalidrawElement,
   SyncableExcalidrawElement,
@@ -47,42 +52,48 @@ import {
   saveUsernameToLocalStorage,
   saveUsernameToLocalStorage,
 } from "../data/localStorage";
 } from "../data/localStorage";
 import Portal from "./Portal";
 import Portal from "./Portal";
-import RoomDialog from "./RoomDialog";
-import { t } from "../../src/i18n";
-import { UserIdleState } from "../../src/types";
-import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../src/constants";
+import { t } from "../../packages/excalidraw/i18n";
+import { UserIdleState } from "../../packages/excalidraw/types";
+import {
+  IDLE_THRESHOLD,
+  ACTIVE_THRESHOLD,
+} from "../../packages/excalidraw/constants";
 import {
 import {
   encodeFilesForUpload,
   encodeFilesForUpload,
   FileManager,
   FileManager,
   updateStaleImageStatuses,
   updateStaleImageStatuses,
 } from "../data/FileManager";
 } from "../data/FileManager";
-import { AbortError } from "../../src/errors";
+import { AbortError } from "../../packages/excalidraw/errors";
 import {
 import {
   isImageElement,
   isImageElement,
   isInitializedImageElement,
   isInitializedImageElement,
-} from "../../src/element/typeChecks";
-import { newElementWith } from "../../src/element/mutateElement";
+} from "../../packages/excalidraw/element/typeChecks";
+import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
 import {
 import {
   ReconciledElements,
   ReconciledElements,
   reconcileElements as _reconcileElements,
   reconcileElements as _reconcileElements,
 } from "./reconciliation";
 } from "./reconciliation";
-import { decryptData } from "../../src/data/encryption";
+import { decryptData } from "../../packages/excalidraw/data/encryption";
 import { resetBrowserStateVersions } from "../data/tabSync";
 import { resetBrowserStateVersions } from "../data/tabSync";
 import { LocalData } from "../data/LocalData";
 import { LocalData } from "../data/LocalData";
-import { atom, useAtom } from "jotai";
+import { atom } from "jotai";
 import { appJotaiStore } from "../app-jotai";
 import { appJotaiStore } from "../app-jotai";
+import { Mutable, ValueOf } from "../../packages/excalidraw/utility-types";
+import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds";
+import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils";
 
 
 export const collabAPIAtom = atom<CollabAPI | null>(null);
 export const collabAPIAtom = atom<CollabAPI | null>(null);
-export const collabDialogShownAtom = atom(false);
 export const isCollaboratingAtom = atom(false);
 export const isCollaboratingAtom = atom(false);
 export const isOfflineAtom = atom(false);
 export const isOfflineAtom = atom(false);
 
 
 interface CollabState {
 interface CollabState {
-  errorMessage: string;
+  errorMessage: string | null;
   username: string;
   username: string;
-  activeRoomLink: string;
+  activeRoomLink: string | null;
 }
 }
 
 
+export const activeRoomLinkAtom = atom<string | null>(null);
+
 type CollabInstance = InstanceType<typeof Collab>;
 type CollabInstance = InstanceType<typeof Collab>;
 
 
 export interface CollabAPI {
 export interface CollabAPI {
@@ -93,32 +104,33 @@ export interface CollabAPI {
   stopCollaboration: CollabInstance["stopCollaboration"];
   stopCollaboration: CollabInstance["stopCollaboration"];
   syncElements: CollabInstance["syncElements"];
   syncElements: CollabInstance["syncElements"];
   fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
   fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
-  setUsername: (username: string) => void;
+  setUsername: CollabInstance["setUsername"];
+  getUsername: CollabInstance["getUsername"];
+  getActiveRoomLink: CollabInstance["getActiveRoomLink"];
+  setErrorMessage: CollabInstance["setErrorMessage"];
 }
 }
 
 
-interface PublicProps {
+interface CollabProps {
   excalidrawAPI: ExcalidrawImperativeAPI;
   excalidrawAPI: ExcalidrawImperativeAPI;
 }
 }
 
 
-type Props = PublicProps & { modalIsShown: boolean };
-
-class Collab extends PureComponent<Props, CollabState> {
+class Collab extends PureComponent<CollabProps, CollabState> {
   portal: Portal;
   portal: Portal;
   fileManager: FileManager;
   fileManager: FileManager;
-  excalidrawAPI: Props["excalidrawAPI"];
+  excalidrawAPI: CollabProps["excalidrawAPI"];
   activeIntervalId: number | null;
   activeIntervalId: number | null;
   idleTimeoutId: number | null;
   idleTimeoutId: number | null;
 
 
   private socketInitializationTimer?: number;
   private socketInitializationTimer?: number;
   private lastBroadcastedOrReceivedSceneVersion: number = -1;
   private lastBroadcastedOrReceivedSceneVersion: number = -1;
-  private collaborators = new Map<string, Collaborator>();
+  private collaborators = new Map<SocketId, Collaborator>();
 
 
-  constructor(props: Props) {
+  constructor(props: CollabProps) {
     super(props);
     super(props);
     this.state = {
     this.state = {
-      errorMessage: "",
+      errorMessage: null,
       username: importUsernameFromLocalStorage() || "",
       username: importUsernameFromLocalStorage() || "",
-      activeRoomLink: "",
+      activeRoomLink: null,
     };
     };
     this.portal = new Portal(this);
     this.portal = new Portal(this);
     this.fileManager = new FileManager({
     this.fileManager = new FileManager({
@@ -151,12 +163,28 @@ class Collab extends PureComponent<Props, CollabState> {
     this.idleTimeoutId = null;
     this.idleTimeoutId = null;
   }
   }
 
 
+  private onUmmount: (() => void) | null = null;
+
   componentDidMount() {
   componentDidMount() {
     window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
     window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
     window.addEventListener("online", this.onOfflineStatusToggle);
     window.addEventListener("online", this.onOfflineStatusToggle);
     window.addEventListener("offline", this.onOfflineStatusToggle);
     window.addEventListener("offline", this.onOfflineStatusToggle);
     window.addEventListener(EVENT.UNLOAD, this.onUnload);
     window.addEventListener(EVENT.UNLOAD, this.onUnload);
 
 
+    const unsubOnUserFollow = this.excalidrawAPI.onUserFollow((payload) => {
+      this.portal.socket && this.portal.broadcastUserFollowed(payload);
+    });
+    const throttledRelayUserViewportBounds = throttleRAF(
+      this.relayVisibleSceneBounds,
+    );
+    const unsubOnScrollChange = this.excalidrawAPI.onScrollChange(() =>
+      throttledRelayUserViewportBounds(),
+    );
+    this.onUmmount = () => {
+      unsubOnUserFollow();
+      unsubOnScrollChange();
+    };
+
     this.onOfflineStatusToggle();
     this.onOfflineStatusToggle();
 
 
     const collabAPI: CollabAPI = {
     const collabAPI: CollabAPI = {
@@ -167,6 +195,9 @@ class Collab extends PureComponent<Props, CollabState> {
       fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase,
       fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase,
       stopCollaboration: this.stopCollaboration,
       stopCollaboration: this.stopCollaboration,
       setUsername: this.setUsername,
       setUsername: this.setUsername,
+      getUsername: this.getUsername,
+      getActiveRoomLink: this.getActiveRoomLink,
+      setErrorMessage: this.setErrorMessage,
     };
     };
 
 
     appJotaiStore.set(collabAPIAtom, collabAPI);
     appJotaiStore.set(collabAPIAtom, collabAPI);
@@ -204,6 +235,7 @@ class Collab extends PureComponent<Props, CollabState> {
       window.clearTimeout(this.idleTimeoutId);
       window.clearTimeout(this.idleTimeoutId);
       this.idleTimeoutId = null;
       this.idleTimeoutId = null;
     }
     }
+    this.onUmmount?.();
   }
   }
 
 
   isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!;
   isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!;
@@ -313,9 +345,7 @@ class Collab extends PureComponent<Props, CollabState> {
     this.fileManager.reset();
     this.fileManager.reset();
     if (!opts?.isUnload) {
     if (!opts?.isUnload) {
       this.setIsCollaborating(false);
       this.setIsCollaborating(false);
-      this.setState({
-        activeRoomLink: "",
-      });
+      this.setActiveRoomLink(null);
       this.collaborators = new Map();
       this.collaborators = new Map();
       this.excalidrawAPI.updateScene({
       this.excalidrawAPI.updateScene({
         collaborators: this.collaborators,
         collaborators: this.collaborators,
@@ -356,7 +386,7 @@ class Collab extends PureComponent<Props, CollabState> {
     iv: Uint8Array,
     iv: Uint8Array,
     encryptedData: ArrayBuffer,
     encryptedData: ArrayBuffer,
     decryptionKey: string,
     decryptionKey: string,
-  ) => {
+  ): Promise<ValueOf<SocketUpdateDataSource>> => {
     try {
     try {
       const decrypted = await decryptData(iv, encryptedData, decryptionKey);
       const decrypted = await decryptData(iv, encryptedData, decryptionKey);
 
 
@@ -368,7 +398,7 @@ class Collab extends PureComponent<Props, CollabState> {
       window.alert(t("alerts.decryptFailed"));
       window.alert(t("alerts.decryptFailed"));
       console.error(error);
       console.error(error);
       return {
       return {
-        type: "INVALID_RESPONSE",
+        type: WS_SUBTYPES.INVALID_RESPONSE,
       };
       };
     }
     }
   };
   };
@@ -381,7 +411,7 @@ class Collab extends PureComponent<Props, CollabState> {
     if (!this.state.username) {
     if (!this.state.username) {
       import("@excalidraw/random-username").then(({ getRandomUsername }) => {
       import("@excalidraw/random-username").then(({ getRandomUsername }) => {
         const username = getRandomUsername();
         const username = getRandomUsername();
-        this.onUsernameChange(username);
+        this.setUsername(username);
       });
       });
     }
     }
 
 
@@ -423,13 +453,9 @@ class Collab extends PureComponent<Props, CollabState> {
     this.fallbackInitializationHandler = fallbackInitializationHandler;
     this.fallbackInitializationHandler = fallbackInitializationHandler;
 
 
     try {
     try {
-      const socketServerData = await getCollabServer();
-
       this.portal.socket = this.portal.open(
       this.portal.socket = this.portal.open(
-        socketIOClient(socketServerData.url, {
-          transports: socketServerData.polling
-            ? ["websocket", "polling"]
-            : ["websocket"],
+        socketIOClient(import.meta.env.VITE_APP_WS_SERVER_URL, {
+          transports: ["websocket", "polling"],
         }),
         }),
         roomId,
         roomId,
         roomKey,
         roomKey,
@@ -484,9 +510,9 @@ class Collab extends PureComponent<Props, CollabState> {
         );
         );
 
 
         switch (decryptedData.type) {
         switch (decryptedData.type) {
-          case "INVALID_RESPONSE":
+          case WS_SUBTYPES.INVALID_RESPONSE:
             return;
             return;
-          case WS_SCENE_EVENT_TYPES.INIT: {
+          case WS_SUBTYPES.INIT: {
             if (!this.portal.socketInitialized) {
             if (!this.portal.socketInitialized) {
               this.initializeRoom({ fetchScene: false });
               this.initializeRoom({ fetchScene: false });
               const remoteElements = decryptedData.payload.elements;
               const remoteElements = decryptedData.payload.elements;
@@ -502,42 +528,76 @@ class Collab extends PureComponent<Props, CollabState> {
             }
             }
             break;
             break;
           }
           }
-          case WS_SCENE_EVENT_TYPES.UPDATE:
+          case WS_SUBTYPES.UPDATE:
             this.handleRemoteSceneUpdate(
             this.handleRemoteSceneUpdate(
               this.reconcileElements(decryptedData.payload.elements),
               this.reconcileElements(decryptedData.payload.elements),
             );
             );
             break;
             break;
-          case "MOUSE_LOCATION": {
+          case WS_SUBTYPES.MOUSE_LOCATION: {
             const { pointer, button, username, selectedElementIds } =
             const { pointer, button, username, selectedElementIds } =
               decryptedData.payload;
               decryptedData.payload;
+
             const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] =
             const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] =
               decryptedData.payload.socketId ||
               decryptedData.payload.socketId ||
               // @ts-ignore legacy, see #2094 (#2097)
               // @ts-ignore legacy, see #2094 (#2097)
               decryptedData.payload.socketID;
               decryptedData.payload.socketID;
 
 
-            const collaborators = new Map(this.collaborators);
-            const user = collaborators.get(socketId) || {}!;
-            user.pointer = pointer;
-            user.button = button;
-            user.selectedElementIds = selectedElementIds;
-            user.username = username;
-            collaborators.set(socketId, user);
+            this.updateCollaborator(socketId, {
+              pointer,
+              button,
+              selectedElementIds,
+              username,
+            });
+
+            break;
+          }
+
+          case WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS: {
+            const { sceneBounds, socketId } = decryptedData.payload;
+
+            const appState = this.excalidrawAPI.getAppState();
+
+            // we're not following the user
+            // (shouldn't happen, but could be late message or bug upstream)
+            if (appState.userToFollow?.socketId !== socketId) {
+              console.warn(
+                `receiving remote client's (from ${socketId}) viewport bounds even though we're not subscribed to it!`,
+              );
+              return;
+            }
+
+            // cross-follow case, ignore updates in this case
+            if (
+              appState.userToFollow &&
+              appState.followedBy.has(appState.userToFollow.socketId)
+            ) {
+              return;
+            }
+
             this.excalidrawAPI.updateScene({
             this.excalidrawAPI.updateScene({
-              collaborators,
+              appState: zoomToFitBounds({
+                appState,
+                bounds: sceneBounds,
+                fitToViewport: true,
+                viewportZoomFactor: 1,
+              }).appState,
             });
             });
+
             break;
             break;
           }
           }
-          case "IDLE_STATUS": {
+
+          case WS_SUBTYPES.IDLE_STATUS: {
             const { userState, socketId, username } = decryptedData.payload;
             const { userState, socketId, username } = decryptedData.payload;
-            const collaborators = new Map(this.collaborators);
-            const user = collaborators.get(socketId) || {}!;
-            user.userState = userState;
-            user.username = username;
-            this.excalidrawAPI.updateScene({
-              collaborators,
+            this.updateCollaborator(socketId, {
+              userState,
+              username,
             });
             });
             break;
             break;
           }
           }
+
+          default: {
+            assertNever(decryptedData, null);
+          }
         }
         }
       },
       },
     );
     );
@@ -553,11 +613,20 @@ class Collab extends PureComponent<Props, CollabState> {
       scenePromise.resolve(sceneData);
       scenePromise.resolve(sceneData);
     });
     });
 
 
+    this.portal.socket.on(
+      WS_EVENTS.USER_FOLLOW_ROOM_CHANGE,
+      (followedBy: SocketId[]) => {
+        this.excalidrawAPI.updateScene({
+          appState: { followedBy: new Set(followedBy) },
+        });
+
+        this.relayVisibleSceneBounds({ force: true });
+      },
+    );
+
     this.initializeIdleDetector();
     this.initializeIdleDetector();
 
 
-    this.setState({
-      activeRoomLink: window.location.href,
-    });
+    this.setActiveRoomLink(window.location.href);
 
 
     return scenePromise;
     return scenePromise;
   };
   };
@@ -721,20 +790,39 @@ class Collab extends PureComponent<Props, CollabState> {
     document.addEventListener(EVENT.VISIBILITY_CHANGE, this.onVisibilityChange);
     document.addEventListener(EVENT.VISIBILITY_CHANGE, this.onVisibilityChange);
   };
   };
 
 
-  setCollaborators(sockets: string[]) {
+  setCollaborators(sockets: SocketId[]) {
     const collaborators: InstanceType<typeof Collab>["collaborators"] =
     const collaborators: InstanceType<typeof Collab>["collaborators"] =
       new Map();
       new Map();
     for (const socketId of sockets) {
     for (const socketId of sockets) {
-      if (this.collaborators.has(socketId)) {
-        collaborators.set(socketId, this.collaborators.get(socketId)!);
-      } else {
-        collaborators.set(socketId, {});
-      }
+      collaborators.set(
+        socketId,
+        Object.assign({}, this.collaborators.get(socketId), {
+          isCurrentUser: socketId === this.portal.socket?.id,
+        }),
+      );
     }
     }
     this.collaborators = collaborators;
     this.collaborators = collaborators;
     this.excalidrawAPI.updateScene({ collaborators });
     this.excalidrawAPI.updateScene({ collaborators });
   }
   }
 
 
+  updateCollaborator = (socketId: SocketId, updates: Partial<Collaborator>) => {
+    const collaborators = new Map(this.collaborators);
+    const user: Mutable<Collaborator> = Object.assign(
+      {},
+      collaborators.get(socketId),
+      updates,
+      {
+        isCurrentUser: socketId === this.portal.socket?.id,
+      },
+    );
+    collaborators.set(socketId, user);
+    this.collaborators = collaborators;
+
+    this.excalidrawAPI.updateScene({
+      collaborators,
+    });
+  };
+
   public setLastBroadcastedOrReceivedSceneVersion = (version: number) => {
   public setLastBroadcastedOrReceivedSceneVersion = (version: number) => {
     this.lastBroadcastedOrReceivedSceneVersion = version;
     this.lastBroadcastedOrReceivedSceneVersion = version;
   };
   };
@@ -760,6 +848,19 @@ class Collab extends PureComponent<Props, CollabState> {
     CURSOR_SYNC_TIMEOUT,
     CURSOR_SYNC_TIMEOUT,
   );
   );
 
 
+  relayVisibleSceneBounds = (props?: { force: boolean }) => {
+    const appState = this.excalidrawAPI.getAppState();
+
+    if (this.portal.socket && (appState.followedBy.size > 0 || props?.force)) {
+      this.portal.broadcastVisibleSceneBounds(
+        {
+          sceneBounds: getVisibleSceneBounds(appState),
+        },
+        `follow@${this.portal.socket.id}`,
+      );
+    }
+  };
+
   onIdleStateChange = (userState: UserIdleState) => {
   onIdleStateChange = (userState: UserIdleState) => {
     this.portal.broadcastIdleChange(userState);
     this.portal.broadcastIdleChange(userState);
   };
   };
@@ -769,7 +870,7 @@ class Collab extends PureComponent<Props, CollabState> {
       getSceneVersion(elements) >
       getSceneVersion(elements) >
       this.getLastBroadcastedOrReceivedSceneVersion()
       this.getLastBroadcastedOrReceivedSceneVersion()
     ) {
     ) {
-      this.portal.broadcastScene(WS_SCENE_EVENT_TYPES.UPDATE, elements, false);
+      this.portal.broadcastScene(WS_SUBTYPES.UPDATE, elements, false);
       this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(elements);
       this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(elements);
       this.queueBroadcastAllElements();
       this.queueBroadcastAllElements();
     }
     }
@@ -782,7 +883,7 @@ class Collab extends PureComponent<Props, CollabState> {
 
 
   queueBroadcastAllElements = throttle(() => {
   queueBroadcastAllElements = throttle(() => {
     this.portal.broadcastScene(
     this.portal.broadcastScene(
-      WS_SCENE_EVENT_TYPES.UPDATE,
+      WS_SUBTYPES.UPDATE,
       this.excalidrawAPI.getSceneElementsIncludingDeleted(),
       this.excalidrawAPI.getSceneElementsIncludingDeleted(),
       true,
       true,
     );
     );
@@ -808,41 +909,31 @@ class Collab extends PureComponent<Props, CollabState> {
     { leading: false },
     { leading: false },
   );
   );
 
 
-  handleClose = () => {
-    appJotaiStore.set(collabDialogShownAtom, false);
-  };
-
   setUsername = (username: string) => {
   setUsername = (username: string) => {
     this.setState({ username });
     this.setState({ username });
+    saveUsernameToLocalStorage(username);
   };
   };
 
 
-  onUsernameChange = (username: string) => {
-    this.setUsername(username);
-    saveUsernameToLocalStorage(username);
+  getUsername = () => this.state.username;
+
+  setActiveRoomLink = (activeRoomLink: string | null) => {
+    this.setState({ activeRoomLink });
+    appJotaiStore.set(activeRoomLinkAtom, activeRoomLink);
   };
   };
 
 
-  render() {
-    const { username, errorMessage, activeRoomLink } = this.state;
+  getActiveRoomLink = () => this.state.activeRoomLink;
+
+  setErrorMessage = (errorMessage: string | null) => {
+    this.setState({ errorMessage });
+  };
 
 
-    const { modalIsShown } = this.props;
+  render() {
+    const { errorMessage } = this.state;
 
 
     return (
     return (
       <>
       <>
-        {modalIsShown && (
-          <RoomDialog
-            handleClose={this.handleClose}
-            activeRoomLink={activeRoomLink}
-            username={username}
-            onUsernameChange={this.onUsernameChange}
-            onRoomCreate={() => this.startCollaboration(null)}
-            onRoomDestroy={this.stopCollaboration}
-            setErrorMessage={(errorMessage) => {
-              this.setState({ errorMessage });
-            }}
-          />
-        )}
-        {errorMessage && (
-          <ErrorDialog onClose={() => this.setState({ errorMessage: "" })}>
+        {errorMessage != null && (
+          <ErrorDialog onClose={() => this.setState({ errorMessage: null })}>
             {errorMessage}
             {errorMessage}
           </ErrorDialog>
           </ErrorDialog>
         )}
         )}
@@ -861,11 +952,6 @@ if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
   window.collab = window.collab || ({} as Window["collab"]);
   window.collab = window.collab || ({} as Window["collab"]);
 }
 }
 
 
-const _Collab: React.FC<PublicProps> = (props) => {
-  const [collabDialogShown] = useAtom(collabDialogShownAtom);
-  return <Collab {...props} modalIsShown={collabDialogShown} />;
-};
-
-export default _Collab;
+export default Collab;
 
 
 export type TCollabClass = Collab;
 export type TCollabClass = Collab;

+ 54 - 21
excalidraw-app/collab/Portal.tsx

@@ -6,23 +6,24 @@ import {
 
 
 import { TCollabClass } from "./Collab";
 import { TCollabClass } from "./Collab";
 
 
-import { ExcalidrawElement } from "../../src/element/types";
+import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
+import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
 import {
 import {
-  WS_EVENTS,
-  FILE_UPLOAD_TIMEOUT,
-  WS_SCENE_EVENT_TYPES,
-} from "../app_constants";
-import { UserIdleState } from "../../src/types";
-import { trackEvent } from "../../src/analytics";
+  OnUserFollowedPayload,
+  SocketId,
+  UserIdleState,
+} from "../../packages/excalidraw/types";
+import { trackEvent } from "../../packages/excalidraw/analytics";
 import throttle from "lodash.throttle";
 import throttle from "lodash.throttle";
-import { newElementWith } from "../../src/element/mutateElement";
+import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
 import { BroadcastedExcalidrawElement } from "./reconciliation";
 import { BroadcastedExcalidrawElement } from "./reconciliation";
-import { encryptData } from "../../src/data/encryption";
-import { PRECEDING_ELEMENT_KEY } from "../../src/constants";
+import { encryptData } from "../../packages/excalidraw/data/encryption";
+import { PRECEDING_ELEMENT_KEY } from "../../packages/excalidraw/constants";
+import type { Socket } from "socket.io-client";
 
 
 class Portal {
 class Portal {
   collab: TCollabClass;
   collab: TCollabClass;
-  socket: SocketIOClient.Socket | null = null;
+  socket: Socket | null = null;
   socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized
   socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized
   roomId: string | null = null;
   roomId: string | null = null;
   roomKey: string | null = null;
   roomKey: string | null = null;
@@ -32,7 +33,7 @@ class Portal {
     this.collab = collab;
     this.collab = collab;
   }
   }
 
 
-  open(socket: SocketIOClient.Socket, id: string, key: string) {
+  open(socket: Socket, id: string, key: string) {
     this.socket = socket;
     this.socket = socket;
     this.roomId = id;
     this.roomId = id;
     this.roomKey = key;
     this.roomKey = key;
@@ -46,12 +47,12 @@ class Portal {
     });
     });
     this.socket.on("new-user", async (_socketId: string) => {
     this.socket.on("new-user", async (_socketId: string) => {
       this.broadcastScene(
       this.broadcastScene(
-        WS_SCENE_EVENT_TYPES.INIT,
+        WS_SUBTYPES.INIT,
         this.collab.getSceneElementsIncludingDeleted(),
         this.collab.getSceneElementsIncludingDeleted(),
         /* syncAll */ true,
         /* syncAll */ true,
       );
       );
     });
     });
-    this.socket.on("room-user-change", (clients: string[]) => {
+    this.socket.on("room-user-change", (clients: SocketId[]) => {
       this.collab.setCollaborators(clients);
       this.collab.setCollaborators(clients);
     });
     });
 
 
@@ -83,6 +84,7 @@ class Portal {
   async _broadcastSocketData(
   async _broadcastSocketData(
     data: SocketUpdateData,
     data: SocketUpdateData,
     volatile: boolean = false,
     volatile: boolean = false,
+    roomId?: string,
   ) {
   ) {
     if (this.isOpen()) {
     if (this.isOpen()) {
       const json = JSON.stringify(data);
       const json = JSON.stringify(data);
@@ -91,7 +93,7 @@ class Portal {
 
 
       this.socket?.emit(
       this.socket?.emit(
         volatile ? WS_EVENTS.SERVER_VOLATILE : WS_EVENTS.SERVER,
         volatile ? WS_EVENTS.SERVER_VOLATILE : WS_EVENTS.SERVER,
-        this.roomId,
+        roomId ?? this.roomId,
         encryptedBuffer,
         encryptedBuffer,
         iv,
         iv,
       );
       );
@@ -130,11 +132,11 @@ class Portal {
   }, FILE_UPLOAD_TIMEOUT);
   }, FILE_UPLOAD_TIMEOUT);
 
 
   broadcastScene = async (
   broadcastScene = async (
-    updateType: WS_SCENE_EVENT_TYPES.INIT | WS_SCENE_EVENT_TYPES.UPDATE,
+    updateType: WS_SUBTYPES.INIT | WS_SUBTYPES.UPDATE,
     allElements: readonly ExcalidrawElement[],
     allElements: readonly ExcalidrawElement[],
     syncAll: boolean,
     syncAll: boolean,
   ) => {
   ) => {
-    if (updateType === WS_SCENE_EVENT_TYPES.INIT && !syncAll) {
+    if (updateType === WS_SUBTYPES.INIT && !syncAll) {
       throw new Error("syncAll must be true when sending SCENE.INIT");
       throw new Error("syncAll must be true when sending SCENE.INIT");
     }
     }
 
 
@@ -183,9 +185,9 @@ class Portal {
   broadcastIdleChange = (userState: UserIdleState) => {
   broadcastIdleChange = (userState: UserIdleState) => {
     if (this.socket?.id) {
     if (this.socket?.id) {
       const data: SocketUpdateDataSource["IDLE_STATUS"] = {
       const data: SocketUpdateDataSource["IDLE_STATUS"] = {
-        type: "IDLE_STATUS",
+        type: WS_SUBTYPES.IDLE_STATUS,
         payload: {
         payload: {
-          socketId: this.socket.id,
+          socketId: this.socket.id as SocketId,
           userState,
           userState,
           username: this.collab.state.username,
           username: this.collab.state.username,
         },
         },
@@ -203,9 +205,9 @@ class Portal {
   }) => {
   }) => {
     if (this.socket?.id) {
     if (this.socket?.id) {
       const data: SocketUpdateDataSource["MOUSE_LOCATION"] = {
       const data: SocketUpdateDataSource["MOUSE_LOCATION"] = {
-        type: "MOUSE_LOCATION",
+        type: WS_SUBTYPES.MOUSE_LOCATION,
         payload: {
         payload: {
-          socketId: this.socket.id,
+          socketId: this.socket.id as SocketId,
           pointer: payload.pointer,
           pointer: payload.pointer,
           button: payload.button || "up",
           button: payload.button || "up",
           selectedElementIds:
           selectedElementIds:
@@ -213,12 +215,43 @@ class Portal {
           username: this.collab.state.username,
           username: this.collab.state.username,
         },
         },
       };
       };
+
       return this._broadcastSocketData(
       return this._broadcastSocketData(
         data as SocketUpdateData,
         data as SocketUpdateData,
         true, // volatile
         true, // volatile
       );
       );
     }
     }
   };
   };
+
+  broadcastVisibleSceneBounds = (
+    payload: {
+      sceneBounds: SocketUpdateDataSource["USER_VISIBLE_SCENE_BOUNDS"]["payload"]["sceneBounds"];
+    },
+    roomId: string,
+  ) => {
+    if (this.socket?.id) {
+      const data: SocketUpdateDataSource["USER_VISIBLE_SCENE_BOUNDS"] = {
+        type: WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS,
+        payload: {
+          socketId: this.socket.id as SocketId,
+          username: this.collab.state.username,
+          sceneBounds: payload.sceneBounds,
+        },
+      };
+
+      return this._broadcastSocketData(
+        data as SocketUpdateData,
+        true, // volatile
+        roomId,
+      );
+    }
+  };
+
+  broadcastUserFollowed = (payload: OnUserFollowedPayload) => {
+    if (this.socket?.id) {
+      this.socket.emit(WS_EVENTS.USER_FOLLOW_CHANGE, payload);
+    }
+  };
 }
 }
 
 
 export default Portal;
 export default Portal;

+ 14 - 14
excalidraw-app/collab/RoomDialog.tsx

@@ -1,13 +1,13 @@
 import { useRef, useState } from "react";
 import { useRef, useState } from "react";
 import * as Popover from "@radix-ui/react-popover";
 import * as Popover from "@radix-ui/react-popover";
 
 
-import { copyTextToSystemClipboard } from "../../src/clipboard";
-import { trackEvent } from "../../src/analytics";
-import { getFrame } from "../../src/utils";
-import { useI18n } from "../../src/i18n";
-import { KEYS } from "../../src/keys";
+import { copyTextToSystemClipboard } from "../../packages/excalidraw/clipboard";
+import { trackEvent } from "../../packages/excalidraw/analytics";
+import { getFrame } from "../../packages/excalidraw/utils";
+import { useI18n } from "../../packages/excalidraw/i18n";
+import { KEYS } from "../../packages/excalidraw/keys";
 
 
-import { Dialog } from "../../src/components/Dialog";
+import { Dialog } from "../../packages/excalidraw/components/Dialog";
 import {
 import {
   copyIcon,
   copyIcon,
   playerPlayIcon,
   playerPlayIcon,
@@ -16,11 +16,11 @@ import {
   shareIOS,
   shareIOS,
   shareWindows,
   shareWindows,
   tablerCheckIcon,
   tablerCheckIcon,
-} from "../../src/components/icons";
-import { TextField } from "../../src/components/TextField";
-import { FilledButton } from "../../src/components/FilledButton";
+} from "../../packages/excalidraw/components/icons";
+import { TextField } from "../../packages/excalidraw/components/TextField";
+import { FilledButton } from "../../packages/excalidraw/components/FilledButton";
 
 
-import { ReactComponent as CollabImage } from "../../src/assets/lock.svg";
+import { ReactComponent as CollabImage } from "../../packages/excalidraw/assets/lock.svg";
 import "./RoomDialog.scss";
 import "./RoomDialog.scss";
 
 
 const getShareIcon = () => {
 const getShareIcon = () => {
@@ -120,7 +120,7 @@ export const RoomModal = ({
               size="large"
               size="large"
               variant="icon"
               variant="icon"
               label="Share"
               label="Share"
-              startIcon={getShareIcon()}
+              icon={getShareIcon()}
               className="RoomDialog__active__share"
               className="RoomDialog__active__share"
               onClick={shareRoomLink}
               onClick={shareRoomLink}
             />
             />
@@ -130,7 +130,7 @@ export const RoomModal = ({
               <FilledButton
               <FilledButton
                 size="large"
                 size="large"
                 label="Copy link"
                 label="Copy link"
-                startIcon={copyIcon}
+                icon={copyIcon}
                 onClick={copyRoomLink}
                 onClick={copyRoomLink}
               />
               />
             </Popover.Trigger>
             </Popover.Trigger>
@@ -166,7 +166,7 @@ export const RoomModal = ({
             variant="outlined"
             variant="outlined"
             color="danger"
             color="danger"
             label={t("roomDialog.button_stopSession")}
             label={t("roomDialog.button_stopSession")}
-            startIcon={playerStopFilledIcon}
+            icon={playerStopFilledIcon}
             onClick={() => {
             onClick={() => {
               trackEvent("share", "room closed");
               trackEvent("share", "room closed");
               onRoomDestroy();
               onRoomDestroy();
@@ -195,7 +195,7 @@ export const RoomModal = ({
         <FilledButton
         <FilledButton
           size="large"
           size="large"
           label={t("roomDialog.button_startSession")}
           label={t("roomDialog.button_startSession")}
-          startIcon={playerPlayIcon}
+          icon={playerPlayIcon}
           onClick={() => {
           onClick={() => {
             trackEvent("share", "room creation", `ui (${getFrame()})`);
             trackEvent("share", "room creation", `ui (${getFrame()})`);
             onRoomCreate();
             onRoomCreate();

+ 4 - 4
excalidraw-app/collab/reconciliation.ts

@@ -1,7 +1,7 @@
-import { PRECEDING_ELEMENT_KEY } from "../../src/constants";
-import { ExcalidrawElement } from "../../src/element/types";
-import { AppState } from "../../src/types";
-import { arrayToMapWithIndex } from "../../src/utils";
+import { PRECEDING_ELEMENT_KEY } from "../../packages/excalidraw/constants";
+import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
+import { AppState } from "../../packages/excalidraw/types";
+import { arrayToMapWithIndex } from "../../packages/excalidraw/utils";
 
 
 export type ReconciledElements = readonly ExcalidrawElement[] & {
 export type ReconciledElements = readonly ExcalidrawElement[] & {
   _brand: "reconciledElements";
   _brand: "reconciledElements";

+ 1 - 1
excalidraw-app/components/AppFooter.tsx

@@ -1,5 +1,5 @@
 import React from "react";
 import React from "react";
-import { Footer } from "../../src/packages/excalidraw/index";
+import { Footer } from "../../packages/excalidraw/index";
 import { EncryptedIcon } from "./EncryptedIcon";
 import { EncryptedIcon } from "./EncryptedIcon";
 import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
 import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
 import { isExcalidrawPlusSignedUser } from "../app_constants";
 import { isExcalidrawPlusSignedUser } from "../app_constants";

+ 4 - 4
excalidraw-app/components/AppMainMenu.tsx

@@ -1,10 +1,10 @@
 import React from "react";
 import React from "react";
-import { PlusPromoIcon } from "../../src/components/icons";
-import { MainMenu } from "../../src/packages/excalidraw/index";
+import { PlusPromoIcon } from "../../packages/excalidraw/components/icons";
+import { MainMenu } from "../../packages/excalidraw/index";
 import { LanguageList } from "./LanguageList";
 import { LanguageList } from "./LanguageList";
 
 
 export const AppMainMenu: React.FC<{
 export const AppMainMenu: React.FC<{
-  setCollabDialogShown: (toggle: boolean) => any;
+  onCollabDialogOpen: () => any;
   isCollaborating: boolean;
   isCollaborating: boolean;
   isCollabEnabled: boolean;
   isCollabEnabled: boolean;
 }> = React.memo((props) => {
 }> = React.memo((props) => {
@@ -17,7 +17,7 @@ export const AppMainMenu: React.FC<{
       {props.isCollabEnabled && (
       {props.isCollabEnabled && (
         <MainMenu.DefaultItems.LiveCollaborationTrigger
         <MainMenu.DefaultItems.LiveCollaborationTrigger
           isCollaborating={props.isCollaborating}
           isCollaborating={props.isCollaborating}
-          onSelect={() => props.setCollabDialogShown(true)}
+          onSelect={() => props.onCollabDialogOpen()}
         />
         />
       )}
       )}
 
 

+ 6 - 6
excalidraw-app/components/AppWelcomeScreen.tsx

@@ -1,12 +1,12 @@
 import React from "react";
 import React from "react";
-import { PlusPromoIcon } from "../../src/components/icons";
-import { useI18n } from "../../src/i18n";
-import { WelcomeScreen } from "../../src/packages/excalidraw/index";
+import { PlusPromoIcon } from "../../packages/excalidraw/components/icons";
+import { useI18n } from "../../packages/excalidraw/i18n";
+import { WelcomeScreen } from "../../packages/excalidraw/index";
 import { isExcalidrawPlusSignedUser } from "../app_constants";
 import { isExcalidrawPlusSignedUser } from "../app_constants";
-import { POINTER_EVENTS } from "../../src/constants";
+import { POINTER_EVENTS } from "../../packages/excalidraw/constants";
 
 
 export const AppWelcomeScreen: React.FC<{
 export const AppWelcomeScreen: React.FC<{
-  setCollabDialogShown: (toggle: boolean) => any;
+  onCollabDialogOpen: () => any;
   isCollabEnabled: boolean;
   isCollabEnabled: boolean;
 }> = React.memo((props) => {
 }> = React.memo((props) => {
   const { t } = useI18n();
   const { t } = useI18n();
@@ -52,7 +52,7 @@ export const AppWelcomeScreen: React.FC<{
           <WelcomeScreen.Center.MenuItemHelp />
           <WelcomeScreen.Center.MenuItemHelp />
           {props.isCollabEnabled && (
           {props.isCollabEnabled && (
             <WelcomeScreen.Center.MenuItemLiveCollaborationTrigger
             <WelcomeScreen.Center.MenuItemLiveCollaborationTrigger
-              onSelect={() => props.setCollabDialogShown(true)}
+              onSelect={() => props.onCollabDialogOpen()}
             />
             />
           )}
           )}
           {!isExcalidrawPlusSignedUser && (
           {!isExcalidrawPlusSignedUser && (

+ 3 - 3
excalidraw-app/components/EncryptedIcon.tsx

@@ -1,6 +1,6 @@
-import { shield } from "../../src/components/icons";
-import { Tooltip } from "../../src/components/Tooltip";
-import { useI18n } from "../../src/i18n";
+import { shield } from "../../packages/excalidraw/components/icons";
+import { Tooltip } from "../../packages/excalidraw/components/Tooltip";
+import { useI18n } from "../../packages/excalidraw/i18n";
 
 
 export const EncryptedIcon = () => {
 export const EncryptedIcon = () => {
   const { t } = useI18n();
   const { t } = useI18n();

+ 22 - 12
excalidraw-app/components/ExportToExcalidrawPlus.tsx

@@ -1,20 +1,30 @@
 import React from "react";
 import React from "react";
-import { Card } from "../../src/components/Card";
-import { ToolButton } from "../../src/components/ToolButton";
-import { serializeAsJSON } from "../../src/data/json";
+import { Card } from "../../packages/excalidraw/components/Card";
+import { ToolButton } from "../../packages/excalidraw/components/ToolButton";
+import { serializeAsJSON } from "../../packages/excalidraw/data/json";
 import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
 import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
-import { FileId, NonDeletedExcalidrawElement } from "../../src/element/types";
-import { AppState, BinaryFileData, BinaryFiles } from "../../src/types";
+import {
+  FileId,
+  NonDeletedExcalidrawElement,
+} from "../../packages/excalidraw/element/types";
+import {
+  AppState,
+  BinaryFileData,
+  BinaryFiles,
+} from "../../packages/excalidraw/types";
 import { nanoid } from "nanoid";
 import { nanoid } from "nanoid";
-import { useI18n } from "../../src/i18n";
-import { encryptData, generateEncryptionKey } from "../../src/data/encryption";
-import { isInitializedImageElement } from "../../src/element/typeChecks";
+import { useI18n } from "../../packages/excalidraw/i18n";
+import {
+  encryptData,
+  generateEncryptionKey,
+} from "../../packages/excalidraw/data/encryption";
+import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
 import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
 import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
 import { encodeFilesForUpload } from "../data/FileManager";
 import { encodeFilesForUpload } from "../data/FileManager";
-import { MIME_TYPES } from "../../src/constants";
-import { trackEvent } from "../../src/analytics";
-import { getFrame } from "../../src/utils";
-import { ExcalidrawLogo } from "../../src/components/ExcalidrawLogo";
+import { MIME_TYPES } from "../../packages/excalidraw/constants";
+import { trackEvent } from "../../packages/excalidraw/analytics";
+import { getFrame } from "../../packages/excalidraw/utils";
+import { ExcalidrawLogo } from "../../packages/excalidraw/components/ExcalidrawLogo";
 
 
 export const exportToExcalidrawPlus = async (
 export const exportToExcalidrawPlus = async (
   elements: readonly NonDeletedExcalidrawElement[],
   elements: readonly NonDeletedExcalidrawElement[],

+ 2 - 2
excalidraw-app/components/GitHubCorner.tsx

@@ -1,7 +1,7 @@
 import oc from "open-color";
 import oc from "open-color";
 import React from "react";
 import React from "react";
-import { THEME } from "../../src/constants";
-import { Theme } from "../../src/element/types";
+import { THEME } from "../../packages/excalidraw/constants";
+import { Theme } from "../../packages/excalidraw/element/types";
 
 
 // https://github.com/tholman/github-corners
 // https://github.com/tholman/github-corners
 export const GitHubCorner = React.memo(
 export const GitHubCorner = React.memo(

+ 3 - 3
excalidraw-app/components/LanguageList.tsx

@@ -1,8 +1,8 @@
 import { useSetAtom } from "jotai";
 import { useSetAtom } from "jotai";
 import React from "react";
 import React from "react";
-import { appLangCodeAtom } from "..";
-import { useI18n } from "../../src/i18n";
-import { languages } from "../../src/i18n";
+import { appLangCodeAtom } from "../App";
+import { useI18n } from "../../packages/excalidraw/i18n";
+import { languages } from "../../packages/excalidraw/i18n";
 
 
 export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
 export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
   const { t, langCode } = useI18n();
   const { t, langCode } = useI18n();

+ 2 - 2
src/components/TopErrorBoundary.tsx → excalidraw-app/components/TopErrorBoundary.tsx

@@ -1,7 +1,7 @@
 import React from "react";
 import React from "react";
 import * as Sentry from "@sentry/browser";
 import * as Sentry from "@sentry/browser";
-import { t } from "../i18n";
-import Trans from "./Trans";
+import { t } from "../../packages/excalidraw/i18n";
+import Trans from "../../packages/excalidraw/components/Trans";
 
 
 interface TopErrorBoundaryState {
 interface TopErrorBoundaryState {
   hasError: boolean;
   hasError: boolean;

+ 6 - 6
excalidraw-app/data/FileManager.ts

@@ -1,19 +1,19 @@
-import { compressData } from "../../src/data/encode";
-import { newElementWith } from "../../src/element/mutateElement";
-import { isInitializedImageElement } from "../../src/element/typeChecks";
+import { compressData } from "../../packages/excalidraw/data/encode";
+import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
+import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
 import {
 import {
   ExcalidrawElement,
   ExcalidrawElement,
   ExcalidrawImageElement,
   ExcalidrawImageElement,
   FileId,
   FileId,
   InitializedExcalidrawImageElement,
   InitializedExcalidrawImageElement,
-} from "../../src/element/types";
-import { t } from "../../src/i18n";
+} from "../../packages/excalidraw/element/types";
+import { t } from "../../packages/excalidraw/i18n";
 import {
 import {
   BinaryFileData,
   BinaryFileData,
   BinaryFileMetadata,
   BinaryFileMetadata,
   ExcalidrawImperativeAPI,
   ExcalidrawImperativeAPI,
   BinaryFiles,
   BinaryFiles,
-} from "../../src/types";
+} from "../../packages/excalidraw/types";
 
 
 export class FileManager {
 export class FileManager {
   /** files being fetched */
   /** files being fetched */

+ 12 - 5
excalidraw-app/data/LocalData.ts

@@ -11,11 +11,18 @@
  */
  */
 
 
 import { createStore, entries, del, getMany, set, setMany } from "idb-keyval";
 import { createStore, entries, del, getMany, set, setMany } from "idb-keyval";
-import { clearAppStateForLocalStorage } from "../../src/appState";
-import { clearElementsForLocalStorage } from "../../src/element";
-import { ExcalidrawElement, FileId } from "../../src/element/types";
-import { AppState, BinaryFileData, BinaryFiles } from "../../src/types";
-import { debounce } from "../../src/utils";
+import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState";
+import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
+import {
+  ExcalidrawElement,
+  FileId,
+} from "../../packages/excalidraw/element/types";
+import {
+  AppState,
+  BinaryFileData,
+  BinaryFiles,
+} from "../../packages/excalidraw/types";
+import { debounce } from "../../packages/excalidraw/utils";
 import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
 import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
 import { FileManager } from "./FileManager";
 import { FileManager } from "./FileManager";
 import { Locker } from "./Locker";
 import { Locker } from "./Locker";

+ 19 - 12
excalidraw-app/data/firebase.ts

@@ -1,20 +1,27 @@
-import { ExcalidrawElement, FileId } from "../../src/element/types";
-import { getSceneVersion } from "../../src/element";
+import {
+  ExcalidrawElement,
+  FileId,
+} from "../../packages/excalidraw/element/types";
+import { getSceneVersion } from "../../packages/excalidraw/element";
 import Portal from "../collab/Portal";
 import Portal from "../collab/Portal";
-import { restoreElements } from "../../src/data/restore";
+import { restoreElements } from "../../packages/excalidraw/data/restore";
 import {
 import {
   AppState,
   AppState,
   BinaryFileData,
   BinaryFileData,
   BinaryFileMetadata,
   BinaryFileMetadata,
   DataURL,
   DataURL,
-} from "../../src/types";
+} from "../../packages/excalidraw/types";
 import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
 import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
-import { decompressData } from "../../src/data/encode";
-import { encryptData, decryptData } from "../../src/data/encryption";
-import { MIME_TYPES } from "../../src/constants";
+import { decompressData } from "../../packages/excalidraw/data/encode";
+import {
+  encryptData,
+  decryptData,
+} from "../../packages/excalidraw/data/encryption";
+import { MIME_TYPES } from "../../packages/excalidraw/constants";
 import { reconcileElements } from "../collab/reconciliation";
 import { reconcileElements } from "../collab/reconciliation";
 import { getSyncableElements, SyncableExcalidrawElement } from ".";
 import { getSyncableElements, SyncableExcalidrawElement } from ".";
-import { ResolutionType } from "../../src/utility-types";
+import { ResolutionType } from "../../packages/excalidraw/utility-types";
+import type { Socket } from "socket.io-client";
 
 
 // private
 // private
 // -----------------------------------------------------------------------------
 // -----------------------------------------------------------------------------
@@ -132,12 +139,12 @@ const decryptElements = async (
 };
 };
 
 
 class FirebaseSceneVersionCache {
 class FirebaseSceneVersionCache {
-  private static cache = new WeakMap<SocketIOClient.Socket, number>();
-  static get = (socket: SocketIOClient.Socket) => {
+  private static cache = new WeakMap<Socket, number>();
+  static get = (socket: Socket) => {
     return FirebaseSceneVersionCache.cache.get(socket);
     return FirebaseSceneVersionCache.cache.get(socket);
   };
   };
   static set = (
   static set = (
-    socket: SocketIOClient.Socket,
+    socket: Socket,
     elements: readonly SyncableExcalidrawElement[],
     elements: readonly SyncableExcalidrawElement[],
   ) => {
   ) => {
     FirebaseSceneVersionCache.cache.set(socket, getSceneVersion(elements));
     FirebaseSceneVersionCache.cache.set(socket, getSceneVersion(elements));
@@ -279,7 +286,7 @@ export const saveToFirebase = async (
 export const loadFromFirebase = async (
 export const loadFromFirebase = async (
   roomId: string,
   roomId: string,
   roomKey: string,
   roomKey: string,
-  socket: SocketIOClient.Socket | null,
+  socket: Socket | null,
 ): Promise<readonly ExcalidrawElement[] | null> => {
 ): Promise<readonly ExcalidrawElement[] | null> => {
   const firebase = await loadFirestore();
   const firebase = await loadFirestore();
   const db = firebase.firestore();
   const db = firebase.firestore();

+ 38 - 50
excalidraw-app/data/index.ts

@@ -1,27 +1,36 @@
-import { compressData, decompressData } from "../../src/data/encode";
+import {
+  compressData,
+  decompressData,
+} from "../../packages/excalidraw/data/encode";
 import {
 import {
   decryptData,
   decryptData,
   generateEncryptionKey,
   generateEncryptionKey,
   IV_LENGTH_BYTES,
   IV_LENGTH_BYTES,
-} from "../../src/data/encryption";
-import { serializeAsJSON } from "../../src/data/json";
-import { restore } from "../../src/data/restore";
-import { ImportedDataState } from "../../src/data/types";
-import { isInvisiblySmallElement } from "../../src/element/sizeHelpers";
-import { isInitializedImageElement } from "../../src/element/typeChecks";
-import { ExcalidrawElement, FileId } from "../../src/element/types";
-import { t } from "../../src/i18n";
+} from "../../packages/excalidraw/data/encryption";
+import { serializeAsJSON } from "../../packages/excalidraw/data/json";
+import { restore } from "../../packages/excalidraw/data/restore";
+import { ImportedDataState } from "../../packages/excalidraw/data/types";
+import { SceneBounds } from "../../packages/excalidraw/element/bounds";
+import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers";
+import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
+import {
+  ExcalidrawElement,
+  FileId,
+} from "../../packages/excalidraw/element/types";
+import { t } from "../../packages/excalidraw/i18n";
 import {
 import {
   AppState,
   AppState,
   BinaryFileData,
   BinaryFileData,
   BinaryFiles,
   BinaryFiles,
+  SocketId,
   UserIdleState,
   UserIdleState,
-} from "../../src/types";
-import { bytesToHexString } from "../../src/utils";
+} from "../../packages/excalidraw/types";
+import { bytesToHexString } from "../../packages/excalidraw/utils";
 import {
 import {
   DELETED_ELEMENT_TIMEOUT,
   DELETED_ELEMENT_TIMEOUT,
   FILE_UPLOAD_MAX_BYTES,
   FILE_UPLOAD_MAX_BYTES,
   ROOM_ID_BYTES,
   ROOM_ID_BYTES,
+  WS_SUBTYPES,
 } from "../app_constants";
 } from "../app_constants";
 import { encodeFilesForUpload } from "./FileManager";
 import { encodeFilesForUpload } from "./FileManager";
 import { saveFilesToFirebase } from "./firebase";
 import { saveFilesToFirebase } from "./firebase";
@@ -56,67 +65,49 @@ const generateRoomId = async () => {
   return bytesToHexString(buffer);
   return bytesToHexString(buffer);
 };
 };
 
 
-/**
- * Right now the reason why we resolve connection params (url, polling...)
- * from upstream is to allow changing the params immediately when needed without
- * having to wait for clients to update the SW.
- *
- * If REACT_APP_WS_SERVER_URL env is set, we use that instead (useful for forks)
- */
-export const getCollabServer = async (): Promise<{
-  url: string;
-  polling: boolean;
-}> => {
-  if (import.meta.env.VITE_APP_WS_SERVER_URL) {
-    return {
-      url: import.meta.env.VITE_APP_WS_SERVER_URL,
-      polling: true,
-    };
-  }
-
-  try {
-    const resp = await fetch(
-      `${import.meta.env.VITE_APP_PORTAL_URL}/collab-server`,
-    );
-    return await resp.json();
-  } catch (error) {
-    console.error(error);
-    throw new Error(t("errors.cannotResolveCollabServer"));
-  }
-};
-
 export type EncryptedData = {
 export type EncryptedData = {
   data: ArrayBuffer;
   data: ArrayBuffer;
   iv: Uint8Array;
   iv: Uint8Array;
 };
 };
 
 
 export type SocketUpdateDataSource = {
 export type SocketUpdateDataSource = {
+  INVALID_RESPONSE: {
+    type: WS_SUBTYPES.INVALID_RESPONSE;
+  };
   SCENE_INIT: {
   SCENE_INIT: {
-    type: "SCENE_INIT";
+    type: WS_SUBTYPES.INIT;
     payload: {
     payload: {
       elements: readonly ExcalidrawElement[];
       elements: readonly ExcalidrawElement[];
     };
     };
   };
   };
   SCENE_UPDATE: {
   SCENE_UPDATE: {
-    type: "SCENE_UPDATE";
+    type: WS_SUBTYPES.UPDATE;
     payload: {
     payload: {
       elements: readonly ExcalidrawElement[];
       elements: readonly ExcalidrawElement[];
     };
     };
   };
   };
   MOUSE_LOCATION: {
   MOUSE_LOCATION: {
-    type: "MOUSE_LOCATION";
+    type: WS_SUBTYPES.MOUSE_LOCATION;
     payload: {
     payload: {
-      socketId: string;
+      socketId: SocketId;
       pointer: { x: number; y: number; tool: "pointer" | "laser" };
       pointer: { x: number; y: number; tool: "pointer" | "laser" };
       button: "down" | "up";
       button: "down" | "up";
       selectedElementIds: AppState["selectedElementIds"];
       selectedElementIds: AppState["selectedElementIds"];
       username: string;
       username: string;
     };
     };
   };
   };
+  USER_VISIBLE_SCENE_BOUNDS: {
+    type: WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS;
+    payload: {
+      socketId: SocketId;
+      username: string;
+      sceneBounds: SceneBounds;
+    };
+  };
   IDLE_STATUS: {
   IDLE_STATUS: {
-    type: "IDLE_STATUS";
+    type: WS_SUBTYPES.IDLE_STATUS;
     payload: {
     payload: {
-      socketId: string;
+      socketId: SocketId;
       userState: UserIdleState;
       userState: UserIdleState;
       username: string;
       username: string;
     };
     };
@@ -124,10 +115,7 @@ export type SocketUpdateDataSource = {
 };
 };
 
 
 export type SocketUpdateDataIncoming =
 export type SocketUpdateDataIncoming =
-  | SocketUpdateDataSource[keyof SocketUpdateDataSource]
-  | {
-      type: "INVALID_RESPONSE";
-    };
+  SocketUpdateDataSource[keyof SocketUpdateDataSource];
 
 
 export type SocketUpdateData =
 export type SocketUpdateData =
   SocketUpdateDataSource[keyof SocketUpdateDataSource] & {
   SocketUpdateDataSource[keyof SocketUpdateDataSource] & {

+ 5 - 5
excalidraw-app/data/localStorage.ts

@@ -1,12 +1,12 @@
-import { ExcalidrawElement } from "../../src/element/types";
-import { AppState } from "../../src/types";
+import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
+import { AppState } from "../../packages/excalidraw/types";
 import {
 import {
   clearAppStateForLocalStorage,
   clearAppStateForLocalStorage,
   getDefaultAppState,
   getDefaultAppState,
-} from "../../src/appState";
-import { clearElementsForLocalStorage } from "../../src/element";
+} from "../../packages/excalidraw/appState";
+import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
 import { STORAGE_KEYS } from "../app_constants";
 import { STORAGE_KEYS } from "../app_constants";
-import { ImportedDataState } from "../../src/data/types";
+import { ImportedDataState } from "../../packages/excalidraw/data/types";
 
 
 export const saveUsernameToLocalStorage = (username: string) => {
 export const saveUsernameToLocalStorage = (username: string) => {
   try {
   try {

+ 3 - 0
excalidraw-app/global.d.ts

@@ -0,0 +1,3 @@
+interface Window {
+  __EXCALIDRAW_SHA__: string | undefined;
+}

+ 2 - 2
index.html → excalidraw-app/index.html

@@ -121,7 +121,7 @@
       crossorigin="anonymous"
       crossorigin="anonymous"
     />
     />
 
 
-    <link rel="stylesheet" href="/fonts.css" type="text/css" />
+    <link rel="stylesheet" href="/fonts/fonts.css" type="text/css" />
     <% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%"==="true" ) { %>
     <% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%"==="true" ) { %>
     <script>
     <script>
       {
       {
@@ -195,7 +195,7 @@
       <h1 class="visually-hidden">Excalidraw</h1>
       <h1 class="visually-hidden">Excalidraw</h1>
     </header>
     </header>
     <div id="root"></div>
     <div id="root"></div>
-    <script type="module" src="/src/index.tsx"></script>
+    <script type="module" src="index.tsx"></script>
     <% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%" !== 'true') { %>
     <% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%" !== 'true') { %>
     <!-- 100% privacy friendly analytics -->
     <!-- 100% privacy friendly analytics -->
     <script>
     <script>

+ 14 - 873
excalidraw-app/index.tsx

@@ -1,874 +1,15 @@
-import polyfill from "../src/polyfill";
-import LanguageDetector from "i18next-browser-languagedetector";
-import { useEffect, useRef, useState } from "react";
-import { trackEvent } from "../src/analytics";
-import { getDefaultAppState } from "../src/appState";
-import { ErrorDialog } from "../src/components/ErrorDialog";
-import { TopErrorBoundary } from "../src/components/TopErrorBoundary";
-import { useMathSubtype } from "../src/element/subtypes/mathjax";
-import {
-  APP_NAME,
-  EVENT,
-  THEME,
-  TITLE_TIMEOUT,
-  VERSION_TIMEOUT,
-} from "../src/constants";
-import { loadFromBlob } from "../src/data/blob";
-import {
-  ExcalidrawElement,
-  FileId,
-  NonDeletedExcalidrawElement,
-  Theme,
-} from "../src/element/types";
-import { useCallbackRefState } from "../src/hooks/useCallbackRefState";
-import { t } from "../src/i18n";
-import {
-  Excalidraw,
-  defaultLang,
-  LiveCollaborationTrigger,
-  TTDDialog,
-  TTDDialogTrigger,
-} from "../src/packages/excalidraw/index";
-import {
-  AppState,
-  LibraryItems,
-  ExcalidrawImperativeAPI,
-  BinaryFiles,
-  ExcalidrawInitialDataState,
-  UIAppState,
-} from "../src/types";
-import {
-  debounce,
-  getVersion,
-  getFrame,
-  isTestEnv,
-  preventUnload,
-  ResolvablePromise,
-  resolvablePromise,
-  isRunningInIframe,
-} from "../src/utils";
-import {
-  FIREBASE_STORAGE_PREFIXES,
-  STORAGE_KEYS,
-  SYNC_BROWSER_TABS_TIMEOUT,
-} from "./app_constants";
-import Collab, {
-  CollabAPI,
-  collabAPIAtom,
-  collabDialogShownAtom,
-  isCollaboratingAtom,
-  isOfflineAtom,
-} from "./collab/Collab";
-import {
-  exportToBackend,
-  getCollaborationLinkData,
-  isCollaborationLink,
-  loadScene,
-} from "./data";
-import {
-  getLibraryItemsFromStorage,
-  importFromLocalStorage,
-  importUsernameFromLocalStorage,
-} from "./data/localStorage";
-import CustomStats from "./CustomStats";
-import {
-  restore,
-  restoreAppState,
-  RestoredDataState,
-} from "../src/data/restore";
-import {
-  ExportToExcalidrawPlus,
-  exportToExcalidrawPlus,
-} from "./components/ExportToExcalidrawPlus";
-import { updateStaleImageStatuses } from "./data/FileManager";
-import { newElementWith } from "../src/element/mutateElement";
-import { isInitializedImageElement } from "../src/element/typeChecks";
-import { loadFilesFromFirebase } from "./data/firebase";
-import { LocalData } from "./data/LocalData";
-import { isBrowserStorageStateNewer } from "./data/tabSync";
-import clsx from "clsx";
-import { reconcileElements } from "./collab/reconciliation";
-import {
-  parseLibraryTokensFromUrl,
-  useHandleLibrary,
-} from "../src/data/library";
-import { AppMainMenu } from "./components/AppMainMenu";
-import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
-import { AppFooter } from "./components/AppFooter";
-import { atom, Provider, useAtom, useAtomValue } from "jotai";
-import { useAtomWithInitialValue } from "../src/jotai";
-import { appJotaiStore } from "./app-jotai";
-
-import "./index.scss";
-import { ResolutionType } from "../src/utility-types";
-import { ShareableLinkDialog } from "../src/components/ShareableLinkDialog";
-import { openConfirmModal } from "../src/components/OverwriteConfirm/OverwriteConfirmState";
-import { OverwriteConfirmDialog } from "../src/components/OverwriteConfirm/OverwriteConfirm";
-import Trans from "../src/components/Trans";
-
-polyfill();
-
-window.EXCALIDRAW_THROTTLE_RENDER = true;
-
-let isSelfEmbedding = false;
-
-if (window.self !== window.top) {
-  try {
-    const parentUrl = new URL(document.referrer);
-    const currentUrl = new URL(window.location.href);
-    if (parentUrl.origin === currentUrl.origin) {
-      isSelfEmbedding = true;
-    }
-  } catch (error) {
-    // ignore
-  }
-}
-
-const languageDetector = new LanguageDetector();
-languageDetector.init({
-  languageUtils: {},
-});
-
-const shareableLinkConfirmDialog = {
-  title: t("overwriteConfirm.modal.shareableLink.title"),
-  description: (
-    <Trans
-      i18nKey="overwriteConfirm.modal.shareableLink.description"
-      bold={(text) => <strong>{text}</strong>}
-      br={() => <br />}
-    />
-  ),
-  actionLabel: t("overwriteConfirm.modal.shareableLink.button"),
-  color: "danger",
-} as const;
-
-const initializeScene = async (opts: {
-  collabAPI: CollabAPI | null;
-  excalidrawAPI: ExcalidrawImperativeAPI;
-}): Promise<
-  { scene: ExcalidrawInitialDataState | null } & (
-    | { isExternalScene: true; id: string; key: string }
-    | { isExternalScene: false; id?: null; key?: null }
-  )
-> => {
-  const searchParams = new URLSearchParams(window.location.search);
-  const id = searchParams.get("id");
-  const jsonBackendMatch = window.location.hash.match(
-    /^#json=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/,
-  );
-  const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/);
-
-  const localDataState = importFromLocalStorage();
-
-  let scene: RestoredDataState & {
-    scrollToContent?: boolean;
-  } = await loadScene(null, null, localDataState);
-
-  let roomLinkData = getCollaborationLinkData(window.location.href);
-  const isExternalScene = !!(id || jsonBackendMatch || roomLinkData);
-  if (isExternalScene) {
-    if (
-      // don't prompt if scene is empty
-      !scene.elements.length ||
-      // don't prompt for collab scenes because we don't override local storage
-      roomLinkData ||
-      // otherwise, prompt whether user wants to override current scene
-      (await openConfirmModal(shareableLinkConfirmDialog))
-    ) {
-      if (jsonBackendMatch) {
-        scene = await loadScene(
-          jsonBackendMatch[1],
-          jsonBackendMatch[2],
-          localDataState,
-        );
-      }
-      scene.scrollToContent = true;
-      if (!roomLinkData) {
-        window.history.replaceState({}, APP_NAME, window.location.origin);
-      }
-    } else {
-      // https://github.com/excalidraw/excalidraw/issues/1919
-      if (document.hidden) {
-        return new Promise((resolve, reject) => {
-          window.addEventListener(
-            "focus",
-            () => initializeScene(opts).then(resolve).catch(reject),
-            {
-              once: true,
-            },
-          );
-        });
-      }
-
-      roomLinkData = null;
-      window.history.replaceState({}, APP_NAME, window.location.origin);
-    }
-  } else if (externalUrlMatch) {
-    window.history.replaceState({}, APP_NAME, window.location.origin);
-
-    const url = externalUrlMatch[1];
-    try {
-      const request = await fetch(window.decodeURIComponent(url));
-      const data = await loadFromBlob(await request.blob(), null, null);
-      if (
-        !scene.elements.length ||
-        (await openConfirmModal(shareableLinkConfirmDialog))
-      ) {
-        return { scene: data, isExternalScene };
-      }
-    } catch (error: any) {
-      return {
-        scene: {
-          appState: {
-            errorMessage: t("alerts.invalidSceneUrl"),
-          },
-        },
-        isExternalScene,
-      };
-    }
-  }
-
-  if (roomLinkData && opts.collabAPI) {
-    const { excalidrawAPI } = opts;
-
-    const scene = await opts.collabAPI.startCollaboration(roomLinkData);
-
-    return {
-      // when collaborating, the state may have already been updated at this
-      // point (we may have received updates from other clients), so reconcile
-      // elements and appState with existing state
-      scene: {
-        ...scene,
-        appState: {
-          ...restoreAppState(
-            {
-              ...scene?.appState,
-              theme: localDataState?.appState?.theme || scene?.appState?.theme,
-            },
-            excalidrawAPI.getAppState(),
-          ),
-          // necessary if we're invoking from a hashchange handler which doesn't
-          // go through App.initializeScene() that resets this flag
-          isLoading: false,
-        },
-        elements: reconcileElements(
-          scene?.elements || [],
-          excalidrawAPI.getSceneElementsIncludingDeleted(),
-          excalidrawAPI.getAppState(),
-        ),
-      },
-      isExternalScene: true,
-      id: roomLinkData.roomId,
-      key: roomLinkData.roomKey,
-    };
-  } else if (scene) {
-    return isExternalScene && jsonBackendMatch
-      ? {
-          scene,
-          isExternalScene,
-          id: jsonBackendMatch[1],
-          key: jsonBackendMatch[2],
-        }
-      : { scene, isExternalScene: false };
-  }
-  return { scene: null, isExternalScene: false };
-};
-
-const detectedLangCode = languageDetector.detect() || defaultLang.code;
-export const appLangCodeAtom = atom(
-  Array.isArray(detectedLangCode) ? detectedLangCode[0] : detectedLangCode,
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import ExcalidrawApp from "./App";
+import { registerSW } from "virtual:pwa-register";
+
+import "../excalidraw-app/sentry";
+window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA;
+const rootElement = document.getElementById("root")!;
+const root = createRoot(rootElement);
+registerSW();
+root.render(
+  <StrictMode>
+    <ExcalidrawApp />
+  </StrictMode>,
 );
 );
-
-const ExcalidrawWrapper = () => {
-  const [errorMessage, setErrorMessage] = useState("");
-  const [langCode, setLangCode] = useAtom(appLangCodeAtom);
-  const isCollabDisabled = isRunningInIframe();
-
-  // initial state
-  // ---------------------------------------------------------------------------
-
-  const initialStatePromiseRef = useRef<{
-    promise: ResolvablePromise<ExcalidrawInitialDataState | null>;
-  }>({ promise: null! });
-  if (!initialStatePromiseRef.current.promise) {
-    initialStatePromiseRef.current.promise =
-      resolvablePromise<ExcalidrawInitialDataState | null>();
-  }
-
-  useEffect(() => {
-    trackEvent("load", "frame", getFrame());
-    // Delayed so that the app has a time to load the latest SW
-    setTimeout(() => {
-      trackEvent("load", "version", getVersion());
-    }, VERSION_TIMEOUT);
-  }, []);
-
-  const [excalidrawAPI, excalidrawRefCallback] =
-    useCallbackRefState<ExcalidrawImperativeAPI>();
-
-  useMathSubtype(excalidrawAPI);
-
-  const [collabAPI] = useAtom(collabAPIAtom);
-  const [, setCollabDialogShown] = useAtom(collabDialogShownAtom);
-  const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
-    return isCollaborationLink(window.location.href);
-  });
-
-  useHandleLibrary({
-    excalidrawAPI,
-    getInitialLibraryItems: getLibraryItemsFromStorage,
-  });
-
-  useEffect(() => {
-    if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) {
-      return;
-    }
-
-    const loadImages = (
-      data: ResolutionType<typeof initializeScene>,
-      isInitialLoad = false,
-    ) => {
-      if (!data.scene) {
-        return;
-      }
-      if (collabAPI?.isCollaborating()) {
-        if (data.scene.elements) {
-          collabAPI
-            .fetchImageFilesFromFirebase({
-              elements: data.scene.elements,
-              forceFetchFiles: true,
-            })
-            .then(({ loadedFiles, erroredFiles }) => {
-              excalidrawAPI.addFiles(loadedFiles);
-              updateStaleImageStatuses({
-                excalidrawAPI,
-                erroredFiles,
-                elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
-              });
-            });
-        }
-      } else {
-        const fileIds =
-          data.scene.elements?.reduce((acc, element) => {
-            if (isInitializedImageElement(element)) {
-              return acc.concat(element.fileId);
-            }
-            return acc;
-          }, [] as FileId[]) || [];
-
-        if (data.isExternalScene) {
-          loadFilesFromFirebase(
-            `${FIREBASE_STORAGE_PREFIXES.shareLinkFiles}/${data.id}`,
-            data.key,
-            fileIds,
-          ).then(({ loadedFiles, erroredFiles }) => {
-            excalidrawAPI.addFiles(loadedFiles);
-            updateStaleImageStatuses({
-              excalidrawAPI,
-              erroredFiles,
-              elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
-            });
-          });
-        } else if (isInitialLoad) {
-          if (fileIds.length) {
-            LocalData.fileStorage
-              .getFiles(fileIds)
-              .then(({ loadedFiles, erroredFiles }) => {
-                if (loadedFiles.length) {
-                  excalidrawAPI.addFiles(loadedFiles);
-                }
-                updateStaleImageStatuses({
-                  excalidrawAPI,
-                  erroredFiles,
-                  elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
-                });
-              });
-          }
-          // on fresh load, clear unused files from IDB (from previous
-          // session)
-          LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds });
-        }
-      }
-    };
-
-    initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => {
-      loadImages(data, /* isInitialLoad */ true);
-      initialStatePromiseRef.current.promise.resolve(data.scene);
-    });
-
-    const onHashChange = async (event: HashChangeEvent) => {
-      event.preventDefault();
-      const libraryUrlTokens = parseLibraryTokensFromUrl();
-      if (!libraryUrlTokens) {
-        if (
-          collabAPI?.isCollaborating() &&
-          !isCollaborationLink(window.location.href)
-        ) {
-          collabAPI.stopCollaboration(false);
-        }
-        excalidrawAPI.updateScene({ appState: { isLoading: true } });
-
-        initializeScene({ collabAPI, excalidrawAPI }).then((data) => {
-          loadImages(data);
-          if (data.scene) {
-            excalidrawAPI.updateScene({
-              ...data.scene,
-              ...restore(data.scene, null, null, { repairBindings: true }),
-              commitToHistory: true,
-            });
-          }
-        });
-      }
-    };
-
-    const titleTimeout = setTimeout(
-      () => (document.title = APP_NAME),
-      TITLE_TIMEOUT,
-    );
-
-    const syncData = debounce(() => {
-      if (isTestEnv()) {
-        return;
-      }
-      if (
-        !document.hidden &&
-        ((collabAPI && !collabAPI.isCollaborating()) || isCollabDisabled)
-      ) {
-        // don't sync if local state is newer or identical to browser state
-        if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
-          const localDataState = importFromLocalStorage();
-          const username = importUsernameFromLocalStorage();
-          let langCode = languageDetector.detect() || defaultLang.code;
-          if (Array.isArray(langCode)) {
-            langCode = langCode[0];
-          }
-          setLangCode(langCode);
-          excalidrawAPI.updateScene({
-            ...localDataState,
-          });
-          excalidrawAPI.updateLibrary({
-            libraryItems: getLibraryItemsFromStorage(),
-          });
-          collabAPI?.setUsername(username || "");
-        }
-
-        if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) {
-          const elements = excalidrawAPI.getSceneElementsIncludingDeleted();
-          const currFiles = excalidrawAPI.getFiles();
-          const fileIds =
-            elements?.reduce((acc, element) => {
-              if (
-                isInitializedImageElement(element) &&
-                // only load and update images that aren't already loaded
-                !currFiles[element.fileId]
-              ) {
-                return acc.concat(element.fileId);
-              }
-              return acc;
-            }, [] as FileId[]) || [];
-          if (fileIds.length) {
-            LocalData.fileStorage
-              .getFiles(fileIds)
-              .then(({ loadedFiles, erroredFiles }) => {
-                if (loadedFiles.length) {
-                  excalidrawAPI.addFiles(loadedFiles);
-                }
-                updateStaleImageStatuses({
-                  excalidrawAPI,
-                  erroredFiles,
-                  elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
-                });
-              });
-          }
-        }
-      }
-    }, SYNC_BROWSER_TABS_TIMEOUT);
-
-    const onUnload = () => {
-      LocalData.flushSave();
-    };
-
-    const visibilityChange = (event: FocusEvent | Event) => {
-      if (event.type === EVENT.BLUR || document.hidden) {
-        LocalData.flushSave();
-      }
-      if (
-        event.type === EVENT.VISIBILITY_CHANGE ||
-        event.type === EVENT.FOCUS
-      ) {
-        syncData();
-      }
-    };
-
-    window.addEventListener(EVENT.HASHCHANGE, onHashChange, false);
-    window.addEventListener(EVENT.UNLOAD, onUnload, false);
-    window.addEventListener(EVENT.BLUR, visibilityChange, false);
-    document.addEventListener(EVENT.VISIBILITY_CHANGE, visibilityChange, false);
-    window.addEventListener(EVENT.FOCUS, visibilityChange, false);
-    return () => {
-      window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false);
-      window.removeEventListener(EVENT.UNLOAD, onUnload, false);
-      window.removeEventListener(EVENT.BLUR, visibilityChange, false);
-      window.removeEventListener(EVENT.FOCUS, visibilityChange, false);
-      document.removeEventListener(
-        EVENT.VISIBILITY_CHANGE,
-        visibilityChange,
-        false,
-      );
-      clearTimeout(titleTimeout);
-    };
-  }, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode]);
-
-  useEffect(() => {
-    const unloadHandler = (event: BeforeUnloadEvent) => {
-      LocalData.flushSave();
-
-      if (
-        excalidrawAPI &&
-        LocalData.fileStorage.shouldPreventUnload(
-          excalidrawAPI.getSceneElements(),
-        )
-      ) {
-        preventUnload(event);
-      }
-    };
-    window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
-    return () => {
-      window.removeEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
-    };
-  }, [excalidrawAPI]);
-
-  useEffect(() => {
-    languageDetector.cacheUserLanguage(langCode);
-  }, [langCode]);
-
-  const [theme, setTheme] = useState<Theme>(
-    () =>
-      (localStorage.getItem(
-        STORAGE_KEYS.LOCAL_STORAGE_THEME,
-      ) as Theme | null) ||
-      // FIXME migration from old LS scheme. Can be removed later. #5660
-      importFromLocalStorage().appState?.theme ||
-      THEME.LIGHT,
-  );
-
-  useEffect(() => {
-    localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_THEME, theme);
-    // currently only used for body styling during init (see public/index.html),
-    // but may change in the future
-    document.documentElement.classList.toggle("dark", theme === THEME.DARK);
-  }, [theme]);
-
-  const onChange = (
-    elements: readonly ExcalidrawElement[],
-    appState: AppState,
-    files: BinaryFiles,
-  ) => {
-    if (collabAPI?.isCollaborating()) {
-      collabAPI.syncElements(elements);
-    }
-
-    setTheme(appState.theme);
-
-    // this check is redundant, but since this is a hot path, it's best
-    // not to evaludate the nested expression every time
-    if (!LocalData.isSavePaused()) {
-      LocalData.save(elements, appState, files, () => {
-        if (excalidrawAPI) {
-          let didChange = false;
-
-          const elements = excalidrawAPI
-            .getSceneElementsIncludingDeleted()
-            .map((element) => {
-              if (
-                LocalData.fileStorage.shouldUpdateImageElementStatus(element)
-              ) {
-                const newElement = newElementWith(element, { status: "saved" });
-                if (newElement !== element) {
-                  didChange = true;
-                }
-                return newElement;
-              }
-              return element;
-            });
-
-          if (didChange) {
-            excalidrawAPI.updateScene({
-              elements,
-            });
-          }
-        }
-      });
-    }
-  };
-
-  const [latestShareableLink, setLatestShareableLink] = useState<string | null>(
-    null,
-  );
-
-  const onExportToBackend = async (
-    exportedElements: readonly NonDeletedExcalidrawElement[],
-    appState: Partial<AppState>,
-    files: BinaryFiles,
-    canvas: HTMLCanvasElement,
-  ) => {
-    if (exportedElements.length === 0) {
-      throw new Error(t("alerts.cannotExportEmptyCanvas"));
-    }
-    if (canvas) {
-      try {
-        const { url, errorMessage } = await exportToBackend(
-          exportedElements,
-          {
-            ...appState,
-            viewBackgroundColor: appState.exportBackground
-              ? appState.viewBackgroundColor
-              : getDefaultAppState().viewBackgroundColor,
-          },
-          files,
-        );
-
-        if (errorMessage) {
-          throw new Error(errorMessage);
-        }
-
-        if (url) {
-          setLatestShareableLink(url);
-        }
-      } catch (error: any) {
-        if (error.name !== "AbortError") {
-          const { width, height } = canvas;
-          console.error(error, { width, height });
-          throw new Error(error.message);
-        }
-      }
-    }
-  };
-
-  const renderCustomStats = (
-    elements: readonly NonDeletedExcalidrawElement[],
-    appState: UIAppState,
-  ) => {
-    return (
-      <CustomStats
-        setToast={(message) => excalidrawAPI!.setToast({ message })}
-        appState={appState}
-        elements={elements}
-      />
-    );
-  };
-
-  const onLibraryChange = async (items: LibraryItems) => {
-    if (!items.length) {
-      localStorage.removeItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
-      return;
-    }
-    const serializedItems = JSON.stringify(items);
-    localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
-  };
-
-  const isOffline = useAtomValue(isOfflineAtom);
-
-  // browsers generally prevent infinite self-embedding, there are
-  // cases where it still happens, and while we disallow self-embedding
-  // by not whitelisting our own origin, this serves as an additional guard
-  if (isSelfEmbedding) {
-    return (
-      <div
-        style={{
-          display: "flex",
-          alignItems: "center",
-          justifyContent: "center",
-          textAlign: "center",
-          height: "100%",
-        }}
-      >
-        <h1>I'm not a pretzel!</h1>
-      </div>
-    );
-  }
-
-  return (
-    <div
-      style={{ height: "100%" }}
-      className={clsx("excalidraw-app", {
-        "is-collaborating": isCollaborating,
-      })}
-    >
-      <Excalidraw
-        excalidrawAPI={excalidrawRefCallback}
-        onChange={onChange}
-        initialData={initialStatePromiseRef.current.promise}
-        isCollaborating={isCollaborating}
-        onPointerUpdate={collabAPI?.onPointerUpdate}
-        UIOptions={{
-          canvasActions: {
-            toggleTheme: true,
-            export: {
-              onExportToBackend,
-              renderCustomUI: (elements, appState, files) => {
-                return (
-                  <ExportToExcalidrawPlus
-                    elements={elements}
-                    appState={appState}
-                    files={files}
-                    onError={(error) => {
-                      excalidrawAPI?.updateScene({
-                        appState: {
-                          errorMessage: error.message,
-                        },
-                      });
-                    }}
-                    onSuccess={() => {
-                      excalidrawAPI?.updateScene({
-                        appState: { openDialog: null },
-                      });
-                    }}
-                  />
-                );
-              },
-            },
-          },
-        }}
-        langCode={langCode}
-        renderCustomStats={renderCustomStats}
-        detectScroll={false}
-        handleKeyboardGlobally={true}
-        onLibraryChange={onLibraryChange}
-        autoFocus={true}
-        theme={theme}
-        renderTopRightUI={(isMobile) => {
-          if (isMobile || !collabAPI || isCollabDisabled) {
-            return null;
-          }
-          return (
-            <LiveCollaborationTrigger
-              isCollaborating={isCollaborating}
-              onSelect={() => setCollabDialogShown(true)}
-            />
-          );
-        }}
-      >
-        <AppMainMenu
-          setCollabDialogShown={setCollabDialogShown}
-          isCollaborating={isCollaborating}
-          isCollabEnabled={!isCollabDisabled}
-        />
-        <AppWelcomeScreen
-          setCollabDialogShown={setCollabDialogShown}
-          isCollabEnabled={!isCollabDisabled}
-        />
-        <OverwriteConfirmDialog>
-          <OverwriteConfirmDialog.Actions.ExportToImage />
-          <OverwriteConfirmDialog.Actions.SaveToDisk />
-          {excalidrawAPI && (
-            <OverwriteConfirmDialog.Action
-              title={t("overwriteConfirm.action.excalidrawPlus.title")}
-              actionLabel={t("overwriteConfirm.action.excalidrawPlus.button")}
-              onClick={() => {
-                exportToExcalidrawPlus(
-                  excalidrawAPI.getSceneElements(),
-                  excalidrawAPI.getAppState(),
-                  excalidrawAPI.getFiles(),
-                );
-              }}
-            >
-              {t("overwriteConfirm.action.excalidrawPlus.description")}
-            </OverwriteConfirmDialog.Action>
-          )}
-        </OverwriteConfirmDialog>
-        <AppFooter />
-        <TTDDialog
-          onTextSubmit={async (input) => {
-            try {
-              const response = await fetch(
-                `${
-                  import.meta.env.VITE_APP_AI_BACKEND
-                }/v1/ai/text-to-diagram/generate`,
-                {
-                  method: "POST",
-                  headers: {
-                    Accept: "application/json",
-                    "Content-Type": "application/json",
-                  },
-                  body: JSON.stringify({ prompt: input }),
-                },
-              );
-
-              const rateLimit = response.headers.has("X-Ratelimit-Limit")
-                ? parseInt(response.headers.get("X-Ratelimit-Limit") || "0", 10)
-                : undefined;
-
-              const rateLimitRemaining = response.headers.has(
-                "X-Ratelimit-Remaining",
-              )
-                ? parseInt(
-                    response.headers.get("X-Ratelimit-Remaining") || "0",
-                    10,
-                  )
-                : undefined;
-
-              const json = await response.json();
-
-              if (!response.ok) {
-                if (response.status === 429) {
-                  return {
-                    rateLimit,
-                    rateLimitRemaining,
-                    error: new Error(
-                      "Too many requests today, please try again tomorrow!",
-                    ),
-                  };
-                }
-
-                throw new Error(json.message || "Generation failed...");
-              }
-
-              const generatedResponse = json.generatedResponse;
-              if (!generatedResponse) {
-                throw new Error("Generation failed...");
-              }
-
-              return { generatedResponse, rateLimit, rateLimitRemaining };
-            } catch (err: any) {
-              throw new Error("Request failed");
-            }
-          }}
-        />
-        <TTDDialogTrigger />
-        {isCollaborating && isOffline && (
-          <div className="collab-offline-warning">
-            {t("alerts.collabOfflineWarning")}
-          </div>
-        )}
-        {latestShareableLink && (
-          <ShareableLinkDialog
-            link={latestShareableLink}
-            onCloseRequest={() => setLatestShareableLink(null)}
-            setErrorMessage={setErrorMessage}
-          />
-        )}
-        {excalidrawAPI && !isCollabDisabled && (
-          <Collab excalidrawAPI={excalidrawAPI} />
-        )}
-        {errorMessage && (
-          <ErrorDialog onClose={() => setErrorMessage("")}>
-            {errorMessage}
-          </ErrorDialog>
-        )}
-      </Excalidraw>
-    </div>
-  );
-};
-
-const ExcalidrawApp = () => {
-  return (
-    <TopErrorBoundary>
-      <Provider unstable_createStore={() => appJotaiStore}>
-        <ExcalidrawWrapper />
-      </Provider>
-    </TopErrorBoundary>
-  );
-};
-
-export default ExcalidrawApp;

+ 40 - 0
excalidraw-app/package.json

@@ -0,0 +1,40 @@
+{
+  "name": "excalidraw-app",
+  "version": "1.0.0",
+  "private": true,
+  "homepage": ".",
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not ie <= 11",
+      "not op_mini all",
+      "not safari < 12",
+      "not kaios <= 2.5",
+      "not edge < 79",
+      "not chrome < 70",
+      "not and_uc < 13",
+      "not samsung < 10"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  },
+  "engines": {
+    "node": ">=18.0.0"
+  },
+  "dependencies": {},
+  "prettier": "@excalidraw/prettier-config",
+  "scripts": {
+    "build-node": "node ./scripts/build-node.js",
+    "build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true VITE_APP_DISABLE_TRACKING=true vite build",
+    "build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
+    "build:version": "node ../scripts/build-version.js",
+    "build": "yarn build:app && yarn build:version",
+    "start": "yarn && vite",
+    "start:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
+    "build:preview": "yarn build && vite preview --port 5000"
+  }
+}

+ 23 - 6
excalidraw-app/collab/RoomDialog.scss → excalidraw-app/share/ShareDialog.scss

@@ -1,7 +1,7 @@
-@import "../../src/css/variables.module";
+@import "../../packages/excalidraw/css/variables.module.scss";
 
 
 .excalidraw {
 .excalidraw {
-  .RoomDialog {
+  .ShareDialog {
     display: flex;
     display: flex;
     flex-direction: column;
     flex-direction: column;
     gap: 1.5rem;
     gap: 1.5rem;
@@ -10,8 +10,25 @@
       height: calc(100vh - 5rem);
       height: calc(100vh - 5rem);
     }
     }
 
 
+    &__separator {
+      border-top: 1px solid var(--dialog-border-color);
+      text-align: center;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      margin-top: 1em;
+
+      span {
+        background: var(--island-bg-color);
+        padding: 0px 0.75rem;
+        transform: translateY(-1ch);
+        display: inline-flex;
+        line-height: 1;
+      }
+    }
+
     &__popover {
     &__popover {
-      @keyframes RoomDialog__popover__scaleIn {
+      @keyframes ShareDialog__popover__scaleIn {
         from {
         from {
           opacity: 0;
           opacity: 0;
         }
         }
@@ -50,10 +67,10 @@
       }
       }
 
 
       transform-origin: var(--radix-popover-content-transform-origin);
       transform-origin: var(--radix-popover-content-transform-origin);
-      animation: RoomDialog__popover__scaleIn 150ms ease-out;
+      animation: ShareDialog__popover__scaleIn 150ms ease-out;
     }
     }
 
 
-    &__inactive {
+    &__picker {
       font-family: "Assistant";
       font-family: "Assistant";
 
 
       &__illustration {
       &__illustration {
@@ -95,7 +112,7 @@
         }
         }
       }
       }
 
 
-      &__start_session {
+      &__button {
         display: flex;
         display: flex;
 
 
         align-items: center;
         align-items: center;

+ 290 - 0
excalidraw-app/share/ShareDialog.tsx

@@ -0,0 +1,290 @@
+import { useRef, useState } from "react";
+import * as Popover from "@radix-ui/react-popover";
+import { copyTextToSystemClipboard } from "../../packages/excalidraw/clipboard";
+import { trackEvent } from "../../packages/excalidraw/analytics";
+import { getFrame } from "../../packages/excalidraw/utils";
+import { useI18n } from "../../packages/excalidraw/i18n";
+import { KEYS } from "../../packages/excalidraw/keys";
+import { Dialog } from "../../packages/excalidraw/components/Dialog";
+import {
+  copyIcon,
+  LinkIcon,
+  playerPlayIcon,
+  playerStopFilledIcon,
+  share,
+  shareIOS,
+  shareWindows,
+  tablerCheckIcon,
+} from "../../packages/excalidraw/components/icons";
+import { TextField } from "../../packages/excalidraw/components/TextField";
+import { FilledButton } from "../../packages/excalidraw/components/FilledButton";
+import { activeRoomLinkAtom, CollabAPI } from "../collab/Collab";
+import { atom, useAtom, useAtomValue } from "jotai";
+
+import "./ShareDialog.scss";
+
+type OnExportToBackend = () => void;
+type ShareDialogType = "share" | "collaborationOnly";
+
+export const shareDialogStateAtom = atom<
+  { isOpen: false } | { isOpen: true; type: ShareDialogType }
+>({ isOpen: false });
+
+const getShareIcon = () => {
+  const navigator = window.navigator as any;
+  const isAppleBrowser = /Apple/.test(navigator.vendor);
+  const isWindowsBrowser = navigator.appVersion.indexOf("Win") !== -1;
+
+  if (isAppleBrowser) {
+    return shareIOS;
+  } else if (isWindowsBrowser) {
+    return shareWindows;
+  }
+
+  return share;
+};
+
+export type ShareDialogProps = {
+  collabAPI: CollabAPI | null;
+  handleClose: () => void;
+  onExportToBackend: OnExportToBackend;
+  type: ShareDialogType;
+};
+
+const ActiveRoomDialog = ({
+  collabAPI,
+  activeRoomLink,
+  handleClose,
+}: {
+  collabAPI: CollabAPI;
+  activeRoomLink: string;
+  handleClose: () => void;
+}) => {
+  const { t } = useI18n();
+  const [justCopied, setJustCopied] = useState(false);
+  const timerRef = useRef<number>(0);
+  const ref = useRef<HTMLInputElement>(null);
+  const isShareSupported = "share" in navigator;
+
+  const copyRoomLink = async () => {
+    try {
+      await copyTextToSystemClipboard(activeRoomLink);
+
+      setJustCopied(true);
+
+      if (timerRef.current) {
+        window.clearTimeout(timerRef.current);
+      }
+
+      timerRef.current = window.setTimeout(() => {
+        setJustCopied(false);
+      }, 3000);
+    } catch (error: any) {
+      collabAPI.setErrorMessage(error.message);
+    }
+
+    ref.current?.select();
+  };
+
+  const shareRoomLink = async () => {
+    try {
+      await navigator.share({
+        title: t("roomDialog.shareTitle"),
+        text: t("roomDialog.shareTitle"),
+        url: activeRoomLink,
+      });
+    } catch (error: any) {
+      // Just ignore.
+    }
+  };
+
+  return (
+    <>
+      <h3 className="ShareDialog__active__header">
+        {t("labels.liveCollaboration").replace(/\./g, "")}
+      </h3>
+      <TextField
+        defaultValue={collabAPI.getUsername()}
+        placeholder="Your name"
+        label="Your name"
+        onChange={collabAPI.setUsername}
+        onKeyDown={(event) => event.key === KEYS.ENTER && handleClose()}
+      />
+      <div className="ShareDialog__active__linkRow">
+        <TextField
+          ref={ref}
+          label="Link"
+          readonly
+          fullWidth
+          value={activeRoomLink}
+        />
+        {isShareSupported && (
+          <FilledButton
+            size="large"
+            variant="icon"
+            label="Share"
+            icon={getShareIcon()}
+            className="ShareDialog__active__share"
+            onClick={shareRoomLink}
+          />
+        )}
+        <Popover.Root open={justCopied}>
+          <Popover.Trigger asChild>
+            <FilledButton
+              size="large"
+              label="Copy link"
+              icon={copyIcon}
+              onClick={copyRoomLink}
+            />
+          </Popover.Trigger>
+          <Popover.Content
+            onOpenAutoFocus={(event) => event.preventDefault()}
+            onCloseAutoFocus={(event) => event.preventDefault()}
+            className="ShareDialog__popover"
+            side="top"
+            align="end"
+            sideOffset={5.5}
+          >
+            {tablerCheckIcon} copied
+          </Popover.Content>
+        </Popover.Root>
+      </div>
+      <div className="ShareDialog__active__description">
+        <p>
+          <span
+            role="img"
+            aria-hidden="true"
+            className="ShareDialog__active__description__emoji"
+          >
+            🔒{" "}
+          </span>
+          {t("roomDialog.desc_privacy")}
+        </p>
+        <p>{t("roomDialog.desc_exitSession")}</p>
+      </div>
+
+      <div className="ShareDialog__active__actions">
+        <FilledButton
+          size="large"
+          variant="outlined"
+          color="danger"
+          label={t("roomDialog.button_stopSession")}
+          icon={playerStopFilledIcon}
+          onClick={() => {
+            trackEvent("share", "room closed");
+            collabAPI.stopCollaboration();
+            if (!collabAPI.isCollaborating()) {
+              handleClose();
+            }
+          }}
+        />
+      </div>
+    </>
+  );
+};
+
+const ShareDialogPicker = (props: ShareDialogProps) => {
+  const { t } = useI18n();
+
+  const { collabAPI } = props;
+
+  const startCollabJSX = collabAPI ? (
+    <>
+      <div className="ShareDialog__picker__header">
+        {t("labels.liveCollaboration").replace(/\./g, "")}
+      </div>
+
+      <div className="ShareDialog__picker__description">
+        <div style={{ marginBottom: "1em" }}>{t("roomDialog.desc_intro")}</div>
+        {t("roomDialog.desc_privacy")}
+      </div>
+
+      <div className="ShareDialog__picker__button">
+        <FilledButton
+          size="large"
+          label={t("roomDialog.button_startSession")}
+          icon={playerPlayIcon}
+          onClick={() => {
+            trackEvent("share", "room creation", `ui (${getFrame()})`);
+            collabAPI.startCollaboration(null);
+          }}
+        />
+      </div>
+
+      {props.type === "share" && (
+        <div className="ShareDialog__separator">
+          <span>{t("shareDialog.or")}</span>
+        </div>
+      )}
+    </>
+  ) : null;
+
+  return (
+    <>
+      {startCollabJSX}
+
+      {props.type === "share" && (
+        <>
+          <div className="ShareDialog__picker__header">
+            {t("exportDialog.link_title")}
+          </div>
+          <div className="ShareDialog__picker__description">
+            {t("exportDialog.link_details")}
+          </div>
+
+          <div className="ShareDialog__picker__button">
+            <FilledButton
+              size="large"
+              label={t("exportDialog.link_button")}
+              icon={LinkIcon}
+              onClick={async () => {
+                await props.onExportToBackend();
+                props.handleClose();
+              }}
+            />
+          </div>
+        </>
+      )}
+    </>
+  );
+};
+
+const ShareDialogInner = (props: ShareDialogProps) => {
+  const activeRoomLink = useAtomValue(activeRoomLinkAtom);
+
+  return (
+    <Dialog size="small" onCloseRequest={props.handleClose} title={false}>
+      <div className="ShareDialog">
+        {props.collabAPI && activeRoomLink ? (
+          <ActiveRoomDialog
+            collabAPI={props.collabAPI}
+            activeRoomLink={activeRoomLink}
+            handleClose={props.handleClose}
+          />
+        ) : (
+          <ShareDialogPicker {...props} />
+        )}
+      </div>
+    </Dialog>
+  );
+};
+
+export const ShareDialog = (props: {
+  collabAPI: CollabAPI | null;
+  onExportToBackend: OnExportToBackend;
+}) => {
+  const [shareDialogState, setShareDialogState] = useAtom(shareDialogStateAtom);
+
+  if (!shareDialogState.isOpen) {
+    return null;
+  }
+
+  return (
+    <ShareDialogInner
+      handleClose={() => setShareDialogState({ isOpen: false })}
+      collabAPI={props.collabAPI}
+      onExportToBackend={props.onExportToBackend}
+      type={shareDialogState.type}
+    ></ShareDialogInner>
+  );
+};

+ 9 - 4
excalidraw-app/tests/LanguageList.test.tsx

@@ -1,8 +1,13 @@
-import { defaultLang } from "../../src/i18n";
-import { UI } from "../../src/tests/helpers/ui";
-import { screen, fireEvent, waitFor, render } from "../../src/tests/test-utils";
+import { defaultLang } from "../../packages/excalidraw/i18n";
+import { UI } from "../../packages/excalidraw/tests/helpers/ui";
+import {
+  screen,
+  fireEvent,
+  waitFor,
+  render,
+} from "../../packages/excalidraw/tests/test-utils";
 
 
-import ExcalidrawApp from "../../excalidraw-app";
+import ExcalidrawApp from "../App";
 
 
 describe("Test LanguageList", () => {
 describe("Test LanguageList", () => {
   it("rerenders UI on language change", async () => {
   it("rerenders UI on language change", async () => {

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels