Compare commits
327 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ce70b815e | |||
| c070c8ffa6 | |||
| e4c70cb6c6 | |||
| 20f694d110 | |||
| 2a82821ec5 | |||
| 070df27e4d | |||
| cd514d72d6 | |||
| 0642e72cfa | |||
| 28a9b1711d | |||
| 1cb9fff569 | |||
| 069982606d | |||
| b324a85ab1 | |||
| a83ac48853 | |||
| 0cf56b19c7 | |||
| 61fe15a51d | |||
| 647a264a48 | |||
| b6d80e4256 | |||
| 3372149277 | |||
| c08be69618 | |||
| b42b1a193d | |||
| f6d85bc80f | |||
| 0457ac9063 | |||
| b2b2815954 | |||
| d992c10bc1 | |||
| 091b9053a3 | |||
| 97274a74b2 | |||
| c59fb8dcbc | |||
| 7f56cc0cf3 | |||
| 974b338b7e | |||
| d2557474e2 | |||
| 3004c642da | |||
| 2dfcc6f0ce | |||
| 3f5fdec04e | |||
| 278cd35772 | |||
| 43fa4b5602 | |||
| 2e1a529c67 | |||
| b1c6bfcf40 | |||
| 1caec99b29 | |||
| e18c1dd213 | |||
| d9e8a33aa4 | |||
| 4be4cc0ed0 | |||
| 4a5c9e990c | |||
| c09e170bdd | |||
| c1082923ee | |||
| 1c292e4936 | |||
| d6f0f34fe9 | |||
| 75789f620d | |||
| a9ca16eb42 | |||
| 987173b52f | |||
| 81ab857a6f | |||
| e8b4620a96 | |||
| 2b0e4c9623 | |||
| c9ba7f839c | |||
| b4ce7c713b | |||
| 816c81c12e | |||
| 92d25446d6 | |||
| e73a5b0116 | |||
| 21dd1cfacc | |||
| fa1f7d9f22 | |||
| 3d8c12fba4 | |||
| 757dfeb6ad | |||
| a0e93b6040 | |||
| 499e9d64a5 | |||
| c1dbbdf678 | |||
| 47c254216b | |||
| d1cff91b75 | |||
| 437595fa65 | |||
| 60b275880d | |||
| cae9d2bcbd | |||
| 2874f9e48c | |||
| 0b3a5e7cc4 | |||
| 7ea3229e17 | |||
| b0404b10b6 | |||
| eb959128ac | |||
| 4c3d037f9c | |||
| 5852d0d410 | |||
| c1e00c44f5 | |||
| ffcb67b21f | |||
| 46ddd60948 | |||
| 89a9badc27 | |||
| 8b3e149db6 | |||
| a70417f23f | |||
| 1c8e8bb0f3 | |||
| 3a5ef4020d | |||
| 063533aede | |||
| b43260d97b | |||
| 83d3943cd0 | |||
| f39ac4a653 | |||
| 54a9826817 | |||
| d29fd62e41 | |||
| b57f3e0096 | |||
| f12ae80ba1 | |||
| f7b537a8b1 | |||
| 94364af68f | |||
| dfa1ce572b | |||
| b552c60714 | |||
| 216afc3625 | |||
| 802cde3501 | |||
| f5cf81ce42 | |||
| 6a891365b9 | |||
| 54fa0c9089 | |||
| dfdd994dbb | |||
| 28691e14b1 | |||
| 60759d314d | |||
| d5e37cda81 | |||
| 6135548534 | |||
| acf54c6f38 | |||
| 84a309d669 | |||
| 3c8e893cab | |||
| 9ba0f5dbc9 | |||
| 60ab14c2f6 | |||
| 0988ecfef4 | |||
| 1f47d61e8c | |||
| 9d760336d1 | |||
| 0443511954 | |||
| 5a73b9a363 | |||
| 24a6941861 | |||
| a0b98a944f | |||
| 6ebf52279d | |||
| 3b97f5a10c | |||
| da59205846 | |||
| b9a255407f | |||
| cc6c29c0b9 | |||
| 87faa5d3da | |||
| c158187f20 | |||
| 63e1148280 | |||
| b5fc873323 | |||
| 6c908553a9 | |||
| 0586fc138c | |||
| e95222ed32 | |||
| d87620b239 | |||
| 7cc31ac64a | |||
| 071b17a217 | |||
| 859207b8bc | |||
| becaabfa0f | |||
| f06484c6ab | |||
| bf4c65f483 | |||
| 8d18078f5c | |||
| d080833f4d | |||
| 451bcac0b7 | |||
| 06f01e11f8 | |||
| 51ad8951d4 | |||
| 7497a08270 | |||
| 210dc85c8c | |||
| 019ce4c52c | |||
| c141960ada | |||
| d7e63e66a7 | |||
| b660478164 | |||
| 37882c66cb | |||
| 7f66e1fe89 | |||
| 2b4540225d | |||
| dc2f25c14a | |||
| 8fb16669ab | |||
| f2600fe3e8 | |||
| 95ddc66339 | |||
| 5bcd8280c9 | |||
| c99e81678b | |||
| d1f39823f1 | |||
| 47cbb5b6fb | |||
| 8fd970320e | |||
| 8d8f696628 | |||
| 19b3dc658a | |||
| 4e0441eeb4 | |||
| 8013eb5e16 | |||
| 725412ebd3 | |||
| 7da176ff7d | |||
| 5fffc4743f | |||
| 8608d7b2e0 | |||
| 19b03b4ca9 | |||
| 416e8b3e42 | |||
| 98e0cd9078 | |||
| f3c16a600d | |||
| 835eb8d2fd | |||
| fde796a7a0 | |||
| 7c41944856 | |||
| f1b097ad06 | |||
| 9fcbbe0d27 | |||
| ec070911b8 | |||
| dcdeb2be57 | |||
| a8acc8212d | |||
| a89a03c66c | |||
| e32836f799 | |||
| 06c40006db | |||
| 91c7748c3d | |||
| f738b74791 | |||
| 00ae455873 | |||
| 06c5ea94d3 | |||
| f55ecb96cc | |||
| a6a32b9b29 | |||
| ac0d3059dc | |||
| 1161f1b8ba | |||
| 204e06b77b | |||
| 414182f599 | |||
| b9d27d308e | |||
| 3bdaafe4b5 | |||
| ae89608985 | |||
| 3085f4af81 | |||
| 531f3e5524 | |||
| 90ec2739ae | |||
| f29e9df72d | |||
| b5ad7ae4e3 | |||
| c78e4aab7f | |||
| b4903a7eab | |||
| c6f8ef9ad2 | |||
| 2535d73054 | |||
| dda3affcb0 | |||
| 54c148f390 | |||
| cc8e490c75 | |||
| 9036812b6d | |||
| df25de7e68 | |||
| a3763648fe | |||
| 178eca5828 | |||
| cb33de25f4 | |||
| 37ad85cbaf | |||
| d6a934ed19 | |||
| 416da62138 | |||
| f38f381989 | |||
| e5e07260c6 | |||
| 8492b144b0 | |||
| e46f038132 | |||
| 678dff25ed | |||
| 0cfa53b764 | |||
| cde46793f8 | |||
| 2d127f8c22 | |||
| 4eadb891f8 | |||
| 258605d1d5 | |||
| c141500400 | |||
| 8e27de2cdc | |||
| 0a19c93509 | |||
| 958597dfaa | |||
| 058918f8e5 | |||
| 3f194918e6 | |||
| 93c92d13e9 | |||
| 84e96e9393 | |||
| 320af405e9 | |||
| 60512f13d5 | |||
| f0458cc216 | |||
| 9f3fdf5505 | |||
| f42e1ab64e | |||
| 18808481fd | |||
| a7b64f02b3 | |||
| 0d4abd1ddc | |||
| 9e77373c81 | |||
| d108053351 | |||
| d4e85a9480 | |||
| 08cd4c4f9a | |||
| 469caadb87 | |||
| ca1a4f25e7 | |||
| 56c05b3099 | |||
| 6c0ff7fc5c | |||
| 7cad3645a0 | |||
| 5921ebc416 | |||
| 864353be5f | |||
| db2911c6c4 | |||
| fc3e062074 | |||
| 87c87a9fb1 | |||
| 4dc205537c | |||
| cc571c4681 | |||
| 14d512f321 | |||
| 41c036e1a5 | |||
| 91d36e9b81 | |||
| 27522110df | |||
| 712f267519 | |||
| 41a7613dff | |||
| 95d89a751a | |||
| 6b5fb30d69 | |||
| d92a849038 | |||
| 0a534f1bc6 | |||
| 4ca5f53b1f | |||
| f7dcc893ea | |||
| 4dfb8a3f8e | |||
| 298812e1d0 | |||
| 35bb449a4b | |||
| c4c064982f | |||
| 51dbd4831b | |||
| 7e41026812 | |||
| a8ebe514da | |||
| a30e1b25c6 | |||
| ff2ed5d26a | |||
| e058a08b33 | |||
| a306a909a0 | |||
| 3dc54a724a | |||
| a7c61319dd | |||
| cec5232a7a | |||
| d4f70e9f31 | |||
| e19fd1332a | |||
| 6e655cdb24 | |||
| 192c4e7658 | |||
| 195a743874 | |||
| 4a60fe3d22 | |||
| 2a0d15799c | |||
| a18b139a60 | |||
| 1913599594 | |||
| debf2ad608 | |||
| 8fb2f70414 | |||
| 5fc13e4309 | |||
| b5d60973b7 | |||
| a5d6939826 | |||
| 0cf36d6b30 | |||
| 58f7d33d80 | |||
| 6fe7de8020 | |||
| 01304aac49 | |||
| dff69e9191 | |||
| 6fc85022ae | |||
| e48b63a0ae | |||
| c2caf78e95 | |||
| ce267aa0d3 | |||
| 6e47fadb59 | |||
| b3d5ba0567 | |||
| c79e892e55 | |||
| 57a9e301d4 | |||
| 7c58477382 | |||
| 83fac6d0db | |||
| f2e8404c7b | |||
| d797c2e210 | |||
| 0cd5a259ae | |||
| 432a46ef9e | |||
| a18f059188 | |||
| ab89d4c16f | |||
| 6c3a434f2a | |||
| e1bb59fb8f | |||
| 77aca48c84 | |||
| 58990b41ae | |||
| 99d8bff175 | |||
| 30983d801a | |||
| 21ffaf4d76 | |||
| 82b9a6b464 |
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18-bullseye
|
FROM node:24-bullseye
|
||||||
|
|
||||||
# Vite wants to open the browser using `open`, so we
|
# Vite wants to open the browser using `open`, so we
|
||||||
# need to install those utils.
|
# need to install those utils.
|
||||||
|
|||||||
+7
-2
@@ -1,3 +1,5 @@
|
|||||||
|
MODE="development"
|
||||||
|
|
||||||
VITE_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
|
VITE_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
|
||||||
VITE_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
|
VITE_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
|
||||||
|
|
||||||
@@ -10,7 +12,7 @@ VITE_APP_WS_SERVER_URL=http://localhost:3002
|
|||||||
VITE_APP_PLUS_LP=https://plus.excalidraw.com
|
VITE_APP_PLUS_LP=https://plus.excalidraw.com
|
||||||
VITE_APP_PLUS_APP=http://localhost:3000
|
VITE_APP_PLUS_APP=http://localhost:3000
|
||||||
|
|
||||||
VITE_APP_AI_BACKEND=http://localhost:3015
|
VITE_APP_AI_BACKEND=http://localhost:3016
|
||||||
|
|
||||||
VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
|
VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
|
||||||
|
|
||||||
@@ -25,7 +27,7 @@ VITE_APP_ENABLE_TRACKING=true
|
|||||||
FAST_REFRESH=false
|
FAST_REFRESH=false
|
||||||
|
|
||||||
# The port the run the dev server
|
# The port the run the dev server
|
||||||
VITE_APP_PORT=3000
|
VITE_APP_PORT=3001
|
||||||
|
|
||||||
#Debug flags
|
#Debug flags
|
||||||
|
|
||||||
@@ -48,3 +50,6 @@ UNWEjuqNMi/lwAErS9fFa2oJlWyT8U7zzv/5kQREkxZI6y9v0AF3qcbsy2731FnD
|
|||||||
s9ChJvOUW9toIab2gsIdrKW8ZNpu084ZFVKb6LNjvIXI1Se4oMTHeszXzNptzlot
|
s9ChJvOUW9toIab2gsIdrKW8ZNpu084ZFVKb6LNjvIXI1Se4oMTHeszXzNptzlot
|
||||||
kdxxjOoaQMAyfljFSot1F1FlU6MQlag7UnFGvFjRHN1JI5q4K+n3a67DX+TMyRqS
|
kdxxjOoaQMAyfljFSot1F1FlU6MQlag7UnFGvFjRHN1JI5q4K+n3a67DX+TMyRqS
|
||||||
HQIDAQAB'
|
HQIDAQAB'
|
||||||
|
|
||||||
|
# set to true in .env.development.local to disable the prevent unload dialog
|
||||||
|
VITE_APP_DISABLE_PREVENT_UNLOAD=
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
MODE="production"
|
||||||
|
|
||||||
VITE_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
|
VITE_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
|
||||||
VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# VITE_DEBUG_DOM
|
||||||
|
# When "true", testing-library failures (waitFor / getBy*) include the full
|
||||||
|
# serialized DOM in the error message. It's off by default because it's noisy.
|
||||||
|
#
|
||||||
|
# Flip it to "true" (or use `VITE_DEBUG_DOM=true yarn test`) when you need to
|
||||||
|
# inspect the DOM of a failing test.
|
||||||
|
VITE_DEBUG_DOM=false
|
||||||
+43
-1
@@ -1,6 +1,21 @@
|
|||||||
{
|
{
|
||||||
"extends": ["@excalidraw/eslint-config", "react-app"],
|
"extends": ["@excalidraw/eslint-config", "react-app"],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"import/order": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"groups": ["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"],
|
||||||
|
"pathGroups": [
|
||||||
|
{
|
||||||
|
"pattern": "@excalidraw/**",
|
||||||
|
"group": "external",
|
||||||
|
"position": "after"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"newlines-between": "always-and-inside-groups",
|
||||||
|
"warnOnUnassignedImports": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"import/no-anonymous-default-export": "off",
|
"import/no-anonymous-default-export": "off",
|
||||||
"no-restricted-globals": "off",
|
"no-restricted-globals": "off",
|
||||||
"@typescript-eslint/consistent-type-imports": [
|
"@typescript-eslint/consistent-type-imports": [
|
||||||
@@ -17,6 +32,33 @@
|
|||||||
"name": "jotai",
|
"name": "jotai",
|
||||||
"message": "Do not import from \"jotai\" directly. Use our app-specific modules (\"editor-jotai\" or \"app-jotai\")."
|
"message": "Do not import from \"jotai\" directly. Use our app-specific modules (\"editor-jotai\" or \"app-jotai\")."
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"react/jsx-no-target-blank": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowReferrer": true
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["packages/excalidraw/**/*.{ts,tsx}"],
|
||||||
|
"excludedFiles": ["packages/excalidraw/**/*.test.{ts,tsx}", "packages/excalidraw/**/*.test.*.{ts,tsx}"],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-restricted-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"group": ["@excalidraw/excalidraw"],
|
||||||
|
"message": "Do not import from the barrel 'index.tsx' files. Use direct relative imports to the specific module instead.",
|
||||||
|
"allowTypeImports": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": [".", "..", "../..", "../../..", "../../../..", "../../../../..", "../index", "../../index", "../../../index", "../../../../index"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Project coding standards
|
||||||
|
|
||||||
|
## Generic Communication Guidelines
|
||||||
|
|
||||||
|
- Be succint and be aware that expansive generative AI answers are costly and slow
|
||||||
|
- Avoid providing explanations, trying to teach unless asked for, your chat partner is an expert
|
||||||
|
- Stop apologising if corrected, just provide the correct information or code
|
||||||
|
- Prefer code unless asked for explanation
|
||||||
|
- Stop summarizing what you've changed after modifications unless asked for
|
||||||
|
|
||||||
|
## TypeScript Guidelines
|
||||||
|
|
||||||
|
- Use TypeScript for all new code
|
||||||
|
- Where possible, prefer implementations without allocation
|
||||||
|
- When there is an option, opt for more performant solutions and trade RAM usage for less CPU cycles
|
||||||
|
- Prefer immutable data (const, readonly)
|
||||||
|
- Use optional chaining (?.) and nullish coalescing (??) operators
|
||||||
|
|
||||||
|
## React Guidelines
|
||||||
|
|
||||||
|
- Use functional components with hooks
|
||||||
|
- Follow the React hooks rules (no conditional hooks)
|
||||||
|
- Keep components small and focused
|
||||||
|
- Use CSS modules for component styling
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
|
||||||
|
- Use PascalCase for component names, interfaces, and type aliases
|
||||||
|
- Use camelCase for variables, functions, and methods
|
||||||
|
- Use ALL_CAPS for constants
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- Use try/catch blocks for async operations
|
||||||
|
- Implement proper error boundaries in React components
|
||||||
|
- Always log errors with contextual information
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Always attempt to fix #problems
|
||||||
|
- Always offer to run `yarn test:app` in the project root after modifications are complete and attempt fixing the issues reported
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
- Always include `packages/math/src/types.ts` in the context when your write math related code and always use the Point type instead of { x, y}
|
||||||
@@ -9,13 +9,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
- name: Set up publish access
|
- name: Set up publish access
|
||||||
run: |
|
run: |
|
||||||
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
||||||
@@ -24,4 +24,4 @@ jobs:
|
|||||||
- name: Auto release
|
- name: Auto release
|
||||||
run: |
|
run: |
|
||||||
yarn add @actions/core -W
|
yarn add @actions/core -W
|
||||||
yarn autorelease
|
yarn release --tag=next --non-interactive
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
name: Auto release excalidraw preview
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created, edited]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
Auto-release-excalidraw-preview:
|
|
||||||
name: Auto release preview
|
|
||||||
if: github.event.comment.body == '@excalibot trigger release' && github.event.issue.pull_request
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: React to release comment
|
|
||||||
uses: peter-evans/create-or-update-comment@v1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
|
||||||
comment-id: ${{ github.event.comment.id }}
|
|
||||||
reactions: "+1"
|
|
||||||
- name: Get PR SHA
|
|
||||||
id: sha
|
|
||||||
uses: actions/github-script@v4
|
|
||||||
with:
|
|
||||||
result-encoding: string
|
|
||||||
script: |
|
|
||||||
const { owner, repo, number } = context.issue;
|
|
||||||
const pr = await github.pulls.get({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
pull_number: number,
|
|
||||||
});
|
|
||||||
return pr.data.head.sha
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
ref: ${{ steps.sha.outputs.result }}
|
|
||||||
fetch-depth: 2
|
|
||||||
- name: Setup Node.js 18.x
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: 18.x
|
|
||||||
- name: Set up publish access
|
|
||||||
run: |
|
|
||||||
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
|
||||||
env:
|
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
- name: Auto release preview
|
|
||||||
id: "autorelease"
|
|
||||||
run: |
|
|
||||||
yarn add @actions/core -W
|
|
||||||
yarn autorelease preview ${{ github.event.issue.number }}
|
|
||||||
- name: Post comment post release
|
|
||||||
if: always()
|
|
||||||
uses: peter-evans/create-or-update-comment@v1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
|
||||||
issue-number: ${{ github.event.issue.number }}
|
|
||||||
body: "@${{ github.event.comment.user.login }} ${{ steps.autorelease.outputs.result }}"
|
|
||||||
@@ -9,5 +9,5 @@ jobs:
|
|||||||
build-docker:
|
build-docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
- run: docker build -t excalidraw .
|
- run: docker build -t excalidraw .
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 3
|
timeout-minutes: 3
|
||||||
steps:
|
steps:
|
||||||
- uses: styfle/cancel-workflow-action@0.6.0
|
- uses: styfle/cancel-workflow-action@ce177499ccf9fd2aded3b0426c97e5434c2e8a73 # 0.6.0
|
||||||
with:
|
with:
|
||||||
workflow_id: 400555, 400556, 905313, 1451724, 1710116, 3185001, 3438604
|
workflow_id: 400555, 400556, 905313, 1451724, 1710116, 3185001, 3438604
|
||||||
access_token: ${{ secrets.GITHUB_TOKEN }}
|
access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
|
|
||||||
- name: Install and lint
|
- name: Install and lint
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
||||||
|
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
|
|
||||||
- name: Create report file
|
- name: Create report file
|
||||||
run: |
|
run: |
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
echo ::set-output name=body::$body
|
echo ::set-output name=body::$body
|
||||||
|
|
||||||
- name: Update description with coverage
|
- name: Update description with coverage
|
||||||
uses: kt3k/update-pr-description@v1.0.1
|
uses: kt3k/update-pr-description@1b35a6dcd84d81aa0bc1889610efdcde7f37b0c0 # v1.0.1
|
||||||
with:
|
with:
|
||||||
pr_body: ${{ steps.getCommentBody.outputs.body }}
|
pr_body: ${{ steps.getCommentBody.outputs.body }}
|
||||||
pr_title: "chore: Update translations from Crowdin"
|
pr_title: "chore: Update translations from Crowdin"
|
||||||
|
|||||||
@@ -11,15 +11,20 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
tags: excalidraw/excalidraw:latest
|
tags: excalidraw/excalidraw:latest
|
||||||
|
platforms: linux/amd64, linux/arm64, linux/arm/v7
|
||||||
|
|||||||
@@ -6,11 +6,97 @@ on:
|
|||||||
- opened
|
- opened
|
||||||
- edited
|
- edited
|
||||||
- synchronize
|
- synchronize
|
||||||
|
- labeled
|
||||||
|
- unlabeled
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
semantic:
|
semantic:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
steps:
|
steps:
|
||||||
- uses: amannn/action-semantic-pull-request@v5
|
- uses: amannn/action-semantic-pull-request@e32d7e603df1aa1ba07e981f2a23455dee596825 # v5
|
||||||
|
with:
|
||||||
|
requireScope: true
|
||||||
|
scopes: |
|
||||||
|
app
|
||||||
|
editor
|
||||||
|
packages/excalidraw
|
||||||
|
packages/utils
|
||||||
|
docker
|
||||||
|
repo
|
||||||
|
ignoreLabels: |
|
||||||
|
skip-semantic-title
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
label-scope:
|
||||||
|
needs: semantic
|
||||||
|
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Label scoped PR
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
|
REPOSITORY: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
scope_labels=(s-app s-editor s-package)
|
||||||
|
|
||||||
|
readarray -t desired_labels < <(
|
||||||
|
node <<'NODE'
|
||||||
|
const title = process.env.PR_TITLE;
|
||||||
|
const match = title.match(/^[a-z]+(?:\(([^)]+)\))?!?:/i);
|
||||||
|
const scopes = match?.[1]?.split(",").map((scope) => scope.trim()) ?? [];
|
||||||
|
const labels = new Set();
|
||||||
|
|
||||||
|
for (const scope of scopes) {
|
||||||
|
if (scope === "app") {
|
||||||
|
labels.add("s-app");
|
||||||
|
} else if (scope === "editor") {
|
||||||
|
labels.add("s-editor");
|
||||||
|
} else if (scope.startsWith("packages/")) {
|
||||||
|
labels.add("s-package");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write([...labels].join("\n"));
|
||||||
|
NODE
|
||||||
|
)
|
||||||
|
|
||||||
|
should_apply_label() {
|
||||||
|
local label="$1"
|
||||||
|
|
||||||
|
for desired_label in "${desired_labels[@]}"; do
|
||||||
|
if [[ "$desired_label" == "$label" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for label in "${scope_labels[@]}"; do
|
||||||
|
if ! should_apply_label "$label"; then
|
||||||
|
gh api \
|
||||||
|
--method DELETE \
|
||||||
|
"repos/${REPOSITORY}/issues/${PR_NUMBER}/labels/${label}" \
|
||||||
|
--silent 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for label in "${desired_labels[@]}"; do
|
||||||
|
if ! gh api \
|
||||||
|
--method POST \
|
||||||
|
"repos/${REPOSITORY}/issues/${PR_NUMBER}/labels" \
|
||||||
|
--field "labels[]=${label}" \
|
||||||
|
--silent; then
|
||||||
|
echo "::warning::Could not apply ${label}. The workflow token likely does not have issues:write permission for this PR."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ jobs:
|
|||||||
sentry:
|
sentry:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
- name: Install and build
|
- name: Install and build
|
||||||
run: |
|
run: |
|
||||||
yarn --frozen-lockfile
|
yarn --frozen-lockfile
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
export SENTRY_RELEASE=$(sentry-cli releases propose-version)
|
export SENTRY_RELEASE=$(sentry-cli releases propose-version)
|
||||||
sentry-cli releases new $SENTRY_RELEASE --project $SENTRY_PROJECT
|
sentry-cli releases new $SENTRY_RELEASE --project $SENTRY_PROJECT
|
||||||
sentry-cli releases set-commits --auto $SENTRY_RELEASE
|
sentry-cli releases set-commits --auto $SENTRY_RELEASE
|
||||||
sentry-cli releases files $SENTRY_RELEASE upload-sourcemaps --no-rewrite ./build/static/js/ --url-prefix "~/static/js"
|
sentry-cli sourcemaps upload --release $SENTRY_RELEASE --no-rewrite ./build/static/js/ --url-prefix "~/static/js"
|
||||||
sentry-cli releases finalize $SENTRY_RELEASE
|
sentry-cli releases finalize $SENTRY_RELEASE
|
||||||
sentry-cli releases deploys $SENTRY_RELEASE new -e production
|
sentry-cli releases deploys $SENTRY_RELEASE new -e production
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -10,17 +10,17 @@ jobs:
|
|||||||
CI_JOB_NUMBER: 1
|
CI_JOB_NUMBER: 1
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
- name: Install in packages/excalidraw
|
- name: Install in packages/excalidraw
|
||||||
run: yarn
|
run: yarn
|
||||||
working-directory: packages/excalidraw
|
working-directory: packages/excalidraw
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
- uses: andresz1/size-limit-action@v1
|
- uses: andresz1/size-limit-action@e7493a72a44b113341c0cf6186ab49c17c4b65c1 # v1
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
build_script: build:esm
|
build_script: build:esm
|
||||||
|
|||||||
@@ -10,17 +10,17 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
- name: "Install Node"
|
- name: "Install Node"
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version: "18.x"
|
node-version: "20.x"
|
||||||
- name: "Install Deps"
|
- name: "Install Deps"
|
||||||
run: yarn install
|
run: yarn install
|
||||||
- name: "Test Coverage"
|
- name: "Test Coverage"
|
||||||
run: yarn test:coverage
|
run: yarn test:coverage
|
||||||
- name: "Report Coverage"
|
- name: "Report Coverage"
|
||||||
if: always() # Also generate the report if tests are failing
|
if: always() # Also generate the report if tests are failing
|
||||||
uses: davelosert/vitest-coverage-report-action@v2
|
uses: davelosert/vitest-coverage-report-action@2500dafcee7dd64f85ab689c0b83798a8359770e # v2
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
- name: Install and test
|
- name: Install and test
|
||||||
run: |
|
run: |
|
||||||
yarn install
|
yarn install
|
||||||
|
|||||||
+2
-1
@@ -25,4 +25,5 @@ packages/excalidraw/types
|
|||||||
coverage
|
coverage
|
||||||
dev-dist
|
dev-dist
|
||||||
html
|
html
|
||||||
meta*.json
|
meta*.json
|
||||||
|
.claude
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
Excalidraw is a **monorepo** with a clear separation between the core library and the application:
|
||||||
|
|
||||||
|
- **`packages/excalidraw/`** - Main React component library published to npm as `@excalidraw/excalidraw`
|
||||||
|
- **`excalidraw-app/`** - Full-featured web application (excalidraw.com) that uses the library
|
||||||
|
- **`packages/`** - Core packages: `@excalidraw/common`, `@excalidraw/element`, `@excalidraw/math`, `@excalidraw/utils`
|
||||||
|
- **`examples/`** - Integration examples (NextJS, browser script)
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
1. **Package Development**: Work in `packages/*` for editor features
|
||||||
|
2. **App Development**: Work in `excalidraw-app/` for app-specific features
|
||||||
|
3. **Testing**: Always run `yarn test:update` before committing
|
||||||
|
4. **Type Safety**: Use `yarn test:typecheck` to verify TypeScript
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn test:typecheck # TypeScript type checking
|
||||||
|
yarn test:update # Run all tests (with snapshot updates)
|
||||||
|
yarn fix # Auto-fix formatting and linting issues
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Notes
|
||||||
|
|
||||||
|
### Package System
|
||||||
|
|
||||||
|
- Uses Yarn workspaces for monorepo management
|
||||||
|
- Internal packages use path aliases (see `vitest.config.mts`)
|
||||||
|
- Build system uses esbuild for packages, Vite for the app
|
||||||
|
- TypeScript throughout with strict configuration
|
||||||
+5
-4
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18 AS build
|
FROM --platform=${BUILDPLATFORM} node:24@sha256:8530f76a96d88820d288761f022e318970dda93d01536919fbc16076b7983e63 AS build
|
||||||
|
|
||||||
WORKDIR /opt/node_app
|
WORKDIR /opt/node_app
|
||||||
|
|
||||||
@@ -6,13 +6,14 @@ COPY . .
|
|||||||
|
|
||||||
# do not ignore optional dependencies:
|
# do not ignore optional dependencies:
|
||||||
# Error: Cannot find module @rollup/rollup-linux-x64-gnu
|
# Error: Cannot find module @rollup/rollup-linux-x64-gnu
|
||||||
RUN yarn --network-timeout 600000
|
RUN --mount=type=cache,target=/root/.cache/yarn \
|
||||||
|
npm_config_target_arch=${TARGETARCH} yarn --frozen-lockfile --network-timeout 600000
|
||||||
|
|
||||||
ARG NODE_ENV=production
|
ARG NODE_ENV=production
|
||||||
|
|
||||||
RUN yarn build:app:docker
|
RUN npm_config_target_arch=${TARGETARCH} yarn build:app:docker
|
||||||
|
|
||||||
FROM nginx:1.27-alpine
|
FROM nginx:stable-alpine-slim@sha256:2c605dbeab79a6b2a63340474fe58119d0ef95bdc4b1f41df0aa689659b3d13b
|
||||||
|
|
||||||
COPY --from=build /opt/node_app/excalidraw-app/build /usr/share/nginx/html
|
COPY --from=build /opt/node_app/excalidraw-app/build /usr/share/nginx/html
|
||||||
|
|
||||||
|
|||||||
@@ -23,20 +23,17 @@
|
|||||||
<br />
|
<br />
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/LICENSE">
|
<a href="https://github.com/excalidraw/excalidraw/blob/master/LICENSE">
|
||||||
<img alt="Excalidraw is released under the MIT license." src="https://img.shields.io/badge/license-MIT-blue.svg" />
|
<img alt="Excalidraw is released under the MIT license." src="https://img.shields.io/badge/license-MIT-blue.svg" /></a>
|
||||||
</a>
|
|
||||||
<a href="https://www.npmjs.com/package/@excalidraw/excalidraw">
|
<a href="https://www.npmjs.com/package/@excalidraw/excalidraw">
|
||||||
<img alt="npm downloads/month" src="https://img.shields.io/npm/dm/@excalidraw/excalidraw" />
|
<img alt="npm downloads/month" src="https://img.shields.io/npm/dm/@excalidraw/excalidraw" /></a>
|
||||||
</a>
|
|
||||||
<a href="https://docs.excalidraw.com/docs/introduction/contributing">
|
<a href="https://docs.excalidraw.com/docs/introduction/contributing">
|
||||||
<img alt="PRs welcome!" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" />
|
<img alt="PRs welcome!" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" /></a>
|
||||||
</a>
|
|
||||||
<a href="https://discord.gg/UexuTaE">
|
<a href="https://discord.gg/UexuTaE">
|
||||||
<img alt="Chat on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widge=false"/>
|
<img alt="Chat on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widget=false"/></a>
|
||||||
</a>
|
<a href="https://deepwiki.com/excalidraw/excalidraw">
|
||||||
|
<img alt="Ask DeepWiki" src="https://deepwiki.com/badge.svg" /></a>
|
||||||
<a href="https://twitter.com/excalidraw">
|
<a href="https://twitter.com/excalidraw">
|
||||||
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/>
|
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/></a>
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
@@ -63,7 +60,7 @@ The Excalidraw editor (npm package) supports:
|
|||||||
- 🏗️ Customizable.
|
- 🏗️ Customizable.
|
||||||
- 📷 Image support.
|
- 📷 Image support.
|
||||||
- 😀 Shape libraries support.
|
- 😀 Shape libraries support.
|
||||||
- 👅 Localization (i18n) support.
|
- 🌐 Localization (i18n) support.
|
||||||
- 🖼️ Export to PNG, SVG & clipboard.
|
- 🖼️ Export to PNG, SVG & clipboard.
|
||||||
- 💾 Open format - export drawings as an `.excalidraw` json file.
|
- 💾 Open format - export drawings as an `.excalidraw` json file.
|
||||||
- ⚒️ Wide range of tools - rectangle, circle, diamond, arrow, line, free-draw, eraser...
|
- ⚒️ Wide range of tools - rectangle, circle, diamond, arrow, line, free-draw, eraser...
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
Earlier we were using `renderFooter` prop to render custom footer which was removed in [#5970](https://github.com/excalidraw/excalidraw/pull/5970). Now you can pass a `Footer` component instead to render the custom UI for footer.
|
Earlier we were using `renderFooter` prop to render custom footer which was removed in [#5970](https://github.com/excalidraw/excalidraw/pull/5970). Now you can pass a `Footer` component instead to render the custom UI for footer.
|
||||||
|
|
||||||
You will need to import the `Footer` component from the package and wrap your component with the Footer component. The `Footer` should a valid React Node.
|
You will need to import the `Footer` component from the package and wrap your component with the Footer component. The `Footer` should be a valid React Node.
|
||||||
|
|
||||||
**Usage**
|
**Usage**
|
||||||
|
|
||||||
```jsx live
|
```jsx live
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div style={{ height: "500px"}}>
|
<div style={{ height: "500px" }}>
|
||||||
<Excalidraw>
|
<Excalidraw>
|
||||||
<Footer>
|
<Footer>
|
||||||
<button
|
<button
|
||||||
@@ -25,21 +25,21 @@ function App() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This will only for `Desktop` devices.
|
This will only work for `Desktop` devices.
|
||||||
|
|
||||||
For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
|
For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useEditorInterface`](#useEditorInterface) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
|
||||||
|
|
||||||
Open the `Menu` in the below playground and you will see the `custom footer` rendered.
|
Open the `Menu` in the below playground and you will see the `custom footer` rendered.
|
||||||
|
|
||||||
```jsx live noInline
|
```jsx live noInline
|
||||||
const MobileFooter = ({}) => {
|
const MobileFooter = ({}) => {
|
||||||
const device = useDevice();
|
const editorInterface = useEditorInterface();
|
||||||
if (device.editor.isMobile) {
|
if (editorInterface.formFactor === "phone") {
|
||||||
return (
|
return (
|
||||||
<Footer>
|
<Footer>
|
||||||
<button
|
<button
|
||||||
className="custom-footer"
|
className="custom-footer"
|
||||||
style= {{ marginLeft: '20px', height: '2rem'}}
|
style={{ marginLeft: "20px", height: "2rem" }}
|
||||||
onClick={() => alert("This is custom footer in mobile menu")}
|
onClick={() => alert("This is custom footer in mobile menu")}
|
||||||
>
|
>
|
||||||
custom footer
|
custom footer
|
||||||
@@ -65,4 +65,4 @@ const App = () => (
|
|||||||
// Need to render when code is span across multiple components
|
// Need to render when code is span across multiple components
|
||||||
// in Live Code blocks editor
|
// in Live Code blocks editor
|
||||||
render(<App />);
|
render(<App />);
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ convertToExcalidrawElements([
|
|||||||
type: "arrow",
|
type: "arrow",
|
||||||
x: 450,
|
x: 450,
|
||||||
y: 20,
|
y: 20,
|
||||||
startArrowhead: "dot",
|
startArrowhead: "circle",
|
||||||
endArrowhead: "triangle",
|
endArrowhead: "triangle",
|
||||||
strokeColor: "#1971c2",
|
strokeColor: "#1971c2",
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
|
|||||||
@@ -363,13 +363,7 @@ This API has the below signature. It sets the `tool` passed in param as the acti
|
|||||||
```ts
|
```ts
|
||||||
(
|
(
|
||||||
tool: (
|
tool: (
|
||||||
| (
|
| { type: ToolType }
|
||||||
| { type: Exclude<ToolType, "image"> }
|
|
||||||
| {
|
|
||||||
type: Extract<ToolType, "image">;
|
|
||||||
insertOnCanvasDirectly?: boolean;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
| { type: "custom"; customType: string }
|
| { type: "custom"; customType: string }
|
||||||
) & { locked?: boolean },
|
) & { locked?: boolean },
|
||||||
) => {};
|
) => {};
|
||||||
@@ -377,7 +371,7 @@ This API has the below signature. It sets the `tool` passed in param as the acti
|
|||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `type` | [ToolType](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L91) | `selection` | The tool type which should be set as active tool. When setting `image` as active tool, the insertion onto canvas when using image tool is disabled by default, so you can enable it by setting `insertOnCanvasDirectly` to `true` |
|
| `type` | [ToolType](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L91) | `selection` | The tool type which should be set as active tool |
|
||||||
| `locked` | `boolean` | `false` | Indicates whether the the active tool should be locked. It behaves the same way when using the `lock` tool in the editor interface |
|
| `locked` | `boolean` | `false` | Indicates whether the the active tool should be locked. It behaves the same way when using the `lock` tool in the editor interface |
|
||||||
|
|
||||||
## setCursor
|
## setCursor
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
All `props` are _optional_.
|
All `props` are _optional_.
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` | `null` | <code>Promise<object | null></code> | `null` | The initial data with which app loads. |
|
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` | `null` | <code>Promise<object | null></code> | `null` | The initial data with which app loads. |
|
||||||
| [`excalidrawAPI`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api) | `function` | \_ | Callback triggered with the excalidraw api once rendered |
|
| [`excalidrawAPI`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api) | `function` | \_ | Callback triggered with the excalidraw api once rendered |
|
||||||
| [`isCollaborating`](#iscollaborating) | `boolean` | \_ | This indicates if the app is in `collaboration` mode |
|
| [`isCollaborating`](#iscollaborating) | `boolean` | \_ | This indicates if the app is in `collaboration` mode |
|
||||||
@@ -13,7 +13,7 @@ All `props` are _optional_.
|
|||||||
| [`onScrollChange`](#onscrollchange) | `function` | \_ | This prop if passed gets triggered when scrolling the canvas. |
|
| [`onScrollChange`](#onscrollchange) | `function` | \_ | This prop if passed gets triggered when scrolling the canvas. |
|
||||||
| [`onPaste`](#onpaste) | `function` | \_ | Callback to be triggered if passed when something is pasted into the scene |
|
| [`onPaste`](#onpaste) | `function` | \_ | Callback to be triggered if passed when something is pasted into the scene |
|
||||||
| [`onLibraryChange`](#onlibrarychange) | `function` | \_ | The callback if supplied is triggered when the library is updated and receives the library items. |
|
| [`onLibraryChange`](#onlibrarychange) | `function` | \_ | The callback if supplied is triggered when the library is updated and receives the library items. |
|
||||||
| [`generateLinkForSelection`](#generateLinkForSelection) | `function` | \_ | Allows you to override `url` generation when linking to Excalidraw elements. |
|
| [`generateLinkForSelection`](#generatelinkforselection) | `function` | \_ | Allows you to override `url` generation when linking to Excalidraw elements. |
|
||||||
| [`onLinkOpen`](#onlinkopen) | `function` | \_ | The callback if supplied is triggered when any link is opened. |
|
| [`onLinkOpen`](#onlinkopen) | `function` | \_ | The callback if supplied is triggered when any link is opened. |
|
||||||
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
|
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
|
||||||
| [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | \_ | Render function that renders custom UI in top right corner |
|
| [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | \_ | Render function that renders custom UI in top right corner |
|
||||||
@@ -29,8 +29,9 @@ All `props` are _optional_.
|
|||||||
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
|
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
|
||||||
| [`autoFocus`](#autofocus) | `boolean` | `false` | Indicates whether to focus the Excalidraw component on page load |
|
| [`autoFocus`](#autofocus) | `boolean` | `false` | Indicates whether to focus the Excalidraw component on page load |
|
||||||
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
|
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
|
||||||
| [`validateEmbeddable`](#validateEmbeddable) | string[] | `boolean | RegExp | RegExp[] | ((link: string) => boolean | undefined)` | \_ | use for custom src url validation |
|
| [`validateEmbeddable`](#validateembeddable) | `string[]` \| `boolean` \| `RegExp` \| `RegExp[]` \| <code>((link: string) => boolean | undefined)</code> | \_ | use for custom src url validation |
|
||||||
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
||||||
|
| [`renderScrollbars`] | `boolean`| | `false` | Indicates whether scrollbars will be shown
|
||||||
|
|
||||||
### Storing custom data on Excalidraw elements
|
### Storing custom data on Excalidraw elements
|
||||||
|
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ viewportCoordsToSceneCoords({ clientX: number, clientY: number },<br/> 
|
|||||||
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a><br/>): {x: number, y: number}
|
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a><br/>): {x: number, y: number}
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
### useDevice
|
### useEditorInterface
|
||||||
|
|
||||||
This hook can be used to check the type of device which is being used. It can only be used inside the `children` of `Excalidraw` component.
|
This hook can be used to check the type of device which is being used. It can only be used inside the `children` of `Excalidraw` component.
|
||||||
|
|
||||||
@@ -300,8 +300,8 @@ Open the `main menu` in the below example to view the footer.
|
|||||||
|
|
||||||
```jsx live noInline
|
```jsx live noInline
|
||||||
const MobileFooter = ({}) => {
|
const MobileFooter = ({}) => {
|
||||||
const device = useDevice();
|
const editorInterface = useEditorInterface();
|
||||||
if (device.editor.isMobile) {
|
if (editorInterface.formFactor === "phone") {
|
||||||
return (
|
return (
|
||||||
<Footer>
|
<Footer>
|
||||||
<button
|
<button
|
||||||
@@ -336,12 +336,20 @@ render(<App />);
|
|||||||
The `device` has the following `attributes`, some grouped into `viewport` and `editor` objects, per context.
|
The `device` has the following `attributes`, some grouped into `viewport` and `editor` objects, per context.
|
||||||
|
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| --- | --- | --- |
|
| ---- | ---- | ----------- |
|
||||||
| `viewport.isMobile` | `boolean` | Set to `true` when viewport is in `mobile` breakpoint |
|
|
||||||
| `viewport.isLandscape` | `boolean` | Set to `true` when the viewport is in `landscape` mode |
|
The `EditorInterface` object has the following properties:
|
||||||
| `editor.canFitSidebar` | `boolean` | Set to `true` if there's enough space to fit the `sidebar` |
|
|
||||||
| `editor.isMobile` | `boolean` | Set to `true` when editor container is in `mobile` breakpoint |
|
| Name | Type | Description |
|
||||||
| `isTouchScreen` | `boolean` | Set to `true` for `touch` when touch event detected |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| `formFactor` | `'phone' | 'tablet' | 'desktop'` | Indicates the device type based on screen size |
|
||||||
|
| `desktopUIMode` | `'compact' | 'full'` | UI mode for desktop form factor |
|
||||||
|
| `userAgent.raw` | `string` | Raw user agent string |
|
||||||
|
| `userAgent.isMobileDevice` | `boolean` | True if device is mobile |
|
||||||
|
| `userAgent.platform` | `'ios' | 'android' | 'other' | 'unknown'` | Device platform |
|
||||||
|
| `isTouchScreen` | `boolean` | True if touch events are detected |
|
||||||
|
| `canFitSidebar` | `boolean` | True if sidebar can fit in the viewport |
|
||||||
|
| `isLandscape` | `boolean` | True if viewport is in landscape mode |
|
||||||
|
|
||||||
### i18n
|
### i18n
|
||||||
|
|
||||||
|
|||||||
@@ -28,32 +28,12 @@ To start the example app using the `@excalidraw/excalidraw` package, follow the
|
|||||||
|
|
||||||
## Releasing
|
## Releasing
|
||||||
|
|
||||||
### Create a test release
|
|
||||||
|
|
||||||
You can create a test release by posting the below comment in your pull request:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
@excalibot trigger release
|
|
||||||
```
|
|
||||||
|
|
||||||
Once the version is released `@excalibot` will post a comment with the release version.
|
|
||||||
|
|
||||||
### Creating a production release
|
### Creating a production release
|
||||||
|
|
||||||
To release the next stable version follow the below steps:
|
To release the next stable version follow the below steps:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn prerelease:excalidraw
|
yarn release --tag=latest --version=0.19.0
|
||||||
```
|
```
|
||||||
|
|
||||||
You need to pass the `version` for which you want to create the release. This will make the changes needed before making the release like updating `package.json`, `changelog` and more.
|
You will need to pass the `latest` tag with `version` for which you want to create the release. This will make the changes needed before publishing the packages into NPM, like updating dependencies of all `@excalidraw/*` packages, generating new entries in `CHANGELOG.md` and more.
|
||||||
|
|
||||||
The next step is to run the `release` script:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn release:excalidraw
|
|
||||||
```
|
|
||||||
|
|
||||||
This will publish the package.
|
|
||||||
|
|
||||||
Right now there are two steps to create a production release but once this works fine these scripts will be combined and more automation will be done.
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ If you want to only import `Excalidraw` component you can do :point_down:
|
|||||||
|
|
||||||
```jsx showLineNumbers
|
```jsx showLineNumbers
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
import "@excalidraw/excalidraw/index.css";
|
||||||
|
|
||||||
const Excalidraw = dynamic(
|
const Excalidraw = dynamic(
|
||||||
async () => (await import("@excalidraw/excalidraw")).Excalidraw,
|
async () => (await import("@excalidraw/excalidraw")).Excalidraw,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -97,8 +97,8 @@ const config = {
|
|||||||
href: "https://discord.gg/UexuTaE",
|
href: "https://discord.gg/UexuTaE",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Twitter",
|
label: "𝕏",
|
||||||
href: "https://twitter.com/excalidraw",
|
href: "https://x.com/excalidraw",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Linkedin",
|
label: "Linkedin",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
const FeatureList = [
|
const FeatureList = [
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
type FeatureItem = {
|
type FeatureItem = {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React from "react";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import Layout from "@theme/Layout";
|
|
||||||
import Link from "@docusaurus/Link";
|
import Link from "@docusaurus/Link";
|
||||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||||
import styles from "./index.module.css";
|
|
||||||
import HomepageFeatures from "@site/src/components/Homepage";
|
import HomepageFeatures from "@site/src/components/Homepage";
|
||||||
|
import Layout from "@theme/Layout";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
function HomepageHeader() {
|
function HomepageHeader() {
|
||||||
const { siteConfig } = useDocusaurusContext();
|
const { siteConfig } = useDocusaurusContext();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Import the original mapper
|
// Import the original mapper
|
||||||
import MDXComponents from "@theme-original/MDXComponents";
|
|
||||||
import Highlight from "@site/src/components/Highlight";
|
import Highlight from "@site/src/components/Highlight";
|
||||||
|
import MDXComponents from "@theme-original/MDXComponents";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// Re-use the default mapping
|
// Re-use the default mapping
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const ExcalidrawScope = {
|
|||||||
initialData,
|
initialData,
|
||||||
useI18n: ExcalidrawComp.useI18n,
|
useI18n: ExcalidrawComp.useI18n,
|
||||||
convertToExcalidrawElements: ExcalidrawComp.convertToExcalidrawElements,
|
convertToExcalidrawElements: ExcalidrawComp.convertToExcalidrawElements,
|
||||||
|
CaptureUpdateAction: ExcalidrawComp.CaptureUpdateAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ExcalidrawScope;
|
export default ExcalidrawScope;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
excalidraw:
|
excalidraw:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm && yarn copy:assets",
|
"build:packages": "yarn --cwd ../../ build:packages",
|
||||||
|
"build:workspace": "yarn build:packages && yarn copy:assets",
|
||||||
"copy:assets": "cp -r ../../packages/excalidraw/dist/prod/fonts ./public",
|
"copy:assets": "cp -r ../../packages/excalidraw/dist/prod/fonts ./public",
|
||||||
"dev": "yarn build:workspace && next dev -p 3005",
|
"dev": "yarn build:workspace && next dev -p 3005",
|
||||||
"build": "yarn build:workspace && next build",
|
"build": "yarn build:workspace && next build",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
|
|
||||||
import "../common.scss";
|
import "../common.scss";
|
||||||
|
|
||||||
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import * as excalidrawLib from "@excalidraw/excalidraw";
|
import * as excalidrawLib from "@excalidraw/excalidraw";
|
||||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
import App from "../../with-script-in-browser/components/ExampleApp";
|
|
||||||
|
|
||||||
import "@excalidraw/excalidraw/index.css";
|
import "@excalidraw/excalidraw/index.css";
|
||||||
|
|
||||||
|
import App from "../../with-script-in-browser/components/ExampleApp";
|
||||||
|
|
||||||
const ExcalidrawWrapper: React.FC = () => {
|
const ExcalidrawWrapper: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
import "../common.scss";
|
import "../common.scss";
|
||||||
|
|
||||||
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.excalidraw .panelColumn {
|
.excalidraw .selected-shape-actions {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { nanoid } from "nanoid";
|
||||||
import React, {
|
import React, {
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
@@ -6,13 +7,24 @@ import React, {
|
|||||||
Children,
|
Children,
|
||||||
cloneElement,
|
cloneElement,
|
||||||
} from "react";
|
} from "react";
|
||||||
import ExampleSidebar from "./sidebar/ExampleSidebar";
|
|
||||||
|
|
||||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||||
|
import type { ImportedLibraryData } from "@excalidraw/excalidraw/data/types";
|
||||||
|
import type {
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
Theme,
|
||||||
|
} from "@excalidraw/excalidraw/element/types";
|
||||||
|
import type {
|
||||||
|
AppState,
|
||||||
|
BinaryFileData,
|
||||||
|
ExcalidrawImperativeAPI,
|
||||||
|
ExcalidrawInitialDataState,
|
||||||
|
Gesture,
|
||||||
|
LibraryItems,
|
||||||
|
PointerDownState as ExcalidrawPointerDownState,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { nanoid } from "nanoid";
|
import initialData from "../initialData";
|
||||||
|
|
||||||
import type { ResolvablePromise } from "../utils";
|
|
||||||
import {
|
import {
|
||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
distance2d,
|
distance2d,
|
||||||
@@ -23,25 +35,12 @@ import {
|
|||||||
|
|
||||||
import CustomFooter from "./CustomFooter";
|
import CustomFooter from "./CustomFooter";
|
||||||
import MobileFooter from "./MobileFooter";
|
import MobileFooter from "./MobileFooter";
|
||||||
import initialData from "../initialData";
|
import ExampleSidebar from "./sidebar/ExampleSidebar";
|
||||||
|
|
||||||
import type {
|
|
||||||
AppState,
|
|
||||||
BinaryFileData,
|
|
||||||
ExcalidrawImperativeAPI,
|
|
||||||
ExcalidrawInitialDataState,
|
|
||||||
Gesture,
|
|
||||||
LibraryItems,
|
|
||||||
PointerDownState as ExcalidrawPointerDownState,
|
|
||||||
} from "@excalidraw/excalidraw/types";
|
|
||||||
import type {
|
|
||||||
NonDeletedExcalidrawElement,
|
|
||||||
Theme,
|
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
|
||||||
import type { ImportedLibraryData } from "@excalidraw/excalidraw/data/types";
|
|
||||||
|
|
||||||
import "./ExampleApp.scss";
|
import "./ExampleApp.scss";
|
||||||
|
|
||||||
|
import type { ResolvablePromise } from "../utils";
|
||||||
|
|
||||||
type Comment = {
|
type Comment = {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
@@ -105,6 +104,7 @@ export default function ExampleApp({
|
|||||||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
||||||
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
||||||
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
||||||
|
const [renderScrollbars, setRenderScrollbars] = useState(false);
|
||||||
const [blobUrl, setBlobUrl] = useState<string>("");
|
const [blobUrl, setBlobUrl] = useState<string>("");
|
||||||
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
||||||
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
||||||
@@ -193,6 +193,7 @@ export default function ExampleApp({
|
|||||||
}) => setPointerData(payload),
|
}) => setPointerData(payload),
|
||||||
viewModeEnabled,
|
viewModeEnabled,
|
||||||
zenModeEnabled,
|
zenModeEnabled,
|
||||||
|
renderScrollbars,
|
||||||
gridModeEnabled,
|
gridModeEnabled,
|
||||||
theme,
|
theme,
|
||||||
name: "Custom name of drawing",
|
name: "Custom name of drawing",
|
||||||
@@ -711,6 +712,14 @@ export default function ExampleApp({
|
|||||||
/>
|
/>
|
||||||
Grid mode
|
Grid mode
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={renderScrollbars}
|
||||||
|
onChange={() => setRenderScrollbars(!renderScrollbars)}
|
||||||
|
/>
|
||||||
|
Render scrollbars
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
|
||||||
import CustomFooter from "./CustomFooter";
|
|
||||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||||
|
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import CustomFooter from "./CustomFooter";
|
||||||
|
|
||||||
const MobileFooter = ({
|
const MobileFooter = ({
|
||||||
excalidrawAPI,
|
excalidrawAPI,
|
||||||
@@ -10,10 +12,10 @@ const MobileFooter = ({
|
|||||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||||
excalidrawLib: typeof TExcalidraw;
|
excalidrawLib: typeof TExcalidraw;
|
||||||
}) => {
|
}) => {
|
||||||
const { useDevice, Footer } = excalidrawLib;
|
const { useEditorInterface, Footer } = excalidrawLib;
|
||||||
|
|
||||||
const device = useDevice();
|
const editorInterface = useEditorInterface();
|
||||||
if (device.editor.isMobile) {
|
if (editorInterface.formFactor === "phone") {
|
||||||
return (
|
return (
|
||||||
<Footer>
|
<Footer>
|
||||||
<CustomFooter
|
<CustomFooter
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import "./ExampleSidebar.scss";
|
import "./ExampleSidebar.scss";
|
||||||
|
|
||||||
export default function Sidebar({ children }: { children: React.ReactNode }) {
|
export default function Sidebar({ children }: { children: React.ReactNode }) {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import App from "./components/ExampleApp";
|
|
||||||
import React, { StrictMode } from "react";
|
import React, { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
import "@excalidraw/excalidraw/index.css";
|
||||||
|
|
||||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
import "@excalidraw/excalidraw/index.css";
|
import App from "./components/ExampleApp";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ExcalidrawElementSkeleton } from "@excalidraw/excalidraw/data/transform";
|
import type { ExcalidrawElementSkeleton } from "@excalidraw/excalidraw/element/transform";
|
||||||
import type { FileId } from "@excalidraw/excalidraw/element/types";
|
import type { FileId } from "@excalidraw/excalidraw/element/types";
|
||||||
|
|
||||||
const elements: ExcalidrawElementSkeleton[] = [
|
const elements: ExcalidrawElementSkeleton[] = [
|
||||||
|
|||||||
@@ -3,19 +3,20 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "19.0.0",
|
|
||||||
"react-dom": "19.0.0",
|
|
||||||
"@excalidraw/excalidraw": "*",
|
"@excalidraw/excalidraw": "*",
|
||||||
"browser-fs-access": "0.29.1"
|
"browser-fs-access": "0.38.0",
|
||||||
|
"react": "19.0.0",
|
||||||
|
"react-dom": "19.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "5.0.12",
|
"typescript": "^5",
|
||||||
"typescript": "^5"
|
"vite": "5.0.12"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:preview": "yarn build && vite preview --port 5002",
|
"preview": "vite preview --port 5002",
|
||||||
"build:package": "yarn workspace @excalidraw/excalidraw run build:esm"
|
"build:preview": "yarn build && yarn preview",
|
||||||
|
"build:packages": "yarn --cwd ../../ build:packages"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { unstable_batchedUpdates } from "react-dom";
|
|
||||||
import { fileOpen as _fileOpen } from "browser-fs-access";
|
|
||||||
import { MIME_TYPES } from "@excalidraw/excalidraw";
|
import { MIME_TYPES } from "@excalidraw/excalidraw";
|
||||||
|
import { fileOpen as _fileOpen } from "browser-fs-access";
|
||||||
|
import { unstable_batchedUpdates } from "react-dom";
|
||||||
|
|
||||||
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
|
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
|
||||||
|
|
||||||
const INPUT_CHANGE_INTERVAL_MS = 500;
|
|
||||||
|
|
||||||
export type ResolvablePromise<T> = Promise<T> & {
|
export type ResolvablePromise<T> = Promise<T> & {
|
||||||
resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void;
|
resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void;
|
||||||
reject: (error: Error) => void;
|
reject: (error: Error) => void;
|
||||||
@@ -54,40 +52,6 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
|||||||
extensions,
|
extensions,
|
||||||
mimeTypes,
|
mimeTypes,
|
||||||
multiple: opts.multiple ?? false,
|
multiple: opts.multiple ?? false,
|
||||||
legacySetup: (resolve, reject, input) => {
|
|
||||||
const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS);
|
|
||||||
const focusHandler = () => {
|
|
||||||
checkForFile();
|
|
||||||
document.addEventListener("keyup", scheduleRejection);
|
|
||||||
document.addEventListener("pointerup", scheduleRejection);
|
|
||||||
scheduleRejection();
|
|
||||||
};
|
|
||||||
const checkForFile = () => {
|
|
||||||
// this hack might not work when expecting multiple files
|
|
||||||
if (input.files?.length) {
|
|
||||||
const ret = opts.multiple ? [...input.files] : input.files[0];
|
|
||||||
resolve(ret as RetType);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
window.addEventListener("focus", focusHandler);
|
|
||||||
});
|
|
||||||
const interval = window.setInterval(() => {
|
|
||||||
checkForFile();
|
|
||||||
}, INPUT_CHANGE_INTERVAL_MS);
|
|
||||||
return (rejectPromise) => {
|
|
||||||
clearInterval(interval);
|
|
||||||
scheduleRejection.cancel();
|
|
||||||
window.removeEventListener("focus", focusHandler);
|
|
||||||
document.removeEventListener("keyup", scheduleRejection);
|
|
||||||
document.removeEventListener("pointerup", scheduleRejection);
|
|
||||||
if (rejectPromise) {
|
|
||||||
// so that something is shown in console if we need to debug this
|
|
||||||
console.warn("Opening the file was canceled (legacy-fs).");
|
|
||||||
rejectPromise(new Error("Request Aborted"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}) as Promise<RetType>;
|
}) as Promise<RetType>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"outputDirectory": "dist",
|
"outputDirectory": "dist",
|
||||||
"installCommand": "yarn install",
|
"installCommand": "yarn install",
|
||||||
"buildCommand": "yarn build:package && yarn build"
|
"buildCommand": "yarn build:packages && yarn build"
|
||||||
}
|
}
|
||||||
|
|||||||
+267
-143
@@ -1,40 +1,28 @@
|
|||||||
import polyfill from "@excalidraw/excalidraw/polyfill";
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
|
||||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
|
||||||
import { getDefaultAppState } from "@excalidraw/excalidraw/appState";
|
|
||||||
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
|
|
||||||
import { TopErrorBoundary } from "./components/TopErrorBoundary";
|
|
||||||
import {
|
|
||||||
APP_NAME,
|
|
||||||
EVENT,
|
|
||||||
THEME,
|
|
||||||
TITLE_TIMEOUT,
|
|
||||||
VERSION_TIMEOUT,
|
|
||||||
} from "@excalidraw/excalidraw/constants";
|
|
||||||
import { loadFromBlob } from "@excalidraw/excalidraw/data/blob";
|
|
||||||
import type {
|
|
||||||
FileId,
|
|
||||||
NonDeletedExcalidrawElement,
|
|
||||||
OrderedExcalidrawElement,
|
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
|
||||||
import { useCallbackRefState } from "@excalidraw/excalidraw/hooks/useCallbackRefState";
|
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
|
||||||
import {
|
import {
|
||||||
Excalidraw,
|
Excalidraw,
|
||||||
LiveCollaborationTrigger,
|
LiveCollaborationTrigger,
|
||||||
TTDDialogTrigger,
|
TTDDialogTrigger,
|
||||||
CaptureUpdateAction,
|
CaptureUpdateAction,
|
||||||
reconcileElements,
|
reconcileElements,
|
||||||
|
useEditorInterface,
|
||||||
|
ExcalidrawAPIProvider,
|
||||||
|
useExcalidrawAPI,
|
||||||
} from "@excalidraw/excalidraw";
|
} from "@excalidraw/excalidraw";
|
||||||
import type {
|
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||||
AppState,
|
import { getDefaultAppState } from "@excalidraw/excalidraw/appState";
|
||||||
ExcalidrawImperativeAPI,
|
|
||||||
BinaryFiles,
|
|
||||||
ExcalidrawInitialDataState,
|
|
||||||
UIAppState,
|
|
||||||
} from "@excalidraw/excalidraw/types";
|
|
||||||
import type { ResolvablePromise } from "@excalidraw/excalidraw/utils";
|
|
||||||
import {
|
import {
|
||||||
|
CommandPalette,
|
||||||
|
DEFAULT_CATEGORIES,
|
||||||
|
} from "@excalidraw/excalidraw/components/CommandPalette/CommandPalette";
|
||||||
|
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
|
||||||
|
import { OverwriteConfirmDialog } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
|
||||||
|
import { openConfirmModal } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
|
||||||
|
import { ShareableLinkDialog } from "@excalidraw/excalidraw/components/ShareableLinkDialog";
|
||||||
|
import Trans from "@excalidraw/excalidraw/components/Trans";
|
||||||
|
import {
|
||||||
|
APP_NAME,
|
||||||
|
EVENT,
|
||||||
|
VERSION_TIMEOUT,
|
||||||
debounce,
|
debounce,
|
||||||
getVersion,
|
getVersion,
|
||||||
getFrame,
|
getFrame,
|
||||||
@@ -42,75 +30,13 @@ import {
|
|||||||
preventUnload,
|
preventUnload,
|
||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
isRunningInIframe,
|
isRunningInIframe,
|
||||||
} from "@excalidraw/excalidraw/utils";
|
isDevEnv,
|
||||||
import {
|
} from "@excalidraw/common";
|
||||||
FIREBASE_STORAGE_PREFIXES,
|
import polyfill from "@excalidraw/excalidraw/polyfill";
|
||||||
isExcalidrawPlusSignedUser,
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
STORAGE_KEYS,
|
import { loadFromBlob } from "@excalidraw/excalidraw/data/blob";
|
||||||
SYNC_BROWSER_TABS_TIMEOUT,
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
} from "./app_constants";
|
|
||||||
import type { CollabAPI } from "./collab/Collab";
|
|
||||||
import Collab, {
|
|
||||||
collabAPIAtom,
|
|
||||||
isCollaboratingAtom,
|
|
||||||
isOfflineAtom,
|
|
||||||
} from "./collab/Collab";
|
|
||||||
import {
|
|
||||||
exportToBackend,
|
|
||||||
getCollaborationLinkData,
|
|
||||||
isCollaborationLink,
|
|
||||||
loadScene,
|
|
||||||
} from "./data";
|
|
||||||
import {
|
|
||||||
importFromLocalStorage,
|
|
||||||
importUsernameFromLocalStorage,
|
|
||||||
} from "./data/localStorage";
|
|
||||||
import CustomStats from "./CustomStats";
|
|
||||||
import type { RestoredDataState } from "@excalidraw/excalidraw/data/restore";
|
|
||||||
import { restore, restoreAppState } from "@excalidraw/excalidraw/data/restore";
|
|
||||||
import {
|
|
||||||
ExportToExcalidrawPlus,
|
|
||||||
exportToExcalidrawPlus,
|
|
||||||
} from "./components/ExportToExcalidrawPlus";
|
|
||||||
import { updateStaleImageStatuses } from "./data/FileManager";
|
|
||||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
|
|
||||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
|
|
||||||
import { loadFilesFromFirebase } from "./data/firebase";
|
|
||||||
import {
|
|
||||||
LibraryIndexedDBAdapter,
|
|
||||||
LibraryLocalStorageMigrationAdapter,
|
|
||||||
LocalData,
|
|
||||||
} from "./data/LocalData";
|
|
||||||
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import {
|
|
||||||
parseLibraryTokensFromUrl,
|
|
||||||
useHandleLibrary,
|
|
||||||
} from "@excalidraw/excalidraw/data/library";
|
|
||||||
import { AppMainMenu } from "./components/AppMainMenu";
|
|
||||||
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
|
||||||
import { AppFooter } from "./components/AppFooter";
|
|
||||||
import {
|
|
||||||
Provider,
|
|
||||||
useAtom,
|
|
||||||
useAtomValue,
|
|
||||||
useAtomWithInitialValue,
|
|
||||||
appJotaiStore,
|
|
||||||
} from "./app-jotai";
|
|
||||||
|
|
||||||
import "./index.scss";
|
|
||||||
import type { ResolutionType } from "@excalidraw/excalidraw/utility-types";
|
|
||||||
import { ShareableLinkDialog } from "@excalidraw/excalidraw/components/ShareableLinkDialog";
|
|
||||||
import { openConfirmModal } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
|
|
||||||
import { OverwriteConfirmDialog } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
|
|
||||||
import Trans from "@excalidraw/excalidraw/components/Trans";
|
|
||||||
import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
|
|
||||||
import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
|
|
||||||
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
|
|
||||||
import {
|
|
||||||
CommandPalette,
|
|
||||||
DEFAULT_CATEGORIES,
|
|
||||||
} from "@excalidraw/excalidraw/components/CommandPalette/CommandPalette";
|
|
||||||
import {
|
import {
|
||||||
GithubIcon,
|
GithubIcon,
|
||||||
XBrandIcon,
|
XBrandIcon,
|
||||||
@@ -121,6 +47,90 @@ import {
|
|||||||
share,
|
share,
|
||||||
youtubeIcon,
|
youtubeIcon,
|
||||||
} from "@excalidraw/excalidraw/components/icons";
|
} from "@excalidraw/excalidraw/components/icons";
|
||||||
|
import { isElementLink } from "@excalidraw/element";
|
||||||
|
import {
|
||||||
|
bumpElementVersions,
|
||||||
|
restoreAppState,
|
||||||
|
restoreElements,
|
||||||
|
} from "@excalidraw/excalidraw/data/restore";
|
||||||
|
import { newElementWith } from "@excalidraw/element";
|
||||||
|
import { isInitializedImageElement } from "@excalidraw/element";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import {
|
||||||
|
parseLibraryTokensFromUrl,
|
||||||
|
useHandleLibrary,
|
||||||
|
} from "@excalidraw/excalidraw/data/library";
|
||||||
|
|
||||||
|
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
|
||||||
|
import type { RestoredDataState } from "@excalidraw/excalidraw/data/restore";
|
||||||
|
import type {
|
||||||
|
FileId,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
import type {
|
||||||
|
AppState,
|
||||||
|
ExcalidrawImperativeAPI,
|
||||||
|
BinaryFiles,
|
||||||
|
ExcalidrawInitialDataState,
|
||||||
|
UIAppState,
|
||||||
|
ExcalidrawProps,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
import type { ResolutionType } from "@excalidraw/common/utility-types";
|
||||||
|
import type { ResolvablePromise } from "@excalidraw/common/utils";
|
||||||
|
|
||||||
|
import CustomStats from "./CustomStats";
|
||||||
|
import {
|
||||||
|
Provider,
|
||||||
|
useAtom,
|
||||||
|
useAtomValue,
|
||||||
|
useAtomWithInitialValue,
|
||||||
|
appJotaiStore,
|
||||||
|
} from "./app-jotai";
|
||||||
|
import {
|
||||||
|
FIREBASE_STORAGE_PREFIXES,
|
||||||
|
isExcalidrawPlusSignedUser,
|
||||||
|
STORAGE_KEYS,
|
||||||
|
SYNC_BROWSER_TABS_TIMEOUT,
|
||||||
|
} from "./app_constants";
|
||||||
|
import Collab, {
|
||||||
|
collabAPIAtom,
|
||||||
|
isCollaboratingAtom,
|
||||||
|
isOfflineAtom,
|
||||||
|
} from "./collab/Collab";
|
||||||
|
import { AppFooter } from "./components/AppFooter";
|
||||||
|
import { AppMainMenu } from "./components/AppMainMenu";
|
||||||
|
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
||||||
|
import {
|
||||||
|
ExportToExcalidrawPlus,
|
||||||
|
exportToExcalidrawPlus,
|
||||||
|
} from "./components/ExportToExcalidrawPlus";
|
||||||
|
import { TopErrorBoundary } from "./components/TopErrorBoundary";
|
||||||
|
|
||||||
|
import {
|
||||||
|
exportToBackend,
|
||||||
|
getCollaborationLinkData,
|
||||||
|
importFromBackend,
|
||||||
|
isCollaborationLink,
|
||||||
|
} from "./data";
|
||||||
|
|
||||||
|
import { updateStaleImageStatuses } from "./data/FileManager";
|
||||||
|
import { FileStatusStore } from "./data/fileStatusStore";
|
||||||
|
import {
|
||||||
|
importFromLocalStorage,
|
||||||
|
importUsernameFromLocalStorage,
|
||||||
|
} from "./data/localStorage";
|
||||||
|
|
||||||
|
import { loadFilesFromFirebase } from "./data/firebase";
|
||||||
|
import {
|
||||||
|
LibraryIndexedDBAdapter,
|
||||||
|
LibraryLocalStorageMigrationAdapter,
|
||||||
|
LocalData,
|
||||||
|
localStorageQuotaExceededAtom,
|
||||||
|
} from "./data/LocalData";
|
||||||
|
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
||||||
|
import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
|
||||||
|
import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
|
||||||
import { useHandleAppTheme } from "./useHandleAppTheme";
|
import { useHandleAppTheme } from "./useHandleAppTheme";
|
||||||
import { getPreferredLanguage } from "./app-language/language-detector";
|
import { getPreferredLanguage } from "./app-language/language-detector";
|
||||||
import { useAppLangCode } from "./app-language/language-state";
|
import { useAppLangCode } from "./app-language/language-state";
|
||||||
@@ -131,7 +141,13 @@ import DebugCanvas, {
|
|||||||
} from "./components/DebugCanvas";
|
} from "./components/DebugCanvas";
|
||||||
import { AIComponents } from "./components/AI";
|
import { AIComponents } from "./components/AI";
|
||||||
import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";
|
import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";
|
||||||
import { isElementLink } from "@excalidraw/excalidraw/element/elementLink";
|
|
||||||
|
import "./index.scss";
|
||||||
|
|
||||||
|
import { ExcalidrawPlusPromoBanner } from "./components/ExcalidrawPlusPromoBanner";
|
||||||
|
import { AppSidebar } from "./components/AppSidebar";
|
||||||
|
|
||||||
|
import type { CollabAPI } from "./collab/Collab";
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
@@ -214,9 +230,20 @@ const initializeScene = async (opts: {
|
|||||||
|
|
||||||
const localDataState = importFromLocalStorage();
|
const localDataState = importFromLocalStorage();
|
||||||
|
|
||||||
let scene: RestoredDataState & {
|
let scene: Omit<
|
||||||
|
RestoredDataState,
|
||||||
|
// we're not storing files in the scene database/localStorage, and instead
|
||||||
|
// fetch them async from a different store
|
||||||
|
"files"
|
||||||
|
> & {
|
||||||
scrollToContent?: boolean;
|
scrollToContent?: boolean;
|
||||||
} = await loadScene(null, null, localDataState);
|
} = {
|
||||||
|
elements: restoreElements(localDataState?.elements, null, {
|
||||||
|
repairBindings: true,
|
||||||
|
deleteInvisibleElements: true,
|
||||||
|
}),
|
||||||
|
appState: restoreAppState(localDataState?.appState, null),
|
||||||
|
};
|
||||||
|
|
||||||
let roomLinkData = getCollaborationLinkData(window.location.href);
|
let roomLinkData = getCollaborationLinkData(window.location.href);
|
||||||
const isExternalScene = !!(id || jsonBackendMatch || roomLinkData);
|
const isExternalScene = !!(id || jsonBackendMatch || roomLinkData);
|
||||||
@@ -230,11 +257,26 @@ const initializeScene = async (opts: {
|
|||||||
(await openConfirmModal(shareableLinkConfirmDialog))
|
(await openConfirmModal(shareableLinkConfirmDialog))
|
||||||
) {
|
) {
|
||||||
if (jsonBackendMatch) {
|
if (jsonBackendMatch) {
|
||||||
scene = await loadScene(
|
const imported = await importFromBackend(
|
||||||
jsonBackendMatch[1],
|
jsonBackendMatch[1],
|
||||||
jsonBackendMatch[2],
|
jsonBackendMatch[2],
|
||||||
localDataState,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
scene = {
|
||||||
|
elements: bumpElementVersions(
|
||||||
|
restoreElements(imported.elements, null, {
|
||||||
|
repairBindings: true,
|
||||||
|
deleteInvisibleElements: true,
|
||||||
|
}),
|
||||||
|
localDataState?.elements,
|
||||||
|
),
|
||||||
|
appState: restoreAppState(
|
||||||
|
imported.appState,
|
||||||
|
// local appState when importing from backend to ensure we restore
|
||||||
|
// localStorage user settings which we do not persist on server.
|
||||||
|
localDataState?.appState,
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
scene.scrollToContent = true;
|
scene.scrollToContent = true;
|
||||||
if (!roomLinkData) {
|
if (!roomLinkData) {
|
||||||
@@ -329,6 +371,8 @@ const initializeScene = async (opts: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ExcalidrawWrapper = () => {
|
const ExcalidrawWrapper = () => {
|
||||||
|
const excalidrawAPI = useExcalidrawAPI();
|
||||||
|
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const isCollabDisabled = isRunningInIframe();
|
const isCollabDisabled = isRunningInIframe();
|
||||||
|
|
||||||
@@ -336,6 +380,8 @@ const ExcalidrawWrapper = () => {
|
|||||||
|
|
||||||
const [langCode, setLangCode] = useAppLangCode();
|
const [langCode, setLangCode] = useAppLangCode();
|
||||||
|
|
||||||
|
const editorInterface = useEditorInterface();
|
||||||
|
|
||||||
// initial state
|
// initial state
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -357,9 +403,6 @@ const ExcalidrawWrapper = () => {
|
|||||||
}, VERSION_TIMEOUT);
|
}, VERSION_TIMEOUT);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [excalidrawAPI, excalidrawRefCallback] =
|
|
||||||
useCallbackRefState<ExcalidrawImperativeAPI>();
|
|
||||||
|
|
||||||
const [, setShareDialogState] = useAtom(shareDialogStateAtom);
|
const [, setShareDialogState] = useAtom(shareDialogStateAtom);
|
||||||
const [collabAPI] = useAtom(collabAPIAtom);
|
const [collabAPI] = useAtom(collabAPIAtom);
|
||||||
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
|
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
|
||||||
@@ -377,7 +420,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
const [, forceRefresh] = useState(false);
|
const [, forceRefresh] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (import.meta.env.DEV) {
|
if (isDevEnv()) {
|
||||||
const debugState = loadSavedDebugState();
|
const debugState = loadSavedDebugState();
|
||||||
|
|
||||||
if (debugState.enabled && !window.visualDebug) {
|
if (debugState.enabled && !window.visualDebug) {
|
||||||
@@ -391,18 +434,15 @@ const ExcalidrawWrapper = () => {
|
|||||||
}
|
}
|
||||||
}, [excalidrawAPI]);
|
}, [excalidrawAPI]);
|
||||||
|
|
||||||
useEffect(() => {
|
// ---------------------------------------------------------------------------
|
||||||
if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) {
|
// Hoisted loadImages
|
||||||
return;
|
// ---------------------------------------------------------------------------
|
||||||
}
|
const loadImages = useCallback(
|
||||||
|
(data: ResolutionType<typeof initializeScene>, isInitialLoad = false) => {
|
||||||
const loadImages = (
|
if (!data.scene || !excalidrawAPI) {
|
||||||
data: ResolutionType<typeof initializeScene>,
|
|
||||||
isInitialLoad = false,
|
|
||||||
) => {
|
|
||||||
if (!data.scene) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collabAPI?.isCollaborating()) {
|
if (collabAPI?.isCollaborating()) {
|
||||||
if (data.scene.elements) {
|
if (data.scene.elements) {
|
||||||
collabAPI
|
collabAPI
|
||||||
@@ -429,6 +469,12 @@ const ExcalidrawWrapper = () => {
|
|||||||
}, [] as FileId[]) || [];
|
}, [] as FileId[]) || [];
|
||||||
|
|
||||||
if (data.isExternalScene) {
|
if (data.isExternalScene) {
|
||||||
|
if (fileIds.length) {
|
||||||
|
// Direct Firebase call (not through FileManager), so track manually
|
||||||
|
FileStatusStore.updateStatuses(
|
||||||
|
fileIds.map((id) => [id, "loading"]),
|
||||||
|
);
|
||||||
|
}
|
||||||
loadFilesFromFirebase(
|
loadFilesFromFirebase(
|
||||||
`${FIREBASE_STORAGE_PREFIXES.shareLinkFiles}/${data.id}`,
|
`${FIREBASE_STORAGE_PREFIXES.shareLinkFiles}/${data.id}`,
|
||||||
data.key,
|
data.key,
|
||||||
@@ -440,12 +486,18 @@ const ExcalidrawWrapper = () => {
|
|||||||
erroredFiles,
|
erroredFiles,
|
||||||
elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
|
elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||||
});
|
});
|
||||||
|
FileStatusStore.updateStatuses([
|
||||||
|
...loadedFiles.map((f) => [f.id, "loaded"] as [FileId, "loaded"]),
|
||||||
|
...[...erroredFiles.keys()].map(
|
||||||
|
(id) => [id, "error"] as [FileId, "error"],
|
||||||
|
),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
} else if (isInitialLoad) {
|
} else if (isInitialLoad) {
|
||||||
if (fileIds.length) {
|
if (fileIds.length) {
|
||||||
LocalData.fileStorage
|
LocalData.fileStorage
|
||||||
.getFiles(fileIds)
|
.getFiles(fileIds)
|
||||||
.then(({ loadedFiles, erroredFiles }) => {
|
.then(async ({ loadedFiles, erroredFiles }) => {
|
||||||
if (loadedFiles.length) {
|
if (loadedFiles.length) {
|
||||||
excalidrawAPI.addFiles(loadedFiles);
|
excalidrawAPI.addFiles(loadedFiles);
|
||||||
}
|
}
|
||||||
@@ -458,10 +510,19 @@ const ExcalidrawWrapper = () => {
|
|||||||
}
|
}
|
||||||
// on fresh load, clear unused files from IDB (from previous
|
// on fresh load, clear unused files from IDB (from previous
|
||||||
// session)
|
// session)
|
||||||
LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds });
|
LocalData.fileStorage.clearObsoleteFiles({
|
||||||
|
currentFileIds: fileIds,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[collabAPI, excalidrawAPI],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => {
|
initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => {
|
||||||
loadImages(data, /* isInitialLoad */ true);
|
loadImages(data, /* isInitialLoad */ true);
|
||||||
@@ -484,8 +545,10 @@ const ExcalidrawWrapper = () => {
|
|||||||
loadImages(data);
|
loadImages(data);
|
||||||
if (data.scene) {
|
if (data.scene) {
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
...data.scene,
|
elements: restoreElements(data.scene.elements, null, {
|
||||||
...restore(data.scene, null, null, { repairBindings: true }),
|
repairBindings: true,
|
||||||
|
}),
|
||||||
|
appState: restoreAppState(data.scene.appState, null),
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -493,11 +556,6 @@ const ExcalidrawWrapper = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const titleTimeout = setTimeout(
|
|
||||||
() => (document.title = APP_NAME),
|
|
||||||
TITLE_TIMEOUT,
|
|
||||||
);
|
|
||||||
|
|
||||||
const syncData = debounce(() => {
|
const syncData = debounce(() => {
|
||||||
if (isTestEnv()) {
|
if (isTestEnv()) {
|
||||||
return;
|
return;
|
||||||
@@ -588,9 +646,8 @@ const ExcalidrawWrapper = () => {
|
|||||||
visibilityChange,
|
visibilityChange,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
clearTimeout(titleTimeout);
|
|
||||||
};
|
};
|
||||||
}, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode]);
|
}, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode, loadImages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unloadHandler = (event: BeforeUnloadEvent) => {
|
const unloadHandler = (event: BeforeUnloadEvent) => {
|
||||||
@@ -602,7 +659,13 @@ const ExcalidrawWrapper = () => {
|
|||||||
excalidrawAPI.getSceneElements(),
|
excalidrawAPI.getSceneElements(),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
preventUnload(event);
|
if (import.meta.env.VITE_APP_DISABLE_PREVENT_UNLOAD !== "true") {
|
||||||
|
preventUnload(event);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
"preventing unload disabled (VITE_APP_DISABLE_PREVENT_UNLOAD)",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
|
window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
|
||||||
@@ -657,8 +720,8 @@ const ExcalidrawWrapper = () => {
|
|||||||
debugRenderer(
|
debugRenderer(
|
||||||
debugCanvasRef.current,
|
debugCanvasRef.current,
|
||||||
appState,
|
appState,
|
||||||
|
elements,
|
||||||
window.devicePixelRatio,
|
window.devicePixelRatio,
|
||||||
() => forceRefresh((prev) => !prev),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -722,11 +785,63 @@ const ExcalidrawWrapper = () => {
|
|||||||
|
|
||||||
const isOffline = useAtomValue(isOfflineAtom);
|
const isOffline = useAtomValue(isOfflineAtom);
|
||||||
|
|
||||||
|
const localStorageQuotaExceeded = useAtomValue(localStorageQuotaExceededAtom);
|
||||||
|
|
||||||
const onCollabDialogOpen = useCallback(
|
const onCollabDialogOpen = useCallback(
|
||||||
() => setShareDialogState({ isOpen: true, type: "collaborationOnly" }),
|
() => setShareDialogState({ isOpen: true, type: "collaborationOnly" }),
|
||||||
[setShareDialogState],
|
[setShareDialogState],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// onExport — intercepts file save to wait for pending image loads
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const onExport: Required<ExcalidrawProps>["onExport"] = useCallback(
|
||||||
|
async function* () {
|
||||||
|
let snapshot = FileStatusStore.getSnapshot();
|
||||||
|
const { pending, total } = FileStatusStore.getPendingCount(
|
||||||
|
snapshot.value,
|
||||||
|
);
|
||||||
|
if (pending === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yield initial progress
|
||||||
|
yield {
|
||||||
|
type: "progress",
|
||||||
|
progress: (total - pending) / total,
|
||||||
|
message: `Loading images (${total - pending}/${total})...`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wait for all pending images to finish
|
||||||
|
while (true) {
|
||||||
|
snapshot = await FileStatusStore.pull(snapshot.version);
|
||||||
|
const { pending: nowPending, total: nowTotal } =
|
||||||
|
FileStatusStore.getPendingCount(snapshot.value);
|
||||||
|
|
||||||
|
yield {
|
||||||
|
type: "progress",
|
||||||
|
progress: (nowTotal - nowPending) / nowTotal,
|
||||||
|
message: `Loading images (${nowTotal - nowPending}/${nowTotal})...`,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (nowPending === 0) {
|
||||||
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
yield {
|
||||||
|
type: "progress",
|
||||||
|
message: `Preparing export...`,
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// const onExport = () => {
|
||||||
|
// return new Promise((r) => setTimeout(r, 2500));
|
||||||
|
// // console.log("onExport");
|
||||||
|
// };
|
||||||
|
|
||||||
// browsers generally prevent infinite self-embedding, there are
|
// browsers generally prevent infinite self-embedding, there are
|
||||||
// cases where it still happens, and while we disallow self-embedding
|
// cases where it still happens, and while we disallow self-embedding
|
||||||
// by not whitelisting our own origin, this serves as an additional guard
|
// by not whitelisting our own origin, this serves as an additional guard
|
||||||
@@ -793,8 +908,8 @@ const ExcalidrawWrapper = () => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
excalidrawAPI={excalidrawRefCallback}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
onExport={onExport}
|
||||||
initialData={initialStatePromiseRef.current.promise}
|
initialData={initialStatePromiseRef.current.promise}
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
onPointerUpdate={collabAPI?.onPointerUpdate}
|
onPointerUpdate={collabAPI?.onPointerUpdate}
|
||||||
@@ -836,18 +951,27 @@ const ExcalidrawWrapper = () => {
|
|||||||
handleKeyboardGlobally={true}
|
handleKeyboardGlobally={true}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
theme={editorTheme}
|
theme={editorTheme}
|
||||||
|
onThemeChange={setAppTheme}
|
||||||
renderTopRightUI={(isMobile) => {
|
renderTopRightUI={(isMobile) => {
|
||||||
if (isMobile || !collabAPI || isCollabDisabled) {
|
if (isMobile || !collabAPI || isCollabDisabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="top-right-ui">
|
<div className="excalidraw-ui-top-right">
|
||||||
|
{excalidrawAPI?.getEditorInterface().formFactor === "desktop" && (
|
||||||
|
<ExcalidrawPlusPromoBanner
|
||||||
|
isSignedIn={isExcalidrawPlusSignedUser}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{collabError.message && <CollabError collabError={collabError} />}
|
{collabError.message && <CollabError collabError={collabError} />}
|
||||||
<LiveCollaborationTrigger
|
<LiveCollaborationTrigger
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
onSelect={() =>
|
onSelect={() =>
|
||||||
setShareDialogState({ isOpen: true, type: "share" })
|
setShareDialogState({ isOpen: true, type: "share" })
|
||||||
}
|
}
|
||||||
|
editorInterface={editorInterface}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -864,7 +988,6 @@ const ExcalidrawWrapper = () => {
|
|||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
isCollabEnabled={!isCollabDisabled}
|
isCollabEnabled={!isCollabDisabled}
|
||||||
theme={appTheme}
|
theme={appTheme}
|
||||||
setTheme={(theme) => setAppTheme(theme)}
|
|
||||||
refresh={() => forceRefresh((prev) => !prev)}
|
refresh={() => forceRefresh((prev) => !prev)}
|
||||||
/>
|
/>
|
||||||
<AppWelcomeScreen
|
<AppWelcomeScreen
|
||||||
@@ -896,10 +1019,15 @@ const ExcalidrawWrapper = () => {
|
|||||||
|
|
||||||
<TTDDialogTrigger />
|
<TTDDialogTrigger />
|
||||||
{isCollaborating && isOffline && (
|
{isCollaborating && isOffline && (
|
||||||
<div className="collab-offline-warning">
|
<div className="alertalert--warning">
|
||||||
{t("alerts.collabOfflineWarning")}
|
{t("alerts.collabOfflineWarning")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{localStorageQuotaExceeded && (
|
||||||
|
<div className="alert alert--danger">
|
||||||
|
{t("alerts.localStorageQuotaExceeded")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{latestShareableLink && (
|
{latestShareableLink && (
|
||||||
<ShareableLinkDialog
|
<ShareableLinkDialog
|
||||||
link={latestShareableLink}
|
link={latestShareableLink}
|
||||||
@@ -928,6 +1056,8 @@ const ExcalidrawWrapper = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AppSidebar />
|
||||||
|
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<ErrorDialog onClose={() => setErrorMessage("")}>
|
<ErrorDialog onClose={() => setErrorMessage("")}>
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
@@ -1098,14 +1228,6 @@ const ExcalidrawWrapper = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
...CommandPalette.defaultItems.toggleTheme,
|
|
||||||
perform: () => {
|
|
||||||
setAppTheme(
|
|
||||||
editorTheme === THEME.DARK ? THEME.LIGHT : THEME.DARK,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: t("labels.installPWA"),
|
label: t("labels.installPWA"),
|
||||||
category: DEFAULT_CATEGORIES.app,
|
category: DEFAULT_CATEGORIES.app,
|
||||||
@@ -1145,7 +1267,9 @@ const ExcalidrawApp = () => {
|
|||||||
return (
|
return (
|
||||||
<TopErrorBoundary>
|
<TopErrorBoundary>
|
||||||
<Provider store={appJotaiStore}>
|
<Provider store={appJotaiStore}>
|
||||||
<ExcalidrawWrapper />
|
<ExcalidrawAPIProvider>
|
||||||
|
<ExcalidrawWrapper />
|
||||||
|
</ExcalidrawAPIProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</TopErrorBoundary>
|
</TopErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
|
import { Stats } from "@excalidraw/excalidraw";
|
||||||
|
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
|
||||||
|
import {
|
||||||
|
DEFAULT_VERSION,
|
||||||
|
debounce,
|
||||||
|
getVersion,
|
||||||
|
nFormatter,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { debounce, getVersion, nFormatter } from "@excalidraw/excalidraw/utils";
|
|
||||||
|
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
import type { UIAppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getElementsStorageSize,
|
getElementsStorageSize,
|
||||||
getTotalStorageSize,
|
getTotalStorageSize,
|
||||||
} from "./data/localStorage";
|
} from "./data/localStorage";
|
||||||
import { DEFAULT_VERSION } from "@excalidraw/excalidraw/constants";
|
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
|
||||||
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
|
|
||||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
|
||||||
import type { UIAppState } from "@excalidraw/excalidraw/types";
|
|
||||||
import { Stats } from "@excalidraw/excalidraw";
|
|
||||||
|
|
||||||
type StorageSizes = { scene: number; total: number };
|
type StorageSizes = { scene: number; total: number };
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
import { base64urlToString } from "@excalidraw/excalidraw/data/encode";
|
||||||
|
import { ExcalidrawError } from "@excalidraw/excalidraw/errors";
|
||||||
import { useLayoutEffect, useRef } from "react";
|
import { useLayoutEffect, useRef } from "react";
|
||||||
import { STORAGE_KEYS } from "./app_constants";
|
|
||||||
import { LocalData } from "./data/LocalData";
|
|
||||||
import type {
|
import type {
|
||||||
FileId,
|
FileId,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
import type { AppState, BinaryFileData } from "@excalidraw/excalidraw/types";
|
import type { AppState, BinaryFileData } from "@excalidraw/excalidraw/types";
|
||||||
import { ExcalidrawError } from "@excalidraw/excalidraw/errors";
|
|
||||||
import { base64urlToString } from "@excalidraw/excalidraw/data/encode";
|
import { STORAGE_KEYS } from "./app_constants";
|
||||||
|
import { LocalData } from "./data/LocalData";
|
||||||
|
|
||||||
const EVENT_REQUEST_SCENE = "REQUEST_SCENE";
|
const EVENT_REQUEST_SCENE = "REQUEST_SCENE";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React from "react";
|
|
||||||
import { useI18n, languages } from "@excalidraw/excalidraw/i18n";
|
import { useI18n, languages } from "@excalidraw/excalidraw/i18n";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import { useSetAtom } from "../app-jotai";
|
import { useSetAtom } from "../app-jotai";
|
||||||
|
|
||||||
import { appLangCodeAtom } from "./language-state";
|
import { appLangCodeAtom } from "./language-state";
|
||||||
|
|
||||||
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
|
||||||
import { defaultLang, languages } from "@excalidraw/excalidraw";
|
import { defaultLang, languages } from "@excalidraw/excalidraw";
|
||||||
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
|
|
||||||
export const languageDetector = new LanguageDetector();
|
export const languageDetector = new LanguageDetector();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { atom, useAtom } from "../app-jotai";
|
import { atom, useAtom } from "../app-jotai";
|
||||||
|
|
||||||
import { getPreferredLanguage, languageDetector } from "./language-detector";
|
import { getPreferredLanguage, languageDetector } from "./language-detector";
|
||||||
|
|
||||||
export const appLangCodeAtom = atom(getPreferredLanguage());
|
export const appLangCodeAtom = atom(getPreferredLanguage());
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ export const SYNC_BROWSER_TABS_TIMEOUT = 50;
|
|||||||
export const CURSOR_SYNC_TIMEOUT = 33; // ~30fps
|
export const CURSOR_SYNC_TIMEOUT = 33; // ~30fps
|
||||||
export const DELETED_ELEMENT_TIMEOUT = 24 * 60 * 60 * 1000; // 1 day
|
export const DELETED_ELEMENT_TIMEOUT = 24 * 60 * 60 * 1000; // 1 day
|
||||||
|
|
||||||
export const FILE_UPLOAD_MAX_BYTES = 3 * 1024 * 1024; // 3 MiB
|
// should be aligned with MAX_ALLOWED_FILE_BYTES
|
||||||
|
export const FILE_UPLOAD_MAX_BYTES = 4 * 1024 * 1024; // 4 MiB
|
||||||
// 1 year (https://stackoverflow.com/a/25201898/927631)
|
// 1 year (https://stackoverflow.com/a/25201898/927631)
|
||||||
export const FILE_CACHE_MAX_AGE_SEC = 31536000;
|
export const FILE_CACHE_MAX_AGE_SEC = 31536000;
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ export const STORAGE_KEYS = {
|
|||||||
VERSION_FILES: "version-files",
|
VERSION_FILES: "version-files",
|
||||||
|
|
||||||
IDB_LIBRARY: "excalidraw-library",
|
IDB_LIBRARY: "excalidraw-library",
|
||||||
|
IDB_TTD_CHATS: "excalidraw-ttd-chats",
|
||||||
|
|
||||||
// do not use apart from migrations
|
// do not use apart from migrations
|
||||||
__LEGACY_LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
__LEGACY_LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
||||||
|
|||||||
@@ -1,21 +1,3 @@
|
|||||||
import throttle from "lodash.throttle";
|
|
||||||
import { PureComponent } from "react";
|
|
||||||
import type {
|
|
||||||
BinaryFileData,
|
|
||||||
ExcalidrawImperativeAPI,
|
|
||||||
SocketId,
|
|
||||||
Collaborator,
|
|
||||||
Gesture,
|
|
||||||
} from "@excalidraw/excalidraw/types";
|
|
||||||
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
|
|
||||||
import { APP_NAME, ENV, EVENT } from "@excalidraw/excalidraw/constants";
|
|
||||||
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
|
||||||
import type {
|
|
||||||
ExcalidrawElement,
|
|
||||||
FileId,
|
|
||||||
InitializedExcalidrawImageElement,
|
|
||||||
OrderedExcalidrawElement,
|
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
|
||||||
import {
|
import {
|
||||||
CaptureUpdateAction,
|
CaptureUpdateAction,
|
||||||
getSceneVersion,
|
getSceneVersion,
|
||||||
@@ -23,12 +5,53 @@ import {
|
|||||||
zoomToFitBounds,
|
zoomToFitBounds,
|
||||||
reconcileElements,
|
reconcileElements,
|
||||||
} from "@excalidraw/excalidraw";
|
} from "@excalidraw/excalidraw";
|
||||||
|
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
|
||||||
|
import { APP_NAME, cloneJSON, EVENT, toBrandedType } from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
|
IDLE_THRESHOLD,
|
||||||
|
ACTIVE_THRESHOLD,
|
||||||
|
UserIdleState,
|
||||||
assertNever,
|
assertNever,
|
||||||
|
isDevEnv,
|
||||||
|
isTestEnv,
|
||||||
preventUnload,
|
preventUnload,
|
||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
throttleRAF,
|
throttleRAF,
|
||||||
} from "@excalidraw/excalidraw/utils";
|
} from "@excalidraw/common";
|
||||||
|
import { decryptData } from "@excalidraw/excalidraw/data/encryption";
|
||||||
|
import { getVisibleSceneBounds } from "@excalidraw/element";
|
||||||
|
import { newElementWith } from "@excalidraw/element";
|
||||||
|
import { isImageElement, isInitializedImageElement } from "@excalidraw/element";
|
||||||
|
import { AbortError } from "@excalidraw/excalidraw/errors";
|
||||||
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
|
import { withBatchedUpdates } from "@excalidraw/excalidraw/reactUtils";
|
||||||
|
|
||||||
|
import throttle from "lodash.throttle";
|
||||||
|
import { PureComponent } from "react";
|
||||||
|
|
||||||
|
import { bumpElementVersions } from "@excalidraw/excalidraw/data/restore";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ReconciledExcalidrawElement,
|
||||||
|
RemoteExcalidrawElement,
|
||||||
|
} from "@excalidraw/excalidraw/data/reconcile";
|
||||||
|
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
||||||
|
import type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
FileId,
|
||||||
|
InitializedExcalidrawImageElement,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
import type {
|
||||||
|
BinaryFileData,
|
||||||
|
ExcalidrawImperativeAPI,
|
||||||
|
SocketId,
|
||||||
|
Collaborator,
|
||||||
|
Gesture,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import { appJotaiStore, atom } from "../app-jotai";
|
||||||
import {
|
import {
|
||||||
CURSOR_SYNC_TIMEOUT,
|
CURSOR_SYNC_TIMEOUT,
|
||||||
FILE_UPLOAD_MAX_BYTES,
|
FILE_UPLOAD_MAX_BYTES,
|
||||||
@@ -39,15 +62,18 @@ import {
|
|||||||
SYNC_FULL_SCENE_INTERVAL_MS,
|
SYNC_FULL_SCENE_INTERVAL_MS,
|
||||||
WS_EVENTS,
|
WS_EVENTS,
|
||||||
} from "../app_constants";
|
} from "../app_constants";
|
||||||
import type {
|
|
||||||
SocketUpdateDataSource,
|
|
||||||
SyncableExcalidrawElement,
|
|
||||||
} from "../data";
|
|
||||||
import {
|
import {
|
||||||
generateCollaborationLinkData,
|
generateCollaborationLinkData,
|
||||||
getCollaborationLink,
|
getCollaborationLink,
|
||||||
getSyncableElements,
|
getSyncableElements,
|
||||||
} from "../data";
|
} from "../data";
|
||||||
|
import {
|
||||||
|
encodeFilesForUpload,
|
||||||
|
FileManager,
|
||||||
|
updateStaleImageStatuses,
|
||||||
|
} from "../data/FileManager";
|
||||||
|
import { FileStatusStore } from "../data/fileStatusStore";
|
||||||
|
import { LocalData } from "../data/LocalData";
|
||||||
import {
|
import {
|
||||||
isSavedToFirebase,
|
isSavedToFirebase,
|
||||||
loadFilesFromFirebase,
|
loadFilesFromFirebase,
|
||||||
@@ -59,36 +85,15 @@ import {
|
|||||||
importUsernameFromLocalStorage,
|
importUsernameFromLocalStorage,
|
||||||
saveUsernameToLocalStorage,
|
saveUsernameToLocalStorage,
|
||||||
} from "../data/localStorage";
|
} from "../data/localStorage";
|
||||||
import Portal from "./Portal";
|
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
|
||||||
import {
|
|
||||||
IDLE_THRESHOLD,
|
|
||||||
ACTIVE_THRESHOLD,
|
|
||||||
UserIdleState,
|
|
||||||
} from "@excalidraw/excalidraw/constants";
|
|
||||||
import {
|
|
||||||
encodeFilesForUpload,
|
|
||||||
FileManager,
|
|
||||||
updateStaleImageStatuses,
|
|
||||||
} from "../data/FileManager";
|
|
||||||
import { AbortError } from "@excalidraw/excalidraw/errors";
|
|
||||||
import {
|
|
||||||
isImageElement,
|
|
||||||
isInitializedImageElement,
|
|
||||||
} from "@excalidraw/excalidraw/element/typeChecks";
|
|
||||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
|
|
||||||
import { decryptData } from "@excalidraw/excalidraw/data/encryption";
|
|
||||||
import { resetBrowserStateVersions } from "../data/tabSync";
|
import { resetBrowserStateVersions } from "../data/tabSync";
|
||||||
import { LocalData } from "../data/LocalData";
|
|
||||||
import { appJotaiStore, atom } from "../app-jotai";
|
|
||||||
import type { Mutable, ValueOf } from "@excalidraw/excalidraw/utility-types";
|
|
||||||
import { getVisibleSceneBounds } from "@excalidraw/excalidraw/element/bounds";
|
|
||||||
import { withBatchedUpdates } from "@excalidraw/excalidraw/reactUtils";
|
|
||||||
import { collabErrorIndicatorAtom } from "./CollabError";
|
import { collabErrorIndicatorAtom } from "./CollabError";
|
||||||
|
import Portal from "./Portal";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ReconciledExcalidrawElement,
|
SocketUpdateDataSource,
|
||||||
RemoteExcalidrawElement,
|
SyncableExcalidrawElement,
|
||||||
} from "@excalidraw/excalidraw/data/reconcile";
|
} from "../data";
|
||||||
|
|
||||||
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
||||||
export const isCollaboratingAtom = atom(false);
|
export const isCollaboratingAtom = atom(false);
|
||||||
@@ -145,6 +150,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
};
|
};
|
||||||
this.portal = new Portal(this);
|
this.portal = new Portal(this);
|
||||||
this.fileManager = new FileManager({
|
this.fileManager = new FileManager({
|
||||||
|
onFileStatusChange: FileStatusStore.updateStatuses.bind(FileStatusStore),
|
||||||
getFiles: async (fileIds) => {
|
getFiles: async (fileIds) => {
|
||||||
const { roomId, roomKey } = this.portal;
|
const { roomId, roomKey } = this.portal;
|
||||||
if (!roomId || !roomKey) {
|
if (!roomId || !roomKey) {
|
||||||
@@ -236,7 +242,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
|
|
||||||
appJotaiStore.set(collabAPIAtom, collabAPI);
|
appJotaiStore.set(collabAPIAtom, collabAPI);
|
||||||
|
|
||||||
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
if (isTestEnv() || isDevEnv()) {
|
||||||
window.collab = window.collab || ({} as Window["collab"]);
|
window.collab = window.collab || ({} as Window["collab"]);
|
||||||
Object.defineProperties(window, {
|
Object.defineProperties(window, {
|
||||||
collab: {
|
collab: {
|
||||||
@@ -296,13 +302,20 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
// the purpose is to run in immediately after user decides to stay
|
// the purpose is to run in immediately after user decides to stay
|
||||||
this.saveCollabRoomToFirebase(syncableElements);
|
this.saveCollabRoomToFirebase(syncableElements);
|
||||||
|
|
||||||
preventUnload(event);
|
if (import.meta.env.VITE_APP_DISABLE_PREVENT_UNLOAD !== "true") {
|
||||||
|
preventUnload(event);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
"preventing unload disabled (VITE_APP_DISABLE_PREVENT_UNLOAD)",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
saveCollabRoomToFirebase = async (
|
saveCollabRoomToFirebase = async (
|
||||||
syncableElements: readonly SyncableExcalidrawElement[],
|
syncableElements: readonly SyncableExcalidrawElement[],
|
||||||
) => {
|
) => {
|
||||||
|
syncableElements = cloneJSON(syncableElements);
|
||||||
try {
|
try {
|
||||||
const storedElements = await saveToFirebase(
|
const storedElements = await saveToFirebase(
|
||||||
this.portal,
|
this.portal,
|
||||||
@@ -433,7 +446,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private decryptPayload = async (
|
private decryptPayload = async (
|
||||||
iv: Uint8Array,
|
iv: Uint8Array<ArrayBuffer>,
|
||||||
encryptedData: ArrayBuffer,
|
encryptedData: ArrayBuffer,
|
||||||
decryptionKey: string,
|
decryptionKey: string,
|
||||||
): Promise<ValueOf<SocketUpdateDataSource>> => {
|
): Promise<ValueOf<SocketUpdateDataSource>> => {
|
||||||
@@ -522,7 +535,10 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!existingRoomLinkData) {
|
if (existingRoomLinkData) {
|
||||||
|
// when joining existing room, don't merge it with current scene data
|
||||||
|
this.excalidrawAPI.resetScene();
|
||||||
|
} else {
|
||||||
const elements = this.excalidrawAPI.getSceneElements().map((element) => {
|
const elements = this.excalidrawAPI.getSceneElements().map((element) => {
|
||||||
if (isImageElement(element) && element.status === "saved") {
|
if (isImageElement(element) && element.status === "saved") {
|
||||||
return newElementWith(element, { status: "pending" });
|
return newElementWith(element, { status: "pending" });
|
||||||
@@ -551,7 +567,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
// All socket listeners are moving to Portal
|
// All socket listeners are moving to Portal
|
||||||
this.portal.socket.on(
|
this.portal.socket.on(
|
||||||
"client-broadcast",
|
"client-broadcast",
|
||||||
async (encryptedData: ArrayBuffer, iv: Uint8Array) => {
|
async (encryptedData: ArrayBuffer, iv: Uint8Array<ArrayBuffer>) => {
|
||||||
if (!this.portal.roomKey) {
|
if (!this.portal.roomKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -568,7 +584,9 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
case WS_SUBTYPES.INIT: {
|
case WS_SUBTYPES.INIT: {
|
||||||
if (!this.portal.socketInitialized) {
|
if (!this.portal.socketInitialized) {
|
||||||
this.initializeRoom({ fetchScene: false });
|
this.initializeRoom({ fetchScene: false });
|
||||||
const remoteElements = decryptedData.payload.elements;
|
const remoteElements = toBrandedType<
|
||||||
|
readonly RemoteExcalidrawElement[]
|
||||||
|
>(decryptedData.payload.elements);
|
||||||
const reconciledElements =
|
const reconciledElements =
|
||||||
this._reconcileElements(remoteElements);
|
this._reconcileElements(remoteElements);
|
||||||
this.handleRemoteSceneUpdate(reconciledElements);
|
this.handleRemoteSceneUpdate(reconciledElements);
|
||||||
@@ -582,7 +600,11 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
}
|
}
|
||||||
case WS_SUBTYPES.UPDATE:
|
case WS_SUBTYPES.UPDATE:
|
||||||
this.handleRemoteSceneUpdate(
|
this.handleRemoteSceneUpdate(
|
||||||
this._reconcileElements(decryptedData.payload.elements),
|
this._reconcileElements(
|
||||||
|
toBrandedType<readonly RemoteExcalidrawElement[]>(
|
||||||
|
decryptedData.payload.elements,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case WS_SUBTYPES.MOUSE_LOCATION: {
|
case WS_SUBTYPES.MOUSE_LOCATION: {
|
||||||
@@ -731,17 +753,28 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private _reconcileElements = (
|
private _reconcileElements = (
|
||||||
remoteElements: readonly ExcalidrawElement[],
|
remoteElements: readonly RemoteExcalidrawElement[],
|
||||||
): ReconciledExcalidrawElement[] => {
|
): ReconciledExcalidrawElement[] => {
|
||||||
const localElements = this.getSceneElementsIncludingDeleted();
|
|
||||||
const appState = this.excalidrawAPI.getAppState();
|
const appState = this.excalidrawAPI.getAppState();
|
||||||
const restoredRemoteElements = restoreElements(remoteElements, null);
|
|
||||||
const reconciledElements = reconcileElements(
|
const existingElements = this.getSceneElementsIncludingDeleted();
|
||||||
localElements,
|
|
||||||
restoredRemoteElements as RemoteExcalidrawElement[],
|
// NOTE ideally we restore _after_ reconciliation but we can't do that
|
||||||
|
// as we'd regenerate even elements such as appState.newElement which would
|
||||||
|
// break the state
|
||||||
|
remoteElements = restoreElements(remoteElements, existingElements);
|
||||||
|
|
||||||
|
let reconciledElements = reconcileElements(
|
||||||
|
existingElements,
|
||||||
|
remoteElements,
|
||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
reconciledElements = bumpElementVersions(
|
||||||
|
reconciledElements,
|
||||||
|
existingElements,
|
||||||
|
);
|
||||||
|
|
||||||
// Avoid broadcasting to the rest of the collaborators the scene
|
// Avoid broadcasting to the rest of the collaborators the scene
|
||||||
// we just received!
|
// we just received!
|
||||||
// Note: this needs to be set before updating the scene as it
|
// Note: this needs to be set before updating the scene as it
|
||||||
@@ -1009,7 +1042,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
if (isTestEnv() || isDevEnv()) {
|
||||||
window.collab = window.collab || ({} as Window["collab"]);
|
window.collab = window.collab || ({} as Window["collab"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Tooltip } from "@excalidraw/excalidraw/components/Tooltip";
|
|||||||
import { warning } from "@excalidraw/excalidraw/components/icons";
|
import { warning } from "@excalidraw/excalidraw/components/icons";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { atom } from "../app-jotai";
|
import { atom } from "../app-jotai";
|
||||||
|
|
||||||
import "./CollabError.scss";
|
import "./CollabError.scss";
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
|
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
||||||
|
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||||
|
import { encryptData } from "@excalidraw/excalidraw/data/encryption";
|
||||||
|
import { newElementWith } from "@excalidraw/element";
|
||||||
|
import throttle from "lodash.throttle";
|
||||||
|
|
||||||
|
import type { UserIdleState } from "@excalidraw/common";
|
||||||
|
import type { OrderedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
import type {
|
||||||
|
OnUserFollowedPayload,
|
||||||
|
SocketId,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
|
||||||
|
import { isSyncableElement } from "../data";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
SocketUpdateData,
|
SocketUpdateData,
|
||||||
SocketUpdateDataSource,
|
SocketUpdateDataSource,
|
||||||
SyncableExcalidrawElement,
|
SyncableExcalidrawElement,
|
||||||
} from "../data";
|
} from "../data";
|
||||||
import { isSyncableElement } from "../data";
|
|
||||||
|
|
||||||
import type { TCollabClass } from "./Collab";
|
import type { TCollabClass } from "./Collab";
|
||||||
|
|
||||||
import type { OrderedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
|
||||||
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
|
|
||||||
import type {
|
|
||||||
OnUserFollowedPayload,
|
|
||||||
SocketId,
|
|
||||||
} from "@excalidraw/excalidraw/types";
|
|
||||||
import type { UserIdleState } from "@excalidraw/excalidraw/constants";
|
|
||||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
|
||||||
import throttle from "lodash.throttle";
|
|
||||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
|
|
||||||
import { encryptData } from "@excalidraw/excalidraw/data/encryption";
|
|
||||||
import type { Socket } from "socket.io-client";
|
import type { Socket } from "socket.io-client";
|
||||||
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
|
||||||
|
|
||||||
class Portal {
|
class Portal {
|
||||||
collab: TCollabClass;
|
collab: TCollabClass;
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
|
||||||
import {
|
import {
|
||||||
DiagramToCodePlugin,
|
DiagramToCodePlugin,
|
||||||
exportToBlob,
|
exportToBlob,
|
||||||
getTextFromElements,
|
getTextFromElements,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
TTDDialog,
|
TTDDialog,
|
||||||
|
TTDStreamFetch,
|
||||||
} from "@excalidraw/excalidraw";
|
} from "@excalidraw/excalidraw";
|
||||||
import { getDataURL } from "@excalidraw/excalidraw/data/blob";
|
import { getDataURL } from "@excalidraw/excalidraw/data/blob";
|
||||||
import { safelyParseJSON } from "@excalidraw/excalidraw/utils";
|
import { safelyParseJSON } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import { TTDIndexedDBAdapter } from "../data/TTDStorage";
|
||||||
|
|
||||||
export const AIComponents = ({
|
export const AIComponents = ({
|
||||||
excalidrawAPI,
|
excalidrawAPI,
|
||||||
@@ -72,7 +76,7 @@ export const AIComponents = ({
|
|||||||
</br>
|
</br>
|
||||||
<div>You can also try <a href="${
|
<div>You can also try <a href="${
|
||||||
import.meta.env.VITE_APP_PLUS_LP
|
import.meta.env.VITE_APP_PLUS_LP
|
||||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=d2c" target="_blank" rel="noreferrer noopener">Excalidraw+</a> to get more requests.</div>
|
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=d2c" target="_blank" rel="noopener">Excalidraw+</a> to get more requests.</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>`,
|
</html>`,
|
||||||
@@ -98,61 +102,23 @@ export const AIComponents = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<TTDDialog
|
<TTDDialog
|
||||||
onTextSubmit={async (input) => {
|
onTextSubmit={async (props) => {
|
||||||
try {
|
const { onChunk, onStreamCreated, signal, messages } = props;
|
||||||
const response = await fetch(
|
|
||||||
`${
|
|
||||||
import.meta.env.VITE_APP_AI_BACKEND
|
|
||||||
}/v1/ai/text-to-diagram/generate`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ prompt: input }),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const rateLimit = response.headers.has("X-Ratelimit-Limit")
|
const result = await TTDStreamFetch({
|
||||||
? parseInt(response.headers.get("X-Ratelimit-Limit") || "0", 10)
|
url: `${
|
||||||
: undefined;
|
import.meta.env.VITE_APP_AI_BACKEND
|
||||||
|
}/v1/ai/text-to-diagram/chat-streaming`,
|
||||||
|
messages,
|
||||||
|
onChunk,
|
||||||
|
onStreamCreated,
|
||||||
|
extractRateLimits: true,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
const rateLimitRemaining = response.headers.has(
|
return result;
|
||||||
"X-Ratelimit-Remaining",
|
|
||||||
)
|
|
||||||
? parseInt(
|
|
||||||
response.headers.get("X-Ratelimit-Remaining") || "0",
|
|
||||||
10,
|
|
||||||
)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const json = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 429) {
|
|
||||||
return {
|
|
||||||
rateLimit,
|
|
||||||
rateLimitRemaining,
|
|
||||||
error: new Error(
|
|
||||||
"Too many requests today, please try again tomorrow!",
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(json.message || "Generation failed...");
|
|
||||||
}
|
|
||||||
|
|
||||||
const generatedResponse = json.generatedResponse;
|
|
||||||
if (!generatedResponse) {
|
|
||||||
throw new Error("Generation failed...");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { generatedResponse, rateLimit, rateLimitRemaining };
|
|
||||||
} catch (err: any) {
|
|
||||||
throw new Error("Request failed");
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
|
persistenceAdapter={TTDIndexedDBAdapter}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React from "react";
|
|
||||||
import { Footer } from "@excalidraw/excalidraw/index";
|
import { Footer } from "@excalidraw/excalidraw/index";
|
||||||
import { EncryptedIcon } from "./EncryptedIcon";
|
import React from "react";
|
||||||
import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
|
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
|
|
||||||
import { DebugFooter, isVisualDebuggerEnabled } from "./DebugCanvas";
|
import { DebugFooter, isVisualDebuggerEnabled } from "./DebugCanvas";
|
||||||
|
import { EncryptedIcon } from "./EncryptedIcon";
|
||||||
|
|
||||||
export const AppFooter = React.memo(
|
export const AppFooter = React.memo(
|
||||||
({ onChange }: { onChange: () => void }) => {
|
({ onChange }: { onChange: () => void }) => {
|
||||||
@@ -17,11 +18,7 @@ export const AppFooter = React.memo(
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isVisualDebuggerEnabled() && <DebugFooter onChange={onChange} />}
|
{isVisualDebuggerEnabled() && <DebugFooter onChange={onChange} />}
|
||||||
{isExcalidrawPlusSignedUser ? (
|
{!isExcalidrawPlusSignedUser && <EncryptedIcon />}
|
||||||
<ExcalidrawPlusAppLink />
|
|
||||||
) : (
|
|
||||||
<EncryptedIcon />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Footer>
|
</Footer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
import {
|
||||||
loginIcon,
|
loginIcon,
|
||||||
ExcalLogo,
|
ExcalLogo,
|
||||||
eyeIcon,
|
eyeIcon,
|
||||||
} from "@excalidraw/excalidraw/components/icons";
|
} from "@excalidraw/excalidraw/components/icons";
|
||||||
import type { Theme } from "@excalidraw/excalidraw/element/types";
|
|
||||||
import { MainMenu } from "@excalidraw/excalidraw/index";
|
import { MainMenu } from "@excalidraw/excalidraw/index";
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import React from "react";
|
||||||
|
|
||||||
|
import { isDevEnv } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type { Theme } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { LanguageList } from "../app-language/LanguageList";
|
import { LanguageList } from "../app-language/LanguageList";
|
||||||
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
|
|
||||||
import { saveDebugState } from "./DebugCanvas";
|
import { saveDebugState } from "./DebugCanvas";
|
||||||
|
|
||||||
export const AppMainMenu: React.FC<{
|
export const AppMainMenu: React.FC<{
|
||||||
@@ -15,7 +20,6 @@ export const AppMainMenu: React.FC<{
|
|||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
isCollabEnabled: boolean;
|
isCollabEnabled: boolean;
|
||||||
theme: Theme | "system";
|
theme: Theme | "system";
|
||||||
setTheme: (theme: Theme | "system") => void;
|
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
}> = React.memo((props) => {
|
}> = React.memo((props) => {
|
||||||
return (
|
return (
|
||||||
@@ -54,10 +58,10 @@ export const AppMainMenu: React.FC<{
|
|||||||
>
|
>
|
||||||
{isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"}
|
{isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"}
|
||||||
</MainMenu.ItemLink>
|
</MainMenu.ItemLink>
|
||||||
{import.meta.env.DEV && (
|
{isDevEnv() && (
|
||||||
<MainMenu.Item
|
<MainMenu.Item
|
||||||
icon={eyeIcon}
|
icon={eyeIcon}
|
||||||
onClick={() => {
|
onSelect={() => {
|
||||||
if (window.visualDebug) {
|
if (window.visualDebug) {
|
||||||
delete window.visualDebug;
|
delete window.visualDebug;
|
||||||
saveDebugState({ enabled: false });
|
saveDebugState({ enabled: false });
|
||||||
@@ -72,11 +76,8 @@ export const AppMainMenu: React.FC<{
|
|||||||
</MainMenu.Item>
|
</MainMenu.Item>
|
||||||
)}
|
)}
|
||||||
<MainMenu.Separator />
|
<MainMenu.Separator />
|
||||||
<MainMenu.DefaultItems.ToggleTheme
|
<MainMenu.DefaultItems.Preferences />
|
||||||
allowSystemTheme
|
<MainMenu.DefaultItems.ToggleTheme allowSystemTheme theme={props.theme} />
|
||||||
theme={props.theme}
|
|
||||||
onSelect={props.setTheme}
|
|
||||||
/>
|
|
||||||
<MainMenu.ItemCustom>
|
<MainMenu.ItemCustom>
|
||||||
<LanguageList style={{ width: "100%" }} />
|
<LanguageList style={{ width: "100%" }} />
|
||||||
</MainMenu.ItemCustom>
|
</MainMenu.ItemCustom>
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
.excalidraw {
|
||||||
|
.app-sidebar-promo-container {
|
||||||
|
padding: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
gap: 1rem;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar-promo-image {
|
||||||
|
margin: 1rem 0;
|
||||||
|
|
||||||
|
height: 16.25rem;
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
|
background-image: radial-gradient(
|
||||||
|
circle,
|
||||||
|
transparent 60%,
|
||||||
|
var(--sidebar-bg-color) 100%
|
||||||
|
),
|
||||||
|
var(--image-source);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar-promo-text {
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-button {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { DefaultSidebar, Sidebar, THEME } from "@excalidraw/excalidraw";
|
||||||
|
import {
|
||||||
|
messageCircleIcon,
|
||||||
|
presentationIcon,
|
||||||
|
} from "@excalidraw/excalidraw/components/icons";
|
||||||
|
import { LinkButton } from "@excalidraw/excalidraw/components/LinkButton";
|
||||||
|
import { useUIAppState } from "@excalidraw/excalidraw/context/ui-appState";
|
||||||
|
|
||||||
|
import "./AppSidebar.scss";
|
||||||
|
|
||||||
|
export const AppSidebar = () => {
|
||||||
|
const { theme, openSidebar } = useUIAppState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaultSidebar>
|
||||||
|
<DefaultSidebar.TabTriggers>
|
||||||
|
<Sidebar.TabTrigger
|
||||||
|
tab="comments"
|
||||||
|
style={{ opacity: openSidebar?.tab === "comments" ? 1 : 0.4 }}
|
||||||
|
>
|
||||||
|
{messageCircleIcon}
|
||||||
|
</Sidebar.TabTrigger>
|
||||||
|
<Sidebar.TabTrigger
|
||||||
|
tab="presentation"
|
||||||
|
style={{ opacity: openSidebar?.tab === "presentation" ? 1 : 0.4 }}
|
||||||
|
>
|
||||||
|
{presentationIcon}
|
||||||
|
</Sidebar.TabTrigger>
|
||||||
|
</DefaultSidebar.TabTriggers>
|
||||||
|
<Sidebar.Tab tab="comments">
|
||||||
|
<div className="app-sidebar-promo-container">
|
||||||
|
<div
|
||||||
|
className="app-sidebar-promo-image"
|
||||||
|
style={{
|
||||||
|
["--image-source" as any]: `url(/oss_promo_comments_${
|
||||||
|
theme === THEME.DARK ? "dark" : "light"
|
||||||
|
}.jpg)`,
|
||||||
|
opacity: 0.7,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="app-sidebar-promo-text">
|
||||||
|
Make comments with Excalidraw+
|
||||||
|
</div>
|
||||||
|
<LinkButton
|
||||||
|
href={`${
|
||||||
|
import.meta.env.VITE_APP_PLUS_LP
|
||||||
|
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=comments_promo#excalidraw-redirect`}
|
||||||
|
>
|
||||||
|
Sign up now
|
||||||
|
</LinkButton>
|
||||||
|
</div>
|
||||||
|
</Sidebar.Tab>
|
||||||
|
<Sidebar.Tab tab="presentation" className="px-3">
|
||||||
|
<div className="app-sidebar-promo-container">
|
||||||
|
<div
|
||||||
|
className="app-sidebar-promo-image"
|
||||||
|
style={{
|
||||||
|
["--image-source" as any]: `url(/oss_promo_presentations_${
|
||||||
|
theme === THEME.DARK ? "dark" : "light"
|
||||||
|
}.svg)`,
|
||||||
|
backgroundSize: "60%",
|
||||||
|
opacity: 0.4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="app-sidebar-promo-text">
|
||||||
|
Create presentations with Excalidraw+
|
||||||
|
</div>
|
||||||
|
<LinkButton
|
||||||
|
href={`${
|
||||||
|
import.meta.env.VITE_APP_PLUS_LP
|
||||||
|
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=presentations_promo#excalidraw-redirect`}
|
||||||
|
>
|
||||||
|
Sign up now
|
||||||
|
</LinkButton>
|
||||||
|
</div>
|
||||||
|
</Sidebar.Tab>
|
||||||
|
</DefaultSidebar>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import React from "react";
|
|
||||||
import { loginIcon } from "@excalidraw/excalidraw/components/icons";
|
import { loginIcon } from "@excalidraw/excalidraw/components/icons";
|
||||||
|
import { POINTER_EVENTS } from "@excalidraw/common";
|
||||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||||
import { WelcomeScreen } from "@excalidraw/excalidraw/index";
|
import { WelcomeScreen } from "@excalidraw/excalidraw/index";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
import { POINTER_EVENTS } from "@excalidraw/excalidraw/constants";
|
|
||||||
|
|
||||||
export const AppWelcomeScreen: React.FC<{
|
export const AppWelcomeScreen: React.FC<{
|
||||||
onCollabDialogOpen: () => any;
|
onCollabDialogOpen: () => any;
|
||||||
@@ -32,7 +33,15 @@ export const AppWelcomeScreen: React.FC<{
|
|||||||
return bit;
|
return bit;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
headingContent = t("welcomeScreen.app.center_heading");
|
headingContent = (
|
||||||
|
<>
|
||||||
|
{t("welcomeScreen.app.center_heading")}
|
||||||
|
<br />
|
||||||
|
{t("welcomeScreen.app.center_heading_line2")}
|
||||||
|
<br />
|
||||||
|
{t("welcomeScreen.app.center_heading_line3")}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,24 +1,45 @@
|
|||||||
import { useCallback, useImperativeHandle, useRef } from "react";
|
|
||||||
import { type AppState } from "@excalidraw/excalidraw/types";
|
|
||||||
import { throttleRAF } from "@excalidraw/excalidraw/utils";
|
|
||||||
import {
|
|
||||||
bootstrapCanvas,
|
|
||||||
getNormalizedCanvasDimensions,
|
|
||||||
} from "@excalidraw/excalidraw/renderer/helpers";
|
|
||||||
import type { DebugElement } from "@excalidraw/excalidraw/visualdebug";
|
|
||||||
import {
|
import {
|
||||||
ArrowheadArrowIcon,
|
ArrowheadArrowIcon,
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "@excalidraw/excalidraw/components/icons";
|
} from "@excalidraw/excalidraw/components/icons";
|
||||||
import { STORAGE_KEYS } from "../app_constants";
|
import {
|
||||||
import type { Curve } from "../../packages/math";
|
bootstrapCanvas,
|
||||||
|
getNormalizedCanvasDimensions,
|
||||||
|
} from "@excalidraw/excalidraw/renderer/helpers";
|
||||||
|
import { type AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
import { arrayToMap, throttleRAF } from "@excalidraw/common";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getGlobalFixedPointForBindableElement,
|
||||||
|
isArrowElement,
|
||||||
|
isBindableElement,
|
||||||
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isLineSegment,
|
isLineSegment,
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
type LineSegment,
|
type LineSegment,
|
||||||
} from "../../packages/math";
|
} from "@excalidraw/math";
|
||||||
import { isCurve } from "../../packages/math/curve";
|
import { isCurve } from "@excalidraw/math/curve";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import type { Curve } from "@excalidraw/math";
|
||||||
|
import type {
|
||||||
|
DebugElement,
|
||||||
|
DebugPolygon,
|
||||||
|
} from "@excalidraw/element/visualdebug";
|
||||||
|
import type {
|
||||||
|
ElementsMap,
|
||||||
|
ExcalidrawArrowElement,
|
||||||
|
ExcalidrawBindableElement,
|
||||||
|
FixedPointBinding,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
|
|
||||||
const renderLine = (
|
const renderLine = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
@@ -57,6 +78,44 @@ const renderCubicBezier = (
|
|||||||
context.restore();
|
context.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderPolygon = (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
zoom: number,
|
||||||
|
polygon: DebugPolygon,
|
||||||
|
color: string,
|
||||||
|
) => {
|
||||||
|
const { points, fill, close } = polygon;
|
||||||
|
|
||||||
|
if (points.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(points[0][0] * zoom, points[0][1] * zoom);
|
||||||
|
for (let i = 1; i < points.length; i += 1) {
|
||||||
|
context.lineTo(points[i][0] * zoom, points[i][1] * zoom);
|
||||||
|
}
|
||||||
|
if (close !== false) {
|
||||||
|
context.closePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fill) {
|
||||||
|
context.save();
|
||||||
|
context.globalAlpha = 0.15;
|
||||||
|
context.fillStyle = color;
|
||||||
|
context.fill();
|
||||||
|
context.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.strokeStyle = color;
|
||||||
|
context.stroke();
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDebugPolygon = (data: DebugElement["data"]): data is DebugPolygon =>
|
||||||
|
(data as DebugPolygon).type === "polygon";
|
||||||
|
|
||||||
const renderOrigin = (context: CanvasRenderingContext2D, zoom: number) => {
|
const renderOrigin = (context: CanvasRenderingContext2D, zoom: number) => {
|
||||||
context.strokeStyle = "#888";
|
context.strokeStyle = "#888";
|
||||||
context.save();
|
context.save();
|
||||||
@@ -69,6 +128,176 @@ const renderOrigin = (context: CanvasRenderingContext2D, zoom: number) => {
|
|||||||
context.save();
|
context.save();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _renderBinding = (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
binding: FixedPointBinding,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
zoom: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
color: string,
|
||||||
|
) => {
|
||||||
|
if (!binding.fixedPoint) {
|
||||||
|
console.warn("Binding must have a fixedPoint");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bindable = elementsMap.get(
|
||||||
|
binding.elementId,
|
||||||
|
) as ExcalidrawBindableElement;
|
||||||
|
const [x, y] = getGlobalFixedPointForBindableElement(
|
||||||
|
binding.fixedPoint,
|
||||||
|
bindable,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
context.strokeStyle = color;
|
||||||
|
context.lineWidth = 1;
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(x * zoom, y * zoom);
|
||||||
|
context.bezierCurveTo(
|
||||||
|
x * zoom - width,
|
||||||
|
y * zoom - height,
|
||||||
|
x * zoom - width,
|
||||||
|
y * zoom + height,
|
||||||
|
x * zoom,
|
||||||
|
y * zoom,
|
||||||
|
);
|
||||||
|
context.stroke();
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
|
const _renderBindableBinding = (
|
||||||
|
binding: FixedPointBinding,
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
zoom: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
color: string,
|
||||||
|
) => {
|
||||||
|
const bindable = elementsMap.get(
|
||||||
|
binding.elementId,
|
||||||
|
) as ExcalidrawBindableElement;
|
||||||
|
if (!binding.fixedPoint) {
|
||||||
|
console.warn("Binding must have a fixedPoint");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [x, y] = getGlobalFixedPointForBindableElement(
|
||||||
|
binding.fixedPoint,
|
||||||
|
bindable,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
context.strokeStyle = color;
|
||||||
|
context.lineWidth = 1;
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(x * zoom, y * zoom);
|
||||||
|
context.bezierCurveTo(
|
||||||
|
x * zoom + width,
|
||||||
|
y * zoom + height,
|
||||||
|
x * zoom + width,
|
||||||
|
y * zoom - height,
|
||||||
|
x * zoom,
|
||||||
|
y * zoom,
|
||||||
|
);
|
||||||
|
context.stroke();
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBindings = (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
elements: readonly OrderedExcalidrawElement[],
|
||||||
|
zoom: number,
|
||||||
|
) => {
|
||||||
|
const elementsMap = arrayToMap(elements);
|
||||||
|
const dim = 16;
|
||||||
|
elements.forEach((element) => {
|
||||||
|
if (element.isDeleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArrowElement(element)) {
|
||||||
|
if (element.startBinding) {
|
||||||
|
if (
|
||||||
|
!elementsMap
|
||||||
|
.get(element.startBinding.elementId)
|
||||||
|
?.boundElements?.find((e) => e.id === element.id)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderBinding(
|
||||||
|
context,
|
||||||
|
element.startBinding,
|
||||||
|
elementsMap,
|
||||||
|
zoom,
|
||||||
|
dim,
|
||||||
|
dim,
|
||||||
|
element.startBinding?.mode === "orbit" ? "red" : "black",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.endBinding) {
|
||||||
|
if (
|
||||||
|
!elementsMap
|
||||||
|
.get(element.endBinding.elementId)
|
||||||
|
?.boundElements?.find((e) => e.id === element.id)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_renderBinding(
|
||||||
|
context,
|
||||||
|
element.endBinding,
|
||||||
|
elementsMap,
|
||||||
|
zoom,
|
||||||
|
dim,
|
||||||
|
dim,
|
||||||
|
element.endBinding?.mode === "orbit" ? "red" : "black",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBindableElement(element) && element.boundElements?.length) {
|
||||||
|
element.boundElements.forEach((boundElement) => {
|
||||||
|
if (boundElement.type !== "arrow") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrow = elementsMap.get(
|
||||||
|
boundElement.id,
|
||||||
|
) as ExcalidrawArrowElement;
|
||||||
|
|
||||||
|
if (arrow && arrow.startBinding?.elementId === element.id) {
|
||||||
|
_renderBindableBinding(
|
||||||
|
arrow.startBinding,
|
||||||
|
context,
|
||||||
|
elementsMap,
|
||||||
|
zoom,
|
||||||
|
dim,
|
||||||
|
dim,
|
||||||
|
"green",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (arrow && arrow.endBinding?.elementId === element.id) {
|
||||||
|
_renderBindableBinding(
|
||||||
|
arrow.endBinding,
|
||||||
|
context,
|
||||||
|
elementsMap,
|
||||||
|
zoom,
|
||||||
|
dim,
|
||||||
|
dim,
|
||||||
|
"green",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const render = (
|
const render = (
|
||||||
frame: DebugElement[],
|
frame: DebugElement[],
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
@@ -92,6 +321,9 @@ const render = (
|
|||||||
el.color,
|
el.color,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case isDebugPolygon(el.data):
|
||||||
|
renderPolygon(context, appState.zoom.value, el.data, el.color);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown element type ${JSON.stringify(el)}`);
|
throw new Error(`Unknown element type ${JSON.stringify(el)}`);
|
||||||
}
|
}
|
||||||
@@ -101,18 +333,14 @@ const render = (
|
|||||||
const _debugRenderer = (
|
const _debugRenderer = (
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
elements: readonly OrderedExcalidrawElement[],
|
||||||
scale: number,
|
scale: number,
|
||||||
refresh: () => void,
|
|
||||||
) => {
|
) => {
|
||||||
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
|
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
|
||||||
canvas,
|
canvas,
|
||||||
scale,
|
scale,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (appState.height !== canvas.height || appState.width !== canvas.width) {
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = bootstrapCanvas({
|
const context = bootstrapCanvas({
|
||||||
canvas,
|
canvas,
|
||||||
scale,
|
scale,
|
||||||
@@ -129,6 +357,7 @@ const _debugRenderer = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
renderOrigin(context, appState.zoom.value);
|
renderOrigin(context, appState.zoom.value);
|
||||||
|
renderBindings(context, elements, appState.zoom.value);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
window.visualDebug?.currentFrame &&
|
window.visualDebug?.currentFrame &&
|
||||||
@@ -180,12 +409,11 @@ export const debugRenderer = throttleRAF(
|
|||||||
(
|
(
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
elements: readonly OrderedExcalidrawElement[],
|
||||||
scale: number,
|
scale: number,
|
||||||
refresh: () => void,
|
|
||||||
) => {
|
) => {
|
||||||
_debugRenderer(canvas, appState, scale, refresh);
|
_debugRenderer(canvas, appState, elements, scale);
|
||||||
},
|
},
|
||||||
{ trailing: true },
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const loadSavedDebugState = () => {
|
export const loadSavedDebugState = () => {
|
||||||
@@ -310,35 +538,29 @@ export const DebugFooter = ({ onChange }: { onChange: () => void }) => {
|
|||||||
interface DebugCanvasProps {
|
interface DebugCanvasProps {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
scale: number;
|
scale: number;
|
||||||
ref?: React.Ref<HTMLCanvasElement>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DebugCanvas = ({ appState, scale, ref }: DebugCanvasProps) => {
|
const DebugCanvas = React.forwardRef<HTMLCanvasElement, DebugCanvasProps>(
|
||||||
const { width, height } = appState;
|
({ appState, scale }, ref) => {
|
||||||
|
const { width, height } = appState;
|
||||||
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
return (
|
||||||
useImperativeHandle<HTMLCanvasElement | null, HTMLCanvasElement | null>(
|
<canvas
|
||||||
ref,
|
style={{
|
||||||
() => canvasRef.current,
|
width,
|
||||||
[canvasRef],
|
height,
|
||||||
);
|
position: "absolute",
|
||||||
|
zIndex: 2,
|
||||||
return (
|
pointerEvents: "none",
|
||||||
<canvas
|
}}
|
||||||
style={{
|
width={width * scale}
|
||||||
width,
|
height={height * scale}
|
||||||
height,
|
ref={ref}
|
||||||
position: "absolute",
|
>
|
||||||
zIndex: 2,
|
Debug Canvas
|
||||||
pointerEvents: "none",
|
</canvas>
|
||||||
}}
|
);
|
||||||
width={width * scale}
|
},
|
||||||
height={height * scale}
|
);
|
||||||
ref={canvasRef}
|
|
||||||
>
|
|
||||||
Debug Canvas
|
|
||||||
</canvas>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DebugCanvas;
|
export default DebugCanvas;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { shield } from "@excalidraw/excalidraw/components/icons";
|
|
||||||
import { Tooltip } from "@excalidraw/excalidraw/components/Tooltip";
|
import { Tooltip } from "@excalidraw/excalidraw/components/Tooltip";
|
||||||
|
import { shield } from "@excalidraw/excalidraw/components/icons";
|
||||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||||
|
|
||||||
export const EncryptedIcon = () => {
|
export const EncryptedIcon = () => {
|
||||||
@@ -10,7 +10,7 @@ export const EncryptedIcon = () => {
|
|||||||
className="encrypted-icon tooltip"
|
className="encrypted-icon tooltip"
|
||||||
href="https://plus.excalidraw.com/blog/end-to-end-encryption"
|
href="https://plus.excalidraw.com/blog/end-to-end-encryption"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener"
|
||||||
aria-label={t("encrypted.link")}
|
aria-label={t("encrypted.link")}
|
||||||
>
|
>
|
||||||
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
|
||||||
|
|
||||||
export const ExcalidrawPlusAppLink = () => {
|
|
||||||
if (!isExcalidrawPlusSignedUser) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={`${
|
|
||||||
import.meta.env.VITE_APP_PLUS_APP
|
|
||||||
}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="plus-button"
|
|
||||||
>
|
|
||||||
Go to Excalidraw+
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
export const ExcalidrawPlusPromoBanner = ({
|
||||||
|
isSignedIn,
|
||||||
|
}: {
|
||||||
|
isSignedIn: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={
|
||||||
|
isSignedIn
|
||||||
|
? import.meta.env.VITE_APP_PLUS_APP
|
||||||
|
: `${
|
||||||
|
import.meta.env.VITE_APP_PLUS_LP
|
||||||
|
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=guestBanner#excalidraw-redirect`
|
||||||
|
}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="plus-banner"
|
||||||
|
>
|
||||||
|
Excalidraw+
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,31 +1,33 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { uploadBytes, ref } from "firebase/storage";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||||
import { Card } from "@excalidraw/excalidraw/components/Card";
|
import { Card } from "@excalidraw/excalidraw/components/Card";
|
||||||
|
import { ExcalidrawLogo } from "@excalidraw/excalidraw/components/ExcalidrawLogo";
|
||||||
import { ToolButton } from "@excalidraw/excalidraw/components/ToolButton";
|
import { ToolButton } from "@excalidraw/excalidraw/components/ToolButton";
|
||||||
|
import { MIME_TYPES, getFrame } from "@excalidraw/common";
|
||||||
|
import {
|
||||||
|
encryptData,
|
||||||
|
generateEncryptionKey,
|
||||||
|
} from "@excalidraw/excalidraw/data/encryption";
|
||||||
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
|
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
|
||||||
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
|
import { isInitializedImageElement } from "@excalidraw/element";
|
||||||
|
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
FileId,
|
FileId,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
} from "@excalidraw/excalidraw/types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
|
||||||
import {
|
|
||||||
encryptData,
|
|
||||||
generateEncryptionKey,
|
|
||||||
} from "@excalidraw/excalidraw/data/encryption";
|
|
||||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
|
|
||||||
import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
|
import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
|
||||||
import { encodeFilesForUpload } from "../data/FileManager";
|
import { encodeFilesForUpload } from "../data/FileManager";
|
||||||
import { uploadBytes, ref } from "firebase/storage";
|
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
|
||||||
import { MIME_TYPES } from "@excalidraw/excalidraw/constants";
|
|
||||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
|
||||||
import { getFrame } from "@excalidraw/excalidraw/utils";
|
|
||||||
import { ExcalidrawLogo } from "@excalidraw/excalidraw/components/ExcalidrawLogo";
|
|
||||||
|
|
||||||
export const exportToExcalidrawPlus = async (
|
export const exportToExcalidrawPlus = async (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import oc from "open-color";
|
|
||||||
import React from "react";
|
|
||||||
import { THEME } from "@excalidraw/excalidraw/constants";
|
|
||||||
import type { Theme } from "@excalidraw/excalidraw/element/types";
|
|
||||||
|
|
||||||
// https://github.com/tholman/github-corners
|
|
||||||
export const GitHubCorner = React.memo(
|
|
||||||
({ theme, dir }: { theme: Theme; dir: string }) => (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="40"
|
|
||||||
height="40"
|
|
||||||
viewBox="0 0 250 250"
|
|
||||||
className="rtl-mirror"
|
|
||||||
style={{
|
|
||||||
marginTop: "calc(var(--space-factor) * -1)",
|
|
||||||
[dir === "rtl" ? "marginLeft" : "marginRight"]:
|
|
||||||
"calc(var(--space-factor) * -1)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://github.com/excalidraw/excalidraw"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label="GitHub repository"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M0 0l115 115h15l12 27 108 108V0z"
|
|
||||||
fill={theme === THEME.LIGHT ? oc.gray[6] : oc.gray[7]}
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="octo-arm"
|
|
||||||
d="M128 109c-15-9-9-19-9-19 3-7 2-11 2-11-1-7 3-2 3-2 4 5 2 11 2 11-3 10 5 15 9 16"
|
|
||||||
style={{ transformOrigin: "130px 106px" }}
|
|
||||||
fill={theme === THEME.LIGHT ? oc.white : "var(--default-bg-color)"}
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="octo-body"
|
|
||||||
d="M115 115s4 2 5 0l14-14c3-2 6-3 8-3-8-11-15-24 2-41 5-5 10-7 16-7 1-2 3-7 12-11 0 0 5 3 7 16 4 2 8 5 12 9s7 8 9 12c14 3 17 7 17 7-4 8-9 11-11 11 0 6-2 11-7 16-16 16-30 10-41 2 0 3-1 7-5 11l-12 11c-1 1 1 5 1 5z"
|
|
||||||
fill={theme === THEME.LIGHT ? oc.white : "var(--default-bg-color)"}
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
);
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
|
||||||
import * as Sentry from "@sentry/browser";
|
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
|
||||||
import Trans from "@excalidraw/excalidraw/components/Trans";
|
import Trans from "@excalidraw/excalidraw/components/Trans";
|
||||||
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
|
import * as Sentry from "@sentry/browser";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
interface TopErrorBoundaryState {
|
interface TopErrorBoundaryState {
|
||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
||||||
import { compressData } from "@excalidraw/excalidraw/data/encode";
|
import { compressData } from "@excalidraw/excalidraw/data/encode";
|
||||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
|
import { newElementWith } from "@excalidraw/element";
|
||||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
|
import { isInitializedImageElement } from "@excalidraw/element";
|
||||||
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
FileId,
|
FileId,
|
||||||
InitializedExcalidrawImageElement,
|
InitializedExcalidrawImageElement,
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
|
||||||
import type {
|
import type {
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFileMetadata,
|
BinaryFileMetadata,
|
||||||
@@ -39,10 +40,12 @@ export class FileManager {
|
|||||||
|
|
||||||
private _getFiles;
|
private _getFiles;
|
||||||
private _saveFiles;
|
private _saveFiles;
|
||||||
|
private _onFileStatusChange;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
getFiles,
|
getFiles,
|
||||||
saveFiles,
|
saveFiles,
|
||||||
|
onFileStatusChange,
|
||||||
}: {
|
}: {
|
||||||
getFiles: (fileIds: FileId[]) => Promise<{
|
getFiles: (fileIds: FileId[]) => Promise<{
|
||||||
loadedFiles: BinaryFileData[];
|
loadedFiles: BinaryFileData[];
|
||||||
@@ -52,9 +55,13 @@ export class FileManager {
|
|||||||
savedFiles: Map<FileId, BinaryFileData>;
|
savedFiles: Map<FileId, BinaryFileData>;
|
||||||
erroredFiles: Map<FileId, BinaryFileData>;
|
erroredFiles: Map<FileId, BinaryFileData>;
|
||||||
}>;
|
}>;
|
||||||
|
onFileStatusChange?: (
|
||||||
|
updates: Array<[FileId, "loading" | "loaded" | "error"]>,
|
||||||
|
) => void;
|
||||||
}) {
|
}) {
|
||||||
this._getFiles = getFiles;
|
this._getFiles = getFiles;
|
||||||
this._saveFiles = saveFiles;
|
this._saveFiles = saveFiles;
|
||||||
|
this._onFileStatusChange = onFileStatusChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -145,6 +152,8 @@ export class FileManager {
|
|||||||
this.fetchingFiles.set(id, true);
|
this.fetchingFiles.set(id, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._onFileStatusChange?.(ids.map((id) => [id, "loading"]));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { loadedFiles, erroredFiles } = await this._getFiles(ids);
|
const { loadedFiles, erroredFiles } = await this._getFiles(ids);
|
||||||
|
|
||||||
@@ -155,6 +164,13 @@ export class FileManager {
|
|||||||
this.erroredFiles_fetch.set(fileId, true);
|
this.erroredFiles_fetch.set(fileId, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._onFileStatusChange?.([
|
||||||
|
...loadedFiles.map((f) => [f.id, "loaded"] as [FileId, "loaded"]),
|
||||||
|
...[...erroredFiles.keys()].map(
|
||||||
|
(id) => [id, "error"] as [FileId, "error"],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
return { loadedFiles, erroredFiles };
|
return { loadedFiles, erroredFiles };
|
||||||
} finally {
|
} finally {
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
@@ -194,6 +210,13 @@ export class FileManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
|
if (this._onFileStatusChange && this.fetchingFiles.size) {
|
||||||
|
this._onFileStatusChange(
|
||||||
|
[...this.fetchingFiles.keys()].map(
|
||||||
|
(id) => [id, "error"] as [FileId, "error"],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
this.fetchingFiles.clear();
|
this.fetchingFiles.clear();
|
||||||
this.savingFiles.clear();
|
this.savingFiles.clear();
|
||||||
this.savedFiles.clear();
|
this.savedFiles.clear();
|
||||||
|
|||||||
@@ -10,6 +10,12 @@
|
|||||||
* (localStorage, indexedDB).
|
* (localStorage, indexedDB).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { clearAppStateForLocalStorage } from "@excalidraw/excalidraw/appState";
|
||||||
|
import {
|
||||||
|
CANVAS_SEARCH_TAB,
|
||||||
|
DEFAULT_SIDEBAR,
|
||||||
|
debounce,
|
||||||
|
} from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
createStore,
|
createStore,
|
||||||
entries,
|
entries,
|
||||||
@@ -19,32 +25,31 @@ import {
|
|||||||
setMany,
|
setMany,
|
||||||
get,
|
get,
|
||||||
} from "idb-keyval";
|
} from "idb-keyval";
|
||||||
import { clearAppStateForLocalStorage } from "@excalidraw/excalidraw/appState";
|
|
||||||
import {
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
CANVAS_SEARCH_TAB,
|
|
||||||
DEFAULT_SIDEBAR,
|
|
||||||
} from "@excalidraw/excalidraw/constants";
|
|
||||||
import type { LibraryPersistedData } from "@excalidraw/excalidraw/data/library";
|
import type { LibraryPersistedData } from "@excalidraw/excalidraw/data/library";
|
||||||
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
||||||
import { clearElementsForLocalStorage } from "@excalidraw/excalidraw/element";
|
import type { ExcalidrawElement, FileId } from "@excalidraw/element/types";
|
||||||
import type {
|
|
||||||
ExcalidrawElement,
|
|
||||||
FileId,
|
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
} from "@excalidraw/excalidraw/types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
import type { MaybePromise } from "@excalidraw/excalidraw/utility-types";
|
import type { MaybePromise } from "@excalidraw/common/utility-types";
|
||||||
import { debounce } from "@excalidraw/excalidraw/utils";
|
|
||||||
|
import { appJotaiStore, atom } from "../app-jotai";
|
||||||
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
|
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
|
||||||
|
|
||||||
import { FileManager } from "./FileManager";
|
import { FileManager } from "./FileManager";
|
||||||
|
import { FileStatusStore } from "./fileStatusStore";
|
||||||
import { Locker } from "./Locker";
|
import { Locker } from "./Locker";
|
||||||
import { updateBrowserStateVersion } from "./tabSync";
|
import { updateBrowserStateVersion } from "./tabSync";
|
||||||
|
|
||||||
const filesStore = createStore("files-db", "files-store");
|
const filesStore = createStore("files-db", "files-store");
|
||||||
|
|
||||||
|
export const localStorageQuotaExceededAtom = atom(false);
|
||||||
|
|
||||||
class LocalFileManager extends FileManager {
|
class LocalFileManager extends FileManager {
|
||||||
clearObsoleteFiles = async (opts: { currentFileIds: FileId[] }) => {
|
clearObsoleteFiles = async (opts: { currentFileIds: FileId[] }) => {
|
||||||
await entries(filesStore).then((entries) => {
|
await entries(filesStore).then((entries) => {
|
||||||
@@ -69,6 +74,9 @@ const saveDataStateToLocalStorage = (
|
|||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
|
const localStorageQuotaExceeded = appJotaiStore.get(
|
||||||
|
localStorageQuotaExceededAtom,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const _appState = clearAppStateForLocalStorage(appState);
|
const _appState = clearAppStateForLocalStorage(appState);
|
||||||
|
|
||||||
@@ -81,19 +89,29 @@ const saveDataStateToLocalStorage = (
|
|||||||
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
||||||
JSON.stringify(clearElementsForLocalStorage(elements)),
|
JSON.stringify(getNonDeletedElements(elements)),
|
||||||
);
|
);
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
||||||
JSON.stringify(_appState),
|
JSON.stringify(_appState),
|
||||||
);
|
);
|
||||||
updateBrowserStateVersion(STORAGE_KEYS.VERSION_DATA_STATE);
|
updateBrowserStateVersion(STORAGE_KEYS.VERSION_DATA_STATE);
|
||||||
|
if (localStorageQuotaExceeded) {
|
||||||
|
appJotaiStore.set(localStorageQuotaExceededAtom, false);
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Unable to access window.localStorage
|
// Unable to access window.localStorage
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
if (isQuotaExceededError(error) && !localStorageQuotaExceeded) {
|
||||||
|
appJotaiStore.set(localStorageQuotaExceededAtom, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isQuotaExceededError = (error: any) => {
|
||||||
|
return error instanceof DOMException && error.name === "QuotaExceededError";
|
||||||
|
};
|
||||||
|
|
||||||
type SavingLockTypes = "collaboration";
|
type SavingLockTypes = "collaboration";
|
||||||
|
|
||||||
export class LocalData {
|
export class LocalData {
|
||||||
@@ -149,6 +167,7 @@ export class LocalData {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
static fileStorage = new LocalFileManager({
|
static fileStorage = new LocalFileManager({
|
||||||
|
onFileStatusChange: FileStatusStore.updateStatuses.bind(FileStatusStore),
|
||||||
getFiles(ids) {
|
getFiles(ids) {
|
||||||
return getMany(ids, filesStore).then(
|
return getMany(ids, filesStore).then(
|
||||||
async (filesData: (BinaryFileData | undefined)[]) => {
|
async (filesData: (BinaryFileData | undefined)[]) => {
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { createStore, get, set } from "idb-keyval";
|
||||||
|
|
||||||
|
import type { SavedChats } from "@excalidraw/excalidraw/components/TTDDialog/types";
|
||||||
|
|
||||||
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IndexedDB adapter for TTD chat storage.
|
||||||
|
* Implements TTDPersistenceAdapter interface.
|
||||||
|
*/
|
||||||
|
export class TTDIndexedDBAdapter {
|
||||||
|
/** IndexedDB database name */
|
||||||
|
private static idb_name = STORAGE_KEYS.IDB_TTD_CHATS;
|
||||||
|
/** Store key for chat data */
|
||||||
|
private static key = "ttdChats";
|
||||||
|
|
||||||
|
private static store = createStore(
|
||||||
|
`${TTDIndexedDBAdapter.idb_name}-db`,
|
||||||
|
`${TTDIndexedDBAdapter.idb_name}-store`,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load saved chats from IndexedDB.
|
||||||
|
* @returns Promise resolving to saved chats array (empty if none found)
|
||||||
|
*/
|
||||||
|
static async loadChats(): Promise<SavedChats> {
|
||||||
|
try {
|
||||||
|
const data = await get<SavedChats>(
|
||||||
|
TTDIndexedDBAdapter.key,
|
||||||
|
TTDIndexedDBAdapter.store,
|
||||||
|
);
|
||||||
|
return data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to load TTD chats from IndexedDB:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save chats to IndexedDB.
|
||||||
|
* @param chats - The chats array to persist
|
||||||
|
*/
|
||||||
|
static async saveChats(chats: SavedChats): Promise<void> {
|
||||||
|
try {
|
||||||
|
await set(TTDIndexedDBAdapter.key, chats, TTDIndexedDBAdapter.store);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to save TTD chats to IndexedDB:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { VersionedSnapshotStore } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type { FileId } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
export type FileLoadingStatus = "loading" | "loaded" | "error";
|
||||||
|
|
||||||
|
export class FileStatusStore {
|
||||||
|
private static store = new VersionedSnapshotStore<
|
||||||
|
Map<FileId, FileLoadingStatus>
|
||||||
|
>(new Map());
|
||||||
|
|
||||||
|
static getSnapshot() {
|
||||||
|
return this.store.getSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
static pull(sinceVersion?: number) {
|
||||||
|
return this.store.pull(sinceVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
static updateStatuses(updates: Array<[FileId, FileLoadingStatus]>) {
|
||||||
|
if (!updates.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.store.update((prev) => {
|
||||||
|
let changed = false;
|
||||||
|
const next = new Map(prev);
|
||||||
|
for (const [id, status] of updates) {
|
||||||
|
if (next.get(id) !== status) {
|
||||||
|
next.set(id, status);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed ? next : prev;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static getPendingCount(statuses: Map<FileId, FileLoadingStatus>) {
|
||||||
|
let pending = 0;
|
||||||
|
let total = 0;
|
||||||
|
for (const status of statuses.values()) {
|
||||||
|
total++;
|
||||||
|
if (status === "loading") {
|
||||||
|
pending++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { pending, total };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,12 @@
|
|||||||
import { reconcileElements } from "@excalidraw/excalidraw";
|
import { reconcileElements } from "@excalidraw/excalidraw";
|
||||||
import type {
|
import { MIME_TYPES, toBrandedType } from "@excalidraw/common";
|
||||||
ExcalidrawElement,
|
|
||||||
FileId,
|
|
||||||
OrderedExcalidrawElement,
|
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
|
||||||
import { getSceneVersion } from "@excalidraw/excalidraw/element";
|
|
||||||
import type Portal from "../collab/Portal";
|
|
||||||
import { restoreElements } from "@excalidraw/excalidraw/data/restore";
|
|
||||||
import type {
|
|
||||||
AppState,
|
|
||||||
BinaryFileData,
|
|
||||||
BinaryFileMetadata,
|
|
||||||
DataURL,
|
|
||||||
} from "@excalidraw/excalidraw/types";
|
|
||||||
import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
|
|
||||||
import { decompressData } from "@excalidraw/excalidraw/data/encode";
|
import { decompressData } from "@excalidraw/excalidraw/data/encode";
|
||||||
import {
|
import {
|
||||||
encryptData,
|
encryptData,
|
||||||
decryptData,
|
decryptData,
|
||||||
} from "@excalidraw/excalidraw/data/encryption";
|
} from "@excalidraw/excalidraw/data/encryption";
|
||||||
import { MIME_TYPES } from "@excalidraw/excalidraw/constants";
|
import { restoreElements } from "@excalidraw/excalidraw/data/restore";
|
||||||
import type { SyncableExcalidrawElement } from ".";
|
import { getSceneVersion } from "@excalidraw/element";
|
||||||
import { getSyncableElements } from ".";
|
|
||||||
import { initializeApp } from "firebase/app";
|
import { initializeApp } from "firebase/app";
|
||||||
import {
|
import {
|
||||||
getFirestore,
|
getFirestore,
|
||||||
@@ -31,8 +16,27 @@ import {
|
|||||||
Bytes,
|
Bytes,
|
||||||
} from "firebase/firestore";
|
} from "firebase/firestore";
|
||||||
import { getStorage, ref, uploadBytes } from "firebase/storage";
|
import { getStorage, ref, uploadBytes } from "firebase/storage";
|
||||||
import type { Socket } from "socket.io-client";
|
|
||||||
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
|
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
|
||||||
|
import type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
FileId,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
import type {
|
||||||
|
AppState,
|
||||||
|
BinaryFileData,
|
||||||
|
BinaryFileMetadata,
|
||||||
|
DataURL,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
|
||||||
|
|
||||||
|
import { getSyncableElements } from ".";
|
||||||
|
|
||||||
|
import type { SyncableExcalidrawElement } from ".";
|
||||||
|
import type Portal from "../collab/Portal";
|
||||||
|
import type { Socket } from "socket.io-client";
|
||||||
|
|
||||||
// private
|
// private
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -101,8 +105,8 @@ const decryptElements = async (
|
|||||||
data: FirebaseStoredScene,
|
data: FirebaseStoredScene,
|
||||||
roomKey: string,
|
roomKey: string,
|
||||||
): Promise<readonly ExcalidrawElement[]> => {
|
): Promise<readonly ExcalidrawElement[]> => {
|
||||||
const ciphertext = data.ciphertext.toUint8Array();
|
const ciphertext = data.ciphertext.toUint8Array() as Uint8Array<ArrayBuffer>;
|
||||||
const iv = data.iv.toUint8Array();
|
const iv = data.iv.toUint8Array() as Uint8Array<ArrayBuffer>;
|
||||||
|
|
||||||
const decrypted = await decryptData(iv, ciphertext, roomKey);
|
const decrypted = await decryptData(iv, ciphertext, roomKey);
|
||||||
const decodedData = new TextDecoder("utf-8").decode(
|
const decodedData = new TextDecoder("utf-8").decode(
|
||||||
@@ -239,7 +243,7 @@ export const saveToFirebase = async (
|
|||||||
|
|
||||||
FirebaseSceneVersionCache.set(socket, storedElements);
|
FirebaseSceneVersionCache.set(socket, storedElements);
|
||||||
|
|
||||||
return storedElements;
|
return toBrandedType<RemoteExcalidrawElement[]>(storedElements);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadFromFirebase = async (
|
export const loadFromFirebase = async (
|
||||||
@@ -255,7 +259,9 @@ export const loadFromFirebase = async (
|
|||||||
}
|
}
|
||||||
const storedScene = docSnap.data() as FirebaseStoredScene;
|
const storedScene = docSnap.data() as FirebaseStoredScene;
|
||||||
const elements = getSyncableElements(
|
const elements = getSyncableElements(
|
||||||
restoreElements(await decryptElements(storedScene, roomKey), null),
|
restoreElements(await decryptElements(storedScene, roomKey), null, {
|
||||||
|
deleteInvisibleElements: true,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (socket) {
|
if (socket) {
|
||||||
|
|||||||
@@ -8,35 +8,38 @@ import {
|
|||||||
IV_LENGTH_BYTES,
|
IV_LENGTH_BYTES,
|
||||||
} from "@excalidraw/excalidraw/data/encryption";
|
} from "@excalidraw/excalidraw/data/encryption";
|
||||||
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
|
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
|
||||||
import { restore } from "@excalidraw/excalidraw/data/restore";
|
import { isInvisiblySmallElement } from "@excalidraw/element";
|
||||||
|
import { isInitializedImageElement } from "@excalidraw/element";
|
||||||
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
|
import { bytesToHexString } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type { UserIdleState } from "@excalidraw/common";
|
||||||
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
||||||
import type { SceneBounds } from "@excalidraw/excalidraw/element/bounds";
|
import type { SceneBounds } from "@excalidraw/element";
|
||||||
import { isInvisiblySmallElement } from "@excalidraw/excalidraw/element/sizeHelpers";
|
|
||||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FileId,
|
FileId,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
SocketId,
|
SocketId,
|
||||||
} from "@excalidraw/excalidraw/types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
import type { UserIdleState } from "@excalidraw/excalidraw/constants";
|
import type { MakeBrand } from "@excalidraw/common/utility-types";
|
||||||
import type { MakeBrand } from "@excalidraw/excalidraw/utility-types";
|
|
||||||
import { bytesToHexString } from "@excalidraw/excalidraw/utils";
|
|
||||||
import type { WS_SUBTYPES } from "../app_constants";
|
|
||||||
import {
|
import {
|
||||||
DELETED_ELEMENT_TIMEOUT,
|
DELETED_ELEMENT_TIMEOUT,
|
||||||
FILE_UPLOAD_MAX_BYTES,
|
FILE_UPLOAD_MAX_BYTES,
|
||||||
ROOM_ID_BYTES,
|
ROOM_ID_BYTES,
|
||||||
} from "../app_constants";
|
} from "../app_constants";
|
||||||
|
|
||||||
import { encodeFilesForUpload } from "./FileManager";
|
import { encodeFilesForUpload } from "./FileManager";
|
||||||
import { saveFilesToFirebase } from "./firebase";
|
import { saveFilesToFirebase } from "./firebase";
|
||||||
|
|
||||||
|
import type { WS_SUBTYPES } from "../app_constants";
|
||||||
|
|
||||||
export type SyncableExcalidrawElement = OrderedExcalidrawElement &
|
export type SyncableExcalidrawElement = OrderedExcalidrawElement &
|
||||||
MakeBrand<"SyncableExcalidrawElement">;
|
MakeBrand<"SyncableExcalidrawElement">;
|
||||||
|
|
||||||
@@ -80,13 +83,13 @@ export type SocketUpdateDataSource = {
|
|||||||
SCENE_INIT: {
|
SCENE_INIT: {
|
||||||
type: WS_SUBTYPES.INIT;
|
type: WS_SUBTYPES.INIT;
|
||||||
payload: {
|
payload: {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly OrderedExcalidrawElement[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
SCENE_UPDATE: {
|
SCENE_UPDATE: {
|
||||||
type: WS_SUBTYPES.UPDATE;
|
type: WS_SUBTYPES.UPDATE;
|
||||||
payload: {
|
payload: {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly OrderedExcalidrawElement[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
MOUSE_LOCATION: {
|
MOUSE_LOCATION: {
|
||||||
@@ -196,7 +199,7 @@ const legacy_decodeFromBackend = async ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const importFromBackend = async (
|
export const importFromBackend = async (
|
||||||
id: string,
|
id: string,
|
||||||
decryptionKey: string,
|
decryptionKey: string,
|
||||||
): Promise<ImportedDataState> => {
|
): Promise<ImportedDataState> => {
|
||||||
@@ -238,40 +241,6 @@ const importFromBackend = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadScene = async (
|
|
||||||
id: string | null,
|
|
||||||
privateKey: string | null,
|
|
||||||
// Supply local state even if importing from backend to ensure we restore
|
|
||||||
// localStorage user settings which we do not persist on server.
|
|
||||||
// Non-optional so we don't forget to pass it even if `undefined`.
|
|
||||||
localDataState: ImportedDataState | undefined | null,
|
|
||||||
) => {
|
|
||||||
let data;
|
|
||||||
if (id != null && privateKey != null) {
|
|
||||||
// the private key is used to decrypt the content from the server, take
|
|
||||||
// extra care not to leak it
|
|
||||||
data = restore(
|
|
||||||
await importFromBackend(id, privateKey),
|
|
||||||
localDataState?.appState,
|
|
||||||
localDataState?.elements,
|
|
||||||
{ repairBindings: true, refreshDimensions: false },
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
data = restore(localDataState || null, null, null, {
|
|
||||||
repairBindings: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
elements: data.elements,
|
|
||||||
appState: data.appState,
|
|
||||||
// note: this will always be empty because we're not storing files
|
|
||||||
// in the scene database/localStorage, and instead fetch them async
|
|
||||||
// from a different database
|
|
||||||
files: data.files,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type ExportToBackendResult =
|
type ExportToBackendResult =
|
||||||
| { url: null; errorMessage: string }
|
| { url: null; errorMessage: string }
|
||||||
| { url: string; errorMessage: null };
|
| { url: string; errorMessage: null };
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { ExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
|
||||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
|
||||||
import {
|
import {
|
||||||
clearAppStateForLocalStorage,
|
clearAppStateForLocalStorage,
|
||||||
getDefaultAppState,
|
getDefaultAppState,
|
||||||
} from "@excalidraw/excalidraw/appState";
|
} from "@excalidraw/excalidraw/appState";
|
||||||
import { clearElementsForLocalStorage } from "@excalidraw/excalidraw/element";
|
|
||||||
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { STORAGE_KEYS } from "../app_constants";
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
|
|
||||||
export const saveUsernameToLocalStorage = (username: string) => {
|
export const saveUsernameToLocalStorage = (username: string) => {
|
||||||
@@ -48,7 +49,7 @@ export const importFromLocalStorage = () => {
|
|||||||
let elements: ExcalidrawElement[] = [];
|
let elements: ExcalidrawElement[] = [];
|
||||||
if (savedElements) {
|
if (savedElements) {
|
||||||
try {
|
try {
|
||||||
elements = clearElementsForLocalStorage(JSON.parse(savedElements));
|
elements = JSON.parse(savedElements);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
// Do nothing because elements array is already empty
|
// Do nothing because elements array is already empty
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Excalidraw | Hand-drawn look & feel • Collaborative • Secure</title>
|
<title>Excalidraw Whiteboard</title>
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no"
|
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no"
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<!-- Primary Meta Tags -->
|
<!-- Primary Meta Tags -->
|
||||||
<meta
|
<meta
|
||||||
name="title"
|
name="title"
|
||||||
content="Excalidraw — Collaborative whiteboarding made easy"
|
content="Free, collaborative whiteboard • Hand-drawn look & feel | Excalidraw"
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
|
|||||||
+27
-14
@@ -1,3 +1,5 @@
|
|||||||
|
@import "../packages/excalidraw/css/variables.module.scss";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
--color-primary-contrast-offset: #625ee0; // to offset Chubb illusion
|
--color-primary-contrast-offset: #625ee0; // to offset Chubb illusion
|
||||||
|
|
||||||
@@ -5,12 +7,6 @@
|
|||||||
--color-primary-contrast-offset: #726dff; // to offset Chubb illusion
|
--color-primary-contrast-offset: #726dff; // to offset Chubb illusion
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-right-ui {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-center {
|
.footer-center {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
@@ -58,7 +54,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.collab-offline-warning {
|
.alert {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6.5rem;
|
top: 6.5rem;
|
||||||
@@ -69,10 +65,18 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: var(--border-radius-md);
|
||||||
background-color: var(--color-warning);
|
|
||||||
color: var(--color-text-warning);
|
|
||||||
z-index: 6;
|
z-index: 6;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
|
||||||
|
&--warning {
|
||||||
|
background-color: var(--color-warning);
|
||||||
|
color: var(--color-text-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--danger {
|
||||||
|
background-color: var(--color-danger-dark);
|
||||||
|
color: var(--color-danger-text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,22 +86,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.plus-button {
|
.plus-banner {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 1px solid var(--color-primary);
|
border: 1px solid var(--color-primary);
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.875rem;
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: var(--border-radius-lg);
|
||||||
background-color: var(--island-bg-color);
|
background-color: var(--island-bg-color);
|
||||||
color: var(--color-primary) !important;
|
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
|
|
||||||
font-size: 0.75rem;
|
font-family: var(--ui-font);
|
||||||
|
font-size: 0.8333rem;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: var(--lg-button-size);
|
height: var(--lg-button-size);
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 0 0 1px var(--color-surface-lowest);
|
||||||
|
background-color: var(--color-surface-low);
|
||||||
|
color: var(--button-color, var(--color-on-surface)) !important;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
box-shadow: 0 0 0 1px var(--color-brand-active);
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--color-primary);
|
background-color: var(--color-primary);
|
||||||
color: white !important;
|
color: white !important;
|
||||||
@@ -109,7 +122,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.theme--dark {
|
.theme--dark {
|
||||||
.plus-button {
|
.plus-banner {
|
||||||
&:hover {
|
&:hover {
|
||||||
color: black !important;
|
color: black !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import ExcalidrawApp from "./App";
|
|
||||||
import { registerSW } from "virtual:pwa-register";
|
import { registerSW } from "virtual:pwa-register";
|
||||||
|
|
||||||
import "../excalidraw-app/sentry";
|
import "../excalidraw-app/sentry";
|
||||||
|
|
||||||
|
import ExcalidrawApp from "./App";
|
||||||
|
|
||||||
window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA;
|
window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA;
|
||||||
const rootElement = document.getElementById("root")!;
|
const rootElement = document.getElementById("root")!;
|
||||||
const root = createRoot(rootElement);
|
const root = createRoot(rootElement);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18.0.0 - 22.x.x"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@excalidraw/random-username": "1.0.0",
|
"@excalidraw/random-username": "1.0.0",
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"socket.io-client": "4.7.2",
|
"socket.io-client": "4.7.2",
|
||||||
|
"uqr": "0.1.2",
|
||||||
"vite-plugin-html": "3.2.2"
|
"vite-plugin-html": "3.2.2"
|
||||||
},
|
},
|
||||||
"prettier": "@excalidraw/prettier-config",
|
"prettier": "@excalidraw/prettier-config",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getFeatureFlag } from "@excalidraw/common";
|
||||||
import * as Sentry from "@sentry/browser";
|
import * as Sentry from "@sentry/browser";
|
||||||
import callsites from "callsites";
|
import callsites from "callsites";
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ Sentry.init({
|
|||||||
Sentry.captureConsoleIntegration({
|
Sentry.captureConsoleIntegration({
|
||||||
levels: ["error"],
|
levels: ["error"],
|
||||||
}),
|
}),
|
||||||
|
Sentry.featureFlagsIntegration(),
|
||||||
],
|
],
|
||||||
beforeSend(event) {
|
beforeSend(event) {
|
||||||
if (event.request?.url) {
|
if (event.request?.url) {
|
||||||
@@ -79,3 +81,14 @@ Sentry.init({
|
|||||||
return event;
|
return event;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const flagsIntegration =
|
||||||
|
Sentry.getClient()?.getIntegrationByName<Sentry.FeatureFlagsIntegration>(
|
||||||
|
"FeatureFlags",
|
||||||
|
);
|
||||||
|
if (flagsIntegration) {
|
||||||
|
flagsIntegration.addFeatureFlag(
|
||||||
|
"COMPLEX_BINDINGS",
|
||||||
|
getFeatureFlag("COMPLEX_BINDINGS"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Spinner from "@excalidraw/excalidraw/components/Spinner";
|
||||||
|
|
||||||
|
interface QRCodeProps {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QRCode = ({ value }: QRCodeProps) => {
|
||||||
|
const [svgData, setSvgData] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
|
||||||
|
import("./qrcode.chunk")
|
||||||
|
.then(({ generateQRCodeSVG }) => {
|
||||||
|
if (mounted) {
|
||||||
|
try {
|
||||||
|
setSvgData(generateQRCodeSVG(value));
|
||||||
|
} catch {
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (mounted) {
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
};
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!svgData) {
|
||||||
|
return (
|
||||||
|
<div className="ShareDialog__active__qrcode ShareDialog__active__qrcode--loading">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="ShareDialog__active__qrcode"
|
||||||
|
role="img"
|
||||||
|
aria-label="QR code for collaboration link"
|
||||||
|
dangerouslySetInnerHTML={{ __html: svgData }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -140,6 +140,31 @@
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__qrcode {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
align-self: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
|
||||||
|
$size: 150px;
|
||||||
|
width: $size;
|
||||||
|
height: $size;
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
width: $size;
|
||||||
|
height: $size;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--loading {
|
||||||
|
background: var(--island-bg-color);
|
||||||
|
border: 1px solid var(--dialog-border-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__description {
|
&__description {
|
||||||
border-top: 1px solid var(--color-gray-20);
|
border-top: 1px solid var(--color-gray-20);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
|
|
||||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||||
import { getFrame } from "@excalidraw/excalidraw/utils";
|
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
|
||||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
|
||||||
import { KEYS } from "@excalidraw/excalidraw/keys";
|
|
||||||
import { Dialog } from "@excalidraw/excalidraw/components/Dialog";
|
import { Dialog } from "@excalidraw/excalidraw/components/Dialog";
|
||||||
|
import { FilledButton } from "@excalidraw/excalidraw/components/FilledButton";
|
||||||
|
import { TextField } from "@excalidraw/excalidraw/components/TextField";
|
||||||
import {
|
import {
|
||||||
copyIcon,
|
copyIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
@@ -14,15 +12,19 @@ import {
|
|||||||
shareIOS,
|
shareIOS,
|
||||||
shareWindows,
|
shareWindows,
|
||||||
} from "@excalidraw/excalidraw/components/icons";
|
} from "@excalidraw/excalidraw/components/icons";
|
||||||
import { TextField } from "@excalidraw/excalidraw/components/TextField";
|
|
||||||
import { FilledButton } from "@excalidraw/excalidraw/components/FilledButton";
|
|
||||||
import type { CollabAPI } from "../collab/Collab";
|
|
||||||
import { activeRoomLinkAtom } from "../collab/Collab";
|
|
||||||
import { useUIAppState } from "@excalidraw/excalidraw/context/ui-appState";
|
import { useUIAppState } from "@excalidraw/excalidraw/context/ui-appState";
|
||||||
import { useCopyStatus } from "@excalidraw/excalidraw/hooks/useCopiedIndicator";
|
import { useCopyStatus } from "@excalidraw/excalidraw/hooks/useCopiedIndicator";
|
||||||
|
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||||
|
import { KEYS, getFrame } from "@excalidraw/common";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { atom, useAtom, useAtomValue } from "../app-jotai";
|
import { atom, useAtom, useAtomValue } from "../app-jotai";
|
||||||
|
import { activeRoomLinkAtom } from "../collab/Collab";
|
||||||
|
|
||||||
import "./ShareDialog.scss";
|
import "./ShareDialog.scss";
|
||||||
|
import { QRCode } from "./QRCode";
|
||||||
|
|
||||||
|
import type { CollabAPI } from "../collab/Collab";
|
||||||
|
|
||||||
type OnExportToBackend = () => void;
|
type OnExportToBackend = () => void;
|
||||||
type ShareDialogType = "share" | "collaborationOnly";
|
type ShareDialogType = "share" | "collaborationOnly";
|
||||||
@@ -141,6 +143,7 @@ const ActiveRoomDialog = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<QRCode value={activeRoomLink} />
|
||||||
<div className="ShareDialog__active__description">
|
<div className="ShareDialog__active__description">
|
||||||
<p>
|
<p>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { renderSVG } from "uqr";
|
||||||
|
|
||||||
|
export const generateQRCodeSVG = (text: string): string => {
|
||||||
|
return renderSVG(text);
|
||||||
|
};
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import ExcalidrawApp from "../App";
|
import { UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
import {
|
import {
|
||||||
mockBoundingClientRect,
|
mockBoundingClientRect,
|
||||||
render,
|
render,
|
||||||
restoreOriginalGetBoundingClientRect,
|
restoreOriginalGetBoundingClientRect,
|
||||||
} from "@excalidraw/excalidraw/tests/test-utils";
|
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
import { UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
import ExcalidrawApp from "../App";
|
||||||
|
|
||||||
describe("Test MobileMenu", () => {
|
describe("Test MobileMenu", () => {
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
@@ -17,30 +17,15 @@ describe("Test MobileMenu", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await render(<ExcalidrawApp />);
|
await render(<ExcalidrawApp />);
|
||||||
// @ts-ignore
|
h.app.refreshEditorInterface();
|
||||||
h.app.refreshViewportBreakpoints();
|
|
||||||
// @ts-ignore
|
|
||||||
h.app.refreshEditorBreakpoints();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
restoreOriginalGetBoundingClientRect();
|
restoreOriginalGetBoundingClientRect();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set device correctly", () => {
|
it("should set editor interface correctly", () => {
|
||||||
expect(h.app.device).toMatchInlineSnapshot(`
|
expect(h.app.editorInterface.formFactor).toBe("phone");
|
||||||
{
|
|
||||||
"editor": {
|
|
||||||
"canFitSidebar": false,
|
|
||||||
"isMobile": true,
|
|
||||||
},
|
|
||||||
"isTouchScreen": false,
|
|
||||||
"viewport": {
|
|
||||||
"isLandscape": false,
|
|
||||||
"isMobile": true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should initialize with welcome screen and hide once user interacts", async () => {
|
it("should initialize with welcome screen and hide once user interacts", async () => {
|
||||||
|
|||||||
@@ -50,7 +50,11 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
|
|||||||
<div
|
<div
|
||||||
class="welcome-screen-center__heading welcome-screen-decor excalifont"
|
class="welcome-screen-center__heading welcome-screen-decor excalifont"
|
||||||
>
|
>
|
||||||
All your data is saved locally in your browser.
|
Your drawings are saved in your browser's storage.
|
||||||
|
<br />
|
||||||
|
Browser storage can be cleared unexpectedly.
|
||||||
|
<br />
|
||||||
|
Save your work to a file regularly to avoid losing it.
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="welcome-screen-menu"
|
class="welcome-screen-menu"
|
||||||
@@ -198,7 +202,7 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
|
|||||||
<a
|
<a
|
||||||
class="welcome-screen-menu-item "
|
class="welcome-screen-menu-item "
|
||||||
href="undefined/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
href="undefined/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { vi } from "vitest";
|
import { CaptureUpdateAction, newElementWith } from "@excalidraw/excalidraw";
|
||||||
import { act, render, waitFor } from "@excalidraw/excalidraw/tests/test-utils";
|
|
||||||
import ExcalidrawApp from "../App";
|
|
||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
|
||||||
import { syncInvalidIndices } from "@excalidraw/excalidraw/fractionalIndex";
|
|
||||||
import {
|
import {
|
||||||
createRedoAction,
|
createRedoAction,
|
||||||
createUndoAction,
|
createUndoAction,
|
||||||
} from "@excalidraw/excalidraw/actions/actionHistory";
|
} from "@excalidraw/excalidraw/actions/actionHistory";
|
||||||
import { CaptureUpdateAction, newElementWith } from "@excalidraw/excalidraw";
|
import { syncInvalidIndices } from "@excalidraw/element";
|
||||||
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
import { act, render, waitFor } from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
|
import { StoreIncrement } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import type { DurableIncrement, EphemeralIncrement } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import ExcalidrawApp from "../App";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@@ -64,6 +69,79 @@ vi.mock("socket.io-client", () => {
|
|||||||
* i.e. multiplayer history tests could be a good first candidate, as we could test both history stacks simultaneously.
|
* i.e. multiplayer history tests could be a good first candidate, as we could test both history stacks simultaneously.
|
||||||
*/
|
*/
|
||||||
describe("collaboration", () => {
|
describe("collaboration", () => {
|
||||||
|
it("should emit two ephemeral increments even though updates get batched", async () => {
|
||||||
|
const durableIncrements: DurableIncrement[] = [];
|
||||||
|
const ephemeralIncrements: EphemeralIncrement[] = [];
|
||||||
|
|
||||||
|
await render(<ExcalidrawApp />);
|
||||||
|
|
||||||
|
h.store.onStoreIncrementEmitter.on((increment) => {
|
||||||
|
if (StoreIncrement.isDurable(increment)) {
|
||||||
|
durableIncrements.push(increment);
|
||||||
|
} else {
|
||||||
|
ephemeralIncrements.push(increment);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line dot-notation
|
||||||
|
expect(h.store["scheduledMicroActions"].length).toBe(0);
|
||||||
|
expect(durableIncrements.length).toBe(0);
|
||||||
|
expect(ephemeralIncrements.length).toBe(0);
|
||||||
|
|
||||||
|
const rectProps = {
|
||||||
|
type: "rectangle",
|
||||||
|
id: "A",
|
||||||
|
height: 200,
|
||||||
|
width: 100,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const rect = API.createElement({ ...rectProps });
|
||||||
|
|
||||||
|
API.updateScene({
|
||||||
|
elements: [rect],
|
||||||
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// expect(commitSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(durableIncrements.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// simulate two batched remote updates
|
||||||
|
act(() => {
|
||||||
|
h.app.updateScene({
|
||||||
|
elements: [newElementWith(h.elements[0], { x: 100 })],
|
||||||
|
captureUpdate: CaptureUpdateAction.NEVER,
|
||||||
|
});
|
||||||
|
h.app.updateScene({
|
||||||
|
elements: [newElementWith(h.elements[0], { x: 200 })],
|
||||||
|
captureUpdate: CaptureUpdateAction.NEVER,
|
||||||
|
});
|
||||||
|
|
||||||
|
// we scheduled two micro actions,
|
||||||
|
// which confirms they are going to be executed as part of one batched component update
|
||||||
|
// eslint-disable-next-line dot-notation
|
||||||
|
expect(h.store["scheduledMicroActions"].length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// altough the updates get batched,
|
||||||
|
// we expect two ephemeral increments for each update,
|
||||||
|
// and each such update should have the expected change
|
||||||
|
expect(ephemeralIncrements.length).toBe(2);
|
||||||
|
expect(ephemeralIncrements[0].change.elements.A).toEqual(
|
||||||
|
expect.objectContaining({ x: 100 }),
|
||||||
|
);
|
||||||
|
expect(ephemeralIncrements[1].change.elements.A).toEqual(
|
||||||
|
expect.objectContaining({ x: 200 }),
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line dot-notation
|
||||||
|
expect(h.store["scheduledMicroActions"].length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should allow to undo / redo even on force-deleted elements", async () => {
|
it("should allow to undo / redo even on force-deleted elements", async () => {
|
||||||
await render(<ExcalidrawApp />);
|
await render(<ExcalidrawApp />);
|
||||||
const rect1Props = {
|
const rect1Props = {
|
||||||
@@ -121,12 +199,13 @@ describe("collaboration", () => {
|
|||||||
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const undoAction = createUndoAction(h.history, h.store);
|
const undoAction = createUndoAction(h.history);
|
||||||
act(() => h.app.actionManager.executeAction(undoAction));
|
act(() => h.app.actionManager.executeAction(undoAction));
|
||||||
|
|
||||||
// with explicit undo (as addition) we expect our item to be restored from the snapshot!
|
// with explicit undo (as addition) we expect our item to be restored from the snapshot!
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(API.getUndoStack().length).toBe(1);
|
expect(API.getUndoStack().length).toBe(1);
|
||||||
|
expect(API.getRedoStack().length).toBe(1);
|
||||||
expect(API.getSnapshot()).toEqual([
|
expect(API.getSnapshot()).toEqual([
|
||||||
expect.objectContaining(rect1Props),
|
expect.objectContaining(rect1Props),
|
||||||
expect.objectContaining({ ...rect2Props, isDeleted: false }),
|
expect.objectContaining({ ...rect2Props, isDeleted: false }),
|
||||||
@@ -153,7 +232,7 @@ describe("collaboration", () => {
|
|||||||
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const redoAction = createRedoAction(h.history, h.store);
|
const redoAction = createRedoAction(h.history);
|
||||||
act(() => h.app.actionManager.executeAction(redoAction));
|
act(() => h.app.actionManager.executeAction(redoAction));
|
||||||
|
|
||||||
// with explicit redo (as removal) we again restore the element from the snapshot!
|
// with explicit redo (as removal) we again restore the element from the snapshot!
|
||||||
@@ -169,79 +248,5 @@ describe("collaboration", () => {
|
|||||||
expect.objectContaining({ ...rect2Props, isDeleted: true }),
|
expect.objectContaining({ ...rect2Props, isDeleted: true }),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => h.app.actionManager.executeAction(undoAction));
|
|
||||||
|
|
||||||
// simulate local update
|
|
||||||
API.updateScene({
|
|
||||||
elements: syncInvalidIndices([
|
|
||||||
h.elements[0],
|
|
||||||
newElementWith(h.elements[1], { x: 100 }),
|
|
||||||
]),
|
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(API.getUndoStack().length).toBe(2);
|
|
||||||
expect(API.getRedoStack().length).toBe(0);
|
|
||||||
expect(API.getSnapshot()).toEqual([
|
|
||||||
expect.objectContaining(rect1Props),
|
|
||||||
expect.objectContaining({ ...rect2Props, isDeleted: false, x: 100 }),
|
|
||||||
]);
|
|
||||||
expect(h.elements).toEqual([
|
|
||||||
expect.objectContaining(rect1Props),
|
|
||||||
expect.objectContaining({ ...rect2Props, isDeleted: false, x: 100 }),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
act(() => h.app.actionManager.executeAction(undoAction));
|
|
||||||
|
|
||||||
// we expect to iterate the stack to the first visible change
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(API.getUndoStack().length).toBe(1);
|
|
||||||
expect(API.getRedoStack().length).toBe(1);
|
|
||||||
expect(API.getSnapshot()).toEqual([
|
|
||||||
expect.objectContaining(rect1Props),
|
|
||||||
expect.objectContaining({ ...rect2Props, isDeleted: false, x: 0 }),
|
|
||||||
]);
|
|
||||||
expect(h.elements).toEqual([
|
|
||||||
expect.objectContaining(rect1Props),
|
|
||||||
expect.objectContaining({ ...rect2Props, isDeleted: false, x: 0 }),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// simulate force deleting the element remotely
|
|
||||||
API.updateScene({
|
|
||||||
elements: syncInvalidIndices([rect1]),
|
|
||||||
captureUpdate: CaptureUpdateAction.NEVER,
|
|
||||||
});
|
|
||||||
|
|
||||||
// snapshot was correctly updated and marked the element as deleted
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(API.getUndoStack().length).toBe(1);
|
|
||||||
expect(API.getRedoStack().length).toBe(1);
|
|
||||||
expect(API.getSnapshot()).toEqual([
|
|
||||||
expect.objectContaining(rect1Props),
|
|
||||||
expect.objectContaining({ ...rect2Props, isDeleted: true, x: 0 }),
|
|
||||||
]);
|
|
||||||
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
act(() => h.app.actionManager.executeAction(redoAction));
|
|
||||||
|
|
||||||
// with explicit redo (as update) we again restored the element from the snapshot!
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(API.getUndoStack().length).toBe(2);
|
|
||||||
expect(API.getRedoStack().length).toBe(0);
|
|
||||||
expect(API.getSnapshot()).toEqual([
|
|
||||||
expect.objectContaining({ id: "A", isDeleted: false }),
|
|
||||||
expect.objectContaining({ id: "B", isDeleted: true, x: 100 }),
|
|
||||||
]);
|
|
||||||
expect(h.history.isRedoStackEmpty).toBeTruthy();
|
|
||||||
expect(h.elements).toEqual([
|
|
||||||
expect.objectContaining({ id: "A", isDeleted: false }),
|
|
||||||
expect.objectContaining({ id: "B", isDeleted: true, x: 100 }),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useLayoutEffect, useState } from "react";
|
|
||||||
import { THEME } from "@excalidraw/excalidraw";
|
import { THEME } from "@excalidraw/excalidraw";
|
||||||
import { EVENT } from "@excalidraw/excalidraw/constants";
|
import { useEffect, useLayoutEffect, useState } from "react";
|
||||||
import type { Theme } from "@excalidraw/excalidraw/element/types";
|
|
||||||
import { CODES, KEYS } from "@excalidraw/excalidraw/keys";
|
import type { Theme } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { STORAGE_KEYS } from "./app_constants";
|
import { STORAGE_KEYS } from "./app_constants";
|
||||||
|
|
||||||
const getDarkThemeMediaQuery = (): MediaQueryList | undefined =>
|
const getDarkThemeMediaQuery = (): MediaQueryList | undefined =>
|
||||||
@@ -30,28 +30,10 @@ export const useHandleAppTheme = () => {
|
|||||||
mediaQuery?.addEventListener("change", handleChange);
|
mediaQuery?.addEventListener("change", handleChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeydown = (event: KeyboardEvent) => {
|
|
||||||
if (
|
|
||||||
!event[KEYS.CTRL_OR_CMD] &&
|
|
||||||
event.altKey &&
|
|
||||||
event.shiftKey &&
|
|
||||||
event.code === CODES.D
|
|
||||||
) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopImmediatePropagation();
|
|
||||||
setAppTheme(editorTheme === THEME.DARK ? THEME.LIGHT : THEME.DARK);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener(EVENT.KEYDOWN, handleKeydown, { capture: true });
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
mediaQuery?.removeEventListener("change", handleChange);
|
mediaQuery?.removeEventListener("change", handleChange);
|
||||||
document.removeEventListener(EVENT.KEYDOWN, handleKeydown, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}, [appTheme, editorTheme, setAppTheme]);
|
}, [appTheme]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_THEME, appTheme);
|
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_THEME, appTheme);
|
||||||
|
|||||||
@@ -23,29 +23,71 @@ export default defineConfig(({ mode }) => {
|
|||||||
envDir: "../",
|
envDir: "../",
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: [
|
alias: [
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/common$/,
|
||||||
|
replacement: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/common/src/index.ts",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/common\/(.*?)/,
|
||||||
|
replacement: path.resolve(__dirname, "../packages/common/src/$1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/element$/,
|
||||||
|
replacement: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/element/src/index.ts",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/element\/(.*?)/,
|
||||||
|
replacement: path.resolve(__dirname, "../packages/element/src/$1"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/excalidraw$/,
|
find: /^@excalidraw\/excalidraw$/,
|
||||||
replacement: path.resolve(__dirname, "../packages/excalidraw/index.tsx"),
|
replacement: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/excalidraw/index.tsx",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/excalidraw\/(.*?)/,
|
find: /^@excalidraw\/excalidraw\/(.*?)/,
|
||||||
replacement: path.resolve(__dirname, "../packages/excalidraw/$1"),
|
replacement: path.resolve(__dirname, "../packages/excalidraw/$1"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
find: /^@excalidraw\/utils$/,
|
|
||||||
replacement: path.resolve(__dirname, "../packages/utils/index.ts"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: /^@excalidraw\/utils\/(.*?)/,
|
|
||||||
replacement: path.resolve(__dirname, "../packages/utils/$1"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/math$/,
|
find: /^@excalidraw\/math$/,
|
||||||
replacement: path.resolve(__dirname, "../packages/math/index.ts"),
|
replacement: path.resolve(__dirname, "../packages/math/src/index.ts"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/math\/(.*?)/,
|
find: /^@excalidraw\/math\/(.*?)/,
|
||||||
replacement: path.resolve(__dirname, "../packages/math/$1"),
|
replacement: path.resolve(__dirname, "../packages/math/src/$1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/utils$/,
|
||||||
|
replacement: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/utils/src/index.ts",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/utils\/(.*?)/,
|
||||||
|
replacement: path.resolve(__dirname, "../packages/utils/src/$1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/fractional-indexing$/,
|
||||||
|
replacement: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/fractional-indexing/src/index.ts",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/laser-pointer$/,
|
||||||
|
replacement: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/laser-pointer/src/index.ts",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -74,6 +116,14 @@ export default defineConfig(({ mode }) => {
|
|||||||
// Taking the substring after "locales/"
|
// Taking the substring after "locales/"
|
||||||
return `locales/${id.substring(index + 8)}`;
|
return `locales/${id.substring(index + 8)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (id.includes("@excalidraw/mermaid-to-excalidraw")) {
|
||||||
|
return "mermaid-to-excalidraw";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id.includes("@codemirror/") || id.includes("@lezer/")) {
|
||||||
|
return "codemirror.chunk";
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -118,6 +168,11 @@ export default defineConfig(({ mode }) => {
|
|||||||
"**/locales/**",
|
"**/locales/**",
|
||||||
"service-worker.js",
|
"service-worker.js",
|
||||||
"**/*.chunk-*.js",
|
"**/*.chunk-*.js",
|
||||||
|
// CodeMirrorEditor can't be assigned a `.chunk` name via
|
||||||
|
// manualChunks because Rollup would hoist shared deps (React)
|
||||||
|
// via a static import from the main bundle, defeating lazy
|
||||||
|
// loading. So we exclude it by name instead.
|
||||||
|
"**/CodeMirrorEditor-*.js",
|
||||||
],
|
],
|
||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
{
|
||||||
@@ -157,7 +212,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
urlPattern: new RegExp(".chunk-.+.js"),
|
urlPattern: new RegExp("(.chunk-.+|CodeMirrorEditor-.+)\\.js"),
|
||||||
handler: "CacheFirst",
|
handler: "CacheFirst",
|
||||||
options: {
|
options: {
|
||||||
cacheName: "chunk",
|
cacheName: "chunk",
|
||||||
@@ -168,6 +223,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
maximumFileSizeToCacheInBytes: 2.3 * 1024 ** 2, // 2.3MB
|
||||||
},
|
},
|
||||||
manifest: {
|
manifest: {
|
||||||
short_name: "Excalidraw",
|
short_name: "Excalidraw",
|
||||||
@@ -197,7 +253,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
start_url: "/",
|
start_url: "/",
|
||||||
id:"excalidraw",
|
id: "excalidraw",
|
||||||
display: "standalone",
|
display: "standalone",
|
||||||
theme_color: "#121212",
|
theme_color: "#121212",
|
||||||
background_color: "#ffffff",
|
background_color: "#ffffff",
|
||||||
|
|||||||
+19
-12
@@ -4,9 +4,7 @@
|
|||||||
"packageManager": "yarn@1.22.22",
|
"packageManager": "yarn@1.22.22",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"excalidraw-app",
|
"excalidraw-app",
|
||||||
"packages/excalidraw",
|
"packages/*",
|
||||||
"packages/utils",
|
|
||||||
"packages/math",
|
|
||||||
"examples/*"
|
"examples/*"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -26,6 +24,7 @@
|
|||||||
"dotenv": "16.0.1",
|
"dotenv": "16.0.1",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
"eslint-config-react-app": "7.0.1",
|
"eslint-config-react-app": "7.0.1",
|
||||||
|
"eslint-plugin-import": "2.31.0",
|
||||||
"eslint-plugin-prettier": "3.3.1",
|
"eslint-plugin-prettier": "3.3.1",
|
||||||
"http-server": "14.1.1",
|
"http-server": "14.1.1",
|
||||||
"husky": "7.0.4",
|
"husky": "7.0.4",
|
||||||
@@ -34,7 +33,8 @@
|
|||||||
"pepjs": "0.5.3",
|
"pepjs": "0.5.3",
|
||||||
"prettier": "2.6.2",
|
"prettier": "2.6.2",
|
||||||
"rewire": "6.0.0",
|
"rewire": "6.0.0",
|
||||||
"typescript": "4.9.4",
|
"rimraf": "^5.0.0",
|
||||||
|
"typescript": "5.9.3",
|
||||||
"vite": "5.0.12",
|
"vite": "5.0.12",
|
||||||
"vite-plugin-checker": "0.7.2",
|
"vite-plugin-checker": "0.7.2",
|
||||||
"vite-plugin-ejs": "1.7.0",
|
"vite-plugin-ejs": "1.7.0",
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
"vitest-canvas-mock": "0.3.3"
|
"vitest-canvas-mock": "0.3.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18.0.0 - 22.x.x"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
"prettier": "@excalidraw/prettier-config",
|
"prettier": "@excalidraw/prettier-config",
|
||||||
@@ -52,13 +52,19 @@
|
|||||||
"build-node": "node ./scripts/build-node.js",
|
"build-node": "node ./scripts/build-node.js",
|
||||||
"build:app:docker": "yarn --cwd ./excalidraw-app build:app:docker",
|
"build:app:docker": "yarn --cwd ./excalidraw-app build:app:docker",
|
||||||
"build:app": "yarn --cwd ./excalidraw-app build:app",
|
"build:app": "yarn --cwd ./excalidraw-app build:app",
|
||||||
"build:package": "yarn --cwd ./packages/excalidraw build:esm",
|
"build:common": "yarn --cwd ./packages/common build:esm",
|
||||||
|
"build:element": "yarn --cwd ./packages/element build:esm",
|
||||||
|
"build:excalidraw": "yarn --cwd ./packages/excalidraw build:esm",
|
||||||
|
"build:math": "yarn --cwd ./packages/math build:esm",
|
||||||
|
"build:fractional-indexing": "yarn --cwd ./packages/fractional-indexing build:esm",
|
||||||
|
"build:laser-pointer": "yarn --cwd ./packages/laser-pointer build:esm",
|
||||||
|
"build:packages": "yarn build:common && yarn build:fractional-indexing && yarn build:laser-pointer && yarn build:math && yarn build:element && yarn build:excalidraw",
|
||||||
"build:version": "yarn --cwd ./excalidraw-app build:version",
|
"build:version": "yarn --cwd ./excalidraw-app build:version",
|
||||||
"build": "yarn --cwd ./excalidraw-app build",
|
"build": "yarn --cwd ./excalidraw-app build",
|
||||||
"build:preview": "yarn --cwd ./excalidraw-app build:preview",
|
"build:preview": "yarn --cwd ./excalidraw-app build:preview",
|
||||||
"start": "yarn --cwd ./excalidraw-app start",
|
"start": "yarn --cwd ./excalidraw-app start",
|
||||||
"start:production": "yarn --cwd ./excalidraw-app start:production",
|
"start:production": "yarn --cwd ./excalidraw-app start:production",
|
||||||
"start:example": "yarn build:package && yarn --cwd ./examples/with-script-in-browser start",
|
"start:example": "yarn build:packages && yarn --cwd ./examples/with-script-in-browser start",
|
||||||
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false",
|
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false",
|
||||||
"test:app": "vitest",
|
"test:app": "vitest",
|
||||||
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
|
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
|
||||||
@@ -76,11 +82,12 @@
|
|||||||
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
||||||
"autorelease": "node scripts/autorelease.js",
|
"release": "node scripts/release.js",
|
||||||
"prerelease:excalidraw": "node scripts/prerelease.js",
|
"release:test": "node scripts/release.js --tag=test",
|
||||||
"release:excalidraw": "node scripts/release.js",
|
"release:next": "node scripts/release.js --tag=next",
|
||||||
"rm:build": "rm -rf excalidraw-app/{build,dist,dev-dist} && rm -rf packages/*/{dist,build} && rm -rf examples/*/{build,dist}",
|
"release:latest": "node scripts/release.js --tag=latest",
|
||||||
"rm:node_modules": "rm -rf node_modules && rm -rf excalidraw-app/node_modules && rm -rf packages/*/node_modules",
|
"rm:build": "rimraf --glob excalidraw-app/build excalidraw-app/dist excalidraw-app/dev-dist packages/*/dist packages/*/build examples/*/build examples/*/dist",
|
||||||
|
"rm:node_modules": "rimraf --glob node_modules excalidraw-app/node_modules packages/*/node_modules",
|
||||||
"clean-install": "yarn rm:node_modules && yarn install"
|
"clean-install": "yarn rm:node_modules && yarn install"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["../eslintrc.base.json"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# @excalidraw/common
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @excalidraw/common
|
||||||
|
```
|
||||||
|
|
||||||
|
If you prefer Yarn over npm, use this command to install the Excalidraw utils package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @excalidraw/common
|
||||||
|
```
|
||||||
|
|
||||||
|
With PNPM, similarly install the package with this command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @excalidraw/common
|
||||||
|
```
|
||||||
@@ -1,17 +1,9 @@
|
|||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
debug: typeof Debug;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const lessPrecise = (num: number, precision = 5) =>
|
const lessPrecise = (num: number, precision = 5) =>
|
||||||
parseFloat(num.toPrecision(precision));
|
parseFloat(num.toPrecision(precision));
|
||||||
|
|
||||||
const getAvgFrameTime = (times: number[]) =>
|
const getAvgFrameTime = (times: number[]) =>
|
||||||
lessPrecise(times.reduce((a, b) => a + b) / times.length);
|
lessPrecise(times.reduce((a, b) => a + b) / times.length);
|
||||||
|
|
||||||
const getFps = (frametime: number) => lessPrecise(1000 / frametime);
|
|
||||||
|
|
||||||
export class Debug {
|
export class Debug {
|
||||||
public static DEBUG_LOG_TIMES = true;
|
public static DEBUG_LOG_TIMES = true;
|
||||||
|
|
||||||
@@ -24,34 +16,35 @@ export class Debug {
|
|||||||
private static LAST_DEBUG_LOG_CALL = 0;
|
private static LAST_DEBUG_LOG_CALL = 0;
|
||||||
private static DEBUG_LOG_INTERVAL_ID: null | number = null;
|
private static DEBUG_LOG_INTERVAL_ID: null | number = null;
|
||||||
|
|
||||||
|
private static LAST_FRAME_TIMESTAMP = 0;
|
||||||
|
private static FRAME_COUNT = 0;
|
||||||
|
private static ANIMATION_FRAME_ID: null | number = null;
|
||||||
|
|
||||||
|
private static scheduleAnimationFrame = () => {
|
||||||
|
if (Debug.DEBUG_LOG_INTERVAL_ID !== null) {
|
||||||
|
Debug.ANIMATION_FRAME_ID = requestAnimationFrame((timestamp) => {
|
||||||
|
if (Debug.LAST_FRAME_TIMESTAMP !== timestamp) {
|
||||||
|
Debug.LAST_FRAME_TIMESTAMP = timestamp;
|
||||||
|
Debug.FRAME_COUNT++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Debug.DEBUG_LOG_INTERVAL_ID !== null) {
|
||||||
|
Debug.scheduleAnimationFrame();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private static setupInterval = () => {
|
private static setupInterval = () => {
|
||||||
if (Debug.DEBUG_LOG_INTERVAL_ID === null) {
|
if (Debug.DEBUG_LOG_INTERVAL_ID === null) {
|
||||||
console.info("%c(starting perf recording)", "color: lime");
|
console.info("%c(starting perf recording)", "color: lime");
|
||||||
Debug.DEBUG_LOG_INTERVAL_ID = window.setInterval(Debug.debugLogger, 1000);
|
Debug.DEBUG_LOG_INTERVAL_ID = window.setInterval(Debug.debugLogger, 1000);
|
||||||
|
Debug.scheduleAnimationFrame();
|
||||||
}
|
}
|
||||||
Debug.LAST_DEBUG_LOG_CALL = Date.now();
|
Debug.LAST_DEBUG_LOG_CALL = Date.now();
|
||||||
};
|
};
|
||||||
|
|
||||||
private static debugLogger = () => {
|
private static debugLogger = () => {
|
||||||
if (
|
|
||||||
Date.now() - Debug.LAST_DEBUG_LOG_CALL > 600 &&
|
|
||||||
Debug.DEBUG_LOG_INTERVAL_ID !== null
|
|
||||||
) {
|
|
||||||
window.clearInterval(Debug.DEBUG_LOG_INTERVAL_ID);
|
|
||||||
Debug.DEBUG_LOG_INTERVAL_ID = null;
|
|
||||||
for (const [name, { avg }] of Object.entries(Debug.TIMES_AVG)) {
|
|
||||||
if (avg != null) {
|
|
||||||
console.info(
|
|
||||||
`%c${name} run avg: ${avg}ms (${getFps(avg)} fps)`,
|
|
||||||
"color: blue",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.info("%c(stopping perf recording)", "color: red");
|
|
||||||
Debug.TIMES_AGGR = {};
|
|
||||||
Debug.TIMES_AVG = {};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Debug.DEBUG_LOG_TIMES) {
|
if (Debug.DEBUG_LOG_TIMES) {
|
||||||
for (const [name, { t, times }] of Object.entries(Debug.TIMES_AGGR)) {
|
for (const [name, { t, times }] of Object.entries(Debug.TIMES_AGGR)) {
|
||||||
if (times.length) {
|
if (times.length) {
|
||||||
@@ -65,8 +58,18 @@ export class Debug {
|
|||||||
}
|
}
|
||||||
for (const [name, { t, times, avg }] of Object.entries(Debug.TIMES_AVG)) {
|
for (const [name, { t, times, avg }] of Object.entries(Debug.TIMES_AVG)) {
|
||||||
if (times.length) {
|
if (times.length) {
|
||||||
const avgFrameTime = getAvgFrameTime(times);
|
// const avgFrameTime = getAvgFrameTime(times);
|
||||||
console.info(name, `${avgFrameTime}ms (${getFps(avgFrameTime)} fps)`);
|
const totalTime = times.reduce((a, b) => a + b);
|
||||||
|
const avgFrameTime = lessPrecise(totalTime / Debug.FRAME_COUNT);
|
||||||
|
console.info(
|
||||||
|
name,
|
||||||
|
`- ${times.length} calls - ${avgFrameTime}ms/frame across ${
|
||||||
|
Debug.FRAME_COUNT
|
||||||
|
} frames (${lessPrecise(
|
||||||
|
(avgFrameTime / 16.67) * 100,
|
||||||
|
1,
|
||||||
|
)}% of frame budget)`,
|
||||||
|
);
|
||||||
Debug.TIMES_AVG[name] = {
|
Debug.TIMES_AVG[name] = {
|
||||||
t,
|
t,
|
||||||
times: [],
|
times: [],
|
||||||
@@ -76,6 +79,24 @@ export class Debug {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Debug.FRAME_COUNT = 0;
|
||||||
|
|
||||||
|
// Check for stop condition after logging
|
||||||
|
if (
|
||||||
|
Date.now() - Debug.LAST_DEBUG_LOG_CALL > 600 &&
|
||||||
|
Debug.DEBUG_LOG_INTERVAL_ID !== null
|
||||||
|
) {
|
||||||
|
console.info("%c(stopping perf recording)", "color: red");
|
||||||
|
window.clearInterval(Debug.DEBUG_LOG_INTERVAL_ID);
|
||||||
|
window.cancelAnimationFrame(Debug.ANIMATION_FRAME_ID!);
|
||||||
|
Debug.ANIMATION_FRAME_ID = null;
|
||||||
|
Debug.FRAME_COUNT = 0;
|
||||||
|
Debug.LAST_FRAME_TIMESTAMP = 0;
|
||||||
|
|
||||||
|
Debug.DEBUG_LOG_INTERVAL_ID = null;
|
||||||
|
Debug.TIMES_AGGR = {};
|
||||||
|
Debug.TIMES_AVG = {};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public static logTime = (time?: number, name = "default") => {
|
public static logTime = (time?: number, name = "default") => {
|
||||||
@@ -109,7 +130,7 @@ export class Debug {
|
|||||||
return (...args: T) => {
|
return (...args: T) => {
|
||||||
const t0 = performance.now();
|
const t0 = performance.now();
|
||||||
const ret = fn(...args);
|
const ret = fn(...args);
|
||||||
Debug.logTime(performance.now() - t0, name);
|
Debug[type](performance.now() - t0, name);
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -130,6 +151,70 @@ export class Debug {
|
|||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static CHANGED_CACHE: Record<string, Record<string, unknown>> = {};
|
||||||
|
|
||||||
|
public static logChanged(name: string, obj: Record<string, unknown>) {
|
||||||
|
const prev = Debug.CHANGED_CACHE[name];
|
||||||
|
|
||||||
|
Debug.CHANGED_CACHE[name] = obj;
|
||||||
|
|
||||||
|
if (!prev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allKeys = new Set([...Object.keys(prev), ...Object.keys(obj)]);
|
||||||
|
const changed: Record<string, { prev: unknown; next: unknown }> = {};
|
||||||
|
|
||||||
|
for (const key of allKeys) {
|
||||||
|
const prevVal = prev[key];
|
||||||
|
const nextVal = obj[key];
|
||||||
|
if (!deepEqual(prevVal, nextVal)) {
|
||||||
|
changed[key] = { prev: prevVal, next: nextVal };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(changed).length > 0) {
|
||||||
|
console.info(`[${name}] changed:`, changed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepEqual(a: unknown, b: unknown): boolean {
|
||||||
|
if (Object.is(a, b)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
a === null ||
|
||||||
|
b === null ||
|
||||||
|
typeof a !== "object" ||
|
||||||
|
typeof b !== "object"
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(a) !== Array.isArray(b)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keysA = Object.keys(a as Record<string, unknown>);
|
||||||
|
const keysB = Object.keys(b as Record<string, unknown>);
|
||||||
|
|
||||||
|
if (keysA.length !== keysB.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of keysA) {
|
||||||
|
if (
|
||||||
|
!deepEqual(
|
||||||
|
(a as Record<string, unknown>)[key],
|
||||||
|
(b as Record<string, unknown>)[key],
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
//@ts-ignore
|
|
||||||
window.debug = Debug;
|
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
import "@excalidraw/excalidraw/global";
|
||||||
|
import "@excalidraw/excalidraw/css";
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user