Compare commits

...

154 Commits

Author SHA1 Message Date
7988cdfb60 Merge branch 'master' of github.com:excalidraw/excalidraw into gcp-portal
* 'master' of github.com:excalidraw/excalidraw: (194 commits)
  fix: Make help toggle tabbable (#3310)
  chore: Update translations from Crowdin (#3270)
  chore(deps): bump @types/jest from 26.0.20 to 26.0.21 (#3298)
  chore(deps): bump @types/react-dom from 17.0.1 to 17.0.2 (#3296)
  chore(deps): bump @types/react from 17.0.2 to 17.0.3 (#3297)
  fix: Show Windows share icon for Windows users (#3306)
  fix: Update browser-fs-access to use new supported export (#3303)
  feat: replaces fontSize and fontFamily text with icons (#2857)
  fix: use random IV for link-sharing encryption (#2829) (#2833)
  fix: Don't scroll to content on INIT websocket message (#3291)
  docs: Release @excalidraw/excalidraw@0.5.0 🎉  (#3289)
  feat: set window.name in excalidraw app & also support target for excalidraw libraries (#3299)
  chore(deps-dev): bump css-loader in /src/packages/utils (#3292)
  chore(deps-dev): bump css-loader in /src/packages/excalidraw (#3293)
  chore(deps-dev): bump webpack in /src/packages/utils (#3294)
  chore(deps-dev): bump webpack in /src/packages/excalidraw (#3295)
  feat: support pasting file contents & always prefer system clip (#3257)
  fix: Don't show export and delete when library is empty (#3288)
  feat: Add label for name field and use input when editable in export dialog (#3286)
  fix: overflow in textinput in export dialog (#3284)
  ...
2021-03-23 13:58:07 +02:00
40656c70d1 fix: Make help toggle tabbable (#3310)
* fix: Make help toggle tabbable

* Update src/components/HelpIcon.tsx

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-03-23 16:48:10 +05:30
c5b4b04d6b chore: Update translations from Crowdin (#3270) 2021-03-22 22:51:06 +02:00
1ad212677b chore(deps): bump @types/jest from 26.0.20 to 26.0.21 (#3298)
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 26.0.20 to 26.0.21.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-22 22:50:15 +02:00
32427c355c chore(deps): bump @types/react-dom from 17.0.1 to 17.0.2 (#3296)
Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 17.0.1 to 17.0.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-22 18:12:02 +00:00
402a812159 chore(deps): bump @types/react from 17.0.2 to 17.0.3 (#3297)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 17.0.2 to 17.0.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-22 20:00:23 +02:00
0480753581 fix: Show Windows share icon for Windows users (#3306)
* fix: Show Windows share icon for Windows users

* move function outside t he component
2021-03-22 17:02:20 +01:00
f7e17a28fa fix: Update browser-fs-access to use new supported export (#3303)
* Use new exported supported

* Bump to v0.15.3
2021-03-22 14:58:26 +01:00
78f3a92dd1 feat: replaces fontSize and fontFamily text with icons (#2857)
Co-authored-by: Hitesh Goyal <hiteshlyearn@Hiteshs-MacBook-Pro.local>
Co-authored-by: dwelle <luzar.david@gmail.com>
2021-03-22 14:26:35 +01:00
c8743a8c02 fix: use random IV for link-sharing encryption (#2829) (#2833)
* fix: use random IV for link-sharing encryption (#2829)

* fix: add backward compatibility for link-sharing encryption (#2829)
2021-03-21 22:31:35 -07:00
127c1be6ad fix: Don't scroll to content on INIT websocket message (#3291)
If you load a shared scene with at least another person on the scene, you can start seeing the content via the firebase response. If you scroll and you receive the response from the websocket INIT, then it scrolls you back to the center which is jarring.

This PR removes the scroll to content for that use case.
2021-03-21 17:25:19 +01:00
86bf2d697d docs: Release @excalidraw/excalidraw@0.5.0 🎉 (#3289)
* docs: Release @excalidraw/excalidraw@0.5.0

* update changelog

* update readme

* remove styles since github strips the styles in markdown
2021-03-21 19:19:09 +05:30
7ee8de0a46 feat: set window.name in excalidraw app & also support target for excalidraw libraries (#3299)
* feat: set window.name in excalidraw app so library installation always opens on same tab & also support target for excalidraw libraries

* update changelog and readme

* Update public/index.html

Co-authored-by: David Luzar <luzar.david@gmail.com>

* use level 4 heading

* Update src/packages/excalidraw/README.md

Co-authored-by: David Luzar <luzar.david@gmail.com>

Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-03-21 18:13:52 +05:30
981f327b48 chore(deps-dev): bump css-loader in /src/packages/utils (#3292)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.1.2 to 5.1.3.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.1.2...v5.1.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-21 12:55:37 +05:30
eeea8406c9 chore(deps-dev): bump css-loader in /src/packages/excalidraw (#3293)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.1.2 to 5.1.3.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.1.2...v5.1.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-21 12:31:15 +05:30
0f249e3b26 chore(deps-dev): bump webpack in /src/packages/utils (#3294)
Bumps [webpack](https://github.com/webpack/webpack) from 5.24.3 to 5.27.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.24.3...v5.27.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-21 12:29:44 +05:30
2c7c80bd75 chore(deps-dev): bump webpack in /src/packages/excalidraw (#3295)
Bumps [webpack](https://github.com/webpack/webpack) from 5.24.3 to 5.27.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.24.3...v5.27.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-21 12:21:09 +05:30
94ad8eaa19 feat: support pasting file contents & always prefer system clip (#3257) 2021-03-20 20:20:47 +01:00
13d9374cde fix: Don't show export and delete when library is empty (#3288) 2021-03-20 21:58:37 +05:30
efb6d0825b feat: Add label for name field and use input when editable in export dialog (#3286)
* feat: Add label for name field and use input when editable in export dialog

* fix

* review fix

* dnt allow to edit file name when view mode

* Update src/components/ProjectName.tsx

Co-authored-by: David Luzar <luzar.david@gmail.com>

Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-03-20 21:57:58 +05:30
80a61db72f fix: overflow in textinput in export dialog (#3284)
* fix: overflow in textinput in export dialog

* use width
2021-03-20 18:21:48 +05:30
9a13dd8836 fix: bail on noop updates for newElementWith (#3279) 2021-03-20 13:29:53 +01:00
cf6a5ff16b fix: state continuously updated when holding ctrl/cmd (#3283) 2021-03-20 13:28:28 +01:00
fa8c7abf50 fix: debounce.flush not invoked if lastArgs not defined (#3281) 2021-03-20 13:15:28 +01:00
c3ecbcb3ab feat: Allow host app to update title of drawing (#3273)
* Allow updating name on updateScene

* Revert "Allow updating name on updateScene"

This reverts commit 4e07a608d3.

* Make requested changes

* Make requested changes

* Remove customName from state

* Remove redundant if statement

* Add tests, update changelog and minor fixes

* remove eempty lines

* minor fixes

* no border and on hover no background change

* Give preference to name prop when initialData.appState.name is present and update specs

* minor fix

* Fix name input style in dark mode

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-03-20 16:08:03 +05:30
de99484a1f feat: Expose the API to calculate offsets and remove offsetTop and offsetLeft props (#3265)
* feat: Expose the API to calculate offsets and remove offsetTop and offsetLeft props

* update

* fix tests

* fix

* update readme and changelog

* fix

* better
2021-03-20 13:00:49 +05:30
add1785ace fix: allow copying text outside the component (#3275) 2021-03-19 18:36:23 +01:00
0e3eb3cc63 chore: fix npm to yarn in contributing.md and package.json (#3274)
* chore: fix npm to yarn in contributing.md

* chore: fix npm pack to yarn pack
2021-03-18 22:57:00 +05:30
c1379c3c10 fix: Revert 906faaf0 #3206 (#3269) 2021-03-17 14:43:58 +01:00
70c36e196c chore(deps): bump @types/socket.io-client from 1.4.35 to 1.4.36 (#3256)
Bumps [@types/socket.io-client](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/socket.io-client) from 1.4.35 to 1.4.36.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/socket.io-client)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-17 10:17:18 +02:00
9757f8e2e5 chore: Update translations from Crowdin (#3221) 2021-03-17 10:16:22 +02:00
052b73d95b refactor: Rename prop scrollToCenter and setScrollToCenter to scrollToContent and setScrollToContent respectively (#3261)
* refactor: Rename prop scrollToCenter and setScrollToCenter to scrollToContent and setScrollToContent respectively

* fix

* update changelog/readme

* fix
2021-03-16 23:02:17 +05:30
e90e56452f fix: stop preventing canvas pointerdown/tapend events (#3207) 2021-03-16 18:04:53 +01:00
edc62c550a feat: Export API's to export the drawing to canvas, svg and blob (#3258)
* feat: Export utilities from package/utils

* update

* fix

* collapsible

* Update README.md

* update change

* update

* fix

* Apply suggestions from code review

Co-authored-by: David Luzar <luzar.david@gmail.com>

* Apply suggestions from code review

Co-authored-by: David Luzar <luzar.david@gmail.com>

* update

* Apply suggestions from code review

Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-03-16 22:21:56 +05:30
84a1863233 feat: Add theme prop (#3228)
* support appearance when updating scene data

* works!

* whoops, missed a prop

* hide appearance button when prop is not set

* cleanup

* fix export + rename prop to theme

* rename to showThemeBtn, hide via react instead of css

* adapt to new state name

* add tests and css selector to target the dark mode toggle

* updated changelog and readme

* fix markdown rendering in readme

* pr feedback
2021-03-16 00:03:46 +05:30
1f295955d0 chore: Add vercel & sentry as sponsors (#3202)
* add vercel & sentry as sponsors

* fix lint

* move images to `.github/assets`

* fix lint

* add Crowdin & change copy
2021-03-14 15:02:36 +01:00
8f45aa2924 fix: double scrollbar on modals (#3226) 2021-03-14 14:30:12 +01:00
b4c0bc6ace chore(deps): bump nanoid from 3.1.20 to 3.1.21 (#3252)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.20 to 3.1.21.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.1.20...3.1.21)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-14 07:33:03 +00:00
8fdc25ce10 chore(deps-dev): bump firebase-tools from 9.5.0 to 9.6.1 (#3254)
Bumps [firebase-tools](https://github.com/firebase/firebase-tools) from 9.5.0 to 9.6.1.
- [Release notes](https://github.com/firebase/firebase-tools/releases)
- [Commits](https://github.com/firebase/firebase-tools/compare/v9.5.0...v9.6.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-14 07:30:40 +00:00
efc2db3af1 chore(deps): bump react-dev-utils from 11.0.3 to 11.0.4 (#3255)
Bumps [react-dev-utils](https://github.com/facebook/create-react-app/tree/HEAD/packages/react-dev-utils) from 11.0.3 to 11.0.4.
- [Release notes](https://github.com/facebook/create-react-app/releases)
- [Changelog](https://github.com/facebook/create-react-app/blob/master/CHANGELOG-1.x.md)
- [Commits](https://github.com/facebook/create-react-app/commits/HEAD/packages/react-dev-utils)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-14 09:24:05 +02:00
fa186c69cb chore(deps-dev): bump @babel/core from 7.13.8 to 7.13.10 in /src/packages/utils (#3248)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.13.8 to 7.13.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.10/packages/babel-core)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-14 09:23:39 +02:00
f2adda46e5 chore(deps-dev): bump ts-loader from 8.0.17 to 8.0.18 in /src/packages/utils (#3246)
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 8.0.17 to 8.0.18.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/v8.0.17...v8.0.18)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-14 09:23:33 +02:00
0786b472cc chore(deps-dev): bump @babel/preset-env from 7.13.9 to 7.13.10 in /src/packages/utils (#3251)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.13.9 to 7.13.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.10/packages/babel-preset-env)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-14 09:23:25 +02:00
15ae64eb7a chore(deps-dev): bump ts-loader from 8.0.17 to 8.0.18 in /src/packages/excalidraw (#3247)
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 8.0.17 to 8.0.18.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/v8.0.17...v8.0.18)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-14 09:23:18 +02:00
f3b9bd7c1d chore(deps-dev): bump @babel/plugin-transform-runtime from 7.13.9 to 7.13.10 in /src/packages/utils (#3250)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-14 09:23:05 +02:00
eae25329f0 chore(deps-dev): bump css-loader from 5.1.1 to 5.1.2 in /src/packages/utils (#3249)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.1.1 to 5.1.2.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.1.1...v5.1.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-14 09:22:51 +02:00
99de8e79e2 chore(deps-dev): bump @babel/plugin-transform-runtime from 7.13.9 to 7.13.10 in /src/packages/excalidraw (#3245)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.13.9 to 7.13.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.10/packages/babel-plugin-transform-runtime)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-14 09:22:35 +02:00
adcfaf34de chore(deps-dev): bump @babel/core from 7.13.8 to 7.13.10 in /src/packages/excalidraw (#3242)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.13.8 to 7.13.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.10/packages/babel-core)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-14 09:21:57 +02:00
04ae836874 chore(deps-dev): bump css-loader from 5.1.1 to 5.1.2 in /src/packages/excalidraw (#3243)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.1.1 to 5.1.2.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.1.1...v5.1.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-14 09:21:40 +02:00
f6ffa7ed45 chore(deps-dev): bump @babel/preset-env from 7.13.9 to 7.13.10 in /src/packages/excalidraw (#3244)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.13.9 to 7.13.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.10/packages/babel-preset-env)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-14 09:21:27 +02:00
2964d36d9a chore(deps): bump @sentry/browser from 6.2.1 to 6.2.2 (#3253)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 6.2.1 to 6.2.2.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/6.2.1...6.2.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-14 09:20:12 +02:00
b9e70ec666 feat: Implement the Web Share Target API (#3230)
* Use the web share target API

* Make requested changes

* Remove line

* Add application/json back

* Add application/vnd.excalidraw+json

* Add 'POST' check back

* Make requested changes

* Update src/appState.ts

Co-authored-by: Thomas Steiner <tomac@google.com>

* Update test

* Override initializeScene

* Use Excalidraw MIME type

* Minor fixes

* More MIME type tweaks

* More permissive file open

* Be overpermissive in file open

Co-authored-by: Thomas Steiner <tomac@google.com>
Co-authored-by: tomayac <steiner.thomas@gmail.com>
2021-03-13 22:42:54 +01:00
f1daff2437 refactor: Rename appearance to theme (#3237)
* refactor: Rename appearance to theme

* fix

* update changelog

* rename theme_dark to theme--dark

* add about migration
2021-03-13 18:58:06 +05:30
91c8b6ecbf feat: support libraryReturnUrl when installing libraries (#3227)
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-03-13 12:35:35 +01:00
47c26cd4cf docs: release patch version 0.4.3 (#3236) 2021-03-13 01:29:54 +05:30
3780a155f4 fix: Apply correct translation when text editor overflows when zoom not 100% (#3225)
* fix: Apply correct translation when zoom not 100%

* fix

* fix

* Update src/element/textWysiwyg.tsx

Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-03-12 02:38:50 +05:30
6252b22b42 feat: Implement the Web Share API for the collaboration dialog (#3222)
* feat: Implement the Web Share API for the collaboration dialog

* Make requested changes
2021-03-11 12:21:17 +01:00
bb612fd768 chore: Track collaboration (#3211) 2021-03-10 17:45:37 +02:00
1bb3d71d22 chore(deps): bump browser-fs-access from 0.13.1 to 0.14.1 (#3201)
Bumps [browser-fs-access](https://github.com/GoogleChromeLabs/browser-fs-access) from 0.13.1 to 0.14.1.
- [Release notes](https://github.com/GoogleChromeLabs/browser-fs-access/releases)
- [Commits](https://github.com/GoogleChromeLabs/browser-fs-access/commits)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-10 11:14:00 +01:00
3912e87b1d chore(deps): bump firebase from 8.2.9 to 8.2.10 (#3208)
Bumps [firebase](https://github.com/firebase/firebase-js-sdk) from 8.2.9 to 8.2.10.
- [Release notes](https://github.com/firebase/firebase-js-sdk/releases)
- [Changelog](https://github.com/firebase/firebase-js-sdk/blob/master/CHANGELOG.md)
- [Commits](https://github.com/firebase/firebase-js-sdk/compare/firebase@8.2.9...firebase@8.2.10)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-10 11:51:13 +02:00
72f1134878 chore: Update translations from Crowdin (#3179) 2021-03-10 10:57:04 +02:00
6e629383ea fix: Don't overflow text beyond width of Excalidraw (#3215)
* fix: Don't overflow text beyond width of Excalidraw

* update changelog
2021-03-10 01:49:32 +05:30
612e71e38b docs: Remove unreleased link as its already available (#3212) 2021-03-09 21:44:56 +05:30
00ac804dd8 fix: Allow copy of excalidraw elements only when inside excalidraw (#3206)
* fix: Allow copy event only inside canvas

* use pointerdown

* use document.getSelection

* remove poiterdown

* remove keyTest

* update changelog
2021-03-09 11:52:28 +05:30
1f7f07fbfb Remove left-over ToDo comment 2021-03-08 17:47:01 +01:00
beffc290fd feat: support importing scene from url (#2726) 2021-03-08 16:37:26 +01:00
9b58efd363 chore(deps): bump @sentry/integrations from 6.2.0 to 6.2.1 (#3209)
Bumps [@sentry/integrations](https://github.com/getsentry/sentry-javascript) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/6.2.0...6.2.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-08 17:26:18 +02:00
23b672484b chore(deps): bump @sentry/browser from 6.2.0 to 6.2.1 (#3194)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/6.2.0...6.2.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-08 15:09:20 +02:00
3a0a638a0d fix: Position text editor absolute and fix the offsets so it doesn't remain fixed when container is scrolled (#3200)
* fix: Position text editor absolute and fix the offsets so it doesn't remain fixed when container is scrolled

* update changelog

* Update src/packages/excalidraw/CHANGELOG.md
2021-03-07 21:12:10 +05:30
862c065e33 chore(deps): bump typescript from 4.1.5 to 4.2.3 (#3196)
* chore(deps): bump typescript from 4.1.5 to 4.2.3

Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.1.5 to 4.2.3.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.1.5...v4.2.3)

Signed-off-by: dependabot[bot] <support@github.com>

* hack types

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
2021-03-07 15:52:59 +01:00
420703ba50 fix: scope css variables so that host css vars don't clash with excalidraw (#3199)
* fix: scope css variables so that host css vars don't clash with excalidraw

* update changelog
2021-03-07 16:41:37 +05:30
4d6ef81c83 chore(deps-dev): bump css-loader from 5.1.0 to 5.1.1 in /src/packages/utils (#3189)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.1.0 to 5.1.1.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.1.0...v5.1.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-07 10:44:53 +01:00
88b980a8e0 chore(deps-dev): bump @babel/preset-env from 7.13.8 to 7.13.9 in /src/packages/utils (#3191)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.13.8 to 7.13.9.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.9/packages/babel-preset-env)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-07 10:44:39 +01:00
d85c1955b4 chore(deps-dev): bump @babel/plugin-transform-runtime from 7.13.8 to 7.13.9 in /src/packages/utils (#3192)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.13.8 to 7.13.9.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.9/packages/babel-plugin-transform-runtime)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-07 10:44:22 +01:00
a27e599798 chore(deps-dev): bump @babel/plugin-transform-runtime (#3190)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.13.8 to 7.13.9.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.9/packages/babel-plugin-transform-runtime)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-07 14:22:28 +05:30
d25ed7bf88 chore(deps-dev): bump @babel/preset-env in /src/packages/excalidraw (#3188)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.13.8 to 7.13.9.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.9/packages/babel-preset-env)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-07 14:16:52 +05:30
28601b88bd chore(deps-dev): bump css-loader in /src/packages/excalidraw (#3187)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.1.0 to 5.1.1.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.1.0...v5.1.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-07 14:15:57 +05:30
1edfcc08d8 chore(deps-dev): bump webpack in /src/packages/excalidraw (#3186)
Bumps [webpack](https://github.com/webpack/webpack) from 5.24.2 to 5.24.3.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.24.2...v5.24.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-07 14:15:03 +05:30
030814c254 chore(deps-dev): bump webpack in /src/packages/utils (#3193)
Bumps [webpack](https://github.com/webpack/webpack) from 5.24.2 to 5.24.3.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.24.2...v5.24.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-07 13:33:35 +05:30
eb24e8ffe4 fix: Wrap excalidraw in position relative & hide scrollbars in zen mode (#3174)
* fix: wrap excalidraw in position relative so that host need not add it explicitly to fix positioning when non zero offsets

* patch version

* Hide scrollbars on excalidraw container

* remove overflow hidden from index.html since its handled in excalidraw container

* review fix

* update changelog
2021-03-06 20:11:26 +05:30
07e71a8071 fix: Reduce the scroll debounce timeout to 100ms and update only if offset changes (#3182)
* fix: Reduce the scroll debounce timeout to 100ms so offsets get updated faster when the container scrolled
fixes https://github.com/excalidraw/excalidraw/issues/3175

* update changelog

* update offsets only when if it changes

* up

* Update src/components/App.tsx

Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-03-06 19:06:42 +05:30
bf97414530 fix: rerender UI on renderFooter prop change (#3183) 2021-03-06 14:23:25 +01:00
f8c3c431da chore: Update translations from Crowdin (#3166) 2021-03-05 17:09:40 +02:00
ef792ad906 docs: update readme with codesandbox example when excalidraw embeded via script tag (#3171)
* docs(README.md): update codesandbox example when excalidraw embeded via script tag

* up
2021-03-05 18:02:21 +05:30
06a945aded fix: temporarily downgrade browser-fs-access to fix legacy FS API (#3172) 2021-03-04 22:55:50 +01:00
0d2f72f87e docs(README.md): fix typo excalidrawRef -> ref (#3170) 2021-03-04 20:20:01 +05:30
3d1cbf444d fix: Use Array.from when spreading over set so that typescript transpiles correctly in the umd build (#3165)
* fix: Use Array.from when spreading over set so that typescript transpiles correctly in the umd build

* patch version

* fix

* update changelog

* tweak
2021-03-03 19:27:15 +05:30
c77c9ce65a fix: cursor being leaked outside of canvas (#3161) 2021-03-03 14:04:02 +01:00
f295ba98c5 feat: Add export info on copy PNG to clipboard toast message (#3159)
Co-authored-by: dwelle <luzar.david@gmail.com>
2021-03-03 14:15:10 +02:00
91eb8834e8 chore: host workbox locally (#3154) 2021-03-02 19:36:28 +01:00
418589e7ad chore: Use @excalidraw/eslint-config (#3142) 2021-03-02 12:29:32 +02:00
bd63dcbcc5 chore: Update translations from Crowdin (#3017) 2021-03-02 08:50:49 +00:00
785a944ac2 Chore: Update .nvmrc to match package.json engines definition (#3148)
Engines is defined as ">=14.0.0" in package.json but .nvmrc specifies the node version as 12.
This commit updates .nvmrc to specify node version as 14
2021-03-01 13:22:20 +01:00
32acde500e fix: hide scrollbars in zenMode (#3144) 2021-02-28 20:20:16 +01:00
053353841a feat: Use the latest version of Virgil (#3124) 2021-02-28 13:22:32 +01:00
286642ffcf chore(deps-dev): bump mini-css-extract-plugin from 1.3.8 to 1.3.9 in /src/packages/excalidraw (#3129)
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 1.3.8 to 1.3.9.
- [Release notes](https://github.com/webpack-contrib/mini-css-extract-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/mini-css-extract-plugin/compare/v1.3.8...v1.3.9)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-28 11:49:29 +02:00
7090938ec1 chore(deps-dev): bump css-loader from 5.0.2 to 5.1.0 in /src/packages/excalidraw (#3141)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.0.2 to 5.1.0.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.0.2...v5.1.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-28 11:48:34 +02:00
6233bc52e3 chore(deps): bump browser-fs-access from 0.13.1 to 0.14.0 (#3140)
Bumps [browser-fs-access](https://github.com/GoogleChromeLabs/browser-fs-access) from 0.13.1 to 0.14.0.
- [Release notes](https://github.com/GoogleChromeLabs/browser-fs-access/releases)
- [Commits](https://github.com/GoogleChromeLabs/browser-fs-access/commits)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-28 09:14:23 +00:00
7f8ee06710 chore(deps-dev): bump @babel/preset-env from 7.13.5 to 7.13.8 in /src/packages/utils (#3135)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.13.5 to 7.13.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.8/packages/babel-preset-env)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-28 11:13:30 +02:00
3e200634e0 chore(deps-dev): bump @babel/plugin-transform-runtime from 7.12.17 to 7.13.8 in /src/packages/utils (#3133)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.12.17 to 7.13.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.8/packages/babel-plugin-transform-runtime)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-28 11:13:01 +02:00
701b02d6df chore(deps-dev): bump webpack from 5.24.1 to 5.24.2 in /src/packages/excalidraw (#3131)
Bumps [webpack](https://github.com/webpack/webpack) from 5.24.1 to 5.24.2.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.24.1...v5.24.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-28 09:12:30 +00:00
535b2b682e chore(deps-dev): bump @babel/plugin-transform-runtime from 7.12.17 to 7.13.8 in /src/packages/excalidraw (#3130)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.12.17 to 7.13.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.8/packages/babel-plugin-transform-runtime)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-28 11:11:47 +02:00
1a4a736d94 chore(deps-dev): bump webpack from 5.24.1 to 5.24.2 in /src/packages/utils (#3137)
Bumps [webpack](https://github.com/webpack/webpack) from 5.24.1 to 5.24.2.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.24.1...v5.24.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-28 11:11:36 +02:00
bb207ef861 chore(deps-dev): bump @babel/core from 7.13.1 to 7.13.8 in /src/packages/utils (#3134)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.13.1 to 7.13.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.8/packages/babel-core)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-28 11:04:30 +02:00
2fcbf8658f chore(deps-dev): bump @babel/core from 7.13.1 to 7.13.8 in /src/packages/excalidraw (#3128)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.13.1 to 7.13.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.8/packages/babel-core)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-28 11:04:08 +02:00
6a8c6e7f47 chore(deps-dev): bump css-loader from 5.0.2 to 5.1.0 in /src/packages/utils (#3136)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.0.2 to 5.1.0.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.0.2...v5.1.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-28 11:03:55 +02:00
f962503425 chore(deps-dev): bump @babel/preset-env from 7.13.5 to 7.13.8 in /src/packages/excalidraw (#3132)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.13.5 to 7.13.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.8/packages/babel-preset-env)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-28 11:03:34 +02:00
02fdc506ee chore(deps-dev): bump eslint-config-prettier from 8.0.0 to 8.1.0 (#3138)
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 8.0.0 to 8.1.0.
- [Release notes](https://github.com/prettier/eslint-config-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v8.0.0...v8.1.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-28 11:02:59 +02:00
2ba6088e97 chore: Use @excalidraw/prettier-config (#3122) 2021-02-25 19:26:27 +01:00
4e421e6e9e feat: Support exporting with dark mode (#3046)
Co-authored-by: Lipis <lipiridis@gmail.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
2021-02-24 15:22:17 +01:00
d213dbb42d chore(deps-dev): bump eslint-config-prettier from 7.2.0 to 8.0.0 (#3119)
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 7.2.0 to 8.0.0.
- [Release notes](https://github.com/prettier/eslint-config-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v7.2.0...v8.0.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-24 13:56:32 +02:00
a5779dd5d8 chore(deps-dev): bump @babel/plugin-transform-arrow-functions (#3107)
Bumps [@babel/plugin-transform-arrow-functions](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-arrow-functions) from 7.12.13 to 7.13.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.0/packages/babel-plugin-transform-arrow-functions)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-24 10:55:24 +00:00
628b4c1eec chore(deps-dev): bump @babel/preset-env in /src/packages/excalidraw (#3109)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.12.17 to 7.13.5.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.5/packages/babel-preset-env)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-24 10:50:49 +00:00
31333e597b chore(deps-dev): bump @babel/core in /src/packages/utils (#3113)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.12.17 to 7.13.1.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.1/packages/babel-core)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-24 10:50:25 +00:00
872b340f1b chore(deps-dev): bump @babel/plugin-transform-typescript (#3104)
Bumps [@babel/plugin-transform-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-typescript) from 7.12.17 to 7.13.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.0/packages/babel-plugin-transform-typescript)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-24 10:49:56 +00:00
83e8167adf chore(deps-dev): bump @babel/core in /src/packages/excalidraw (#3110)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.12.17 to 7.13.1.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.1/packages/babel-core)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-24 10:44:24 +00:00
baea88942c chore(deps-dev): bump @babel/preset-typescript (#3116)
Bumps [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) from 7.12.17 to 7.13.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.0/packages/babel-preset-typescript)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-24 10:43:40 +00:00
ae35037a3b chore(deps-dev): bump @babel/plugin-transform-arrow-functions (#3102)
Bumps [@babel/plugin-transform-arrow-functions](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-arrow-functions) from 7.12.13 to 7.13.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.0/packages/babel-plugin-transform-arrow-functions)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-24 10:37:52 +00:00
4f31ae1e4b chore(deps-dev): bump webpack in /src/packages/excalidraw (#3117)
Bumps [webpack](https://github.com/webpack/webpack) from 5.23.0 to 5.24.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.23.0...v5.24.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-24 10:37:04 +00:00
704ee30ae6 chore(deps-dev): bump @babel/preset-typescript in /src/packages/utils (#3103)
Bumps [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) from 7.12.17 to 7.13.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.0/packages/babel-preset-typescript)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-24 10:33:12 +00:00
176baef9c2 chore(deps-dev): bump @babel/plugin-transform-async-to-generator (#3112)
Bumps [@babel/plugin-transform-async-to-generator](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-async-to-generator) from 7.12.13 to 7.13.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.0/packages/babel-plugin-transform-async-to-generator)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-24 10:31:57 +00:00
3558f07fe0 chore(deps): bump react-scripts from 4.0.2 to 4.0.3 (#3114)
Bumps [react-scripts](https://github.com/facebook/create-react-app/tree/HEAD/packages/react-scripts) from 4.0.2 to 4.0.3.
- [Release notes](https://github.com/facebook/create-react-app/releases)
- [Changelog](https://github.com/facebook/create-react-app/blob/master/CHANGELOG.md)
- [Commits](https://github.com/facebook/create-react-app/commits/react-scripts@4.0.3/packages/react-scripts)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-24 10:29:46 +00:00
fd24c74ab1 chore(deps-dev): bump firebase-tools from 9.4.0 to 9.5.0 (#3115)
Bumps [firebase-tools](https://github.com/firebase/firebase-tools) from 9.4.0 to 9.5.0.
- [Release notes](https://github.com/firebase/firebase-tools/releases)
- [Commits](https://github.com/firebase/firebase-tools/compare/v9.4.0...v9.5.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-24 10:22:02 +00:00
ae6892501d chore(deps-dev): bump @babel/plugin-transform-typescript (#3105)
Bumps [@babel/plugin-transform-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-typescript) from 7.12.17 to 7.13.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.0/packages/babel-plugin-transform-typescript)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-24 10:19:06 +00:00
1e6adaf0b5 chore(deps-dev): bump webpack in /src/packages/utils (#3106)
Bumps [webpack](https://github.com/webpack/webpack) from 5.23.0 to 5.24.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.23.0...v5.24.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-24 10:18:21 +00:00
7ccd38a37f chore(deps-dev): bump @babel/plugin-transform-async-to-generator (#3111)
Bumps [@babel/plugin-transform-async-to-generator](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-async-to-generator) from 7.12.13 to 7.13.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.0/packages/babel-plugin-transform-async-to-generator)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-24 10:18:03 +00:00
e6ce9e0ea7 chore(deps-dev): bump @babel/preset-env in /src/packages/utils (#3108)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.12.17 to 7.13.5.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.5/packages/babel-preset-env)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-24 10:17:31 +00:00
c0e05445b1 chore: Consistent job names in GH actions (#3099) 2021-02-24 12:12:19 +02:00
b44531d94a chore: Remove -- (leftovers from Yarn switch) (#3097) 2021-02-23 15:11:36 +05:30
464c2cc05e docs: minor readme/changelog changes (#3098)
* docs: minor readme/changelog changes

* Update src/packages/excalidraw/README.md

* Update src/packages/excalidraw/README.md

* fix

* Update src/packages/excalidraw/README.md

Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-02-23 01:55:26 +05:30
bd13c2ed48 chore(deps-dev): bump @babel/plugin-transform-typescript (#3079)
Bumps [@babel/plugin-transform-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-typescript) from 7.12.16 to 7.12.17.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.12.17/packages/babel-plugin-transform-typescript)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-22 09:19:34 +00:00
2e58aaae66 chore(deps-dev): bump webpack in /src/packages/utils (#3083)
Bumps [webpack](https://github.com/webpack/webpack) from 5.21.2 to 5.23.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.21.2...v5.23.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-22 09:14:05 +00:00
d4c14d484c chore(deps-dev): bump @babel/plugin-transform-runtime (#3078)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.12.15 to 7.12.17.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.12.17/packages/babel-plugin-transform-runtime)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-22 09:13:36 +00:00
06d1871640 chore(deps-dev): bump @babel/preset-typescript (#3080)
Bumps [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) from 7.12.16 to 7.12.17.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.12.17/packages/babel-preset-typescript)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-22 09:13:32 +00:00
e7a59335e4 chore(deps-dev): bump @babel/plugin-transform-typescript (#3074)
Bumps [@babel/plugin-transform-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-typescript) from 7.12.16 to 7.12.17.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.12.17/packages/babel-plugin-transform-typescript)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-22 09:09:04 +00:00
0dbef18044 chore(deps-dev): bump @babel/preset-typescript in /src/packages/utils (#3081)
Bumps [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) from 7.12.16 to 7.12.17.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.12.17/packages/babel-preset-typescript)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-22 09:08:43 +00:00
e16c2d592f chore(deps-dev): bump @babel/core in /src/packages/excalidraw (#3073)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.12.16 to 7.12.17.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.12.17/packages/babel-core)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-22 09:08:23 +00:00
1f4cf4610f chore(deps-dev): bump mini-css-extract-plugin (#3076)
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 1.3.6 to 1.3.8.
- [Release notes](https://github.com/webpack-contrib/mini-css-extract-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/mini-css-extract-plugin/compare/v1.3.6...v1.3.8)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-22 11:07:11 +02:00
1bee959660 chore(deps-dev): bump @babel/plugin-transform-runtime (#3077)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.12.15 to 7.12.17.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.12.17/packages/babel-plugin-transform-runtime)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-22 11:04:05 +02:00
f609a4ac3a chore(deps-dev): bump @babel/preset-env in /src/packages/excalidraw (#3082)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.12.16 to 7.12.17.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.12.17/packages/babel-preset-env)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-22 11:03:39 +02:00
eee9d1bc16 chore(deps-dev): bump @babel/core in /src/packages/utils (#3075)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.12.16 to 7.12.17.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.12.17/packages/babel-core)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-22 11:02:59 +02:00
ecdc2582e9 chore(deps-dev): bump webpack in /src/packages/excalidraw (#3072)
Bumps [webpack](https://github.com/webpack/webpack) from 5.21.2 to 5.23.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.21.2...v5.23.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-22 11:02:35 +02:00
245e13a884 chore(deps-dev): bump @babel/preset-env in /src/packages/utils (#3084)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.12.16 to 7.12.17.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.12.17/packages/babel-preset-env)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-22 11:01:52 +02:00
8a322b22ed chore(deps): bump @sentry/browser from 6.1.0 to 6.2.0 (#3087)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/6.1.0...6.2.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-22 08:53:11 +00:00
3f784d76fc chore(deps-dev): bump firebase-tools from 9.3.0 to 9.4.0 (#3086)
Bumps [firebase-tools](https://github.com/firebase/firebase-tools) from 9.3.0 to 9.4.0.
- [Release notes](https://github.com/firebase/firebase-tools/releases)
- [Changelog](https://github.com/firebase/firebase-tools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/firebase/firebase-tools/compare/v9.3.0...v9.4.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-22 10:27:43 +02:00
ba5afe9139 chore(deps): bump firebase from 8.2.7 to 8.2.9 (#3088)
Bumps [firebase](https://github.com/firebase/firebase-js-sdk) from 8.2.7 to 8.2.9.
- [Release notes](https://github.com/firebase/firebase-js-sdk/releases)
- [Changelog](https://github.com/firebase/firebase-js-sdk/blob/master/CHANGELOG.md)
- [Commits](https://github.com/firebase/firebase-js-sdk/compare/firebase@8.2.7...firebase@8.2.9)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-22 10:26:46 +02:00
9e6f351672 chore(deps): bump @sentry/integrations from 6.1.0 to 6.2.0 (#3085)
Bumps [@sentry/integrations](https://github.com/getsentry/sentry-javascript) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/6.1.0...6.2.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-22 10:26:18 +02:00
fcd10a6a43 chore: use yarn for packages (#3092) 2021-02-22 08:41:51 +05:30
3bc18f6aed feat: expose variable window.EXCALIDRAW_ASSET_PATH to allow host define the path for excalidraw assets (#3068)
* feat: expose variable window.EXCALIDRAW_ASSET_PATH to allow host define the path for excalidraw assets
No more __webpack_public_path__ needed explicitly in host and it will default to unpkg cdn if window.EXCALIDRAW_ASSET_PATH is not defined

* fix

* add public path

* add public path

* assign only when env not test

* read from package.json

* Append content hash to excalidraw-assets so cache bursting happens when version update

* update changelog and readme

* update
2021-02-21 21:08:30 +05:30
7c5481b877 feat: Add support for scrollToCenter in initialData so host can control whether to scroll to center on mount (#3070)
* feat: Add support for scrollToCenter in initialData so host can control whether to scroll to center on mount

* fix

* update changelog and readme

* fix

* Scroll to center only for collab and shareable links in excalidraw app

* fix test

* update readme

* Update src/packages/excalidraw/README.md
2021-02-21 19:01:34 +05:30
d17464fbaa chore: Replace node-sass with sass (#3067) 2021-02-20 18:31:34 +02:00
baf9da2b83 chore: Update action versions and docker build (#3065) 2021-02-20 14:28:33 +00:00
4bfcf105a5 build: Switch to Yarn (#3057)
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-02-20 12:47:17 +00:00
1f7c75de63 feat: GCP Portal 2021-02-08 00:17:36 +02:00
168 changed files with 21394 additions and 33228 deletions

View File

@ -1,10 +1,10 @@
*
!.env
!.eslintrc.json
!.npmrc
!.prettierrc
!package.json
!public/
!src/
!.npmrc
!.eslintrc.json
!.prettierrc
!package-lock.json
!package.json
!tsconfig.json
!.env
!yarn.lock

2
.env
View File

@ -1,5 +1,5 @@
REACT_APP_BACKEND_V1_GET_URL=https://json.excalidraw.com/api/v1/
REACT_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
REACT_APP_SOCKET_SERVER_URL=https://portal.excalidraw.com
REACT_APP_SOCKET_SERVER_URL=https://excalidraw-portal.uc.r.appspot.com
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'

View File

@ -4,3 +4,4 @@ package-lock.json
.vscode/
firebase/
dist/
public/workbox

View File

@ -1,40 +1,6 @@
{
"extends": ["prettier", "react-app"],
"plugins": ["prettier"],
"extends": ["@excalidraw/eslint-config", "react-app"],
"rules": {
"@typescript-eslint/no-unused-vars": "warn",
"curly": "warn",
"dot-notation": "warn",
"import/no-anonymous-default-export": "off",
"no-console": [
"warn",
{
"allow": ["warn", "error", "info"]
}
],
"no-else-return": "warn",
"no-lonely-if": "warn",
"no-restricted-syntax": [
"warn",
{
"message": "Use 't(...)' instead of literal text in JSX",
"selector": "JSXText[value=/\\w/]"
}
],
"no-unneeded-ternary": "warn",
"no-unused-expressions": "warn",
"no-useless-return": "warn",
"no-var": "warn",
"object-shorthand": "warn",
"one-var": ["warn", "never"],
"prefer-arrow-callback": "warn",
"prefer-const": [
"warn",
{
"destructuring": "all"
}
],
"prefer-template": "warn",
"prettier/prettier": "warn"
"import/no-anonymous-default-export": "off"
}
}

6
.github/assets/crowdin.svg vendored Normal file
View File

@ -0,0 +1,6 @@
<svg height="50" viewBox="0 0 257 50" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path fill="#fff" d="M-7.977-9.253h288.95v78.13H-7.977z" />
<path d="M67.626 32.315c-1.34 0-2.207 0-2.207-1.025 0-.236.079-.551.236-.946l4.02-8.907h12.929c1.34 0 2.128-.08 2.128.946 0 .315-.078.63-.236.946l-.788 1.734h5.439l1.104-2.444c.157-.394.157-.71.157-1.025 0-2.207-2.365-3.31-4.257-3.31H65.655l-5.754 12.691c-.158.394-.158.71-.158 1.025 0 2.365 1.97 3.547 4.73 3.547h20.26l1.26-3.232H67.627zm42.727-14.11H95.059l-6.937 17.342h5.518l5.519-14.032h8.435c1.34 0 2.05-.157 2.05.868 0 .315-.08.63-.237.946l-.789 1.734h5.518l1.104-2.444c.158-.394.158-.71.158-1.025 0-1.025-.552-1.892-1.734-2.522-.946-.473-2.208-.868-3.311-.868zm30.35 0h-21.285l-5.754 12.691c-.158.316-.158.63-.158 1.025 0 1.97 1.419 3.547 3.232 3.547h21.52l5.834-13.007c.158-.394.158-.71.158-1.024 0-2.05-1.734-3.233-3.547-3.233zm-6.701 14.19h-12.85c-1.34 0-1.97-.159-1.97-1.183 0-.316.079-.631.236-.946l4.178-8.908h12.929c1.26 0 1.891-.08 1.891.946 0 .315-.078.63-.236 1.025l-4.178 9.065zm13.953 3.152h28.695l7.41-17.264h-5.676l-6.149 14.032h-9.223l6.149-14.11h-5.676l-6.386 14.031h-6.306c-1.34 0-2.05-.157-2.05-1.182 0-.315.08-.63.237-.946l5.282-11.982h-5.519l-5.518 12.455c-1.103 3.39 2.207 4.966 4.73 4.966zm67.874-23.649l-5.913 1.577-1.97 4.73h-14.584c-3.548 0-6.7 1.576-8.278 4.73l-3.941 9.46c-.788 1.576.63 3.152 3.31 3.152h21.128l10.248-23.649zm-27.591 20.496c-1.183 0-1.735-.788-1.577-1.577l3.469-7.567c.788-1.813 2.68-1.892 4.414-1.892h11.825l-4.73 11.036h-13.401zm26.802 3.153l7.49-17.737-6.307 1.183-7.095 16.554h5.912zm8.435-19.944l1.656-3.705-6.228 1.261-1.577 3.705 6.15-1.26zm22.23 2.601h-20.417l-7.094 17.343h5.518l5.518-14.19h13.48c1.34 0 2.05-.078 2.05 1.026 0 .315-.08.63-.237.946l-5.518 12.297h5.518l5.834-13.007c.157-.315.157-.63.157-1.025 0-1.025-.552-1.892-1.734-2.522-.867-.473-1.892-.868-3.074-.868zm-192.82.868c-8.672-1.025-16.476.71-17.58 6.148 0 .237-.157 1.262-.157 1.42l1.419.157v2.207l-1.34-.157c.551 5.597 3.626 7.252 6.858 7.331h.236c1.42.079 2.917-.237 4.178-.788.08 0 .08-.08.08-.08v-.157c0-.079-.08-.079-.08-.157-.078 0-.078-.08-.157-.08-2.996.395-5.755-2.049-5.755-7.015 0-6.228 4.888-8.514 12.298-8.514.236.158.315-.237 0-.315zM36.803 30.344c.788 0 1.498.158 2.207.237.237 1.655 1.025 3.232 2.208 4.336-1.183-.158-2.208-.71-3.075-1.498a6.051 6.051 0 01-1.34-3.075zm2.68-5.439c0 .237-.157.552-.236.946h-1.025c-.552 0-1.025-.079-1.576-.158v-.157c.63-3.39 4.02-4.73 7.252-5.36a7.997 7.997 0 00-2.76 1.812c-.787.868-1.34 1.813-1.655 2.917z" fill="#2e3340" fill-rule="nonzero" />
<path d="M56.274 14.105c-6.543-1.813-34.055-4.02-34.055 11.273.946.158 1.577.315 2.05.394-.079 1.183 0 2.444 0 3.626l-2.444-.394c0 8.83 6.464 11.667 11.588 11.667.868 0 1.656-.078 2.523-.157 2.128-.237 4.178-.867 5.991-1.892.079 0 .079-.08.079-.08v-.157c0-.079-.079-.079-.079-.157-.079 0-.079-.08-.157-.08-4.336.868-10.17-.315-10.17-10.563 0-13.637 19.156-12.77 24.753-13.007.08 0 .08-.079.08-.079v-.157c0-.08 0-.08-.08-.158 0-.079 0-.079-.079-.079zM33.414 39.41a9.362 9.362 0 01-6.78-2.286c-1.892-1.656-3.074-3.942-3.31-6.385 1.655.236 3.704.394 5.438.473a9.43 9.43 0 001.577 4.808c.946 1.42 2.207 2.602 3.705 3.39h-.63zM28.92 24.984l-2.601-.237-2.602-.315c0-7.962 12.77-11.036 18.683-10.484-5.912 1.34-13.086 4.099-13.48 11.036z" fill="#2e3340" fill-rule="nonzero" />
<path d="M59.664 9.533c-7.962-2.68-17.027-4.02-25.462-3.941-12.22 0-27.67 3.626-28.064 16.081l3.31.788c-.393 1.577-.393 4.81-.393 4.81s-1.892-.553-2.917-.79c0 14.821 8.671 18.526 17.027 18.526 3.39 0 6.701-.552 9.854-1.734.08 0 .08-.08.08-.08v-.157c0-.079-.08-.079-.08-.157h-.157c-2.602 0-4.651.867-8.75-2.05-7.963-5.597-7.017-20.102 2.128-26.408 9.46-6.701 29.798-4.573 33.267-4.415h.157s.079 0 .079-.079v-.236l-.079-.158zm-36.42 34.292c-9.932 0-14.978-5.36-15.45-15.609 2.68.71 5.202 1.34 7.961 1.734-.157 4.02 1.262 7.962 4.02 11.037a12.488 12.488 0 005.046 2.916l-1.577-.078zM45.632 7.956c-12.06 0-26.014 1.42-28.773 14.584 0 0-7.41-1.182-9.066-1.576C9.843 4.409 38.38 5.67 49.89 7.956h-4.257z" fill="#2e3340" fill-rule="nonzero" />
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

9
.github/assets/sentry.svg vendored Normal file
View File

@ -0,0 +1,9 @@
<svg class="__sntry__ css-15xgryy e10nushx5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 222 66" height="50" style="background-color: rgb(255, 255, 255);">
<defs>
<style type="text/css">
@media (prefers-color-scheme: dark) {svg.__sntry__ { background-color: #584674 !important; }path.__sntry__ { fill: #ffffff !important; }}
</style>
</defs>
<path d="M29,2.26a4.67,4.67,0,0,0-8,0L14.42,13.53A32.21,32.21,0,0,1,32.17,40.19H27.55A27.68,27.68,0,0,0,12.09,17.47L6,28a15.92,15.92,0,0,1,9.23,12.17H4.62A.76.76,0,0,1,4,39.06l2.94-5a10.74,10.74,0,0,0-3.36-1.9l-2.91,5a4.54,4.54,0,0,0,1.69,6.24A4.66,4.66,0,0,0,4.62,44H19.15a19.4,19.4,0,0,0-8-17.31l2.31-4A23.87,23.87,0,0,1,23.76,44H36.07a35.88,35.88,0,0,0-16.41-31.8l4.67-8a.77.77,0,0,1,1.05-.27c.53.29,20.29,34.77,20.66,35.17a.76.76,0,0,1-.68,1.13H40.6q.09,1.91,0,3.81h4.78A4.59,4.59,0,0,0,50,39.43a4.49,4.49,0,0,0-.62-2.28Z M124.32,28.28,109.56,9.22h-3.68V34.77h3.73V15.19l15.18,19.58h3.26V9.22h-3.73ZM87.15,23.54h13.23V20.22H87.14V12.53h14.93V9.21H83.34V34.77h18.92V31.45H87.14ZM71.59,20.3h0C66.44,19.06,65,18.08,65,15.7c0-2.14,1.89-3.59,4.71-3.59a12.06,12.06,0,0,1,7.07,2.55l2-2.83a14.1,14.1,0,0,0-9-3c-5.06,0-8.59,3-8.59,7.27,0,4.6,3,6.19,8.46,7.52C74.51,24.74,76,25.78,76,28.11s-2,3.77-5.09,3.77a12.34,12.34,0,0,1-8.3-3.26l-2.25,2.69a15.94,15.94,0,0,0,10.42,3.85c5.48,0,9-2.95,9-7.51C79.75,23.79,77.47,21.72,71.59,20.3ZM195.7,9.22l-7.69,12-7.64-12h-4.46L186,24.67V34.78h3.84V24.55L200,9.22Zm-64.63,3.46h8.37v22.1h3.84V12.68h8.37V9.22H131.08ZM169.41,24.8c3.86-1.07,6-3.77,6-7.63,0-4.91-3.59-8-9.38-8H154.67V34.76h3.8V25.58h6.45l6.48,9.2h4.44l-7-9.82Zm-10.95-2.5V12.6h7.17c3.74,0,5.88,1.77,5.88,4.84s-2.29,4.86-5.84,4.86Z" transform="translate(11, 11)" fill="#362d59" class="__sntry__">
</path>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

3
.github/assets/vercel.svg vendored Normal file
View File

@ -0,0 +1,3 @@
<svg height="50" viewBox="0 0 164 50" xmlns="http://www.w3.org/2000/svg" style="background:#fff" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M78.21 15.587c-5.672 0-9.762 3.864-9.762 9.661s4.604 9.66 10.276 9.66c3.427 0 6.448-1.416 8.319-3.805l-3.931-2.372c-1.038 1.186-2.615 1.879-4.388 1.879-2.461 0-4.552-1.342-5.328-3.489h14.397c.113-.601.18-1.223.18-1.879 0-5.79-4.09-9.655-9.763-9.655zm-4.86 7.783c.642-2.142 2.399-3.489 4.855-3.489 2.461 0 4.219 1.347 4.855 3.489h-9.71zm60.187-7.783c-5.673 0-9.763 3.864-9.763 9.661s4.604 9.66 10.276 9.66c3.427 0 6.449-1.416 8.319-3.805l-3.931-2.372c-1.038 1.186-2.615 1.879-4.388 1.879-2.461 0-4.552-1.342-5.328-3.489h14.397c.113-.601.18-1.223.18-1.879 0-5.79-4.09-9.655-9.762-9.655zm-4.856 7.783c.642-2.142 2.4-3.489 4.856-3.489 2.46 0 4.218 1.347 4.855 3.489h-9.711zm-20.054 1.878c0 3.22 2.015 5.367 5.139 5.367 2.116 0 3.704-1.003 4.52-2.64l3.947 2.378c-1.634 2.843-4.696 4.556-8.467 4.556-5.678 0-9.763-3.864-9.763-9.661s4.09-9.66 9.763-9.66c3.77 0 6.828 1.712 8.467 4.556l-3.946 2.377c-.817-1.637-2.405-2.64-4.521-2.64-3.12 0-5.139 2.147-5.139 5.367zm42.378-15.565v24.69h-4.624V9.682h4.624zM24.73 7l18.985 34.35H5.744L24.73 7zm47.465 2.683L57.956 35.446 43.72 9.683h5.338l8.9 16.102 8.898-16.102h5.339zm30.268 6.44v5.202a5.634 5.634 0 00-1.644-.263c-2.985 0-5.138 2.147-5.138 5.367v7.943h-4.624V16.124h4.624v4.938c0-2.727 3.036-4.938 6.782-4.938z" fill-rule="nonzero" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,36 +1,33 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
directory: /
schedule:
interval: weekly
day: sunday
time: "01:00"
open-pull-requests-limit: 99
reviewers:
- lipis
assignees:
- lipis
- package-ecosystem: npm
directory: "/src/packages/excalidraw/"
directory: /src/packages/excalidraw/
schedule:
interval: weekly
day: sunday
time: "01:00"
open-pull-requests-limit: 99
reviewers:
- ad1992
assignees:
- ad1992
- package-ecosystem: npm
directory: "/src/packages/utils/"
directory: /src/packages/utils/
schedule:
interval: weekly
day: sunday
time: "01:00"
open-pull-requests-limit: 99
reviewers:
- ad1992
assignees:

View File

@ -6,9 +6,8 @@ on:
- master
jobs:
build:
build-docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- run: docker build -t excalidraw .

View File

@ -7,27 +7,23 @@ on:
pull_request:
jobs:
build:
packages:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Setup Node.js 14.x
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: 14.x
- name: Install dependencies
run: |
npm ci
npm ci --prefix src/packages/excalidraw
npm ci --prefix src/packages/utils
yarn --frozen-lockfile
yarn --cwd src/packages/excalidraw
yarn --cwd src/packages/utils
- name: Build @excalidraw/excalidraw
run: |
npm run pack --prefix src/packages/excalidraw
yarn --cwd src/packages/excalidraw run pack
- name: Build @excalidraw/utils
run: |
npm run pack --prefix src/packages/utils
yarn --cwd src/packages/utils run pack

View File

@ -9,7 +9,6 @@ on:
jobs:
cancel:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: styfle/cancel-workflow-action@0.6.0

View File

@ -7,16 +7,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Setup Node.js 14.x
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: 14.x
- name: Install and lint
run: |
npm ci
npm run test:other
npm run test:code
npm run test:typecheck
yarn --frozen-lockfile
yarn test:other
yarn test:code
yarn test:typecheck

View File

@ -3,7 +3,7 @@ name: Build locales coverage
on:
push:
branches:
- "l10n_master"
- l10n_master
jobs:
locales:
@ -15,13 +15,13 @@ jobs:
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
- name: Setup Node.js 14.x
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: 14.x
- name: Create report file
run: |
npm run locales-coverage
yarn locales-coverage
FILE_CHANGED=$(git diff src/locales/percentages.json)
if [ ! -z "${FILE_CHANGED}" ]; then
git config --global user.name 'Excalidraw Bot'

View File

@ -1,4 +1,4 @@
name: "Semantic PR title"
name: Semantic PR title
on:
pull_request_target:
@ -8,9 +8,8 @@ on:
- synchronize
jobs:
main:
semantic:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v3.0.0
env:

View File

@ -1,4 +1,4 @@
name: New Sentry Production Release
name: New Sentry production release
on:
push:
@ -6,28 +6,23 @@ on:
- master
jobs:
release:
sentry:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1.0.0
- uses: actions/checkout@v2
- name: Setup Node.js 14.x
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: 14.x
- name: Install and build
run: |
npm ci
npm run build:app
yarn --frozen-lockfile
yarn build:app
env:
CI: true
- name: Install Sentry
run: |
curl -sL https://sentry.io/get-cli/ | bash
- name: Create new Sentry release
run: |
export SENTRY_RELEASE=$(sentry-cli releases propose-version)

View File

@ -5,16 +5,13 @@ on: pull_request
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Setup Node.js 14.x
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: 14.x
- name: Install and test
run: |
npm ci
npm run test:app
yarn --frozen-lockfile
yarn test:app

2
.gitignore vendored
View File

@ -16,7 +16,7 @@ firebase
logs
node_modules
npm-debug.log*
package-lock.json
static
yarn-debug.log*
yarn-error.log*
yarn.lock

2
.nvmrc
View File

@ -1 +1 @@
12
14

View File

@ -1,4 +0,0 @@
{
"proseWrap": "never",
"trailingComma": "all"
}

View File

@ -5,7 +5,7 @@
### Option 1 - Manual
1. Fork and clone the repo
1. Run `npm install` to install dependencies
1. Run `yarn` to install dependencies
1. Create a branch for your PR with `git checkout -b your-branch-name`
> To keep `master` branch pointing to remote repository and make pull requests from branches on your fork. To do this, run:

View File

@ -2,13 +2,13 @@ FROM node:14-alpine AS build
WORKDIR /opt/node_app
COPY package.json package-lock.json ./
RUN npm i --no-optional
COPY package.json yarn.lock ./
RUN yarn --ignore-optional
ARG NODE_ENV=production
COPY . .
RUN npm run build:app:docker
RUN yarn build:app:docker
FROM nginx:1.17-alpine

View File

@ -28,6 +28,10 @@ If you like the project, you can become a sponsor at [Open Collective](https://o
<a href="https://opencollective.com/excalidraw#category-CONTRIBUTE" target="_blank"><img src="https://opencollective.com/excalidraw/tiers/backers.svg?avatarHeight=32"/></a>
Last but not least, we're thankful to these companies for offering their services for free:
[![Vercel](./.github/assets/vercel.svg)](https://vercel.com) [![Sentry](./.github/assets/sentry.svg)](https://sentry.io) [![Crowdin](./.github/assets/crowdin.svg)](https://crowdin.com)
## Documentation
### Shortcuts
@ -86,6 +90,12 @@ Try out [`@excalidraw/excalidraw`](https://www.npmjs.com/package/@excalidraw/exc
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
#### Requirements
- [Node.js](https://nodejs.org/en/)
- [Yarn](https://yarnpkg.com/getting-started/install)
- [Git](https://git-scm.com/downloads)
#### Clone the repo
```bash
@ -94,14 +104,14 @@ git clone https://github.com/excalidraw/excalidraw.git
#### Commands
| Command | Description |
| --------------------- | --------------------------------- |
| `npm install` | Install the dependencies |
| `npm start` | Run the project |
| `npm run fix` | Reformat all files with Prettier |
| `npm test` | Run tests |
| `npm run test:update` | Update test snapshots |
| `npm run test:code` | Test for formatting with Prettier |
| Command | Description |
| ------------------ | --------------------------------- |
| `yarn` | Install the dependencies |
| `yarn start` | Run the project |
| `yarn fix` | Reformat all files with Prettier |
| `yarn test` | Run tests |
| `yarn test:update` | Update test snapshots |
| `yarn test:code` | Test for formatting with Prettier |
#### Docker Compose

View File

@ -18,7 +18,7 @@ services:
volumes:
- ./:/opt/node_app/app:delegated
- ./package.json:/opt/node_app/package.json
- ./package-lock.json:/opt/node_app/package-lock.json
- ./yarn.lock:/opt/node_app/yarn.lock
- notused:/opt/node_app/app/node_modules
volumes:

23611
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,21 +19,20 @@
]
},
"dependencies": {
"@sentry/browser": "6.1.0",
"@sentry/integrations": "6.1.0",
"@sentry/browser": "6.2.2",
"@sentry/integrations": "6.2.1",
"@testing-library/jest-dom": "5.11.9",
"@testing-library/react": "11.2.5",
"@types/jest": "26.0.20",
"@types/react": "17.0.2",
"@types/react-dom": "17.0.1",
"@types/socket.io-client": "1.4.35",
"browser-fs-access": "0.13.1",
"@types/jest": "26.0.21",
"@types/react": "17.0.3",
"@types/react-dom": "17.0.2",
"@types/socket.io-client": "1.4.36",
"browser-fs-access": "0.15.3",
"clsx": "1.1.1",
"firebase": "8.2.7",
"firebase": "8.2.10",
"i18next-browser-languagedetector": "6.0.1",
"lodash.throttle": "4.1.1",
"nanoid": "3.1.20",
"node-sass": "4.14.1",
"nanoid": "3.1.21",
"open-color": "1.8.0",
"pako": "1.0.11",
"png-chunk-text": "1.0.0",
@ -43,18 +42,21 @@
"pwacompat": "2.0.17",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-scripts": "4.0.2",
"react-scripts": "4.0.3",
"roughjs": "4.3.1",
"sass": "1.32.8",
"socket.io-client": "2.3.1",
"typescript": "4.1.5"
"typescript": "4.2.3"
},
"devDependencies": {
"@excalidraw/eslint-config": "1.0.0",
"@excalidraw/prettier-config": "1.0.2",
"@types/lodash.throttle": "4.1.6",
"@types/pako": "1.0.1",
"@types/resize-observer-browser": "0.1.5",
"eslint-config-prettier": "7.2.0",
"eslint-config-prettier": "8.1.0",
"eslint-plugin-prettier": "3.3.1",
"firebase-tools": "9.3.0",
"firebase-tools": "9.6.1",
"husky": "4.3.8",
"jest-canvas-mock": "2.3.1",
"lint-staged": "10.5.4",
@ -63,7 +65,7 @@
"rewire": "5.0.0"
},
"engines": {
"node": ">=12.0.0"
"node": ">=14.0.0"
},
"homepage": ".",
"husky": {
@ -78,28 +80,29 @@
"resetMocks": false
},
"name": "excalidraw",
"prettier": "@excalidraw/prettier-config",
"private": true,
"scripts": {
"build-node": "node ./scripts/build-node.js",
"build:app:docker": "REACT_APP_DISABLE_SENTRY=true react-scripts build",
"build:app": "REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
"build:version": "node ./scripts/build-version.js",
"build": "npm run build:app && npm run build:version",
"build": "yarn build:app && yarn build:version",
"eject": "react-scripts eject",
"fix:code": "npm run test:code -- --fix",
"fix:other": "npm run prettier -- --write",
"fix": "npm run fix:other && npm run fix:code",
"fix:code": "yarn test:code --fix",
"fix:other": "yarn prettier --write",
"fix": "yarn fix:other && yarn fix:code",
"locales-coverage": "node scripts/build-locales-coverage.js",
"locales-coverage:description": "node scripts/locales-coverage-description.js",
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
"start": "react-scripts start",
"test:all": "npm run test:typecheck && npm run test:code && npm run test:other && npm run test:app -- --watchAll=false",
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watchAll=false",
"test:app": "react-scripts test --passWithNoTests",
"test:code": "eslint --max-warnings=0 --ignore-path .gitignore --ext .js,.ts,.tsx .",
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
"test:other": "npm run prettier -- --list-different",
"test:other": "yarn prettier --list-different",
"test:typecheck": "tsc",
"test:update": "npm run test:app -- --updateSnapshot --watchAll=false",
"test": "npm run test:app"
"test:update": "yarn test:app --updateSnapshot --watchAll=false",
"test": "yarn test:app"
}
}

Binary file not shown.

BIN
public/Virgil.woff2 Normal file

Binary file not shown.

View File

@ -1,7 +1,7 @@
/* http://www.eaglefonts.com/fg-virgil-ttf-131249.htm */
@font-face {
font-family: "Virgil";
src: url("FG_Virgil.woff2");
src: url("Virgil.woff2");
font-display: swap;
}

View File

@ -60,7 +60,7 @@
<link
rel="preload"
href="FG_Virgil.woff2"
href="Virgil.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
@ -86,7 +86,11 @@
/>
<link rel="stylesheet" href="fonts.css" type="text/css" />
<script>
window.EXCALIDRAW_ASSET_PATH = "/";
// setting this so that libraries installation reuses this window tab.
window.name = "_excalidraw";
</script>
<% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
<script
async
@ -110,8 +114,7 @@
Roboto, Helvetica, Arial, sans-serif;
font-family: var(--ui-font);
-webkit-text-size-adjust: 100%;
-webkit-user-select: none;
user-select: none;
width: 100vw;
height: 100vh;
}

View File

@ -26,5 +26,18 @@
}
}
],
"capture_links": "new_client"
"capture_links": "new_client",
"share_target": {
"action": "/web-share-target",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"files": [
{
"name": "file",
"accept": ["application/vnd.excalidraw+json", "application/json", ".excalidraw"]
}
]
}
}
}

View File

@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.backgroundSync=function(t,e,s){"use strict";try{self["workbox:background-sync:4.3.1"]&&_()}catch(t){}const i=3,n="workbox-background-sync",a="requests",r="queueName";class c{constructor(t){this.t=t,this.s=new s.DBWrapper(n,i,{onupgradeneeded:this.i})}async pushEntry(t){delete t.id,t.queueName=this.t,await this.s.add(a,t)}async unshiftEntry(t){const[e]=await this.s.getAllMatching(a,{count:1});e?t.id=e.id-1:delete t.id,t.queueName=this.t,await this.s.add(a,t)}async popEntry(){return this.h({direction:"prev"})}async shiftEntry(){return this.h({direction:"next"})}async getAll(){return await this.s.getAllMatching(a,{index:r,query:IDBKeyRange.only(this.t)})}async deleteEntry(t){await this.s.delete(a,t)}async h({direction:t}){const[e]=await this.s.getAllMatching(a,{direction:t,index:r,query:IDBKeyRange.only(this.t),count:1});if(e)return await this.deleteEntry(e.id),e}i(t){const e=t.target.result;t.oldVersion>0&&t.oldVersion<i&&e.objectStoreNames.contains(a)&&e.deleteObjectStore(a),e.createObjectStore(a,{autoIncrement:!0,keyPath:"id"}).createIndex(r,r,{unique:!1})}}const h=["method","referrer","referrerPolicy","mode","credentials","cache","redirect","integrity","keepalive"];class o{static async fromRequest(t){const e={url:t.url,headers:{}};"GET"!==t.method&&(e.body=await t.clone().arrayBuffer());for(const[s,i]of t.headers.entries())e.headers[s]=i;for(const s of h)void 0!==t[s]&&(e[s]=t[s]);return new o(e)}constructor(t){"navigate"===t.mode&&(t.mode="same-origin"),this.o=t}toObject(){const t=Object.assign({},this.o);return t.headers=Object.assign({},this.o.headers),t.body&&(t.body=t.body.slice(0)),t}toRequest(){return new Request(this.o.url,this.o)}clone(){return new o(this.toObject())}}const u="workbox-background-sync",y=10080,w=new Set;class d{constructor(t,{onSync:s,maxRetentionTime:i}={}){if(w.has(t))throw new e.WorkboxError("duplicate-queue-name",{name:t});w.add(t),this.u=t,this.l=s||this.replayRequests,this.q=i||y,this.m=new c(this.u),this.p()}get name(){return this.u}async pushRequest(t){await this.g(t,"push")}async unshiftRequest(t){await this.g(t,"unshift")}async popRequest(){return this.R("pop")}async shiftRequest(){return this.R("shift")}async getAll(){const t=await this.m.getAll(),e=Date.now(),s=[];for(const i of t){const t=60*this.q*1e3;e-i.timestamp>t?await this.m.deleteEntry(i.id):s.push(f(i))}return s}async g({request:t,metadata:e,timestamp:s=Date.now()},i){const n={requestData:(await o.fromRequest(t.clone())).toObject(),timestamp:s};e&&(n.metadata=e),await this.m[`${i}Entry`](n),this.k?this.D=!0:await this.registerSync()}async R(t){const e=Date.now(),s=await this.m[`${t}Entry`]();if(s){const i=60*this.q*1e3;return e-s.timestamp>i?this.R(t):f(s)}}async replayRequests(){let t;for(;t=await this.shiftRequest();)try{await fetch(t.request.clone())}catch(s){throw await this.unshiftRequest(t),new e.WorkboxError("queue-replay-failed",{name:this.u})}}async registerSync(){if("sync"in registration)try{await registration.sync.register(`${u}:${this.u}`)}catch(t){}}p(){"sync"in registration?self.addEventListener("sync",t=>{if(t.tag===`${u}:${this.u}`){const e=async()=>{let e;this.k=!0;try{await this.l({queue:this})}catch(t){throw e=t}finally{!this.D||e&&!t.lastChance||await this.registerSync(),this.k=!1,this.D=!1}};t.waitUntil(e())}}):this.l({queue:this})}static get _(){return w}}const f=t=>{const e={request:new o(t.requestData).toRequest(),timestamp:t.timestamp};return t.metadata&&(e.metadata=t.metadata),e};return t.Queue=d,t.Plugin=class{constructor(...t){this.v=new d(...t),this.fetchDidFail=this.fetchDidFail.bind(this)}async fetchDidFail({request:t}){await this.v.pushRequest({request:t})}},t}({},workbox.core._private,workbox.core._private);
//# sourceMappingURL=workbox-background-sync.prod.js.map

View File

@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.broadcastUpdate=function(e,t){"use strict";try{self["workbox:broadcast-update:4.3.1"]&&_()}catch(e){}const s=(e,t,s)=>{return!s.some(s=>e.headers.has(s)&&t.headers.has(s))||s.every(s=>{const n=e.headers.has(s)===t.headers.has(s),a=e.headers.get(s)===t.headers.get(s);return n&&a})},n="workbox",a=1e4,i=["content-length","etag","last-modified"],o=async({channel:e,cacheName:t,url:s})=>{const n={type:"CACHE_UPDATED",meta:"workbox-broadcast-update",payload:{cacheName:t,updatedURL:s}};if(e)e.postMessage(n);else{const e=await clients.matchAll({type:"window"});for(const t of e)t.postMessage(n)}};class c{constructor({headersToCheck:e,channelName:t,deferNoticationTimeout:s}={}){this.t=e||i,this.s=t||n,this.i=s||a,this.o()}notifyIfUpdated({oldResponse:e,newResponse:t,url:n,cacheName:a,event:i}){if(!s(e,t,this.t)){const e=(async()=>{i&&i.request&&"navigate"===i.request.mode&&await this.h(i),await this.l({channel:this.u(),cacheName:a,url:n})})();if(i)try{i.waitUntil(e)}catch(e){}return e}}async l(e){await o(e)}u(){return"BroadcastChannel"in self&&!this.p&&(this.p=new BroadcastChannel(this.s)),this.p}h(e){if(!this.m.has(e)){const s=new t.Deferred;this.m.set(e,s);const n=setTimeout(()=>{s.resolve()},this.i);s.promise.then(()=>clearTimeout(n))}return this.m.get(e).promise}o(){this.m=new Map,self.addEventListener("message",e=>{if("WINDOW_READY"===e.data.type&&"workbox-window"===e.data.meta&&this.m.size>0){for(const e of this.m.values())e.resolve();this.m.clear()}})}}return e.BroadcastCacheUpdate=c,e.Plugin=class{constructor(e){this.l=new c(e)}cacheDidUpdate({cacheName:e,oldResponse:t,newResponse:s,request:n,event:a}){t&&this.l.notifyIfUpdated({cacheName:e,oldResponse:t,newResponse:s,event:a,url:n.url})}},e.broadcastUpdate=o,e.responsesAreSame=s,e}({},workbox.core._private);
//# sourceMappingURL=workbox-broadcast-update.prod.js.map

View File

@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.cacheableResponse=function(t){"use strict";try{self["workbox:cacheable-response:4.3.1"]&&_()}catch(t){}class s{constructor(t={}){this.t=t.statuses,this.s=t.headers}isResponseCacheable(t){let s=!0;return this.t&&(s=this.t.includes(t.status)),this.s&&s&&(s=Object.keys(this.s).some(s=>t.headers.get(s)===this.s[s])),s}}return t.CacheableResponse=s,t.Plugin=class{constructor(t){this.i=new s(t)}cacheWillUpdate({response:t}){return this.i.isResponseCacheable(t)?t:null}},t}({});
//# sourceMappingURL=workbox-cacheable-response.prod.js.map

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.expiration=function(t,e,s,i,a,n){"use strict";try{self["workbox:expiration:4.3.1"]&&_()}catch(t){}const h="workbox-expiration",c="cache-entries",r=t=>{const e=new URL(t,location);return e.hash="",e.href};class o{constructor(t){this.t=t,this.s=new e.DBWrapper(h,1,{onupgradeneeded:t=>this.i(t)})}i(t){const e=t.target.result.createObjectStore(c,{keyPath:"id"});e.createIndex("cacheName","cacheName",{unique:!1}),e.createIndex("timestamp","timestamp",{unique:!1}),s.deleteDatabase(this.t)}async setTimestamp(t,e){t=r(t),await this.s.put(c,{url:t,timestamp:e,cacheName:this.t,id:this.h(t)})}async getTimestamp(t){return(await this.s.get(c,this.h(t))).timestamp}async expireEntries(t,e){const s=await this.s.transaction(c,"readwrite",(s,i)=>{const a=s.objectStore(c),n=[];let h=0;a.index("timestamp").openCursor(null,"prev").onsuccess=(({target:s})=>{const a=s.result;if(a){const s=a.value;s.cacheName===this.t&&(t&&s.timestamp<t||e&&h>=e?n.push(a.value):h++),a.continue()}else i(n)})}),i=[];for(const t of s)await this.s.delete(c,t.id),i.push(t.url);return i}h(t){return this.t+"|"+r(t)}}class u{constructor(t,e={}){this.o=!1,this.u=!1,this.l=e.maxEntries,this.p=e.maxAgeSeconds,this.t=t,this.m=new o(t)}async expireEntries(){if(this.o)return void(this.u=!0);this.o=!0;const t=this.p?Date.now()-1e3*this.p:void 0,e=await this.m.expireEntries(t,this.l),s=await caches.open(this.t);for(const t of e)await s.delete(t);this.o=!1,this.u&&(this.u=!1,this.expireEntries())}async updateTimestamp(t){await this.m.setTimestamp(t,Date.now())}async isURLExpired(t){return await this.m.getTimestamp(t)<Date.now()-1e3*this.p}async delete(){this.u=!1,await this.m.expireEntries(1/0)}}return t.CacheExpiration=u,t.Plugin=class{constructor(t={}){this.D=t,this.p=t.maxAgeSeconds,this.g=new Map,t.purgeOnQuotaError&&n.registerQuotaErrorCallback(()=>this.deleteCacheAndMetadata())}k(t){if(t===a.cacheNames.getRuntimeName())throw new i.WorkboxError("expire-custom-caches-only");let e=this.g.get(t);return e||(e=new u(t,this.D),this.g.set(t,e)),e}cachedResponseWillBeUsed({event:t,request:e,cacheName:s,cachedResponse:i}){if(!i)return null;let a=this.N(i);const n=this.k(s);n.expireEntries();const h=n.updateTimestamp(e.url);if(t)try{t.waitUntil(h)}catch(t){}return a?i:null}N(t){if(!this.p)return!0;const e=this._(t);return null===e||e>=Date.now()-1e3*this.p}_(t){if(!t.headers.has("date"))return null;const e=t.headers.get("date"),s=new Date(e).getTime();return isNaN(s)?null:s}async cacheDidUpdate({cacheName:t,request:e}){const s=this.k(t);await s.updateTimestamp(e.url),await s.expireEntries()}async deleteCacheAndMetadata(){for(const[t,e]of this.g)await caches.delete(t),await e.delete();this.g=new Map}},t}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private,workbox.core);
//# sourceMappingURL=workbox-expiration.prod.js.map

View File

@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.navigationPreload=function(t){"use strict";try{self["workbox:navigation-preload:4.3.1"]&&_()}catch(t){}function e(){return Boolean(self.registration&&self.registration.navigationPreload)}return t.disable=function(){e()&&self.addEventListener("activate",t=>{t.waitUntil(self.registration.navigationPreload.disable().then(()=>{}))})},t.enable=function(t){e()&&self.addEventListener("activate",e=>{e.waitUntil(self.registration.navigationPreload.enable().then(()=>{t&&self.registration.navigationPreload.setHeaderValue(t)}))})},t.isSupported=e,t}({});
//# sourceMappingURL=workbox-navigation-preload.prod.js.map

View File

@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.googleAnalytics=function(e,t,o,n,a,c,w){"use strict";try{self["workbox:google-analytics:4.3.1"]&&_()}catch(e){}const r=/^\/(\w+\/)?collect/,s=e=>async({queue:t})=>{let o;for(;o=await t.shiftRequest();){const{request:n,timestamp:a}=o,c=new URL(n.url);try{const w="POST"===n.method?new URLSearchParams(await n.clone().text()):c.searchParams,r=a-(Number(w.get("qt"))||0),s=Date.now()-r;if(w.set("qt",s),e.parameterOverrides)for(const t of Object.keys(e.parameterOverrides)){const o=e.parameterOverrides[t];w.set(t,o)}"function"==typeof e.hitFilter&&e.hitFilter.call(null,w),await fetch(new Request(c.origin+c.pathname,{body:w.toString(),method:"POST",mode:"cors",credentials:"omit",headers:{"Content-Type":"text/plain"}}))}catch(e){throw await t.unshiftRequest(o),e}}},i=e=>{const t=({url:e})=>"www.google-analytics.com"===e.hostname&&r.test(e.pathname),o=new w.NetworkOnly({plugins:[e]});return[new n.Route(t,o,"GET"),new n.Route(t,o,"POST")]},l=e=>{const t=new c.NetworkFirst({cacheName:e});return new n.Route(({url:e})=>"www.google-analytics.com"===e.hostname&&"/analytics.js"===e.pathname,t,"GET")},m=e=>{const t=new c.NetworkFirst({cacheName:e});return new n.Route(({url:e})=>"www.googletagmanager.com"===e.hostname&&"/gtag/js"===e.pathname,t,"GET")},u=e=>{const t=new c.NetworkFirst({cacheName:e});return new n.Route(({url:e})=>"www.googletagmanager.com"===e.hostname&&"/gtm.js"===e.pathname,t,"GET")};return e.initialize=((e={})=>{const n=o.cacheNames.getGoogleAnalyticsName(e.cacheName),c=new t.Plugin("workbox-google-analytics",{maxRetentionTime:2880,onSync:s(e)}),w=[u(n),l(n),m(n),...i(c)],r=new a.Router;for(const e of w)r.registerRoute(e);r.addFetchListener()}),e}({},workbox.backgroundSync,workbox.core._private,workbox.routing,workbox.routing,workbox.strategies,workbox.strategies);
//# sourceMappingURL=workbox-offline-ga.prod.js.map

View File

@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.precaching=function(t,e,n,s,c){"use strict";try{self["workbox:precaching:4.3.1"]&&_()}catch(t){}const o=[],i={get:()=>o,add(t){o.push(...t)}};const a="__WB_REVISION__";function r(t){if(!t)throw new c.WorkboxError("add-to-cache-list-unexpected-type",{entry:t});if("string"==typeof t){const e=new URL(t,location);return{cacheKey:e.href,url:e.href}}const{revision:e,url:n}=t;if(!n)throw new c.WorkboxError("add-to-cache-list-unexpected-type",{entry:t});if(!e){const t=new URL(n,location);return{cacheKey:t.href,url:t.href}}const s=new URL(n,location),o=new URL(n,location);return o.searchParams.set(a,e),{cacheKey:o.href,url:s.href}}class l{constructor(t){this.t=e.cacheNames.getPrecacheName(t),this.s=new Map}addToCacheList(t){for(const e of t){const{cacheKey:t,url:n}=r(e);if(this.s.has(n)&&this.s.get(n)!==t)throw new c.WorkboxError("add-to-cache-list-conflicting-entries",{firstEntry:this.s.get(n),secondEntry:t});this.s.set(n,t)}}async install({event:t,plugins:e}={}){const n=[],s=[],c=await caches.open(this.t),o=await c.keys(),i=new Set(o.map(t=>t.url));for(const t of this.s.values())i.has(t)?s.push(t):n.push(t);const a=n.map(n=>this.o({event:t,plugins:e,url:n}));return await Promise.all(a),{updatedURLs:n,notUpdatedURLs:s}}async activate(){const t=await caches.open(this.t),e=await t.keys(),n=new Set(this.s.values()),s=[];for(const c of e)n.has(c.url)||(await t.delete(c),s.push(c.url));return{deletedURLs:s}}async o({url:t,event:e,plugins:o}){const i=new Request(t,{credentials:"same-origin"});let a,r=await s.fetchWrapper.fetch({event:e,plugins:o,request:i});for(const t of o||[])"cacheWillUpdate"in t&&(a=t.cacheWillUpdate.bind(t));if(!(a?a({event:e,request:i,response:r}):r.status<400))throw new c.WorkboxError("bad-precaching-response",{url:t,status:r.status});r.redirected&&(r=await async function(t){const e=t.clone(),n="body"in e?Promise.resolve(e.body):e.blob(),s=await n;return new Response(s,{headers:e.headers,status:e.status,statusText:e.statusText})}(r)),await n.cacheWrapper.put({event:e,plugins:o,request:i,response:r,cacheName:this.t,matchOptions:{ignoreSearch:!0}})}getURLsToCacheKeys(){return this.s}getCachedURLs(){return[...this.s.keys()]}getCacheKeyForURL(t){const e=new URL(t,location);return this.s.get(e.href)}}let u;const h=()=>(u||(u=new l),u);const d=(t,e)=>{const n=h().getURLsToCacheKeys();for(const s of function*(t,{ignoreURLParametersMatching:e,directoryIndex:n,cleanURLs:s,urlManipulation:c}={}){const o=new URL(t,location);o.hash="",yield o.href;const i=function(t,e){for(const n of[...t.searchParams.keys()])e.some(t=>t.test(n))&&t.searchParams.delete(n);return t}(o,e);if(yield i.href,n&&i.pathname.endsWith("/")){const t=new URL(i);t.pathname+=n,yield t.href}if(s){const t=new URL(i);t.pathname+=".html",yield t.href}if(c){const t=c({url:o});for(const e of t)yield e.href}}(t,e)){const t=n.get(s);if(t)return t}};let w=!1;const f=t=>{w||((({ignoreURLParametersMatching:t=[/^utm_/],directoryIndex:n="index.html",cleanURLs:s=!0,urlManipulation:c=null}={})=>{const o=e.cacheNames.getPrecacheName();addEventListener("fetch",e=>{const i=d(e.request.url,{cleanURLs:s,directoryIndex:n,ignoreURLParametersMatching:t,urlManipulation:c});if(!i)return;let a=caches.open(o).then(t=>t.match(i)).then(t=>t||fetch(i));e.respondWith(a)})})(t),w=!0)},y=t=>{const e=h(),n=i.get();t.waitUntil(e.install({event:t,plugins:n}).catch(t=>{throw t}))},p=t=>{const e=h(),n=i.get();t.waitUntil(e.activate({event:t,plugins:n}))},L=t=>{h().addToCacheList(t),t.length>0&&(addEventListener("install",y),addEventListener("activate",p))};return t.addPlugins=(t=>{i.add(t)}),t.addRoute=f,t.cleanupOutdatedCaches=(()=>{addEventListener("activate",t=>{const n=e.cacheNames.getPrecacheName();t.waitUntil((async(t,e="-precache-")=>{const n=(await caches.keys()).filter(n=>n.includes(e)&&n.includes(self.registration.scope)&&n!==t);return await Promise.all(n.map(t=>caches.delete(t))),n})(n).then(t=>{}))})}),t.getCacheKeyForURL=(t=>{return h().getCacheKeyForURL(t)}),t.precache=L,t.precacheAndRoute=((t,e)=>{L(t),f(e)}),t.PrecacheController=l,t}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private);
//# sourceMappingURL=workbox-precaching.prod.js.map

View File

@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.rangeRequests=function(e,n){"use strict";try{self["workbox:range-requests:4.3.1"]&&_()}catch(e){}async function t(e,t){try{if(206===t.status)return t;const s=e.headers.get("range");if(!s)throw new n.WorkboxError("no-range-header");const a=function(e){const t=e.trim().toLowerCase();if(!t.startsWith("bytes="))throw new n.WorkboxError("unit-must-be-bytes",{normalizedRangeHeader:t});if(t.includes(","))throw new n.WorkboxError("single-range-only",{normalizedRangeHeader:t});const s=/(\d*)-(\d*)/.exec(t);if(null===s||!s[1]&&!s[2])throw new n.WorkboxError("invalid-range-values",{normalizedRangeHeader:t});return{start:""===s[1]?null:Number(s[1]),end:""===s[2]?null:Number(s[2])}}(s),r=await t.blob(),i=function(e,t,s){const a=e.size;if(s>a||t<0)throw new n.WorkboxError("range-not-satisfiable",{size:a,end:s,start:t});let r,i;return null===t?(r=a-s,i=a):null===s?(r=t,i=a):(r=t,i=s+1),{start:r,end:i}}(r,a.start,a.end),o=r.slice(i.start,i.end),u=o.size,l=new Response(o,{status:206,statusText:"Partial Content",headers:t.headers});return l.headers.set("Content-Length",u),l.headers.set("Content-Range",`bytes ${i.start}-${i.end-1}/`+r.size),l}catch(e){return new Response("",{status:416,statusText:"Range Not Satisfiable"})}}return e.createPartialResponse=t,e.Plugin=class{async cachedResponseWillBeUsed({request:e,cachedResponse:n}){return n&&e.headers.has("range")?await t(e,n):n}},e}({},workbox.core._private);
//# sourceMappingURL=workbox-range-requests.prod.js.map

View File

@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.routing=function(t,e,r){"use strict";try{self["workbox:routing:4.3.1"]&&_()}catch(t){}const s="GET",n=t=>t&&"object"==typeof t?t:{handle:t};class o{constructor(t,e,r){this.handler=n(e),this.match=t,this.method=r||s}}class i extends o{constructor(t,{whitelist:e=[/./],blacklist:r=[]}={}){super(t=>this.t(t),t),this.s=e,this.o=r}t({url:t,request:e}){if("navigate"!==e.mode)return!1;const r=t.pathname+t.search;for(const t of this.o)if(t.test(r))return!1;return!!this.s.some(t=>t.test(r))}}class u extends o{constructor(t,e,r){super(({url:e})=>{const r=t.exec(e.href);return r?e.origin!==location.origin&&0!==r.index?null:r.slice(1):null},e,r)}}class c{constructor(){this.i=new Map}get routes(){return this.i}addFetchListener(){self.addEventListener("fetch",t=>{const{request:e}=t,r=this.handleRequest({request:e,event:t});r&&t.respondWith(r)})}addCacheListener(){self.addEventListener("message",async t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,r=Promise.all(e.urlsToCache.map(t=>{"string"==typeof t&&(t=[t]);const e=new Request(...t);return this.handleRequest({request:e})}));t.waitUntil(r),t.ports&&t.ports[0]&&(await r,t.ports[0].postMessage(!0))}})}handleRequest({request:t,event:e}){const r=new URL(t.url,location);if(!r.protocol.startsWith("http"))return;let s,{params:n,route:o}=this.findMatchingRoute({url:r,request:t,event:e}),i=o&&o.handler;if(!i&&this.u&&(i=this.u),i){try{s=i.handle({url:r,request:t,event:e,params:n})}catch(t){s=Promise.reject(t)}return s&&this.h&&(s=s.catch(t=>this.h.handle({url:r,event:e,err:t}))),s}}findMatchingRoute({url:t,request:e,event:r}){const s=this.i.get(e.method)||[];for(const n of s){let s,o=n.match({url:t,request:e,event:r});if(o)return Array.isArray(o)&&o.length>0?s=o:o.constructor===Object&&Object.keys(o).length>0&&(s=o),{route:n,params:s}}return{}}setDefaultHandler(t){this.u=n(t)}setCatchHandler(t){this.h=n(t)}registerRoute(t){this.i.has(t.method)||this.i.set(t.method,[]),this.i.get(t.method).push(t)}unregisterRoute(t){if(!this.i.has(t.method))throw new r.WorkboxError("unregister-route-but-not-found-with-method",{method:t.method});const e=this.i.get(t.method).indexOf(t);if(!(e>-1))throw new r.WorkboxError("unregister-route-route-not-registered");this.i.get(t.method).splice(e,1)}}let a;const h=()=>(a||((a=new c).addFetchListener(),a.addCacheListener()),a);return t.NavigationRoute=i,t.RegExpRoute=u,t.registerNavigationRoute=((t,r={})=>{const s=e.cacheNames.getPrecacheName(r.cacheName),n=new i(async()=>{try{const e=await caches.match(t,{cacheName:s});if(e)return e;throw new Error(`The cache ${s} did not have an entry for `+`${t}.`)}catch(e){return fetch(t)}},{whitelist:r.whitelist,blacklist:r.blacklist});return h().registerRoute(n),n}),t.registerRoute=((t,e,s="GET")=>{let n;if("string"==typeof t){const r=new URL(t,location);n=new o(({url:t})=>t.href===r.href,e,s)}else if(t instanceof RegExp)n=new u(t,e,s);else if("function"==typeof t)n=new o(t,e,s);else{if(!(t instanceof o))throw new r.WorkboxError("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});n=t}return h().registerRoute(n),n}),t.Route=o,t.Router=c,t.setCatchHandler=(t=>{h().setCatchHandler(t)}),t.setDefaultHandler=(t=>{h().setDefaultHandler(t)}),t}({},workbox.core._private,workbox.core._private);
//# sourceMappingURL=workbox-routing.prod.js.map

View File

@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.strategies=function(e,t,s,n,r){"use strict";try{self["workbox:strategies:4.3.1"]&&_()}catch(e){}class i{constructor(e={}){this.t=t.cacheNames.getRuntimeName(e.cacheName),this.s=e.plugins||[],this.i=e.fetchOptions||null,this.h=e.matchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){"string"==typeof t&&(t=new Request(t));let n,i=await s.cacheWrapper.match({cacheName:this.t,request:t,event:e,matchOptions:this.h,plugins:this.s});if(!i)try{i=await this.u(t,e)}catch(e){n=e}if(!i)throw new r.WorkboxError("no-response",{url:t.url,error:n});return i}async u(e,t){const r=await n.fetchWrapper.fetch({request:e,event:t,fetchOptions:this.i,plugins:this.s}),i=r.clone(),h=s.cacheWrapper.put({cacheName:this.t,request:e,response:i,event:t,plugins:this.s});if(t)try{t.waitUntil(h)}catch(e){}return r}}class h{constructor(e={}){this.t=t.cacheNames.getRuntimeName(e.cacheName),this.s=e.plugins||[],this.h=e.matchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){"string"==typeof t&&(t=new Request(t));const n=await s.cacheWrapper.match({cacheName:this.t,request:t,event:e,matchOptions:this.h,plugins:this.s});if(!n)throw new r.WorkboxError("no-response",{url:t.url});return n}}const u={cacheWillUpdate:({response:e})=>200===e.status||0===e.status?e:null};class a{constructor(e={}){if(this.t=t.cacheNames.getRuntimeName(e.cacheName),e.plugins){let t=e.plugins.some(e=>!!e.cacheWillUpdate);this.s=t?e.plugins:[u,...e.plugins]}else this.s=[u];this.o=e.networkTimeoutSeconds,this.i=e.fetchOptions||null,this.h=e.matchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){const s=[];"string"==typeof t&&(t=new Request(t));const n=[];let i;if(this.o){const{id:r,promise:h}=this.l({request:t,event:e,logs:s});i=r,n.push(h)}const h=this.q({timeoutId:i,request:t,event:e,logs:s});n.push(h);let u=await Promise.race(n);if(u||(u=await h),!u)throw new r.WorkboxError("no-response",{url:t.url});return u}l({request:e,logs:t,event:s}){let n;return{promise:new Promise(t=>{n=setTimeout(async()=>{t(await this.p({request:e,event:s}))},1e3*this.o)}),id:n}}async q({timeoutId:e,request:t,logs:r,event:i}){let h,u;try{u=await n.fetchWrapper.fetch({request:t,event:i,fetchOptions:this.i,plugins:this.s})}catch(e){h=e}if(e&&clearTimeout(e),h||!u)u=await this.p({request:t,event:i});else{const e=u.clone(),n=s.cacheWrapper.put({cacheName:this.t,request:t,response:e,event:i,plugins:this.s});if(i)try{i.waitUntil(n)}catch(e){}}return u}p({event:e,request:t}){return s.cacheWrapper.match({cacheName:this.t,request:t,event:e,matchOptions:this.h,plugins:this.s})}}class c{constructor(e={}){this.t=t.cacheNames.getRuntimeName(e.cacheName),this.s=e.plugins||[],this.i=e.fetchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){let s,i;"string"==typeof t&&(t=new Request(t));try{i=await n.fetchWrapper.fetch({request:t,event:e,fetchOptions:this.i,plugins:this.s})}catch(e){s=e}if(!i)throw new r.WorkboxError("no-response",{url:t.url,error:s});return i}}class o{constructor(e={}){if(this.t=t.cacheNames.getRuntimeName(e.cacheName),this.s=e.plugins||[],e.plugins){let t=e.plugins.some(e=>!!e.cacheWillUpdate);this.s=t?e.plugins:[u,...e.plugins]}else this.s=[u];this.i=e.fetchOptions||null,this.h=e.matchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){"string"==typeof t&&(t=new Request(t));const n=this.u({request:t,event:e});let i,h=await s.cacheWrapper.match({cacheName:this.t,request:t,event:e,matchOptions:this.h,plugins:this.s});if(h){if(e)try{e.waitUntil(n)}catch(i){}}else try{h=await n}catch(e){i=e}if(!h)throw new r.WorkboxError("no-response",{url:t.url,error:i});return h}async u({request:e,event:t}){const r=await n.fetchWrapper.fetch({request:e,event:t,fetchOptions:this.i,plugins:this.s}),i=s.cacheWrapper.put({cacheName:this.t,request:e,response:r.clone(),event:t,plugins:this.s});if(t)try{t.waitUntil(i)}catch(e){}return r}}const l={cacheFirst:i,cacheOnly:h,networkFirst:a,networkOnly:c,staleWhileRevalidate:o},q=e=>{const t=l[e];return e=>new t(e)},w=q("cacheFirst"),p=q("cacheOnly"),v=q("networkFirst"),y=q("networkOnly"),m=q("staleWhileRevalidate");return e.CacheFirst=i,e.CacheOnly=h,e.NetworkFirst=a,e.NetworkOnly=c,e.StaleWhileRevalidate=o,e.cacheFirst=w,e.cacheOnly=p,e.networkFirst=v,e.networkOnly=y,e.staleWhileRevalidate=m,e}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private);
//# sourceMappingURL=workbox-strategies.prod.js.map

View File

@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.streams=function(e){"use strict";try{self["workbox:streams:4.3.1"]&&_()}catch(e){}function n(e){const n=e.map(e=>Promise.resolve(e).then(e=>(function(e){return e.body&&e.body.getReader?e.body.getReader():e.getReader?e.getReader():new Response(e).body.getReader()})(e)));let t,r;const s=new Promise((e,n)=>{t=e,r=n});let o=0;return{done:s,stream:new ReadableStream({pull(e){return n[o].then(e=>e.read()).then(r=>{if(r.done)return++o>=n.length?(e.close(),void t()):this.pull(e);e.enqueue(r.value)}).catch(e=>{throw r(e),e})},cancel(){t()}})}}function t(e={}){const n=new Headers(e);return n.has("content-type")||n.set("content-type","text/html"),n}function r(e,r){const{done:s,stream:o}=n(e),a=t(r);return{done:s,response:new Response(o,{headers:a})}}let s=void 0;function o(){if(void 0===s)try{new ReadableStream({start(){}}),s=!0}catch(e){s=!1}return s}return e.concatenate=n,e.concatenateToResponse=r,e.isSupported=o,e.strategy=function(e,n){return async({event:s,url:a,params:c})=>{if(o()){const{done:t,response:o}=r(e.map(e=>e({event:s,url:a,params:c})),n);return s.waitUntil(t),o}const i=await Promise.all(e.map(e=>e({event:s,url:a,params:c})).map(async e=>{const n=await e;return n instanceof Response?n.blob():n})),u=t(n);return new Response(new Blob(i),{headers:u})}},e}({});
//# sourceMappingURL=workbox-streams.prod.js.map

View File

@ -0,0 +1,2 @@
!function(){"use strict";try{self["workbox:sw:4.3.1"]&&_()}catch(t){}const t="https://storage.googleapis.com/workbox-cdn/releases/4.3.1",e={backgroundSync:"background-sync",broadcastUpdate:"broadcast-update",cacheableResponse:"cacheable-response",core:"core",expiration:"expiration",googleAnalytics:"offline-ga",navigationPreload:"navigation-preload",precaching:"precaching",rangeRequests:"range-requests",routing:"routing",strategies:"strategies",streams:"streams"};self.workbox=new class{constructor(){return this.v={},this.t={debug:"localhost"===self.location.hostname,modulePathPrefix:null,modulePathCb:null},this.s=this.t.debug?"dev":"prod",this.o=!1,new Proxy(this,{get(t,s){if(t[s])return t[s];const o=e[s];return o&&t.loadModule(`workbox-${o}`),t[s]}})}setConfig(t={}){if(this.o)throw new Error("Config must be set before accessing workbox.* modules");Object.assign(this.t,t),this.s=this.t.debug?"dev":"prod"}loadModule(t){const e=this.i(t);try{importScripts(e),this.o=!0}catch(s){throw console.error(`Unable to import module '${t}' from '${e}'.`),s}}i(e){if(this.t.modulePathCb)return this.t.modulePathCb(e,this.t.debug);let s=[t];const o=`${e}.${this.s}.js`,r=this.t.modulePathPrefix;return r&&""===(s=r.split("/"))[s.length-1]&&s.splice(s.length-1,1),s.push(o),s.join("/")}}}();
//# sourceMappingURL=workbox-sw.js.map

View File

@ -0,0 +1,2 @@
try{self["workbox:window:4.3.1"]&&_()}catch(n){}var n=function(n,t){return new Promise(function(i){var e=new MessageChannel;e.port1.onmessage=function(n){return i(n.data)},n.postMessage(t,[e.port2])})};function t(n,t){for(var i=0;i<t.length;i++){var e=t[i];e.enumerable=e.enumerable||!1,e.configurable=!0,"value"in e&&(e.writable=!0),Object.defineProperty(n,e.key,e)}}function i(n){if(void 0===n)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return n}try{self["workbox:core:4.3.1"]&&_()}catch(n){}var e=function(){var n=this;this.promise=new Promise(function(t,i){n.resolve=t,n.reject=i})},r=function(n,t){return new URL(n,location).href===new URL(t,location).href},o=function(n,t){Object.assign(this,t,{type:n})};function u(n){return function(){for(var t=[],i=0;i<arguments.length;i++)t[i]=arguments[i];try{return Promise.resolve(n.apply(this,t))}catch(n){return Promise.reject(n)}}}function a(n,t,i){return i?t?t(n):n:(n&&n.then||(n=Promise.resolve(n)),t?n.then(t):n)}function s(){}var c=function(c){var f,h;function v(n,t){var r;return void 0===t&&(t={}),(r=c.call(this)||this).t=n,r.i=t,r.o=0,r.u=new e,r.s=new e,r.h=new e,r.v=r.v.bind(i(i(r))),r.l=r.l.bind(i(i(r))),r.g=r.g.bind(i(i(r))),r.m=r.m.bind(i(i(r))),r}h=c,(f=v).prototype=Object.create(h.prototype),f.prototype.constructor=f,f.__proto__=h;var l,w,g,d=v.prototype;return d.register=u(function(n){var t,i,e=this,u=(void 0===n?{}:n).immediate,c=void 0!==u&&u;return t=function(){return e.p=Boolean(navigator.serviceWorker.controller),e.P=e.R(),a(e.k(),function(n){e.B=n,e.P&&(e.O=e.P,e.s.resolve(e.P),e.h.resolve(e.P),e.j(e.P),e.P.addEventListener("statechange",e.l,{once:!0}));var t=e.B.waiting;return t&&r(t.scriptURL,e.t)&&(e.O=t,Promise.resolve().then(function(){e.dispatchEvent(new o("waiting",{sw:t,wasWaitingBeforeRegister:!0}))})),e.O&&e.u.resolve(e.O),e.B.addEventListener("updatefound",e.g),navigator.serviceWorker.addEventListener("controllerchange",e.m,{once:!0}),"BroadcastChannel"in self&&(e.C=new BroadcastChannel("workbox"),e.C.addEventListener("message",e.v)),navigator.serviceWorker.addEventListener("message",e.v),e.B})},(i=function(){if(!c&&"complete"!==document.readyState)return function(n,t){if(!t)return n&&n.then?n.then(s):Promise.resolve()}(new Promise(function(n){return addEventListener("load",n)}))}())&&i.then?i.then(t):t(i)}),d.getSW=u(function(){return this.O||this.u.promise}),d.messageSW=u(function(t){return a(this.getSW(),function(i){return n(i,t)})}),d.R=function(){var n=navigator.serviceWorker.controller;if(n&&r(n.scriptURL,this.t))return n},d.k=u(function(){var n=this;return function(n,t){try{var i=n()}catch(n){return t(n)}return i&&i.then?i.then(void 0,t):i}(function(){return a(navigator.serviceWorker.register(n.t,n.i),function(t){return n.L=performance.now(),t})},function(n){throw n})}),d.j=function(t){n(t,{type:"WINDOW_READY",meta:"workbox-window"})},d.g=function(){var n=this.B.installing;this.o>0||!r(n.scriptURL,this.t)||performance.now()>this.L+6e4?(this.W=n,this.B.removeEventListener("updatefound",this.g)):(this.O=n,this.u.resolve(n)),++this.o,n.addEventListener("statechange",this.l)},d.l=function(n){var t=this,i=n.target,e=i.state,r=i===this.W,u=r?"external":"",a={sw:i,originalEvent:n};!r&&this.p&&(a.isUpdate=!0),this.dispatchEvent(new o(u+e,a)),"installed"===e?this._=setTimeout(function(){"installed"===e&&t.B.waiting===i&&t.dispatchEvent(new o(u+"waiting",a))},200):"activating"===e&&(clearTimeout(this._),r||this.s.resolve(i))},d.m=function(n){var t=this.O;t===navigator.serviceWorker.controller&&(this.dispatchEvent(new o("controlling",{sw:t,originalEvent:n})),this.h.resolve(t))},d.v=function(n){var t=n.data;this.dispatchEvent(new o("message",{data:t,originalEvent:n}))},l=v,(w=[{key:"active",get:function(){return this.s.promise}},{key:"controlling",get:function(){return this.h.promise}}])&&t(l.prototype,w),g&&t(l,g),v}(function(){function n(){this.D={}}var t=n.prototype;return t.addEventListener=function(n,t){this.T(n).add(t)},t.removeEventListener=function(n,t){this.T(n).delete(t)},t.dispatchEvent=function(n){n.target=this,this.T(n.type).forEach(function(t){return t(n)})},t.T=function(n){return this.D[n]=this.D[n]||new Set},n}());export{c as Workbox,n as messageSW};
//# sourceMappingURL=workbox-window.prod.es5.mjs.map

View File

@ -0,0 +1,2 @@
try{self["workbox:window:4.3.1"]&&_()}catch(t){}const t=(t,s)=>new Promise(i=>{let e=new MessageChannel;e.port1.onmessage=(t=>i(t.data)),t.postMessage(s,[e.port2])});try{self["workbox:core:4.3.1"]&&_()}catch(t){}class s{constructor(){this.promise=new Promise((t,s)=>{this.resolve=t,this.reject=s})}}class i{constructor(){this.t={}}addEventListener(t,s){this.s(t).add(s)}removeEventListener(t,s){this.s(t).delete(s)}dispatchEvent(t){t.target=this,this.s(t.type).forEach(s=>s(t))}s(t){return this.t[t]=this.t[t]||new Set}}const e=(t,s)=>new URL(t,location).href===new URL(s,location).href;class n{constructor(t,s){Object.assign(this,s,{type:t})}}const h=200,a=6e4;class o extends i{constructor(t,i={}){super(),this.i=t,this.h=i,this.o=0,this.l=new s,this.g=new s,this.u=new s,this.m=this.m.bind(this),this.v=this.v.bind(this),this.p=this.p.bind(this),this._=this._.bind(this)}async register({immediate:t=!1}={}){t||"complete"===document.readyState||await new Promise(t=>addEventListener("load",t)),this.C=Boolean(navigator.serviceWorker.controller),this.W=this.L(),this.S=await this.B(),this.W&&(this.R=this.W,this.g.resolve(this.W),this.u.resolve(this.W),this.P(this.W),this.W.addEventListener("statechange",this.v,{once:!0}));const s=this.S.waiting;return s&&e(s.scriptURL,this.i)&&(this.R=s,Promise.resolve().then(()=>{this.dispatchEvent(new n("waiting",{sw:s,wasWaitingBeforeRegister:!0}))})),this.R&&this.l.resolve(this.R),this.S.addEventListener("updatefound",this.p),navigator.serviceWorker.addEventListener("controllerchange",this._,{once:!0}),"BroadcastChannel"in self&&(this.T=new BroadcastChannel("workbox"),this.T.addEventListener("message",this.m)),navigator.serviceWorker.addEventListener("message",this.m),this.S}get active(){return this.g.promise}get controlling(){return this.u.promise}async getSW(){return this.R||this.l.promise}async messageSW(s){const i=await this.getSW();return t(i,s)}L(){const t=navigator.serviceWorker.controller;if(t&&e(t.scriptURL,this.i))return t}async B(){try{const t=await navigator.serviceWorker.register(this.i,this.h);return this.U=performance.now(),t}catch(t){throw t}}P(s){t(s,{type:"WINDOW_READY",meta:"workbox-window"})}p(){const t=this.S.installing;this.o>0||!e(t.scriptURL,this.i)||performance.now()>this.U+a?(this.k=t,this.S.removeEventListener("updatefound",this.p)):(this.R=t,this.l.resolve(t)),++this.o,t.addEventListener("statechange",this.v)}v(t){const s=t.target,{state:i}=s,e=s===this.k,a=e?"external":"",o={sw:s,originalEvent:t};!e&&this.C&&(o.isUpdate=!0),this.dispatchEvent(new n(a+i,o)),"installed"===i?this.D=setTimeout(()=>{"installed"===i&&this.S.waiting===s&&this.dispatchEvent(new n(a+"waiting",o))},h):"activating"===i&&(clearTimeout(this.D),e||this.g.resolve(s))}_(t){const s=this.R;s===navigator.serviceWorker.controller&&(this.dispatchEvent(new n("controlling",{sw:s,originalEvent:t})),this.u.resolve(s))}m(t){const{data:s}=t;this.dispatchEvent(new n("message",{data:s,originalEvent:t}))}}export{o as Workbox,t as messageSW};
//# sourceMappingURL=workbox-window.prod.mjs.map

View File

@ -0,0 +1,2 @@
!function(n,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((n=n||self).workbox={})}(this,function(n){"use strict";try{self["workbox:window:4.3.1"]&&_()}catch(n){}var t=function(n,t){return new Promise(function(i){var e=new MessageChannel;e.port1.onmessage=function(n){return i(n.data)},n.postMessage(t,[e.port2])})};function i(n,t){for(var i=0;i<t.length;i++){var e=t[i];e.enumerable=e.enumerable||!1,e.configurable=!0,"value"in e&&(e.writable=!0),Object.defineProperty(n,e.key,e)}}function e(n){if(void 0===n)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return n}try{self["workbox:core:4.3.1"]&&_()}catch(n){}var r=function(){var n=this;this.promise=new Promise(function(t,i){n.resolve=t,n.reject=i})},o=function(n,t){return new URL(n,location).href===new URL(t,location).href},u=function(n,t){Object.assign(this,t,{type:n})};function s(n){return function(){for(var t=[],i=0;i<arguments.length;i++)t[i]=arguments[i];try{return Promise.resolve(n.apply(this,t))}catch(n){return Promise.reject(n)}}}function a(n,t,i){return i?t?t(n):n:(n&&n.then||(n=Promise.resolve(n)),t?n.then(t):n)}function c(){}var f=function(n){var f,h;function v(t,i){var o;return void 0===i&&(i={}),(o=n.call(this)||this).t=t,o.i=i,o.o=0,o.u=new r,o.s=new r,o.h=new r,o.v=o.v.bind(e(e(o))),o.l=o.l.bind(e(e(o))),o.g=o.g.bind(e(e(o))),o.m=o.m.bind(e(e(o))),o}h=n,(f=v).prototype=Object.create(h.prototype),f.prototype.constructor=f,f.__proto__=h;var l,w,d,g=v.prototype;return g.register=s(function(n){var t,i,e=this,r=(void 0===n?{}:n).immediate,s=void 0!==r&&r;return t=function(){return e.p=Boolean(navigator.serviceWorker.controller),e.P=e.j(),a(e.O(),function(n){e.R=n,e.P&&(e._=e.P,e.s.resolve(e.P),e.h.resolve(e.P),e.k(e.P),e.P.addEventListener("statechange",e.l,{once:!0}));var t=e.R.waiting;return t&&o(t.scriptURL,e.t)&&(e._=t,Promise.resolve().then(function(){e.dispatchEvent(new u("waiting",{sw:t,wasWaitingBeforeRegister:!0}))})),e._&&e.u.resolve(e._),e.R.addEventListener("updatefound",e.g),navigator.serviceWorker.addEventListener("controllerchange",e.m,{once:!0}),"BroadcastChannel"in self&&(e.B=new BroadcastChannel("workbox"),e.B.addEventListener("message",e.v)),navigator.serviceWorker.addEventListener("message",e.v),e.R})},(i=function(){if(!s&&"complete"!==document.readyState)return function(n,t){if(!t)return n&&n.then?n.then(c):Promise.resolve()}(new Promise(function(n){return addEventListener("load",n)}))}())&&i.then?i.then(t):t(i)}),g.getSW=s(function(){return this._||this.u.promise}),g.messageSW=s(function(n){return a(this.getSW(),function(i){return t(i,n)})}),g.j=function(){var n=navigator.serviceWorker.controller;if(n&&o(n.scriptURL,this.t))return n},g.O=s(function(){var n=this;return function(n,t){try{var i=n()}catch(n){return t(n)}return i&&i.then?i.then(void 0,t):i}(function(){return a(navigator.serviceWorker.register(n.t,n.i),function(t){return n.C=performance.now(),t})},function(n){throw n})}),g.k=function(n){t(n,{type:"WINDOW_READY",meta:"workbox-window"})},g.g=function(){var n=this.R.installing;this.o>0||!o(n.scriptURL,this.t)||performance.now()>this.C+6e4?(this.L=n,this.R.removeEventListener("updatefound",this.g)):(this._=n,this.u.resolve(n)),++this.o,n.addEventListener("statechange",this.l)},g.l=function(n){var t=this,i=n.target,e=i.state,r=i===this.L,o=r?"external":"",s={sw:i,originalEvent:n};!r&&this.p&&(s.isUpdate=!0),this.dispatchEvent(new u(o+e,s)),"installed"===e?this.W=setTimeout(function(){"installed"===e&&t.R.waiting===i&&t.dispatchEvent(new u(o+"waiting",s))},200):"activating"===e&&(clearTimeout(this.W),r||this.s.resolve(i))},g.m=function(n){var t=this._;t===navigator.serviceWorker.controller&&(this.dispatchEvent(new u("controlling",{sw:t,originalEvent:n})),this.h.resolve(t))},g.v=function(n){var t=n.data;this.dispatchEvent(new u("message",{data:t,originalEvent:n}))},l=v,(w=[{key:"active",get:function(){return this.s.promise}},{key:"controlling",get:function(){return this.h.promise}}])&&i(l.prototype,w),d&&i(l,d),v}(function(){function n(){this.D={}}var t=n.prototype;return t.addEventListener=function(n,t){this.M(n).add(t)},t.removeEventListener=function(n,t){this.M(n).delete(t)},t.dispatchEvent=function(n){n.target=this,this.M(n.type).forEach(function(t){return t(n)})},t.M=function(n){return this.D[n]=this.D[n]||new Set},n}());n.Workbox=f,n.messageSW=t,Object.defineProperty(n,"__esModule",{value:!0})});
//# sourceMappingURL=workbox-window.prod.umd.js.map

View File

@ -5,7 +5,7 @@
// In order to run:
// npm install canvas # please do not check it in
// npm run build-node
// yarn build-node
// node build/static/js/build-node.js
// open test.png

View File

@ -24,6 +24,7 @@ const crowdinMap = {
"nb-NO": "en-nb",
"nl-NL": "en-nl",
"nn-NO": "en-nnno",
"oc-FR": "en-oc",
"pa-IN": "en-pain",
"pl-PL": "en-pl",
"pt-BR": "en-ptbr",
@ -60,6 +61,7 @@ const flags = {
"nb-NO": "🇳🇴",
"nl-NL": "🇳🇱",
"nn-NO": "🇳🇴",
"oc-FR": "🏳",
"pa-IN": "🇮🇳",
"pl-PL": "🇵🇱",
"pt-BR": "🇧🇷",
@ -96,6 +98,7 @@ const languages = {
"nb-NO": "Norsk bokmål",
"nl-NL": "Nederlands",
"nn-NO": "Norsk nynorsk",
"oc-FR": "Occitan",
"pa-IN": "ਪੰਜਾਬੀ",
"pl-PL": "Polski",
"pt-BR": "Português Brasileiro",

View File

@ -58,7 +58,7 @@ export const actionAlignTop = register({
<ToolButton
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={<AlignTopIcon appearance={appState.appearance} />}
icon={<AlignTopIcon theme={appState.theme} />}
onClick={() => updateData(null)}
title={`${t("labels.alignTop")}${getShortcutKey(
"CtrlOrCmd+Shift+Up",
@ -87,7 +87,7 @@ export const actionAlignBottom = register({
<ToolButton
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={<AlignBottomIcon appearance={appState.appearance} />}
icon={<AlignBottomIcon theme={appState.theme} />}
onClick={() => updateData(null)}
title={`${t("labels.alignBottom")}${getShortcutKey(
"CtrlOrCmd+Shift+Down",
@ -116,7 +116,7 @@ export const actionAlignLeft = register({
<ToolButton
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={<AlignLeftIcon appearance={appState.appearance} />}
icon={<AlignLeftIcon theme={appState.theme} />}
onClick={() => updateData(null)}
title={`${t("labels.alignLeft")}${getShortcutKey(
"CtrlOrCmd+Shift+Left",
@ -145,7 +145,7 @@ export const actionAlignRight = register({
<ToolButton
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={<AlignRightIcon appearance={appState.appearance} />}
icon={<AlignRightIcon theme={appState.theme} />}
onClick={() => updateData(null)}
title={`${t("labels.alignRight")}${getShortcutKey(
"CtrlOrCmd+Shift+Right",
@ -172,7 +172,7 @@ export const actionAlignVerticallyCentered = register({
<ToolButton
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={<CenterVerticallyIcon appearance={appState.appearance} />}
icon={<CenterVerticallyIcon theme={appState.theme} />}
onClick={() => updateData(null)}
title={t("labels.centerVertically")}
aria-label={t("labels.centerVertically")}
@ -197,7 +197,7 @@ export const actionAlignHorizontallyCentered = register({
<ToolButton
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={<CenterHorizontallyIcon appearance={appState.appearance} />}
icon={<CenterHorizontallyIcon theme={appState.theme} />}
onClick={() => updateData(null)}
title={t("labels.centerHorizontally")}
aria-label={t("labels.centerHorizontally")}

View File

@ -48,7 +48,7 @@ export const actionClearCanvas = register({
),
appState: {
...getDefaultAppState(),
appearance: appState.appearance,
theme: appState.theme,
elementLocked: appState.elementLocked,
exportBackground: appState.exportBackground,
exportEmbedScene: appState.exportEmbedScene,

View File

@ -17,7 +17,8 @@ export const actionCopy = register({
};
},
contextItemLabel: "labels.copy",
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.C,
// don't supply a shortcut since we handle this conditionally via onCopy event
keyTest: undefined,
});
export const actionCut = register({
@ -94,7 +95,14 @@ export const actionCopyAsPng = register({
return {
appState: {
...appState,
toastMessage: t("toast.copyToClipboardAsPng"),
toastMessage: t("toast.copyToClipboardAsPng", {
exportSelection: selectedElements.length
? t("toast.selection")
: t("toast.canvas"),
exportColorScheme: appState.exportWithDarkMode
? t("buttons.darkMode")
: t("buttons.lightMode"),
}),
},
commitToHistory: false,
};

View File

@ -53,7 +53,7 @@ export const distributeHorizontally = register({
<ToolButton
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={<DistributeHorizontallyIcon appearance={appState.appearance} />}
icon={<DistributeHorizontallyIcon theme={appState.theme} />}
onClick={() => updateData(null)}
title={`${t("labels.distributeHorizontally")}${getShortcutKey(
"Alt+H",
@ -81,7 +81,7 @@ export const distributeVertically = register({
<ToolButton
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={<DistributeVerticallyIcon appearance={appState.appearance} />}
icon={<DistributeVerticallyIcon theme={appState.theme} />}
onClick={() => updateData(null)}
title={`${t("labels.distributeVertically")}${getShortcutKey("Alt+V")}`}
aria-label={t("labels.distributeVertically")}

View File

@ -5,11 +5,13 @@ import { ProjectName } from "../components/ProjectName";
import { ToolButton } from "../components/ToolButton";
import "../components/ToolIcon.scss";
import { Tooltip } from "../components/Tooltip";
import { DarkModeToggle, Appearence } from "../components/DarkModeToggle";
import { loadFromJSON, saveAsJSON } from "../data";
import { t } from "../i18n";
import useIsMobile from "../is-mobile";
import { KEYS } from "../keys";
import { register } from "./register";
import { supported } from "browser-fs-access";
export const actionChangeProjectName = register({
name: "changeProjectName",
@ -17,11 +19,14 @@ export const actionChangeProjectName = register({
trackEvent("change", "title");
return { appState: { ...appState, name: value }, commitToHistory: false };
},
PanelComponent: ({ appState, updateData }) => (
PanelComponent: ({ appState, updateData, appProps }) => (
<ProjectName
label={t("labels.fileTitle")}
value={appState.name || "Unnamed"}
onChange={(name: string) => updateData(name)}
isNameEditable={
typeof appProps.name === "undefined" && !appState.viewModeEnabled
}
/>
),
});
@ -160,9 +165,7 @@ export const actionSaveAsScene = register({
title={t("buttons.saveAs")}
aria-label={t("buttons.saveAs")}
showAriaLabel={useIsMobile()}
hidden={
!("chooseFileSystemEntries" in window || "showOpenFilePicker" in window)
}
hidden={!supported}
onClick={() => updateData(null)}
/>
),
@ -204,3 +207,31 @@ export const actionLoadScene = register({
/>
),
});
export const actionExportWithDarkMode = register({
name: "exportWithDarkMode",
perform: (_elements, appState, value) => {
return {
appState: { ...appState, exportWithDarkMode: value },
commitToHistory: false,
};
},
PanelComponent: ({ appState, updateData }) => (
<div
style={{
display: "flex",
justifyContent: "flex-end",
marginTop: "-45px",
marginBottom: "10px",
}}
>
<DarkModeToggle
value={appState.exportWithDarkMode ? "dark" : "light"}
onChange={(theme: Appearence) => {
updateData(theme === "dark");
}}
title={t("labels.toggleExportColorScheme")}
/>
</div>
),
});

View File

@ -18,7 +18,7 @@ import { isBindingElement } from "../element/typeChecks";
export const actionFinalize = register({
name: "finalize",
perform: (elements, appState) => {
perform: (elements, appState, _, { canvas }) => {
if (appState.editingLinearElement) {
const {
elementId,
@ -126,7 +126,7 @@ export const actionFinalize = register({
(!appState.elementLocked && appState.elementType !== "draw") ||
!multiPointElement
) {
resetCursor();
resetCursor(canvas);
}
return {
elements: newElements,

View File

@ -134,7 +134,7 @@ export const actionGroup = register({
<ToolButton
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={<GroupIcon appearance={appState.appearance} />}
icon={<GroupIcon theme={appState.theme} />}
onClick={() => updateData(null)}
title={`${t("labels.group")}${getShortcutKey("CtrlOrCmd+G")}`}
aria-label={t("labels.group")}
@ -181,7 +181,7 @@ export const actionUngroup = register({
<ToolButton
type="button"
hidden={getSelectedGroupIds(appState).length === 0}
icon={<UngroupIcon appearance={appState.appearance} />}
icon={<UngroupIcon theme={appState.theme} />}
onClick={() => updateData(null)}
title={`${t("labels.ungroup")}${getShortcutKey("CtrlOrCmd+Shift+G")}`}
aria-label={t("labels.ungroup")}

View File

@ -1,7 +1,6 @@
import React from "react";
import { AppState } from "../../src/types";
import { ButtonIconSelect } from "../components/ButtonIconSelect";
import { ButtonSelect } from "../components/ButtonSelect";
import { ColorPicker } from "../components/ColorPicker";
import { IconPicker } from "../components/IconPicker";
import {
@ -21,6 +20,16 @@ import {
StrokeStyleDottedIcon,
StrokeStyleSolidIcon,
StrokeWidthIcon,
FontSizeSmallIcon,
FontSizeMediumIcon,
FontSizeLargeIcon,
FontSizeExtraLargeIcon,
FontFamilyHandDrawnIcon,
FontFamilyNormalIcon,
FontFamilyCodeIcon,
TextAlignLeftIcon,
TextAlignCenterIcon,
TextAlignRightIcon,
} from "../components/icons";
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE } from "../constants";
import {
@ -169,17 +178,17 @@ export const actionChangeFillStyle = register({
{
value: "hachure",
text: t("labels.hachure"),
icon: <FillHachureIcon appearance={appState.appearance} />,
icon: <FillHachureIcon theme={appState.theme} />,
},
{
value: "cross-hatch",
text: t("labels.crossHatch"),
icon: <FillCrossHatchIcon appearance={appState.appearance} />,
icon: <FillCrossHatchIcon theme={appState.theme} />,
},
{
value: "solid",
text: t("labels.solid"),
icon: <FillSolidIcon appearance={appState.appearance} />,
icon: <FillSolidIcon theme={appState.theme} />,
},
]}
group="fill"
@ -219,32 +228,17 @@ export const actionChangeStrokeWidth = register({
{
value: 1,
text: t("labels.thin"),
icon: (
<StrokeWidthIcon
appearance={appState.appearance}
strokeWidth={2}
/>
),
icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={2} />,
},
{
value: 2,
text: t("labels.bold"),
icon: (
<StrokeWidthIcon
appearance={appState.appearance}
strokeWidth={6}
/>
),
icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={6} />,
},
{
value: 4,
text: t("labels.extraBold"),
icon: (
<StrokeWidthIcon
appearance={appState.appearance}
strokeWidth={10}
/>
),
icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={10} />,
},
]}
value={getFormValue(
@ -282,17 +276,17 @@ export const actionChangeSloppiness = register({
{
value: 0,
text: t("labels.architect"),
icon: <SloppinessArchitectIcon appearance={appState.appearance} />,
icon: <SloppinessArchitectIcon theme={appState.theme} />,
},
{
value: 1,
text: t("labels.artist"),
icon: <SloppinessArtistIcon appearance={appState.appearance} />,
icon: <SloppinessArtistIcon theme={appState.theme} />,
},
{
value: 2,
text: t("labels.cartoonist"),
icon: <SloppinessCartoonistIcon appearance={appState.appearance} />,
icon: <SloppinessCartoonistIcon theme={appState.theme} />,
},
]}
value={getFormValue(
@ -329,17 +323,17 @@ export const actionChangeStrokeStyle = register({
{
value: "solid",
text: t("labels.strokeStyle_solid"),
icon: <StrokeStyleSolidIcon appearance={appState.appearance} />,
icon: <StrokeStyleSolidIcon theme={appState.theme} />,
},
{
value: "dashed",
text: t("labels.strokeStyle_dashed"),
icon: <StrokeStyleDashedIcon appearance={appState.appearance} />,
icon: <StrokeStyleDashedIcon theme={appState.theme} />,
},
{
value: "dotted",
text: t("labels.strokeStyle_dotted"),
icon: <StrokeStyleDottedIcon appearance={appState.appearance} />,
icon: <StrokeStyleDottedIcon theme={appState.theme} />,
},
]}
value={getFormValue(
@ -428,13 +422,29 @@ export const actionChangeFontSize = register({
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
<legend>{t("labels.fontSize")}</legend>
<ButtonSelect
<ButtonIconSelect
group="font-size"
options={[
{ value: 16, text: t("labels.small") },
{ value: 20, text: t("labels.medium") },
{ value: 28, text: t("labels.large") },
{ value: 36, text: t("labels.veryLarge") },
{
value: 16,
text: t("labels.small"),
icon: <FontSizeSmallIcon theme={appState.theme} />,
},
{
value: 20,
text: t("labels.medium"),
icon: <FontSizeMediumIcon theme={appState.theme} />,
},
{
value: 28,
text: t("labels.large"),
icon: <FontSizeLargeIcon theme={appState.theme} />,
},
{
value: 36,
text: t("labels.veryLarge"),
icon: <FontSizeExtraLargeIcon theme={appState.theme} />,
},
]}
value={getFormValue(
elements,
@ -471,16 +481,28 @@ export const actionChangeFontFamily = register({
};
},
PanelComponent: ({ elements, appState, updateData }) => {
const options: { value: FontFamily; text: string }[] = [
{ value: 1, text: t("labels.handDrawn") },
{ value: 2, text: t("labels.normal") },
{ value: 3, text: t("labels.code") },
const options: { value: FontFamily; text: string; icon: JSX.Element }[] = [
{
value: 1,
text: t("labels.handDrawn"),
icon: <FontFamilyHandDrawnIcon theme={appState.theme} />,
},
{
value: 2,
text: t("labels.normal"),
icon: <FontFamilyNormalIcon theme={appState.theme} />,
},
{
value: 3,
text: t("labels.code"),
icon: <FontFamilyCodeIcon theme={appState.theme} />,
},
];
return (
<fieldset>
<legend>{t("labels.fontFamily")}</legend>
<ButtonSelect<FontFamily | false>
<ButtonIconSelect<FontFamily | false>
group="font-family"
options={options}
value={getFormValue(
@ -521,12 +543,24 @@ export const actionChangeTextAlign = register({
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
<legend>{t("labels.textAlign")}</legend>
<ButtonSelect<TextAlign | false>
<ButtonIconSelect<TextAlign | false>
group="text-align"
options={[
{ value: "left", text: t("labels.left") },
{ value: "center", text: t("labels.center") },
{ value: "right", text: t("labels.right") },
{
value: "left",
text: t("labels.left"),
icon: <TextAlignLeftIcon theme={appState.theme} />,
},
{
value: "center",
text: t("labels.center"),
icon: <TextAlignCenterIcon theme={appState.theme} />,
},
{
value: "right",
text: t("labels.right"),
icon: <TextAlignRightIcon theme={appState.theme} />,
},
]}
value={getFormValue(
elements,
@ -580,12 +614,12 @@ export const actionChangeSharpness = register({
{
value: "sharp",
text: t("labels.sharp"),
icon: <EdgeSharpIcon appearance={appState.appearance} />,
icon: <EdgeSharpIcon theme={appState.theme} />,
},
{
value: "round",
text: t("labels.round"),
icon: <EdgeRoundIcon appearance={appState.appearance} />,
icon: <EdgeRoundIcon theme={appState.theme} />,
},
]}
value={getFormValue(
@ -653,40 +687,27 @@ export const actionChangeArrowhead = register({
{
value: null,
text: t("labels.arrowhead_none"),
icon: <ArrowheadNoneIcon appearance={appState.appearance} />,
icon: <ArrowheadNoneIcon theme={appState.theme} />,
keyBinding: "q",
},
{
value: "arrow",
text: t("labels.arrowhead_arrow"),
icon: (
<ArrowheadArrowIcon
appearance={appState.appearance}
flip={!isRTL}
/>
<ArrowheadArrowIcon theme={appState.theme} flip={!isRTL} />
),
keyBinding: "w",
},
{
value: "bar",
text: t("labels.arrowhead_bar"),
icon: (
<ArrowheadBarIcon
appearance={appState.appearance}
flip={!isRTL}
/>
),
icon: <ArrowheadBarIcon theme={appState.theme} flip={!isRTL} />,
keyBinding: "e",
},
{
value: "dot",
text: t("labels.arrowhead_dot"),
icon: (
<ArrowheadDotIcon
appearance={appState.appearance}
flip={!isRTL}
/>
),
icon: <ArrowheadDotIcon theme={appState.theme} flip={!isRTL} />,
keyBinding: "r",
},
]}
@ -709,40 +730,27 @@ export const actionChangeArrowhead = register({
value: null,
text: t("labels.arrowhead_none"),
keyBinding: "q",
icon: <ArrowheadNoneIcon appearance={appState.appearance} />,
icon: <ArrowheadNoneIcon theme={appState.theme} />,
},
{
value: "arrow",
text: t("labels.arrowhead_arrow"),
keyBinding: "w",
icon: (
<ArrowheadArrowIcon
appearance={appState.appearance}
flip={isRTL}
/>
<ArrowheadArrowIcon theme={appState.theme} flip={isRTL} />
),
},
{
value: "bar",
text: t("labels.arrowhead_bar"),
keyBinding: "e",
icon: (
<ArrowheadBarIcon
appearance={appState.appearance}
flip={isRTL}
/>
),
icon: <ArrowheadBarIcon theme={appState.theme} flip={isRTL} />,
},
{
value: "dot",
text: t("labels.arrowhead_dot"),
keyBinding: "r",
icon: (
<ArrowheadDotIcon
appearance={appState.appearance}
flip={isRTL}
/>
),
icon: <ArrowheadDotIcon theme={appState.theme} flip={isRTL} />,
},
]}
value={getFormValue<Arrowhead | null>(

View File

@ -38,7 +38,7 @@ export const actionSendBackward = register({
onClick={() => updateData(null)}
title={`${t("labels.sendBackward")}${getShortcutKey("CtrlOrCmd+[")}`}
>
<SendBackwardIcon appearance={appState.appearance} />
<SendBackwardIcon theme={appState.theme} />
</button>
),
});
@ -65,7 +65,7 @@ export const actionBringForward = register({
onClick={() => updateData(null)}
title={`${t("labels.bringForward")}${getShortcutKey("CtrlOrCmd+]")}`}
>
<BringForwardIcon appearance={appState.appearance} />
<BringForwardIcon theme={appState.theme} />
</button>
),
});
@ -99,7 +99,7 @@ export const actionSendToBack = register({
: getShortcutKey("CtrlOrCmd+Shift+[")
}`}
>
<SendToBackIcon appearance={appState.appearance} />
<SendToBackIcon theme={appState.theme} />
</button>
),
});
@ -133,7 +133,7 @@ export const actionBringToFront = register({
: getShortcutKey("CtrlOrCmd+Shift+]")
}`}
>
<BringToFrontIcon appearance={appState.appearance} />
<BringToFrontIcon theme={appState.theme} />
</button>
),
});

View File

@ -122,6 +122,7 @@ export class ActionManager implements ActionsManagerInterface {
appState={this.getAppState()}
updateData={updateData}
id={id}
appProps={this.app.props}
/>
);
}

View File

@ -1,6 +1,6 @@
import React from "react";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { AppState, ExcalidrawProps } from "../types";
/** if false, the action should be prevented */
export type ActionResult =
@ -85,7 +85,8 @@ export type ActionName =
| "alignHorizontallyCentered"
| "distributeHorizontally"
| "distributeVertically"
| "viewMode";
| "viewMode"
| "exportWithDarkMode";
export interface Action {
name: ActionName;
@ -93,6 +94,7 @@ export interface Action {
elements: readonly ExcalidrawElement[];
appState: AppState;
updateData: (formData?: any) => void;
appProps: ExcalidrawProps;
id?: string;
}>;
perform: ActionFn;

View File

@ -13,7 +13,7 @@ export const getDefaultAppState = (): Omit<
"offsetTop" | "offsetLeft"
> => {
return {
appearance: "light",
theme: "light",
collaborators: new Map(),
currentChartType: "bar",
currentItemBackgroundColor: "transparent",
@ -40,6 +40,7 @@ export const getDefaultAppState = (): Omit<
errorMessage: null,
exportBackground: true,
exportEmbedScene: false,
exportWithDarkMode: false,
fileHandle: null,
gridSize: null,
height: window.innerHeight,
@ -91,7 +92,7 @@ const APP_STATE_STORAGE_CONF = (<
>(
config: { [K in keyof T]: K extends keyof AppState ? T[K] : never },
) => config)({
appearance: { browser: true, export: false },
theme: { browser: true, export: false },
collaborators: { browser: false, export: false },
currentChartType: { browser: true, export: false },
currentItemBackgroundColor: { browser: true, export: false },
@ -118,6 +119,7 @@ const APP_STATE_STORAGE_CONF = (<
errorMessage: { browser: false, export: false },
exportBackground: { browser: true, export: false },
exportEmbedScene: { browser: true, export: false },
exportWithDarkMode: { browser: true, export: false },
fileHandle: { browser: false, export: false },
gridSize: { browser: true, export: true },
height: { browser: false, export: false },

View File

@ -7,12 +7,10 @@ import { AppState } from "./types";
import { SVG_EXPORT_TAG } from "./scene/export";
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
import { canvasToBlob } from "./data/blob";
const TYPE_ELEMENTS = "excalidraw/elements";
import { EXPORT_DATA_TYPES } from "./constants";
type ElementsClipboard = {
type: typeof TYPE_ELEMENTS;
created: number;
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
elements: ExcalidrawElement[];
};
@ -31,8 +29,16 @@ export const probablySupportsClipboardBlob =
"ClipboardItem" in window &&
"toBlob" in HTMLCanvasElement.prototype;
const isElementsClipboard = (contents: any): contents is ElementsClipboard => {
if (contents?.type === TYPE_ELEMENTS) {
const clipboardContainsElements = (
contents: any,
): contents is { elements: ExcalidrawElement[] } => {
if (
[
EXPORT_DATA_TYPES.excalidraw,
EXPORT_DATA_TYPES.excalidrawClipboard,
].includes(contents?.type) &&
Array.isArray(contents.elements)
) {
return true;
}
return false;
@ -43,8 +49,7 @@ export const copyToClipboard = async (
appState: AppState,
) => {
const contents: ElementsClipboard = {
type: TYPE_ELEMENTS,
created: Date.now(),
type: EXPORT_DATA_TYPES.excalidrawClipboard,
elements: getSelectedElements(elements, appState),
};
const json = JSON.stringify(contents);
@ -131,15 +136,9 @@ export const parseClipboard = async (
try {
const systemClipboardData = JSON.parse(systemClipboard);
// system clipboard elements are newer than in-app clipboard
if (
isElementsClipboard(systemClipboardData) &&
(!appClipboardData?.created ||
appClipboardData.created < systemClipboardData.created)
) {
if (clipboardContainsElements(systemClipboardData)) {
return { elements: systemClipboardData.elements };
}
// in-app clipboard is newer than system clipboard
return appClipboardData;
} catch {
// system clipboard doesn't contain excalidraw elements → return plaintext

View File

@ -151,10 +151,12 @@ const LIBRARY_ICON = (
);
export const ShapesSwitcher = ({
canvas,
elementType,
setAppState,
isLibraryOpen,
}: {
canvas: HTMLCanvasElement | null;
elementType: ExcalidrawElement["type"];
setAppState: React.Component<any, AppState>["setState"];
isLibraryOpen: boolean;
@ -185,7 +187,7 @@ export const ShapesSwitcher = ({
multiElement: null,
selectedElementIds: {},
});
setCursorForShape(value);
setCursorForShape(canvas, value);
setAppState({});
}}
/>

View File

@ -3,6 +3,7 @@ import React from "react";
import { RoughCanvas } from "roughjs/bin/canvas";
import rough from "roughjs/bin/rough";
import clsx from "clsx";
import { supported } from "browser-fs-access";
import {
actionAddToLibrary,
@ -172,6 +173,7 @@ import {
ResolvablePromise,
resolvablePromise,
sceneCoordsToViewportCoords,
setCursor,
setCursorForShape,
tupleToCoors,
viewportCoordsToSceneCoords,
@ -270,9 +272,10 @@ export type ExcalidrawImperativeAPI = {
history: {
clear: InstanceType<typeof App>["resetHistory"];
};
setScrollToCenter: InstanceType<typeof App>["setScrollToCenter"];
setScrollToContent: InstanceType<typeof App>["setScrollToContent"];
getSceneElements: InstanceType<typeof App>["getSceneElements"];
getAppState: () => InstanceType<typeof App>["state"];
setCanvasOffsets: InstanceType<typeof App>["setCanvasOffsets"];
readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>;
ready: true;
};
@ -296,22 +299,24 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const {
width = window.innerWidth,
height = window.innerHeight,
offsetLeft,
offsetTop,
excalidrawRef,
viewModeEnabled = false,
zenModeEnabled = false,
gridModeEnabled = false,
theme = defaultAppState.theme,
name = defaultAppState.name,
} = props;
this.state = {
...defaultAppState,
theme,
isLoading: true,
width,
height,
...this.getCanvasOffsets({ offsetLeft, offsetTop }),
...this.getCanvasOffsets(),
viewModeEnabled,
zenModeEnabled,
gridSize: gridModeEnabled ? GRID_SIZE : null,
name,
};
if (excalidrawRef) {
const readyPromise =
@ -327,9 +332,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
history: {
clear: this.resetHistory,
},
setScrollToCenter: this.setScrollToCenter,
setScrollToContent: this.setScrollToContent,
getSceneElements: this.getSceneElements,
getAppState: () => this.state,
setCanvasOffsets: this.setCanvasOffsets,
} as const;
if (typeof excalidrawRef === "function") {
excalidrawRef(api);
@ -413,8 +419,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
zenModeEnabled,
width: canvasDOMWidth,
height: canvasDOMHeight,
offsetTop,
offsetLeft,
viewModeEnabled,
} = this.state;
@ -432,8 +436,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
style={{
width: canvasDOMWidth,
height: canvasDOMHeight,
top: offsetTop,
left: offsetLeft,
}}
>
<LayerUI
@ -461,6 +463,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
showExitZenModeBtn={
typeof this.props?.zenModeEnabled === "undefined" && zenModeEnabled
}
showThemeBtn={typeof this.props?.theme === "undefined"}
libraryReturnUrl={this.props.libraryReturnUrl}
/>
<div className="excalidraw-textEditorContainer" />
{this.state.showStats && (
@ -521,7 +525,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
let gridSize = actionResult?.appState?.gridSize || null;
let theme = actionResult?.appState?.theme || "light";
let name = actionResult?.appState?.name ?? this.state.name;
if (typeof this.props.viewModeEnabled !== "undefined") {
viewModeEnabled = this.props.viewModeEnabled;
}
@ -534,19 +539,33 @@ class App extends React.Component<ExcalidrawProps, AppState> {
gridSize = this.props.gridModeEnabled ? GRID_SIZE : null;
}
if (typeof this.props.theme !== "undefined") {
theme = this.props.theme;
}
if (typeof this.props.name !== "undefined") {
name = this.props.name;
}
this.setState(
(state) => ({
...actionResult.appState,
editingElement:
editingElement || actionResult.appState?.editingElement || null,
width: state.width,
height: state.height,
offsetTop: state.offsetTop,
offsetLeft: state.offsetLeft,
viewModeEnabled,
zenModeEnabled,
gridSize,
}),
(state) => {
// using Object.assign instead of spread to fool TS 4.2.2+ into
// regarding the resulting type as not containing undefined
// (which the following expression will never contain)
return Object.assign(actionResult.appState || {}, {
editingElement:
editingElement || actionResult.appState?.editingElement || null,
width: state.width,
height: state.height,
offsetTop: state.offsetTop,
offsetLeft: state.offsetLeft,
viewModeEnabled,
zenModeEnabled,
gridSize,
theme,
name,
});
},
() => {
if (actionResult.syncHistory) {
history.setCurrentState(
@ -587,7 +606,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
private importLibraryFromUrl = async (url: string) => {
window.history.replaceState({}, APP_NAME, window.location.origin);
try {
const request = await fetch(url);
const request = await fetch(decodeURIComponent(url));
const blob = await request.blob();
const json = JSON.parse(await blob.text());
if (!isValidLibrary(json)) {
@ -623,7 +642,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.setState((state) => ({
...getDefaultAppState(),
isLoading: opts?.resetLoadingState ? false : state.isLoading,
appearance: this.state.appearance,
theme: this.state.theme,
}));
this.resetHistory();
},
@ -672,19 +691,24 @@ class App extends React.Component<ExcalidrawProps, AppState> {
scene.appState = {
...scene.appState,
...calculateScrollCenter(
scene.elements,
{
...scene.appState,
width: this.state.width,
height: this.state.height,
offsetTop: this.state.offsetTop,
offsetLeft: this.state.offsetLeft,
},
null,
),
isLoading: false,
};
if (initialData?.scrollToContent) {
scene.appState = {
...scene.appState,
...calculateScrollCenter(
scene.elements,
{
...scene.appState,
width: this.state.width,
height: this.state.height,
offsetTop: this.state.offsetTop,
offsetLeft: this.state.offsetLeft,
},
null,
),
};
}
this.resetHistory();
this.syncActionResult({
@ -730,14 +754,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.scene.addCallback(this.onSceneUpdated);
this.addEventListeners();
// optim to avoid extra render on init
if (
typeof this.props.offsetLeft === "number" &&
typeof this.props.offsetTop === "number"
) {
this.initializeScene();
const searchParams = new URLSearchParams(window.location.search.slice(1));
if (searchParams.has("web-share-target")) {
// Obtain a file that was shared via the Web Share Target API.
this.restoreFileFromShare();
} else {
this.setState(this.getCanvasOffsets(this.props), () => {
this.setState(this.getCanvasOffsets(), () => {
this.initializeScene();
});
}
@ -842,16 +865,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
if (
prevProps.width !== this.props.width ||
prevProps.height !== this.props.height ||
(typeof this.props.offsetLeft === "number" &&
prevProps.offsetLeft !== this.props.offsetLeft) ||
(typeof this.props.offsetTop === "number" &&
prevProps.offsetTop !== this.props.offsetTop)
prevProps.height !== this.props.height
) {
this.setState({
width: this.props.width ?? window.innerWidth,
height: this.props.height ?? window.innerHeight,
...this.getCanvasOffsets(this.props),
...this.getCanvasOffsets(),
});
}
@ -870,14 +889,25 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.setState({ zenModeEnabled: !!this.props.zenModeEnabled });
}
if (prevProps.theme !== this.props.theme && this.props.theme) {
this.setState({ theme: this.props.theme });
}
if (prevProps.gridModeEnabled !== this.props.gridModeEnabled) {
this.setState({
gridSize: this.props.gridModeEnabled ? GRID_SIZE : null,
});
}
if (this.props.name && prevProps.name !== this.props.name) {
this.setState({
name: this.props.name,
});
}
document
.querySelector(".excalidraw")
?.classList.toggle("Appearance_dark", this.state.appearance === "dark");
?.classList.toggle("theme--dark", this.state.theme === "dark");
if (
this.state.editingLinearElement &&
@ -1002,7 +1032,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}
private onScroll = debounce(() => {
this.setState({ ...this.getCanvasOffsets() });
const { offsetTop, offsetLeft } = this.getCanvasOffsets();
this.setState((state) => {
if (state.offsetLeft === offsetLeft && state.offsetTop === offsetTop) {
return null;
}
return { offsetTop, offsetLeft };
});
}, SCROLL_TIMEOUT);
// Copy/paste
@ -1016,6 +1052,21 @@ class App extends React.Component<ExcalidrawProps, AppState> {
});
private onCopy = withBatchedUpdates((event: ClipboardEvent) => {
const activeSelection = document.getSelection();
// if there's a selected text is outside the component, prevent our copy
// action
if (
activeSelection?.anchorNode &&
// it can happen that certain interactions will create a selection
// outside (or potentially inside) the component without actually
// selecting anything (i.e. the selection range is collapsed). Copying
// in such case wouldn't copy anything to the clipboard anyway, so prevent
// our copy handler only if the selection isn't collapsed
!activeSelection.isCollapsed &&
!this.excalidrawContainerRef.current!.contains(activeSelection.anchorNode)
) {
return;
}
if (isWritableElement(event.target)) {
return;
}
@ -1067,7 +1118,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
};
private onTapEnd = (event: TouchEvent) => {
event.preventDefault();
if (event.touches.length > 0) {
this.setState({
previousSelectedElementIds: {},
@ -1244,7 +1294,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.actionManager.executeAction(actionToggleStats);
};
setScrollToCenter = (remoteElements: readonly ExcalidrawElement[]) => {
setScrollToContent = (remoteElements: readonly ExcalidrawElement[]) => {
this.setState({
...calculateScrollCenter(
getNonDeletedElements(remoteElements),
@ -1258,6 +1308,22 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.setState({ toastMessage: null });
};
restoreFileFromShare = async () => {
try {
const webShareTargetCache = await caches.open("web-share-target");
const file = await webShareTargetCache.match("shared-file");
if (file) {
const blob = await file.blob();
this.loadFileToCanvas(blob);
await webShareTargetCache.delete("shared-file");
window.history.replaceState(null, APP_NAME, window.location.pathname);
}
} catch (error) {
this.setState({ errorMessage: error.message });
}
};
public updateScene = withBatchedUpdates((sceneData: SceneData) => {
if (sceneData.commitToHistory) {
history.resumeRecording();
@ -1339,7 +1405,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
return;
}
if (event[KEYS.CTRL_OR_CMD]) {
if (event[KEYS.CTRL_OR_CMD] && this.state.isBindingEnabled) {
this.setState({ isBindingEnabled: false });
}
@ -1435,16 +1501,16 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}
if (event.key === KEYS.SPACE && gesture.pointers.size === 0) {
isHoldingSpace = true;
document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
setCursor(this.canvas, CURSOR_TYPE.GRABBING);
}
});
private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
if (event.key === KEYS.SPACE) {
if (this.state.elementType === "selection") {
resetCursor();
resetCursor(this.canvas);
} else {
setCursorForShape(this.state.elementType);
setCursorForShape(this.canvas, this.state.elementType);
this.setState({
selectedElementIds: {},
selectedGroupIds: {},
@ -1470,7 +1536,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
private selectShapeTool(elementType: AppState["elementType"]) {
if (!isHoldingSpace) {
setCursorForShape(elementType);
setCursorForShape(this.canvas, elementType);
}
if (isToolIcon(document.activeElement)) {
document.activeElement.blur();
@ -1566,7 +1632,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
},
this.state,
);
return [viewportX, viewportY];
return [
viewportX - this.state.offsetLeft,
viewportY - this.state.offsetTop,
];
},
onChange: withBatchedUpdates((text) => {
updateElement(text);
@ -1574,10 +1643,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
updateBoundElements(element);
}
}),
onSubmit: withBatchedUpdates((text) => {
onSubmit: withBatchedUpdates(({ text, viaKeyboard }) => {
const isDeleted = !text.trim();
updateElement(text, isDeleted);
if (!isDeleted) {
// select the created text element only if submitting via keyboard
// (when submitting via click it should act as signal to deselect)
if (!isDeleted && viaKeyboard) {
this.setState((prevState) => ({
selectedElementIds: {
...prevState.selectedElementIds,
@ -1596,7 +1667,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
editingElement: null,
});
if (this.state.elementLocked) {
setCursorForShape(this.state.elementType);
setCursorForShape(this.canvas, this.state.elementType);
}
}),
element,
@ -1777,7 +1848,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
return;
}
resetCursor();
resetCursor(this.canvas);
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
event,
@ -1809,7 +1880,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}
}
resetCursor();
resetCursor(this.canvas);
if (!event[KEYS.CTRL_OR_CMD]) {
this.startTextEditing({
@ -1875,9 +1946,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const isOverScrollBar = isPointerOverScrollBars.isOverEither;
if (!this.state.draggingElement && !this.state.multiElement) {
if (isOverScrollBar) {
resetCursor();
resetCursor(this.canvas);
} else {
setCursorForShape(this.state.elementType);
setCursorForShape(this.canvas, this.state.elementType);
}
}
@ -1928,7 +1999,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const { points, lastCommittedPoint } = multiElement;
const lastPoint = points[points.length - 1];
setCursorForShape(this.state.elementType);
setCursorForShape(this.canvas, this.state.elementType);
if (lastPoint === lastCommittedPoint) {
// if we haven't yet created a temp point and we're beyond commit-zone
@ -1945,7 +2016,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
points: [...points, [scenePointerX - rx, scenePointerY - ry]],
});
} else {
document.documentElement.style.cursor = CURSOR_TYPE.POINTER;
setCursor(this.canvas, CURSOR_TYPE.POINTER);
// in this branch, we're inside the commit zone, and no uncommitted
// point exists. Thus do nothing (don't add/remove points).
}
@ -1959,13 +2030,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
lastCommittedPoint[1],
) < LINE_CONFIRM_THRESHOLD
) {
document.documentElement.style.cursor = CURSOR_TYPE.POINTER;
setCursor(this.canvas, CURSOR_TYPE.POINTER);
mutateElement(multiElement, {
points: points.slice(0, -1),
});
} else {
if (isPathALoop(points, this.state.zoom.value)) {
document.documentElement.style.cursor = CURSOR_TYPE.POINTER;
setCursor(this.canvas, CURSOR_TYPE.POINTER);
}
// update last uncommitted point
mutateElement(multiElement, {
@ -2008,8 +2079,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
elementWithTransformHandleType &&
elementWithTransformHandleType.transformHandleType
) {
document.documentElement.style.cursor = getCursorForResizingElement(
elementWithTransformHandleType,
setCursor(
this.canvas,
getCursorForResizingElement(elementWithTransformHandleType),
);
return;
}
@ -2022,9 +2094,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
event.pointerType,
);
if (transformHandleType) {
document.documentElement.style.cursor = getCursorForResizingElement({
transformHandleType,
});
setCursor(
this.canvas,
getCursorForResizingElement({
transformHandleType,
}),
);
return;
}
}
@ -2034,11 +2109,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
scenePointer.y,
);
if (this.state.elementType === "text") {
document.documentElement.style.cursor = isTextElement(hitElement)
? CURSOR_TYPE.TEXT
: CURSOR_TYPE.CROSSHAIR;
setCursor(
this.canvas,
isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR,
);
} else if (isOverScrollBar) {
document.documentElement.style.cursor = CURSOR_TYPE.AUTO;
setCursor(this.canvas, CURSOR_TYPE.AUTO);
} else if (
hitElement ||
this.isHittingCommonBoundingBoxOfSelectedElements(
@ -2046,9 +2122,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
selectedElements,
)
) {
document.documentElement.style.cursor = CURSOR_TYPE.MOVE;
setCursor(this.canvas, CURSOR_TYPE.MOVE);
} else {
document.documentElement.style.cursor = CURSOR_TYPE.AUTO;
setCursor(this.canvas, CURSOR_TYPE.AUTO);
}
};
@ -2062,6 +2138,14 @@ class App extends React.Component<ExcalidrawProps, AppState> {
) => {
event.persist();
// remove any active selection when we start to interact with canvas
// (mainly, we care about removing selection outside the component which
// would prevent our copy handling otherwise)
const selection = document.getSelection();
if (selection?.anchorNode) {
selection.removeAllRanges();
}
this.maybeOpenContextMenuAfterPointerDownOnTouchDevices(event);
this.maybeCleanupAfterMissingPointerUp(event);
@ -2089,15 +2173,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.updateGestureOnPointerDown(event);
// fixes pointermove causing selection of UI texts #32
event.preventDefault();
// Preventing the event above disables default behavior
// of defocusing potentially focused element, which is what we
// want when clicking inside the canvas.
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
// don't select while panning
if (gesture.pointers.size > 1) {
return;
@ -2221,7 +2296,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
let nextPastePrevented = false;
const isLinux = /Linux/.test(window.navigator.platform);
document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
setCursor(this.canvas, CURSOR_TYPE.GRABBING);
let { clientX: lastX, clientY: lastY } = event;
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
const deltaX = lastX - event.clientX;
@ -2273,7 +2348,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
lastPointerUp = null;
isPanning = false;
if (!isHoldingSpace) {
setCursorForShape(this.state.elementType);
setCursorForShape(this.canvas, this.state.elementType);
}
this.setState({
cursorButton: "up",
@ -2389,7 +2464,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const onPointerUp = withBatchedUpdates(() => {
isDraggingScrollBar = false;
setCursorForShape(this.state.elementType);
setCursorForShape(this.canvas, this.state.elementType);
lastPointerUp = null;
this.setState({
cursorButton: "up",
@ -2452,9 +2527,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
);
}
if (pointerDownState.resize.handleType) {
document.documentElement.style.cursor = getCursorForResizingElement({
transformHandleType: pointerDownState.resize.handleType,
});
setCursor(
this.canvas,
getCursorForResizingElement({
transformHandleType: pointerDownState.resize.handleType,
}),
);
pointerDownState.resize.isResizing = true;
pointerDownState.resize.offset = tupleToCoors(
getResizeOffsetXY(
@ -2619,7 +2697,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
insertAtParentCenter: !event.altKey,
});
resetCursor();
resetCursor(this.canvas);
if (!this.state.elementLocked) {
this.setState({
elementType: "selection",
@ -2676,7 +2754,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
mutateElement(multiElement, {
lastCommittedPoint: multiElement.points[multiElement.points.length - 1],
});
document.documentElement.style.cursor = CURSOR_TYPE.POINTER;
setCursor(this.canvas, CURSOR_TYPE.POINTER);
} else {
const [gridX, gridY] = getGridPoint(
pointerDownState.origin.x,
@ -3211,7 +3289,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}
this.setState({ suggestedBindings: [], startBoundElement: null });
if (!elementLocked && elementType !== "draw") {
resetCursor();
resetCursor(this.canvas);
this.setState((prevState) => ({
draggingElement: null,
elementType: "selection",
@ -3382,7 +3460,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}
if (!elementLocked && elementType !== "draw") {
resetCursor();
resetCursor(this.canvas);
this.setState({
draggingElement: null,
suggestedBindings: [],
@ -3532,34 +3610,17 @@ class App extends React.Component<ExcalidrawProps, AppState> {
file?.name.endsWith(".excalidraw")
) {
this.setState({ isLoading: true });
if (
"chooseFileSystemEntries" in window ||
"showOpenFilePicker" in window
) {
if (supported) {
try {
// This will only work as of Chrome 86,
// but can be safely ignored on older releases.
const item = event.dataTransfer.items[0];
// TODO: Make this part of `AppState`.
(file as any).handle = await (item as any).getAsFileSystemHandle();
} catch (error) {
console.warn(error.name, error.message);
}
}
loadFromBlob(file, this.state)
.then(({ elements, appState }) =>
this.syncActionResult({
elements,
appState: {
...(appState || this.state),
isLoading: false,
},
commitToHistory: true,
}),
)
.catch((error) => {
this.setState({ isLoading: false, errorMessage: error.message });
});
this.loadFileToCanvas(file);
} else if (
file?.type === MIME_TYPES.excalidrawlib ||
file?.name.endsWith(".excalidrawlib")
@ -3579,6 +3640,23 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}
};
loadFileToCanvas = (file: Blob) => {
loadFromBlob(file, this.state)
.then(({ elements, appState }) =>
this.syncActionResult({
elements,
appState: {
...(appState || this.state),
isLoading: false,
},
commitToHistory: true,
}),
)
.catch((error) => {
this.setState({ isLoading: false, errorMessage: error.message });
});
};
private handleCanvasContextMenu = (
event: React.PointerEvent<HTMLCanvasElement>,
) => {
@ -3959,33 +4037,22 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}
}, 300);
private getCanvasOffsets(offsets?: {
offsetLeft?: number;
offsetTop?: number;
}): Pick<AppState, "offsetTop" | "offsetLeft"> {
if (
typeof offsets?.offsetLeft === "number" &&
typeof offsets?.offsetTop === "number"
) {
return {
offsetLeft: offsets.offsetLeft,
offsetTop: offsets.offsetTop,
};
}
public setCanvasOffsets = () => {
this.setState({ ...this.getCanvasOffsets() });
};
private getCanvasOffsets(): Pick<AppState, "offsetTop" | "offsetLeft"> {
if (this.excalidrawContainerRef?.current?.parentElement) {
const parentElement = this.excalidrawContainerRef.current.parentElement;
const { left, top } = parentElement.getBoundingClientRect();
return {
offsetLeft:
typeof offsets?.offsetLeft === "number" ? offsets.offsetLeft : left,
offsetTop:
typeof offsets?.offsetTop === "number" ? offsets.offsetTop : top,
offsetLeft: left,
offsetTop: top,
};
}
return {
offsetLeft:
typeof offsets?.offsetLeft === "number" ? offsets.offsetLeft : 0,
offsetTop: typeof offsets?.offsetTop === "number" ? offsets.offsetTop : 0,
offsetLeft: 0,
offsetTop: 0,
};
}

View File

@ -7,20 +7,24 @@ export const BackgroundPickerAndDarkModeToggle = ({
appState,
setAppState,
actionManager,
showThemeBtn,
}: {
actionManager: ActionManager;
appState: AppState;
setAppState: React.Component<any, AppState>["setState"];
showThemeBtn: boolean;
}) => (
<div style={{ display: "flex" }}>
{actionManager.renderAction("changeViewBackgroundColor")}
<div style={{ marginInlineStart: "0.25rem" }}>
<DarkModeToggle
value={appState.appearance}
onChange={(appearance) => {
setAppState({ appearance });
}}
/>
</div>
{showThemeBtn && (
<div style={{ marginInlineStart: "0.25rem" }}>
<DarkModeToggle
value={appState.theme}
onChange={(theme) => {
setAppState({ theme });
}}
/>
</div>
)}
</div>
);

View File

@ -73,7 +73,7 @@
box-sizing: border-box;
border: 1px solid #ddd;
background-color: currentColor !important;
filter: var(--appearance-filter);
filter: var(--theme-filter);
&:focus {
/* TODO: only show the border when the color is too light to see as a shadow */
@ -192,7 +192,7 @@
position: relative;
overflow: hidden;
background-color: transparent !important;
filter: var(--appearance-filter);
filter: var(--theme-filter);
&:after {
content: "";
@ -239,7 +239,7 @@
color: #d4d4d4;
}
&.Appearance_dark {
&.theme--dark {
.color-picker-type-elementBackground .color-picker-keybinding {
color: $oc-black;
}

View File

@ -34,11 +34,11 @@ const ContextMenu = ({
}: ContextMenuProps) => {
const isDarkTheme = !!document
.querySelector(".excalidraw")
?.classList.contains("Appearance_dark");
?.classList.contains("theme--dark");
return (
<div
className={clsx("excalidraw", {
"Appearance_dark Appearance_dark-background-none": isDarkTheme,
"theme--dark theme--dark-background-none": isDarkTheme,
})}
>
<Popover

View File

@ -10,13 +10,19 @@ export type Appearence = "light" | "dark";
export const DarkModeToggle = (props: {
value: Appearence;
onChange: (value: Appearence) => void;
title?: string;
}) => {
const title = props.title
? props.title
: props.value === "dark"
? t("buttons.lightMode")
: t("buttons.darkMode");
return (
<label
className={`ToolIcon ToolIcon_type_floating ToolIcon_size_M`}
title={
props.value === "dark" ? t("buttons.lightMode") : t("buttons.darkMode")
}
className="ToolIcon ToolIcon_type_floating ToolIcon_size_M"
data-testid="toggle-dark-mode"
title={title}
>
<input
className="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
@ -25,11 +31,7 @@ export const DarkModeToggle = (props: {
props.onChange(event.target.checked ? "dark" : "light")
}
checked={props.value === "dark"}
aria-label={
props.value === "dark"
? t("buttons.lightMode")
: t("buttons.darkMode")
}
aria-label={title}
/>
<div className="ToolIcon__icon">
{props.value === "light" ? ICONS.MOON : ICONS.SUN}

View File

@ -16,7 +16,7 @@
max-height: 25rem;
}
&.Appearance_dark .ExportDialog__preview canvas {
&.theme--dark .ExportDialog__preview canvas {
filter: none;
}
@ -31,9 +31,27 @@
.ExportDialog__name {
grid-column: project-name;
margin: auto;
display: flex;
align-items: center;
.TextInput {
height: calc(1rem - 3px);
width: 200px;
overflow: hidden;
text-align: center;
margin-left: 8px;
text-overflow: ellipsis;
&--readonly {
background: none;
border: none;
&:hover {
background: none;
}
width: auto;
max-width: 200px;
padding-left: 2px;
}
}
}

View File

@ -19,6 +19,9 @@ import { ToolButton } from "./ToolButton";
const scales = [1, 2, 3];
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
const supportsContextFilters =
"filter" in document.createElement("canvas").getContext("2d")!;
export const ErrorCanvasPreview = () => {
return (
<div>
@ -128,6 +131,8 @@ const ExportModal = ({
return (
<div className="ExportDialog">
<div className="ExportDialog__preview" ref={previewRef} />
{supportsContextFilters &&
actionManager.renderAction("exportWithDarkMode")}
<Stack.Col gap={2} align="center">
<div className="ExportDialog__actions">
<Stack.Row gap={2}>
@ -252,6 +257,7 @@ export const ExportDialog = ({
onClick={() => {
setModalIsShown(true);
}}
data-testid="export-button"
icon={exportFile}
type="button"
aria-label={t("buttons.export")}

View File

@ -3,7 +3,7 @@ import React from "react";
// https://github.com/tholman/github-corners
export const GitHubCorner = React.memo(
({ appearance }: { appearance: "light" | "dark" }) => (
({ theme }: { theme: "light" | "dark" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="40"
@ -19,18 +19,18 @@ export const GitHubCorner = React.memo(
>
<path
d="M0 0l115 115h15l12 27 108 108V0z"
fill={appearance === "light" ? oc.gray[6] : oc.gray[8]}
fill={theme === "light" ? oc.gray[6] : oc.gray[8]}
/>
<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={appearance === "light" ? oc.white : oc.black}
fill={theme === "light" ? oc.white : oc.black}
/>
<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={appearance === "light" ? oc.white : oc.black}
fill={theme === "light" ? oc.white : oc.black}
/>
</a>
</svg>

View File

@ -9,7 +9,13 @@ type HelpIconProps = {
};
export const HelpIcon = (props: HelpIconProps) => (
<label title={`${props.title} — ?`} className="help-icon">
<div onClick={props.onClick}>{questionCircle}</div>
</label>
<button
className="help-icon"
onClick={props.onClick}
type="button"
title={`${props.title} — ?`}
aria-label={props.title}
>
{questionCircle}
</button>
);

View File

@ -132,7 +132,7 @@
color: #d4d4d4;
}
&.Appearance_dark {
&.theme--dark {
.picker-type-elementBackground .picker-keybinding {
color: $oc-black;
}

View File

@ -17,7 +17,7 @@ import { Language, t } from "../i18n";
import useIsMobile from "../is-mobile";
import { calculateScrollCenter, getSelectedElements } from "../scene";
import { ExportType } from "../scene/types";
import { AppState, LibraryItem, LibraryItems } from "../types";
import { AppState, ExcalidrawProps, LibraryItem, LibraryItems } from "../types";
import { muteFSAbortError } from "../utils";
import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
@ -53,6 +53,7 @@ interface LayerUIProps {
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
zenModeEnabled: boolean;
showExitZenModeBtn: boolean;
showThemeBtn: boolean;
toggleZenMode: () => void;
langCode: Language["code"];
isCollaborating: boolean;
@ -63,6 +64,7 @@ interface LayerUIProps {
) => void;
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
viewModeEnabled: boolean;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
}
const useOnClickOutside = (
@ -101,6 +103,7 @@ const LibraryMenuItems = ({
pendingElements,
setAppState,
setLibraryItems,
libraryReturnUrl,
}: {
library: LibraryItems;
pendingElements: LibraryItem;
@ -109,6 +112,7 @@ const LibraryMenuItems = ({
onAddToLibrary: (elements: LibraryItem) => void;
setAppState: React.Component<any, AppState>["setState"];
setLibraryItems: (library: LibraryItems) => void;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
}) => {
const isMobile = useIsMobile();
const numCells = library.length + (pendingElements.length > 0 ? 1 : 0);
@ -117,6 +121,8 @@ const LibraryMenuItems = ({
const rows = [];
let addedPendingElements = false;
const referrer = libraryReturnUrl || window.location.origin;
rows.push(
<div className="layer-ui__library-header">
<ToolButton
@ -138,35 +144,43 @@ const LibraryMenuItems = ({
});
}}
/>
<ToolButton
key="export"
type="button"
title={t("buttons.export")}
aria-label={t("buttons.export")}
icon={exportFile}
onClick={() => {
saveLibraryAsJSON()
.catch(muteFSAbortError)
.catch((error) => {
setAppState({ errorMessage: error.message });
});
}}
/>
<ToolButton
key="reset"
type="button"
title={t("buttons.resetLibrary")}
aria-label={t("buttons.resetLibrary")}
icon={trash}
onClick={() => {
if (window.confirm(t("alerts.resetLibrary"))) {
Library.resetLibrary();
setLibraryItems([]);
}
}}
/>
<a href="https://libraries.excalidraw.com" target="_excalidraw_libraries">
{!!library.length && (
<>
<ToolButton
key="export"
type="button"
title={t("buttons.export")}
aria-label={t("buttons.export")}
icon={exportFile}
onClick={() => {
saveLibraryAsJSON()
.catch(muteFSAbortError)
.catch((error) => {
setAppState({ errorMessage: error.message });
});
}}
/>
<ToolButton
key="reset"
type="button"
title={t("buttons.resetLibrary")}
aria-label={t("buttons.resetLibrary")}
icon={trash}
onClick={() => {
if (window.confirm(t("alerts.resetLibrary"))) {
Library.resetLibrary();
setLibraryItems([]);
}
}}
/>
</>
)}
<a
href={`https://libraries.excalidraw.com?target=${
window.name || "_blank"
}&referrer=${referrer}`}
target="_excalidraw_libraries"
>
{t("labels.libraries")}
</a>
</div>,
@ -219,12 +233,14 @@ const LibraryMenu = ({
pendingElements,
onAddToLibrary,
setAppState,
libraryReturnUrl,
}: {
pendingElements: LibraryItem;
onClickOutside: (event: MouseEvent) => void;
onInsertShape: (elements: LibraryItem) => void;
onAddToLibrary: () => void;
setAppState: React.Component<any, AppState>["setState"];
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
}) => {
const ref = useRef<HTMLDivElement | null>(null);
useOnClickOutside(ref, (event) => {
@ -297,6 +313,7 @@ const LibraryMenu = ({
pendingElements={pendingElements}
setAppState={setAppState}
setLibraryItems={setLibraryItems}
libraryReturnUrl={libraryReturnUrl}
/>
)}
</Island>
@ -314,11 +331,13 @@ const LayerUI = ({
onInsertElements,
zenModeEnabled,
showExitZenModeBtn,
showThemeBtn,
toggleZenMode,
isCollaborating,
onExportToBackend,
renderCustomFooter,
viewModeEnabled,
libraryReturnUrl,
}: LayerUIProps) => {
const isMobile = useIsMobile();
@ -429,6 +448,7 @@ const LayerUI = ({
actionManager={actionManager}
appState={appState}
setAppState={setAppState}
showThemeBtn={showThemeBtn}
/>
</Stack.Col>
</Island>
@ -482,6 +502,7 @@ const LayerUI = ({
onInsertShape={onInsertElements}
onAddToLibrary={deselectItems}
setAppState={setAppState}
libraryReturnUrl={libraryReturnUrl}
/>
) : null;
@ -516,6 +537,7 @@ const LayerUI = ({
{heading}
<Stack.Row gap={1}>
<ShapesSwitcher
canvas={canvas}
elementType={appState.elementType}
setAppState={setAppState}
isLibraryOpen={appState.isLibraryOpen}
@ -589,7 +611,7 @@ const LayerUI = ({
},
)}
>
<GitHubCorner appearance={appState.appearance} />
<GitHubCorner theme={appState.theme} />
</aside>
);
};
@ -657,6 +679,7 @@ const LayerUI = ({
isCollaborating={isCollaborating}
renderCustomFooter={renderCustomFooter}
viewModeEnabled={viewModeEnabled}
showThemeBtn={showThemeBtn}
/>
</>
) : (
@ -703,6 +726,7 @@ const areEqual = (prev: LayerUIProps, next: LayerUIProps) => {
const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
return (
prev.renderCustomFooter === next.renderCustomFooter &&
prev.langCode === next.langCode &&
prev.elements === next.elements &&
keys.every((key) => prevAppState[key] === nextAppState[key])

View File

@ -16,7 +16,7 @@
}
.library-unit__dragger > svg {
filter: var(--appearance-filter);
filter: var(--theme-filter);
flex-grow: 1;
max-height: 100%;
max-width: 100%;

View File

@ -30,6 +30,7 @@ type MobileMenuProps = {
isCollaborating: boolean;
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
viewModeEnabled: boolean;
showThemeBtn: boolean;
};
export const MobileMenu = ({
@ -45,6 +46,7 @@ export const MobileMenu = ({
isCollaborating,
renderCustomFooter,
viewModeEnabled,
showThemeBtn,
}: MobileMenuProps) => {
const renderToolbar = () => {
return (
@ -57,6 +59,7 @@ export const MobileMenu = ({
{heading}
<Stack.Row gap={1}>
<ShapesSwitcher
canvas={canvas}
elementType={appState.elementType}
setAppState={setAppState}
isLibraryOpen={appState.isLibraryOpen}
@ -129,6 +132,7 @@ export const MobileMenu = ({
actionManager={actionManager}
appState={appState}
setAppState={setAppState}
showThemeBtn={showThemeBtn}
/>
}
</>

View File

@ -50,6 +50,7 @@
border: 1px solid var(--dialog-border-color);
box-shadow: 0 2px 10px transparentize($oc-black, 0.75);
border-radius: 6px;
box-sizing: border-box;
@media #{$is-mobile-query} {
max-width: 100%;

View File

@ -51,14 +51,14 @@ const useBodyRoot = () => {
useLayoutEffect(() => {
const isDarkTheme = !!document
.querySelector(".excalidraw")
?.classList.contains("Appearance_dark");
?.classList.contains("theme--dark");
const div = document.createElement("div");
div.classList.add("excalidraw", "excalidraw-modal-container");
if (isDarkTheme) {
div.classList.add("Appearance_dark");
div.classList.add("Appearance_dark-background-none");
div.classList.add("theme--dark");
div.classList.add("theme--dark-background-none");
}
document.body.appendChild(div);

View File

@ -1,25 +1,26 @@
import "./TextInput.scss";
import React, { Component } from "react";
import { selectNode, removeSelection } from "../utils";
type Props = {
value: string;
onChange: (value: string) => void;
label: string;
isNameEditable: boolean;
};
export class ProjectName extends Component<Props> {
private handleFocus = (event: React.FocusEvent<HTMLElement>) => {
selectNode(event.currentTarget);
type State = {
fileName: string;
};
export class ProjectName extends Component<Props, State> {
state = {
fileName: this.props.value,
};
private handleBlur = (event: React.FocusEvent<HTMLElement>) => {
const value = event.currentTarget.innerText.trim();
private handleBlur = (event: any) => {
const value = event.target.value;
if (value !== this.props.value) {
this.props.onChange(value);
}
removeSelection();
};
private handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
@ -31,32 +32,30 @@ export class ProjectName extends Component<Props> {
event.currentTarget.blur();
}
};
private makeEditable = (editable: HTMLSpanElement | null) => {
if (!editable) {
return;
}
try {
editable.contentEditable = "plaintext-only";
} catch {
editable.contentEditable = "true";
}
};
public render() {
return (
<span
suppressContentEditableWarning
ref={this.makeEditable}
data-type="wysiwyg"
className="TextInput"
role="textbox"
aria-label={this.props.label}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
onFocus={this.handleFocus}
>
{this.props.value}
</span>
<>
<label htmlFor="file-name">
{`${this.props.label}${this.props.isNameEditable ? "" : ":"}`}
</label>
{this.props.isNameEditable ? (
<input
className="TextInput"
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
id="file-name"
value={this.state.fileName}
onChange={(event) =>
this.setState({ fileName: event.target.value })
}
/>
) : (
<span className="TextInput TextInput--readonly" id="file-name">
{this.props.value}
</span>
)}
</>
);
}
}

View File

@ -19,6 +19,7 @@
.Toast__message {
color: var(--popup-text-color);
white-space: pre-wrap;
}
@keyframes fade-in {

View File

@ -58,6 +58,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
"ToolIcon--selected": props.selected,
},
)}
data-testid={props["data-testid"]}
hidden={props.hidden}
title={props.title}
aria-label={props["aria-label"]}

View File

@ -1,6 +1,5 @@
import React from "react";
import * as Sentry from "@sentry/browser";
import { resetCursor } from "../utils";
import { t } from "../i18n";
interface TopErrorBoundaryState {
@ -24,7 +23,6 @@ export class TopErrorBoundary extends React.Component<
}
componentDidCatch(error: Error, errorInfo: any) {
resetCursor();
const _localStorage: any = {};
for (const [key, value] of Object.entries({ ...localStorage })) {
try {

View File

@ -11,12 +11,12 @@ import React from "react";
import oc from "open-color";
import clsx from "clsx";
const activeElementColor = (appearance: "light" | "dark") =>
appearance === "light" ? oc.orange[4] : oc.orange[9];
const iconFillColor = (appearance: "light" | "dark") =>
appearance === "light" ? oc.black : oc.gray[4];
const handlerColor = (appearance: "light" | "dark") =>
appearance === "light" ? oc.white : "#1e1e1e";
const activeElementColor = (theme: "light" | "dark") =>
theme === "light" ? oc.orange[4] : oc.orange[9];
const iconFillColor = (theme: "light" | "dark") =>
theme === "light" ? oc.black : oc.gray[4];
const handlerColor = (theme: "light" | "dark") =>
theme === "light" ? oc.white : "#1e1e1e";
type Opts = {
width?: number;
@ -113,6 +113,32 @@ export const questionCircle = createIcon(
{ mirror: true },
);
export const share = createIcon(
"M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z",
{ width: 24, height: 24 },
);
export const shareIOS = createIcon(
"M16 5l-1.42 1.42-1.59-1.59V16h-1.98V4.83L9.42 6.42 8 5l4-4 4 4zm4 5v11c0 1.1-.9 2-2 2H6c-1.11 0-2-.9-2-2V10c0-1.11.89-2 2-2h3v2H6v11h12V10h-3V8h3c1.1 0 2 .89 2 2z",
{ width: 24, height: 24 },
);
export const shareWindows = createIcon(
<>
<path
stroke="currentColor"
fill="currentColor"
d="M40 5.6v6.1l-4.1.7c-8.9 1.4-16.5 6.9-20.6 15C13 32 10.9 43 12.4 43c.4 0 2.4-1.3 4.4-3 5-3.9 12.1-7 18.2-7.7l5-.6v12.8l11.2-11.3L62.5 22 51.2 10.8 40-.5v6.1zm10.2 22.6L44 34.5v-6.8l-6.9.6c-3.9.3-9.8 1.7-13.2 3.1-3.5 1.4-6.5 2.4-6.7 2.2-.9-1 3-7.5 6.4-10.8C28 18.6 34.4 16 40.1 16c3.7 0 3.9-.1 3.9-3.2V9.5l6.2 6.3 6.3 6.2-6.3 6.2z"
/>
<path
stroke="currentColor"
fill="currentColor"
d="M0 36v20h48v-6.2c0-6 0-6.1-2-4.3-1.1 1-2 2.9-2 4.2V52H4V34c0-17.3-.1-18-2-18s-2 .7-2 20z"
/>
</>,
{ width: 64, height: 64 },
);
// Icon imported form Storybook
// Storybook is licensed under MIT https://github.com/storybookjs/storybook/blob/next/LICENSE
export const resetZoom = createIcon(
@ -126,19 +152,19 @@ export const resetZoom = createIcon(
);
export const BringForwardIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<>
<path
d="M22 9.556C22 8.696 21.303 8 20.444 8H16v8H8v4.444C8 21.304 8.697 22 9.556 22h10.888c.86 0 1.556-.697 1.556-1.556V9.556z"
fill={iconFillColor(appearance)}
stroke={iconFillColor(appearance)}
fill={iconFillColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="2"
/>
<path
d="M16 3.556C16 2.696 15.303 2 14.444 2H3.556C2.696 2 2 2.697 2 3.556v10.888C2 15.304 2.697 16 3.556 16h10.888c.86 0 1.556-.697 1.556-1.556V3.556z"
fill={activeElementColor(appearance)}
stroke={activeElementColor(appearance)}
fill={activeElementColor(theme)}
stroke={activeElementColor(theme)}
strokeWidth="2"
/>
</>,
@ -147,19 +173,19 @@ export const BringForwardIcon = React.memo(
);
export const SendBackwardIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<>
<path
d="M16 3.556C16 2.696 15.303 2 14.444 2H3.556C2.696 2 2 2.697 2 3.556v10.888C2 15.304 2.697 16 3.556 16h10.888c.86 0 1.556-.697 1.556-1.556V3.556z"
fill={activeElementColor(appearance)}
stroke={activeElementColor(appearance)}
fill={activeElementColor(theme)}
stroke={activeElementColor(theme)}
strokeWidth="2"
/>
<path
d="M22 9.556C22 8.696 21.303 8 20.444 8H9.556C8.696 8 8 8.697 8 9.556v10.888C8 21.304 8.697 22 9.556 22h10.888c.86 0 1.556-.697 1.556-1.556V9.556z"
fill={iconFillColor(appearance)}
stroke={iconFillColor(appearance)}
fill={iconFillColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="2"
/>
</>,
@ -168,19 +194,19 @@ export const SendBackwardIcon = React.memo(
);
export const BringToFrontIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<>
<path
d="M13 21a1 1 0 001 1h7a1 1 0 001-1v-7a1 1 0 00-1-1h-3v5h-5v3zM11 3a1 1 0 00-1-1H3a1 1 0 00-1 1v7a1 1 0 001 1h3V6h5V3z"
fill={iconFillColor(appearance)}
stroke={iconFillColor(appearance)}
fill={iconFillColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="2"
/>
<path
d="M18 7.333C18 6.597 17.403 6 16.667 6H7.333C6.597 6 6 6.597 6 7.333v9.334C6 17.403 6.597 18 7.333 18h9.334c.736 0 1.333-.597 1.333-1.333V7.333z"
fill={activeElementColor(appearance)}
stroke={activeElementColor(appearance)}
fill={activeElementColor(theme)}
stroke={activeElementColor(theme)}
strokeWidth="2"
/>
</>,
@ -189,20 +215,20 @@ export const BringToFrontIcon = React.memo(
);
export const SendToBackIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<>
<path
d="M18 7.333C18 6.597 17.403 6 16.667 6H7.333C6.597 6 6 6.597 6 7.333v9.334C6 17.403 6.597 18 7.333 18h9.334c.736 0 1.333-.597 1.333-1.333V7.333z"
fill={activeElementColor(appearance)}
stroke={activeElementColor(appearance)}
fill={activeElementColor(theme)}
stroke={activeElementColor(theme)}
strokeLinejoin="round"
strokeWidth="2"
/>
<path
d="M11 3a1 1 0 00-1-1H3a1 1 0 00-1 1v7a1 1 0 001 1h8V3zM22 14a1 1 0 00-1-1h-7a1 1 0 00-1 1v7a1 1 0 001 1h8v-8z"
fill={iconFillColor(appearance)}
stroke={iconFillColor(appearance)}
fill={iconFillColor(theme)}
stroke={iconFillColor(theme)}
strokeLinejoin="round"
strokeWidth="2"
/>
@ -218,20 +244,20 @@ export const SendToBackIcon = React.memo(
// that would make them lie about their function.
//
export const AlignTopIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<>
<path
d="M 2,5 H 22"
fill={iconFillColor(appearance)}
stroke={iconFillColor(appearance)}
fill={iconFillColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M 6,7 C 5.446,7 5,7.446 5,8 v 9.999992 c 0,0.554 0.446,1 1,1 h 3.0000001 c 0.554,0 0.9999999,-0.446 0.9999999,-1 V 8 C 10,7.446 9.5540001,7 9.0000001,7 Z m 9,0 c -0.554,0 -1,0.446 -1,1 v 5.999992 c 0,0.554 0.446,1 1,1 h 3 c 0.554,0 1,-0.446 1,-1 V 8 C 19,7.446 18.554,7 18,7 Z"
fill={activeElementColor(appearance)}
stroke={activeElementColor(appearance)}
fill={activeElementColor(theme)}
stroke={activeElementColor(theme)}
strokeWidth="2"
/>
</>,
@ -240,20 +266,20 @@ export const AlignTopIcon = React.memo(
);
export const AlignBottomIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<>
<path
d="M 2,19 H 22"
fill={iconFillColor(appearance)}
stroke={iconFillColor(appearance)}
fill={iconFillColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="m 6,16.999992 c -0.554,0 -1,-0.446 -1,-1 V 6 C 5,5.446 5.446,5 6,5 H 9.0000001 C 9.5540001,5 10,5.446 10,6 v 9.999992 c 0,0.554 -0.4459999,1 -0.9999999,1 z m 9,0 c -0.554,0 -1,-0.446 -1,-1 V 10 c 0,-0.554 0.446,-1 1,-1 h 3 c 0.554,0 1,0.446 1,1 v 5.999992 c 0,0.554 -0.446,1 -1,1 z"
fill={activeElementColor(appearance)}
stroke={activeElementColor(appearance)}
fill={activeElementColor(theme)}
stroke={activeElementColor(theme)}
strokeWidth="2"
/>
</>,
@ -262,20 +288,20 @@ export const AlignBottomIcon = React.memo(
);
export const AlignLeftIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<>
<path
d="M 5,2 V 22"
fill={iconFillColor(appearance)}
stroke={iconFillColor(appearance)}
fill={iconFillColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="m 7.000004,5.999996 c 0,-0.554 0.446,-1 1,-1 h 9.999992 c 0.554,0 1,0.446 1,1 v 3.0000001 c 0,0.554 -0.446,0.9999999 -1,0.9999999 H 8.000004 c -0.554,0 -1,-0.4459999 -1,-0.9999999 z m 0,9 c 0,-0.554 0.446,-1 1,-1 h 5.999992 c 0.554,0 1,0.446 1,1 v 3 c 0,0.554 -0.446,1 -1,1 H 8.000004 c -0.554,0 -1,-0.446 -1,-1 z"
fill={activeElementColor(appearance)}
stroke={activeElementColor(appearance)}
fill={activeElementColor(theme)}
stroke={activeElementColor(theme)}
strokeWidth="2"
/>
</>,
@ -284,20 +310,20 @@ export const AlignLeftIcon = React.memo(
);
export const AlignRightIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<>
<path
d="M 19,2 V 22"
fill={iconFillColor(appearance)}
stroke={iconFillColor(appearance)}
fill={iconFillColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="m 16.999996,5.999996 c 0,-0.554 -0.446,-1 -1,-1 H 6.000004 c -0.554,0 -1,0.446 -1,1 v 3.0000001 c 0,0.554 0.446,0.9999999 1,0.9999999 h 9.999992 c 0.554,0 1,-0.4459999 1,-0.9999999 z m 0,9 c 0,-0.554 -0.446,-1 -1,-1 h -5.999992 c -0.554,0 -1,0.446 -1,1 v 3 c 0,0.554 0.446,1 1,1 h 5.999992 c 0.554,0 1,-0.446 1,-1 z"
fill={activeElementColor(appearance)}
stroke={activeElementColor(appearance)}
fill={activeElementColor(theme)}
stroke={activeElementColor(theme)}
strokeWidth="2"
/>
</>,
@ -306,20 +332,20 @@ export const AlignRightIcon = React.memo(
);
export const DistributeHorizontallyIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<>
<path d="M5 5V19Z" fill="black" />
<path
d="M19 5V19M5 5V19"
stroke={iconFillColor(appearance)}
stroke={iconFillColor(theme)}
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M15 9C15.554 9 16 9.446 16 10V14C16 14.554 15.554 15 15 15H9C8.446 15 8 14.554 8 14V10C8 9.446 8.446 9 9 9H15Z"
fill={activeElementColor(appearance)}
stroke={activeElementColor(appearance)}
fill={activeElementColor(theme)}
stroke={activeElementColor(theme)}
strokeWidth="2"
/>
</>,
@ -336,20 +362,20 @@ export const DistributeHorizontallyIcon = React.memo(
></svg>;
export const DistributeVerticallyIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<>
<path
d="M5 5L19 5M5 19H19"
fill={iconFillColor(appearance)}
stroke={iconFillColor(appearance)}
fill={iconFillColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M15 9C15.554 9 16 9.446 16 10V14C16 14.554 15.554 15 15 15H9C8.446 15 8 14.554 8 14V10C8 9.446 8.446 9 9 9H15Z"
fill={activeElementColor(appearance)}
stroke={activeElementColor(appearance)}
fill={activeElementColor(theme)}
stroke={activeElementColor(theme)}
strokeWidth="2"
/>
</>,
@ -358,19 +384,19 @@ export const DistributeVerticallyIcon = React.memo(
);
export const CenterVerticallyIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<>
<path
d="m 5.000004,16.999996 c 0,0.554 0.446,1 1,1 h 3 c 0.554,0 1,-0.446 1,-1 v -10 c 0,-0.554 -0.446,-1 -1,-1 h -3 c -0.554,0 -1,0.446 -1,1 z m 9,-2 c 0,0.554 0.446,1 1,1 h 3 c 0.554,0 1,-0.446 1,-1 v -6 c 0,-0.554 -0.446,-1 -1,-1 h -3 c -0.554,0 -1,0.446 -1,1 z"
fill={activeElementColor(appearance)}
stroke={activeElementColor(appearance)}
fill={activeElementColor(theme)}
stroke={activeElementColor(theme)}
strokeWidth="2"
/>
<path
d="M 2,12 H 22"
fill={iconFillColor(appearance)}
stroke={iconFillColor(appearance)}
fill={iconFillColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="2"
strokeDasharray="1, 2.8"
strokeLinecap="round"
@ -381,19 +407,19 @@ export const CenterVerticallyIcon = React.memo(
);
export const CenterHorizontallyIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<>
<path
d="M 7 5 C 6.446 5 6 5.446 6 6 L 6 9 C 6 9.554 6.446 10 7 10 L 17 10 C 17.554 10 18 9.554 18 9 L 18 6 C 18 5.446 17.554 5 17 5 L 7 5 z M 9 14 C 8.446 14 8 14.446 8 15 L 8 18 C 8 18.554 8.446 19 9 19 L 15 19 C 15.554 19 16 18.554 16 18 L 16 15 C 16 14.446 15.554 14 15 14 L 9 14 z "
fill={activeElementColor(appearance)}
stroke={activeElementColor(appearance)}
fill={activeElementColor(theme)}
stroke={activeElementColor(theme)}
strokeWidth="2"
/>
<path
d="M 12,2 V 22"
fill={iconFillColor(appearance)}
stroke={iconFillColor(appearance)}
fill={iconFillColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="2"
strokeDasharray="1, 2.8"
strokeLinecap="round"
@ -438,77 +464,76 @@ export const shield = createIcon(
{ width: 24 },
);
export const GroupIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
createIcon(
<>
<path d="M25 26H111V111H25" fill={iconFillColor(appearance)} />
<path
d="M25 111C25 80.2068 25 49.4135 25 26M25 26C48.6174 26 72.2348 26 111 26H25ZM25 26C53.3671 26 81.7343 26 111 26H25ZM111 26C111 52.303 111 78.606 111 111V26ZM111 26C111 51.2947 111 76.5893 111 111V26ZM111 111C87.0792 111 63.1585 111 25 111H111ZM111 111C87.4646 111 63.9293 111 25 111H111ZM25 111C25 81.1514 25 51.3028 25 26V111Z"
stroke={iconFillColor(appearance)}
strokeWidth="2"
/>
<path d="M100 100H160V160H100" fill={iconFillColor(appearance)} />
<path
d="M100 160C100 144.106 100 128.211 100 100M100 100C117.706 100 135.412 100 160 100H100ZM100 100C114.214 100 128.428 100 160 100H100ZM160 100C160 120.184 160 140.369 160 160V100ZM160 100C160 113.219 160 126.437 160 160V100ZM160 160C145.534 160 131.068 160 100 160H160ZM160 160C143.467 160 126.934 160 100 160H160ZM100 160C100 143.661 100 127.321 100 100V160Z"
stroke={iconFillColor(appearance)}
strokeWidth="2"
/>
<rect
x="2.5"
y="2.5"
width="30"
height="30"
fill={handlerColor(appearance)}
stroke={iconFillColor(appearance)}
strokeWidth="6"
/>
<rect
x="2.5"
y="149.5"
width="30"
height="30"
fill={handlerColor(appearance)}
stroke={iconFillColor(appearance)}
strokeWidth="6"
/>
<rect
x="147.5"
y="149.5"
width="30"
height="30"
fill={handlerColor(appearance)}
stroke={iconFillColor(appearance)}
strokeWidth="6"
/>
<rect
x="147.5"
y="2.5"
width="30"
height="30"
fill={handlerColor(appearance)}
stroke={iconFillColor(appearance)}
strokeWidth="6"
/>
</>,
{ width: 182, height: 182, mirror: true },
),
export const GroupIcon = React.memo(({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<>
<path d="M25 26H111V111H25" fill={iconFillColor(theme)} />
<path
d="M25 111C25 80.2068 25 49.4135 25 26M25 26C48.6174 26 72.2348 26 111 26H25ZM25 26C53.3671 26 81.7343 26 111 26H25ZM111 26C111 52.303 111 78.606 111 111V26ZM111 26C111 51.2947 111 76.5893 111 111V26ZM111 111C87.0792 111 63.1585 111 25 111H111ZM111 111C87.4646 111 63.9293 111 25 111H111ZM25 111C25 81.1514 25 51.3028 25 26V111Z"
stroke={iconFillColor(theme)}
strokeWidth="2"
/>
<path d="M100 100H160V160H100" fill={iconFillColor(theme)} />
<path
d="M100 160C100 144.106 100 128.211 100 100M100 100C117.706 100 135.412 100 160 100H100ZM100 100C114.214 100 128.428 100 160 100H100ZM160 100C160 120.184 160 140.369 160 160V100ZM160 100C160 113.219 160 126.437 160 160V100ZM160 160C145.534 160 131.068 160 100 160H160ZM160 160C143.467 160 126.934 160 100 160H160ZM100 160C100 143.661 100 127.321 100 100V160Z"
stroke={iconFillColor(theme)}
strokeWidth="2"
/>
<rect
x="2.5"
y="2.5"
width="30"
height="30"
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
<rect
x="2.5"
y="149.5"
width="30"
height="30"
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
<rect
x="147.5"
y="149.5"
width="30"
height="30"
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
<rect
x="147.5"
y="2.5"
width="30"
height="30"
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
</>,
{ width: 182, height: 182, mirror: true },
),
);
export const UngroupIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<>
<path d="M25 26H111V111H25" fill={iconFillColor(appearance)} />
<path d="M25 26H111V111H25" fill={iconFillColor(theme)} />
<path
d="M25 111C25 80.2068 25 49.4135 25 26M25 26C48.6174 26 72.2348 26 111 26H25ZM25 26C53.3671 26 81.7343 26 111 26H25ZM111 26C111 52.303 111 78.606 111 111V26ZM111 26C111 51.2947 111 76.5893 111 111V26ZM111 111C87.0792 111 63.1585 111 25 111H111ZM111 111C87.4646 111 63.9293 111 25 111H111ZM25 111C25 81.1514 25 51.3028 25 26V111Z"
stroke={iconFillColor(appearance)}
stroke={iconFillColor(theme)}
strokeWidth="2"
/>
<path d="M100 100H160V160H100" fill={iconFillColor(appearance)} />
<path d="M100 100H160V160H100" fill={iconFillColor(theme)} />
<path
d="M100 160C100 144.106 100 128.211 100 100M100 100C117.706 100 135.412 100 160 100H100ZM100 100C114.214 100 128.428 100 160 100H100ZM160 100C160 120.184 160 140.369 160 160V100ZM160 100C160 113.219 160 126.437 160 160V100ZM160 160C145.534 160 131.068 160 100 160H160ZM160 160C143.467 160 126.934 160 100 160H160ZM100 160C100 143.661 100 127.321 100 100V160Z"
stroke={iconFillColor(appearance)}
stroke={iconFillColor(theme)}
strokeWidth="2"
/>
<rect
@ -516,8 +541,8 @@ export const UngroupIcon = React.memo(
y="2.5"
width="30"
height="30"
fill={handlerColor(appearance)}
stroke={iconFillColor(appearance)}
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
<rect
@ -525,8 +550,8 @@ export const UngroupIcon = React.memo(
y="149.5"
width="30"
height="30"
fill={handlerColor(appearance)}
stroke={iconFillColor(appearance)}
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
<rect
@ -534,8 +559,8 @@ export const UngroupIcon = React.memo(
y="149.5"
width="30"
height="30"
fill={handlerColor(appearance)}
stroke={iconFillColor(appearance)}
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
<rect
@ -543,8 +568,8 @@ export const UngroupIcon = React.memo(
y="78.5"
width="30"
height="30"
fill={handlerColor(appearance)}
stroke={iconFillColor(appearance)}
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
<rect
@ -552,8 +577,8 @@ export const UngroupIcon = React.memo(
y="2.5"
width="30"
height="30"
fill={handlerColor(appearance)}
stroke={iconFillColor(appearance)}
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
<rect
@ -561,8 +586,8 @@ export const UngroupIcon = React.memo(
y="102.5"
width="30"
height="30"
fill={handlerColor(appearance)}
stroke={iconFillColor(appearance)}
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
</>,
@ -571,22 +596,22 @@ export const UngroupIcon = React.memo(
);
export const FillHachureIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
fillRule="evenodd"
clipRule="evenodd"
d="M20.101 16H28.0934L36 8.95989V4H33.5779L20.101 16ZM30.5704 4L17.0935 16H9.10101L22.5779 4H30.5704ZM19.5704 4L6.09349 16H4V10.7475L11.5779 4H19.5704ZM8.57036 4H4V8.06952L8.57036 4ZM36 11.6378L31.101 16H36V11.6378ZM2 2V18H38V2H2Z"
fill={iconFillColor(appearance)}
fill={iconFillColor(theme)}
/>,
{ width: 40, height: 20 },
),
);
export const FillCrossHatchIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<g fill={iconFillColor(appearance)} fillRule="evenodd" clipRule="evenodd">
<g fill={iconFillColor(theme)} fillRule="evenodd" clipRule="evenodd">
<path d="M20.101 16H28.0934L36 8.95989V4H33.5779L20.101 16ZM30.5704 4L17.0935 16H9.10101L22.5779 4H30.5704ZM19.5704 4L6.09349 16H4V10.7475L11.5779 4H19.5704ZM8.57036 4H4V8.06952L8.57036 4ZM36 11.6378L31.101 16H36V11.6378ZM2 2V18H38V2H2Z" />
<path d="M14.0001 18L3.00006 4.00002L4.5727 2.76438L15.5727 16.7644L14.0001 18ZM25.0001 18L14.0001 4.00002L15.5727 2.76438L26.5727 16.7644L25.0001 18ZM36.0001 18L25.0001 4.00002L26.5727 2.76438L37.5727 16.7644L36.0001 18Z" />
</g>,
@ -595,25 +620,19 @@ export const FillCrossHatchIcon = React.memo(
);
export const FillSolidIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
createIcon(<path d="M2 2H38V18H2V2Z" fill={iconFillColor(appearance)} />, {
({ theme }: { theme: "light" | "dark" }) =>
createIcon(<path d="M2 2H38V18H2V2Z" fill={iconFillColor(theme)} />, {
width: 40,
height: 20,
}),
);
export const StrokeWidthIcon = React.memo(
({
appearance,
strokeWidth,
}: {
appearance: "light" | "dark";
strokeWidth: number;
}) =>
({ theme, strokeWidth }: { theme: "light" | "dark"; strokeWidth: number }) =>
createIcon(
<path
d="M6 10H34"
stroke={iconFillColor(appearance)}
stroke={iconFillColor(theme)}
strokeWidth={strokeWidth}
fill="none"
/>,
@ -622,11 +641,11 @@ export const StrokeWidthIcon = React.memo(
);
export const StrokeStyleSolidIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
d="M6 10H34"
stroke={iconFillColor(appearance)}
stroke={iconFillColor(theme)}
strokeWidth={2}
fill="none"
/>,
@ -638,11 +657,11 @@ export const StrokeStyleSolidIcon = React.memo(
);
export const StrokeStyleDashedIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
d="M6 10H34"
stroke={iconFillColor(appearance)}
stroke={iconFillColor(theme)}
strokeWidth={2.5}
strokeDasharray={"10, 8"}
fill="none"
@ -652,11 +671,11 @@ export const StrokeStyleDashedIcon = React.memo(
);
export const StrokeStyleDottedIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
d="M6 10H34"
stroke={iconFillColor(appearance)}
stroke={iconFillColor(theme)}
strokeWidth={2.5}
strokeDasharray={"4, 4"}
fill="none"
@ -666,11 +685,11 @@ export const StrokeStyleDottedIcon = React.memo(
);
export const SloppinessArchitectIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
d="M3.00098 16.1691C6.28774 13.9744 19.6399 2.8905 22.7215 3.00082C25.8041 3.11113 19.1158 15.5488 21.4962 16.8309C23.8757 18.1131 34.4155 11.7148 37.0001 10.6919"
stroke={iconFillColor(appearance)}
stroke={iconFillColor(theme)}
strokeWidth={2}
fill="none"
/>,
@ -679,11 +698,11 @@ export const SloppinessArchitectIcon = React.memo(
);
export const SloppinessArtistIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
d="M3 17C6.68158 14.8752 16.1296 9.09849 22.0648 6.54922C28 3.99995 22.2896 13.3209 25 14C27.7104 14.6791 36.3757 9.6471 36.3757 9.6471M6.40706 15C13 11.1918 20.0468 1.51045 23.0234 3.0052C26 4.49995 20.457 12.8659 22.7285 16.4329C25 20 36.3757 13 36.3757 13"
stroke={iconFillColor(appearance)}
stroke={iconFillColor(theme)}
strokeWidth={2}
fill="none"
/>,
@ -692,11 +711,11 @@ export const SloppinessArtistIcon = React.memo(
);
export const SloppinessCartoonistIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
d="M3 15.6468C6.93692 13.5378 22.5544 2.81528 26.6206 3.00242C30.6877 3.18956 25.6708 15.3346 27.4009 16.7705C29.1309 18.2055 35.4001 12.4762 37 11.6177M3.97143 10.4917C6.61158 9.24563 16.3706 2.61886 19.8104 3.01724C23.2522 3.41472 22.0773 12.2013 24.6181 12.8783C27.1598 13.5536 33.3179 8.04068 35.0571 7.07244"
stroke={iconFillColor(appearance)}
stroke={iconFillColor(theme)}
strokeWidth={2}
fill="none"
/>,
@ -705,11 +724,11 @@ export const SloppinessCartoonistIcon = React.memo(
);
export const EdgeSharpIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
d="M10 17L10 5L35 5"
stroke={iconFillColor(appearance)}
stroke={iconFillColor(theme)}
strokeWidth={2}
fill="none"
/>,
@ -718,11 +737,11 @@ export const EdgeSharpIcon = React.memo(
);
export const EdgeRoundIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
d="M10 17V15C10 8 13 5 21 5L33.5 5"
stroke={iconFillColor(appearance)}
stroke={iconFillColor(theme)}
strokeWidth={2}
fill="none"
/>,
@ -731,11 +750,11 @@ export const EdgeRoundIcon = React.memo(
);
export const ArrowheadNoneIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
d="M6 10H34"
stroke={iconFillColor(appearance)}
stroke={iconFillColor(theme)}
strokeWidth={2}
fill="none"
/>,
@ -747,17 +766,11 @@ export const ArrowheadNoneIcon = React.memo(
);
export const ArrowheadArrowIcon = React.memo(
({
appearance,
flip = false,
}: {
appearance: "light" | "dark";
flip?: boolean;
}) =>
({ theme, flip = false }: { theme: "light" | "dark"; flip?: boolean }) =>
createIcon(
<g
transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}
stroke={iconFillColor(appearance)}
stroke={iconFillColor(theme)}
strokeWidth={2}
fill="none"
>
@ -769,17 +782,11 @@ export const ArrowheadArrowIcon = React.memo(
);
export const ArrowheadDotIcon = React.memo(
({
appearance,
flip = false,
}: {
appearance: "light" | "dark";
flip?: boolean;
}) =>
({ theme, flip = false }: { theme: "light" | "dark"; flip?: boolean }) =>
createIcon(
<g
stroke={iconFillColor(appearance)}
fill={iconFillColor(appearance)}
stroke={iconFillColor(theme)}
fill={iconFillColor(theme)}
transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}
>
<path d="M32 10L6 10" strokeWidth={2} />
@ -790,18 +797,12 @@ export const ArrowheadDotIcon = React.memo(
);
export const ArrowheadBarIcon = React.memo(
({
appearance,
flip = false,
}: {
appearance: "light" | "dark";
flip?: boolean;
}) =>
({ theme, flip = false }: { theme: "light" | "dark"; flip?: boolean }) =>
createIcon(
<g transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}>
<path
d="M34 10H5.99996M34 10L34 5M34 10L34 15"
stroke={iconFillColor(appearance)}
stroke={iconFillColor(theme)}
strokeWidth={2}
fill="none"
/>
@ -809,3 +810,121 @@ export const ArrowheadBarIcon = React.memo(
{ width: 40, height: 20 },
),
);
export const FontSizeSmallIcon = React.memo(
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
fill={iconFillColor(theme)}
d="M 0 69.092 L 0 55.03 A 124.24 124.24 0 0 0 4.706 57.02 Q 6.826 57.863 8.708 58.5 A 53.466 53.466 0 0 0 12.231 59.571 Q 17.236 60.889 21.387 60.889 A 20.909 20.909 0 0 0 24.265 60.704 Q 25.719 60.502 26.903 60.077 A 8.649 8.649 0 0 0 29.028 58.985 Q 31.689 57.08 31.689 53.321 Q 31.689 51.221 30.518 49.585 A 10.126 10.126 0 0 0 29.282 48.177 Q 28.352 47.287 27.075 46.436 A 23.719 23.719 0 0 0 25.752 45.627 Q 23.774 44.492 20.176 42.735 A 254.44 254.44 0 0 0 17.822 41.602 Q 11.503 38.631 8.236 35.888 A 19.742 19.742 0 0 1 8.008 35.694 A 22.18 22.18 0 0 1 2.783 29.102 Q 0.83 25.342 0.83 20.313 A 22.471 22.471 0 0 1 1.733 13.778 A 17.283 17.283 0 0 1 7.251 5.42 A 21.486 21.486 0 0 1 15.177 1.272 Q 18.361 0.338 22.166 0.09 A 43.573 43.573 0 0 1 25 0 A 42.399 42.399 0 0 1 34.349 1.01 A 39.075 39.075 0 0 1 35.62 1.319 A 67.407 67.407 0 0 1 42.108 3.382 A 83.357 83.357 0 0 1 46.191 5.03 L 41.309 16.797 Q 35.596 14.453 31.86 13.526 A 30.762 30.762 0 0 0 25.417 12.612 A 28.337 28.337 0 0 0 24.512 12.598 A 14.846 14.846 0 0 0 22.022 12.793 Q 19.498 13.224 17.92 14.6 Q 15.625 16.602 15.625 19.824 Q 15.625 21.826 16.553 23.316 Q 17.48 24.805 19.507 26.197 A 18.343 18.343 0 0 0 20.659 26.912 Q 22.596 28.035 26.516 29.953 A 299.99 299.99 0 0 0 29.102 31.201 Q 37.91 35.412 41.841 39.642 A 16.553 16.553 0 0 1 42.822 40.796 A 17.675 17.675 0 0 1 46.301 49.233 A 23.517 23.517 0 0 1 46.533 52.588 A 21.581 21.581 0 0 1 45.471 59.515 A 17.733 17.733 0 0 1 39.575 67.823 Q 33.745 72.486 24.094 73.243 A 49.683 49.683 0 0 1 20.215 73.389 A 51.712 51.712 0 0 1 9.448 72.315 A 40.672 40.672 0 0 1 0 69.092 Z"
/>,
{ width: 47, height: 77 },
),
);
export const FontSizeMediumIcon = React.memo(
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
fill={iconFillColor(theme)}
d="M 44.092 71.387 L 30.225 71.387 L 13.037 15.381 L 12.598 15.381 A 1505.093 1505.093 0 0 1 12.959 22.313 Q 13.426 31.715 13.508 36.4 A 102.991 102.991 0 0 1 13.525 38.184 L 13.525 71.387 L 0 71.387 L 0 0 L 20.605 0 L 37.5 54.59 L 37.793 54.59 L 55.713 0 L 76.318 0 L 76.318 71.387 L 62.207 71.387 L 62.207 37.598 Q 62.207 35.205 62.28 32.08 A 160.703 160.703 0 0 1 62.326 30.544 Q 62.452 26.754 62.866 17.168 A 5390.536 5390.536 0 0 1 62.939 15.479 L 62.5 15.479 L 44.092 71.387 Z"
/>,
{ width: 77, height: 75 },
),
);
export const FontSizeLargeIcon = React.memo(
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
fill={iconFillColor(theme)}
d="M 44.092 71.387 L 0 71.387 L 0 0 L 15.137 0 L 15.137 58.887 L 44.092 58.887 L 44.092 71.387 Z"
/>,
{ width: 45, height: 75 },
),
);
export const FontSizeExtraLargeIcon = React.memo(
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
fill={iconFillColor(theme)}
d="M 42.578 35.4 L 66.699 71.387 L 49.414 71.387 L 32.813 44.385 L 16.211 71.387 L 0 71.387 L 23.682 34.57 L 1.514 0 L 18.213 0 L 33.594 25.684 L 48.682 0 L 64.99 0 L 42.578 35.4 Z M 119.775 71.387 L 75.684 71.387 L 75.684 0 L 90.82 0 L 90.82 58.887 L 119.775 58.887 L 119.775 71.387 Z"
/>,
{ width: 120, height: 75 },
),
);
export const FontFamilyHandDrawnIcon = React.memo(
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
fill={iconFillColor(theme)}
d="M290.74 93.24l128.02 128.02-277.99 277.99-114.14 12.6C11.35 513.54-1.56 500.62.14 485.34l12.7-114.22 277.9-277.88zm207.2-19.06l-60.11-60.11c-18.75-18.75-49.16-18.75-67.91 0l-56.55 56.55 128.02 128.02 56.55-56.55c18.75-18.76 18.75-49.16 0-67.91z"
/>,
{ width: 448, height: 512 },
),
);
export const FontFamilyNormalIcon = React.memo(
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<>
<path
fill={iconFillColor(theme)}
d="M 63.818 71.68 L 54.492 71.68 L 45.898 49.561 L 17.578 49.561 L 9.082 71.68 L 0 71.68 L 27.881 0 L 35.986 0 L 63.818 71.68 Z M 20.605 41.602 L 43.213 41.602 L 35.205 19.971 L 31.787 9.277 Q 30.322 15.137 28.711 19.971 L 20.605 41.602 Z"
/>
<path
fill={iconFillColor(theme)}
d="M 68.994 71.68 L 52.686 71.68 L 47.51 54.688 L 21.484 54.688 L 16.309 71.68 L 0 71.68 L 25.195 0 L 43.701 0 L 68.994 71.68 Z M 25.293 41.992 L 43.896 41.992 A 27590.463 27590.463 0 0 1 42.2 36.532 Q 36.965 19.676 35.937 16.273 A 120.932 120.932 0 0 1 35.815 15.869 A 131.65 131.65 0 0 1 35.396 14.435 Q 34.951 12.879 34.675 11.741 A 34.866 34.866 0 0 1 34.521 11.084 A 141.762 141.762 0 0 1 33.706 14.075 Q 31.482 21.957 25.293 41.992 Z"
/>
</>,
{ width: 70, height: 78 },
),
);
export const FontFamilyCodeIcon = React.memo(
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<>
<path
fill={iconFillColor(theme)}
d="M278.9 511.5l-61-17.7c-6.4-1.8-10-8.5-8.2-14.9L346.2 8.7c1.8-6.4 8.5-10 14.9-8.2l61 17.7c6.4 1.8 10 8.5 8.2 14.9L293.8 503.3c-1.9 6.4-8.5 10.1-14.9 8.2zm-114-112.2l43.5-46.4c4.6-4.9 4.3-12.7-.8-17.2L117 256l90.6-79.7c5.1-4.5 5.5-12.3.8-17.2l-43.5-46.4c-4.5-4.8-12.1-5.1-17-.5L3.8 247.2c-5.1 4.7-5.1 12.8 0 17.5l144.1 135.1c4.9 4.6 12.5 4.4 17-.5zm327.2.6l144.1-135.1c5.1-4.7 5.1-12.8 0-17.5L492.1 112.1c-4.8-4.5-12.4-4.3-17 .5L431.6 159c-4.6 4.9-4.3 12.7.8 17.2L523 256l-90.6 79.7c-5.1 4.5-5.5 12.3-.8 17.2l43.5 46.4c4.5 4.9 12.1 5.1 17 .6z"
/>
</>,
{ width: 640, height: 512 },
),
);
export const TextAlignLeftIcon = React.memo(
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
d="M12.83 352h262.34A12.82 12.82 0 00288 339.17v-38.34A12.82 12.82 0 00275.17 288H12.83A12.82 12.82 0 000 300.83v38.34A12.82 12.82 0 0012.83 352zm0-256h262.34A12.82 12.82 0 00288 83.17V44.83A12.82 12.82 0 00275.17 32H12.83A12.82 12.82 0 000 44.83v38.34A12.82 12.82 0 0012.83 96zM432 160H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm0 256H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16z"
fill={iconFillColor(theme)}
/>,
{ width: 448, height: 512 },
),
);
export const TextAlignCenterIcon = React.memo(
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
d="M432 160H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm0 256H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zM108.1 96h231.81A12.09 12.09 0 00352 83.9V44.09A12.09 12.09 0 00339.91 32H108.1A12.09 12.09 0 0096 44.09V83.9A12.1 12.1 0 00108.1 96zm231.81 256A12.09 12.09 0 00352 339.9v-39.81A12.09 12.09 0 00339.91 288H108.1A12.09 12.09 0 0096 300.09v39.81a12.1 12.1 0 0012.1 12.1z"
fill={iconFillColor(theme)}
/>,
{ width: 448, height: 512 },
),
);
export const TextAlignRightIcon = React.memo(
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
d="M16 224h416a16 16 0 0016-16v-32a16 16 0 00-16-16H16a16 16 0 00-16 16v32a16 16 0 0016 16zm416 192H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm3.17-384H172.83A12.82 12.82 0 00160 44.83v38.34A12.82 12.82 0 00172.83 96h262.34A12.82 12.82 0 00448 83.17V44.83A12.82 12.82 0 00435.17 32zm0 256H172.83A12.82 12.82 0 00160 300.83v38.34A12.82 12.82 0 00172.83 352h262.34A12.82 12.82 0 00448 339.17v-38.34A12.82 12.82 0 00435.17 288z"
fill={iconFillColor(theme)}
/>,
{ width: 448, height: 512 },
),
);

View File

@ -1,4 +1,5 @@
import { FontFamily } from "./element/types";
import cssVariables from "./css/variables.module.scss";
export const APP_NAME = "Excalidraw";
@ -83,9 +84,15 @@ export const MIME_TYPES = {
excalidrawlib: "application/vnd.excalidrawlib+json",
};
export const EXPORT_DATA_TYPES = {
excalidraw: "excalidraw",
excalidrawClipboard: "excalidraw/clipboard",
excalidrawLibrary: "excalidrawlib",
} as const;
export const STORAGE_KEYS = {
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
};
} as const;
// time in milliseconds
export const TAP_TWICE_TIMEOUT = 300;
@ -93,7 +100,7 @@ export const TOUCH_CTX_MENU_TIMEOUT = 500;
export const TITLE_TIMEOUT = 10000;
export const TOAST_TIMEOUT = 5000;
export const VERSION_TIMEOUT = 30000;
export const SCROLL_TIMEOUT = 500;
export const SCROLL_TIMEOUT = 100;
export const ZOOM_STEP = 0.1;
@ -107,3 +114,5 @@ export const MODES = {
ZEN: "zenMode",
GRID: "gridMode",
};
export const THEME_FILTER = cssVariables.themeFilter;

View File

@ -8,6 +8,8 @@
}
.excalidraw {
position: relative;
overflow: hidden;
color: var(--text-primary-color);
display: flex;
top: 0;
@ -15,6 +17,13 @@
left: 0;
right: 0;
// serves 2 purposes:
// 1. prevent selecting text outside the component when double-clicking or
// dragging inside it (e.g. on canvas)
// 2. prevent selecting UI, both from the inside, and from outside the
// component (e.g. if you select text in a sidebar)
user-select: none;
a {
font-weight: 500;
text-decoration: none;
@ -27,7 +36,6 @@
canvas {
touch-action: none;
user-select: none;
// following props improve blurriness at certain devicePixelRatios.
// AFAIK it doesn't affect export (in fact, export seems sharp either way).
@ -39,13 +47,13 @@
z-index: var(--zIndex-canvas);
}
&.Appearance_dark {
&.theme--dark {
// The percentage is inspired by
// https://material.io/design/color/dark-theme.html#properties, which
// recommends surface color of #121212, 93% yields #111111 for #FFF
canvas {
filter: var(--appearance-filter);
filter: var(--theme-filter);
}
}
@ -214,7 +222,8 @@
align-items: center;
svg {
width: 36px;
height: 18px;
height: 14px;
padding: 2px;
opacity: 0.6;
}
&.active svg {
@ -445,6 +454,14 @@
fill: $oc-gray-6;
bottom: 14px;
width: 1.5rem;
padding: 0;
margin: 0;
background: none;
color: var(--icon-fill-color);
&:hover {
background: none;
}
:root[dir="ltr"] & {
right: 14px;

View File

@ -1,7 +1,8 @@
@import "open-color/open-color.scss";
@import "./variables.module.scss";
:root {
--appearance-filter: none;
.excalidraw {
--theme-filter: none;
--button-destructive-bg-color: #{$oc-red-1};
--button-destructive-color: #{$oc-red-9};
--button-gray-1: #{$oc-gray-2};
@ -33,19 +34,17 @@
--shadow-island: 0 1px 5px #{transparentize($oc-black, 0.85)};
--space-factor: 0.25rem;
--text-primary-color: #{$oc-gray-8};
}
.excalidraw {
&.Appearance_dark {
&.theme--dark {
background: $oc-black;
&.Appearance_dark-background-none {
&.theme--dark-background-none {
background: none;
}
}
&.Appearance_dark {
--appearance-filter: invert(93%) hue-rotate(180deg);
&.theme--dark {
--theme-filter: #{$theme-filter};
--button-destructive-bg-color: #5a0000;
--button-destructive-color: #{$oc-red-3};
--button-gray-1: #363636;

View File

@ -2,7 +2,9 @@
// keep up to date with is-mobile.tsx
$is-mobile-query: "(max-width: 600px), (max-height: 500px) and (max-width: 1000px)";
$theme-filter: "invert(93%) hue-rotate(180deg)";
:export {
isMobileQuery: unquote($is-mobile-query);
themeFilter: unquote($theme-filter);
}

View File

@ -1,12 +1,13 @@
import { cleanAppStateForExport } from "../appState";
import { MIME_TYPES } from "../constants";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
import { clearElementsForExport } from "../element";
import { CanvasError } from "../errors";
import { t } from "../i18n";
import { calculateScrollCenter } from "../scene";
import { AppState } from "../types";
import { isValidExcalidrawData } from "./json";
import { restore } from "./restore";
import { ImportedDataState, LibraryData } from "./types";
import { LibraryData } from "./types";
const parseFileContents = async (blob: Blob | File) => {
let contents: string;
@ -85,15 +86,15 @@ export const loadFromBlob = async (
) => {
const contents = await parseFileContents(blob);
try {
const data: ImportedDataState = JSON.parse(contents);
if (data.type !== "excalidraw") {
const data = JSON.parse(contents);
if (!isValidExcalidrawData(data)) {
throw new Error(t("alerts.couldNotLoadInvalidFile"));
}
const result = restore(
{
elements: clearElementsForExport(data.elements || []),
appState: {
appearance: localAppState?.appearance,
theme: localAppState?.theme,
fileHandle:
blob.handle &&
["application/json", MIME_TYPES.excalidraw].includes(
@ -120,7 +121,7 @@ export const loadFromBlob = async (
export const loadLibraryFromBlob = async (blob: Blob) => {
const contents = await parseFileContents(blob);
const data: LibraryData = JSON.parse(contents);
if (data.type !== "excalidrawlib") {
if (data.type !== EXPORT_DATA_TYPES.excalidrawLibrary) {
throw new Error(t("alerts.couldNotLoadInvalidFile"));
}
return data;

View File

@ -2,7 +2,7 @@ import decodePng from "png-chunks-extract";
import tEXt from "png-chunk-text";
import encodePng from "png-chunks-encode";
import { stringToBase64, encode, decode, base64ToString } from "./encode";
import { MIME_TYPES } from "../constants";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
// -----------------------------------------------------------------------------
// PNG
@ -67,7 +67,10 @@ export const decodePngMetadata = async (blob: Blob) => {
const encodedData = JSON.parse(metadata.text);
if (!("encoded" in encodedData)) {
// legacy, un-encoded scene JSON
if ("type" in encodedData && encodedData.type === "excalidraw") {
if (
"type" in encodedData &&
encodedData.type === EXPORT_DATA_TYPES.excalidraw
) {
return metadata.text;
}
throw new Error("FAILED");
@ -115,7 +118,10 @@ export const decodeSvgMetadata = async ({ svg }: { svg: string }) => {
const encodedData = JSON.parse(json);
if (!("encoded" in encodedData)) {
// legacy, un-encoded scene JSON
if ("type" in encodedData && encodedData.type === "excalidraw") {
if (
"type" in encodedData &&
encodedData.type === EXPORT_DATA_TYPES.excalidraw
) {
return json;
}
throw new Error("FAILED");

View File

@ -41,6 +41,7 @@ export const exportCanvas = async (
if (type === "svg" || type === "clipboard-svg") {
const tempSvg = exportToSvg(elements, {
exportBackground,
exportWithDarkMode: appState.exportWithDarkMode,
viewBackgroundColor,
exportPadding,
scale,

View File

@ -1,11 +1,12 @@
import { fileOpen, fileSave } from "browser-fs-access";
import { cleanAppStateForExport } from "../appState";
import { MIME_TYPES } from "../constants";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
import { clearElementsForExport } from "../element";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { loadFromBlob } from "./blob";
import { Library } from "./library";
import { ImportedDataState } from "./types";
export const serializeAsJSON = (
elements: readonly ExcalidrawElement[],
@ -13,7 +14,7 @@ export const serializeAsJSON = (
): string =>
JSON.stringify(
{
type: "excalidraw",
type: EXPORT_DATA_TYPES.excalidraw,
version: 2,
source: window.location.origin,
elements: clearElementsForExport(elements),
@ -29,13 +30,13 @@ export const saveAsJSON = async (
) => {
const serialized = serializeAsJSON(elements, appState);
const blob = new Blob([serialized], {
type: "application/json",
type: MIME_TYPES.excalidraw,
});
const fileHandle = await fileSave(
blob,
{
fileName: appState.name,
fileName: `${appState.name}.excalidraw`,
description: "Excalidraw file",
extensions: [".excalidraw"],
},
@ -47,17 +48,39 @@ export const saveAsJSON = async (
export const loadFromJSON = async (localAppState: AppState) => {
const blob = await fileOpen({
description: "Excalidraw files",
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
/*
extensions: [".json", ".excalidraw", ".png", ".svg"],
mimeTypes: ["application/json", "image/png", "image/svg+xml"],
mimeTypes: [
MIME_TYPES.excalidraw,
"application/json",
"image/png",
"image/svg+xml",
],
*/
});
return loadFromBlob(blob, localAppState);
};
export const isValidExcalidrawData = (data?: {
type?: any;
elements?: any;
appState?: any;
}): data is ImportedDataState => {
return (
data?.type === EXPORT_DATA_TYPES.excalidraw &&
(!data.elements ||
(Array.isArray(data.elements) &&
(!data.appState || typeof data.appState === "object")))
);
};
export const isValidLibrary = (json: any) => {
return (
typeof json === "object" &&
json &&
json.type === "excalidrawlib" &&
json.type === EXPORT_DATA_TYPES.excalidrawLibrary &&
json.version === 1
);
};
@ -66,7 +89,7 @@ export const saveLibraryAsJSON = async () => {
const library = await Library.loadLibrary();
const serialized = JSON.stringify(
{
type: "excalidrawlib",
type: EXPORT_DATA_TYPES.excalidrawLibrary,
version: 1,
library,
},
@ -87,8 +110,11 @@ export const saveLibraryAsJSON = async () => {
export const importLibraryFromJSON = async () => {
const blob = await fileOpen({
description: "Excalidraw library files",
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
/*
extensions: [".json", ".excalidrawlib"],
mimeTypes: ["application/json"],
*/
});
Library.importLibrary(blob);
};

View File

@ -15,6 +15,7 @@ export interface ImportedDataState {
source?: string;
elements?: DataState["elements"] | null;
appState?: Partial<DataState["appState"]> | null;
scrollToContent?: boolean;
}
export interface LibraryData {

View File

@ -182,9 +182,9 @@ const bindLinearElement = (
} as PointBinding,
});
mutateElement(hoveredElement, {
boundElementIds: [
...new Set([...(hoveredElement.boundElementIds ?? []), linearElement.id]),
],
boundElementIds: Array.from(
new Set([...(hoveredElement.boundElementIds ?? []), linearElement.id]),
),
});
};

View File

@ -87,9 +87,30 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
export const newElementWith = <TElement extends ExcalidrawElement>(
element: TElement,
updates: ElementUpdate<TElement>,
): TElement => ({
...element,
...updates,
version: element.version + 1,
versionNonce: randomInteger(),
});
): TElement => {
let didChange = false;
for (const key in updates) {
const value = (updates as any)[key];
if (typeof value !== "undefined") {
if (
(element as any)[key] === value &&
// if object, always update in case its deep prop was mutated
(typeof value !== "object" || value === null || key === "groupIds")
) {
continue;
}
didChange = true;
}
}
if (!didChange) {
return element;
}
return {
...element,
...updates,
version: element.version + 1,
versionNonce: randomInteger(),
};
};

View File

@ -21,7 +21,6 @@ import {
import { isLinearElement, isTextElement } from "./typeChecks";
import { mutateElement } from "./mutateElement";
import { getPerfectElementSize } from "./sizeHelpers";
import { getCursorForResizingElement } from "./resizeTest";
import { measureText, getFontString } from "../utils";
import { updateBoundElements } from "./binding";
import {
@ -105,13 +104,6 @@ export const transformElements = (
);
}
// update cursor
// FIXME it is not very nice to have this here
document.documentElement.style.cursor = getCursorForResizingElement({
element,
transformHandleType,
});
return true;
} else if (selectedElements.length > 1) {
if (transformHandleType === "rotation") {

View File

@ -21,14 +21,18 @@ const getTransform = (
height: number,
angle: number,
appState: AppState,
maxWidth: number,
) => {
const { zoom, offsetTop, offsetLeft } = appState;
const degree = (180 * angle) / Math.PI;
// offsets must be multiplied by 2 to account for the division by 2 of
// the whole expression afterwards
return `translate(${((width - offsetLeft * 2) * (zoom.value - 1)) / 2}px, ${
((height - offsetTop * 2) * (zoom.value - 1)) / 2
}px) scale(${zoom.value}) rotate(${degree}deg)`;
let translateX = ((width - offsetLeft * 2) * (zoom.value - 1)) / 2;
const translateY = ((height - offsetTop * 2) * (zoom.value - 1)) / 2;
if (width > maxWidth && zoom.value !== 1) {
translateX = (maxWidth / 2) * (zoom.value - 1);
}
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
};
export const textWysiwyg = ({
@ -43,7 +47,7 @@ export const textWysiwyg = ({
id: ExcalidrawElement["id"];
appState: AppState;
onChange?: (text: string) => void;
onSubmit: (text: string) => void;
onSubmit: (data: { text: string; viaKeyboard: boolean }) => void;
getViewportCoords: (x: number, y: number) => [number, number];
element: ExcalidrawElement;
canvas: HTMLCanvasElement | null;
@ -61,6 +65,15 @@ export const textWysiwyg = ({
const lines = updatedElement.text.replace(/\r\n?/g, "\n").split("\n");
const lineHeight = updatedElement.height / lines.length;
const maxWidth =
(appState.offsetLeft + appState.width - viewportX - 8) /
appState.zoom.value -
// margin-right of parent if any
Number(
getComputedStyle(
document.querySelector(".excalidraw")!.parentNode as Element,
).marginRight.slice(0, -2),
);
Object.assign(editable.style, {
font: getFontString(updatedElement),
@ -75,11 +88,13 @@ export const textWysiwyg = ({
updatedElement.height,
angle,
appState,
maxWidth,
),
textAlign,
color: updatedElement.strokeColor,
opacity: updatedElement.opacity / 100,
filter: "var(--appearance-filter)",
filter: "var(--theme-filter)",
maxWidth: `${maxWidth}px`,
});
}
};
@ -93,7 +108,7 @@ export const textWysiwyg = ({
editable.wrap = "off";
Object.assign(editable.style, {
position: "fixed",
position: "absolute",
display: "inline-block",
minHeight: "1em",
backfaceVisibility: "hidden",
@ -121,12 +136,14 @@ export const textWysiwyg = ({
editable.onkeydown = (event) => {
if (event.key === KEYS.ESCAPE) {
event.preventDefault();
submittedViaKeyboard = true;
handleSubmit();
} else if (event.key === KEYS.ENTER && event[KEYS.CTRL_OR_CMD]) {
event.preventDefault();
if (event.isComposing || event.keyCode === 229) {
return;
}
submittedViaKeyboard = true;
handleSubmit();
} else if (event.key === KEYS.ENTER && !event.altKey) {
event.stopPropagation();
@ -138,8 +155,14 @@ export const textWysiwyg = ({
event.stopPropagation();
};
// using a state variable instead of passing it to the handleSubmit callback
// so that we don't need to create separate a callback for event handlers
let submittedViaKeyboard = false;
const handleSubmit = () => {
onSubmit(normalizeText(editable.value));
onSubmit({
text: normalizeText(editable.value),
viaKeyboard: submittedViaKeyboard,
});
cleanup();
};
@ -160,7 +183,7 @@ export const textWysiwyg = ({
window.removeEventListener("resize", updateWysiwygStyle);
window.removeEventListener("wheel", stopEvent, true);
window.removeEventListener("pointerdown", onPointerDown);
window.removeEventListener("pointerup", rebindBlur);
window.removeEventListener("pointerup", bindBlurEvent);
window.removeEventListener("blur", handleSubmit);
unbindUpdate();
@ -168,10 +191,12 @@ export const textWysiwyg = ({
editable.remove();
};
const rebindBlur = () => {
window.removeEventListener("pointerup", rebindBlur);
// deferred to guard against focus traps on various UIs that steal focus
// upon pointerUp
const bindBlurEvent = () => {
window.removeEventListener("pointerup", bindBlurEvent);
// Deferred so that the pointerdown that initiates the wysiwyg doesn't
// trigger the blur on ensuing pointerup.
// Also to handle cases such as picking a color which would trigger a blur
// in that same tick.
setTimeout(() => {
editable.onblur = handleSubmit;
// case: clicking on the same property → no change → no update → no focus
@ -182,12 +207,13 @@ export const textWysiwyg = ({
// prevent blur when changing properties from the menu
const onPointerDown = (event: MouseEvent) => {
if (
event.target instanceof HTMLElement &&
(event.target instanceof HTMLElement ||
event.target instanceof SVGElement) &&
event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) &&
!isWritableElement(event.target)
) {
editable.onblur = null;
window.addEventListener("pointerup", rebindBlur);
window.addEventListener("pointerup", bindBlurEvent);
// handle edge-case where pointerup doesn't fire e.g. due to user
// alt-tabbing away
window.addEventListener("blur", handleSubmit);
@ -200,9 +226,14 @@ export const textWysiwyg = ({
editable.focus();
});
// ---------------------------------------------------------------------------
let isDestroyed = false;
editable.onblur = handleSubmit;
// select on init (focusing is done separately inside the bindBlurEvent()
// because we need it to happen *after* the blur event from `pointerdown`)
editable.select();
bindBlurEvent();
// reposition wysiwyg in case of canvas is resized. Using ResizeObserver
// is preferred so we catch changes from host, where window may not resize.
@ -224,6 +255,4 @@ export const textWysiwyg = ({
document
.querySelector(".excalidraw-textEditorContainer")!
.appendChild(editable);
editable.focus();
editable.select();
};

View File

@ -40,6 +40,7 @@ import { createInverseContext } from "../../createInverseContext";
import { t } from "../../i18n";
import { UserIdleState } from "./types";
import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants";
import { trackEvent } from "../../analytics";
interface CollabState {
modalIsShown: boolean;
@ -188,6 +189,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
};
openPortal = async () => {
trackEvent("share", "room creation");
return this.initializeSocketClient(null);
};
@ -196,6 +198,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
if (window.confirm(t("alerts.collabStopOverridePrompt"))) {
window.history.pushState({}, APP_NAME, window.location.origin);
this.destroySocketClient();
trackEvent("share", "room closed");
}
};
@ -256,6 +259,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
if (elements) {
scenePromise.resolve({
elements,
scrollToContent: true,
});
}
} catch (error) {
@ -307,7 +311,10 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
init: true,
});
// noop if already resolved via init from firebase
scenePromise.resolve({ elements: reconciledElements });
scenePromise.resolve({
elements: reconciledElements,
scrollToContent: true,
});
}
break;
}
@ -441,15 +448,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
private handleRemoteSceneUpdate = (
elements: ReconciledElements,
{
init = false,
initFromSnapshot = false,
}: { init?: boolean; initFromSnapshot?: boolean } = {},
{ init = false }: { init?: boolean } = {},
) => {
if (init || initFromSnapshot) {
this.excalidrawAPI.setScrollToCenter(elements);
}
this.excalidrawAPI.updateScene({
elements,
commitToHistory: !!init,

View File

@ -10,6 +10,7 @@ import { getSyncableElements } from "../../packages/excalidraw/index";
import { ExcalidrawElement } from "../../element/types";
import { BROADCAST, SCENE } from "../app_constants";
import { UserIdleState } from "./types";
import { trackEvent } from "../../analytics";
class Portal {
collab: CollabWrapper;
@ -32,6 +33,7 @@ class Portal {
this.socket.on("init-room", () => {
if (this.socket) {
this.socket.emit("join-room", this.roomId);
trackEvent("share", "room joined");
}
});
this.socket.on("new-user", async (_socketId: string) => {

View File

@ -1,10 +1,32 @@
import React, { useRef } from "react";
import { copyTextToSystemClipboard } from "../../clipboard";
import { Dialog } from "../../components/Dialog";
import { clipboard, start, stop } from "../../components/icons";
import {
clipboard,
start,
stop,
share,
shareIOS,
shareWindows,
} from "../../components/icons";
import { ToolButton } from "../../components/ToolButton";
import { t } from "../../i18n";
import "./RoomDialog.scss";
import Stack from "../../components/Stack";
const getShareIcon = () => {
const navigator = window.navigator as any;
const isAppleBrowser = /Apple/.test(navigator.vendor);
const isWindowsBrowser = navigator.appVersion.indexOf("Win") !== -1;
if (isAppleBrowser) {
return shareIOS;
} else if (isWindowsBrowser) {
return shareWindows;
}
return share;
};
const RoomDialog = ({
handleClose,
@ -36,6 +58,18 @@ const RoomDialog = ({
}
};
const shareRoomLink = async () => {
try {
await navigator.share({
title: t("roomDialog.shareTitle"),
text: t("roomDialog.shareTitle"),
url: activeRoomLink,
});
} catch (error) {
// Just ignore.
}
};
const selectInput = (event: React.MouseEvent<HTMLInputElement>) => {
if (event.target !== document.activeElement) {
event.preventDefault();
@ -68,13 +102,24 @@ const RoomDialog = ({
<p>{t("roomDialog.desc_inProgressIntro")}</p>
<p>{t("roomDialog.desc_shareLink")}</p>
<div className="RoomDialog-linkContainer">
<ToolButton
type="button"
icon={clipboard}
title={t("labels.copy")}
aria-label={t("labels.copy")}
onClick={copyRoomLink}
/>
<Stack.Row gap={2}>
{"share" in navigator ? (
<ToolButton
type="button"
icon={getShareIcon()}
title={t("labels.share")}
aria-label={t("labels.share")}
onClick={shareRoomLink}
/>
) : null}
<ToolButton
type="button"
icon={clipboard}
title={t("labels.copy")}
aria-label={t("labels.copy")}
onClick={copyRoomLink}
/>
</Stack.Row>
<input
value={activeRoomLink}
readOnly={true}

View File

@ -80,8 +80,10 @@ export type SocketUpdateData = SocketUpdateDataSource[keyof SocketUpdateDataSour
_brand: "socketUpdateData";
};
const IV_LENGTH_BYTES = 12; // 96 bits
export const createIV = () => {
const arr = new Uint8Array(12);
const arr = new Uint8Array(IV_LENGTH_BYTES);
return window.crypto.getRandomValues(arr);
};
@ -175,6 +177,22 @@ export const getImportedKey = (key: string, usage: KeyUsage) =>
[usage],
);
const decryptImported = async (
iv: ArrayBuffer,
encrypted: ArrayBuffer,
privateKey: string,
): Promise<ArrayBuffer> => {
const key = await getImportedKey(privateKey, "decrypt");
return window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv,
},
key,
encrypted,
);
};
const importFromBackend = async (
id: string | null,
privateKey?: string | null,
@ -183,6 +201,7 @@ const importFromBackend = async (
const response = await fetch(
privateKey ? `${BACKEND_V2_GET}${id}` : `${BACKEND_GET}${id}.json`,
);
if (!response.ok) {
window.alert(t("alerts.importBackendFailed"));
return {};
@ -190,16 +209,19 @@ const importFromBackend = async (
let data: ImportedDataState;
if (privateKey) {
const buffer = await response.arrayBuffer();
const key = await getImportedKey(privateKey, "decrypt");
const iv = new Uint8Array(12);
const decrypted = await window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv,
},
key,
buffer,
);
let decrypted: ArrayBuffer;
try {
// Buffer should contain both the IV (fixed length) and encrypted data
const iv = buffer.slice(0, IV_LENGTH_BYTES);
const encrypted = buffer.slice(IV_LENGTH_BYTES, buffer.byteLength);
decrypted = await decryptImported(iv, encrypted, privateKey);
} catch (error) {
// Fixed IV (old format, backward compatibility)
const fixedIv = new Uint8Array(IV_LENGTH_BYTES);
decrypted = await decryptImported(fixedIv, buffer, privateKey);
}
// We need to convert the decrypted array buffer to a string
const string = new window.TextDecoder("utf-8").decode(
new Uint8Array(decrypted) as any,
@ -263,9 +285,8 @@ export const exportToBackend = async (
true, // extractable
["encrypt", "decrypt"],
);
// The iv is set to 0. We are never going to reuse the same key so we don't
// need to have an iv. (I hope that's correct...)
const iv = new Uint8Array(12);
const iv = createIV();
// We use symmetric encryption. AES-GCM is the recommended algorithm and
// includes checks that the ciphertext has not been modified by an attacker.
const encrypted = await window.crypto.subtle.encrypt(
@ -276,6 +297,11 @@ export const exportToBackend = async (
key,
encoded,
);
// Concatenate IV with encrypted data (IV does not have to be secret).
const payloadBlob = new Blob([iv.buffer, encrypted]);
const payload = await new Response(payloadBlob).arrayBuffer();
// We use jwk encoding to be able to extract just the base64 encoded key.
// We will hardcode the rest of the attributes when importing back the key.
const exportedKey = await window.crypto.subtle.exportKey("jwk", key);
@ -283,7 +309,7 @@ export const exportToBackend = async (
try {
const response = await fetch(BACKEND_V2_POST, {
method: "POST",
body: encrypted,
body: payload,
});
const json = await response.json();
if (json.id) {

View File

@ -13,7 +13,8 @@ import { ExcalidrawImperativeAPI } from "../components/App";
import { ErrorDialog } from "../components/ErrorDialog";
import { TopErrorBoundary } from "../components/TopErrorBoundary";
import { APP_NAME, EVENT, TITLE_TIMEOUT, VERSION_TIMEOUT } from "../constants";
import { ImportedDataState } from "../data/types";
import { loadFromBlob } from "../data/blob";
import { DataState, ImportedDataState } from "../data/types";
import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
@ -69,16 +70,21 @@ const initializeScene = async (opts: {
}): Promise<ImportedDataState | null> => {
const searchParams = new URLSearchParams(window.location.search);
const id = searchParams.get("id");
const jsonMatch = window.location.hash.match(
const jsonBackendMatch = window.location.hash.match(
/^#json=([0-9]+),([a-zA-Z0-9_-]+)$/,
);
const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/);
const initialData = importFromLocalStorage();
let scene = await loadScene(null, null, initialData);
let scene: DataState & { scrollToContent?: boolean } = await loadScene(
null,
null,
initialData,
);
let roomLinkData = getCollaborationLinkData(window.location.href);
const isExternalScene = !!(id || jsonMatch || roomLinkData);
const isExternalScene = !!(id || jsonBackendMatch || roomLinkData);
if (isExternalScene) {
if (
// don't prompt if scene is empty
@ -91,9 +97,14 @@ const initializeScene = async (opts: {
// Backwards compatibility with legacy url format
if (id) {
scene = await loadScene(id, null, initialData);
} else if (jsonMatch) {
scene = await loadScene(jsonMatch[1], jsonMatch[2], initialData);
} else if (jsonBackendMatch) {
scene = await loadScene(
jsonBackendMatch[1],
jsonBackendMatch[2],
initialData,
);
}
scene.scrollToContent = true;
if (!roomLinkData) {
window.history.replaceState({}, APP_NAME, window.location.origin);
}
@ -114,7 +125,28 @@ const initializeScene = async (opts: {
roomLinkData = null;
window.history.replaceState({}, APP_NAME, window.location.origin);
}
} else if (externalUrlMatch) {
window.history.replaceState({}, APP_NAME, window.location.origin);
const url = externalUrlMatch[1];
try {
const request = await fetch(window.decodeURIComponent(url));
const data = await loadFromBlob(await request.blob(), null);
if (
!scene.elements.length ||
window.confirm(t("alerts.loadSceneOverridePrompt"))
) {
return data;
}
} catch (error) {
return {
appState: {
errorMessage: t("alerts.invalidSceneUrl"),
},
};
}
}
if (roomLinkData) {
return opts.collabAPI.initializeSocketClient(roomLinkData);
} else if (scene) {

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