Prechádzať zdrojové kódy

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

Daniel J. Geiger 2 rokov pred
rodič
commit
62f5475c4a
91 zmenil súbory, kde vykonal 1337 pridanie a 604 odobranie
  1. 429 0
      dev-docs/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton.mdx
  2. 1 1
      dev-docs/package.json
  3. 2 6
      dev-docs/sidebars.js
  4. 4 4
      dev-docs/yarn.lock
  5. 6 6
      excalidraw-app/CustomStats.tsx
  6. 0 0
      excalidraw-app/app-jotai.ts
  7. 0 0
      excalidraw-app/app_constants.ts
  8. 15 15
      excalidraw-app/collab/Collab.tsx
  9. 6 6
      excalidraw-app/collab/Portal.tsx
  10. 1 1
      excalidraw-app/collab/RoomDialog.scss
  11. 10 10
      excalidraw-app/collab/RoomDialog.tsx
  12. 4 4
      excalidraw-app/collab/reconciliation.ts
  13. 7 3
      excalidraw-app/components/AppFooter.tsx
  14. 2 2
      excalidraw-app/components/AppMainMenu.tsx
  15. 5 4
      excalidraw-app/components/AppWelcomeScreen.tsx
  16. 3 3
      excalidraw-app/components/EncryptedIcon.tsx
  17. 0 0
      excalidraw-app/components/ExcalidrawPlusAppLink.tsx
  18. 12 12
      excalidraw-app/components/ExportToExcalidrawPlus.tsx
  19. 2 2
      excalidraw-app/components/GitHubCorner.tsx
  20. 2 2
      excalidraw-app/components/LanguageList.tsx
  21. 6 6
      excalidraw-app/data/FileManager.ts
  22. 5 5
      excalidraw-app/data/LocalData.ts
  23. 0 0
      excalidraw-app/data/Locker.ts
  24. 8 8
      excalidraw-app/data/firebase.ts
  25. 11 11
      excalidraw-app/data/index.ts
  26. 5 5
      excalidraw-app/data/localStorage.ts
  27. 0 0
      excalidraw-app/data/tabSync.ts
  28. 0 0
      excalidraw-app/debug.ts
  29. 3 2
      excalidraw-app/index.scss
  30. 31 24
      excalidraw-app/index.tsx
  31. 0 0
      excalidraw-app/sentry.ts
  32. 29 0
      excalidraw-app/tests/LanguageList.test.tsx
  33. 3 3
      excalidraw-app/tests/MobileMenu.test.tsx
  34. 19 0
      excalidraw-app/tests/__snapshots__/MobileMenu.test.tsx.snap
  35. 6 6
      excalidraw-app/tests/collab.test.tsx
  36. 5 5
      excalidraw-app/tests/reconciliation.test.ts
  37. 50 21
      src/components/App.tsx
  38. 7 4
      src/components/ColorPicker/ColorInput.tsx
  39. 5 12
      src/components/ColorPicker/ColorPicker.tsx
  40. 50 36
      src/components/EyeDropper.tsx
  41. 1 1
      src/components/FixedSideContainer.scss
  42. 20 11
      src/components/HintViewer.tsx
  43. 28 10
      src/components/LayerUI.scss
  44. 40 10
      src/components/LayerUI.tsx
  45. 2 0
      src/components/Sidebar/Sidebar.scss
  46. 1 1
      src/components/Stats.scss
  47. 1 1
      src/components/UserList.scss
  48. 1 1
      src/components/footer/Footer.tsx
  49. 2 1
      src/components/footer/FooterCenter.scss
  50. 3 3
      src/components/welcome-screen/WelcomeScreen.scss
  51. 8 0
      src/constants.ts
  52. 7 18
      src/css/styles.scss
  53. 9 8
      src/element/linearElementEditor.ts
  54. 0 1
      src/element/newElement.test.ts
  55. 3 3
      src/element/subtypes/mathjax/tests/implementation.test.tsx
  56. 5 5
      src/element/textWysiwyg.test.tsx
  57. 7 0
      src/hooks/useStable.ts
  58. 2 2
      src/index.tsx
  59. 2 1
      src/locales/en.json
  60. 232 21
      src/packages/excalidraw/CHANGELOG.md
  61. 1 1
      src/packages/excalidraw/package.json
  62. 2 2
      src/tests/App.test.tsx
  63. 50 0
      src/tests/__snapshots__/App.test.tsx.snap
  64. 0 120
      src/tests/__snapshots__/regressionTests.test.tsx.snap
  65. 3 3
      src/tests/actionStyles.test.tsx
  66. 2 2
      src/tests/align.test.tsx
  67. 21 16
      src/tests/appState.test.tsx
  68. 2 2
      src/tests/binding.test.tsx
  69. 8 4
      src/tests/clipboard.test.tsx
  70. 5 8
      src/tests/contextmenu.test.tsx
  71. 2 2
      src/tests/customActions.test.tsx
  72. 15 11
      src/tests/dragCreate.test.tsx
  73. 2 2
      src/tests/elementLocking.test.tsx
  74. 2 2
      src/tests/export.test.tsx
  75. 6 6
      src/tests/fitToContent.test.tsx
  76. 2 2
      src/tests/flip.test.tsx
  77. 22 18
      src/tests/history.test.tsx
  78. 3 3
      src/tests/library.test.tsx
  79. 2 2
      src/tests/linearElementEditor.test.tsx
  80. 4 4
      src/tests/move.test.tsx
  81. 12 11
      src/tests/multiPointCreate.test.tsx
  82. 2 24
      src/tests/regressionTests.test.tsx
  83. 2 2
      src/tests/resize.test.tsx
  84. 1 2
      src/tests/scroll.test.tsx
  85. 23 13
      src/tests/selection.test.tsx
  86. 8 7
      src/tests/subtypes.test.tsx
  87. 1 1
      src/tests/test-utils.ts
  88. 2 2
      src/tests/viewMode.test.tsx
  89. 2 3
      src/tests/zindex.test.tsx
  90. 1 1
      src/zindex.ts
  91. 1 1
      tsconfig.json

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

@@ -0,0 +1,429 @@
+# Creating Elements programmatically
+
+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).
+
+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).
+
+## convertToExcalidrawElements
+
+**_Signature_**
+
+<pre>
+  convertToExcalidrawElements(elements:{" "}
+  <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133">
+    ExcalidrawElementSkeleton
+  </a>
+  )
+</pre>
+
+**_How to use_**
+
+```js
+import { convertToExcalidrawElements } from "@excalidraw/excalidraw";
+```
+
+This function converts the Excalidraw Element Skeleton to excalidraw elements which could be then rendered on the canvas. Hence calling this function is necessary before passing it to APIs like [`initialData`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/initialdata), [`updateScene`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/ref#updatescene) if you are using the Skeleton API
+
+## Supported Features
+
+### Rectangle, Ellipse, and Diamond
+
+To create these shapes you need to pass its `type` and `x` and `y` coordinates for position. The rest of the attributes are optional_.
+
+For the Skeleton API to work, `convertToExcalidrawElements` needs to be called before passing it to Excalidraw Component via initialData, updateScene or any such API.
+
+```jsx live
+function App() {
+  const elements = convertToExcalidrawElements([
+    {
+      type: "rectangle",
+      x: 100,
+      y: 250,
+    },
+    {
+      type: "ellipse",
+      x: 250,
+      y: 250,
+    },
+    {
+      type: "diamond",
+      x: 380,
+      y: 250,
+    },
+  ]);
+  return (
+    <div style={{ height: "500px" }}>
+      <Excalidraw
+        initialData={{
+          elements,
+          appState: { zenModeEnabled: true, viewBackgroundColor: "#a5d8ff" },
+          scrollToContent: true,
+        }}
+      />
+    </div>
+  );
+}
+```
+
+You can pass additional [`properties`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L27) as well to decorate the shapes.
+
+:::info
+
+You can copy the below test examples and replace the elements in the live editor above to test it out.
+
+:::
+
+```js
+convertToExcalidrawElements([
+  {
+    type: "rectangle",
+    x: 50,
+    y: 250,
+    width: 200,
+    height: 100,
+    backgroundColor: "#c0eb75",
+    strokeWidth: 2,
+  },
+  {
+    type: "ellipse",
+    x: 300,
+    y: 250,
+    width: 200,
+    height: 100,
+    backgroundColor: "#ffc9c9",
+    strokeStyle: "dotted",
+    fillStyle: "solid",
+    strokeWidth: 2,
+  },
+  {
+    type: "diamond",
+    x: 550,
+    y: 250,
+    width: 200,
+    height: 100,
+    backgroundColor: "#a5d8ff",
+    strokeColor: "#1971c2",
+    strokeStyle: "dashed",
+    fillStyle: "cross-hatch",
+    strokeWidth: 2,
+  },
+]);
+```
+
+![image](https://github.com/excalidraw/excalidraw/assets/11256141/70ca7063-88fb-434c-838a-cd466e1bc3c2)
+
+### Text Element
+
+The `type`, `x`, `y` and `text` properties are required to create a text element, rest of the attributes are optional
+
+```js
+convertToExcalidrawElements([
+  {
+    type: "text",
+    x: 100,
+    y: 100,
+    text: "HELLO WORLD!",
+  },
+  {
+    type: "text",
+    x: 100,
+    y: 150,
+    text: "STYLED HELLO WORLD!",
+    fontSize: 20,
+    strokeColor: "#5f3dc4",
+  },
+]);
+```
+
+![image](https://github.com/excalidraw/excalidraw/assets/11256141/085c7ac3-7952-4f22-b9c3-6beb51438526)
+
+### Lines and Arrows
+
+The `type`, `x`, and `y` properties are required, rest of the attributes are optional
+
+```js
+convertToExcalidrawElements([
+  {
+    type: "arrow",
+    x: 100,
+    y: 20,
+  },
+  {
+    type: "line",
+    x: 100,
+    y: 60,
+  },
+]);
+```
+
+![image](https://github.com/excalidraw/excalidraw/assets/11256141/0c22a06b-b568-4ab5-9848-a5f0160f66a6)
+
+#### With Addtional properties
+
+```js
+convertToExcalidrawElements([
+  {
+    type: "arrow",
+    x: 450,
+    y: 20,
+    startArrowhead: "dot",
+    endArrowhead: "triangle",
+    strokeColor: "#1971c2",
+    strokeWidth: 2,
+  },
+  {
+    type: "line",
+    x: 450,
+    y: 60,
+    strokeColor: "#2f9e44",
+    strokeWidth: 2,
+    strokeStyle: "dotted",
+  },
+]);
+```
+
+![image](https://github.com/excalidraw/excalidraw/assets/11256141/14f1bf3f-ad81-4096-8c1c-f35235084ec5)
+
+### 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.
+
+If you don't provide the dimensions of container, we calculate it based of the label dimensions.
+
+```js
+convertToExcalidrawElements([
+  {
+    type: "rectangle",
+    x: 300,
+    y: 290,
+    label: {
+      text: "RECTANGLE TEXT CONTAINER",
+    },
+  },
+  {
+    type: "ellipse",
+    x: 500,
+    y: 100,
+    label: {
+      text: "ELLIPSE\n TEXT CONTAINER",
+    },
+  },
+  {
+    type: "diamond",
+    x: 100,
+    y: 100,
+    label: {
+      text: "DIAMOND\nTEXT CONTAINER",
+    },
+  },
+]);
+```
+
+![image](https://github.com/excalidraw/excalidraw/assets/11256141/1e2c7b5d-fcb4-4f86-946d-0bfb0e97d532)
+
+#### With Additional properties
+
+```js
+convertToExcalidrawElements([
+  {
+    type: "diamond",
+    x: -120,
+    y: 100,
+    width: 270,
+    backgroundColor: "#fff3bf",
+    strokeWidth: 2,
+    label: {
+      text: "STYLED DIAMOND TEXT CONTAINER",
+      strokeColor: "#099268",
+      fontSize: 20,
+    },
+  },
+  {
+    type: "rectangle",
+    x: 180,
+    y: 150,
+    width: 200,
+    strokeColor: "#c2255c",
+    label: {
+      text: "TOP LEFT ALIGNED RECTANGLE TEXT CONTAINER",
+      textAlign: "left",
+      verticalAlign: "top",
+      fontSize: 20,
+    },
+  },
+  {
+    type: "ellipse",
+    x: 400,
+    y: 130,
+    strokeColor: "#f08c00",
+    backgroundColor: "#ffec99",
+    width: 200,
+    label: {
+      text: "STYLED ELLIPSE TEXT CONTAINER",
+      strokeColor: "#c2255c",
+    },
+  },
+]);
+```
+
+![image](https://github.com/excalidraw/excalidraw/assets/11256141/f8123cd1-c9aa-452d-b96b-05c846c5030d)
+
+### Labelled Arrows
+
+Similar to Text Containers, you can create labelled arrows as well.
+
+```js
+convertToExcalidrawElements([
+  {
+    type: "arrow",
+    x: 100,
+    y: 100,
+    label: {
+      text: "LABELED ARROW",
+    },
+  },
+  {
+    type: "arrow",
+    x: 100,
+    y: 200,
+    label: {
+      text: "STYLED LABELED ARROW",
+      strokeColor: "#099268",
+      fontSize: 20,
+    },
+  },
+  {
+    type: "arrow",
+    x: 100,
+    y: 300,
+    strokeColor: "#1098ad",
+    strokeWidth: 2,
+    label: {
+      text: "ANOTHER STYLED LABELLED ARROW",
+    },
+  },
+  {
+    type: "arrow",
+    x: 100,
+    y: 400,
+    strokeColor: "#1098ad",
+    strokeWidth: 2,
+    label: {
+      text: "ANOTHER STYLED LABELLED ARROW",
+      strokeColor: "#099268",
+    },
+  },
+]);
+```
+
+![image](https://github.com/excalidraw/excalidraw/assets/11256141/70635e9b-f1c8-4839-89e1-73b813abeb93)
+
+### 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
+
+```js
+convertToExcalidrawElements([
+  {
+    type: "arrow",
+    x: 255,
+    y: 239,
+    label: {
+      text: "HELLO WORLD!!",
+    },
+    start: {
+      type: "rectangle",
+    },
+    end: {
+      type: "ellipse",
+    },
+  },
+]);
+```
+
+When position for `start` and `end ` properties are not specified, we compute it according to arrow position.
+
+![image](https://github.com/excalidraw/excalidraw/assets/11256141/5aff09fd-b7e8-4c63-98be-da40b0698704)
+
+```js
+convertToExcalidrawElements([
+  {
+    type: "arrow",
+    x: 255,
+    y: 239,
+    label: {
+      text: "HELLO WORLD!!",
+    },
+    start: {
+      type: "text",
+      text: "HEYYYYY",
+    },
+    end: {
+      type: "text",
+      text: "WHATS UP ?",
+    },
+  },
+]);
+```
+
+![image](https://github.com/excalidraw/excalidraw/assets/11256141/2a9f03ac-e45c-4fbd-9be0-5d9f8c8e0343)
+
+#### When passing `id`
+
+Useful when you want to bind multiple arrows to one diagram / use some existing diagram
+
+```js
+convertToExcalidrawElements([
+  {
+    type: "ellipse",
+    id: "ellipse-1",
+    strokeColor: "#66a80f",
+    x: 390,
+    y: 356,
+    width: 150,
+    height: 150,
+    backgroundColor: "#d8f5a2",
+  },
+  {
+    type: "diamond",
+    id: "diamond-1",
+    strokeColor: "#9c36b5",
+    width: 100,
+    x: -30,
+    y: 380,
+  },
+  {
+    type: "arrow",
+    x: 100,
+    y: 440,
+    width: 295,
+    height: 35,
+    strokeColor: "#1864ab",
+    start: {
+      type: "rectangle",
+      width: 150,
+      height: 150,
+    },
+    end: {
+      id: "ellipse-1",
+    },
+  },
+  {
+    type: "arrow",
+    x: 60,
+    y: 420,
+    width: 330,
+    strokeColor: "#e67700",
+    start: {
+      id: "diamond-1",
+    },
+    end: {
+      id: "ellipse-1",
+    },
+  },
+]);
+```
+
+![image](https://github.com/excalidraw/excalidraw/assets/11256141/a8b047c8-2eed-4aea-82a2-e1e6bbddb8d4)

+ 1 - 1
dev-docs/package.json

@@ -18,7 +18,7 @@
     "@docusaurus/core": "2.2.0",
     "@docusaurus/preset-classic": "2.2.0",
     "@docusaurus/theme-live-codeblock": "2.2.0",
-    "@excalidraw/excalidraw": "0.15.2-6546-3398d86",
+    "@excalidraw/excalidraw": "0.15.2-eb020d0",
     "@mdx-js/react": "^1.6.22",
     "clsx": "^1.2.1",
     "docusaurus-plugin-sass": "0.2.3",

+ 2 - 6
dev-docs/sidebars.js

@@ -81,12 +81,8 @@ const sidebars = {
                 "@excalidraw/excalidraw/api/utils/restore",
               ],
             },
-            {
-              type: "category",
-              label: "Constants",
-              link: { type: "doc", id: "@excalidraw/excalidraw/api/constants" },
-              items: [],
-            },
+            "@excalidraw/excalidraw/api/constants",
+            "@excalidraw/excalidraw/api/excalidraw-element-skeleton",
           ],
         },
         "@excalidraw/excalidraw/faq",

+ 4 - 4
dev-docs/yarn.lock

@@ -1631,10 +1631,10 @@
     url-loader "^4.1.1"
     webpack "^5.73.0"
 
-"@excalidraw/[email protected]6546-3398d86":
-  version "0.15.2-6546-3398d86"
-  resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2-6546-3398d86.tgz#e74d5ad944b8b414924d27ee91469a32b4f08dbf"
-  integrity sha512-Tzq6qighJUytXRA8iMzQ8onoGclo9CuvPSw7DMvPxME8nxAxn5CeK/gsxIs3zwooj9CC6XF42BSrx0+n+fPxaQ==
+"@excalidraw/[email protected]eb020d0":
+  version "0.15.2-eb020d0"
+  resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2-eb020d0.tgz#25bd61e6f23da7c084fb16a3e0fe0dd9ad8e6533"
+  integrity sha512-TKGLzpOVqFQcwK1GFKTDXgg1s2U6tc5KE3qXuv87osbzOtftQn3x4+VH61vwdj11l00nEN80SMdXUC43T9uJqQ==
 
 "@hapi/hoek@^9.0.0":
   version "9.3.0"

+ 6 - 6
src/excalidraw-app/CustomStats.tsx → excalidraw-app/CustomStats.tsx

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

+ 0 - 0
src/excalidraw-app/app-jotai.ts → excalidraw-app/app-jotai.ts


+ 0 - 0
src/excalidraw-app/app_constants.ts → excalidraw-app/app_constants.ts


+ 15 - 15
src/excalidraw-app/collab/Collab.tsx → excalidraw-app/collab/Collab.tsx

@@ -1,23 +1,23 @@
 import throttle from "lodash.throttle";
 import { PureComponent } from "react";
-import { ExcalidrawImperativeAPI } from "../../types";
-import { ErrorDialog } from "../../components/ErrorDialog";
-import { APP_NAME, ENV, EVENT } from "../../constants";
-import { ImportedDataState } from "../../data/types";
+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 {
   ExcalidrawElement,
   InitializedExcalidrawImageElement,
-} from "../../element/types";
+} from "../../src/element/types";
 import {
   getSceneVersion,
   restoreElements,
-} from "../../packages/excalidraw/index";
-import { Collaborator, Gesture } from "../../types";
+} from "../../src/packages/excalidraw/index";
+import { Collaborator, Gesture } from "../../src/types";
 import {
   preventUnload,
   resolvablePromise,
   withBatchedUpdates,
-} from "../../utils";
+} from "../../src/utils";
 import {
   CURSOR_SYNC_TIMEOUT,
   FILE_UPLOAD_MAX_BYTES,
@@ -48,25 +48,25 @@ import {
 } from "../data/localStorage";
 import Portal from "./Portal";
 import RoomDialog from "./RoomDialog";
-import { t } from "../../i18n";
-import { UserIdleState } from "../../types";
-import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants";
+import { t } from "../../src/i18n";
+import { UserIdleState } from "../../src/types";
+import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../src/constants";
 import {
   encodeFilesForUpload,
   FileManager,
   updateStaleImageStatuses,
 } from "../data/FileManager";
-import { AbortError } from "../../errors";
+import { AbortError } from "../../src/errors";
 import {
   isImageElement,
   isInitializedImageElement,
-} from "../../element/typeChecks";
-import { newElementWith } from "../../element/mutateElement";
+} from "../../src/element/typeChecks";
+import { newElementWith } from "../../src/element/mutateElement";
 import {
   ReconciledElements,
   reconcileElements as _reconcileElements,
 } from "./reconciliation";
-import { decryptData } from "../../data/encryption";
+import { decryptData } from "../../src/data/encryption";
 import { resetBrowserStateVersions } from "../data/tabSync";
 import { LocalData } from "../data/LocalData";
 import { atom, useAtom } from "jotai";

+ 6 - 6
src/excalidraw-app/collab/Portal.tsx → excalidraw-app/collab/Portal.tsx

@@ -6,19 +6,19 @@ import {
 
 import { TCollabClass } from "./Collab";
 
-import { ExcalidrawElement } from "../../element/types";
+import { ExcalidrawElement } from "../../src/element/types";
 import {
   WS_EVENTS,
   FILE_UPLOAD_TIMEOUT,
   WS_SCENE_EVENT_TYPES,
 } from "../app_constants";
-import { UserIdleState } from "../../types";
-import { trackEvent } from "../../analytics";
+import { UserIdleState } from "../../src/types";
+import { trackEvent } from "../../src/analytics";
 import throttle from "lodash.throttle";
-import { newElementWith } from "../../element/mutateElement";
+import { newElementWith } from "../../src/element/mutateElement";
 import { BroadcastedExcalidrawElement } from "./reconciliation";
-import { encryptData } from "../../data/encryption";
-import { PRECEDING_ELEMENT_KEY } from "../../constants";
+import { encryptData } from "../../src/data/encryption";
+import { PRECEDING_ELEMENT_KEY } from "../../src/constants";
 
 class Portal {
   collab: TCollabClass;

+ 1 - 1
src/excalidraw-app/collab/RoomDialog.scss → excalidraw-app/collab/RoomDialog.scss

@@ -1,4 +1,4 @@
-@import "../../css/variables.module";
+@import "../../src/css/variables.module";
 
 .excalidraw {
   .RoomDialog {

+ 10 - 10
src/excalidraw-app/collab/RoomDialog.tsx → 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 "../../clipboard";
-import { trackEvent } from "../../analytics";
-import { getFrame } from "../../utils";
-import { useI18n } from "../../i18n";
-import { KEYS } from "../../keys";
+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 { Dialog } from "../../components/Dialog";
+import { Dialog } from "../../src/components/Dialog";
 import {
   copyIcon,
   playerPlayIcon,
@@ -16,11 +16,11 @@ import {
   shareIOS,
   shareWindows,
   tablerCheckIcon,
-} from "../../components/icons";
-import { TextField } from "../../components/TextField";
-import { FilledButton } from "../../components/FilledButton";
+} from "../../src/components/icons";
+import { TextField } from "../../src/components/TextField";
+import { FilledButton } from "../../src/components/FilledButton";
 
-import { ReactComponent as CollabImage } from "../../assets/lock.svg";
+import { ReactComponent as CollabImage } from "../../src/assets/lock.svg";
 import "./RoomDialog.scss";
 
 const getShareIcon = () => {

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

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

+ 7 - 3
src/excalidraw-app/components/AppFooter.tsx → excalidraw-app/components/AppFooter.tsx

@@ -1,7 +1,8 @@
 import React from "react";
-import { Footer } from "../../packages/excalidraw/index";
+import { Footer } from "../../src/packages/excalidraw/index";
 import { EncryptedIcon } from "./EncryptedIcon";
 import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
+import { isExcalidrawPlusSignedUser } from "../app_constants";
 
 export const AppFooter = React.memo(() => {
   return (
@@ -13,8 +14,11 @@ export const AppFooter = React.memo(() => {
           alignItems: "center",
         }}
       >
-        <ExcalidrawPlusAppLink />
-        <EncryptedIcon />
+        {isExcalidrawPlusSignedUser ? (
+          <ExcalidrawPlusAppLink />
+        ) : (
+          <EncryptedIcon />
+        )}
       </div>
     </Footer>
   );

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

@@ -1,6 +1,6 @@
 import React from "react";
-import { PlusPromoIcon } from "../../components/icons";
-import { MainMenu } from "../../packages/excalidraw/index";
+import { PlusPromoIcon } from "../../src/components/icons";
+import { MainMenu } from "../../src/packages/excalidraw/index";
 import { LanguageList } from "./LanguageList";
 
 export const AppMainMenu: React.FC<{

+ 5 - 4
src/excalidraw-app/components/AppWelcomeScreen.tsx → excalidraw-app/components/AppWelcomeScreen.tsx

@@ -1,8 +1,9 @@
 import React from "react";
-import { PlusPromoIcon } from "../../components/icons";
-import { useI18n } from "../../i18n";
-import { WelcomeScreen } from "../../packages/excalidraw/index";
+import { PlusPromoIcon } from "../../src/components/icons";
+import { useI18n } from "../../src/i18n";
+import { WelcomeScreen } from "../../src/packages/excalidraw/index";
 import { isExcalidrawPlusSignedUser } from "../app_constants";
+import { POINTER_EVENTS } from "../../src/constants";
 
 export const AppWelcomeScreen: React.FC<{
   setCollabDialogShown: (toggle: boolean) => any;
@@ -18,7 +19,7 @@ export const AppWelcomeScreen: React.FC<{
         if (bit === "Excalidraw+") {
           return (
             <a
-              style={{ pointerEvents: "all" }}
+              style={{ pointerEvents: POINTER_EVENTS.inheritFromUI }}
               href={`${
                 import.meta.env.VITE_APP_PLUS_APP
               }?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}

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

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

+ 0 - 0
src/excalidraw-app/components/ExcalidrawPlusAppLink.tsx → excalidraw-app/components/ExcalidrawPlusAppLink.tsx


+ 12 - 12
src/excalidraw-app/components/ExportToExcalidrawPlus.tsx → excalidraw-app/components/ExportToExcalidrawPlus.tsx

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

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

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

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

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

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

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

+ 5 - 5
src/excalidraw-app/data/LocalData.ts → excalidraw-app/data/LocalData.ts

@@ -11,11 +11,11 @@
  */
 
 import { createStore, entries, del, getMany, set, setMany } from "idb-keyval";
-import { clearAppStateForLocalStorage } from "../../appState";
-import { clearElementsForLocalStorage } from "../../element";
-import { ExcalidrawElement, FileId } from "../../element/types";
-import { AppState, BinaryFileData, BinaryFiles } from "../../types";
-import { debounce } from "../../utils";
+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 { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
 import { FileManager } from "./FileManager";
 import { Locker } from "./Locker";

+ 0 - 0
src/excalidraw-app/data/Locker.ts → excalidraw-app/data/Locker.ts


+ 8 - 8
src/excalidraw-app/data/firebase.ts → excalidraw-app/data/firebase.ts

@@ -1,20 +1,20 @@
-import { ExcalidrawElement, FileId } from "../../element/types";
-import { getSceneVersion } from "../../element";
+import { ExcalidrawElement, FileId } from "../../src/element/types";
+import { getSceneVersion } from "../../src/element";
 import Portal from "../collab/Portal";
-import { restoreElements } from "../../data/restore";
+import { restoreElements } from "../../src/data/restore";
 import {
   AppState,
   BinaryFileData,
   BinaryFileMetadata,
   DataURL,
-} from "../../types";
+} from "../../src/types";
 import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
-import { decompressData } from "../../data/encode";
-import { encryptData, decryptData } from "../../data/encryption";
-import { MIME_TYPES } from "../../constants";
+import { decompressData } from "../../src/data/encode";
+import { encryptData, decryptData } from "../../src/data/encryption";
+import { MIME_TYPES } from "../../src/constants";
 import { reconcileElements } from "../collab/reconciliation";
 import { getSyncableElements, SyncableExcalidrawElement } from ".";
-import { ResolutionType } from "../../utility-types";
+import { ResolutionType } from "../../src/utility-types";
 
 // private
 // -----------------------------------------------------------------------------

+ 11 - 11
src/excalidraw-app/data/index.ts → excalidraw-app/data/index.ts

@@ -1,23 +1,23 @@
-import { compressData, decompressData } from "../../data/encode";
+import { compressData, decompressData } from "../../src/data/encode";
 import {
   decryptData,
   generateEncryptionKey,
   IV_LENGTH_BYTES,
-} from "../../data/encryption";
-import { serializeAsJSON } from "../../data/json";
-import { restore } from "../../data/restore";
-import { ImportedDataState } from "../../data/types";
-import { isInvisiblySmallElement } from "../../element/sizeHelpers";
-import { isInitializedImageElement } from "../../element/typeChecks";
-import { ExcalidrawElement, FileId } from "../../element/types";
-import { t } from "../../i18n";
+} 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";
 import {
   AppState,
   BinaryFileData,
   BinaryFiles,
   UserIdleState,
-} from "../../types";
-import { bytesToHexString } from "../../utils";
+} from "../../src/types";
+import { bytesToHexString } from "../../src/utils";
 import {
   DELETED_ELEMENT_TIMEOUT,
   FILE_UPLOAD_MAX_BYTES,

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

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

+ 0 - 0
src/excalidraw-app/data/tabSync.ts → excalidraw-app/data/tabSync.ts


+ 0 - 0
src/excalidraw-app/debug.ts → excalidraw-app/debug.ts


+ 3 - 2
src/excalidraw-app/index.scss → excalidraw-app/index.scss

@@ -77,13 +77,14 @@
   align-items: center;
   border: 1px solid var(--color-primary);
   padding: 0.5rem 0.75rem;
-  border-radius: var(--space-factor);
+  border-radius: var(--border-radius-lg);
+  background-color: var(--island-bg-color);
   color: var(--color-primary) !important;
   text-decoration: none !important;
 
   font-size: 0.75rem;
   box-sizing: border-box;
-  height: var(--default-button-size);
+  height: var(--lg-button-size);
 
   &:hover {
     background-color: var(--color-primary);

+ 31 - 24
src/excalidraw-app/index.tsx → excalidraw-app/index.tsx

@@ -1,32 +1,32 @@
-import polyfill from "../polyfill";
+import polyfill from "../src/polyfill";
 import LanguageDetector from "i18next-browser-languagedetector";
 import { useEffect, useRef, useState } from "react";
-import { trackEvent } from "../analytics";
-import { getDefaultAppState } from "../appState";
-import { ErrorDialog } from "../components/ErrorDialog";
-import { TopErrorBoundary } from "../components/TopErrorBoundary";
-import { useSubtypes } from "../element/subtypes/use";
+import { trackEvent } from "../src/analytics";
+import { getDefaultAppState } from "../src/appState";
+import { ErrorDialog } from "../src/components/ErrorDialog";
+import { TopErrorBoundary } from "../src/components/TopErrorBoundary";
+import { useSubtypes } from "../src/element/subtypes/use";
 import {
   APP_NAME,
   EVENT,
   THEME,
   TITLE_TIMEOUT,
   VERSION_TIMEOUT,
-} from "../constants";
-import { loadFromBlob } from "../data/blob";
+} from "../src/constants";
+import { loadFromBlob } from "../src/data/blob";
 import {
   ExcalidrawElement,
   FileId,
   NonDeletedExcalidrawElement,
   Theme,
-} from "../element/types";
-import { useCallbackRefState } from "../hooks/useCallbackRefState";
-import { t } from "../i18n";
+} from "../src/element/types";
+import { useCallbackRefState } from "../src/hooks/useCallbackRefState";
+import { t } from "../src/i18n";
 import {
   Excalidraw,
   defaultLang,
   LiveCollaborationTrigger,
-} from "../packages/excalidraw/index";
+} from "../src/packages/excalidraw/index";
 import {
   AppState,
   LibraryItems,
@@ -34,7 +34,7 @@ import {
   BinaryFiles,
   ExcalidrawInitialDataState,
   UIAppState,
-} from "../types";
+} from "../src/types";
 import {
   debounce,
   getVersion,
@@ -44,7 +44,7 @@ import {
   ResolvablePromise,
   resolvablePromise,
   isRunningInIframe,
-} from "../utils";
+} from "../src/utils";
 import {
   FIREBASE_STORAGE_PREFIXES,
   STORAGE_KEYS,
@@ -69,33 +69,40 @@ import {
   importUsernameFromLocalStorage,
 } from "./data/localStorage";
 import CustomStats from "./CustomStats";
-import { restore, restoreAppState, RestoredDataState } from "../data/restore";
+import {
+  restore,
+  restoreAppState,
+  RestoredDataState,
+} from "../src/data/restore";
 import {
   ExportToExcalidrawPlus,
   exportToExcalidrawPlus,
 } from "./components/ExportToExcalidrawPlus";
 import { updateStaleImageStatuses } from "./data/FileManager";
-import { newElementWith } from "../element/mutateElement";
-import { isInitializedImageElement } from "../element/typeChecks";
+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 "../data/library";
+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 "../jotai";
+import { useAtomWithInitialValue } from "../src/jotai";
 import { appJotaiStore } from "./app-jotai";
 
 import "./index.scss";
-import { ResolutionType } from "../utility-types";
-import { ShareableLinkDialog } from "../components/ShareableLinkDialog";
-import { openConfirmModal } from "../components/OverwriteConfirm/OverwriteConfirmState";
-import { OverwriteConfirmDialog } from "../components/OverwriteConfirm/OverwriteConfirm";
-import Trans from "../components/Trans";
+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();
 

+ 0 - 0
src/excalidraw-app/sentry.ts → excalidraw-app/sentry.ts


+ 29 - 0
excalidraw-app/tests/LanguageList.test.tsx

@@ -0,0 +1,29 @@
+import { defaultLang } from "../../src/i18n";
+import { UI } from "../../src/tests/helpers/ui";
+import { screen, fireEvent, waitFor, render } from "../../src/tests/test-utils";
+
+import ExcalidrawApp from "../../excalidraw-app";
+
+describe("Test LanguageList", () => {
+  it("rerenders UI on language change", async () => {
+    await render(<ExcalidrawApp />);
+
+    // select rectangle tool to show properties menu
+    UI.clickTool("rectangle");
+    // english lang should display `thin` label
+    expect(screen.queryByTitle(/thin/i)).not.toBeNull();
+    fireEvent.click(document.querySelector(".dropdown-menu-button")!);
+
+    fireEvent.change(document.querySelector(".dropdown-select__language")!, {
+      target: { value: "de-DE" },
+    });
+    // switching to german, `thin` label should no longer exist
+    await waitFor(() => expect(screen.queryByTitle(/thin/i)).toBeNull());
+    // reset language
+    fireEvent.change(document.querySelector(".dropdown-select__language")!, {
+      target: { value: defaultLang.code },
+    });
+    // switching back to English
+    await waitFor(() => expect(screen.queryByTitle(/thin/i)).not.toBeNull());
+  });
+});

+ 3 - 3
src/tests/MobileMenu.test.tsx → excalidraw-app/tests/MobileMenu.test.tsx

@@ -1,11 +1,11 @@
-import ExcalidrawApp from "../excalidraw-app";
+import ExcalidrawApp from "../../excalidraw-app";
 import {
   mockBoundingClientRect,
   render,
   restoreOriginalGetBoundingClientRect,
-} from "./test-utils";
+} from "../../src/tests/test-utils";
 
-import { UI } from "./helpers/ui";
+import { UI } from "../../src/tests/helpers/ui";
 
 describe("Test MobileMenu", () => {
   const { h } = window;

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 19 - 0
excalidraw-app/tests/__snapshots__/MobileMenu.test.tsx.snap


+ 6 - 6
src/tests/collab.test.tsx → excalidraw-app/tests/collab.test.tsx

@@ -1,8 +1,8 @@
 import { vi } from "vitest";
-import { render, updateSceneData, waitFor } from "./test-utils";
-import ExcalidrawApp from "../excalidraw-app";
-import { API } from "./helpers/api";
-import { createUndoAction } from "../actions/actionHistory";
+import { render, updateSceneData, waitFor } from "../../src/tests/test-utils";
+import ExcalidrawApp from "../../excalidraw-app";
+import { API } from "../../src/tests/helpers/api";
+import { createUndoAction } from "../../src/actions/actionHistory";
 const { h } = window;
 
 Object.defineProperty(window, "crypto", {
@@ -16,7 +16,7 @@ Object.defineProperty(window, "crypto", {
   },
 });
 
-vi.mock("../excalidraw-app/data/index.ts", async (importActual) => {
+vi.mock("../../excalidraw-app/data/index.ts", async (importActual) => {
   const module = (await importActual()) as any;
   return {
     __esmodule: true,
@@ -27,7 +27,7 @@ vi.mock("../excalidraw-app/data/index.ts", async (importActual) => {
   };
 });
 
-vi.mock("../excalidraw-app/data/firebase.ts", () => {
+vi.mock("../../excalidraw-app/data/firebase.ts", () => {
   const loadFromFirebase = async () => null;
   const saveToFirebase = () => {};
   const isSavedToFirebase = () => true;

+ 5 - 5
src/tests/reconciliation.test.ts → excalidraw-app/tests/reconciliation.test.ts

@@ -1,13 +1,13 @@
 import { expect } from "chai";
-import { PRECEDING_ELEMENT_KEY } from "../constants";
-import { ExcalidrawElement } from "../element/types";
+import { PRECEDING_ELEMENT_KEY } from "../../src/constants";
+import { ExcalidrawElement } from "../../src/element/types";
 import {
   BroadcastedExcalidrawElement,
   ReconciledElements,
   reconcileElements,
-} from "../excalidraw-app/collab/reconciliation";
-import { randomInteger } from "../random";
-import { AppState } from "../types";
+} from "../../excalidraw-app/collab/reconciliation";
+import { randomInteger } from "../../src/random";
+import { AppState } from "../../src/types";
 
 type Id = string;
 type ElementLike = {

+ 50 - 21
src/components/App.tsx

@@ -85,6 +85,7 @@ import {
   VERTICAL_ALIGN,
   YOUTUBE_STATES,
   ZOOM_STEP,
+  POINTER_EVENTS,
 } from "../constants";
 import { exportCanvas, loadFromBlob } from "../data";
 import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
@@ -886,7 +887,9 @@ class App extends React.Component<AppProps, AppState> {
                   width: isVisible ? `${el.width}px` : 0,
                   height: isVisible ? `${el.height}px` : 0,
                   transform: isVisible ? `rotate(${el.angle}rad)` : "none",
-                  pointerEvents: isActive ? "auto" : "none",
+                  pointerEvents: isActive
+                    ? POINTER_EVENTS.enabled
+                    : POINTER_EVENTS.disabled,
                 }}
               >
                 {isHovered && (
@@ -1110,9 +1113,9 @@ class App extends React.Component<AppProps, AppState> {
             whiteSpace: "nowrap",
             textOverflow: "ellipsis",
             cursor: CURSOR_TYPE.MOVE,
-            // disable all interaction (e.g. cursor change) when in view
-            // mode
-            pointerEvents: this.state.viewModeEnabled ? "none" : "all",
+            pointerEvents: this.state.viewModeEnabled
+              ? POINTER_EVENTS.disabled
+              : POINTER_EVENTS.inheritFromUI,
           }}
           onPointerDown={(event) => this.handleCanvasPointerDown(event)}
           onWheel={(event) => this.handleWheel(event)}
@@ -1154,6 +1157,16 @@ class App extends React.Component<AppProps, AppState> {
           "excalidraw--view-mode": this.state.viewModeEnabled,
           "excalidraw--mobile": this.device.isMobile,
         })}
+        style={{
+          ["--ui-pointerEvents" as any]:
+            this.state.selectionElement ||
+            this.state.draggingElement ||
+            this.state.resizingElement ||
+            (this.state.editingElement &&
+              !isTextElement(this.state.editingElement))
+              ? POINTER_EVENTS.disabled
+              : POINTER_EVENTS.enabled,
+        }}
         ref={this.excalidrawContainerRef}
         onDrop={this.handleAppOnDrop}
         tabIndex={0}
@@ -1346,7 +1359,8 @@ class App extends React.Component<AppProps, AppState> {
   private openEyeDropper = ({ type }: { type: "stroke" | "background" }) => {
     jotaiStore.set(activeEyeDropperAtom, {
       swapPreviewOnAlt: true,
-      previewType: type === "stroke" ? "strokeColor" : "backgroundColor",
+      colorPickerType:
+        type === "stroke" ? "elementStroke" : "elementBackground",
       onSelect: (color, event) => {
         const shouldUpdateStrokeColor =
           (type === "background" && event.altKey) ||
@@ -1357,12 +1371,14 @@ class App extends React.Component<AppProps, AppState> {
           this.state.activeTool.type !== "selection"
         ) {
           if (shouldUpdateStrokeColor) {
-            this.setState({
-              currentItemStrokeColor: color,
+            this.syncActionResult({
+              appState: { ...this.state, currentItemStrokeColor: color },
+              commitToHistory: true,
             });
           } else {
-            this.setState({
-              currentItemBackgroundColor: color,
+            this.syncActionResult({
+              appState: { ...this.state, currentItemBackgroundColor: color },
+              commitToHistory: true,
             });
           }
         } else {
@@ -3975,7 +3991,7 @@ class App extends React.Component<AppProps, AppState> {
         const [gridX, gridY] = getGridPoint(
           scenePointerX,
           scenePointerY,
-          this.state.gridSize,
+          event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
         );
 
         const [lastCommittedX, lastCommittedY] =
@@ -4792,7 +4808,11 @@ class App extends React.Component<AppProps, AppState> {
       origin,
       withCmdOrCtrl: event[KEYS.CTRL_OR_CMD],
       originInGrid: tupleToCoors(
-        getGridPoint(origin.x, origin.y, this.state.gridSize),
+        getGridPoint(
+          origin.x,
+          origin.y,
+          event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
+        ),
       ),
       scrollbars: isOverScrollBars(
         currentScrollBars,
@@ -5316,7 +5336,11 @@ class App extends React.Component<AppProps, AppState> {
     sceneY: number;
     link: string;
   }) => {
-    const [gridX, gridY] = getGridPoint(sceneX, sceneY, this.state.gridSize);
+    const [gridX, gridY] = getGridPoint(
+      sceneX,
+      sceneY,
+      this.lastPointerDown?.[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
+    );
 
     const embedLink = getEmbedLink(link);
 
@@ -5362,7 +5386,11 @@ class App extends React.Component<AppProps, AppState> {
     sceneX: number;
     sceneY: number;
   }) => {
-    const [gridX, gridY] = getGridPoint(sceneX, sceneY, this.state.gridSize);
+    const [gridX, gridY] = getGridPoint(
+      sceneX,
+      sceneY,
+      this.lastPointerDown?.[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
+    );
 
     const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
       x: gridX,
@@ -5446,7 +5474,7 @@ class App extends React.Component<AppProps, AppState> {
       const [gridX, gridY] = getGridPoint(
         pointerDownState.origin.x,
         pointerDownState.origin.y,
-        this.state.gridSize,
+        event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
       );
 
       const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
@@ -5540,7 +5568,7 @@ class App extends React.Component<AppProps, AppState> {
     const [gridX, gridY] = getGridPoint(
       pointerDownState.origin.x,
       pointerDownState.origin.y,
-      this.state.gridSize,
+      this.lastPointerDown?.[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
     );
 
     const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
@@ -5599,7 +5627,7 @@ class App extends React.Component<AppProps, AppState> {
     const [gridX, gridY] = getGridPoint(
       pointerDownState.origin.x,
       pointerDownState.origin.y,
-      this.state.gridSize,
+      this.lastPointerDown?.[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
     );
 
     const frame = newFrameElement({
@@ -5682,7 +5710,7 @@ class App extends React.Component<AppProps, AppState> {
       const [gridX, gridY] = getGridPoint(
         pointerCoords.x,
         pointerCoords.y,
-        this.state.gridSize,
+        event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
       );
 
       // for arrows/lines, don't start dragging until a given threshold
@@ -5728,6 +5756,7 @@ class App extends React.Component<AppProps, AppState> {
             this.state.selectedLinearElement,
             pointerCoords,
             this.state,
+            !event[KEYS.CTRL_OR_CMD],
           );
           if (!ret) {
             return;
@@ -5853,7 +5882,7 @@ class App extends React.Component<AppProps, AppState> {
           const [dragX, dragY] = getGridPoint(
             pointerCoords.x - pointerDownState.drag.offset.x,
             pointerCoords.y - pointerDownState.drag.offset.y,
-            this.state.gridSize,
+            event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
           );
 
           const [dragDistanceX, dragDistanceY] = [
@@ -5920,7 +5949,7 @@ class App extends React.Component<AppProps, AppState> {
                 const [originDragX, originDragY] = getGridPoint(
                   pointerDownState.origin.x - pointerDownState.drag.offset.x,
                   pointerDownState.origin.y - pointerDownState.drag.offset.y,
-                  this.state.gridSize,
+                  event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
                 );
                 mutateElement(duplicatedElement, {
                   x: duplicatedElement.x + (originDragX - dragX),
@@ -7713,7 +7742,7 @@ class App extends React.Component<AppProps, AppState> {
       const [gridX, gridY] = getGridPoint(
         pointerCoords.x,
         pointerCoords.y,
-        this.state.gridSize,
+        event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
       );
 
       const image =
@@ -7782,7 +7811,7 @@ class App extends React.Component<AppProps, AppState> {
     const [resizeX, resizeY] = getGridPoint(
       pointerCoords.x - pointerDownState.resize.offset.x,
       pointerCoords.y - pointerDownState.resize.offset.y,
-      this.state.gridSize,
+      event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
     );
 
     const frameElementsOffsetsMap = new Map<

+ 7 - 4
src/components/ColorPicker/ColorInput.tsx

@@ -1,7 +1,10 @@
 import { useCallback, useEffect, useRef, useState } from "react";
 import { getColor } from "./ColorPicker";
 import { useAtom } from "jotai";
-import { activeColorPickerSectionAtom } from "./colorPickerUtils";
+import {
+  ColorPickerType,
+  activeColorPickerSectionAtom,
+} from "./colorPickerUtils";
 import { eyeDropperIcon } from "../icons";
 import { jotaiScope } from "../../jotai";
 import { KEYS } from "../../keys";
@@ -15,14 +18,14 @@ interface ColorInputProps {
   color: string;
   onChange: (color: string) => void;
   label: string;
-  eyeDropperType: "strokeColor" | "backgroundColor";
+  colorPickerType: ColorPickerType;
 }
 
 export const ColorInput = ({
   color,
   onChange,
   label,
-  eyeDropperType,
+  colorPickerType,
 }: ColorInputProps) => {
   const device = useDevice();
   const [innerValue, setInnerValue] = useState(color);
@@ -116,7 +119,7 @@ export const ColorInput = ({
                   : {
                       keepOpenOnAlt: false,
                       onSelect: (color) => onChange(color),
-                      previewType: eyeDropperType,
+                      colorPickerType,
                     },
               )
             }

+ 5 - 12
src/components/ColorPicker/ColorPicker.tsx

@@ -82,14 +82,7 @@ const ColorPickerPopupContent = ({
   const { container } = useExcalidrawContainer();
   const { isMobile, isLandscape } = useDevice();
 
-  const eyeDropperType =
-    type === "canvasBackground"
-      ? undefined
-      : type === "elementBackground"
-      ? "backgroundColor"
-      : "strokeColor";
-
-  const colorInputJSX = eyeDropperType && (
+  const colorInputJSX = (
     <div>
       <PickerHeading>{t("colorPicker.hexCode")}</PickerHeading>
       <ColorInput
@@ -98,7 +91,7 @@ const ColorPickerPopupContent = ({
         onChange={(color) => {
           onChange(color);
         }}
-        eyeDropperType={eyeDropperType}
+        colorPickerType={type}
       />
     </div>
   );
@@ -160,7 +153,7 @@ const ColorPickerPopupContent = ({
             "0px 7px 14px rgba(0, 0, 0, 0.05), 0px 0px 3.12708px rgba(0, 0, 0, 0.0798), 0px 0px 0.931014px rgba(0, 0, 0, 0.1702)",
         }}
       >
-        {palette && eyeDropperType ? (
+        {palette ? (
           <Picker
             palette={palette}
             color={color}
@@ -173,7 +166,7 @@ const ColorPickerPopupContent = ({
                   state = state || {
                     keepOpenOnAlt: true,
                     onSelect: onChange,
-                    previewType: eyeDropperType,
+                    colorPickerType: type,
                   };
                   state.keepOpenOnAlt = true;
                   return state;
@@ -184,7 +177,7 @@ const ColorPickerPopupContent = ({
                   : {
                       keepOpenOnAlt: false,
                       onSelect: onChange,
-                      previewType: eyeDropperType,
+                      colorPickerType: type,
                     };
               });
             }}

+ 50 - 36
src/components/EyeDropper.tsx

@@ -1,35 +1,47 @@
 import { atom } from "jotai";
-import { useEffect, useRef } from "react";
+import React, { useEffect, useRef } from "react";
 import { createPortal } from "react-dom";
 import { rgbToHex } from "../colors";
 import { EVENT } from "../constants";
 import { useUIAppState } from "../context/ui-appState";
-import { mutateElement } from "../element/mutateElement";
 import { useCreatePortalContainer } from "../hooks/useCreatePortalContainer";
 import { useOutsideClick } from "../hooks/useOutsideClick";
 import { KEYS } from "../keys";
 import { getSelectedElements } from "../scene";
-import Scene from "../scene/Scene";
-import { ShapeCache } from "../scene/ShapeCache";
 import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App";
+import { useStable } from "../hooks/useStable";
 
 import "./EyeDropper.scss";
+import { ColorPickerType } from "./ColorPicker/colorPickerUtils";
+import { ExcalidrawElement } from "../element/types";
 
-type EyeDropperProperties = {
+export type EyeDropperProperties = {
   keepOpenOnAlt: boolean;
   swapPreviewOnAlt?: boolean;
+  /** called when user picks color (on pointerup) */
   onSelect: (color: string, event: PointerEvent) => void;
-  previewType: "strokeColor" | "backgroundColor";
+  /**
+   * property of selected elements to update live when alt-dragging.
+   * Supply `null` if not applicable (e.g. updating the canvas bg instead of
+   * elements)
+   **/
+  colorPickerType: ColorPickerType;
 };
 
 export const activeEyeDropperAtom = atom<null | EyeDropperProperties>(null);
 
 export const EyeDropper: React.FC<{
   onCancel: () => void;
-  onSelect: Required<EyeDropperProperties>["onSelect"];
-  swapPreviewOnAlt?: EyeDropperProperties["swapPreviewOnAlt"];
-  previewType: EyeDropperProperties["previewType"];
-}> = ({ onCancel, onSelect, swapPreviewOnAlt, previewType }) => {
+  onSelect: EyeDropperProperties["onSelect"];
+  /** called when color changes, on pointerdown for preview */
+  onChange: (
+    type: ColorPickerType,
+    color: string,
+    selectedElements: ExcalidrawElement[],
+    event: { altKey: boolean },
+  ) => void;
+  colorPickerType: EyeDropperProperties["colorPickerType"];
+}> = ({ onCancel, onChange, onSelect, colorPickerType }) => {
   const eyeDropperContainer = useCreatePortalContainer({
     className: "excalidraw-eye-dropper-backdrop",
     parentSelector: ".excalidraw-eye-dropper-container",
@@ -40,9 +52,13 @@ export const EyeDropper: React.FC<{
 
   const selectedElements = getSelectedElements(elements, appState);
 
-  const metaStuffRef = useRef({ selectedElements, app });
-  metaStuffRef.current.selectedElements = selectedElements;
-  metaStuffRef.current.app = app;
+  const stableProps = useStable({
+    app,
+    onCancel,
+    onChange,
+    onSelect,
+    selectedElements,
+  });
 
   const { container: excalidrawContainer } = useExcalidrawContainer();
 
@@ -90,28 +106,28 @@ export const EyeDropper: React.FC<{
       const currentColor = getCurrentColor({ clientX, clientY });
 
       if (isHoldingPointerDown) {
-        for (const element of metaStuffRef.current.selectedElements) {
-          mutateElement(
-            element,
-            {
-              [altKey && swapPreviewOnAlt
-                ? previewType === "strokeColor"
-                  ? "backgroundColor"
-                  : "strokeColor"
-                : previewType]: currentColor,
-            },
-            false,
-          );
-          ShapeCache.delete(element);
-        }
-        Scene.getScene(
-          metaStuffRef.current.selectedElements[0],
-        )?.informMutation();
+        stableProps.onChange(
+          colorPickerType,
+          currentColor,
+          stableProps.selectedElements,
+          { altKey },
+        );
       }
 
       colorPreviewDiv.style.background = currentColor;
     };
 
+    const onCancel = () => {
+      stableProps.onCancel();
+    };
+
+    const onSelect: Required<EyeDropperProperties>["onSelect"] = (
+      color,
+      event,
+    ) => {
+      stableProps.onSelect(color, event);
+    };
+
     const pointerDownListener = (event: PointerEvent) => {
       isHoldingPointerDown = true;
       // NOTE we can't event.preventDefault() as that would stop
@@ -148,8 +164,8 @@ export const EyeDropper: React.FC<{
 
     // init color preview else it would show only after the first mouse move
     mouseMoveListener({
-      clientX: metaStuffRef.current.app.lastViewportPosition.x,
-      clientY: metaStuffRef.current.app.lastViewportPosition.y,
+      clientX: stableProps.app.lastViewportPosition.x,
+      clientY: stableProps.app.lastViewportPosition.y,
       altKey: false,
     });
 
@@ -179,12 +195,10 @@ export const EyeDropper: React.FC<{
       window.removeEventListener(EVENT.BLUR, onCancel);
     };
   }, [
+    stableProps,
     app.canvas,
     eyeDropperContainer,
-    onCancel,
-    onSelect,
-    swapPreviewOnAlt,
-    previewType,
+    colorPickerType,
     excalidrawContainer,
     appState.offsetLeft,
     appState.offsetTop,

+ 1 - 1
src/components/FixedSideContainer.scss

@@ -7,7 +7,7 @@
   }
 
   .FixedSideContainer > * {
-    pointer-events: all;
+    pointer-events: var(--ui-pointerEvents);
   }
 
   .FixedSideContainer_side_top {

+ 20 - 11
src/components/HintViewer.tsx

@@ -83,27 +83,36 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
   if (activeTool.type === "selection") {
     if (
       appState.draggingElement?.type === "selection" &&
+      !selectedElements.length &&
       !appState.editingElement &&
       !appState.editingLinearElement
     ) {
       return t("hints.deepBoxSelect");
     }
+
+    if (appState.gridSize && appState.draggingElement) {
+      return t("hints.disableSnapping");
+    }
+
     if (!selectedElements.length && !isMobile) {
       return t("hints.canvasPanning");
     }
-  }
 
-  if (selectedElements.length === 1) {
-    if (isLinearElement(selectedElements[0])) {
-      if (appState.editingLinearElement) {
-        return appState.editingLinearElement.selectedPointsIndices
-          ? t("hints.lineEditor_pointSelected")
-          : t("hints.lineEditor_nothingSelected");
+    if (selectedElements.length === 1) {
+      if (isLinearElement(selectedElements[0])) {
+        if (appState.editingLinearElement) {
+          return appState.editingLinearElement.selectedPointsIndices
+            ? t("hints.lineEditor_pointSelected")
+            : t("hints.lineEditor_nothingSelected");
+        }
+        return t("hints.lineEditor_info");
+      }
+      if (
+        !appState.draggingElement &&
+        isTextBindableContainer(selectedElements[0])
+      ) {
+        return t("hints.bindTextToElement");
       }
-      return t("hints.lineEditor_info");
-    }
-    if (isTextBindableContainer(selectedElements[0])) {
-      return t("hints.bindTextToElement");
     }
   }
 

+ 28 - 10
src/components/LayerUI.scss

@@ -56,34 +56,52 @@
     }
 
     .disable-zen-mode {
-      height: 30px;
+      padding: 10px;
       position: absolute;
-      bottom: 10px;
+      bottom: 0;
       [dir="ltr"] & {
-        right: 15px;
+        right: 1rem;
       }
       [dir="rtl"] & {
-        left: 15px;
+        left: 1rem;
       }
-      font-size: 10px;
-      padding: 10px;
-      font-weight: 500;
       opacity: 0;
       visibility: hidden;
       transition: visibility 0s linear 0s, opacity 0.5s;
 
+      font-family: var(--ui-font);
+      font-size: 0.75rem;
+      font-weight: 500;
+      line-height: 1;
+
+      border-radius: var(--border-radius-lg);
+      border: 1px solid var(--default-border-color);
+      background-color: var(--island-bg-color);
+      color: var(--text-primary-color);
+
+      &:hover {
+        background-color: var(--button-hover-bg);
+      }
+      &:active {
+        border-color: var(--color-primary);
+      }
+
       &--visible {
         opacity: 1;
         visibility: visible;
         transition: visibility 0s linear 300ms, opacity 0.5s;
         transition-delay: 0.8s;
+
+        pointer-events: var(--ui-pointerEvents);
       }
     }
 
     .layer-ui__wrapper__footer-left,
-    .layer-ui__wrapper__footer-right,
-    .disable-zen-mode--visible {
-      pointer-events: all;
+    .footer-center,
+    .layer-ui__wrapper__footer-right {
+      & > * {
+        pointer-events: var(--ui-pointerEvents);
+      }
     }
 
     .layer-ui__wrapper__footer-right {

+ 40 - 10
src/components/LayerUI.tsx

@@ -2,7 +2,7 @@ import clsx from "clsx";
 import React from "react";
 import { ActionManager } from "../actions/manager";
 import { CLASSES, DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_WIDTH } from "../constants";
-import { isTextElement, showSelectedShapeActions } from "../element";
+import { showSelectedShapeActions } from "../element";
 import { NonDeletedExcalidrawElement } from "../element/types";
 import { Language, t } from "../i18n";
 import { calculateScrollCenter } from "../scene";
@@ -52,6 +52,9 @@ import { EyeDropper, activeEyeDropperAtom } from "./EyeDropper";
 
 import "./LayerUI.scss";
 import "./Toolbar.scss";
+import { mutateElement } from "../element/mutateElement";
+import { ShapeCache } from "../scene/ShapeCache";
+import Scene from "../scene/Scene";
 
 interface LayerUIProps {
   actionManager: ActionManager;
@@ -368,11 +371,44 @@ const LayerUI = ({
       )}
       {eyeDropperState && !device.isMobile && (
         <EyeDropper
-          swapPreviewOnAlt={eyeDropperState.swapPreviewOnAlt}
-          previewType={eyeDropperState.previewType}
+          colorPickerType={eyeDropperState.colorPickerType}
           onCancel={() => {
             setEyeDropperState(null);
           }}
+          onChange={(colorPickerType, color, selectedElements, { altKey }) => {
+            if (
+              colorPickerType !== "elementBackground" &&
+              colorPickerType !== "elementStroke"
+            ) {
+              return;
+            }
+
+            if (selectedElements.length) {
+              for (const element of selectedElements) {
+                mutateElement(
+                  element,
+                  {
+                    [altKey && eyeDropperState.swapPreviewOnAlt
+                      ? colorPickerType === "elementBackground"
+                        ? "strokeColor"
+                        : "backgroundColor"
+                      : colorPickerType === "elementBackground"
+                      ? "backgroundColor"
+                      : "strokeColor"]: color,
+                  },
+                  false,
+                );
+                ShapeCache.delete(element);
+              }
+              Scene.getScene(selectedElements[0])?.informMutation();
+            } else if (colorPickerType === "elementBackground") {
+              setAppState({
+                currentItemBackgroundColor: color,
+              });
+            } else {
+              setAppState({ currentItemStrokeColor: color });
+            }
+          }}
           onSelect={(color, event) => {
             setEyeDropperState((state) => {
               return state?.keepOpenOnAlt && event.altKey ? state : null;
@@ -427,13 +463,7 @@ const LayerUI = ({
       {!device.isMobile && (
         <>
           <div
-            className={clsx("layer-ui__wrapper", {
-              "disable-pointerEvents":
-                appState.draggingElement ||
-                appState.resizingElement ||
-                (appState.editingElement &&
-                  !isTextElement(appState.editingElement)),
-            })}
+            className="layer-ui__wrapper"
             style={
               appState.openSidebar &&
               isSidebarDocked &&

+ 2 - 0
src/components/Sidebar/Sidebar.scss

@@ -17,6 +17,8 @@
     background-color: var(--sidebar-bg-color);
     box-shadow: var(--sidebar-shadow);
 
+    pointer-events: var(--ui-pointerEvents);
+
     :root[dir="rtl"] & {
       left: 0;
       right: auto;

+ 1 - 1
src/components/Stats.scss

@@ -7,7 +7,7 @@
     right: 12px;
     font-size: 12px;
     z-index: 10;
-    pointer-events: all;
+    pointer-events: var(--ui-pointerEvents);
 
     h3 {
       margin: 0 24px 8px 0;

+ 1 - 1
src/components/UserList.scss

@@ -26,7 +26,7 @@
   }
 
   .UserList > * {
-    pointer-events: all;
+    pointer-events: var(--ui-pointerEvents);
   }
 
   .UserList_mobile {

+ 1 - 1
src/components/footer/Footer.tsx

@@ -73,7 +73,7 @@ const Footer = ({
       <FooterCenterTunnel.Out />
       <div
         className={clsx("layer-ui__wrapper__footer-right zen-mode-transition", {
-          "transition-right disable-pointerEvents": appState.zenModeEnabled,
+          "transition-right": appState.zenModeEnabled,
         })}
       >
         <div style={{ position: "relative" }}>

+ 2 - 1
src/components/footer/FooterCenter.scss

@@ -1,10 +1,11 @@
 .footer-center {
   pointer-events: none;
   & > * {
-    pointer-events: all;
+    pointer-events: var(--ui-pointerEvents);
   }
 
   display: flex;
   width: 100%;
   justify-content: flex-start;
+  margin-inline-end: 0.6rem;
 }

+ 3 - 3
src/components/welcome-screen/WelcomeScreen.scss

@@ -161,7 +161,7 @@
   .welcome-screen-menu-item {
     box-sizing: border-box;
 
-    pointer-events: all;
+    pointer-events: var(--ui-pointerEvents);
 
     color: var(--color-gray-50);
     font-size: 0.875rem;
@@ -202,7 +202,7 @@
     }
   }
 
-  &:not(:active) .welcome-screen-menu-item:hover {
+  .welcome-screen-menu-item:hover {
     text-decoration: none;
     background: var(--color-gray-10);
 
@@ -246,7 +246,7 @@
       }
     }
 
-    &:not(:active) .welcome-screen-menu-item:hover {
+    .welcome-screen-menu-item:hover {
       background: var(--color-gray-85);
 
       .welcome-screen-menu-item__shortcut {

+ 8 - 0
src/constants.ts

@@ -41,6 +41,14 @@ export const POINTER_BUTTON = {
   TOUCH: -1,
 } as const;
 
+export const POINTER_EVENTS = {
+  enabled: "all",
+  disabled: "none",
+  // asserted as any so it can be freely assigned to React Element
+  // "pointerEnvets" CSS prop
+  inheritFromUI: "var(--ui-pointerEvents)" as any,
+} as const;
+
 export enum EVENT {
   COPY = "copy",
   PASTE = "paste",

+ 7 - 18
src/css/styles.scss

@@ -253,7 +253,7 @@
       max-height: 100%;
       display: flex;
       flex-direction: column;
-      pointer-events: initial;
+      pointer-events: var(--ui-pointerEvents);
 
       .panelColumn {
         padding: 8px 8px 0 8px;
@@ -301,7 +301,7 @@
     pointer-events: none !important;
 
     & > * {
-      pointer-events: all;
+      pointer-events: var(--ui-pointerEvents);
     }
   }
 
@@ -312,16 +312,16 @@
     cursor: default;
     pointer-events: none !important;
 
+    & > * {
+      pointer-events: var(--ui-pointerEvents);
+    }
+
     @media (min-width: 1536px) {
       grid-template-columns: 1fr 1fr 1fr;
       grid-gap: 3rem;
     }
   }
 
-  .layer-ui__wrapper:not(.disable-pointerEvents) .App-menu_top > * {
-    pointer-events: all;
-  }
-
   .App-menu_top > *:first-child {
     justify-self: flex-start;
   }
@@ -425,17 +425,6 @@
     }
   }
 
-  .disable-zen-mode {
-    border-radius: var(--border-radius-lg);
-    background-color: var(--color-gray-20);
-    border: 1px solid var(--color-gray-30);
-    padding: 10px 20px;
-
-    &:hover {
-      background-color: var(--color-gray-30);
-    }
-  }
-
   .scroll-back-to-content {
     border-radius: var(--border-radius-lg);
     background-color: var(--island-bg-color);
@@ -447,7 +436,7 @@
     left: 50%;
     bottom: 30px;
     transform: translateX(-50%);
-    pointer-events: all;
+    pointer-events: var(--ui-pointerEvents);
     font-family: inherit;
 
     &:hover {

+ 9 - 8
src/element/linearElementEditor.ts

@@ -42,7 +42,7 @@ import {
 } from "./binding";
 import { tupleToCoors } from "../utils";
 import { isBindingElement } from "./typeChecks";
-import { shouldRotateWithDiscreteAngle } from "../keys";
+import { KEYS, shouldRotateWithDiscreteAngle } from "../keys";
 import { getBoundTextElement, handleBindTextResize } from "./textElement";
 import { DRAGGING_THRESHOLD } from "../constants";
 import { Mutable } from "../utility-types";
@@ -221,7 +221,7 @@ export class LinearElementEditor {
           element,
           referencePoint,
           [scenePointerX, scenePointerY],
-          appState.gridSize,
+          event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
         );
 
         LinearElementEditor.movePoints(element, [
@@ -238,7 +238,7 @@ export class LinearElementEditor {
           element,
           scenePointerX - linearElementEditor.pointerOffset.x,
           scenePointerY - linearElementEditor.pointerOffset.y,
-          appState.gridSize,
+          event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
         );
 
         const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
@@ -254,7 +254,7 @@ export class LinearElementEditor {
                     element,
                     scenePointerX - linearElementEditor.pointerOffset.x,
                     scenePointerY - linearElementEditor.pointerOffset.y,
-                    appState.gridSize,
+                    event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
                   )
                 : ([
                     element.points[pointIndex][0] + deltaX,
@@ -647,7 +647,7 @@ export class LinearElementEditor {
               element,
               scenePointer.x,
               scenePointer.y,
-              appState.gridSize,
+              event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
             ),
           ],
         });
@@ -798,7 +798,7 @@ export class LinearElementEditor {
         element,
         lastCommittedPoint,
         [scenePointerX, scenePointerY],
-        appState.gridSize,
+        event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
       );
 
       newPoint = [
@@ -810,7 +810,7 @@ export class LinearElementEditor {
         element,
         scenePointerX - appState.editingLinearElement.pointerOffset.x,
         scenePointerY - appState.editingLinearElement.pointerOffset.y,
-        appState.gridSize,
+        event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
       );
     }
 
@@ -1176,6 +1176,7 @@ export class LinearElementEditor {
     linearElementEditor: LinearElementEditor,
     pointerCoords: PointerCoords,
     appState: AppState,
+    snapToGrid: boolean,
   ) {
     const element = LinearElementEditor.getElement(
       linearElementEditor.elementId,
@@ -1196,7 +1197,7 @@ export class LinearElementEditor {
       element,
       pointerCoords.x,
       pointerCoords.y,
-      appState.gridSize,
+      snapToGrid ? appState.gridSize : null,
     );
     const points = [
       ...element.points.slice(0, segmentMidpoint.index!),

+ 0 - 1
src/element/newElement.test.ts

@@ -203,7 +203,6 @@ describe("duplicating multiple elements", () => {
     );
 
     clonedArrows.forEach((arrow) => {
-      // console.log(arrow);
       expect(
         clonedRectangle.boundElements!.find((e) => e.id === arrow.id),
       ).toEqual(

+ 3 - 3
src/element/subtypes/mathjax/tests/implementation.test.tsx

@@ -1,13 +1,13 @@
 import { render } from "../../../../tests/test-utils";
 import { API } from "../../../../tests/helpers/api";
-import ExcalidrawApp from "../../../../excalidraw-app";
+import { Excalidraw } from "../../../../packages/excalidraw/index";
 
 import { measureTextElement } from "../../../textElement";
 import { ensureSubtypesLoaded } from "../../";
 
 describe("mathjax", () => {
   it("text-only measurements match", async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw />);
     await ensureSubtypesLoaded(["math"]);
     const text = "A quick brown fox jumps over the lazy dog.";
     const elements = [
@@ -19,7 +19,7 @@ describe("mathjax", () => {
     expect(metrics1).toStrictEqual(metrics2);
   });
   it("minimum height remains", async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw />);
     await ensureSubtypesLoaded(["math"]);
     const elements = [
       API.createElement({ type: "text", id: "A", text: "a" }),

+ 5 - 5
src/element/textWysiwyg.test.tsx

@@ -1,5 +1,5 @@
 import ReactDOM from "react-dom";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import { GlobalTestState, render, screen } from "../tests/test-utils";
 import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
 import { CODES, KEYS } from "../keys";
@@ -41,7 +41,7 @@ describe("textWysiwyg", () => {
   describe("start text editing", () => {
     const { h } = window;
     beforeEach(async () => {
-      await render(<ExcalidrawApp />);
+      await render(<Excalidraw handleKeyboardGlobally={true} />);
       h.elements = [];
     });
 
@@ -243,7 +243,7 @@ describe("textWysiwyg", () => {
     });
 
     beforeEach(async () => {
-      await render(<ExcalidrawApp />);
+      await render(<Excalidraw handleKeyboardGlobally={true} />);
       //@ts-ignore
       h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!);
 
@@ -477,7 +477,7 @@ describe("textWysiwyg", () => {
     const { h } = window;
 
     beforeEach(async () => {
-      await render(<ExcalidrawApp />);
+      await render(<Excalidraw handleKeyboardGlobally={true} />);
       h.elements = [];
 
       rectangle = UI.createElement("rectangle", {
@@ -1511,7 +1511,7 @@ describe("textWysiwyg", () => {
   });
 
   it("should bump the version of labelled arrow when label updated", async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw handleKeyboardGlobally={true} />);
     const arrow = UI.createElement("arrow", {
       width: 300,
       height: 0,

+ 7 - 0
src/hooks/useStable.ts

@@ -0,0 +1,7 @@
+import { useRef } from "react";
+
+export const useStable = <T extends Record<string, any>>(value: T) => {
+  const ref = useRef<T>(value);
+  Object.assign(ref.current, value);
+  return ref.current;
+};

+ 2 - 2
src/index.tsx

@@ -1,9 +1,9 @@
 import { StrictMode } from "react";
 import { createRoot } from "react-dom/client";
-import ExcalidrawApp from "./excalidraw-app";
+import ExcalidrawApp from "../excalidraw-app";
 import { registerSW } from "virtual:pwa-register";
 
-import "./excalidraw-app/sentry";
+import "../excalidraw-app/sentry";
 window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA;
 const rootElement = document.getElementById("root")!;
 const root = createRoot(rootElement);

+ 2 - 1
src/locales/en.json

@@ -264,7 +264,8 @@
     "bindTextToElement": "Press enter to add text",
     "deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging",
     "eraserRevert": "Hold Alt to revert the elements marked for deletion",
-    "firefox_clipboard_write": "This feature can likely be enabled by setting the \"dom.events.asyncClipboard.clipboardItem\" flag to \"true\". To change the browser flags in Firefox, visit the \"about:config\" page."
+    "firefox_clipboard_write": "This feature can likely be enabled by setting the \"dom.events.asyncClipboard.clipboardItem\" flag to \"true\". To change the browser flags in Firefox, visit the \"about:config\" page.",
+    "disableSnapping": "Hold CtrlOrCmd to disable snapping"
   },
   "canvasError": {
     "cannotShowPreview": "Cannot show preview",

+ 232 - 21
src/packages/excalidraw/CHANGELOG.md

@@ -11,29 +11,11 @@ The change should be grouped under one of the below section and must contain PR
 Please add the latest change on the top under the correct section.
 -->
 
-## Unreleased
-
-### renderEmbeddable
-
-```tsx
-(element: NonDeletedExcalidrawElement, radius: number, appState: UIAppState) => JSX.Element | null;`
-```
-
-The renderEmbeddable function allows you to customize the rendering of a JSX component instead of using the default `<iframe>`. By setting props.renderEmbeddable, you can provide a custom implementation for rendering the element.
-
-#### Parameters:
-
-- element (NonDeletedExcalidrawElement): The element to be rendered.
-- radius (number): The calculated border radius in pixels.
-- appState (UIAppState): The current state of the UI.
-
-#### Return value:
-
-JSX.Element | null: The JSX component representing the custom rendering, or null if the default `<iframe>` should be rendered.
-
-### Features
+## 0.16.0 (2023-09-19)
 
 - Add a `subtype` attribute to `ExcalidrawElement` to allow self-contained extensions of any `ExcalidrawElement` type. Implement MathJax support on stem.excalidraw.com as a `math` subtype of `ExcalidrawTextElement`. Both standard Latex input and simplified AsciiMath input are supported. [#6037](https://github.com/excalidraw/excalidraw/pull/6037).
+- Support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically [#6546](https://github.com/excalidraw/excalidraw/pull/6546)
+- Introducing Web-Embeds (alias iframe element)[#6691](https://github.com/excalidraw/excalidraw/pull/6691)
 - Added [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateEmbeddable) to customize embeddable src url validation. [#6691](https://github.com/excalidraw/excalidraw/pull/6691)
 - Add support for `opts.fitToViewport` and `opts.viewportZoomFactor` in the [`ExcalidrawAPI.scrollToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/ref#scrolltocontent) API. [#6581](https://github.com/excalidraw/excalidraw/pull/6581).
 - Properly sanitize element `link` urls. [#6728](https://github.com/excalidraw/excalidraw/pull/6728).
@@ -49,6 +31,235 @@ JSX.Element | null: The JSX component representing the custom rendering, or null
   - `props.onClose` replaced with `props.onStateChange`.
   - `restore()`/`restoreAppState()` now retains `appState.openSidebar` regardless of docked state.
 
+## Excalidraw Library
+
+**_This section lists the updates made to the excalidraw library and will not affect the integration._**
+
+### Features
+
+- allow `avif`, `jfif`, `webp`, `bmp`, `ico` image types [#6500](https://github.com/excalidraw/excalidraw/pull/6500)
+- Zen-mode/go-to-plus button style tweaks [#7006](https://github.com/excalidraw/excalidraw/pull/7006)
+
+- Holding down CMD/CTRL will disable snap to grid when grid is active [#6983](https://github.com/excalidraw/excalidraw/pull/6983)
+
+- Update logo [#6979](https://github.com/excalidraw/excalidraw/pull/6979)
+
+- Export `changeProperty()` and `getFormValue()`. [#6957](https://github.com/excalidraw/excalidraw/pull/6957)
+
+- Partition main canvas vertically [#6759](https://github.com/excalidraw/excalidraw/pull/6759)
+
+- Support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically [#6546](https://github.com/excalidraw/excalidraw/pull/6546)
+
+- Add support for simplePDF in Web-Embeds [#6810](https://github.com/excalidraw/excalidraw/pull/6810)
+
+- Add support for val.town embeds [#6821](https://github.com/excalidraw/excalidraw/pull/6821)
+
+- Render bold lines in grid [#6779](https://github.com/excalidraw/excalidraw/pull/6779)
+
+- Adds support for stackblitz.com embeds [#6813](https://github.com/excalidraw/excalidraw/pull/6813)
+
+- Cache most of element selection [#6747](https://github.com/excalidraw/excalidraw/pull/6747)
+
+- Support customizing what parts of frames are rendered [#6752](https://github.com/excalidraw/excalidraw/pull/6752)
+
+- Make `appState.selectedElementIds` more stable [#6745](https://github.com/excalidraw/excalidraw/pull/6745)
+
+- Overwrite confirmation dialogs [#6658](https://github.com/excalidraw/excalidraw/pull/6658)
+
+- Simple analitycs [#6683](https://github.com/excalidraw/excalidraw/pull/6683)
+
+- Introduce frames [#6123](https://github.com/excalidraw/excalidraw/pull/6123)
+
+- Add canvas-roundrect-polyfill package [#6675](https://github.com/excalidraw/excalidraw/pull/6675)
+
+- Polyfill `CanvasRenderingContext2D.roundRect` [#6673](https://github.com/excalidraw/excalidraw/pull/6673)
+
+- Disable collab feature when running in iframe [#6646](https://github.com/excalidraw/excalidraw/pull/6646)
+
+- Assign random user name when not set [#6663](https://github.com/excalidraw/excalidraw/pull/6663)
+
+- Redesigned collab cursors [#6659](https://github.com/excalidraw/excalidraw/pull/6659)
+
+- Eye dropper [#6615](https://github.com/excalidraw/excalidraw/pull/6615)
+
+- Redesign of Live Collaboration dialog [#6635](https://github.com/excalidraw/excalidraw/pull/6635)
+
+- Recover scrolled position after Library re-opening [#6624](https://github.com/excalidraw/excalidraw/pull/6624)
+
+- Clearing library cache [#6621](https://github.com/excalidraw/excalidraw/pull/6621)
+
+- Update design of ImageExportDialog [#6614](https://github.com/excalidraw/excalidraw/pull/6614)
+
+- Add flipping for multiple elements [#5578](https://github.com/excalidraw/excalidraw/pull/5578)
+
+- Color picker redesign [#6216](https://github.com/excalidraw/excalidraw/pull/6216)
+
+- Add "unlock all elements" to canvas contextMenu [#5894](https://github.com/excalidraw/excalidraw/pull/5894)
+
+- Library sidebar design tweaks [#6582](https://github.com/excalidraw/excalidraw/pull/6582)
+
+- Add Trans component for interpolating JSX in translations [#6534](https://github.com/excalidraw/excalidraw/pull/6534)
+
+- Testing simple analytics and fathom analytics for better privacy of the users [#6529](https://github.com/excalidraw/excalidraw/pull/6529)
+
+- Retain `seed` on shift-paste [#6509](https://github.com/excalidraw/excalidraw/pull/6509)
+
+- Allow `avif`, `jfif`, `webp`, `bmp`, `ico` image types (#6500
+
+### Fixes
+
+- Improperly disabling UI pointer-events on canvas interaction [#7005](https://github.com/excalidraw/excalidraw/pull/7005)
+
+- Several eyeDropper fixes [#7002](https://github.com/excalidraw/excalidraw/pull/7002)
+
+- IsBindableElement to affirm frames [#6900](https://github.com/excalidraw/excalidraw/pull/6900)
+
+- Use `device.isMobile` for sidebar trigger label breakpoint [#6994](https://github.com/excalidraw/excalidraw/pull/6994)
+
+- Export to plus url [#6980](https://github.com/excalidraw/excalidraw/pull/6980)
+
+- Z-index inconsistencies during addition / deletion in frames [#6914](https://github.com/excalidraw/excalidraw/pull/6914)
+
+- Update size-limit so react is not installed as dependency [#6964](https://github.com/excalidraw/excalidraw/pull/6964)
+
+- Stale labeled arrow bounds cache after editing the label [#6893](https://github.com/excalidraw/excalidraw/pull/6893)
+
+- Canvas flickering due to resetting canvas on skipped frames [#6960](https://github.com/excalidraw/excalidraw/pull/6960)
+
+- Grid jittery after partition PR [#6935](https://github.com/excalidraw/excalidraw/pull/6935)
+
+- Regression in indexing when adding elements to frame [#6904](https://github.com/excalidraw/excalidraw/pull/6904)
+
+- Stabilize `selectedElementIds` when box selecting [#6912](https://github.com/excalidraw/excalidraw/pull/6912)
+
+- Resetting deleted elements on duplication [#6906](https://github.com/excalidraw/excalidraw/pull/6906)
+
+- Make canvas compos memoize appState on props they declare [#6897](https://github.com/excalidraw/excalidraw/pull/6897)
+
+- Scope `--color-selection` retrieval to given instance [#6886](https://github.com/excalidraw/excalidraw/pull/6886)
+
+- Webpack config exclude statement to system agnostic [#6857](https://github.com/excalidraw/excalidraw/pull/6857)
+
+- Remove `embeddable` from generic elements [#6853](https://github.com/excalidraw/excalidraw/pull/6853)
+
+- Resizing arrow labels [#6789](https://github.com/excalidraw/excalidraw/pull/6789)
+
+- Eye-dropper not working with app offset correctly on non-1 dPR [#6835](https://github.com/excalidraw/excalidraw/pull/6835)
+
+- Add self destroying service-worker.js to migrate everyone from CRA to Vite [#6833](https://github.com/excalidraw/excalidraw/pull/6833)
+
+- Forgotten REACT_APP env variables [#6834](https://github.com/excalidraw/excalidraw/pull/6834)
+
+- Refresh sw when browser refreshed [#6824](https://github.com/excalidraw/excalidraw/pull/6824)
+
+- Adding to selection via shift box-select [#6815](https://github.com/excalidraw/excalidraw/pull/6815)
+
+- Prevent binding focus NaN value [#6803](https://github.com/excalidraw/excalidraw/pull/6803)
+
+- Use pull request in semantic workflow for better security [#6799](https://github.com/excalidraw/excalidraw/pull/6799)
+
+- Don't show `canvasBackground` label when `UIOptions.canvasActions.changeViewBackgroundColor` is false [#6781](https://github.com/excalidraw/excalidraw/pull/6781)
+
+- Use subdirectory for @excalidraw/excalidraw size limit [#6787](https://github.com/excalidraw/excalidraw/pull/6787)
+
+- Use actual dock state to not close docked library on insert [#6766](https://github.com/excalidraw/excalidraw/pull/6766)
+
+- UI disappears when pressing the eyedropper shortcut on mobile [#6725](https://github.com/excalidraw/excalidraw/pull/6725)
+
+- Elements in non-existing frame getting removed [#6708](https://github.com/excalidraw/excalidraw/pull/6708)
+
+- Scrollbars renders but disable [#6706](https://github.com/excalidraw/excalidraw/pull/6706)
+
+- Typo in chart.ts [#6696](https://github.com/excalidraw/excalidraw/pull/6696)
+
+- Do not bind text to container using text tool when it has text already [#6694](https://github.com/excalidraw/excalidraw/pull/6694)
+
+- Don't allow binding text to images [#6693](https://github.com/excalidraw/excalidraw/pull/6693)
+
+- Updated link for documentation page under help section [#6654](https://github.com/excalidraw/excalidraw/pull/6654)
+
+- Collab username style fixes [#6668](https://github.com/excalidraw/excalidraw/pull/6668)
+
+- Bound arrows not updated when rotating multiple elements [#6662](https://github.com/excalidraw/excalidraw/pull/6662)
+
+- Delete setCursor when resize [#6660](https://github.com/excalidraw/excalidraw/pull/6660)
+
+- Creating text while color picker open [#6651](https://github.com/excalidraw/excalidraw/pull/6651)
+
+- Cleanup textWysiwyg and getAdjustedDimensions [#6520](https://github.com/excalidraw/excalidraw/pull/6520)
+
+- Eye dropper not accounting for offsets [#6640](https://github.com/excalidraw/excalidraw/pull/6640)
+
+- Color picker input closing problem [#6599](https://github.com/excalidraw/excalidraw/pull/6599)
+
+- Export dialog shortcut toggles console on firefox [#6620](https://github.com/excalidraw/excalidraw/pull/6620)
+
+- Add react v17 `useTransition` polyfill [#6618](https://github.com/excalidraw/excalidraw/pull/6618)
+
+- Library dropdown visibility issue for mobile [#6613](https://github.com/excalidraw/excalidraw/pull/6613)
+
+- `withInternalFallback` leaking state in multi-instance scenarios [#6602](https://github.com/excalidraw/excalidraw/pull/6602)
+
+- Language list containing duplicate `en` lang [#6583](https://github.com/excalidraw/excalidraw/pull/6583)
+
+- Garbled text displayed on avatars [#6575](https://github.com/excalidraw/excalidraw/pull/6575)
+
+- Assign the original text to text editor only during init [#6580](https://github.com/excalidraw/excalidraw/pull/6580)
+
+- I18n: Apply Trans component to publish library dialogue [#6564](https://github.com/excalidraw/excalidraw/pull/6564)
+
+- Fix brave error i18n string and remove unused [#6561](https://github.com/excalidraw/excalidraw/pull/6561)
+
+- Revert add version tags to Docker build [#6540](https://github.com/excalidraw/excalidraw/pull/6540)
+
+- Don't refresh dimensions for text containers on font load [#6523](https://github.com/excalidraw/excalidraw/pull/6523)
+
+- Cleanup getMaxContainerHeight and getMaxContainerWidth [#6519](https://github.com/excalidraw/excalidraw/pull/6519)
+
+- Cleanup redrawTextBoundingBox [#6518](https://github.com/excalidraw/excalidraw/pull/6518)
+
+- Text jumps when editing on Android Chrome [#6503](https://github.com/excalidraw/excalidraw/pull/6503)
+
+### Styles
+
+- Removes extra spaces [#6558](https://github.com/excalidraw/excalidraw/pull/6558)
+
+- Fix font family inconsistencies [#6501](https://github.com/excalidraw/excalidraw/pull/6501)
+
+### Refactor
+
+- Factor out shape generation from `renderElement.ts` pt 2 [#6878](https://github.com/excalidraw/excalidraw/pull/6878)
+
+- Add typeScript support to enforce valid translation keys [#6776](https://github.com/excalidraw/excalidraw/pull/6776)
+
+- Simplify `ImageExportDialog` [#6578](https://github.com/excalidraw/excalidraw/pull/6578)
+
+### Performance
+
+- Limiting the suggested binding to fix performance issue [#6877](https://github.com/excalidraw/excalidraw/pull/6877)
+
+- Memoize rendering of library [#6622](https://github.com/excalidraw/excalidraw/pull/6622)
+
+- Improve rendering performance for Library [#6587](https://github.com/excalidraw/excalidraw/pull/6587)
+
+- Use `UIAppState` where possible to reduce UI rerenders [#6560](https://github.com/excalidraw/excalidraw/pull/6560)
+
+### Build
+
+- Increase limit for bundle by 1kb [#6880](https://github.com/excalidraw/excalidraw/pull/6880)
+
+- Update to node 18 in docker [#6822](https://github.com/excalidraw/excalidraw/pull/6822)
+
+- Migrate to Vite 🚀 [#6818](https://github.com/excalidraw/excalidraw/pull/6818)
+
+- Migrate to Vite 🚀 [#6713](https://github.com/excalidraw/excalidraw/pull/6713)
+
+- Increase limit to 290 kB for prod bundle [#6809](https://github.com/excalidraw/excalidraw/pull/6809)
+
+- Add version tags to Docker build [#6508](https://github.com/excalidraw/excalidraw/pull/6508)
+
+---
+
 ## 0.15.2 (2023-04-20)
 
 ### Docs

+ 1 - 1
src/packages/excalidraw/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@excalidraw/excalidraw",
-  "version": "0.15.2",
+  "version": "0.16.0",
   "main": "main.js",
   "types": "types/packages/excalidraw/index.d.ts",
   "files": [

+ 2 - 2
src/components/App.test.tsx → src/tests/App.test.tsx

@@ -3,7 +3,7 @@ import * as Renderer from "../renderer/renderScene";
 import { reseed } from "../random";
 import { render, queryByTestId } from "../tests/test-utils";
 
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import { vi } from "vitest";
 
 const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
@@ -35,7 +35,7 @@ describe("Test <App/>", () => {
       };
     };
 
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw />);
     expect(
       queryByTestId(
         document.querySelector(".excalidraw-modal-container")!,

+ 50 - 0
src/tests/__snapshots__/App.test.tsx.snap

@@ -0,0 +1,50 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Test <App/> > should show error modal when using brave and measureText API is not working 1`] = `
+<div
+  data-testid="brave-measure-text-error"
+>
+  <p>
+    Looks like you are using Brave browser with the 
+    <span
+      style="font-weight: 600;"
+    >
+      Aggressively Block Fingerprinting
+    </span>
+     setting enabled.
+  </p>
+  <p>
+    This could result in breaking the 
+    <span
+      style="font-weight: 600;"
+    >
+      Text Elements
+    </span>
+     in your drawings.
+  </p>
+  <p>
+    We strongly recommend disabling this setting. You can follow 
+    <a
+      href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser"
+    >
+      these steps
+    </a>
+     on how to do so.
+  </p>
+  <p>
+    If disabling this setting doesn't fix the display of text elements, please open an 
+    <a
+      href="https://github.com/excalidraw/excalidraw/issues/new"
+    >
+      issue
+    </a>
+     on our GitHub, or write us on 
+    <a
+      href="https://discord.gg/UexuTaE"
+    >
+      Discord
+      .
+    </a>
+  </p>
+</div>
+`;

+ 0 - 120
src/tests/__snapshots__/regressionTests.test.tsx.snap

@@ -13089,126 +13089,6 @@ exports[`regression tests > pinch-to-zoom works > [end of test] number of elemen
 
 exports[`regression tests > pinch-to-zoom works > [end of test] number of renders 1`] = `7`;
 
-exports[`regression tests > rerenders UI on language change > [end of test] appState 1`] = `
-{
-  "activeEmbeddable": null,
-  "activeTool": {
-    "customType": null,
-    "lastActiveTool": null,
-    "locked": false,
-    "type": "rectangle",
-  },
-  "collaborators": Map {},
-  "contextMenu": null,
-  "currentChartType": "bar",
-  "currentItemBackgroundColor": "transparent",
-  "currentItemEndArrowhead": "arrow",
-  "currentItemFillStyle": "hachure",
-  "currentItemFontFamily": 1,
-  "currentItemFontSize": 20,
-  "currentItemOpacity": 100,
-  "currentItemRoughness": 1,
-  "currentItemRoundness": "round",
-  "currentItemStartArrowhead": null,
-  "currentItemStrokeColor": "#1e1e1e",
-  "currentItemStrokeStyle": "solid",
-  "currentItemStrokeWidth": 1,
-  "currentItemTextAlign": "left",
-  "cursorButton": "up",
-  "defaultSidebarDockedPreference": false,
-  "draggingElement": null,
-  "editingElement": null,
-  "editingFrame": null,
-  "editingGroupId": null,
-  "editingLinearElement": null,
-  "elementsToHighlight": null,
-  "errorMessage": null,
-  "exportBackground": true,
-  "exportEmbedScene": false,
-  "exportScale": 1,
-  "exportWithDarkMode": false,
-  "fileHandle": null,
-  "frameRendering": {
-    "clip": true,
-    "enabled": true,
-    "name": true,
-    "outline": true,
-  },
-  "frameToHighlight": null,
-  "gridSize": null,
-  "height": 768,
-  "isBindingEnabled": true,
-  "isLoading": false,
-  "isResizing": false,
-  "isRotating": false,
-  "lastPointerDownWith": "mouse",
-  "multiElement": null,
-  "name": "Untitled-201933152653",
-  "offsetLeft": 0,
-  "offsetTop": 0,
-  "openDialog": null,
-  "openMenu": "canvas",
-  "openPopup": null,
-  "openSidebar": null,
-  "pasteDialog": {
-    "data": null,
-    "shown": false,
-  },
-  "penDetected": false,
-  "penMode": false,
-  "pendingImageElementId": null,
-  "previousSelectedElementIds": {},
-  "resizingElement": null,
-  "scrollX": 0,
-  "scrollY": 0,
-  "scrolledOutside": false,
-  "selectedElementIds": {},
-  "selectedElementsAreBeingDragged": false,
-  "selectedGroupIds": {},
-  "selectedLinearElement": null,
-  "selectionElement": null,
-  "shouldCacheIgnoreZoom": false,
-  "showHyperlinkPopup": false,
-  "showStats": false,
-  "showWelcomeScreen": true,
-  "startBoundElement": null,
-  "suggestedBindings": [],
-  "theme": "light",
-  "toast": null,
-  "viewBackgroundColor": "#ffffff",
-  "viewModeEnabled": false,
-  "width": 1024,
-  "zenModeEnabled": false,
-  "zoom": {
-    "value": 1,
-  },
-}
-`;
-
-exports[`regression tests > rerenders UI on language change > [end of test] history 1`] = `
-{
-  "recording": false,
-  "redoStack": [],
-  "stateHistory": [
-    {
-      "appState": {
-        "editingGroupId": null,
-        "editingLinearElement": null,
-        "name": "Untitled-201933152653",
-        "selectedElementIds": {},
-        "selectedGroupIds": {},
-        "viewBackgroundColor": "#ffffff",
-      },
-      "elements": [],
-    },
-  ],
-}
-`;
-
-exports[`regression tests > rerenders UI on language change > [end of test] number of elements 1`] = `0`;
-
-exports[`regression tests > rerenders UI on language change > [end of test] number of renders 1`] = `5`;
-
 exports[`regression tests > shift click on selected element should deselect it on pointer up > [end of test] appState 1`] = `
 {
   "activeEmbeddable": null,

+ 3 - 3
src/actions/actionStyles.test.tsx → src/tests/actionStyles.test.tsx

@@ -1,4 +1,4 @@
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import { CODES } from "../keys";
 import { API } from "../tests/helpers/api";
 import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
@@ -9,7 +9,7 @@ import {
   screen,
   togglePopover,
 } from "../tests/test-utils";
-import { copiedStyles } from "./actionStyles";
+import { copiedStyles } from "../actions/actionStyles";
 
 const { h } = window;
 
@@ -17,7 +17,7 @@ const mouse = new Pointer("mouse");
 
 describe("actionStyles", () => {
   beforeEach(async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw handleKeyboardGlobally={true} />);
   });
 
   afterEach(async () => {

+ 2 - 2
src/tests/align.test.tsx

@@ -1,6 +1,6 @@
 import ReactDOM from "react-dom";
 import { render } from "./test-utils";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../../src/packages/excalidraw/index";
 import { defaultLang, setLanguage } from "../i18n";
 import { UI, Pointer, Keyboard } from "./helpers/ui";
 import { API } from "./helpers/api";
@@ -60,7 +60,7 @@ describe("aligning", () => {
     mouse.reset();
 
     await setLanguage(defaultLang);
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw handleKeyboardGlobally={true} />);
   });
 
   it("aligns two objects correctly to the top", () => {

+ 21 - 16
src/tests/appState.test.tsx

@@ -1,6 +1,6 @@
 import { queryByTestId, render, waitFor } from "./test-utils";
 
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import { API } from "./helpers/api";
 import { getDefaultAppState } from "../appState";
 import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
@@ -14,14 +14,17 @@ describe("appState", () => {
     const defaultAppState = getDefaultAppState();
     const exportBackground = !defaultAppState.exportBackground;
 
-    await render(<ExcalidrawApp />, {
-      localStorageData: {
-        appState: {
-          exportBackground,
-          viewBackgroundColor: "#F00",
-        },
-      },
-    });
+    await render(
+      <Excalidraw
+        initialData={{
+          appState: {
+            exportBackground,
+            viewBackgroundColor: "#F00",
+          },
+        }}
+      />,
+      {},
+    );
 
     await waitFor(() => {
       expect(h.state.exportBackground).toBe(exportBackground);
@@ -53,13 +56,15 @@ describe("appState", () => {
   });
 
   it("changing fontSize with text tool selected (no element created yet)", async () => {
-    const { container } = await render(<ExcalidrawApp />, {
-      localStorageData: {
-        appState: {
-          currentItemFontSize: 30,
-        },
-      },
-    });
+    const { container } = await render(
+      <Excalidraw
+        initialData={{
+          appState: {
+            currentItemFontSize: 30,
+          },
+        }}
+      />,
+    );
 
     UI.clickTool("text");
 

+ 2 - 2
src/tests/binding.test.tsx

@@ -1,5 +1,5 @@
 import { fireEvent, render } from "./test-utils";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../../src/packages/excalidraw/index";
 import { UI, Pointer, Keyboard } from "./helpers/ui";
 import { getTransformHandles } from "../element/transformHandles";
 import { API } from "./helpers/api";
@@ -12,7 +12,7 @@ const mouse = new Pointer("mouse");
 
 describe("element binding", () => {
   beforeEach(async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw handleKeyboardGlobally={true} />);
   });
 
   it("should create valid binding if duplicate start/end points", async () => {

+ 8 - 4
src/tests/clipboard.test.tsx

@@ -7,7 +7,7 @@ import {
   createPasteEvent,
 } from "./test-utils";
 import { Pointer, Keyboard } from "./helpers/ui";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import { KEYS } from "../keys";
 import {
   getDefaultLineHeight,
@@ -79,8 +79,13 @@ beforeEach(async () => {
 
   mouse.reset();
 
-  await render(<ExcalidrawApp />);
-  h.app.setAppState({ zoom: { value: 1 as NormalizedZoomValue } });
+  await render(
+    <Excalidraw
+      autoFocus={true}
+      handleKeyboardGlobally={true}
+      initialData={{ appState: { zoom: { value: 1 as NormalizedZoomValue } } }}
+    />,
+  );
   setClipboardText("");
   Object.assign(document, {
     elementFromPoint: () => GlobalTestState.canvas,
@@ -91,7 +96,6 @@ describe("general paste behavior", () => {
   it("should randomize seed on paste", async () => {
     const rectangle = API.createElement({ type: "rectangle" });
     const clipboardJSON = (await copyToClipboard([rectangle], null))!;
-
     pasteWithCtrlCmdV(clipboardJSON);
 
     await waitFor(() => {

+ 5 - 8
src/tests/contextmenu.test.tsx

@@ -11,7 +11,7 @@ import {
   waitFor,
   togglePopover,
 } from "./test-utils";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import * as Renderer from "../renderer/renderScene";
 import { reseed } from "../random";
 import { UI, Pointer, Keyboard } from "./helpers/ui";
@@ -20,7 +20,6 @@ import { ShortcutName } from "../actions/shortcuts";
 import { copiedStyles } from "../actions/actionStyles";
 import { API } from "./helpers/api";
 import { setDateTimeForTests } from "../utils";
-import { LibraryItem } from "../types";
 import { vi } from "vitest";
 
 const checkpoint = (name: string) => {
@@ -56,7 +55,7 @@ describe("contextMenu element", () => {
     reseed(7);
     setDateTimeForTests("201933152653");
 
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw handleKeyboardGlobally={true} />);
   });
 
   beforeAll(() => {
@@ -394,11 +393,9 @@ describe("contextMenu element", () => {
     const contextMenu = UI.queryContextMenu();
     fireEvent.click(queryByText(contextMenu!, "Add to library")!);
 
-    await waitFor(() => {
-      const library = localStorage.getItem("excalidraw-library");
-      expect(library).not.toBeNull();
-      const addedElement = JSON.parse(library!)[0] as LibraryItem;
-      expect(addedElement.elements[0]).toEqual(h.elements[0]);
+    await waitFor(async () => {
+      const libraryItems = await h.app.library.getLatestLibrary();
+      expect(libraryItems[0].elements[0]).toEqual(h.elements[0]);
     });
   });
 

+ 2 - 2
src/tests/customActions.test.tsx

@@ -2,7 +2,7 @@ import { ExcalidrawElement } from "../element/types";
 import { getShortcutKey } from "../utils";
 import { API } from "./helpers/api";
 import { render } from "./test-utils";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import {
   CustomShortcutName,
   getShortcutFromShortcutName,
@@ -27,7 +27,7 @@ describe("regression tests", () => {
   });
 
   it("should apply universal action predicates", async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw />);
     // Create the test elements
     const el1 = API.createElement({ type: "rectangle", id: "A", y: 0 });
     const el2 = API.createElement({ type: "rectangle", id: "B", y: 30 });

+ 15 - 11
src/tests/dragCreate.test.tsx

@@ -1,5 +1,5 @@
 import ReactDOM from "react-dom";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import * as Renderer from "../renderer/renderScene";
 import { KEYS } from "../keys";
 import {
@@ -30,7 +30,7 @@ const { h } = window;
 describe("Test dragCreate", () => {
   describe("add element to the scene when pointer dragging long enough", () => {
     it("rectangle", async () => {
-      const { getByToolName, container } = await render(<ExcalidrawApp />);
+      const { getByToolName, container } = await render(<Excalidraw />);
       // select tool
       const tool = getByToolName("rectangle");
       fireEvent.click(tool);
@@ -62,7 +62,7 @@ describe("Test dragCreate", () => {
     });
 
     it("ellipse", async () => {
-      const { getByToolName, container } = await render(<ExcalidrawApp />);
+      const { getByToolName, container } = await render(<Excalidraw />);
       // select tool
       const tool = getByToolName("ellipse");
       fireEvent.click(tool);
@@ -95,7 +95,7 @@ describe("Test dragCreate", () => {
     });
 
     it("diamond", async () => {
-      const { getByToolName, container } = await render(<ExcalidrawApp />);
+      const { getByToolName, container } = await render(<Excalidraw />);
       // select tool
       const tool = getByToolName("diamond");
       fireEvent.click(tool);
@@ -127,7 +127,7 @@ describe("Test dragCreate", () => {
     });
 
     it("arrow", async () => {
-      const { getByToolName, container } = await render(<ExcalidrawApp />);
+      const { getByToolName, container } = await render(<Excalidraw />);
       // select tool
       const tool = getByToolName("arrow");
       fireEvent.click(tool);
@@ -163,7 +163,7 @@ describe("Test dragCreate", () => {
     });
 
     it("line", async () => {
-      const { getByToolName, container } = await render(<ExcalidrawApp />);
+      const { getByToolName, container } = await render(<Excalidraw />);
       // select tool
       const tool = getByToolName("line");
       fireEvent.click(tool);
@@ -207,7 +207,7 @@ describe("Test dragCreate", () => {
     });
 
     it("rectangle", async () => {
-      const { getByToolName, container } = await render(<ExcalidrawApp />);
+      const { getByToolName, container } = await render(<Excalidraw />);
       // select tool
       const tool = getByToolName("rectangle");
       fireEvent.click(tool);
@@ -227,7 +227,7 @@ describe("Test dragCreate", () => {
     });
 
     it("ellipse", async () => {
-      const { getByToolName, container } = await render(<ExcalidrawApp />);
+      const { getByToolName, container } = await render(<Excalidraw />);
       // select tool
       const tool = getByToolName("ellipse");
       fireEvent.click(tool);
@@ -247,7 +247,7 @@ describe("Test dragCreate", () => {
     });
 
     it("diamond", async () => {
-      const { getByToolName, container } = await render(<ExcalidrawApp />);
+      const { getByToolName, container } = await render(<Excalidraw />);
       // select tool
       const tool = getByToolName("diamond");
       fireEvent.click(tool);
@@ -267,7 +267,9 @@ describe("Test dragCreate", () => {
     });
 
     it("arrow", async () => {
-      const { getByToolName, container } = await render(<ExcalidrawApp />);
+      const { getByToolName, container } = await render(
+        <Excalidraw handleKeyboardGlobally={true} />,
+      );
       // select tool
       const tool = getByToolName("arrow");
       fireEvent.click(tool);
@@ -292,7 +294,9 @@ describe("Test dragCreate", () => {
     });
 
     it("line", async () => {
-      const { getByToolName, container } = await render(<ExcalidrawApp />);
+      const { getByToolName, container } = await render(
+        <Excalidraw handleKeyboardGlobally={true} />,
+      );
       // select tool
       const tool = getByToolName("line");
       fireEvent.click(tool);

+ 2 - 2
src/tests/elementLocking.test.tsx

@@ -1,5 +1,5 @@
 import ReactDOM from "react-dom";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import { render } from "../tests/test-utils";
 import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
 import { KEYS } from "../keys";
@@ -15,7 +15,7 @@ const h = window.h;
 
 describe("element locking", () => {
   beforeEach(async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw handleKeyboardGlobally={true} />);
     h.elements = [];
   });
 

+ 2 - 2
src/tests/export.test.tsx

@@ -1,5 +1,5 @@
 import { render, waitFor } from "./test-utils";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import { API } from "./helpers/api";
 import {
   encodePngMetadata,
@@ -42,7 +42,7 @@ Object.defineProperty(window, "TextDecoder", {
 
 describe("export", () => {
   beforeEach(async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw />);
   });
 
   it("export embedded png and reimport", async () => {

+ 6 - 6
src/tests/fitToContent.test.tsx

@@ -1,14 +1,14 @@
 import { render } from "./test-utils";
 import { API } from "./helpers/api";
 
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import { vi } from "vitest";
 
 const { h } = window;
 
 describe("fitToContent", () => {
   it("should zoom to fit the selected element", async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw />);
 
     h.state.width = 10;
     h.state.height = 10;
@@ -30,7 +30,7 @@ describe("fitToContent", () => {
   });
 
   it("should zoom to fit multiple elements", async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw />);
 
     const topLeft = API.createElement({
       width: 20,
@@ -61,7 +61,7 @@ describe("fitToContent", () => {
   });
 
   it("should scroll the viewport to the selected element", async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw />);
 
     h.state.width = 10;
     h.state.height = 10;
@@ -106,7 +106,7 @@ describe("fitToContent animated", () => {
   });
 
   it("should ease scroll the viewport to the selected element", async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw />);
 
     h.state.width = 10;
     h.state.height = 10;
@@ -142,7 +142,7 @@ describe("fitToContent animated", () => {
   });
 
   it("should animate the scroll but not the zoom", async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw />);
 
     h.state.width = 50;
     h.state.height = 50;

+ 2 - 2
src/tests/flip.test.tsx

@@ -19,7 +19,7 @@ import {
   FileId,
 } from "../element/types";
 import { newLinearElement } from "../element";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import { mutateElement } from "../element/mutateElement";
 import { NormalizedZoomValue } from "../types";
 import { ROUNDNESS } from "../constants";
@@ -52,7 +52,7 @@ beforeEach(async () => {
   Object.assign(document, {
     elementFromPoint: () => GlobalTestState.canvas,
   });
-  await render(<ExcalidrawApp />);
+  await render(<Excalidraw autoFocus={true} handleKeyboardGlobally={true} />);
   h.setState({
     zoom: {
       value: 1 as NormalizedZoomValue,

+ 22 - 18
src/tests/history.test.tsx

@@ -1,5 +1,5 @@
 import { assertSelectedElements, render } from "./test-utils";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import { Keyboard, Pointer, UI } from "./helpers/ui";
 import { API } from "./helpers/api";
 import { getDefaultAppState } from "../appState";
@@ -13,14 +13,16 @@ const mouse = new Pointer("mouse");
 
 describe("history", () => {
   it("initializing scene should end up with single history entry", async () => {
-    await render(<ExcalidrawApp />, {
-      localStorageData: {
-        elements: [API.createElement({ type: "rectangle", id: "A" })],
-        appState: {
-          zenModeEnabled: true,
-        },
-      },
-    });
+    await render(
+      <Excalidraw
+        initialData={{
+          elements: [API.createElement({ type: "rectangle", id: "A" })],
+          appState: {
+            zenModeEnabled: true,
+          },
+        }}
+      />,
+    );
 
     await waitFor(() => expect(h.state.zenModeEnabled).toBe(true));
     await waitFor(() =>
@@ -60,14 +62,16 @@ describe("history", () => {
   });
 
   it("scene import via drag&drop should create new history entry", async () => {
-    await render(<ExcalidrawApp />, {
-      localStorageData: {
-        elements: [API.createElement({ type: "rectangle", id: "A" })],
-        appState: {
-          viewBackgroundColor: "#FFF",
-        },
-      },
-    });
+    await render(
+      <Excalidraw
+        initialData={{
+          elements: [API.createElement({ type: "rectangle", id: "A" })],
+          appState: {
+            viewBackgroundColor: "#FFF",
+          },
+        }}
+      />,
+    );
 
     await waitFor(() => expect(h.state.viewBackgroundColor).toBe("#FFF"));
     await waitFor(() =>
@@ -113,7 +117,7 @@ describe("history", () => {
   });
 
   it("undo/redo works properly with groups", async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw handleKeyboardGlobally={true} />);
     const rect1 = API.createElement({ type: "rectangle", groupIds: ["A"] });
     const rect2 = API.createElement({ type: "rectangle", groupIds: ["A"] });
 

+ 3 - 3
src/tests/library.test.tsx

@@ -2,7 +2,7 @@ import { vi } from "vitest";
 import { fireEvent, render, waitFor } from "./test-utils";
 import { queryByTestId } from "@testing-library/react";
 
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import { API } from "./helpers/api";
 import { MIME_TYPES } from "../constants";
 import { LibraryItem, LibraryItems } from "../types";
@@ -42,7 +42,7 @@ vi.mock("../data/filesystem.ts", async (importOriginal) => {
 
 describe("library", () => {
   beforeEach(async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw />);
     h.app.library.resetLibrary();
   });
 
@@ -189,7 +189,7 @@ describe("library", () => {
 
 describe("library menu", () => {
   it("should load library from file picker", async () => {
-    const { container } = await render(<ExcalidrawApp />);
+    const { container } = await render(<Excalidraw />);
 
     const latestLibrary = await h.app.library.getLatestLibrary();
     expect(latestLibrary.length).toBe(0);

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

@@ -5,7 +5,7 @@ import {
   ExcalidrawTextElementWithContainer,
   FontString,
 } from "../element/types";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import { centerPoint } from "../math";
 import { reseed } from "../random";
 import * as Renderer from "../renderer/renderScene";
@@ -43,7 +43,7 @@ describe("Test Linear Elements", () => {
     renderInteractiveScene.mockClear();
     renderStaticScene.mockClear();
     reseed(7);
-    const comp = await render(<ExcalidrawApp />);
+    const comp = await render(<Excalidraw handleKeyboardGlobally={true} />);
     h.state.width = 1000;
     h.state.height = 1000;
     container = comp.container;

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

@@ -1,7 +1,7 @@
 import React from "react";
 import ReactDOM from "react-dom";
 import { render, fireEvent } from "./test-utils";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import * as Renderer from "../renderer/renderScene";
 import { reseed } from "../random";
 import { bindOrUnbindLinearElement } from "../element/binding";
@@ -31,7 +31,7 @@ const { h } = window;
 
 describe("move element", () => {
   it("rectangle", async () => {
-    const { getByToolName, container } = await render(<ExcalidrawApp />);
+    const { getByToolName, container } = await render(<Excalidraw />);
     const canvas = container.querySelector("canvas.interactive")!;
 
     {
@@ -67,7 +67,7 @@ describe("move element", () => {
   });
 
   it("rectangles with binding arrow", async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw handleKeyboardGlobally={true} />);
 
     // create elements
     const rectA = UI.createElement("rectangle", { size: 100 });
@@ -119,7 +119,7 @@ describe("move element", () => {
 
 describe("duplicate element on move when ALT is clicked", () => {
   it("rectangle", async () => {
-    const { getByToolName, container } = await render(<ExcalidrawApp />);
+    const { getByToolName, container } = await render(<Excalidraw />);
     const canvas = container.querySelector("canvas.interactive")!;
 
     {

+ 12 - 11
src/tests/multiPointCreate.test.tsx

@@ -5,7 +5,7 @@ import {
   mockBoundingClientRect,
   restoreOriginalGetBoundingClientRect,
 } from "./test-utils";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import * as Renderer from "../renderer/renderScene";
 import { KEYS } from "../keys";
 import { ExcalidrawLinearElement } from "../element/types";
@@ -29,7 +29,7 @@ const { h } = window;
 
 describe("remove shape in non linear elements", () => {
   beforeAll(() => {
-    mockBoundingClientRect();
+    mockBoundingClientRect({ width: 1000, height: 1000 });
   });
 
   afterAll(() => {
@@ -37,12 +37,13 @@ describe("remove shape in non linear elements", () => {
   });
 
   it("rectangle", async () => {
-    const { getByToolName, container } = await render(<ExcalidrawApp />);
+    const { getByToolName, container } = await render(<Excalidraw />);
     // select tool
     const tool = getByToolName("rectangle");
     fireEvent.click(tool);
 
     const canvas = container.querySelector("canvas.interactive")!;
+
     fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
     fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
 
@@ -52,7 +53,7 @@ describe("remove shape in non linear elements", () => {
   });
 
   it("ellipse", async () => {
-    const { getByToolName, container } = await render(<ExcalidrawApp />);
+    const { getByToolName, container } = await render(<Excalidraw />);
     // select tool
     const tool = getByToolName("ellipse");
     fireEvent.click(tool);
@@ -67,7 +68,7 @@ describe("remove shape in non linear elements", () => {
   });
 
   it("diamond", async () => {
-    const { getByToolName, container } = await render(<ExcalidrawApp />);
+    const { getByToolName, container } = await render(<Excalidraw />);
     // select tool
     const tool = getByToolName("diamond");
     fireEvent.click(tool);
@@ -84,7 +85,7 @@ describe("remove shape in non linear elements", () => {
 
 describe("multi point mode in linear elements", () => {
   it("arrow", async () => {
-    const { getByToolName, container } = await render(<ExcalidrawApp />);
+    const { getByToolName, container } = await render(<Excalidraw />);
     // select tool
     const tool = getByToolName("arrow");
     fireEvent.click(tool);
@@ -109,8 +110,8 @@ describe("multi point mode in linear elements", () => {
       key: KEYS.ENTER,
     });
 
-    expect(renderInteractiveScene).toHaveBeenCalledTimes(10);
-    expect(renderStaticScene).toHaveBeenCalledTimes(11);
+    expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
+    expect(renderStaticScene).toHaveBeenCalledTimes(10);
     expect(h.elements.length).toEqual(1);
 
     const element = h.elements[0] as ExcalidrawLinearElement;
@@ -128,7 +129,7 @@ describe("multi point mode in linear elements", () => {
   });
 
   it("line", async () => {
-    const { getByToolName, container } = await render(<ExcalidrawApp />);
+    const { getByToolName, container } = await render(<Excalidraw />);
     // select tool
     const tool = getByToolName("line");
     fireEvent.click(tool);
@@ -153,8 +154,8 @@ describe("multi point mode in linear elements", () => {
       key: KEYS.ENTER,
     });
 
-    expect(renderInteractiveScene).toHaveBeenCalledTimes(10);
-    expect(renderStaticScene).toHaveBeenCalledTimes(11);
+    expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
+    expect(renderStaticScene).toHaveBeenCalledTimes(10);
     expect(h.elements.length).toEqual(1);
 
     const element = h.elements[0] as ExcalidrawLinearElement;

+ 2 - 24
src/tests/regressionTests.test.tsx

@@ -1,7 +1,7 @@
 import ReactDOM from "react-dom";
 import { ExcalidrawElement } from "../element/types";
 import { CODES, KEYS } from "../keys";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import { reseed } from "../random";
 import * as Renderer from "../renderer/renderScene";
 import { setDateTimeForTests } from "../utils";
@@ -13,9 +13,7 @@ import {
   render,
   screen,
   togglePopover,
-  waitFor,
 } from "./test-utils";
-import { defaultLang } from "../i18n";
 import { FONT_FAMILY } from "../constants";
 import { vi } from "vitest";
 
@@ -56,7 +54,7 @@ beforeEach(async () => {
   finger1.reset();
   finger2.reset();
 
-  await render(<ExcalidrawApp />);
+  await render(<Excalidraw handleKeyboardGlobally={true} />);
   h.setState({ height: 768, width: 1024 });
 });
 
@@ -443,26 +441,6 @@ describe("regression tests", () => {
     expect(h.state.zoom.value).toBe(1);
   });
 
-  it("rerenders UI on language change", async () => {
-    // select rectangle tool to show properties menu
-    UI.clickTool("rectangle");
-    // english lang should display `thin` label
-    expect(screen.queryByTitle(/thin/i)).not.toBeNull();
-    fireEvent.click(document.querySelector(".dropdown-menu-button")!);
-
-    fireEvent.change(document.querySelector(".dropdown-select__language")!, {
-      target: { value: "de-DE" },
-    });
-    // switching to german, `thin` label should no longer exist
-    await waitFor(() => expect(screen.queryByTitle(/thin/i)).toBeNull());
-    // reset language
-    fireEvent.change(document.querySelector(".dropdown-select__language")!, {
-      target: { value: defaultLang.code },
-    });
-    // switching back to English
-    await waitFor(() => expect(screen.queryByTitle(/thin/i)).not.toBeNull());
-  });
-
   it("make a group and duplicate it", () => {
     UI.clickTool("rectangle");
     mouse.down(10, 10);

+ 2 - 2
src/tests/resize.test.tsx

@@ -6,7 +6,7 @@ import { reseed } from "../random";
 import { UI, Keyboard } from "./helpers/ui";
 import { resize } from "./utils";
 import { ExcalidrawTextElement } from "../element/types";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import { API } from "./helpers/api";
 import { KEYS } from "../keys";
 import { vi } from "vitest";
@@ -126,7 +126,7 @@ describe("resize rectangle ellipses and diamond elements", () => {
 
 describe("Test text element", () => {
   it("should update font size via keyboard", async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw handleKeyboardGlobally={true} />);
 
     const textElement = API.createElement({
       type: "text",

+ 1 - 2
src/tests/scroll.test.tsx

@@ -8,7 +8,6 @@ import { Excalidraw } from "../packages/excalidraw/index";
 import { API } from "./helpers/api";
 import { Keyboard } from "./helpers/ui";
 import { KEYS } from "../keys";
-import ExcalidrawApp from "../excalidraw-app";
 
 const { h } = window;
 
@@ -56,7 +55,7 @@ describe("appState", () => {
 
   it("moving by page up/down/left/right", async () => {
     mockBoundingClientRect();
-    await render(<ExcalidrawApp />, {});
+    await render(<Excalidraw handleKeyboardGlobally={true} />, {});
 
     const scrollTest = () => {
       const initialScrollY = h.state.scrollY;

+ 23 - 13
src/tests/selection.test.tsx

@@ -6,7 +6,7 @@ import {
   restoreOriginalGetBoundingClientRect,
   assertSelectedElements,
 } from "./test-utils";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import * as Renderer from "../renderer/renderScene";
 import { KEYS } from "../keys";
 import { reseed } from "../random";
@@ -34,7 +34,7 @@ const mouse = new Pointer("mouse");
 
 describe("box-selection", () => {
   beforeEach(async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw />);
   });
 
   it("should allow adding to selection via box-select when holding shift", async () => {
@@ -102,7 +102,7 @@ describe("box-selection", () => {
 
 describe("inner box-selection", () => {
   beforeEach(async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw />);
   });
   it("selecting elements visually nested inside another", async () => {
     const rect1 = API.createElement({
@@ -218,7 +218,7 @@ describe("inner box-selection", () => {
 
 describe("selection element", () => {
   it("create selection element on pointer down", async () => {
-    const { getByToolName, container } = await render(<ExcalidrawApp />);
+    const { getByToolName, container } = await render(<Excalidraw />);
     // select tool
     const tool = getByToolName("selection");
     fireEvent.click(tool);
@@ -239,7 +239,7 @@ describe("selection element", () => {
   });
 
   it("resize selection element on pointer move", async () => {
-    const { getByToolName, container } = await render(<ExcalidrawApp />);
+    const { getByToolName, container } = await render(<Excalidraw />);
     // select tool
     const tool = getByToolName("selection");
     fireEvent.click(tool);
@@ -261,7 +261,7 @@ describe("selection element", () => {
   });
 
   it("remove selection element on pointer up", async () => {
-    const { getByToolName, container } = await render(<ExcalidrawApp />);
+    const { getByToolName, container } = await render(<Excalidraw />);
     // select tool
     const tool = getByToolName("selection");
     fireEvent.click(tool);
@@ -287,7 +287,9 @@ describe("select single element on the scene", () => {
   });
 
   it("rectangle", async () => {
-    const { getByToolName, container } = await render(<ExcalidrawApp />);
+    const { getByToolName, container } = await render(
+      <Excalidraw handleKeyboardGlobally={true} />,
+    );
     const canvas = container.querySelector("canvas.interactive")!;
     {
       // create element
@@ -317,7 +319,9 @@ describe("select single element on the scene", () => {
   });
 
   it("diamond", async () => {
-    const { getByToolName, container } = await render(<ExcalidrawApp />);
+    const { getByToolName, container } = await render(
+      <Excalidraw handleKeyboardGlobally={true} />,
+    );
     const canvas = container.querySelector("canvas.interactive")!;
     {
       // create element
@@ -347,7 +351,9 @@ describe("select single element on the scene", () => {
   });
 
   it("ellipse", async () => {
-    const { getByToolName, container } = await render(<ExcalidrawApp />);
+    const { getByToolName, container } = await render(
+      <Excalidraw handleKeyboardGlobally={true} />,
+    );
     const canvas = container.querySelector("canvas.interactive")!;
     {
       // create element
@@ -377,7 +383,9 @@ describe("select single element on the scene", () => {
   });
 
   it("arrow", async () => {
-    const { getByToolName, container } = await render(<ExcalidrawApp />);
+    const { getByToolName, container } = await render(
+      <Excalidraw handleKeyboardGlobally={true} />,
+    );
     const canvas = container.querySelector("canvas.interactive")!;
     {
       // create element
@@ -419,7 +427,9 @@ describe("select single element on the scene", () => {
   });
 
   it("arrow escape", async () => {
-    const { getByToolName, container } = await render(<ExcalidrawApp />);
+    const { getByToolName, container } = await render(
+      <Excalidraw handleKeyboardGlobally={true} />,
+    );
     const canvas = container.querySelector("canvas.interactive")!;
     {
       // create element
@@ -464,7 +474,7 @@ describe("select single element on the scene", () => {
 
 describe("tool locking & selection", () => {
   it("should not select newly created element while tool is locked", async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw />);
 
     UI.clickTool("lock");
     expect(h.state.activeTool.locked).toBe(true);
@@ -480,7 +490,7 @@ describe("tool locking & selection", () => {
 
 describe("selectedElementIds stability", () => {
   beforeEach(async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw />);
   });
 
   it("box-selection should be stable when not changing selection", () => {

+ 8 - 7
src/tests/subtypes.test.tsx

@@ -16,7 +16,7 @@ import {
 
 import { render } from "./test-utils";
 import { API } from "./helpers/api";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 
 import {
   ExcalidrawElement,
@@ -252,7 +252,7 @@ const { h } = window;
 
 describe("subtype registration", () => {
   it("should check for invalid subtype or parents", async () => {
-    await render(<ExcalidrawApp />, {});
+    await render(<Excalidraw />, {});
     // Define invalid subtype records
     const null1 = {} as SubtypeRecord;
     const null2 = { subtype: "" } as SubtypeRecord;
@@ -368,7 +368,7 @@ describe("subtypes", () => {
       API.createElement({ type: "diamond", id: "D", subtype: test1.subtype }),
       API.createElement({ type: "ellipse", id: "E", subtype: test1.subtype }),
     ];
-    await render(<ExcalidrawApp />, { localStorageData: { elements } });
+    await render(<Excalidraw />, { localStorageData: { elements } });
     elements.forEach((el) => expect(el.subtype).toBe(test1.subtype));
   });
   it("should enforce prop value restrictions", async () => {
@@ -381,7 +381,7 @@ describe("subtypes", () => {
       }),
       API.createElement({ type: "line", id: "B", roughness: 1 }),
     ];
-    await render(<ExcalidrawApp />, { localStorageData: { elements } });
+    await render(<Excalidraw />, { localStorageData: { elements } });
     elements.forEach((el) => {
       if (el.subtype === test1.subtype) {
         expect(el.roughness).toBe(0);
@@ -440,7 +440,7 @@ describe("subtypes", () => {
         fontSize: FONTSIZE,
       }),
     ];
-    await render(<ExcalidrawApp />, { localStorageData: { elements } });
+    await render(<Excalidraw />, { localStorageData: { elements } });
     const mockMeasureText = (text: string, font: FontString) => {
       if (text === testString) {
         let multiplier = 1;
@@ -608,7 +608,7 @@ describe("subtype actions", () => {
       API.createElement({ type: "line", id: "C", subtype: test3.subtype }),
       API.createElement({ type: "text", id: "D", subtype: test3.subtype }),
     ];
-    await render(<ExcalidrawApp />, { localStorageData: { elements } });
+    await render(<Excalidraw />, { localStorageData: { elements } });
   });
   it("should apply to elements with their subtype", async () => {
     h.setState({ selectedElementIds: { A: true } });
@@ -672,7 +672,8 @@ describe("subtype loading", () => {
         text: testString,
       }),
     ];
-    await render(<ExcalidrawApp />, { localStorageData: { elements } });
+    await render(<Excalidraw />, { localStorageData: { elements } });
+    h.elements = elements;
   });
   it("should redraw text bounding boxes", async () => {
     h.setState({ selectedElementIds: { A: true } });

+ 1 - 1
src/tests/test-utils.ts

@@ -11,7 +11,7 @@ import {
 
 import * as toolQueries from "./queries/toolQueries";
 import { ImportedDataState } from "../data/types";
-import { STORAGE_KEYS } from "../excalidraw-app/app_constants";
+import { STORAGE_KEYS } from "../../excalidraw-app/app_constants";
 
 import { SceneData } from "../types";
 import { getSelectedElements } from "../scene/selection";

+ 2 - 2
src/tests/viewMode.test.tsx

@@ -1,5 +1,5 @@
 import { render, GlobalTestState } from "./test-utils";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import { KEYS } from "../keys";
 import { Keyboard, Pointer, UI } from "./helpers/ui";
 import { CURSOR_TYPE } from "../constants";
@@ -12,7 +12,7 @@ const pointerTypes = [mouse, touch, pen];
 
 describe("view mode", () => {
   beforeEach(async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw />);
   });
 
   it("after switching to view mode – cursor type should be pointer", async () => {

+ 2 - 3
src/tests/zindex.test.tsx

@@ -1,6 +1,6 @@
 import ReactDOM from "react-dom";
 import { render } from "./test-utils";
-import ExcalidrawApp from "../excalidraw-app";
+import { Excalidraw } from "../packages/excalidraw/index";
 import { reseed } from "../random";
 import {
   actionSendBackward,
@@ -121,7 +121,6 @@ const assertZindex = ({
   operations: [Actions, string[]][];
 }) => {
   const selectedElementIds = populateElements(elements, appState);
-
   operations.forEach(([action, expected]) => {
     h.app.actionManager.executeAction(action);
     expect(h.elements.map((element) => element.id)).toEqual(expected);
@@ -131,7 +130,7 @@ const assertZindex = ({
 
 describe("z-index manipulation", () => {
   beforeEach(async () => {
-    await render(<ExcalidrawApp />);
+    await render(<Excalidraw />);
   });
 
   it("send back", () => {

+ 1 - 1
src/zindex.ts

@@ -96,9 +96,9 @@ const getTargetIndexAccountingForBinding = (
       if (direction === "left") {
         return elements.indexOf(nextElement);
       }
+
       const boundTextElement =
         Scene.getScene(nextElement)!.getElement(boundElementId);
-
       if (boundTextElement) {
         return elements.indexOf(boundTextElement);
       }

+ 1 - 1
tsconfig.json

@@ -16,6 +16,6 @@
     "noEmit": true,
     "jsx": "react-jsx"
   },
-  "include": ["src"],
+  "include": ["src", "excalidraw-app"],
   "exclude": ["src/packages/excalidraw/types"]
 }

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov