Compare commits
372 Commits
grid
...
gcp-portal
Author | SHA1 | Date | |
---|---|---|---|
7988cdfb60 | |||
40656c70d1 | |||
c5b4b04d6b | |||
1ad212677b | |||
32427c355c | |||
402a812159 | |||
0480753581 | |||
f7e17a28fa | |||
78f3a92dd1 | |||
c8743a8c02 | |||
127c1be6ad | |||
86bf2d697d | |||
7ee8de0a46 | |||
981f327b48 | |||
eeea8406c9 | |||
0f249e3b26 | |||
2c7c80bd75 | |||
94ad8eaa19 | |||
13d9374cde | |||
efb6d0825b | |||
80a61db72f | |||
9a13dd8836 | |||
cf6a5ff16b | |||
fa8c7abf50 | |||
c3ecbcb3ab | |||
de99484a1f | |||
add1785ace | |||
0e3eb3cc63 | |||
c1379c3c10 | |||
70c36e196c | |||
9757f8e2e5 | |||
052b73d95b | |||
e90e56452f | |||
edc62c550a | |||
84a1863233 | |||
1f295955d0 | |||
8f45aa2924 | |||
b4c0bc6ace | |||
8fdc25ce10 | |||
efc2db3af1 | |||
fa186c69cb | |||
f2adda46e5 | |||
0786b472cc | |||
15ae64eb7a | |||
f3b9bd7c1d | |||
eae25329f0 | |||
99de8e79e2 | |||
adcfaf34de | |||
04ae836874 | |||
f6ffa7ed45 | |||
2964d36d9a | |||
b9e70ec666 | |||
f1daff2437 | |||
91c8b6ecbf | |||
47c26cd4cf | |||
3780a155f4 | |||
6252b22b42 | |||
bb612fd768 | |||
1bb3d71d22 | |||
3912e87b1d | |||
72f1134878 | |||
6e629383ea | |||
612e71e38b | |||
00ac804dd8 | |||
1f7f07fbfb | |||
beffc290fd | |||
9b58efd363 | |||
23b672484b | |||
3a0a638a0d | |||
862c065e33 | |||
420703ba50 | |||
4d6ef81c83 | |||
88b980a8e0 | |||
d85c1955b4 | |||
a27e599798 | |||
d25ed7bf88 | |||
28601b88bd | |||
1edfcc08d8 | |||
030814c254 | |||
eb24e8ffe4 | |||
07e71a8071 | |||
bf97414530 | |||
f8c3c431da | |||
ef792ad906 | |||
06a945aded | |||
0d2f72f87e | |||
3d1cbf444d | |||
c77c9ce65a | |||
f295ba98c5 | |||
91eb8834e8 | |||
418589e7ad | |||
bd63dcbcc5 | |||
785a944ac2 | |||
32acde500e | |||
053353841a | |||
286642ffcf | |||
7090938ec1 | |||
6233bc52e3 | |||
7f8ee06710 | |||
3e200634e0 | |||
701b02d6df | |||
535b2b682e | |||
1a4a736d94 | |||
bb207ef861 | |||
2fcbf8658f | |||
6a8c6e7f47 | |||
f962503425 | |||
02fdc506ee | |||
2ba6088e97 | |||
4e421e6e9e | |||
d213dbb42d | |||
a5779dd5d8 | |||
628b4c1eec | |||
31333e597b | |||
872b340f1b | |||
83e8167adf | |||
baea88942c | |||
ae35037a3b | |||
4f31ae1e4b | |||
704ee30ae6 | |||
176baef9c2 | |||
3558f07fe0 | |||
fd24c74ab1 | |||
ae6892501d | |||
1e6adaf0b5 | |||
7ccd38a37f | |||
e6ce9e0ea7 | |||
c0e05445b1 | |||
b44531d94a | |||
464c2cc05e | |||
bd13c2ed48 | |||
2e58aaae66 | |||
d4c14d484c | |||
06d1871640 | |||
e7a59335e4 | |||
0dbef18044 | |||
e16c2d592f | |||
1f4cf4610f | |||
1bee959660 | |||
f609a4ac3a | |||
eee9d1bc16 | |||
ecdc2582e9 | |||
245e13a884 | |||
8a322b22ed | |||
3f784d76fc | |||
ba5afe9139 | |||
9e6f351672 | |||
fcd10a6a43 | |||
3bc18f6aed | |||
7c5481b877 | |||
d17464fbaa | |||
baf9da2b83 | |||
4bfcf105a5 | |||
74e82d0d7c | |||
9dd2257932 | |||
9c0f832a41 | |||
6cafb6bb90 | |||
e6cd97c4f2 | |||
ba9b65b051 | |||
830fb64a25 | |||
5b343a9d46 | |||
f8beb305de | |||
4b4eecbd27 | |||
e8bd910b9b | |||
a9a3e1bca5 | |||
2a922dd477 | |||
07dab85ebf | |||
d2ce4a7523 | |||
dc73f3a9eb | |||
d100f38750 | |||
503500cc74 | |||
1a828a43d9 | |||
d88884466b | |||
d3d470ac3d | |||
54df521a78 | |||
c5557b5cc1 | |||
51875fd627 | |||
b2ba61bbcf | |||
3b7f62c9a0 | |||
aeafb81479 | |||
dfe81bf6b2 | |||
1d332d597a | |||
b5fc8757a4 | |||
ecbd5ba55d | |||
6967d8c985 | |||
4b253c7362 | |||
4fdddb518a | |||
0b2e4dd60b | |||
f162512988 | |||
2f7154cdf2 | |||
5ab0ce5a33 | |||
073f4032f3 | |||
73cba59d2d | |||
4085071347 | |||
8a63187d4f | |||
1f7c75de63 | |||
9c51ba6067 | |||
bf50c9cae7 | |||
1801048763 | |||
414deea084 | |||
979d28d5c6 | |||
1f8b7e417f | |||
8c968cd13e | |||
f798000006 | |||
7ff3a71179 | |||
6c0804d4c3 | |||
ae8e7aca16 | |||
842b185aa6 | |||
f59387471e | |||
60eb709eb3 | |||
02539bbb89 | |||
77ae5d4605 | |||
bdd4f69bf6 | |||
87ca829490 | |||
a31a7fd766 | |||
e70f02063f | |||
769f727bd4 | |||
bc994fcbe2 | |||
ac5e058222 | |||
d61970cdac | |||
489d4b7469 | |||
d88de08872 | |||
42882e2a93 | |||
f6374e5bde | |||
aac9d4e837 | |||
d9103b8b24 | |||
8b56346011 | |||
e63a0ec5be | |||
86222662f2 | |||
066560311b | |||
d6ca981f7a | |||
f7f98d9dda | |||
1a67642fd1 | |||
6aa22bada8 | |||
00209ef9c3 | |||
b79ef0d428 | |||
dc25fe06d0 | |||
1e17c1967b | |||
23a8891e0e | |||
6c81a32d62 | |||
f0f5430313 | |||
e18e945cd3 | |||
bd0c6e63ff | |||
dbae33e4f8 | |||
0f4a053759 | |||
1837147c55 | |||
15f698dc21 | |||
ce507b0a0b | |||
02598c6163 | |||
3e2890bd21 | |||
210649f383 | |||
f8087e01c8 | |||
e2522645f7 | |||
d46a9166be | |||
675da16ca4 | |||
2b1b62d8f2 | |||
627c56ef1c | |||
0d0fe32485 | |||
6576b9442e | |||
ee4cb2d4a9 | |||
cdcc91faa5 | |||
9093341dc1 | |||
1973ae9444 | |||
10cd6a24b0 | |||
6abf4f52ff | |||
4624ec2bd6 | |||
e8685c5236 | |||
6e9df2bae7 | |||
ed0bec41dc | |||
d4e12a2962 | |||
978e85a33b | |||
b5e26ba81f | |||
4392a4644a | |||
b1c8c538ee | |||
f6492895df | |||
e75f5f20e7 | |||
0a0be839b9 | |||
03f6d9c783 | |||
df745c1098 | |||
b888f7e7ba | |||
3010253f72 | |||
0815f3282e | |||
9dd58288de | |||
c2e2bb495c | |||
a31cfe1f07 | |||
d3367bfe12 | |||
70791dfa7b | |||
d63ec678db | |||
26acebcdb6 | |||
9dc930b447 | |||
6e767fc949 | |||
49bd683401 | |||
3922ee8c11 | |||
8c2bc94336 | |||
fb4d97ef78 | |||
a8e28afbad | |||
68347ba476 | |||
ce2c341910 | |||
07cc858926 | |||
a7a2936f7c | |||
e72ff6be66 | |||
0ea29675df | |||
da2ad4f37c | |||
33a7cf0d3f | |||
f8e890df7b | |||
12337a54a3 | |||
7a9ed2cfa1 | |||
553bf2956f | |||
1e16a6e5bd | |||
e26f374ca6 | |||
c799b28a0e | |||
ee703206b0 | |||
543c624405 | |||
0bf6830373 | |||
af79461f41 | |||
fd699c0447 | |||
6a16caf13c | |||
511eb62228 | |||
04c46fc01a | |||
49e792649d | |||
4e1caf2417 | |||
034f2e470b | |||
adcd28f348 | |||
62f1ed74f1 | |||
8fa4273969 | |||
672068ce7e | |||
f1fc308a5d | |||
001880ba88 | |||
3a130cb102 | |||
e682cf9bf6 | |||
2a169924d0 | |||
eb6e75b806 | |||
38857b9e9d | |||
342289f261 | |||
095d7de618 | |||
f57d52028a | |||
60557df23a | |||
bafbe9bbc8 | |||
eb71e571e0 | |||
b608ab74cc | |||
a13c4f72f5 | |||
629341da4d | |||
778e4b08af | |||
e16266ce5d | |||
b6708fb73f | |||
cdffed285d | |||
3aa01ad272 | |||
4acdc47ef0 | |||
ade2565f49 | |||
c35d983fef | |||
69878167c2 | |||
eb1f717d35 | |||
8e9af5c51b | |||
afe0c760f6 | |||
a231cefac0 | |||
cb4c9d16fc | |||
7366f089ba | |||
7c3513b9df | |||
aef3644c93 | |||
0cf58adb4c | |||
3aab81bc35 | |||
3b0fb1562d | |||
0488b7b5c6 | |||
b8d13c98b5 | |||
6f82a88b79 | |||
022f349dc6 | |||
c1e2146d78 | |||
8091ac6c08 | |||
bc414ccaaf | |||
0cf5f1ac1f | |||
e9cb7ee77c | |||
86c036505b |
@ -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
@ -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://excalidraw-socket.herokuapp.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"}'
|
||||
|
@ -1 +1 @@
|
||||
REACT_APP_INCLUDE_GTAG=true
|
||||
REACT_APP_GOOGLE_ANALYTICS_ID=UA-387204-13
|
||||
|
@ -4,3 +4,4 @@ package-lock.json
|
||||
.vscode/
|
||||
firebase/
|
||||
dist/
|
||||
public/workbox
|
||||
|
@ -1,40 +1,6 @@
|
||||
{
|
||||
"extends": ["prettier", "react-app"],
|
||||
"plugins": ["prettier"],
|
||||
"extends": ["@excalidraw/eslint-config", "react-app"],
|
||||
"rules": {
|
||||
"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-unused-vars": "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
@ -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 |
BIN
.github/assets/logo.png
vendored
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 20 KiB |
9
.github/assets/sentry.svg
vendored
Normal 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
@ -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 |
9
.github/dependabot.yml
vendored
@ -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:
|
||||
|
3
.github/workflows/build-docker.yml
vendored
@ -8,7 +8,6 @@ on:
|
||||
jobs:
|
||||
build-docker:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
- run: docker build -t excalidraw .
|
||||
|
24
.github/workflows/build-packages.yml
vendored
@ -7,27 +7,23 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
packages:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Setup Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Node.js 14.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 12.x
|
||||
|
||||
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
|
||||
|
17
.github/workflows/cancel.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
name: Cancel previous runs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
cancel:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
steps:
|
||||
- uses: styfle/cancel-workflow-action@0.6.0
|
||||
with:
|
||||
workflow_id: 400555, 400556, 905313, 1451724, 1710116, 3185001, 3438604
|
||||
access_token: ${{ secrets.GITHUB_TOKEN }}
|
24
.github/workflows/lint.yml
vendored
@ -1,28 +1,22 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
- name: Setup Node.js 14.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 12.x
|
||||
node-version: 14.x
|
||||
|
||||
- name: Install and lint
|
||||
run: |
|
||||
npm ci
|
||||
npm run test:other
|
||||
npm run test:code
|
||||
npm run test:typecheck
|
||||
env:
|
||||
CI: true
|
||||
yarn --frozen-lockfile
|
||||
yarn test:other
|
||||
yarn test:code
|
||||
yarn test:typecheck
|
||||
|
16
.github/workflows/locales-coverage.yml
vendored
@ -3,7 +3,7 @@ name: Build locales coverage
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "l10n_master"
|
||||
- l10n_master
|
||||
|
||||
jobs:
|
||||
locales:
|
||||
@ -14,18 +14,18 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
||||
|
||||
- name: Setup Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
- name: Setup Node.js 14.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 12.x
|
||||
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 'Kostas Bariotis'
|
||||
git config --global user.email 'konmpar@gmail.com'
|
||||
git config --global user.name 'Excalidraw Bot'
|
||||
git config --global user.email 'bot@excalidraw.com'
|
||||
git add src/locales/percentages.json
|
||||
git commit -am "Auto commit: Calculate translation coverage"
|
||||
git push
|
||||
@ -43,5 +43,5 @@ jobs:
|
||||
uses: kt3k/update-pr-description@v1.0.1
|
||||
with:
|
||||
pr_body: ${{ steps.getCommentBody.outputs.body }}
|
||||
pr_title: "chore: New Crowdin updates"
|
||||
pr_title: "chore: Update translations from Crowdin"
|
||||
github_token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
||||
|
6
.github/workflows/semantic-pr-title.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: "Semantic PR title"
|
||||
name: Semantic PR title
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
@ -8,9 +8,9 @@ on:
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
main:
|
||||
semantic:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v2.1.0
|
||||
- uses: amannn/action-semantic-pull-request@v3.0.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
20
.github/workflows/sentry-production.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: New Sentry Production Release
|
||||
name: New Sentry production release
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -6,27 +6,23 @@ on:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
release:
|
||||
sentry:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1.0.0
|
||||
|
||||
- name: Setup Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Node.js 14.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 12.x
|
||||
|
||||
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)
|
||||
|
23
.github/workflows/test.yml
vendored
@ -1,26 +1,17 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Setup Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Node.js 14.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 12.x
|
||||
|
||||
node-version: 14.x
|
||||
- name: Install and test
|
||||
run: |
|
||||
npm ci
|
||||
npm run test:app
|
||||
env:
|
||||
CI: true
|
||||
yarn --frozen-lockfile
|
||||
yarn test:app
|
||||
|
2
.gitignore
vendored
@ -16,7 +16,7 @@ firebase
|
||||
logs
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
package-lock.json
|
||||
static
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
yarn.lock
|
||||
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"trailingComma": "all"
|
||||
}
|
@ -5,11 +5,10 @@
|
||||
### 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:
|
||||
> To keep `master` branch pointing to remote repository and make pull requests from branches on your fork. To do this, run:
|
||||
>
|
||||
> ```sh
|
||||
> git remote add upstream https://github.com/excalidraw/excalidraw.git
|
||||
@ -20,8 +19,45 @@
|
||||
### Option 2 - CodeSandbox
|
||||
|
||||
1. Go to https://codesandbox.io/s/github/excalidraw/excalidraw
|
||||
1. Connect your Github account
|
||||
1. Connect your GitHub account
|
||||
1. Go to Git tab on left side
|
||||
1. Tap on `Fork Sandbox`
|
||||
1. Write your code
|
||||
1. Commit and PR automatically
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
Don't worry if you get any of the below wrong, or if you don't know how. We'll gladly help out.
|
||||
|
||||
### Title
|
||||
|
||||
Make sure the title starts with a semantic prefix:
|
||||
|
||||
- **feat**: A new feature
|
||||
- **fix**: A bug fix
|
||||
- **docs**: Documentation only changes
|
||||
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
- **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||
- **perf**: A code change that improves performance
|
||||
- **test**: Adding missing tests or correcting existing tests
|
||||
- **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
|
||||
- **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
|
||||
- **chore**: Other changes that don't modify src or test files
|
||||
- **revert**: Reverts a previous commit
|
||||
|
||||
### Changelog
|
||||
|
||||
Add a brief description of your pull request to the changelog located here: [`src/packages/excalidraw/CHANGELOG.md`](src/packages/excalidraw/CHANGELOG.md)
|
||||
|
||||
Notes:
|
||||
|
||||
- Make sure to prepend to the section corresponding with the semantic prefix you selected in the title
|
||||
- Link to your pull request - this will require updating the CHANGELOG _after_ creating the pull request
|
||||
|
||||
### Testing
|
||||
|
||||
Once you submit your pull request it will automatically be tested. Be sure to check the results of the test and fix any issues that arise.
|
||||
|
||||
It's also a good idea to consider if your change should include additional tests. This is highly recommended for new features or bug-fixes. For example, it's good practice to create a test for each bug you fix which ensures that we don't regress the code in the future.
|
||||
|
||||
Finally - always manually test your changes using the convenient staging environment deployed for each pull request. As much as local development attempts to replicate production, there can still be subtle differences in behavior. For larger features consider testing your change in multiple browsers as well.
|
||||
|
@ -2,14 +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 REACT_APP_INCLUDE_GTAG=false
|
||||
ARG NODE_ENV=production
|
||||
|
||||
COPY . .
|
||||
RUN npm run build:app:docker
|
||||
RUN yarn build:app:docker
|
||||
|
||||
FROM nginx:1.17-alpine
|
||||
|
||||
|
148
README.md
@ -1,8 +1,8 @@
|
||||
<div align="center" style="display:flex;flex-direction:column;">
|
||||
<a href="https://excalidraw.com">
|
||||
<img src="./public/og-image.png" alt="Excalidraw logo: Sketch handrawn like diagrams." />
|
||||
<img width="540" src="./public/og-image-sm.png" alt="Excalidraw logo: Sketch handrawn like diagrams." />
|
||||
</a>
|
||||
<h3>Virtual whiteboard for sketching hand-drawn like diagrams.</h3>
|
||||
<h3>Virtual whiteboard for sketching hand-drawn like diagrams.<br>Collaborative and end-to-end encrypted.</h3>
|
||||
<p>
|
||||
<a href="https://twitter.com/Excalidraw">
|
||||
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+excalidraw&style=social&logo=twitter">
|
||||
@ -10,34 +10,92 @@
|
||||
<a target="_blank" href="https://crowdin.com/project/excalidraw">
|
||||
<img src="https://badges.crowdin.net/excalidraw/localized.svg">
|
||||
</a>
|
||||
<a target="_blank" href="https://hub.docker.com/r/excalidraw/excalidraw">
|
||||
<img src="https://img.shields.io/docker/pulls/excalidraw/excalidraw">
|
||||
</a>
|
||||
</p>
|
||||
<p>Ask questions or hang out on our <a target="_blank" href="https://discord.gg/UexuTaE">discord.gg/UexuTaE</a>.</p>
|
||||
</div>
|
||||
|
||||
## Try it now
|
||||
|
||||
Go to [excalidraw.com](https://excalidraw.com) to start sketching.
|
||||
|
||||
Read our [blog](https://blog.excalidraw.com) and follow the [guides](https://howto.excalidraw.com) to learn more about Excalidraw and how to use it effectively.
|
||||
Read the latest news and updates on our [blog](https://blog.excalidraw.com). A good start is to see all the updates of [One Year of Excalidraw](https://blog.excalidraw.com/one-year-of-excalidraw/).
|
||||
|
||||
## Supporting Excalidraw
|
||||
|
||||
If you like the project, you can become a sponsor at [Open Collective](https://opencollective.com/excalidraw).
|
||||
|
||||
[<img src="https://opencollective.com/excalidraw/tiers/sponsors/0/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/0/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/1/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/1/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/2/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/2/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/3/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/3/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/4/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/4/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/5/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/5/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/6/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/6/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/7/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/7/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/8/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/8/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/9/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/9/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/10/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/10/website)
|
||||
|
||||
<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:
|
||||
|
||||
[](https://vercel.com) [](https://sentry.io) [](https://crowdin.com)
|
||||
|
||||
## Documentation
|
||||
|
||||
### Shortcuts
|
||||
|
||||
You can almost do anything with shortcuts. Click on the help icon on the bottom right corner to see them all.
|
||||
|
||||
### Curved lines and arrows
|
||||
|
||||
Choose line or arrow and click click click instead of drag.
|
||||
|
||||
### Charts
|
||||
|
||||
You can easily create charts by copy pasting data from Excel or just plain comma separated text.
|
||||
|
||||
### Translating
|
||||
|
||||
To translate Excalidraw into other languages, please visit [our Crowdin page](https://crowdin.com/project/excalidraw). To add a new language, [open an issue](https://github.com/excalidraw/excalidraw/issues/new) so we can get things set up on our end first.
|
||||
|
||||
Translations will be available on the app if they exceed a certain threshold of completion (currently 85%).
|
||||
|
||||
### Create a collaboration session manually
|
||||
|
||||
In order to create a session manually, you just need to generate a link of this form:
|
||||
|
||||
```
|
||||
https://excalidraw.com/#room=[0-9a-f]{20},[a-zA-Z0-9_-]{22}
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```
|
||||
https://excalidraw.com/#room=91bd46ae3aa84dff9d20,pfLqgEoY1c2ioq8LmGwsFA
|
||||
```
|
||||
|
||||
The first set of digits is the room. This is visible from the server that’s going to dispatch messages to everyone that knows this number.
|
||||
|
||||
The second set of digits is the encryption key. The Excalidraw server doesn’t know about it. This is what all the participants use to encrypt/decrypt the messages.
|
||||
|
||||
## Shape libraries
|
||||
|
||||
Find a growing list of libraries containing assets for your drawings at [libraries.excalidraw.com](https://libraries.excalidraw.com).
|
||||
|
||||
## Run the code
|
||||
## Embedding Excalidraw in your App?
|
||||
|
||||
Try out [`@excalidraw/excalidraw`](https://www.npmjs.com/package/@excalidraw/excalidraw). This package allows you to easily embed Excalidraw as a React component into your apps.
|
||||
|
||||
## Development
|
||||
|
||||
### Code Sandbox
|
||||
|
||||
- Go to https://codesandbox.io/s/github/excalidraw/excalidraw
|
||||
- You may need to sign in with Github and reload the page
|
||||
- You may need to sign in with GitHub and reload the page
|
||||
- You can start coding instantly, and even send PRs from there!
|
||||
|
||||
### Local Installation
|
||||
|
||||
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
|
||||
@ -46,26 +104,26 @@ 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
|
||||
|
||||
You can use docker-compose to work on excalidraw locally if you don't want to setup a Node.js env.
|
||||
You can use docker-compose to work on Excalidraw locally if you don't want to setup a Node.js env.
|
||||
|
||||
```sh
|
||||
docker-compose up --build -d
|
||||
```
|
||||
|
||||
## Self hosting
|
||||
### Self-hosting
|
||||
|
||||
We publish a Docker image with the Excalidraw client at [excalidraw/excalidraw](https://hub.docker.com/r/excalidraw/excalidraw). You can use it to self host your own client under your own domain, on Kubernetes, AWS ECS, etc.
|
||||
We publish a Docker image with the Excalidraw client at [excalidraw/excalidraw](https://hub.docker.com/r/excalidraw/excalidraw). You can use it to self-host your own client under your own domain, on Kubernetes, AWS ECS, etc.
|
||||
|
||||
```sh
|
||||
docker build -t excalidraw/excalidraw .
|
||||
@ -76,63 +134,17 @@ The Docker image is free of analytics and other tracking libraries.
|
||||
|
||||
**At the moment, self-hosting your own instance doesn't support sharing or collaboration features.**
|
||||
|
||||
We are working towards providing a full-fledged solution for self hosting your own Excalidraw.
|
||||
We are working towards providing a full-fledged solution for self-hosting your own Excalidraw.
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/excalidraw/issues/new) first to discuss what you would like to change.
|
||||
|
||||
## Translating
|
||||
## Notable used tools
|
||||
|
||||
To translate Excalidraw into other languages, please visit [our Crowdin page](https://crowdin.com/project/excalidraw). To add a new language, [open an issue](https://github.com/excalidraw/excalidraw/issues/new) so we can get things set up on our end first.
|
||||
|
||||
Translations will be available on the app if they exceed a certain threshold of completion (currently 85%).
|
||||
|
||||
## Excalidraw is built using these awesome tools
|
||||
|
||||
- [React](https://reactjs.org)
|
||||
- [Create React App](https://github.com/facebook/create-react-app)
|
||||
- [Rough.js](https://roughjs.com)
|
||||
- [TypeScript](https://www.typescriptlang.org)
|
||||
- [Vercel](https://vercel.com)
|
||||
|
||||
And the main source of inspiration for starting the project is the awesome [Zwibbler](https://zwibbler.com/demo/) app.
|
||||
|
||||
## Testimonials
|
||||
|
||||
<a href="https://twitter.com/Lissy_Sykes/status/1213813117177729026"><img width="398" src="https://user-images.githubusercontent.com/197597/71783813-dbf8a600-2fa0-11ea-9c0d-bb3cc45969e6.png"></a>
|
||||
<a href="https://twitter.com/dan_abramov/status/1213762494428262400"><img width="398" src="https://user-images.githubusercontent.com/197597/71783990-4d395880-2fa3-11ea-9ad7-186138db5003.png"></a>
|
||||
|
||||
<a href="https://twitter.com/kyehohenberger/status/1214288572037025792"><img width="423" src="https://user-images.githubusercontent.com/197597/71851802-34f13880-308c-11ea-9416-191099e6349c.png"></a>
|
||||
<a href="https://twitter.com/lucasazzola/status/1215126440330416128"><img width="429" src="https://user-images.githubusercontent.com/197597/72039003-48e99580-3258-11ea-8daa-85dd055f2a82.png">
|
||||
|
||||
<a href="https://twitter.com/jordwalke/status/1214858186789806080"><img width="434" src="https://user-images.githubusercontent.com/197597/72036874-07a1b780-3251-11ea-99e8-6bafd93483a0.png"></a>
|
||||
|
||||
## Contributors
|
||||
|
||||
### Code Contributors
|
||||
|
||||
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
|
||||
<a href="https://github.com/excalidraw/excalidraw/graphs/contributors"><img src="https://opencollective.com/excalidraw/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
### Financial Contributors
|
||||
|
||||
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/excalidraw/contribute)]
|
||||
|
||||
#### Individuals
|
||||
|
||||
<a href="https://opencollective.com/excalidraw"><img src="https://opencollective.com/excalidraw/individuals.svg?width=890"></a>
|
||||
|
||||
#### Organizations
|
||||
|
||||
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/excalidraw/contribute)]
|
||||
|
||||
<a href="https://opencollective.com/excalidraw/organization/0/website"><img src="https://opencollective.com/excalidraw/organization/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/excalidraw/organization/1/website"><img src="https://opencollective.com/excalidraw/organization/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/excalidraw/organization/2/website"><img src="https://opencollective.com/excalidraw/organization/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/excalidraw/organization/3/website"><img src="https://opencollective.com/excalidraw/organization/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/excalidraw/organization/4/website"><img src="https://opencollective.com/excalidraw/organization/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/excalidraw/organization/5/website"><img src="https://opencollective.com/excalidraw/organization/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/excalidraw/organization/6/website"><img src="https://opencollective.com/excalidraw/organization/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/excalidraw/organization/7/website"><img src="https://opencollective.com/excalidraw/organization/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/excalidraw/organization/8/website"><img src="https://opencollective.com/excalidraw/organization/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/excalidraw/organization/9/website"><img src="https://opencollective.com/excalidraw/organization/9/avatar.svg"></a>
|
||||
|
64
analytics.md
@ -1,64 +0,0 @@
|
||||
| Excalidraw | Category | Name | Label | Value |
|
||||
| ----------------------- | -------- | ---------------------------------- | ------------------------------- | --------- |
|
||||
| Shape / Selection | shape | selection, rectangle, diamond, etc | `toolbar` or `shortcut` |
|
||||
| Text on double click | shape | text | `double-click` |
|
||||
| Lock selection | shape | lock | `on` or `off` |
|
||||
| Clear canvas | action | clear canvas |
|
||||
| Zoom in | action | zoom | in | `zoom` |
|
||||
| Zoom out | action | zoom | out | `zoom` |
|
||||
| Zoom fit | action | zoom | fit | `zoom` |
|
||||
| Zoom reset | action | zoom | reset | `zoom` |
|
||||
| Scroll back to content | action | scroll to content |
|
||||
| Load file | io | load | `MIME type` |
|
||||
| Import from URL | io | import |
|
||||
| Save | io | save |
|
||||
| Save as | io | save as |
|
||||
| Export to backend | io | export | backend |
|
||||
| Export as SVG | io | export | `svg` or `clipboard-svg` |
|
||||
| Export to PNG | io | export | `png` or `clipboard-png` |
|
||||
| Canvas color | change | canvas color | `color` |
|
||||
| Background color | change | background color | `color` |
|
||||
| Stroke color | change | stroke color | `color` |
|
||||
| Stroke width | change | stroke | width | `width` |
|
||||
| Stroke style | change | style | `solid` or `dashed` or `dotted` |
|
||||
| Stroke sloppiness | change | stroke | sloppiness | `value` |
|
||||
| Fill | change | fill | `value` |
|
||||
| Edge | change | edge | `value` |
|
||||
| Opacity | change | opacity | value | `opacity` |
|
||||
| Project name | change | title |
|
||||
| Theme | change | theme | `light` or `dark` |
|
||||
| Change language | change | language | `language` |
|
||||
| Send to back | layer | move | `back` |
|
||||
| Send backward | layer | move | `down` |
|
||||
| Bring to front | layer | move | `front` |
|
||||
| Bring forward | layer | move | `up` |
|
||||
| Align left | align | align | `left` |
|
||||
| Align right | align | align | `right` |
|
||||
| Align top | align | align | `top` |
|
||||
| Align bottom | align | align | `bottom` |
|
||||
| Center horizontally | align | horizontally | `center` |
|
||||
| Center vertically | align | vertically | `center` |
|
||||
| Distribute horizontally | align | distribute | `horizontally` |
|
||||
| Distribute vertically | align | distribute | `vertically` |
|
||||
| Start session | share | session start |
|
||||
| Join session | share | session join |
|
||||
| Start end | share | session end |
|
||||
| Copy room link | share | copy link |
|
||||
| Go to collaborator | share | go to collaborator |
|
||||
| Change name | share | name |
|
||||
| Add to library | library | add |
|
||||
| Remove from library | library | remove |
|
||||
| Load library | library | load |
|
||||
| Save library | library | save |
|
||||
| Import library | library | import |
|
||||
| Shortcuts dialog | dialog | shortcuts |
|
||||
| Collaboration dialog | dialog | collaboration |
|
||||
| Export dialog | dialog | export |
|
||||
| Library dialog | dialog | library |
|
||||
| E2EE shield | exit | e2ee shield |
|
||||
| GitHub corner | exit | github |
|
||||
| Excalidraw blog | exit | blog |
|
||||
| Excalidraw guides | exit | guides |
|
||||
| File issues | exit | issues |
|
||||
| First load | load | first load |
|
||||
| Load from stroage | load | storage | size | `bytes` |
|
@ -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:
|
||||
|
26404
package-lock.json
generated
73
package.json
@ -19,23 +19,21 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/browser": "5.29.0",
|
||||
"@sentry/integrations": "5.29.0",
|
||||
"@testing-library/jest-dom": "5.11.6",
|
||||
"@testing-library/react": "11.2.2",
|
||||
"@types/jest": "26.0.19",
|
||||
"@types/nanoid": "2.1.0",
|
||||
"@types/react": "17.0.0",
|
||||
"@types/react-dom": "17.0.0",
|
||||
"@types/socket.io-client": "1.4.34",
|
||||
"browser-nativefs": "0.12.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.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.1",
|
||||
"firebase": "8.2.10",
|
||||
"i18next-browser-languagedetector": "6.0.1",
|
||||
"lodash.throttle": "4.1.1",
|
||||
"nanoid": "2.1.11",
|
||||
"node-sass": "4.14.1",
|
||||
"open-color": "1.7.0",
|
||||
"nanoid": "3.1.21",
|
||||
"open-color": "1.8.0",
|
||||
"pako": "1.0.11",
|
||||
"png-chunk-text": "1.0.0",
|
||||
"png-chunks-encode": "1.0.0",
|
||||
@ -44,26 +42,30 @@
|
||||
"pwacompat": "2.0.17",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1",
|
||||
"react-scripts": "4.0.1",
|
||||
"react-scripts": "4.0.3",
|
||||
"roughjs": "4.3.1",
|
||||
"sass": "1.32.8",
|
||||
"socket.io-client": "2.3.1",
|
||||
"typescript": "4.1.3"
|
||||
"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",
|
||||
"eslint-config-prettier": "7.1.0",
|
||||
"eslint-plugin-prettier": "3.3.0",
|
||||
"firebase-tools": "9.0.1",
|
||||
"husky": "4.3.6",
|
||||
"jest-canvas-mock": "2.3.0",
|
||||
"lint-staged": "10.5.3",
|
||||
"@types/resize-observer-browser": "0.1.5",
|
||||
"eslint-config-prettier": "8.1.0",
|
||||
"eslint-plugin-prettier": "3.3.1",
|
||||
"firebase-tools": "9.6.1",
|
||||
"husky": "4.3.8",
|
||||
"jest-canvas-mock": "2.3.1",
|
||||
"lint-staged": "10.5.4",
|
||||
"pepjs": "0.5.3",
|
||||
"prettier": "2.2.1",
|
||||
"rewire": "5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"homepage": ".",
|
||||
"husky": {
|
||||
@ -73,33 +75,34 @@
|
||||
},
|
||||
"jest": {
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-nativefs)/)"
|
||||
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access)/)"
|
||||
],
|
||||
"resetMocks": false
|
||||
},
|
||||
"name": "excalidraw",
|
||||
"prettier": "@excalidraw/prettier-config",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build-node": "node ./scripts/build-node.js",
|
||||
"build:app:docker": "REACT_APP_INCLUDE_GTAG=false REACT_APP_DISABLE_SENTRY=true react-scripts build",
|
||||
"build:app": "REACT_APP_INCLUDE_GTAG=true REACT_APP_GIT_SHA=$NOW_GITHUB_COMMIT_SHA react-scripts build",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
BIN
public/Virgil.woff2
Normal file
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 4.3 KiB |
@ -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;
|
||||
}
|
||||
|
||||
|
@ -57,9 +57,10 @@
|
||||
|
||||
<!-- Excalidraw version -->
|
||||
<meta name="version" content="{version}" />
|
||||
|
||||
<link
|
||||
rel="preload"
|
||||
href="FG_Virgil.woff2"
|
||||
href="Virgil.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
@ -85,11 +86,15 @@
|
||||
/>
|
||||
|
||||
<link rel="stylesheet" href="fonts.css" type="text/css" />
|
||||
|
||||
<% if (process.env.REACT_APP_INCLUDE_GTAG === 'true') { %>
|
||||
<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
|
||||
src="https://www.googletagmanager.com/gtag/js?id=UA-387204-13"
|
||||
src="https://www.googletagmanager.com/gtag/js?id=%REACT_APP_GOOGLE_ANALYTICS_ID%"
|
||||
></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
@ -97,7 +102,7 @@
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag("js", new Date());
|
||||
gtag("config", "UA-387204-13");
|
||||
gtag("config", "%REACT_APP_GOOGLE_ANALYTICS_ID%");
|
||||
</script>
|
||||
<% } %>
|
||||
|
||||
@ -109,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;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 2.6 KiB |
@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
BIN
public/og-image-sm.png
Normal file
After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 70 KiB |
2
public/workbox/workbox-background-sync.prod.js
Normal 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
|
2
public/workbox/workbox-broadcast-update.prod.js
Normal 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
|
2
public/workbox/workbox-cacheable-response.prod.js
Normal 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
|
2
public/workbox/workbox-core.prod.js
Normal file
2
public/workbox/workbox-expiration.prod.js
Normal 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
|
2
public/workbox/workbox-navigation-preload.prod.js
Normal 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
|
2
public/workbox/workbox-offline-ga.prod.js
Normal 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
|
2
public/workbox/workbox-precaching.prod.js
Normal 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
|
2
public/workbox/workbox-range-requests.prod.js
Normal 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
|
2
public/workbox/workbox-routing.prod.js
Normal 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
|
2
public/workbox/workbox-strategies.prod.js
Normal 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
|
2
public/workbox/workbox-streams.prod.js
Normal 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
|
2
public/workbox/workbox-sw.js
Normal 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
|
2
public/workbox/workbox-window.prod.es5.mjs
Normal 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
|
2
public/workbox/workbox-window.prod.mjs
Normal 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
|
2
public/workbox/workbox-window.prod.umd.js
Normal 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
|
@ -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
|
||||
|
||||
|
@ -5,23 +5,40 @@ const path = require("path");
|
||||
const versionFile = path.join("build", "version.json");
|
||||
const indexFile = path.join("build", "index.html");
|
||||
|
||||
const zero = (digit) => `0${digit}`.slice(-2);
|
||||
const versionDate = (date) => date.toISOString().replace(".000", "");
|
||||
|
||||
const versionDate = (date) => {
|
||||
const date_ = `${date.getFullYear()}-${zero(date.getMonth() + 1)}-${zero(
|
||||
date.getDate(),
|
||||
)}`;
|
||||
const time = `${zero(date.getHours())}-${zero(date.getMinutes())}-${zero(
|
||||
date.getSeconds(),
|
||||
)}`;
|
||||
return `${date_}-${time}`;
|
||||
const commitHash = () => {
|
||||
try {
|
||||
return require("child_process")
|
||||
.execSync("git rev-parse --short HEAD")
|
||||
.toString()
|
||||
.trim();
|
||||
} catch {
|
||||
return "none";
|
||||
}
|
||||
};
|
||||
|
||||
const now = new Date();
|
||||
const commitDate = (hash) => {
|
||||
try {
|
||||
const unix = require("child_process")
|
||||
.execSync(`git show -s --format=%ct ${hash}`)
|
||||
.toString()
|
||||
.trim();
|
||||
const date = new Date(parseInt(unix) * 1000);
|
||||
return versionDate(date);
|
||||
} catch {
|
||||
return versionDate(new Date());
|
||||
}
|
||||
};
|
||||
|
||||
const getFullVersion = () => {
|
||||
const hash = commitHash();
|
||||
return `${commitDate(hash)}-${hash}`;
|
||||
};
|
||||
|
||||
const data = JSON.stringify(
|
||||
{
|
||||
version: versionDate(now),
|
||||
version: getFullVersion(),
|
||||
},
|
||||
undefined,
|
||||
2,
|
||||
@ -34,7 +51,7 @@ fs.readFile(indexFile, "utf8", (error, data) => {
|
||||
if (error) {
|
||||
return console.error(error);
|
||||
}
|
||||
const result = data.replace(/{version}/g, versionDate(now));
|
||||
const result = data.replace(/{version}/g, getFullVersion());
|
||||
|
||||
fs.writeFile(indexFile, result, "utf8", (error) => {
|
||||
if (error) {
|
||||
|
@ -4,26 +4,30 @@ const THRESSHOLD = 85;
|
||||
|
||||
const crowdinMap = {
|
||||
"ar-SA": "en-ar",
|
||||
"el-GR": "en-el",
|
||||
"fi-FI": "en-fi",
|
||||
"ja-JP": "en-ja",
|
||||
"bg-BG": "en-bg",
|
||||
"ca-ES": "en-ca",
|
||||
"de-DE": "en-de",
|
||||
"el-GR": "en-el",
|
||||
"es-ES": "en-es",
|
||||
"fa-IR": "en-fa",
|
||||
"fi-FI": "en-fi",
|
||||
"fr-FR": "en-fr",
|
||||
"he-IL": "en-he",
|
||||
"hi-IN": "en-hi",
|
||||
"hu-HU": "en-hu",
|
||||
"id-ID": "en-id",
|
||||
"it-IT": "en-it",
|
||||
"ja-JP": "en-ja",
|
||||
"kab-KAB": "en-kab",
|
||||
"ko-KR": "en-ko",
|
||||
"my-MM": "en-my",
|
||||
"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",
|
||||
"pt-PT": "en-pt",
|
||||
"ro-RO": "en-ro",
|
||||
"ru-RU": "en-ru",
|
||||
@ -38,7 +42,7 @@ const crowdinMap = {
|
||||
const flags = {
|
||||
"ar-SA": "🇸🇦",
|
||||
"bg-BG": "🇧🇬",
|
||||
"ca-ES": "🇪🇸",
|
||||
"ca-ES": "🏳",
|
||||
"de-DE": "🇩🇪",
|
||||
"el-GR": "🇬🇷",
|
||||
"es-ES": "🇪🇸",
|
||||
@ -51,12 +55,16 @@ const flags = {
|
||||
"id-ID": "🇮🇩",
|
||||
"it-IT": "🇮🇹",
|
||||
"ja-JP": "🇯🇵",
|
||||
"kab-KAB": "🏳",
|
||||
"ko-KR": "🇰🇷",
|
||||
"my-MM": "🇲🇲",
|
||||
"nb-NO": "🇳🇴",
|
||||
"nl-NL": "🇳🇱",
|
||||
"nn-NO": "🇳🇴",
|
||||
"oc-FR": "🏳",
|
||||
"pa-IN": "🇮🇳",
|
||||
"pl-PL": "🇵🇱",
|
||||
"pt-BR": "🇧🇷",
|
||||
"pt-PT": "🇵🇹",
|
||||
"ro-RO": "🇷🇴",
|
||||
"ru-RU": "🇷🇺",
|
||||
@ -71,7 +79,7 @@ const flags = {
|
||||
const languages = {
|
||||
"ar-SA": "العربية",
|
||||
"bg-BG": "Български",
|
||||
"ca-ES": "Catalan",
|
||||
"ca-ES": "Català",
|
||||
"de-DE": "Deutsch",
|
||||
"el-GR": "Ελληνικά",
|
||||
"es-ES": "Español",
|
||||
@ -84,12 +92,16 @@ const languages = {
|
||||
"id-ID": "Bahasa Indonesia",
|
||||
"it-IT": "Italiano",
|
||||
"ja-JP": "日本語",
|
||||
"kab-KAB": "Taqbaylit",
|
||||
"ko-KR": "한국어",
|
||||
"my-MM": "Burmese",
|
||||
"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",
|
||||
"pt-PT": "Português",
|
||||
"ro-RO": "Română",
|
||||
"ru-RU": "Русский",
|
||||
@ -114,16 +126,14 @@ const boldIf = (text, condition) => (condition ? `**${text}**` : text);
|
||||
|
||||
const printHeader = () => {
|
||||
let result = "| | Flag | Locale | % |\n";
|
||||
result += "| --: | :--: | -- | --: |";
|
||||
result += "| :--: | :--: | -- | :--: |";
|
||||
return result;
|
||||
};
|
||||
|
||||
const printRow = (id, locale, coverage) => {
|
||||
const isOver = coverage > THRESSHOLD;
|
||||
let result = `| ${boldIf(id, isOver)} | `;
|
||||
|
||||
const isOver = coverage >= THRESSHOLD;
|
||||
let result = `| ${isOver ? id : "..."} | `;
|
||||
result += `${locale in flags ? flags[locale] : ""} | `;
|
||||
|
||||
const language = locale in languages ? languages[locale] : locale;
|
||||
if (locale in crowdinMap && crowdinMap[locale]) {
|
||||
result += `[${boldIf(
|
||||
@ -133,14 +143,12 @@ const printRow = (id, locale, coverage) => {
|
||||
} else {
|
||||
result += `${boldIf(language, isOver)} | `;
|
||||
}
|
||||
result += `${boldIf(coverage, isOver)} |`;
|
||||
result += `${coverage === 100 ? "💯" : boldIf(coverage, isOver)} |`;
|
||||
return result;
|
||||
};
|
||||
|
||||
console.info("## Languages check");
|
||||
console.info("\n\r");
|
||||
console.info(
|
||||
`Our translations for every languages should be at least **${THRESSHOLD}%** to appear on Excalidraw. Join our project in [Crowdin](https://crowdin.com/project/excalidraw) and help us translate it in your language. **Can't find your own?** Open an [issue](https://github.com/excalidraw/excalidraw/issues/new) and we'll add it to the list.`,
|
||||
`Each language must be at least **${THRESSHOLD}%** translated in order to appear on Excalidraw. Join us on [Crowdin](https://crowdin.com/project/excalidraw) and help us translate your own language. **Can't find yours yet?** Open an [issue](https://github.com/excalidraw/excalidraw/issues/new) and we'll add it to the list.`,
|
||||
);
|
||||
console.info("\n\r");
|
||||
console.info(printHeader());
|
||||
|
@ -3,7 +3,6 @@ import { getSelectedElements } from "../scene";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { deepCopyElement } from "../element/newElement";
|
||||
import { Library } from "../data/library";
|
||||
import { EVENT_LIBRARY, trackEvent } from "../analytics";
|
||||
|
||||
export const actionAddToLibrary = register({
|
||||
name: "addToLibrary",
|
||||
@ -16,9 +15,7 @@ export const actionAddToLibrary = register({
|
||||
Library.loadLibrary().then((items) => {
|
||||
Library.saveLibrary([...items, selectedElements.map(deepCopyElement)]);
|
||||
});
|
||||
trackEvent(EVENT_LIBRARY, "add");
|
||||
return false;
|
||||
},
|
||||
contextMenuOrder: 6,
|
||||
contextItemLabel: "labels.addToLibrary",
|
||||
});
|
||||
|
@ -1,7 +1,5 @@
|
||||
import React from "react";
|
||||
import { KEYS } from "../keys";
|
||||
import { t } from "../i18n";
|
||||
import { register } from "./register";
|
||||
import { alignElements, Alignment } from "../align";
|
||||
import {
|
||||
AlignBottomIcon,
|
||||
AlignLeftIcon,
|
||||
@ -10,14 +8,15 @@ import {
|
||||
CenterHorizontallyIcon,
|
||||
CenterVerticallyIcon,
|
||||
} from "../components/icons";
|
||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||
import { getElementMap, getNonDeletedElements } from "../element";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { getElementMap, getNonDeletedElements } from "../element";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { KEYS } from "../keys";
|
||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||
import { AppState } from "../types";
|
||||
import { alignElements, Alignment } from "../align";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import { trackEvent, EVENT_ALIGN } from "../analytics";
|
||||
import { register } from "./register";
|
||||
|
||||
const enableActionGroup = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
@ -44,7 +43,6 @@ const alignSelectedElements = (
|
||||
export const actionAlignTop = register({
|
||||
name: "alignTop",
|
||||
perform: (elements, appState) => {
|
||||
trackEvent(EVENT_ALIGN, "align", "top");
|
||||
return {
|
||||
appState,
|
||||
elements: alignSelectedElements(elements, appState, {
|
||||
@ -60,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",
|
||||
@ -74,7 +72,6 @@ export const actionAlignTop = register({
|
||||
export const actionAlignBottom = register({
|
||||
name: "alignBottom",
|
||||
perform: (elements, appState) => {
|
||||
trackEvent(EVENT_ALIGN, "align", "bottom");
|
||||
return {
|
||||
appState,
|
||||
elements: alignSelectedElements(elements, appState, {
|
||||
@ -90,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",
|
||||
@ -104,7 +101,6 @@ export const actionAlignBottom = register({
|
||||
export const actionAlignLeft = register({
|
||||
name: "alignLeft",
|
||||
perform: (elements, appState) => {
|
||||
trackEvent(EVENT_ALIGN, "align", "left");
|
||||
return {
|
||||
appState,
|
||||
elements: alignSelectedElements(elements, appState, {
|
||||
@ -120,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",
|
||||
@ -134,7 +130,6 @@ export const actionAlignLeft = register({
|
||||
export const actionAlignRight = register({
|
||||
name: "alignRight",
|
||||
perform: (elements, appState) => {
|
||||
trackEvent(EVENT_ALIGN, "align", "right");
|
||||
return {
|
||||
appState,
|
||||
elements: alignSelectedElements(elements, appState, {
|
||||
@ -150,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",
|
||||
@ -164,7 +159,6 @@ export const actionAlignRight = register({
|
||||
export const actionAlignVerticallyCentered = register({
|
||||
name: "alignVerticallyCentered",
|
||||
perform: (elements, appState) => {
|
||||
trackEvent(EVENT_ALIGN, "vertically", "center");
|
||||
return {
|
||||
appState,
|
||||
elements: alignSelectedElements(elements, appState, {
|
||||
@ -178,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")}
|
||||
@ -190,7 +184,6 @@ export const actionAlignVerticallyCentered = register({
|
||||
export const actionAlignHorizontallyCentered = register({
|
||||
name: "alignHorizontallyCentered",
|
||||
perform: (elements, appState) => {
|
||||
trackEvent(EVENT_ALIGN, "horizontally", "center");
|
||||
return {
|
||||
appState,
|
||||
elements: alignSelectedElements(elements, appState, {
|
||||
@ -204,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")}
|
||||
|
@ -1,36 +1,25 @@
|
||||
import React from "react";
|
||||
import { ColorPicker } from "../components/ColorPicker";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import { trash, zoomIn, zoomOut, resetZoom } from "../components/icons";
|
||||
import { ColorPicker } from "../components/ColorPicker";
|
||||
import { resetZoom, trash, zoomIn, zoomOut } from "../components/icons";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { t } from "../i18n";
|
||||
import { getNormalizedZoom, getSelectedElements } from "../scene";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { register } from "./register";
|
||||
import { ZOOM_STEP } from "../constants";
|
||||
import { getCommonBounds, getNonDeletedElements } from "../element";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppState, NormalizedZoomValue } from "../types";
|
||||
import { getCommonBounds } from "../element";
|
||||
import { getNewZoom } from "../scene/zoom";
|
||||
import { t } from "../i18n";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { getNormalizedZoom, getSelectedElements } from "../scene";
|
||||
import { centerScrollOn } from "../scene/scroll";
|
||||
import { EVENT_ACTION, EVENT_CHANGE, trackEvent } from "../analytics";
|
||||
import colors from "../colors";
|
||||
import { getNewZoom } from "../scene/zoom";
|
||||
import { AppState, NormalizedZoomValue } from "../types";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import { register } from "./register";
|
||||
|
||||
export const actionChangeViewBackgroundColor = register({
|
||||
name: "changeViewBackgroundColor",
|
||||
perform: (_, appState, value) => {
|
||||
if (value !== appState.viewBackgroundColor) {
|
||||
trackEvent(
|
||||
EVENT_CHANGE,
|
||||
"canvas color",
|
||||
colors.canvasBackground.includes(value)
|
||||
? `${value} (picker ${colors.canvasBackground.indexOf(value)})`
|
||||
: value,
|
||||
);
|
||||
}
|
||||
return {
|
||||
appState: { ...appState, viewBackgroundColor: value },
|
||||
commitToHistory: true,
|
||||
@ -53,20 +42,20 @@ export const actionChangeViewBackgroundColor = register({
|
||||
export const actionClearCanvas = register({
|
||||
name: "clearCanvas",
|
||||
perform: (elements, appState: AppState) => {
|
||||
trackEvent(EVENT_ACTION, "clear canvas");
|
||||
return {
|
||||
elements: elements.map((element) =>
|
||||
newElementWith(element, { isDeleted: true }),
|
||||
),
|
||||
appState: {
|
||||
...getDefaultAppState(),
|
||||
appearance: appState.appearance,
|
||||
theme: appState.theme,
|
||||
elementLocked: appState.elementLocked,
|
||||
exportBackground: appState.exportBackground,
|
||||
exportEmbedScene: appState.exportEmbedScene,
|
||||
gridSize: appState.gridSize,
|
||||
shouldAddWatermark: appState.shouldAddWatermark,
|
||||
showStats: appState.showStats,
|
||||
pasteDialog: appState.pasteDialog,
|
||||
},
|
||||
commitToHistory: true,
|
||||
};
|
||||
@ -87,8 +76,6 @@ export const actionClearCanvas = register({
|
||||
),
|
||||
});
|
||||
|
||||
const ZOOM_STEP = 0.1;
|
||||
|
||||
export const actionZoomIn = register({
|
||||
name: "zoomIn",
|
||||
perform: (_elements, appState) => {
|
||||
@ -98,7 +85,6 @@ export const actionZoomIn = register({
|
||||
{ left: appState.offsetLeft, top: appState.offsetTop },
|
||||
{ x: appState.width / 2, y: appState.height / 2 },
|
||||
);
|
||||
trackEvent(EVENT_ACTION, "zoom", "in", zoom.value * 100);
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
@ -133,7 +119,6 @@ export const actionZoomOut = register({
|
||||
{ x: appState.width / 2, y: appState.height / 2 },
|
||||
);
|
||||
|
||||
trackEvent(EVENT_ACTION, "zoom", "out", zoom.value * 100);
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
@ -161,7 +146,6 @@ export const actionZoomOut = register({
|
||||
export const actionResetZoom = register({
|
||||
name: "resetZoom",
|
||||
perform: (_elements, appState) => {
|
||||
trackEvent(EVENT_ACTION, "zoom", "reset", 100);
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
@ -234,12 +218,10 @@ const zoomToFitElements = (
|
||||
left: appState.offsetLeft,
|
||||
top: appState.offsetTop,
|
||||
});
|
||||
const action = zoomToSelection ? "selection" : "fit";
|
||||
|
||||
const [x1, y1, x2, y2] = commonBounds;
|
||||
const centerX = (x1 + x2) / 2;
|
||||
const centerY = (y1 + y2) / 2;
|
||||
trackEvent(EVENT_ACTION, "zoom", action, newZoom.value * 100);
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
|
122
src/actions/actionClipboard.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { register } from "./register";
|
||||
import { copyToClipboard } from "../clipboard";
|
||||
import { actionDeleteSelected } from "./actionDeleteSelected";
|
||||
import { getSelectedElements } from "../scene/selection";
|
||||
import { exportCanvas } from "../data/index";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { t } from "../i18n";
|
||||
|
||||
export const actionCopy = register({
|
||||
name: "copy",
|
||||
perform: (elements, appState) => {
|
||||
copyToClipboard(getNonDeletedElements(elements), appState);
|
||||
|
||||
return {
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
contextItemLabel: "labels.copy",
|
||||
// don't supply a shortcut since we handle this conditionally via onCopy event
|
||||
keyTest: undefined,
|
||||
});
|
||||
|
||||
export const actionCut = register({
|
||||
name: "cut",
|
||||
perform: (elements, appState, data, app) => {
|
||||
actionCopy.perform(elements, appState, data, app);
|
||||
return actionDeleteSelected.perform(elements, appState, data, app);
|
||||
},
|
||||
contextItemLabel: "labels.cut",
|
||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.X,
|
||||
});
|
||||
|
||||
export const actionCopyAsSvg = register({
|
||||
name: "copyAsSvg",
|
||||
perform: async (elements, appState, _data, app) => {
|
||||
if (!app.canvas) {
|
||||
return {
|
||||
commitToHistory: false,
|
||||
};
|
||||
}
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
try {
|
||||
await exportCanvas(
|
||||
"clipboard-svg",
|
||||
selectedElements.length
|
||||
? selectedElements
|
||||
: getNonDeletedElements(elements),
|
||||
appState,
|
||||
app.canvas,
|
||||
appState,
|
||||
);
|
||||
return {
|
||||
commitToHistory: false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
errorMessage: error.message,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
}
|
||||
},
|
||||
contextItemLabel: "labels.copyAsSvg",
|
||||
});
|
||||
|
||||
export const actionCopyAsPng = register({
|
||||
name: "copyAsPng",
|
||||
perform: async (elements, appState, _data, app) => {
|
||||
if (!app.canvas) {
|
||||
return {
|
||||
commitToHistory: false,
|
||||
};
|
||||
}
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
try {
|
||||
await exportCanvas(
|
||||
"clipboard",
|
||||
selectedElements.length
|
||||
? selectedElements
|
||||
: getNonDeletedElements(elements),
|
||||
appState,
|
||||
app.canvas,
|
||||
appState,
|
||||
);
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
toastMessage: t("toast.copyToClipboardAsPng", {
|
||||
exportSelection: selectedElements.length
|
||||
? t("toast.selection")
|
||||
: t("toast.canvas"),
|
||||
exportColorScheme: appState.exportWithDarkMode
|
||||
? t("buttons.darkMode")
|
||||
: t("buttons.lightMode"),
|
||||
}),
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
errorMessage: error.message,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
}
|
||||
},
|
||||
contextItemLabel: "labels.copyAsPng",
|
||||
keyTest: (event) => event.code === CODES.C && event.altKey && event.shiftKey,
|
||||
});
|
@ -136,7 +136,6 @@ export const actionDeleteSelected = register({
|
||||
};
|
||||
},
|
||||
contextItemLabel: "labels.delete",
|
||||
contextMenuOrder: 999999,
|
||||
keyTest: (event) => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<ToolButton
|
||||
|
@ -1,19 +1,18 @@
|
||||
import React from "react";
|
||||
import { CODES } from "../keys";
|
||||
import { t } from "../i18n";
|
||||
import { register } from "./register";
|
||||
import {
|
||||
DistributeHorizontallyIcon,
|
||||
DistributeVerticallyIcon,
|
||||
} from "../components/icons";
|
||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||
import { getElementMap, getNonDeletedElements } from "../element";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppState } from "../types";
|
||||
import { distributeElements, Distribution } from "../disitrubte";
|
||||
import { getElementMap, getNonDeletedElements } from "../element";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { CODES } from "../keys";
|
||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||
import { AppState } from "../types";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import { EVENT_ALIGN, trackEvent } from "../analytics";
|
||||
import { register } from "./register";
|
||||
|
||||
const enableActionGroup = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
@ -40,7 +39,6 @@ const distributeSelectedElements = (
|
||||
export const distributeHorizontally = register({
|
||||
name: "distributeHorizontally",
|
||||
perform: (elements, appState) => {
|
||||
trackEvent(EVENT_ALIGN, "distribute", "horizontally");
|
||||
return {
|
||||
appState,
|
||||
elements: distributeSelectedElements(elements, appState, {
|
||||
@ -55,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",
|
||||
@ -69,7 +67,6 @@ export const distributeHorizontally = register({
|
||||
export const distributeVertically = register({
|
||||
name: "distributeVertically",
|
||||
perform: (elements, appState) => {
|
||||
trackEvent(EVENT_ALIGN, "distribute", "vertically");
|
||||
return {
|
||||
appState,
|
||||
elements: distributeSelectedElements(elements, appState, {
|
||||
@ -84,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")}
|
||||
|
@ -1,29 +1,32 @@
|
||||
import React from "react";
|
||||
import { EVENT_CHANGE, EVENT_IO, trackEvent } from "../analytics";
|
||||
import { load, save, saveAs } from "../components/icons";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { load, questionCircle, save, saveAs } from "../components/icons";
|
||||
import { ProjectName } from "../components/ProjectName";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import "../components/ToolIcon.scss";
|
||||
import { Tooltip } from "../components/Tooltip";
|
||||
import { questionCircle } from "../components/icons";
|
||||
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 { muteFSAbortError } from "../utils";
|
||||
import { register } from "./register";
|
||||
import "../components/ToolIcon.scss";
|
||||
import { supported } from "browser-fs-access";
|
||||
|
||||
export const actionChangeProjectName = register({
|
||||
name: "changeProjectName",
|
||||
perform: (_elements, appState, value) => {
|
||||
trackEvent(EVENT_CHANGE, "title");
|
||||
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
|
||||
}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@ -98,10 +101,24 @@ export const actionChangeShouldAddWatermark = register({
|
||||
export const actionSaveScene = register({
|
||||
name: "saveScene",
|
||||
perform: async (elements, appState, value) => {
|
||||
const fileHandleExists = !!appState.fileHandle;
|
||||
try {
|
||||
const { fileHandle } = await saveAsJSON(elements, appState);
|
||||
trackEvent(EVENT_IO, "save");
|
||||
return { commitToHistory: false, appState: { ...appState, fileHandle } };
|
||||
return {
|
||||
commitToHistory: false,
|
||||
appState: {
|
||||
...appState,
|
||||
fileHandle,
|
||||
toastMessage: fileHandleExists
|
||||
? fileHandle.name
|
||||
? t("toast.fileSavedToFilename").replace(
|
||||
"{filename}",
|
||||
`"${fileHandle.name}"`,
|
||||
)
|
||||
: t("toast.fileSaved")
|
||||
: null,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (error?.name !== "AbortError") {
|
||||
console.error(error);
|
||||
@ -131,7 +148,6 @@ export const actionSaveAsScene = register({
|
||||
...appState,
|
||||
fileHandle: null,
|
||||
});
|
||||
trackEvent(EVENT_IO, "save as");
|
||||
return { commitToHistory: false, appState: { ...appState, fileHandle } };
|
||||
} catch (error) {
|
||||
if (error?.name !== "AbortError") {
|
||||
@ -149,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)}
|
||||
/>
|
||||
),
|
||||
@ -159,18 +173,29 @@ export const actionSaveAsScene = register({
|
||||
|
||||
export const actionLoadScene = register({
|
||||
name: "loadScene",
|
||||
perform: (
|
||||
elements,
|
||||
appState,
|
||||
{ elements: loadedElements, appState: loadedAppState, error },
|
||||
) => ({
|
||||
elements: loadedElements,
|
||||
appState: {
|
||||
...loadedAppState,
|
||||
errorMessage: error,
|
||||
},
|
||||
commitToHistory: true,
|
||||
}),
|
||||
perform: async (elements, appState) => {
|
||||
try {
|
||||
const {
|
||||
elements: loadedElements,
|
||||
appState: loadedAppState,
|
||||
} = await loadFromJSON(appState);
|
||||
return {
|
||||
elements: loadedElements,
|
||||
appState: loadedAppState,
|
||||
commitToHistory: true,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error?.name === "AbortError") {
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
elements,
|
||||
appState: { ...appState, errorMessage: error.message },
|
||||
commitToHistory: false,
|
||||
};
|
||||
}
|
||||
},
|
||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
|
||||
PanelComponent: ({ updateData, appState }) => (
|
||||
<ToolButton
|
||||
type="button"
|
||||
@ -178,16 +203,35 @@ export const actionLoadScene = register({
|
||||
title={t("buttons.load")}
|
||||
aria-label={t("buttons.load")}
|
||||
showAriaLabel={useIsMobile()}
|
||||
onClick={() => {
|
||||
loadFromJSON(appState)
|
||||
.then(({ elements, appState }) => {
|
||||
updateData({ elements, appState });
|
||||
})
|
||||
.catch(muteFSAbortError)
|
||||
.catch((error) => {
|
||||
updateData({ error: error.message });
|
||||
});
|
||||
}}
|
||||
onClick={updateData}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
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>
|
||||
),
|
||||
});
|
||||
|
@ -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,
|
||||
@ -83,7 +83,7 @@ export const actionFinalize = register({
|
||||
// If the multi point line closes the loop,
|
||||
// set the last point to first point.
|
||||
// This ensures that loop remains closed at different scales.
|
||||
const isLoop = isPathALoop(multiPointElement.points);
|
||||
const isLoop = isPathALoop(multiPointElement.points, appState.zoom.value);
|
||||
if (
|
||||
multiPointElement.type === "line" ||
|
||||
multiPointElement.type === "draw"
|
||||
@ -118,19 +118,23 @@ export const actionFinalize = register({
|
||||
);
|
||||
}
|
||||
|
||||
if (!appState.elementLocked) {
|
||||
if (!appState.elementLocked && appState.elementType !== "draw") {
|
||||
appState.selectedElementIds[multiPointElement.id] = true;
|
||||
}
|
||||
}
|
||||
if (!appState.elementLocked || !multiPointElement) {
|
||||
resetCursor();
|
||||
if (
|
||||
(!appState.elementLocked && appState.elementType !== "draw") ||
|
||||
!multiPointElement
|
||||
) {
|
||||
resetCursor(canvas);
|
||||
}
|
||||
return {
|
||||
elements: newElements,
|
||||
appState: {
|
||||
...appState,
|
||||
elementType:
|
||||
appState.elementLocked && multiPointElement
|
||||
(appState.elementLocked || appState.elementType === "draw") &&
|
||||
multiPointElement
|
||||
? appState.elementType
|
||||
: "selection",
|
||||
draggingElement: null,
|
||||
@ -139,7 +143,9 @@ export const actionFinalize = register({
|
||||
startBoundElement: null,
|
||||
suggestedBindings: [],
|
||||
selectedElementIds:
|
||||
multiPointElement && !appState.elementLocked
|
||||
multiPointElement &&
|
||||
!appState.elementLocked &&
|
||||
appState.elementType !== "draw"
|
||||
? {
|
||||
...appState.selectedElementIds,
|
||||
[multiPointElement.id]: true,
|
||||
|
@ -125,7 +125,6 @@ export const actionGroup = register({
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
contextMenuOrder: 4,
|
||||
contextItemLabel: "labels.group",
|
||||
contextItemPredicate: (elements, appState) =>
|
||||
enableActionGroup(elements, appState),
|
||||
@ -135,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")}
|
||||
@ -174,7 +173,6 @@ export const actionUngroup = register({
|
||||
},
|
||||
keyTest: (event) =>
|
||||
event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.code === CODES.G,
|
||||
contextMenuOrder: 5,
|
||||
contextItemLabel: "labels.ungroup",
|
||||
contextItemPredicate: (elements, appState) =>
|
||||
getSelectedGroupIds(appState).length > 0,
|
||||
@ -183,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")}
|
||||
|
@ -6,7 +6,7 @@ import { t } from "../i18n";
|
||||
import { SceneHistory, HistoryEntry } from "../history";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppState } from "../types";
|
||||
import { KEYS } from "../keys";
|
||||
import { isWindows, KEYS } from "../keys";
|
||||
import { getElementMap } from "../element";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import { fixBindingsAfterDeletion } from "../element/binding";
|
||||
@ -59,16 +59,16 @@ const writeData = (
|
||||
return { commitToHistory };
|
||||
};
|
||||
|
||||
const testUndo = (shift: boolean) => (event: KeyboardEvent) =>
|
||||
event[KEYS.CTRL_OR_CMD] && /z/i.test(event.key) && event.shiftKey === shift;
|
||||
|
||||
type ActionCreator = (history: SceneHistory) => Action;
|
||||
|
||||
export const createUndoAction: ActionCreator = (history) => ({
|
||||
name: "undo",
|
||||
perform: (elements, appState) =>
|
||||
writeData(elements, appState, () => history.undoOnce()),
|
||||
keyTest: testUndo(false),
|
||||
keyTest: (event) =>
|
||||
event[KEYS.CTRL_OR_CMD] &&
|
||||
event.key.toLowerCase() === KEYS.Z &&
|
||||
!event.shiftKey,
|
||||
PanelComponent: ({ updateData }) => (
|
||||
<ToolButton
|
||||
type="button"
|
||||
@ -84,7 +84,11 @@ export const createRedoAction: ActionCreator = (history) => ({
|
||||
name: "redo",
|
||||
perform: (elements, appState) =>
|
||||
writeData(elements, appState, () => history.redoOnce()),
|
||||
keyTest: testUndo(true),
|
||||
keyTest: (event) =>
|
||||
(event[KEYS.CTRL_OR_CMD] &&
|
||||
event.shiftKey &&
|
||||
event.key.toLowerCase() === KEYS.Z) ||
|
||||
(isWindows && event.ctrlKey && !event.shiftKey && event.key === KEYS.Y),
|
||||
PanelComponent: ({ updateData }) => (
|
||||
<ToolButton
|
||||
type="button"
|
||||
|
@ -7,7 +7,6 @@ import { register } from "./register";
|
||||
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { HelpIcon } from "../components/HelpIcon";
|
||||
import { EVENT_DIALOG, trackEvent } from "../analytics";
|
||||
|
||||
export const actionToggleCanvasMenu = register({
|
||||
name: "toggleCanvasMenu",
|
||||
@ -72,17 +71,16 @@ export const actionFullScreen = register({
|
||||
export const actionShortcuts = register({
|
||||
name: "toggleShortcuts",
|
||||
perform: (_elements, appState) => {
|
||||
trackEvent(EVENT_DIALOG, "shortcuts");
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
showShortcutsDialog: true,
|
||||
showHelpDialog: !appState.showHelpDialog,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ updateData }) => (
|
||||
<HelpIcon title={t("shortcutsDialog.title")} onClick={updateData} />
|
||||
<HelpIcon title={t("helpDialog.title")} onClick={updateData} />
|
||||
),
|
||||
keyTest: (event) => event.key === KEYS.QUESTION_MARK,
|
||||
});
|
||||
|
@ -1,16 +1,14 @@
|
||||
import React from "react";
|
||||
import { Avatar } from "../components/Avatar";
|
||||
import { register } from "./register";
|
||||
import { getClientColors, getClientInitials } from "../clients";
|
||||
import { Collaborator } from "../types";
|
||||
import { Avatar } from "../components/Avatar";
|
||||
import { centerScrollOn } from "../scene/scroll";
|
||||
import { EVENT_SHARE, trackEvent } from "../analytics";
|
||||
import { Collaborator } from "../types";
|
||||
import { register } from "./register";
|
||||
|
||||
export const actionGoToCollaborator = register({
|
||||
name: "goToCollaborator",
|
||||
perform: (_elements, appState, value) => {
|
||||
const point = value as Collaborator["pointer"];
|
||||
trackEvent(EVENT_SHARE, "go to collaborator");
|
||||
if (!point) {
|
||||
return { appState, commitToHistory: false };
|
||||
}
|
||||
@ -44,7 +42,7 @@ export const actionGoToCollaborator = register({
|
||||
return null;
|
||||
}
|
||||
|
||||
const { background, stroke } = getClientColors(clientId);
|
||||
const { background, stroke } = getClientColors(clientId, appState);
|
||||
const shortName = getClientInitials(collaborator.username);
|
||||
|
||||
return (
|
||||
|
@ -1,56 +1,62 @@
|
||||
import React from "react";
|
||||
import { getLanguage } from "../i18n";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawTextElement,
|
||||
TextAlign,
|
||||
FontFamily,
|
||||
ExcalidrawLinearElement,
|
||||
Arrowhead,
|
||||
} from "../element/types";
|
||||
import {
|
||||
getCommonAttributeOfSelectedElements,
|
||||
isSomeElementSelected,
|
||||
getTargetElements,
|
||||
canChangeSharpness,
|
||||
canHaveArrowheads,
|
||||
} from "../scene";
|
||||
import { ButtonSelect } from "../components/ButtonSelect";
|
||||
import { AppState } from "../../src/types";
|
||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
||||
import { ColorPicker } from "../components/ColorPicker";
|
||||
import { IconPicker } from "../components/IconPicker";
|
||||
import {
|
||||
isTextElement,
|
||||
redrawTextBoundingBox,
|
||||
getNonDeletedElements,
|
||||
} from "../element";
|
||||
import { isLinearElement, isLinearElementType } from "../element/typeChecks";
|
||||
import { ColorPicker } from "../components/ColorPicker";
|
||||
import { AppState } from "../../src/types";
|
||||
import { t } from "../i18n";
|
||||
import { register } from "./register";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import { DEFAULT_FONT_SIZE, DEFAULT_FONT_FAMILY } from "../constants";
|
||||
import { randomInteger } from "../random";
|
||||
import {
|
||||
FillHachureIcon,
|
||||
FillCrossHatchIcon,
|
||||
FillSolidIcon,
|
||||
StrokeWidthIcon,
|
||||
StrokeStyleSolidIcon,
|
||||
StrokeStyleDashedIcon,
|
||||
StrokeStyleDottedIcon,
|
||||
EdgeSharpIcon,
|
||||
EdgeRoundIcon,
|
||||
SloppinessArchitectIcon,
|
||||
SloppinessArtistIcon,
|
||||
SloppinessCartoonistIcon,
|
||||
ArrowheadArrowIcon,
|
||||
ArrowheadBarIcon,
|
||||
ArrowheadDotIcon,
|
||||
ArrowheadNoneIcon,
|
||||
EdgeRoundIcon,
|
||||
EdgeSharpIcon,
|
||||
FillCrossHatchIcon,
|
||||
FillHachureIcon,
|
||||
FillSolidIcon,
|
||||
SloppinessArchitectIcon,
|
||||
SloppinessArtistIcon,
|
||||
SloppinessCartoonistIcon,
|
||||
StrokeStyleDashedIcon,
|
||||
StrokeStyleDottedIcon,
|
||||
StrokeStyleSolidIcon,
|
||||
StrokeWidthIcon,
|
||||
FontSizeSmallIcon,
|
||||
FontSizeMediumIcon,
|
||||
FontSizeLargeIcon,
|
||||
FontSizeExtraLargeIcon,
|
||||
FontFamilyHandDrawnIcon,
|
||||
FontFamilyNormalIcon,
|
||||
FontFamilyCodeIcon,
|
||||
TextAlignLeftIcon,
|
||||
TextAlignCenterIcon,
|
||||
TextAlignRightIcon,
|
||||
} from "../components/icons";
|
||||
import { EVENT_CHANGE, trackEvent } from "../analytics";
|
||||
import colors from "../colors";
|
||||
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE } from "../constants";
|
||||
import {
|
||||
getNonDeletedElements,
|
||||
isTextElement,
|
||||
redrawTextBoundingBox,
|
||||
} from "../element";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import { isLinearElement, isLinearElementType } from "../element/typeChecks";
|
||||
import {
|
||||
Arrowhead,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawTextElement,
|
||||
FontFamily,
|
||||
TextAlign,
|
||||
} from "../element/types";
|
||||
import { getLanguage, t } from "../i18n";
|
||||
import { randomInteger } from "../random";
|
||||
import {
|
||||
canChangeSharpness,
|
||||
canHaveArrowheads,
|
||||
getCommonAttributeOfSelectedElements,
|
||||
getTargetElements,
|
||||
isSomeElementSelected,
|
||||
} from "../scene";
|
||||
import { register } from "./register";
|
||||
|
||||
const changeProperty = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
@ -92,15 +98,6 @@ const getFormValue = function <T>(
|
||||
export const actionChangeStrokeColor = register({
|
||||
name: "changeStrokeColor",
|
||||
perform: (elements, appState, value) => {
|
||||
if (value !== appState.currentItemStrokeColor) {
|
||||
trackEvent(
|
||||
EVENT_CHANGE,
|
||||
"stroke color",
|
||||
colors.elementStroke.includes(value)
|
||||
? `${value} (picker ${colors.elementStroke.indexOf(value)})`
|
||||
: value,
|
||||
);
|
||||
}
|
||||
return {
|
||||
elements: changeProperty(elements, appState, (el) =>
|
||||
newElementWith(el, {
|
||||
@ -132,16 +129,6 @@ export const actionChangeStrokeColor = register({
|
||||
export const actionChangeBackgroundColor = register({
|
||||
name: "changeBackgroundColor",
|
||||
perform: (elements, appState, value) => {
|
||||
if (value !== appState.currentItemBackgroundColor) {
|
||||
trackEvent(
|
||||
EVENT_CHANGE,
|
||||
"background color",
|
||||
colors.elementBackground.includes(value)
|
||||
? `${value} (picker ${colors.elementBackground.indexOf(value)})`
|
||||
: value,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
elements: changeProperty(elements, appState, (el) =>
|
||||
newElementWith(el, {
|
||||
@ -173,7 +160,6 @@ export const actionChangeBackgroundColor = register({
|
||||
export const actionChangeFillStyle = register({
|
||||
name: "changeFillStyle",
|
||||
perform: (elements, appState, value) => {
|
||||
trackEvent(EVENT_CHANGE, "fill", value);
|
||||
return {
|
||||
elements: changeProperty(elements, appState, (el) =>
|
||||
newElementWith(el, {
|
||||
@ -192,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"
|
||||
@ -223,7 +209,6 @@ export const actionChangeFillStyle = register({
|
||||
export const actionChangeStrokeWidth = register({
|
||||
name: "changeStrokeWidth",
|
||||
perform: (elements, appState, value) => {
|
||||
trackEvent(EVENT_CHANGE, "stroke", "width", value);
|
||||
return {
|
||||
elements: changeProperty(elements, appState, (el) =>
|
||||
newElementWith(el, {
|
||||
@ -243,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(
|
||||
@ -286,7 +256,6 @@ export const actionChangeStrokeWidth = register({
|
||||
export const actionChangeSloppiness = register({
|
||||
name: "changeSloppiness",
|
||||
perform: (elements, appState, value) => {
|
||||
trackEvent(EVENT_CHANGE, "stroke", "sloppiness", value);
|
||||
return {
|
||||
elements: changeProperty(elements, appState, (el) =>
|
||||
newElementWith(el, {
|
||||
@ -307,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(
|
||||
@ -335,7 +304,6 @@ export const actionChangeSloppiness = register({
|
||||
export const actionChangeStrokeStyle = register({
|
||||
name: "changeStrokeStyle",
|
||||
perform: (elements, appState, value) => {
|
||||
trackEvent(EVENT_CHANGE, "style", value);
|
||||
return {
|
||||
elements: changeProperty(elements, appState, (el) =>
|
||||
newElementWith(el, {
|
||||
@ -355,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(
|
||||
@ -383,7 +351,6 @@ export const actionChangeStrokeStyle = register({
|
||||
export const actionChangeOpacity = register({
|
||||
name: "changeOpacity",
|
||||
perform: (elements, appState, value) => {
|
||||
trackEvent(EVENT_CHANGE, "opacity", "value", value);
|
||||
return {
|
||||
elements: changeProperty(elements, appState, (el) =>
|
||||
newElementWith(el, {
|
||||
@ -455,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,
|
||||
@ -498,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(
|
||||
@ -548,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,7 +587,6 @@ export const actionChangeSharpness = register({
|
||||
const shouldUpdateForLinearElements = targetElements.length
|
||||
? targetElements.every(isLinearElement)
|
||||
: isLinearElementType(appState.elementType);
|
||||
trackEvent(EVENT_CHANGE, "edge", value);
|
||||
return {
|
||||
elements: changeProperty(elements, appState, (el) =>
|
||||
newElementWith(el, {
|
||||
@ -608,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(
|
||||
@ -642,12 +648,6 @@ export const actionChangeArrowhead = register({
|
||||
return {
|
||||
elements: changeProperty(elements, appState, (el) => {
|
||||
if (isLinearElement(el)) {
|
||||
trackEvent(
|
||||
EVENT_CHANGE,
|
||||
`arrowhead ${value.position}`,
|
||||
value.type || "none",
|
||||
);
|
||||
|
||||
const { position, type } = value;
|
||||
|
||||
if (position === "start") {
|
||||
@ -687,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",
|
||||
},
|
||||
]}
|
||||
@ -743,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>(
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
redrawTextBoundingBox,
|
||||
} from "../element";
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { t } from "../i18n";
|
||||
import { register } from "./register";
|
||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||
import {
|
||||
@ -23,13 +24,16 @@ export const actionCopyStyles = register({
|
||||
copiedStyles = JSON.stringify(element);
|
||||
}
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
toastMessage: t("toast.copyStyles"),
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
contextItemLabel: "labels.copyStyles",
|
||||
keyTest: (event) =>
|
||||
event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.C,
|
||||
contextMenuOrder: 0,
|
||||
});
|
||||
|
||||
export const actionPasteStyles = register({
|
||||
@ -69,5 +73,4 @@ export const actionPasteStyles = register({
|
||||
contextItemLabel: "labels.pasteStyles",
|
||||
keyTest: (event) =>
|
||||
event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
|
||||
contextMenuOrder: 1,
|
||||
});
|
||||
|
22
src/actions/actionToggleGridMode.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { register } from "./register";
|
||||
import { GRID_SIZE } from "../constants";
|
||||
import { AppState } from "../types";
|
||||
import { trackEvent } from "../analytics";
|
||||
|
||||
export const actionToggleGridMode = register({
|
||||
name: "gridMode",
|
||||
perform(elements, appState) {
|
||||
trackEvent("view", "mode", "grid");
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
gridSize: this.checked!(appState) ? null : GRID_SIZE,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
checked: (appState: AppState) => appState.gridSize !== null,
|
||||
contextItemLabel: "labels.showGrid",
|
||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.QUOTE,
|
||||
});
|
16
src/actions/actionToggleStats.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { register } from "./register";
|
||||
|
||||
export const actionToggleStats = register({
|
||||
name: "stats",
|
||||
perform(elements, appState) {
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
showStats: !this.checked!(appState),
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
checked: (appState) => appState.showStats,
|
||||
contextItemLabel: "stats.title",
|
||||
});
|
22
src/actions/actionToggleViewMode.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { register } from "./register";
|
||||
import { trackEvent } from "../analytics";
|
||||
|
||||
export const actionToggleViewMode = register({
|
||||
name: "viewMode",
|
||||
perform(elements, appState) {
|
||||
trackEvent("view", "mode", "view");
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
viewModeEnabled: !this.checked!(appState),
|
||||
selectedElementIds: {},
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
checked: (appState) => appState.viewModeEnabled,
|
||||
contextItemLabel: "labels.viewMode",
|
||||
keyTest: (event) =>
|
||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.R,
|
||||
});
|
22
src/actions/actionToggleZenMode.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { register } from "./register";
|
||||
import { trackEvent } from "../analytics";
|
||||
|
||||
export const actionToggleZenMode = register({
|
||||
name: "zenMode",
|
||||
perform(elements, appState) {
|
||||
trackEvent("view", "mode", "zen");
|
||||
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
zenModeEnabled: !this.checked!(appState),
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
checked: (appState) => appState.zenModeEnabled,
|
||||
contextItemLabel: "buttons.zenMode",
|
||||
keyTest: (event) =>
|
||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.Z,
|
||||
});
|
@ -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>
|
||||
),
|
||||
});
|
||||
|
@ -65,3 +65,15 @@ export {
|
||||
distributeHorizontally,
|
||||
distributeVertically,
|
||||
} from "./actionDistribute";
|
||||
|
||||
export {
|
||||
actionCopy,
|
||||
actionCut,
|
||||
actionCopyAsPng,
|
||||
actionCopyAsSvg,
|
||||
} from "./actionClipboard";
|
||||
|
||||
export { actionToggleGridMode } from "./actionToggleGridMode";
|
||||
export { actionToggleZenMode } from "./actionToggleZenMode";
|
||||
|
||||
export { actionToggleStats } from "./actionToggleStats";
|
||||
|
@ -3,14 +3,16 @@ import {
|
||||
Action,
|
||||
ActionsManagerInterface,
|
||||
UpdaterFn,
|
||||
ActionFilterFn,
|
||||
ActionName,
|
||||
ActionResult,
|
||||
} from "./types";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppState } from "../types";
|
||||
import { t } from "../i18n";
|
||||
import { ShortcutName } from "./shortcuts";
|
||||
import { AppState, ExcalidrawProps } from "../types";
|
||||
import { MODES } from "../constants";
|
||||
|
||||
// This is the <App> component, but for now we don't care about anything but its
|
||||
// `canvas` state.
|
||||
type App = { canvas: HTMLCanvasElement | null; props: ExcalidrawProps };
|
||||
|
||||
export class ActionManager implements ActionsManagerInterface {
|
||||
actions = {} as ActionsManagerInterface["actions"];
|
||||
@ -18,13 +20,14 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
|
||||
|
||||
getAppState: () => Readonly<AppState>;
|
||||
|
||||
getElementsIncludingDeleted: () => readonly ExcalidrawElement[];
|
||||
app: App;
|
||||
|
||||
constructor(
|
||||
updater: UpdaterFn,
|
||||
getAppState: () => AppState,
|
||||
getElementsIncludingDeleted: () => readonly ExcalidrawElement[],
|
||||
app: App,
|
||||
) {
|
||||
this.updater = (actionResult) => {
|
||||
if (actionResult && "then" in actionResult) {
|
||||
@ -37,6 +40,7 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
};
|
||||
this.getAppState = getAppState;
|
||||
this.getElementsIncludingDeleted = getElementsIncludingDeleted;
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
registerAction(action: Action) {
|
||||
@ -63,6 +67,12 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
if (data.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const { viewModeEnabled } = this.getAppState();
|
||||
if (viewModeEnabled) {
|
||||
if (!Object.values(MODES).includes(data[0].name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
this.updater(
|
||||
@ -70,6 +80,7 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
this.getElementsIncludingDeleted(),
|
||||
this.getAppState(),
|
||||
null,
|
||||
this.app,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
@ -81,43 +92,11 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
this.getElementsIncludingDeleted(),
|
||||
this.getAppState(),
|
||||
null,
|
||||
this.app,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getContextMenuItems(actionFilter: ActionFilterFn = (action) => action) {
|
||||
return Object.values(this.actions)
|
||||
.filter(actionFilter)
|
||||
.filter((action) => "contextItemLabel" in action)
|
||||
.filter((action) =>
|
||||
action.contextItemPredicate
|
||||
? action.contextItemPredicate(
|
||||
this.getElementsIncludingDeleted(),
|
||||
this.getAppState(),
|
||||
)
|
||||
: true,
|
||||
)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(a.contextMenuOrder !== undefined ? a.contextMenuOrder : 999) -
|
||||
(b.contextMenuOrder !== undefined ? b.contextMenuOrder : 999),
|
||||
)
|
||||
.map((action) => ({
|
||||
// take last bit of the label "labels.<shortcutName>"
|
||||
shortcutName: action.contextItemLabel?.split(".").pop() as ShortcutName,
|
||||
label: action.contextItemLabel ? t(action.contextItemLabel) : "",
|
||||
action: () => {
|
||||
this.updater(
|
||||
action.perform(
|
||||
this.getElementsIncludingDeleted(),
|
||||
this.getAppState(),
|
||||
null,
|
||||
),
|
||||
);
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
// Id is an attribute that we can use to pass in data like keys.
|
||||
// This is needed for dynamically generated action components
|
||||
// like the user list. We can use this key to extract more
|
||||
@ -132,6 +111,7 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
this.getElementsIncludingDeleted(),
|
||||
this.getAppState(),
|
||||
formState,
|
||||
this.app,
|
||||
),
|
||||
);
|
||||
};
|
||||
@ -142,6 +122,7 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
appState={this.getAppState()}
|
||||
updateData={updateData}
|
||||
id={id}
|
||||
appProps={this.app.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ export type ShortcutName =
|
||||
| "copyStyles"
|
||||
| "pasteStyles"
|
||||
| "selectAll"
|
||||
| "delete"
|
||||
| "deleteSelectedElements"
|
||||
| "duplicateSelection"
|
||||
| "sendBackward"
|
||||
| "bringForward"
|
||||
@ -20,8 +20,10 @@ export type ShortcutName =
|
||||
| "group"
|
||||
| "ungroup"
|
||||
| "gridMode"
|
||||
| "zenMode"
|
||||
| "stats"
|
||||
| "addToLibrary";
|
||||
| "addToLibrary"
|
||||
| "viewMode";
|
||||
|
||||
const shortcutMap: Record<ShortcutName, string[]> = {
|
||||
cut: [getShortcutKey("CtrlOrCmd+X")],
|
||||
@ -30,10 +32,10 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
||||
copyStyles: [getShortcutKey("CtrlOrCmd+Alt+C")],
|
||||
pasteStyles: [getShortcutKey("CtrlOrCmd+Alt+V")],
|
||||
selectAll: [getShortcutKey("CtrlOrCmd+A")],
|
||||
delete: [getShortcutKey("Del")],
|
||||
deleteSelectedElements: [getShortcutKey("Del")],
|
||||
duplicateSelection: [
|
||||
getShortcutKey("CtrlOrCmd+D"),
|
||||
getShortcutKey(`Alt+${t("shortcutsDialog.drag")}`),
|
||||
getShortcutKey(`Alt+${t("helpDialog.drag")}`),
|
||||
],
|
||||
sendBackward: [getShortcutKey("CtrlOrCmd+[")],
|
||||
bringForward: [getShortcutKey("CtrlOrCmd+]")],
|
||||
@ -52,8 +54,10 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
||||
group: [getShortcutKey("CtrlOrCmd+G")],
|
||||
ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
|
||||
gridMode: [getShortcutKey("CtrlOrCmd+'")],
|
||||
zenMode: [getShortcutKey("Alt+Z")],
|
||||
stats: [],
|
||||
addToLibrary: [],
|
||||
viewMode: [getShortcutKey("Alt+R")],
|
||||
};
|
||||
|
||||
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
||||
|
@ -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 =
|
||||
@ -16,12 +16,18 @@ type ActionFn = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: Readonly<AppState>,
|
||||
formData: any,
|
||||
app: { canvas: HTMLCanvasElement | null },
|
||||
) => ActionResult | Promise<ActionResult>;
|
||||
|
||||
export type UpdaterFn = (res: ActionResult) => void;
|
||||
export type ActionFilterFn = (action: Action) => void;
|
||||
|
||||
export type ActionName =
|
||||
| "copy"
|
||||
| "cut"
|
||||
| "paste"
|
||||
| "copyAsPng"
|
||||
| "copyAsSvg"
|
||||
| "sendBackward"
|
||||
| "bringForward"
|
||||
| "sendToBack"
|
||||
@ -29,6 +35,9 @@ export type ActionName =
|
||||
| "copyStyles"
|
||||
| "selectAll"
|
||||
| "pasteStyles"
|
||||
| "gridMode"
|
||||
| "zenMode"
|
||||
| "stats"
|
||||
| "changeStrokeColor"
|
||||
| "changeBackgroundColor"
|
||||
| "changeFillStyle"
|
||||
@ -75,7 +84,9 @@ export type ActionName =
|
||||
| "alignVerticallyCentered"
|
||||
| "alignHorizontallyCentered"
|
||||
| "distributeHorizontally"
|
||||
| "distributeVertically";
|
||||
| "distributeVertically"
|
||||
| "viewMode"
|
||||
| "exportWithDarkMode";
|
||||
|
||||
export interface Action {
|
||||
name: ActionName;
|
||||
@ -83,6 +94,7 @@ export interface Action {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
appState: AppState;
|
||||
updateData: (formData?: any) => void;
|
||||
appProps: ExcalidrawProps;
|
||||
id?: string;
|
||||
}>;
|
||||
perform: ActionFn;
|
||||
@ -93,19 +105,16 @@ export interface Action {
|
||||
elements: readonly ExcalidrawElement[],
|
||||
) => boolean;
|
||||
contextItemLabel?: string;
|
||||
contextMenuOrder?: number;
|
||||
contextItemPredicate?: (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => boolean;
|
||||
checked?: (appState: Readonly<AppState>) => boolean;
|
||||
}
|
||||
|
||||
export interface ActionsManagerInterface {
|
||||
actions: Record<ActionName, Action>;
|
||||
registerAction: (action: Action) => void;
|
||||
handleKeyDown: (event: KeyboardEvent) => boolean;
|
||||
getContextMenuItems: (
|
||||
actionFilter: ActionFilterFn,
|
||||
) => { label: string; action: () => void }[];
|
||||
renderAction: (name: ActionName) => React.ReactElement | null;
|
||||
}
|
||||
|
@ -1,18 +1,8 @@
|
||||
export const EVENT_ACTION = "action";
|
||||
export const EVENT_ALIGN = "align";
|
||||
export const EVENT_CHANGE = "change";
|
||||
export const EVENT_DIALOG = "dialog";
|
||||
export const EVENT_EXIT = "exit";
|
||||
export const EVENT_IO = "io";
|
||||
export const EVENT_LAYER = "layer";
|
||||
export const EVENT_LIBRARY = "library";
|
||||
export const EVENT_LOAD = "load";
|
||||
export const EVENT_SHAPE = "shape";
|
||||
export const EVENT_SHARE = "share";
|
||||
export const EVENT_MAGIC = "magic";
|
||||
|
||||
export const trackEvent =
|
||||
typeof window !== "undefined" && window.gtag
|
||||
typeof process !== "undefined" &&
|
||||
process.env?.REACT_APP_GOOGLE_ANALYTICS_ID &&
|
||||
typeof window !== "undefined" &&
|
||||
window.gtag
|
||||
? (category: string, name: string, label?: string, value?: number) => {
|
||||
window.gtag("event", name, {
|
||||
event_category: category,
|
||||
@ -20,8 +10,9 @@ export const trackEvent =
|
||||
value,
|
||||
});
|
||||
}
|
||||
: typeof process !== "undefined" && process?.env?.JEST_WORKER_ID
|
||||
: typeof process !== "undefined" && process.env?.JEST_WORKER_ID
|
||||
? (category: string, name: string, label?: string, value?: number) => {}
|
||||
: (category: string, name: string, label?: string, value?: number) => {
|
||||
console.info("Track Event", category, name, label, value);
|
||||
// Uncomment the next line to track locally
|
||||
// console.info("Track Event", category, name, label, value);
|
||||
};
|
||||
|
135
src/appState.ts
@ -1,79 +1,79 @@
|
||||
import oc from "open-color";
|
||||
import { AppState, FlooredNumber, NormalizedZoomValue } from "./types";
|
||||
import { getDateTime } from "./utils";
|
||||
import { t } from "./i18n";
|
||||
import {
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_FONT_FAMILY,
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_TEXT_ALIGN,
|
||||
} from "./constants";
|
||||
import { t } from "./i18n";
|
||||
import { AppState, NormalizedZoomValue } from "./types";
|
||||
import { getDateTime } from "./utils";
|
||||
|
||||
export const getDefaultAppState = (): Omit<
|
||||
AppState,
|
||||
"offsetTop" | "offsetLeft"
|
||||
> => {
|
||||
return {
|
||||
appearance: "light",
|
||||
isLoading: false,
|
||||
errorMessage: null,
|
||||
theme: "light",
|
||||
collaborators: new Map(),
|
||||
currentChartType: "bar",
|
||||
currentItemBackgroundColor: "transparent",
|
||||
currentItemEndArrowhead: "arrow",
|
||||
currentItemFillStyle: "hachure",
|
||||
currentItemFontFamily: DEFAULT_FONT_FAMILY,
|
||||
currentItemFontSize: DEFAULT_FONT_SIZE,
|
||||
currentItemLinearStrokeSharpness: "round",
|
||||
currentItemOpacity: 100,
|
||||
currentItemRoughness: 1,
|
||||
currentItemStartArrowhead: null,
|
||||
currentItemStrokeColor: oc.black,
|
||||
currentItemStrokeSharpness: "sharp",
|
||||
currentItemStrokeStyle: "solid",
|
||||
currentItemStrokeWidth: 1,
|
||||
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
||||
cursorButton: "up",
|
||||
draggingElement: null,
|
||||
resizingElement: null,
|
||||
multiElement: null,
|
||||
editingElement: null,
|
||||
startBoundElement: null,
|
||||
editingGroupId: null,
|
||||
editingLinearElement: null,
|
||||
elementType: "selection",
|
||||
elementLocked: false,
|
||||
elementType: "selection",
|
||||
errorMessage: null,
|
||||
exportBackground: true,
|
||||
exportEmbedScene: false,
|
||||
shouldAddWatermark: false,
|
||||
currentItemStrokeColor: oc.black,
|
||||
currentItemBackgroundColor: "transparent",
|
||||
currentItemFillStyle: "hachure",
|
||||
currentItemStrokeWidth: 1,
|
||||
currentItemStrokeStyle: "solid",
|
||||
currentItemRoughness: 1,
|
||||
currentItemOpacity: 100,
|
||||
currentItemFontSize: DEFAULT_FONT_SIZE,
|
||||
currentItemFontFamily: DEFAULT_FONT_FAMILY,
|
||||
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
||||
currentItemStrokeSharpness: "sharp",
|
||||
currentItemLinearStrokeSharpness: "round",
|
||||
currentItemStartArrowhead: null,
|
||||
currentItemEndArrowhead: "arrow",
|
||||
viewBackgroundColor: oc.white,
|
||||
scrollX: 0 as FlooredNumber,
|
||||
scrollY: 0 as FlooredNumber,
|
||||
cursorX: 0,
|
||||
cursorY: 0,
|
||||
cursorButton: "up",
|
||||
scrolledOutside: false,
|
||||
name: `${t("labels.untitled")}-${getDateTime()}`,
|
||||
exportWithDarkMode: false,
|
||||
fileHandle: null,
|
||||
gridSize: null,
|
||||
height: window.innerHeight,
|
||||
isBindingEnabled: true,
|
||||
isLibraryOpen: false,
|
||||
isLoading: false,
|
||||
isResizing: false,
|
||||
isRotating: false,
|
||||
selectionElement: null,
|
||||
zoom: {
|
||||
value: 1 as NormalizedZoomValue,
|
||||
translation: { x: 0, y: 0 },
|
||||
},
|
||||
openMenu: null,
|
||||
lastPointerDownWith: "mouse",
|
||||
selectedElementIds: {},
|
||||
multiElement: null,
|
||||
name: `${t("labels.untitled")}-${getDateTime()}`,
|
||||
openMenu: null,
|
||||
pasteDialog: { shown: false, data: null },
|
||||
previousSelectedElementIds: {},
|
||||
shouldCacheIgnoreZoom: false,
|
||||
showShortcutsDialog: false,
|
||||
suggestedBindings: [],
|
||||
zenModeEnabled: false,
|
||||
gridSize: null,
|
||||
editingGroupId: null,
|
||||
resizingElement: null,
|
||||
scrolledOutside: false,
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
selectedElementIds: {},
|
||||
selectedGroupIds: {},
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
isLibraryOpen: false,
|
||||
fileHandle: null,
|
||||
collaborators: new Map(),
|
||||
selectionElement: null,
|
||||
shouldAddWatermark: false,
|
||||
shouldCacheIgnoreZoom: false,
|
||||
showHelpDialog: false,
|
||||
showStats: false,
|
||||
startBoundElement: null,
|
||||
suggestedBindings: [],
|
||||
toastMessage: null,
|
||||
viewBackgroundColor: oc.white,
|
||||
width: window.innerWidth,
|
||||
zenModeEnabled: false,
|
||||
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } },
|
||||
viewModeEnabled: false,
|
||||
};
|
||||
};
|
||||
|
||||
@ -92,27 +92,26 @@ 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 },
|
||||
currentItemEndArrowhead: { browser: true, export: false },
|
||||
currentItemFillStyle: { browser: true, export: false },
|
||||
currentItemFontFamily: { browser: true, export: false },
|
||||
currentItemFontSize: { browser: true, export: false },
|
||||
currentItemLinearStrokeSharpness: { browser: true, export: false },
|
||||
currentItemOpacity: { browser: true, export: false },
|
||||
currentItemRoughness: { browser: true, export: false },
|
||||
currentItemStartArrowhead: { browser: true, export: false },
|
||||
currentItemStrokeColor: { browser: true, export: false },
|
||||
currentItemStrokeSharpness: { browser: true, export: false },
|
||||
currentItemStrokeStyle: { browser: true, export: false },
|
||||
currentItemStrokeWidth: { browser: true, export: false },
|
||||
currentItemTextAlign: { browser: true, export: false },
|
||||
currentItemStrokeSharpness: { browser: true, export: false },
|
||||
currentItemLinearStrokeSharpness: { browser: true, export: false },
|
||||
currentItemStartArrowhead: { browser: true, export: false },
|
||||
currentItemEndArrowhead: { browser: true, export: false },
|
||||
cursorButton: { browser: true, export: false },
|
||||
cursorX: { browser: true, export: false },
|
||||
cursorY: { browser: true, export: false },
|
||||
draggingElement: { browser: false, export: false },
|
||||
editingElement: { browser: false, export: false },
|
||||
startBoundElement: { browser: false, export: false },
|
||||
editingGroupId: { browser: true, export: false },
|
||||
editingLinearElement: { browser: false, export: false },
|
||||
elementLocked: { browser: true, export: false },
|
||||
@ -120,6 +119,8 @@ 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 },
|
||||
isBindingEnabled: { browser: false, export: false },
|
||||
@ -130,7 +131,10 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
lastPointerDownWith: { browser: true, export: false },
|
||||
multiElement: { browser: false, export: false },
|
||||
name: { browser: true, export: false },
|
||||
offsetLeft: { browser: false, export: false },
|
||||
offsetTop: { browser: false, export: false },
|
||||
openMenu: { browser: true, export: false },
|
||||
pasteDialog: { browser: false, export: false },
|
||||
previousSelectedElementIds: { browser: true, export: false },
|
||||
resizingElement: { browser: false, export: false },
|
||||
scrolledOutside: { browser: true, export: false },
|
||||
@ -141,17 +145,16 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
selectionElement: { browser: false, export: false },
|
||||
shouldAddWatermark: { browser: true, export: false },
|
||||
shouldCacheIgnoreZoom: { browser: true, export: false },
|
||||
showShortcutsDialog: { browser: false, export: false },
|
||||
showHelpDialog: { browser: false, export: false },
|
||||
showStats: { browser: true, export: false },
|
||||
startBoundElement: { browser: false, export: false },
|
||||
suggestedBindings: { browser: false, export: false },
|
||||
toastMessage: { browser: false, export: false },
|
||||
viewBackgroundColor: { browser: true, export: true },
|
||||
width: { browser: false, export: false },
|
||||
zenModeEnabled: { browser: true, export: false },
|
||||
zoom: { browser: true, export: false },
|
||||
offsetTop: { browser: false, export: false },
|
||||
offsetLeft: { browser: false, export: false },
|
||||
fileHandle: { browser: false, export: false },
|
||||
collaborators: { browser: false, export: false },
|
||||
showStats: { browser: true, export: false },
|
||||
viewModeEnabled: { browser: false, export: false },
|
||||
});
|
||||
|
||||
const _clearAppStateForStorage = <ExportType extends "export" | "browser">(
|
||||
|
426
src/charts.ts
@ -1,13 +1,15 @@
|
||||
import { EVENT_MAGIC, trackEvent } from "./analytics";
|
||||
import colors from "./colors";
|
||||
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE } from "./constants";
|
||||
import { newElement, newTextElement, newLinearElement } from "./element";
|
||||
import { ExcalidrawElement } from "./element/types";
|
||||
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, ENV } from "./constants";
|
||||
import { newElement, newLinearElement, newTextElement } from "./element";
|
||||
import { NonDeletedExcalidrawElement } from "./element/types";
|
||||
import { randomId } from "./random";
|
||||
|
||||
export type ChartElements = readonly NonDeletedExcalidrawElement[];
|
||||
|
||||
const BAR_WIDTH = 32;
|
||||
const BAR_GAP = 12;
|
||||
const BAR_HEIGHT = 256;
|
||||
const GRID_OPACITY = 50;
|
||||
|
||||
export interface Spreadsheet {
|
||||
title: string | null;
|
||||
@ -139,114 +141,48 @@ export const tryParseSpreadsheet = (text: string): ParseSpreadsheetResult => {
|
||||
return transposedResults;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// For the maths behind it https://excalidraw.com/#json=6320864370884608,O_5xfD-Agh32tytHpRJx1g
|
||||
export const renderSpreadsheet = (
|
||||
const bgColors = colors.elementBackground.slice(
|
||||
2,
|
||||
colors.elementBackground.length,
|
||||
);
|
||||
|
||||
// Put all the common properties here so when the whole chart is selected
|
||||
// the properties dialog shows the correct selected values
|
||||
const commonProps = {
|
||||
fillStyle: "hachure",
|
||||
fontFamily: DEFAULT_FONT_FAMILY,
|
||||
fontSize: DEFAULT_FONT_SIZE,
|
||||
opacity: 100,
|
||||
roughness: 1,
|
||||
strokeColor: colors.elementStroke[0],
|
||||
strokeSharpness: "sharp",
|
||||
strokeStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
verticalAlign: "middle",
|
||||
} as const;
|
||||
|
||||
const getChartDimentions = (spreadsheet: Spreadsheet) => {
|
||||
const chartWidth =
|
||||
(BAR_WIDTH + BAR_GAP) * spreadsheet.values.length + BAR_GAP;
|
||||
const chartHeight = BAR_HEIGHT + BAR_GAP * 2;
|
||||
return { chartWidth, chartHeight };
|
||||
};
|
||||
|
||||
const chartXLabels = (
|
||||
spreadsheet: Spreadsheet,
|
||||
x: number,
|
||||
y: number,
|
||||
): ExcalidrawElement[] => {
|
||||
const values = spreadsheet.values;
|
||||
const max = Math.max(...values);
|
||||
const chartHeight = BAR_HEIGHT + BAR_GAP * 2;
|
||||
const chartWidth = (BAR_WIDTH + BAR_GAP) * values.length + BAR_GAP;
|
||||
const maxColors = colors.elementBackground.length;
|
||||
const bgColors = colors.elementBackground.slice(2, maxColors);
|
||||
|
||||
// Put all the common properties here so when the whole chart is selected
|
||||
// the properties dialog shows the correct selected values
|
||||
const commonProps = {
|
||||
backgroundColor: bgColors[Math.floor(Math.random() * bgColors.length)],
|
||||
fillStyle: "hachure",
|
||||
fontFamily: DEFAULT_FONT_FAMILY,
|
||||
fontSize: DEFAULT_FONT_SIZE,
|
||||
groupIds: [randomId()],
|
||||
opacity: 100,
|
||||
roughness: 1,
|
||||
strokeColor: colors.elementStroke[0],
|
||||
strokeSharpness: "sharp",
|
||||
strokeStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
verticalAlign: "middle",
|
||||
} as const;
|
||||
|
||||
const minYLabel = newTextElement({
|
||||
...commonProps,
|
||||
x: x - BAR_GAP,
|
||||
y: y - BAR_GAP,
|
||||
text: "0",
|
||||
textAlign: "right",
|
||||
});
|
||||
|
||||
const maxYLabel = newTextElement({
|
||||
...commonProps,
|
||||
x: x - BAR_GAP,
|
||||
y: y - BAR_HEIGHT - minYLabel.height / 2,
|
||||
text: max.toLocaleString(),
|
||||
textAlign: "right",
|
||||
});
|
||||
|
||||
const xAxisLine = newLinearElement({
|
||||
type: "line",
|
||||
x,
|
||||
y,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
width: chartWidth,
|
||||
points: [
|
||||
[0, 0],
|
||||
[chartWidth, 0],
|
||||
],
|
||||
...commonProps,
|
||||
});
|
||||
|
||||
const yAxisLine = newLinearElement({
|
||||
type: "line",
|
||||
x,
|
||||
y,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
height: chartHeight,
|
||||
points: [
|
||||
[0, 0],
|
||||
[0, -chartHeight],
|
||||
],
|
||||
...commonProps,
|
||||
});
|
||||
|
||||
const maxValueLine = newLinearElement({
|
||||
type: "line",
|
||||
x,
|
||||
y: y - BAR_HEIGHT - BAR_GAP,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
...commonProps,
|
||||
strokeStyle: "dotted",
|
||||
width: chartWidth,
|
||||
points: [
|
||||
[0, 0],
|
||||
[chartWidth, 0],
|
||||
],
|
||||
});
|
||||
|
||||
const bars = values.map((value, index) => {
|
||||
const barHeight = (value / max) * BAR_HEIGHT;
|
||||
return newElement({
|
||||
...commonProps,
|
||||
type: "rectangle",
|
||||
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP,
|
||||
y: y - barHeight - BAR_GAP,
|
||||
width: BAR_WIDTH,
|
||||
height: barHeight,
|
||||
});
|
||||
});
|
||||
|
||||
const xLabels =
|
||||
groupId: string,
|
||||
backgroundColor: string,
|
||||
): ChartElements => {
|
||||
return (
|
||||
spreadsheet.labels?.map((label, index) => {
|
||||
return newTextElement({
|
||||
groupIds: [groupId],
|
||||
backgroundColor,
|
||||
...commonProps,
|
||||
text: label.length > 8 ? `${label.slice(0, 5)}...` : label,
|
||||
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
|
||||
@ -257,29 +193,287 @@ export const renderSpreadsheet = (
|
||||
textAlign: "center",
|
||||
verticalAlign: "top",
|
||||
});
|
||||
}) || [];
|
||||
}) || []
|
||||
);
|
||||
};
|
||||
|
||||
const chartYLabels = (
|
||||
spreadsheet: Spreadsheet,
|
||||
x: number,
|
||||
y: number,
|
||||
groupId: string,
|
||||
backgroundColor: string,
|
||||
): ChartElements => {
|
||||
const minYLabel = newTextElement({
|
||||
groupIds: [groupId],
|
||||
backgroundColor,
|
||||
...commonProps,
|
||||
x: x - BAR_GAP,
|
||||
y: y - BAR_GAP,
|
||||
text: "0",
|
||||
textAlign: "right",
|
||||
});
|
||||
|
||||
const maxYLabel = newTextElement({
|
||||
groupIds: [groupId],
|
||||
backgroundColor,
|
||||
...commonProps,
|
||||
x: x - BAR_GAP,
|
||||
y: y - BAR_HEIGHT - minYLabel.height / 2,
|
||||
text: Math.max(...spreadsheet.values).toLocaleString(),
|
||||
textAlign: "right",
|
||||
});
|
||||
|
||||
return [minYLabel, maxYLabel];
|
||||
};
|
||||
|
||||
const chartLines = (
|
||||
spreadsheet: Spreadsheet,
|
||||
x: number,
|
||||
y: number,
|
||||
groupId: string,
|
||||
backgroundColor: string,
|
||||
): ChartElements => {
|
||||
const { chartWidth, chartHeight } = getChartDimentions(spreadsheet);
|
||||
const xLine = newLinearElement({
|
||||
backgroundColor,
|
||||
groupIds: [groupId],
|
||||
...commonProps,
|
||||
type: "line",
|
||||
x,
|
||||
y,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
width: chartWidth,
|
||||
points: [
|
||||
[0, 0],
|
||||
[chartWidth, 0],
|
||||
],
|
||||
});
|
||||
|
||||
const yLine = newLinearElement({
|
||||
backgroundColor,
|
||||
groupIds: [groupId],
|
||||
...commonProps,
|
||||
type: "line",
|
||||
x,
|
||||
y,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
height: chartHeight,
|
||||
points: [
|
||||
[0, 0],
|
||||
[0, -chartHeight],
|
||||
],
|
||||
});
|
||||
|
||||
const maxLine = newLinearElement({
|
||||
backgroundColor,
|
||||
groupIds: [groupId],
|
||||
...commonProps,
|
||||
type: "line",
|
||||
x,
|
||||
y: y - BAR_HEIGHT - BAR_GAP,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
strokeStyle: "dotted",
|
||||
width: chartWidth,
|
||||
opacity: GRID_OPACITY,
|
||||
points: [
|
||||
[0, 0],
|
||||
[chartWidth, 0],
|
||||
],
|
||||
});
|
||||
|
||||
return [xLine, yLine, maxLine];
|
||||
};
|
||||
|
||||
// For the maths behind it https://excalidraw.com/#json=6320864370884608,O_5xfD-Agh32tytHpRJx1g
|
||||
const chartBaseElements = (
|
||||
spreadsheet: Spreadsheet,
|
||||
x: number,
|
||||
y: number,
|
||||
groupId: string,
|
||||
backgroundColor: string,
|
||||
debug?: boolean,
|
||||
): ChartElements => {
|
||||
const { chartWidth, chartHeight } = getChartDimentions(spreadsheet);
|
||||
|
||||
const title = spreadsheet.title
|
||||
? newTextElement({
|
||||
backgroundColor,
|
||||
groupIds: [groupId],
|
||||
...commonProps,
|
||||
text: spreadsheet.title,
|
||||
x: x + chartWidth / 2,
|
||||
y: y - BAR_HEIGHT - BAR_GAP * 2 - maxYLabel.height,
|
||||
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
|
||||
strokeSharpness: "sharp",
|
||||
strokeStyle: "solid",
|
||||
textAlign: "center",
|
||||
})
|
||||
: null;
|
||||
|
||||
trackEvent(EVENT_MAGIC, "chart", "bars", bars.length);
|
||||
const debugRect = debug
|
||||
? newElement({
|
||||
backgroundColor,
|
||||
groupIds: [groupId],
|
||||
...commonProps,
|
||||
type: "rectangle",
|
||||
x,
|
||||
y: y - chartHeight,
|
||||
width: chartWidth,
|
||||
height: chartHeight,
|
||||
strokeColor: colors.elementStroke[0],
|
||||
fillStyle: "solid",
|
||||
opacity: 6,
|
||||
})
|
||||
: null;
|
||||
|
||||
return [
|
||||
title,
|
||||
...bars,
|
||||
...xLabels,
|
||||
xAxisLine,
|
||||
yAxisLine,
|
||||
maxValueLine,
|
||||
minYLabel,
|
||||
maxYLabel,
|
||||
].filter((element) => element !== null) as ExcalidrawElement[];
|
||||
...(debugRect ? [debugRect] : []),
|
||||
...(title ? [title] : []),
|
||||
...chartXLabels(spreadsheet, x, y, groupId, backgroundColor),
|
||||
...chartYLabels(spreadsheet, x, y, groupId, backgroundColor),
|
||||
...chartLines(spreadsheet, x, y, groupId, backgroundColor),
|
||||
];
|
||||
};
|
||||
|
||||
const chartTypeBar = (
|
||||
spreadsheet: Spreadsheet,
|
||||
x: number,
|
||||
y: number,
|
||||
): ChartElements => {
|
||||
const max = Math.max(...spreadsheet.values);
|
||||
const groupId = randomId();
|
||||
const backgroundColor = bgColors[Math.floor(Math.random() * bgColors.length)];
|
||||
|
||||
const bars = spreadsheet.values.map((value, index) => {
|
||||
const barHeight = (value / max) * BAR_HEIGHT;
|
||||
return newElement({
|
||||
backgroundColor,
|
||||
groupIds: [groupId],
|
||||
...commonProps,
|
||||
type: "rectangle",
|
||||
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP,
|
||||
y: y - barHeight - BAR_GAP,
|
||||
width: BAR_WIDTH,
|
||||
height: barHeight,
|
||||
});
|
||||
});
|
||||
|
||||
return [
|
||||
...bars,
|
||||
...chartBaseElements(
|
||||
spreadsheet,
|
||||
x,
|
||||
y,
|
||||
groupId,
|
||||
backgroundColor,
|
||||
process.env.NODE_ENV === ENV.DEVELOPMENT,
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
const chartTypeLine = (
|
||||
spreadsheet: Spreadsheet,
|
||||
x: number,
|
||||
y: number,
|
||||
): ChartElements => {
|
||||
const max = Math.max(...spreadsheet.values);
|
||||
const groupId = randomId();
|
||||
const backgroundColor = bgColors[Math.floor(Math.random() * bgColors.length)];
|
||||
|
||||
let index = 0;
|
||||
const points = [];
|
||||
for (const value of spreadsheet.values) {
|
||||
const cx = index * (BAR_WIDTH + BAR_GAP);
|
||||
const cy = -(value / max) * BAR_HEIGHT;
|
||||
points.push([cx, cy]);
|
||||
index++;
|
||||
}
|
||||
|
||||
const maxX = Math.max(...points.map((element) => element[0]));
|
||||
const maxY = Math.max(...points.map((element) => element[1]));
|
||||
const minX = Math.min(...points.map((element) => element[0]));
|
||||
const minY = Math.min(...points.map((element) => element[1]));
|
||||
|
||||
const line = newLinearElement({
|
||||
backgroundColor,
|
||||
groupIds: [groupId],
|
||||
...commonProps,
|
||||
type: "line",
|
||||
x: x + BAR_GAP + BAR_WIDTH / 2,
|
||||
y: y - BAR_GAP,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
height: maxY - minY,
|
||||
width: maxX - minX,
|
||||
strokeWidth: 2,
|
||||
points: points as any,
|
||||
});
|
||||
|
||||
const dots = spreadsheet.values.map((value, index) => {
|
||||
const cx = index * (BAR_WIDTH + BAR_GAP) + BAR_GAP / 2;
|
||||
const cy = -(value / max) * BAR_HEIGHT + BAR_GAP / 2;
|
||||
return newElement({
|
||||
backgroundColor,
|
||||
groupIds: [groupId],
|
||||
...commonProps,
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 2,
|
||||
type: "ellipse",
|
||||
x: x + cx + BAR_WIDTH / 2,
|
||||
y: y + cy - BAR_GAP * 2,
|
||||
width: BAR_GAP,
|
||||
height: BAR_GAP,
|
||||
});
|
||||
});
|
||||
|
||||
const lines = spreadsheet.values.map((value, index) => {
|
||||
const cx = index * (BAR_WIDTH + BAR_GAP) + BAR_GAP / 2;
|
||||
const cy = (value / max) * BAR_HEIGHT + BAR_GAP / 2 + BAR_GAP;
|
||||
return newLinearElement({
|
||||
backgroundColor,
|
||||
groupIds: [groupId],
|
||||
...commonProps,
|
||||
type: "line",
|
||||
x: x + cx + BAR_WIDTH / 2 + BAR_GAP / 2,
|
||||
y: y - cy,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
height: cy,
|
||||
strokeStyle: "dotted",
|
||||
opacity: GRID_OPACITY,
|
||||
points: [
|
||||
[0, 0],
|
||||
[0, cy],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
return [
|
||||
...chartBaseElements(
|
||||
spreadsheet,
|
||||
x,
|
||||
y,
|
||||
groupId,
|
||||
backgroundColor,
|
||||
process.env.NODE_ENV === ENV.DEVELOPMENT,
|
||||
),
|
||||
line,
|
||||
...lines,
|
||||
...dots,
|
||||
];
|
||||
};
|
||||
|
||||
export const renderSpreadsheet = (
|
||||
chartType: string,
|
||||
spreadsheet: Spreadsheet,
|
||||
x: number,
|
||||
y: number,
|
||||
): ChartElements => {
|
||||
if (chartType === "line") {
|
||||
return chartTypeLine(spreadsheet, x, y);
|
||||
}
|
||||
return chartTypeBar(spreadsheet, x, y);
|
||||
};
|
||||
|
@ -1,6 +1,13 @@
|
||||
import colors from "./colors";
|
||||
import { AppState } from "./types";
|
||||
|
||||
export const getClientColors = (clientId: string) => {
|
||||
export const getClientColors = (clientId: string, appState: AppState) => {
|
||||
if (appState?.collaborators) {
|
||||
const currentUser = appState.collaborators.get(clientId);
|
||||
if (currentUser?.color) {
|
||||
return currentUser.color;
|
||||
}
|
||||
}
|
||||
// Naive way of getting an integer out of the clientId
|
||||
const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0);
|
||||
|
||||
|
@ -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
|
||||
|
@ -1,23 +1,22 @@
|
||||
import React from "react";
|
||||
import { AppState, Zoom } from "../types";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import {
|
||||
hasBackground,
|
||||
hasStroke,
|
||||
canChangeSharpness,
|
||||
hasText,
|
||||
canHaveArrowheads,
|
||||
getTargetElements,
|
||||
hasBackground,
|
||||
hasStroke,
|
||||
hasText,
|
||||
} from "../scene";
|
||||
import { t } from "../i18n";
|
||||
import { SHAPES } from "../shapes";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { AppState, Zoom } from "../types";
|
||||
import { capitalizeString, isTransparent, setCursorForShape } from "../utils";
|
||||
import Stack from "./Stack";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { trackEvent, EVENT_SHAPE, EVENT_DIALOG } from "../analytics";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
|
||||
export const SelectedShapeActions = ({
|
||||
appState,
|
||||
@ -152,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;
|
||||
@ -164,9 +165,9 @@ export const ShapesSwitcher = ({
|
||||
{SHAPES.map(({ value, icon, key }, index) => {
|
||||
const label = t(`toolBar.${value}`);
|
||||
const letter = typeof key === "string" ? key : key[0];
|
||||
const shortcut = `${capitalizeString(letter)} ${t(
|
||||
"shortcutsDialog.or",
|
||||
)} ${index + 1}`;
|
||||
const shortcut = `${capitalizeString(letter)} ${t("helpDialog.or")} ${
|
||||
index + 1
|
||||
}`;
|
||||
return (
|
||||
<ToolButton
|
||||
className="Shape"
|
||||
@ -181,13 +182,12 @@ export const ShapesSwitcher = ({
|
||||
aria-keyshortcuts={shortcut}
|
||||
data-testid={value}
|
||||
onChange={() => {
|
||||
trackEvent(EVENT_SHAPE, value, "toolbar");
|
||||
setAppState({
|
||||
elementType: value,
|
||||
multiElement: null,
|
||||
selectedElementIds: {},
|
||||
});
|
||||
setCursorForShape(value);
|
||||
setCursorForShape(canvas, value);
|
||||
setAppState({});
|
||||
}}
|
||||
/>
|
||||
@ -203,9 +203,6 @@ export const ShapesSwitcher = ({
|
||||
title={`${capitalizeString(t("toolBar.library"))} — 9`}
|
||||
aria-label={capitalizeString(t("toolBar.library"))}
|
||||
onClick={() => {
|
||||
if (!isLibraryOpen) {
|
||||
trackEvent(EVENT_DIALOG, "library");
|
||||
}
|
||||
setAppState({ isLibraryOpen: !isLibraryOpen });
|
||||
}}
|
||||
/>
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import "../css/_variables";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.Avatar {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { EVENT_CHANGE, trackEvent } from "../analytics";
|
||||
import { AppState } from "../types";
|
||||
import { DarkModeToggle } from "./DarkModeToggle";
|
||||
|
||||
@ -8,22 +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) => {
|
||||
// TODO: track the theme on the first load too
|
||||
trackEvent(EVENT_CHANGE, "theme", appearance);
|
||||
setAppState({ appearance });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{showThemeBtn && (
|
||||
<div style={{ marginInlineStart: "0.25rem" }}>
|
||||
<DarkModeToggle
|
||||
value={appState.theme}
|
||||
onChange={(theme) => {
|
||||
setAppState({ theme });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,10 +1,11 @@
|
||||
@import "../css/_variables";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.CollabButton.is-collaborating {
|
||||
background-color: var(--button-special-active-background-color);
|
||||
background-color: var(--button-special-active-bg-color);
|
||||
|
||||
.ToolIcon__icon svg {
|
||||
.ToolIcon__icon svg,
|
||||
.ToolIcon__label {
|
||||
color: var(--icon-green-fill-color);
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import useIsMobile from "../is-mobile";
|
||||
import { users } from "./icons";
|
||||
|
||||
import "./CollabButton.scss";
|
||||
import { EVENT_DIALOG, trackEvent } from "../analytics";
|
||||
|
||||
const CollabButton = ({
|
||||
isCollaborating,
|
||||
@ -23,14 +22,11 @@ const CollabButton = ({
|
||||
className={clsx("CollabButton", {
|
||||
"is-collaborating": isCollaborating,
|
||||
})}
|
||||
onClick={() => {
|
||||
trackEvent(EVENT_DIALOG, "collaboration");
|
||||
onClick();
|
||||
}}
|
||||
onClick={onClick}
|
||||
icon={users}
|
||||
type="button"
|
||||
title={t("buttons.roomDialog")}
|
||||
aria-label={t("buttons.roomDialog")}
|
||||
title={t("labels.liveCollaboration")}
|
||||
aria-label={t("labels.liveCollaboration")}
|
||||
showAriaLabel={useIsMobile()}
|
||||
>
|
||||
{collaboratorCount > 0 && (
|
||||
|
@ -1,10 +1,10 @@
|
||||
@import "../css/_variables";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.color-picker {
|
||||
background: var(--popup-background-color);
|
||||
border: 0px solid transparentize($oc-white, 0.75);
|
||||
box-shadow: transparentize($oc-black, 0.75) 0px 1px 4px;
|
||||
background: var(--popup-bg-color);
|
||||
border: 0 solid transparentize($oc-white, 0.75);
|
||||
box-shadow: transparentize($oc-black, 0.75) 0 1px 4px;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
|
||||
@ -24,11 +24,11 @@
|
||||
}
|
||||
|
||||
.color-picker-triangle {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 0px 9px 10px;
|
||||
border-color: transparent transparent var(--popup-background-color);
|
||||
border-width: 0 9px 10px;
|
||||
border-color: transparent transparent var(--popup-bg-color);
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
|
||||
@ -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 */
|
||||
@ -84,12 +84,12 @@
|
||||
|
||||
.color-picker-transparent {
|
||||
border-radius: 4px;
|
||||
box-shadow: transparentize($oc-black, 0.9) 0px 0px 0px 1px inset;
|
||||
box-shadow: transparentize($oc-black, 0.9) 0 0 0 1px inset;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.color-picker-transparent,
|
||||
@ -104,11 +104,11 @@
|
||||
width: 1.875rem;
|
||||
|
||||
:root[dir="ltr"] & {
|
||||
border-radius: 4px 0px 0px 4px;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
:root[dir="rtl"] & {
|
||||
border-radius: 0px 4px 4px 0px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
color: var(--input-label-color);
|
||||
@ -144,7 +144,7 @@
|
||||
}
|
||||
|
||||
.color-input-container:focus-within .color-picker-hash::after {
|
||||
background: var(--input-background-color);
|
||||
background: var(--input-bg-color);
|
||||
|
||||
:root[dir="ltr"] & {
|
||||
right: -2px;
|
||||
@ -163,19 +163,19 @@
|
||||
width: 12ch; /* length of `transparent` + 1 */
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
background-color: var(--input-background-color);
|
||||
color: var(--text-color-primary);
|
||||
border: 0px;
|
||||
background-color: var(--input-bg-color);
|
||||
color: var(--text-primary-color);
|
||||
border: 0;
|
||||
outline: none;
|
||||
height: 1.75em;
|
||||
box-shadow: var(--input-border-color) 0px 0px 0px 1px inset;
|
||||
box-shadow: var(--input-border-color) 0 0 0 1px inset;
|
||||
|
||||
:root[dir="ltr"] & {
|
||||
border-radius: 0px 4px 4px 0px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
:root[dir="rtl"] & {
|
||||
border-radius: 4px 0px 0px 4px;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
float: left;
|
||||
@ -192,7 +192,7 @@
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: transparent !important;
|
||||
filter: var(--appearance-filter);
|
||||
filter: var(--theme-filter);
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
@ -218,7 +218,7 @@
|
||||
left: 2px;
|
||||
}
|
||||
|
||||
@media #{$media-query} {
|
||||
@media #{$is-mobile-query} {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@ -228,7 +228,7 @@
|
||||
}
|
||||
|
||||
.color-picker-type-elementBackground .color-picker-keybinding {
|
||||
color: #fff;
|
||||
color: $oc-white;
|
||||
}
|
||||
|
||||
.color-picker-swatch[aria-label="transparent"] .color-picker-keybinding {
|
||||
@ -239,12 +239,12 @@
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
&.Appearance_dark {
|
||||
&.theme--dark {
|
||||
.color-picker-type-elementBackground .color-picker-keybinding {
|
||||
color: #000;
|
||||
color: $oc-black;
|
||||
}
|
||||
.color-picker-swatch[aria-label="transparent"] .color-picker-keybinding {
|
||||
color: #000;
|
||||
color: $oc-black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,18 @@
|
||||
@import "open-color/open-color.scss";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.context-menu {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 3px 10px transparentize($oc-black, 0.8);
|
||||
box-shadow: 0 3px 10px transparentize($oc-black, 0.8);
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
margin: -0.25rem 0 0 0.125rem;
|
||||
padding: 0.25rem 0;
|
||||
background-color: var(--popup-secondary-background-color);
|
||||
padding: 0.5rem 0;
|
||||
background-color: var(--popup-secondary-bg-color);
|
||||
border: 1px solid var(--button-gray-3);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.context-menu button {
|
||||
@ -42,29 +43,30 @@
|
||||
}
|
||||
|
||||
&.dangerous {
|
||||
div:nth-child(1) {
|
||||
.context-menu-option__label {
|
||||
color: $oc-red-7;
|
||||
}
|
||||
}
|
||||
|
||||
div:nth-child(1) {
|
||||
.context-menu-option__label {
|
||||
justify-self: start;
|
||||
margin-inline-end: 20px;
|
||||
}
|
||||
div:nth-child(2) {
|
||||
.context-menu-option__shortcut {
|
||||
justify-self: end;
|
||||
opacity: 0.6;
|
||||
font-family: inherit;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-option:hover {
|
||||
color: var(--popup-background-color);
|
||||
color: var(--popup-bg-color);
|
||||
background-color: var(--select-highlight-color);
|
||||
|
||||
&.dangerous {
|
||||
div:nth-child(1) {
|
||||
color: var(--popup-background-color);
|
||||
.context-menu-option__label {
|
||||
color: var(--popup-bg-color);
|
||||
}
|
||||
background-color: $oc-red-6;
|
||||
}
|
||||
@ -73,4 +75,23 @@
|
||||
.context-menu-option:focus {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@media #{$is-mobile-query} {
|
||||
.context-menu-option {
|
||||
display: block;
|
||||
|
||||
.context-menu-option__label {
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
.context-menu-option__shortcut {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-option-separator {
|
||||
border: none;
|
||||
border-top: 1px solid $oc-gray-5;
|
||||
}
|
||||
}
|
||||
|
@ -2,35 +2,43 @@ import React from "react";
|
||||
import { render, unmountComponentAtNode } from "react-dom";
|
||||
import clsx from "clsx";
|
||||
import { Popover } from "./Popover";
|
||||
import { t } from "../i18n";
|
||||
|
||||
import "./ContextMenu.scss";
|
||||
import {
|
||||
getShortcutFromShortcutName,
|
||||
ShortcutName,
|
||||
} from "../actions/shortcuts";
|
||||
import { Action } from "../actions/types";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { AppState } from "../types";
|
||||
|
||||
type ContextMenuOption = {
|
||||
checked?: boolean;
|
||||
shortcutName: ShortcutName;
|
||||
label: string;
|
||||
action(): void;
|
||||
};
|
||||
export type ContextMenuOption = "separator" | Action;
|
||||
|
||||
type Props = {
|
||||
type ContextMenuProps = {
|
||||
options: ContextMenuOption[];
|
||||
onCloseRequest?(): void;
|
||||
top: number;
|
||||
left: number;
|
||||
actionManager: ActionManager;
|
||||
appState: Readonly<AppState>;
|
||||
};
|
||||
|
||||
const ContextMenu = ({ options, onCloseRequest, top, left }: Props) => {
|
||||
const ContextMenu = ({
|
||||
options,
|
||||
onCloseRequest,
|
||||
top,
|
||||
left,
|
||||
actionManager,
|
||||
appState,
|
||||
}: 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
|
||||
@ -43,23 +51,34 @@ const ContextMenu = ({ options, onCloseRequest, top, left }: Props) => {
|
||||
className="context-menu"
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
>
|
||||
{options.map(({ action, checked, shortcutName, label }, idx) => (
|
||||
<li data-testid={shortcutName} key={idx} onClick={onCloseRequest}>
|
||||
<button
|
||||
className={`context-menu-option
|
||||
${shortcutName === "delete" ? "dangerous" : ""}
|
||||
${checked ? "checkmark" : ""}`}
|
||||
onClick={action}
|
||||
>
|
||||
<div>{label}</div>
|
||||
<div>
|
||||
{shortcutName
|
||||
? getShortcutFromShortcutName(shortcutName)
|
||||
: ""}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{options.map((option, idx) => {
|
||||
if (option === "separator") {
|
||||
return <hr key={idx} className="context-menu-option-separator" />;
|
||||
}
|
||||
|
||||
const actionName = option.name;
|
||||
const label = option.contextItemLabel
|
||||
? t(option.contextItemLabel)
|
||||
: "";
|
||||
return (
|
||||
<li key={idx} data-testid={actionName} onClick={onCloseRequest}>
|
||||
<button
|
||||
className={clsx("context-menu-option", {
|
||||
dangerous: actionName === "deleteSelectedElements",
|
||||
checkmark: option.checked?.(appState),
|
||||
})}
|
||||
onClick={() => actionManager.executeAction(option)}
|
||||
>
|
||||
<div className="context-menu-option__label">{label}</div>
|
||||
<kbd className="context-menu-option__shortcut">
|
||||
{actionName
|
||||
? getShortcutFromShortcutName(actionName as ShortcutName)
|
||||
: ""}
|
||||
</kbd>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</Popover>
|
||||
</div>
|
||||
@ -78,8 +97,10 @@ const getContextMenuNode = (): HTMLDivElement => {
|
||||
|
||||
type ContextMenuParams = {
|
||||
options: (ContextMenuOption | false | null | undefined)[];
|
||||
top: number;
|
||||
left: number;
|
||||
top: ContextMenuProps["top"];
|
||||
left: ContextMenuProps["left"];
|
||||
actionManager: ContextMenuProps["actionManager"];
|
||||
appState: Readonly<AppState>;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
@ -101,6 +122,8 @@ export default {
|
||||
left={params.left}
|
||||
options={options}
|
||||
onCloseRequest={handleClose}
|
||||
actionManager={params.actionManager}
|
||||
appState={params.appState}
|
||||
/>,
|
||||
getContextMenuNode(),
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -1,12 +1,21 @@
|
||||
@import "../css/_variables";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.Dialog {
|
||||
user-select: text;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.Dialog__title {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
margin-top: 0;
|
||||
grid-template-columns: 1fr calc(var(--space-factor) * 7);
|
||||
grid-gap: var(--metric);
|
||||
padding: calc(var(--space-factor) * 2);
|
||||
text-align: center;
|
||||
font-variant: small-caps;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.Dialog__titleContent {
|
||||
@ -18,7 +27,11 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media #{$media-query} {
|
||||
.Dialog__content {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
@media #{$is-mobile-query} {
|
||||
.Dialog {
|
||||
--metric: calc(var(--space-factor) * 4);
|
||||
--inset-left: #{"max(var(--metric), var(--sal))"};
|
||||
@ -30,14 +43,9 @@
|
||||
var(--space-factor) * 7
|
||||
);
|
||||
position: sticky;
|
||||
top: calc(-1 * var(--metric));
|
||||
margin: calc(-1 * var(--inset-right));
|
||||
margin-top: calc(-1 * var(--metric));
|
||||
margin-bottom: var(--metric);
|
||||
top: 0;
|
||||
padding: calc(var(--space-factor) * 2);
|
||||
padding-left: var(--inset-left);
|
||||
padding-right: var(--inset-right);
|
||||
background: var(--bg-color-island);
|
||||
background: var(--island-bg-color);
|
||||
font-size: 1.25em;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
@ -1,30 +1,23 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { Modal } from "./Modal";
|
||||
import { Island } from "./Island";
|
||||
import React, { useEffect } from "react";
|
||||
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
||||
import { t } from "../i18n";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { back, close } from "./icons";
|
||||
import { KEYS } from "../keys";
|
||||
|
||||
import "./Dialog.scss";
|
||||
|
||||
const useRefState = <T,>() => {
|
||||
const [refValue, setRefValue] = useState<T | null>(null);
|
||||
const refCallback = useCallback((value: T) => {
|
||||
setRefValue(value);
|
||||
}, []);
|
||||
return [refValue, refCallback] as const;
|
||||
};
|
||||
import { back, close } from "./icons";
|
||||
import { Island } from "./Island";
|
||||
import { Modal } from "./Modal";
|
||||
|
||||
export const Dialog = (props: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
maxWidth?: number;
|
||||
small?: boolean;
|
||||
onCloseRequest(): void;
|
||||
title: React.ReactNode;
|
||||
autofocus?: boolean;
|
||||
}) => {
|
||||
const [islandNode, setIslandNode] = useRefState<HTMLDivElement>();
|
||||
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!islandNode) {
|
||||
@ -33,7 +26,7 @@ export const Dialog = (props: {
|
||||
|
||||
const focusableElements = queryFocusableElements(islandNode);
|
||||
|
||||
if (focusableElements.length > 0) {
|
||||
if (focusableElements.length > 0 && props.autofocus !== false) {
|
||||
// If there's an element other than close, focus it.
|
||||
(focusableElements[1] || focusableElements[0]).focus();
|
||||
}
|
||||
@ -62,7 +55,7 @@ export const Dialog = (props: {
|
||||
islandNode.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => islandNode.removeEventListener("keydown", handleKeyDown);
|
||||
}, [islandNode]);
|
||||
}, [islandNode, props.autofocus]);
|
||||
|
||||
const queryFocusableElements = (node: HTMLElement) => {
|
||||
const focusableElements = node.querySelectorAll<HTMLElement>(
|
||||
@ -76,10 +69,10 @@ export const Dialog = (props: {
|
||||
<Modal
|
||||
className={clsx("Dialog", props.className)}
|
||||
labelledBy="dialog-title"
|
||||
maxWidth={props.maxWidth}
|
||||
maxWidth={props.small ? 550 : 800}
|
||||
onCloseRequest={props.onCloseRequest}
|
||||
>
|
||||
<Island padding={4} ref={setIslandNode}>
|
||||
<Island ref={setIslandNode}>
|
||||
<h2 id="dialog-title" className="Dialog__title">
|
||||
<span className="Dialog__titleContent">{props.title}</span>
|
||||
<button
|
||||
@ -90,7 +83,7 @@ export const Dialog = (props: {
|
||||
{useIsMobile() ? back : close}
|
||||
</button>
|
||||
</h2>
|
||||
{props.children}
|
||||
<div className="Dialog__content">{props.children}</div>
|
||||
</Island>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -24,7 +24,7 @@ export const ErrorDialog = ({
|
||||
<>
|
||||
{modalIsShown && (
|
||||
<Dialog
|
||||
maxWidth={500}
|
||||
small
|
||||
onCloseRequest={handleClose}
|
||||
title={t("errorDialog.title")}
|
||||
>
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import "../css/_variables";
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.ExportDialog__preview {
|
||||
@ -16,7 +16,7 @@
|
||||
max-height: 25rem;
|
||||
}
|
||||
|
||||
&.Appearance_dark .ExportDialog__preview canvas {
|
||||
&.theme--dark .ExportDialog__preview canvas {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
@ -31,13 +31,31 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
@media #{$is-mobile-query} {
|
||||
.ExportDialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -51,9 +69,7 @@
|
||||
.ExportDialog__actions > * {
|
||||
margin-bottom: calc(var(--space-factor) * 3);
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$media-query} {
|
||||
.ExportDialog__preview canvas {
|
||||
max-height: 30vh;
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { render, unmountComponentAtNode } from "react-dom";
|
||||
import { ActionsManagerInterface } from "../actions/types";
|
||||
import { EVENT_DIALOG, trackEvent } from "../analytics";
|
||||
import { probablySupportsClipboardBlob } from "../clipboard";
|
||||
import { canvasToBlob } from "../data/blob";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
@ -20,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>
|
||||
@ -129,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}>
|
||||
@ -251,9 +255,9 @@ export const ExportDialog = ({
|
||||
<>
|
||||
<ToolButton
|
||||
onClick={() => {
|
||||
trackEvent(EVENT_DIALOG, "export");
|
||||
setModalIsShown(true);
|
||||
}}
|
||||
data-testid="export-button"
|
||||
icon={exportFile}
|
||||
type="button"
|
||||
aria-label={t("buttons.export")}
|
||||
@ -262,11 +266,7 @@ export const ExportDialog = ({
|
||||
ref={triggerButton}
|
||||
/>
|
||||
{modalIsShown && (
|
||||
<Dialog
|
||||
maxWidth={800}
|
||||
onCloseRequest={handleClose}
|
||||
title={t("buttons.export")}
|
||||
>
|
||||
<Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
|
||||
<ExportModal
|
||||
elements={elements}
|
||||
appState={appState}
|
||||
|
@ -1,10 +1,9 @@
|
||||
import React from "react";
|
||||
import oc from "open-color";
|
||||
import { EVENT_EXIT, trackEvent } from "../analytics";
|
||||
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"
|
||||
@ -17,24 +16,21 @@ export const GitHubCorner = React.memo(
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="GitHub repository"
|
||||
onClick={() => {
|
||||
trackEvent(EVENT_EXIT, "github");
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|