Browse Source

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

Daniel J. Geiger 1 year ago
parent
commit
bff220e0f5
100 changed files with 3152 additions and 1484 deletions
  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
 !package.json
 !public/
-!src/
+!packages/
 !tsconfig.json
 !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)
 VITE_APP_WS_SERVER_URL=http://localhost:3002
 
-# set this only if using the collaboration workflow we use on excalidraw.com
-VITE_APP_PORTAL_URL=
-
 VITE_APP_PLUS_LP=https://plus.excalidraw.com
 VITE_APP_PLUS_APP=https://app.excalidraw.com
 

+ 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_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
 
-VITE_APP_PORTAL_URL=https://portal.excalidraw.com
-
 VITE_APP_PLUS_LP=https://plus.excalidraw.com
 VITE_APP_PLUS_APP=https://app.excalidraw.com
 
 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"}'
 

+ 1 - 1
.eslintignore

@@ -5,4 +5,4 @@ package-lock.json
 firebase/
 dist/
 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 }}
       - name: Auto release
         run: |
-          yarn add @actions/core
+          yarn add @actions/core -W
           yarn autorelease

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

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

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

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

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

@@ -22,11 +22,11 @@ jobs:
       - name: Create report file
         run: |
           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
             git config --global user.name 'Excalidraw Bot'
             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 push
           fi

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

@@ -15,16 +15,14 @@ jobs:
         uses: actions/setup-node@v3
         with:
           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:
           CI: true
       - uses: andresz1/size-limit-action@v1
         with:
           github_token: ${{ secrets.GITHUB_TOKEN }}
-          build_script: build:umd
+          build_script: build:esm
           skip_step: install
-          directory: src/packages/excalidraw
+          directory: packages/excalidraw

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

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

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

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

+ 2 - 4
.gitignore

@@ -21,10 +21,8 @@ npm-debug.log*
 package-lock.json
 yarn-debug.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
 dev-dist
 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
 
-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
@@ -97,7 +97,7 @@ or via yarn
 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
 

+ 2 - 2
crowdin.yml

@@ -1,3 +1,3 @@
 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
 

+ 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`](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 **
 

+ 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.
 
-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
 
@@ -19,7 +19,7 @@ convertToExcalidrawElements(
 
 | 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`. |
 
 **_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
 
@@ -192,7 +192,7 @@ convertToExcalidrawElements([
 
 ### 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.
 
@@ -326,7 +326,7 @@ convertToExcalidrawElements([
 
 ### 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
 convertToExcalidrawElements([

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

@@ -2,7 +2,7 @@
 
 <pre>
   (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
   </a>
   ) => 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 |
 | --- | --- | --- |
@@ -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 |
 | [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 |
-| [toggleMenu](#togglemenu) | `function` | Toggles specific menus on/off |
+| [toggleSidebar](#toggleSidebar) | `function` | Toggles specific sidebar on/off |
 | [onChange](#onChange) | `function` | Subscribes to change events |
 | [onPointerDown](#onPointerDown) | `function` | Subscribes to `pointerdown` events |
 | [onPointerUp](#onPointerUp) | `function` | Subscribes to `pointerup` events |
@@ -52,7 +52,7 @@ Additionally `ready` and `readyPromise` from the API have been discontinued. The
 
 <pre>
   (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
   </a>
   ) => void
@@ -62,9 +62,9 @@ You can use this function to update the scene with the sceneData. It accepts the
 
 | 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`. |
 
 ```jsx live
@@ -125,13 +125,13 @@ function App() {
 
 <pre>
   (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
   </a>
   ;<br /> merge?: boolean; <br /> prompt?: boolean;
   <br /> openLibraryMenu?: boolean;
   <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
   </a>
   &gt;
@@ -141,7 +141,7 @@ You can use this function to update the library. It accepts the below attributes
 
 | 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. |
 | `prompt` | boolean | `false` | Whether to prompt user for confirmation. |
 | `openLibraryMenu` | boolean | `false` | Keep the library menu open after library is updated. |
@@ -189,7 +189,7 @@ function App() {
       </button>
       <Excalidraw
         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={{
           libraryItems: initialData.libraryItems,
           appState: { openSidebar: "library" },
@@ -204,7 +204,7 @@ function App() {
 
 <pre>
   (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
   </a>
   ) => void
@@ -224,7 +224,7 @@ Resets the scene. If `resetLoadingState` is passed as true then it will also for
 
 <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[]
   </a>
 </pre>
@@ -235,7 +235,7 @@ Returns all the elements including the deleted in the scene.
 
 <pre>
   () => 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
   </a>
   []&#62;
@@ -247,7 +247,7 @@ Returns all the elements excluding the deleted in the scene
 
 <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
   </a>
 </pre>
@@ -288,7 +288,7 @@ Scroll the nearest element out of the elements supplied to the center of the vie
 
 | 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.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%) |
@@ -336,7 +336,7 @@ The unique id of the excalidraw component. This can be used to identify the exca
 
 <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
   </a>
 </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 |
 | --- | --- | --- | --- |
-| `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 |
 
 ## setCursor

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

@@ -1,18 +1,18 @@
 # initialData
 
 <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>
 
 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 |
 | --- | --- | --- |
-| `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 |
-| `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.
 

+ 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 |
 | [`theme`](#theme) | `"light"` &#124; `"dark"` | `"light"` | The theme of the Excalidraw component |
 | [`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. |
 | [`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 |
@@ -33,7 +33,7 @@ All `props` are _optional_.
 
 ### 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.
 
@@ -59,11 +59,11 @@ Every time component updates, this callback if passed will get triggered and has
 (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.
 
@@ -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"]`
 
-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
 (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.
 
 ### onPointerDown
@@ -96,11 +96,11 @@ This prop if passed will be triggered on pointer down events and has the below s
 
 <pre>
 (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"]
   </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
   </a>) => void
 </pre>
@@ -119,7 +119,7 @@ This callback is triggered if passed when something is pasted into the scene. Yo
 
 <pre>
   (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
   </a>
   , event: ClipboardEvent &#124; null) => boolean
@@ -135,7 +135,7 @@ This callback if supplied will get triggered when the library is updated and has
 
 <pre>
   (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
   </a>
   ) => 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>
   (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
   </a>
   , event: CustomEvent&lt;&#123; nativeEvent: MouseEvent }&gt;) => void
@@ -182,7 +182,7 @@ const onLinkOpen: ExcalidrawProps["onLinkOpen"] = useCallback(
 
 ### 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
 import { defaultLang, languages } from "@excalidraw/excalidraw";
@@ -191,7 +191,7 @@ import { defaultLang, languages } from "@excalidraw/excalidraw";
 | name | type |
 | --- | --- |
 | `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
 

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

@@ -4,7 +4,7 @@
 
 <pre>
   (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
   </a>) => JSX | null
 </pre>
@@ -66,7 +66,7 @@ function App() {
 
 <pre>
   (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
   </a>
   ) => 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>
   &#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
   </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
 
-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 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
 
-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.
 
 | 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;
   files,<br/>&nbsp;
   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>
 
 | 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. |
 | `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. |
 
 
@@ -105,7 +105,7 @@ function App() {
 
 <pre>
 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;
   quality?: number,<br/>&nbsp;
   exportPadding?: number;<br/>
@@ -134,16 +134,16 @@ Returns a promise which resolves with a [blob](https://developer.mozilla.org/en-
 <pre>
 exportToSvg(&#123;<br/>&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[]
     </a>,<br/>&nbsp;
   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;
   exportPadding: number,<br/>&nbsp;
   metadata: string,<br/>&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
     </a>,<br/>
 &#125;);
@@ -151,10 +151,10 @@ exportToSvg(&#123;<br/>&nbsp;
 
 | 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 |
-| 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.
 
@@ -164,7 +164,7 @@ This function returns a promise which resolves to `svg` of the exported drawing.
 
 <pre>
 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;
   quality?: number;<br/>&nbsp;
   type: 'png' | 'svg' |'json'<br/>

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

@@ -8,7 +8,7 @@ id: "restore"
 **_Signature_**
 
 <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>
 
 **_How to use_**
@@ -17,7 +17,7 @@ restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob
 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.  
 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>
 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/>
 )
 </pre>
 
 | 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
 
 #### localElements
@@ -70,15 +70,15 @@ Parameter `refreshDimensions` indicates whether we should also `recalculate` tex
 
 <pre>
 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/>
 
 )
 </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_**
 
@@ -93,7 +93,7 @@ This function makes sure elements and state is set to appropriate values and set
 **_Signature_**
 
 <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")
 </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
 
-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.
 
@@ -16,8 +16,8 @@ If you want to overwrite the `source` field in the `JSON` string, you can set `w
 
 <pre>
 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
 </pre>
 
@@ -37,7 +37,7 @@ If you want to overwrite the source field in the JSON string, you can set `windo
 
 <pre>
 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>
 
 **How to use**
@@ -53,7 +53,7 @@ Returns `true` if element is invisibly small (e.g. width & height are zero).
 **_Signature_**
 
 <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>
 
 **How to use**
@@ -80,10 +80,10 @@ excalidrawAPI.updateScene(scene);
 <pre>
 loadFromBlob(<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/>
-) => 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>
 
 ### loadLibraryFromBlob
@@ -130,10 +130,10 @@ if (contents.type === MIME_TYPES.excalidraw) {
 <pre>
 loadSceneOrLibraryFromBlob(<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/>
-) => 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>
 
 ### getFreeDrawSvgPath
@@ -149,7 +149,7 @@ import { getFreeDrawSvgPath } from "@excalidraw/excalidraw";
 **Signature**
 
 <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>
 
 ### isLinearElement
@@ -165,7 +165,7 @@ import { isLinearElement } from "@excalidraw/excalidraw";
 **Signature**
 
 <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>
 
 ### getNonDeletedElements
@@ -181,7 +181,7 @@ import { getNonDeletedElements } from "@excalidraw/excalidraw";
 **Signature**
 
 <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>
 
 ### mergeLibraryItems
@@ -196,9 +196,9 @@ import { mergeLibraryItems } from "@excalidraw/excalidraw";
 
 <pre>
 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>
 
 ### parseLibraryTokensFromUrl
@@ -239,8 +239,8 @@ export const App = () => {
 
 <pre>
 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>
 
@@ -253,7 +253,7 @@ This function returns the current `scene` version.
 **_Signature_**
 
 <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>
 
 **How to use**
@@ -274,7 +274,7 @@ import { sceneCoordsToViewportCoords } from "@excalidraw/excalidraw";
 
 <pre>
 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>
 
 ### viewportCoordsToSceneCoords
@@ -289,7 +289,7 @@ import { viewportCoordsToSceneCoords } from "@excalidraw/excalidraw";
 
 <pre>
 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>
 
 ### useDevice
@@ -350,8 +350,8 @@ To help with localization, we export the following.
 | name | type |
 | --- | --- |
 | `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
 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-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
 .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
 
    ```bash
-   cd src/packages/excalidraw && yarn
+   cd packages/excalidraw && yarn
    ```
 
 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: {
-    "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
 
-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
 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)
 
@@ -93,7 +150,7 @@ Since Vite removes env variables by default, you can update the vite config to e
 
 ```
  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>
       <div id="app"></div>
     </div>
-    <script type="text/javascript" src="src/index.js"></script>
+    <script type="text/javascript" src="packages/excalidraw/index.js"></script>
   </body>
 </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
 
-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!
 

+ 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)
 
-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
@@ -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:
 

+ 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
 
-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 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)

+ 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
 - **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
 
 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,
         },
         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%;
     }
   }
+  .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 {

+ 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 type * as TExcalidraw from "../index";
+import type * as TExcalidraw from "@excalidraw/excalidraw";
 
-import "./App.scss";
-import initialData from "./initialData";
 import { nanoid } from "nanoid";
+
 import {
   resolvablePromise,
   ResolvablePromise,
+  distance2d,
+  fileOpen,
   withBatchedUpdates,
   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,
   BinaryFileData,
   ExcalidrawImperativeAPI,
@@ -25,18 +33,14 @@ import {
   Gesture,
   LibraryItems,
   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 = {
   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_INPUT_HEIGHT = 50;
 const COMMENT_INPUT_WIDTH = 150;
@@ -88,9 +69,38 @@ export interface AppProps {
   appTitle: string;
   useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void;
   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 [viewModeEnabled, setViewModeEnabled] = useState(false);
   const [zenModeEnabled, setZenModeEnabled] = useState(false);
@@ -152,8 +162,105 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
       };
     };
     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) => {
     return (
       <>
@@ -337,8 +444,8 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
     pointerDownState: PointerDownState,
   ) => {
     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" });
       const distance = distance2d(
         pointerDownState.x,
@@ -402,8 +509,8 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
               onPointerMoveFromPointerDownHandler(pointerDownState);
             const onPointerUp =
               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.onUp = onPointerUp;
@@ -495,7 +602,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
         }}
         onBlur={saveComment}
         onKeyDown={(event) => {
-          if (!event.shiftKey && event.key === KEYS.ENTER) {
+          if (!event.shiftKey && event.key === "Enter") {
             event.preventDefault();
             saveComment();
           }
@@ -528,7 +635,12 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
         </MainMenu.ItemCustom>
         <MainMenu.DefaultItems.Help />
 
-        {excalidrawAPI && <MobileFooter excalidrawAPI={excalidrawAPI} />}
+        {excalidrawAPI && (
+          <MobileFooter
+            excalidrawLib={excalidrawLib}
+            excalidrawAPI={excalidrawAPI}
+          />
+        )}
       </MainMenu>
     );
   };
@@ -677,83 +789,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
           </div>
         </div>
         <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()}
           {comment && renderComment()}
         </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 = (
   <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>
   </svg>
 );
+
 const CustomFooter = ({
   excalidrawAPI,
+  excalidrawLib,
 }: {
   excalidrawAPI: ExcalidrawImperativeAPI;
+  excalidrawLib: typeof TExcalidraw;
 }) => {
+  const { Button, MIME_TYPES } = excalidrawLib;
+
   return (
     <>
       <Button
         onSelect={() => alert("General Kenobi!")}
-        className="you are a bold one"
-        style={{ marginLeft: "1rem" }}
+        style={{ marginLeft: "1rem", width: "auto" }}
         title="Hello there!"
       >
-        {COMMENT_SVG}
+        Hit me
       </Button>
-      <button
+      <Button
         className="custom-element"
-        onClick={() => {
+        onSelect={() => {
           excalidrawAPI?.setActiveTool({
             type: "custom",
             customType: "comment",
@@ -58,15 +61,10 @@ const CustomFooter = ({
           )}`;
           excalidrawAPI?.setCursor(`url(${url}), auto`);
         }}
+        title="Comments!"
       >
         {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";
+
 export default function Sidebar({ children }: { children: React.ReactNode }) {
   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[] = [
   {

+ 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>
       window.name = "codesandbox";
     </script>
+    <link rel="stylesheet" href="/dist/browser/dev/index.css" />
   </head>
 
   <body>
     <noscript> You need to enable JavaScript to run this app. </noscript>
     <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
     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>
 </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 { debounce, getVersion, nFormatter } from "../src/utils";
+import { debounce, getVersion, nFormatter } from "../packages/excalidraw/utils";
 import {
   getElementsStorageSize,
   getTotalStorageSize,
 } 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 };
 

+ 8 - 2
excalidraw-app/app_constants.ts

@@ -15,11 +15,17 @@ export const FILE_CACHE_MAX_AGE_SEC = 31536000;
 export const WS_EVENTS = {
   SERVER_VOLATILE: "server-volatile-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",
   UPDATE = "SCENE_UPDATE",
+  MOUSE_LOCATION = "MOUSE_LOCATION",
+  IDLE_STATUS = "IDLE_STATUS",
+  USER_VISIBLE_SCENE_BOUNDS = "USER_VISIBLE_SCENE_BOUNDS",
 }
 
 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 { 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 {
   ExcalidrawElement,
   InitializedExcalidrawImageElement,
-} from "../../src/element/types";
+} from "../../packages/excalidraw/element/types";
 import {
   getSceneVersion,
   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 {
+  assertNever,
   preventUnload,
   resolvablePromise,
-  withBatchedUpdates,
-} from "../../src/utils";
+  throttleRAF,
+} from "../../packages/excalidraw/utils";
 import {
   CURSOR_SYNC_TIMEOUT,
   FILE_UPLOAD_MAX_BYTES,
   FIREBASE_STORAGE_PREFIXES,
   INITIAL_SCENE_UPDATE_TIMEOUT,
   LOAD_IMAGES_TIMEOUT,
-  WS_SCENE_EVENT_TYPES,
+  WS_SUBTYPES,
   SYNC_FULL_SCENE_INTERVAL_MS,
+  WS_EVENTS,
 } from "../app_constants";
 import {
   generateCollaborationLinkData,
   getCollaborationLink,
-  getCollabServer,
   getSyncableElements,
   SocketUpdateDataSource,
   SyncableExcalidrawElement,
@@ -47,42 +52,48 @@ import {
   saveUsernameToLocalStorage,
 } from "../data/localStorage";
 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 {
   encodeFilesForUpload,
   FileManager,
   updateStaleImageStatuses,
 } from "../data/FileManager";
-import { AbortError } from "../../src/errors";
+import { AbortError } from "../../packages/excalidraw/errors";
 import {
   isImageElement,
   isInitializedImageElement,
-} from "../../src/element/typeChecks";
-import { newElementWith } from "../../src/element/mutateElement";
+} from "../../packages/excalidraw/element/typeChecks";
+import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
 import {
   ReconciledElements,
   reconcileElements as _reconcileElements,
 } from "./reconciliation";
-import { decryptData } from "../../src/data/encryption";
+import { decryptData } from "../../packages/excalidraw/data/encryption";
 import { resetBrowserStateVersions } from "../data/tabSync";
 import { LocalData } from "../data/LocalData";
-import { atom, useAtom } from "jotai";
+import { atom } from "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 collabDialogShownAtom = atom(false);
 export const isCollaboratingAtom = atom(false);
 export const isOfflineAtom = atom(false);
 
 interface CollabState {
-  errorMessage: string;
+  errorMessage: string | null;
   username: string;
-  activeRoomLink: string;
+  activeRoomLink: string | null;
 }
 
+export const activeRoomLinkAtom = atom<string | null>(null);
+
 type CollabInstance = InstanceType<typeof Collab>;
 
 export interface CollabAPI {
@@ -93,32 +104,33 @@ export interface CollabAPI {
   stopCollaboration: CollabInstance["stopCollaboration"];
   syncElements: CollabInstance["syncElements"];
   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;
 }
 
-type Props = PublicProps & { modalIsShown: boolean };
-
-class Collab extends PureComponent<Props, CollabState> {
+class Collab extends PureComponent<CollabProps, CollabState> {
   portal: Portal;
   fileManager: FileManager;
-  excalidrawAPI: Props["excalidrawAPI"];
+  excalidrawAPI: CollabProps["excalidrawAPI"];
   activeIntervalId: number | null;
   idleTimeoutId: number | null;
 
   private socketInitializationTimer?: number;
   private lastBroadcastedOrReceivedSceneVersion: number = -1;
-  private collaborators = new Map<string, Collaborator>();
+  private collaborators = new Map<SocketId, Collaborator>();
 
-  constructor(props: Props) {
+  constructor(props: CollabProps) {
     super(props);
     this.state = {
-      errorMessage: "",
+      errorMessage: null,
       username: importUsernameFromLocalStorage() || "",
-      activeRoomLink: "",
+      activeRoomLink: null,
     };
     this.portal = new Portal(this);
     this.fileManager = new FileManager({
@@ -151,12 +163,28 @@ class Collab extends PureComponent<Props, CollabState> {
     this.idleTimeoutId = null;
   }
 
+  private onUmmount: (() => void) | null = null;
+
   componentDidMount() {
     window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
     window.addEventListener("online", this.onOfflineStatusToggle);
     window.addEventListener("offline", this.onOfflineStatusToggle);
     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();
 
     const collabAPI: CollabAPI = {
@@ -167,6 +195,9 @@ class Collab extends PureComponent<Props, CollabState> {
       fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase,
       stopCollaboration: this.stopCollaboration,
       setUsername: this.setUsername,
+      getUsername: this.getUsername,
+      getActiveRoomLink: this.getActiveRoomLink,
+      setErrorMessage: this.setErrorMessage,
     };
 
     appJotaiStore.set(collabAPIAtom, collabAPI);
@@ -204,6 +235,7 @@ class Collab extends PureComponent<Props, CollabState> {
       window.clearTimeout(this.idleTimeoutId);
       this.idleTimeoutId = null;
     }
+    this.onUmmount?.();
   }
 
   isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!;
@@ -313,9 +345,7 @@ class Collab extends PureComponent<Props, CollabState> {
     this.fileManager.reset();
     if (!opts?.isUnload) {
       this.setIsCollaborating(false);
-      this.setState({
-        activeRoomLink: "",
-      });
+      this.setActiveRoomLink(null);
       this.collaborators = new Map();
       this.excalidrawAPI.updateScene({
         collaborators: this.collaborators,
@@ -356,7 +386,7 @@ class Collab extends PureComponent<Props, CollabState> {
     iv: Uint8Array,
     encryptedData: ArrayBuffer,
     decryptionKey: string,
-  ) => {
+  ): Promise<ValueOf<SocketUpdateDataSource>> => {
     try {
       const decrypted = await decryptData(iv, encryptedData, decryptionKey);
 
@@ -368,7 +398,7 @@ class Collab extends PureComponent<Props, CollabState> {
       window.alert(t("alerts.decryptFailed"));
       console.error(error);
       return {
-        type: "INVALID_RESPONSE",
+        type: WS_SUBTYPES.INVALID_RESPONSE,
       };
     }
   };
@@ -381,7 +411,7 @@ class Collab extends PureComponent<Props, CollabState> {
     if (!this.state.username) {
       import("@excalidraw/random-username").then(({ getRandomUsername }) => {
         const username = getRandomUsername();
-        this.onUsernameChange(username);
+        this.setUsername(username);
       });
     }
 
@@ -423,13 +453,9 @@ class Collab extends PureComponent<Props, CollabState> {
     this.fallbackInitializationHandler = fallbackInitializationHandler;
 
     try {
-      const socketServerData = await getCollabServer();
-
       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,
         roomKey,
@@ -484,9 +510,9 @@ class Collab extends PureComponent<Props, CollabState> {
         );
 
         switch (decryptedData.type) {
-          case "INVALID_RESPONSE":
+          case WS_SUBTYPES.INVALID_RESPONSE:
             return;
-          case WS_SCENE_EVENT_TYPES.INIT: {
+          case WS_SUBTYPES.INIT: {
             if (!this.portal.socketInitialized) {
               this.initializeRoom({ fetchScene: false });
               const remoteElements = decryptedData.payload.elements;
@@ -502,42 +528,76 @@ class Collab extends PureComponent<Props, CollabState> {
             }
             break;
           }
-          case WS_SCENE_EVENT_TYPES.UPDATE:
+          case WS_SUBTYPES.UPDATE:
             this.handleRemoteSceneUpdate(
               this.reconcileElements(decryptedData.payload.elements),
             );
             break;
-          case "MOUSE_LOCATION": {
+          case WS_SUBTYPES.MOUSE_LOCATION: {
             const { pointer, button, username, selectedElementIds } =
               decryptedData.payload;
+
             const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] =
               decryptedData.payload.socketId ||
               // @ts-ignore legacy, see #2094 (#2097)
               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({
-              collaborators,
+              appState: zoomToFitBounds({
+                appState,
+                bounds: sceneBounds,
+                fitToViewport: true,
+                viewportZoomFactor: 1,
+              }).appState,
             });
+
             break;
           }
-          case "IDLE_STATUS": {
+
+          case WS_SUBTYPES.IDLE_STATUS: {
             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;
           }
+
+          default: {
+            assertNever(decryptedData, null);
+          }
         }
       },
     );
@@ -553,11 +613,20 @@ class Collab extends PureComponent<Props, CollabState> {
       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.setState({
-      activeRoomLink: window.location.href,
-    });
+    this.setActiveRoomLink(window.location.href);
 
     return scenePromise;
   };
@@ -721,20 +790,39 @@ class Collab extends PureComponent<Props, CollabState> {
     document.addEventListener(EVENT.VISIBILITY_CHANGE, this.onVisibilityChange);
   };
 
-  setCollaborators(sockets: string[]) {
+  setCollaborators(sockets: SocketId[]) {
     const collaborators: InstanceType<typeof Collab>["collaborators"] =
       new Map();
     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.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) => {
     this.lastBroadcastedOrReceivedSceneVersion = version;
   };
@@ -760,6 +848,19 @@ class Collab extends PureComponent<Props, CollabState> {
     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) => {
     this.portal.broadcastIdleChange(userState);
   };
@@ -769,7 +870,7 @@ class Collab extends PureComponent<Props, CollabState> {
       getSceneVersion(elements) >
       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.queueBroadcastAllElements();
     }
@@ -782,7 +883,7 @@ class Collab extends PureComponent<Props, CollabState> {
 
   queueBroadcastAllElements = throttle(() => {
     this.portal.broadcastScene(
-      WS_SCENE_EVENT_TYPES.UPDATE,
+      WS_SUBTYPES.UPDATE,
       this.excalidrawAPI.getSceneElementsIncludingDeleted(),
       true,
     );
@@ -808,41 +909,31 @@ class Collab extends PureComponent<Props, CollabState> {
     { leading: false },
   );
 
-  handleClose = () => {
-    appJotaiStore.set(collabDialogShownAtom, false);
-  };
-
   setUsername = (username: string) => {
     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 (
       <>
-        {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}
           </ErrorDialog>
         )}
@@ -861,11 +952,6 @@ if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
   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;

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

@@ -6,23 +6,24 @@ import {
 
 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 {
-  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 { newElementWith } from "../../src/element/mutateElement";
+import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
 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 {
   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
   roomId: string | null = null;
   roomKey: string | null = null;
@@ -32,7 +33,7 @@ class Portal {
     this.collab = collab;
   }
 
-  open(socket: SocketIOClient.Socket, id: string, key: string) {
+  open(socket: Socket, id: string, key: string) {
     this.socket = socket;
     this.roomId = id;
     this.roomKey = key;
@@ -46,12 +47,12 @@ class Portal {
     });
     this.socket.on("new-user", async (_socketId: string) => {
       this.broadcastScene(
-        WS_SCENE_EVENT_TYPES.INIT,
+        WS_SUBTYPES.INIT,
         this.collab.getSceneElementsIncludingDeleted(),
         /* syncAll */ true,
       );
     });
-    this.socket.on("room-user-change", (clients: string[]) => {
+    this.socket.on("room-user-change", (clients: SocketId[]) => {
       this.collab.setCollaborators(clients);
     });
 
@@ -83,6 +84,7 @@ class Portal {
   async _broadcastSocketData(
     data: SocketUpdateData,
     volatile: boolean = false,
+    roomId?: string,
   ) {
     if (this.isOpen()) {
       const json = JSON.stringify(data);
@@ -91,7 +93,7 @@ class Portal {
 
       this.socket?.emit(
         volatile ? WS_EVENTS.SERVER_VOLATILE : WS_EVENTS.SERVER,
-        this.roomId,
+        roomId ?? this.roomId,
         encryptedBuffer,
         iv,
       );
@@ -130,11 +132,11 @@ class Portal {
   }, FILE_UPLOAD_TIMEOUT);
 
   broadcastScene = async (
-    updateType: WS_SCENE_EVENT_TYPES.INIT | WS_SCENE_EVENT_TYPES.UPDATE,
+    updateType: WS_SUBTYPES.INIT | WS_SUBTYPES.UPDATE,
     allElements: readonly ExcalidrawElement[],
     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");
     }
 
@@ -183,9 +185,9 @@ class Portal {
   broadcastIdleChange = (userState: UserIdleState) => {
     if (this.socket?.id) {
       const data: SocketUpdateDataSource["IDLE_STATUS"] = {
-        type: "IDLE_STATUS",
+        type: WS_SUBTYPES.IDLE_STATUS,
         payload: {
-          socketId: this.socket.id,
+          socketId: this.socket.id as SocketId,
           userState,
           username: this.collab.state.username,
         },
@@ -203,9 +205,9 @@ class Portal {
   }) => {
     if (this.socket?.id) {
       const data: SocketUpdateDataSource["MOUSE_LOCATION"] = {
-        type: "MOUSE_LOCATION",
+        type: WS_SUBTYPES.MOUSE_LOCATION,
         payload: {
-          socketId: this.socket.id,
+          socketId: this.socket.id as SocketId,
           pointer: payload.pointer,
           button: payload.button || "up",
           selectedElementIds:
@@ -213,12 +215,43 @@ class Portal {
           username: this.collab.state.username,
         },
       };
+
       return this._broadcastSocketData(
         data as SocketUpdateData,
         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;

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

@@ -1,13 +1,13 @@
 import { useRef, useState } from "react";
 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 {
   copyIcon,
   playerPlayIcon,
@@ -16,11 +16,11 @@ import {
   shareIOS,
   shareWindows,
   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";
 
 const getShareIcon = () => {
@@ -120,7 +120,7 @@ export const RoomModal = ({
               size="large"
               variant="icon"
               label="Share"
-              startIcon={getShareIcon()}
+              icon={getShareIcon()}
               className="RoomDialog__active__share"
               onClick={shareRoomLink}
             />
@@ -130,7 +130,7 @@ export const RoomModal = ({
               <FilledButton
                 size="large"
                 label="Copy link"
-                startIcon={copyIcon}
+                icon={copyIcon}
                 onClick={copyRoomLink}
               />
             </Popover.Trigger>
@@ -166,7 +166,7 @@ export const RoomModal = ({
             variant="outlined"
             color="danger"
             label={t("roomDialog.button_stopSession")}
-            startIcon={playerStopFilledIcon}
+            icon={playerStopFilledIcon}
             onClick={() => {
               trackEvent("share", "room closed");
               onRoomDestroy();
@@ -195,7 +195,7 @@ export const RoomModal = ({
         <FilledButton
           size="large"
           label={t("roomDialog.button_startSession")}
-          startIcon={playerPlayIcon}
+          icon={playerPlayIcon}
           onClick={() => {
             trackEvent("share", "room creation", `ui (${getFrame()})`);
             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[] & {
   _brand: "reconciledElements";

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

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

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

@@ -1,10 +1,10 @@
 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";
 
 export const AppMainMenu: React.FC<{
-  setCollabDialogShown: (toggle: boolean) => any;
+  onCollabDialogOpen: () => any;
   isCollaborating: boolean;
   isCollabEnabled: boolean;
 }> = React.memo((props) => {
@@ -17,7 +17,7 @@ export const AppMainMenu: React.FC<{
       {props.isCollabEnabled && (
         <MainMenu.DefaultItems.LiveCollaborationTrigger
           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 { 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 { POINTER_EVENTS } from "../../src/constants";
+import { POINTER_EVENTS } from "../../packages/excalidraw/constants";
 
 export const AppWelcomeScreen: React.FC<{
-  setCollabDialogShown: (toggle: boolean) => any;
+  onCollabDialogOpen: () => any;
   isCollabEnabled: boolean;
 }> = React.memo((props) => {
   const { t } = useI18n();
@@ -52,7 +52,7 @@ export const AppWelcomeScreen: React.FC<{
           <WelcomeScreen.Center.MenuItemHelp />
           {props.isCollabEnabled && (
             <WelcomeScreen.Center.MenuItemLiveCollaborationTrigger
-              onSelect={() => props.setCollabDialogShown(true)}
+              onSelect={() => props.onCollabDialogOpen()}
             />
           )}
           {!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 = () => {
   const { t } = useI18n();

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

@@ -1,20 +1,30 @@
 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 { 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 { 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 { 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 (
   elements: readonly NonDeletedExcalidrawElement[],

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

@@ -1,7 +1,7 @@
 import oc from "open-color";
 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
 export const GitHubCorner = React.memo(

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

@@ -1,8 +1,8 @@
 import { useSetAtom } from "jotai";
 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 }) => {
   const { t, langCode } = useI18n();

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

@@ -1,7 +1,7 @@
 import React from "react";
 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 {
   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 {
   ExcalidrawElement,
   ExcalidrawImageElement,
   FileId,
   InitializedExcalidrawImageElement,
-} from "../../src/element/types";
-import { t } from "../../src/i18n";
+} from "../../packages/excalidraw/element/types";
+import { t } from "../../packages/excalidraw/i18n";
 import {
   BinaryFileData,
   BinaryFileMetadata,
   ExcalidrawImperativeAPI,
   BinaryFiles,
-} from "../../src/types";
+} from "../../packages/excalidraw/types";
 
 export class FileManager {
   /** 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 { 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 { FileManager } from "./FileManager";
 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 { restoreElements } from "../../src/data/restore";
+import { restoreElements } from "../../packages/excalidraw/data/restore";
 import {
   AppState,
   BinaryFileData,
   BinaryFileMetadata,
   DataURL,
-} from "../../src/types";
+} from "../../packages/excalidraw/types";
 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 { getSyncableElements, SyncableExcalidrawElement } from ".";
-import { ResolutionType } from "../../src/utility-types";
+import { ResolutionType } from "../../packages/excalidraw/utility-types";
+import type { Socket } from "socket.io-client";
 
 // private
 // -----------------------------------------------------------------------------
@@ -132,12 +139,12 @@ const decryptElements = async (
 };
 
 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);
   };
   static set = (
-    socket: SocketIOClient.Socket,
+    socket: Socket,
     elements: readonly SyncableExcalidrawElement[],
   ) => {
     FirebaseSceneVersionCache.cache.set(socket, getSceneVersion(elements));
@@ -279,7 +286,7 @@ export const saveToFirebase = async (
 export const loadFromFirebase = async (
   roomId: string,
   roomKey: string,
-  socket: SocketIOClient.Socket | null,
+  socket: Socket | null,
 ): Promise<readonly ExcalidrawElement[] | null> => {
   const firebase = await loadFirestore();
   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 {
   decryptData,
   generateEncryptionKey,
   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 {
   AppState,
   BinaryFileData,
   BinaryFiles,
+  SocketId,
   UserIdleState,
-} from "../../src/types";
-import { bytesToHexString } from "../../src/utils";
+} from "../../packages/excalidraw/types";
+import { bytesToHexString } from "../../packages/excalidraw/utils";
 import {
   DELETED_ELEMENT_TIMEOUT,
   FILE_UPLOAD_MAX_BYTES,
   ROOM_ID_BYTES,
+  WS_SUBTYPES,
 } from "../app_constants";
 import { encodeFilesForUpload } from "./FileManager";
 import { saveFilesToFirebase } from "./firebase";
@@ -56,67 +65,49 @@ const generateRoomId = async () => {
   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 = {
   data: ArrayBuffer;
   iv: Uint8Array;
 };
 
 export type SocketUpdateDataSource = {
+  INVALID_RESPONSE: {
+    type: WS_SUBTYPES.INVALID_RESPONSE;
+  };
   SCENE_INIT: {
-    type: "SCENE_INIT";
+    type: WS_SUBTYPES.INIT;
     payload: {
       elements: readonly ExcalidrawElement[];
     };
   };
   SCENE_UPDATE: {
-    type: "SCENE_UPDATE";
+    type: WS_SUBTYPES.UPDATE;
     payload: {
       elements: readonly ExcalidrawElement[];
     };
   };
   MOUSE_LOCATION: {
-    type: "MOUSE_LOCATION";
+    type: WS_SUBTYPES.MOUSE_LOCATION;
     payload: {
-      socketId: string;
+      socketId: SocketId;
       pointer: { x: number; y: number; tool: "pointer" | "laser" };
       button: "down" | "up";
       selectedElementIds: AppState["selectedElementIds"];
       username: string;
     };
   };
+  USER_VISIBLE_SCENE_BOUNDS: {
+    type: WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS;
+    payload: {
+      socketId: SocketId;
+      username: string;
+      sceneBounds: SceneBounds;
+    };
+  };
   IDLE_STATUS: {
-    type: "IDLE_STATUS";
+    type: WS_SUBTYPES.IDLE_STATUS;
     payload: {
-      socketId: string;
+      socketId: SocketId;
       userState: UserIdleState;
       username: string;
     };
@@ -124,10 +115,7 @@ export type SocketUpdateDataSource = {
 };
 
 export type SocketUpdateDataIncoming =
-  | SocketUpdateDataSource[keyof SocketUpdateDataSource]
-  | {
-      type: "INVALID_RESPONSE";
-    };
+  SocketUpdateDataSource[keyof SocketUpdateDataSource];
 
 export type SocketUpdateData =
   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 {
   clearAppStateForLocalStorage,
   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 { ImportedDataState } from "../../src/data/types";
+import { ImportedDataState } from "../../packages/excalidraw/data/types";
 
 export const saveUsernameToLocalStorage = (username: string) => {
   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"
     />
 
-    <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" ) { %>
     <script>
       {
@@ -195,7 +195,7 @@
       <h1 class="visually-hidden">Excalidraw</h1>
     </header>
     <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') { %>
     <!-- 100% privacy friendly analytics -->
     <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 {
-  .RoomDialog {
+  .ShareDialog {
     display: flex;
     flex-direction: column;
     gap: 1.5rem;
@@ -10,8 +10,25 @@
       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 {
-      @keyframes RoomDialog__popover__scaleIn {
+      @keyframes ShareDialog__popover__scaleIn {
         from {
           opacity: 0;
         }
@@ -50,10 +67,10 @@
       }
 
       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";
 
       &__illustration {
@@ -95,7 +112,7 @@
         }
       }
 
-      &__start_session {
+      &__button {
         display: flex;
 
         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", () => {
   it("rerenders UI on language change", async () => {

Some files were not shown because too many files changed in this diff