Compare commits
2165 Commits
v0.17.3
...
l10n_maste
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c507d1726a | ||
![]() |
d6ef71c15c | ||
![]() |
4b9978cd57 | ||
![]() |
79cac89d45 | ||
![]() |
2651ce2e9b | ||
![]() |
6c3c6f26d9 | ||
![]() |
38da08662c | ||
![]() |
571053aec9 | ||
![]() |
88b3d9b6a5 | ||
![]() |
f2b9da3255 | ||
![]() |
ba4a9f1e82 | ||
![]() |
90ee80a3fa | ||
![]() |
01c6cbefbc | ||
![]() |
8aee0f5f24 | ||
![]() |
d865bfb985 | ||
![]() |
4ac707927a | ||
![]() |
e102ee38cd | ||
![]() |
4df31a063a | ||
![]() |
f8e0e7cda9 | ||
![]() |
bfd79929b7 | ||
![]() |
182df6516a | ||
![]() |
08840c89ca | ||
![]() |
30cf52e15c | ||
![]() |
09f2f38ce7 | ||
![]() |
df54053d44 | ||
![]() |
1850a09ffd | ||
![]() |
16e2694301 | ||
![]() |
48fdd416e4 | ||
![]() |
0e71b552a0 | ||
![]() |
201df105a0 | ||
![]() |
8b0297e024 | ||
![]() |
88f2d3d848 | ||
![]() |
2262458d08 | ||
![]() |
367dbb6a20 | ||
![]() |
15bb6af068 | ||
![]() |
0da5f82b4b | ||
![]() |
b6fadd90c6 | ||
![]() |
f4dd5ced14 | ||
![]() |
67b4a6083e | ||
![]() |
d011d8ffbe | ||
![]() |
9341ae94fe | ||
![]() |
5d9d93e400 | ||
![]() |
8660a48958 | ||
![]() |
361cb1b94c | ||
![]() |
4282f3c2dc | ||
![]() |
37ee37caaa | ||
![]() |
9ef56c2b20 | ||
![]() |
7f235f5830 | ||
![]() |
cbe30a553f | ||
![]() |
28eef4fc08 | ||
![]() |
10bf4dae6b | ||
![]() |
ccb2b9e5e2 | ||
![]() |
25c9334ebe | ||
![]() |
057a5b555a | ||
![]() |
7adb935619 | ||
![]() |
6d6bb7a6d0 | ||
![]() |
7fa8edcdfe | ||
![]() |
63facdd1b3 | ||
![]() |
0ead775db7 | ||
![]() |
5af0f1c11e | ||
![]() |
fd5c2b81d8 | ||
![]() |
052e456826 | ||
![]() |
0e71e2e49b | ||
![]() |
0d50481428 | ||
![]() |
c82882e7e4 | ||
![]() |
62bd7cca34 | ||
![]() |
0e37679c89 | ||
![]() |
cfcac1d74d | ||
![]() |
f8935be5fd | ||
![]() |
2ad3c1e3a0 | ||
![]() |
8c69e77458 | ||
![]() |
7c60960708 | ||
![]() |
c050f50920 | ||
![]() |
fc0364e49b | ||
![]() |
1c2bc960e3 | ||
![]() |
fa3e95e62d | ||
![]() |
91ff8ece0d | ||
![]() |
08241465c4 | ||
![]() |
4a70e3c8c6 | ||
![]() |
58ae2ef67f | ||
![]() |
d4474ce348 | ||
![]() |
a02df75456 | ||
![]() |
e0d1d4ae53 | ||
![]() |
91f906d122 | ||
![]() |
8e76a3576a | ||
![]() |
575ba7c167 | ||
![]() |
f4ea623e65 | ||
![]() |
a74946d8ba | ||
![]() |
5cf9ce5a19 | ||
![]() |
06b26a347e | ||
![]() |
4a071e90e8 | ||
![]() |
11e7ea11df | ||
![]() |
01b0ac438c | ||
![]() |
3b6c1fdbaa | ||
![]() |
a6119a0910 | ||
![]() |
c42898a939 | ||
![]() |
aaa239113f | ||
![]() |
110d3d7c69 | ||
![]() |
1e9fe91adf | ||
![]() |
b8baa27839 | ||
![]() |
c1c2091a88 | ||
![]() |
8f7aa71a4c | ||
![]() |
d38ea58a27 | ||
![]() |
cb62a51426 | ||
![]() |
c300216307 | ||
![]() |
a01bbedada | ||
![]() |
0d9e1e8f92 | ||
![]() |
93a95db1b2 | ||
![]() |
b22d830b3b | ||
![]() |
de09e6867c | ||
![]() |
6f37102175 | ||
![]() |
351aac987b | ||
![]() |
9c534fe870 | ||
![]() |
fbe2745b2d | ||
![]() |
cfa03593a6 | ||
![]() |
b13771496f | ||
![]() |
a7cc1247d1 | ||
![]() |
323008553e | ||
![]() |
253017fe8e | ||
![]() |
97ee0973dd | ||
![]() |
a0f3c50ceb | ||
![]() |
0a9ae2505d | ||
![]() |
5bb7a37121 | ||
![]() |
532c4bb639 | ||
![]() |
218c9ef1c3 | ||
![]() |
21016e21a5 | ||
![]() |
20ca0ccf12 | ||
![]() |
a96c2013b7 | ||
![]() |
bf9733f7ef | ||
![]() |
7566d96b35 | ||
![]() |
4b8642d1d0 | ||
![]() |
3ca773f54d | ||
![]() |
3dbdfdcffc | ||
![]() |
4acea43b12 | ||
![]() |
aabc4b701e | ||
![]() |
1325c85a48 | ||
![]() |
3f3134435e | ||
![]() |
2d370b9360 | ||
![]() |
dab6df1a77 | ||
![]() |
4390f6d75e | ||
![]() |
4d1730882b | ||
![]() |
6bff123aea | ||
![]() |
e255caefc5 | ||
![]() |
a62ebd7f71 | ||
![]() |
376d25ad8b | ||
![]() |
dfc89f9d6e | ||
![]() |
62114e309f | ||
![]() |
c6901cae98 | ||
![]() |
d4b760f7b3 | ||
![]() |
5e21607153 | ||
![]() |
3e79c6caab | ||
![]() |
fee26ea12c | ||
![]() |
b551e5336c | ||
![]() |
172ecace2f | ||
![]() |
e146d42598 | ||
![]() |
0546406d4b | ||
![]() |
4a5ca8d3ea | ||
![]() |
267341b9b7 | ||
![]() |
c3a15bc297 | ||
![]() |
4b1d99fbf4 | ||
![]() |
c4c1193094 | ||
![]() |
03adc9b7bc | ||
![]() |
fd491e45e9 | ||
![]() |
a19c1d64e4 | ||
![]() |
836c8f2dba | ||
![]() |
01f2001a0d | ||
![]() |
8136b719bd | ||
![]() |
20870a78c2 | ||
![]() |
e741a6f370 | ||
![]() |
e76ef06074 | ||
![]() |
0fd05e34c4 | ||
![]() |
b11c80361a | ||
![]() |
f19c0d14ed | ||
![]() |
b242a9df79 | ||
![]() |
9fed9613fa | ||
![]() |
01fa484a5e | ||
![]() |
9a7e1710a4 | ||
![]() |
d1957e4056 | ||
![]() |
68c96c5c9f | ||
![]() |
617897be5b | ||
![]() |
b116388c7e | ||
![]() |
473d4da923 | ||
![]() |
4c15aa2cf2 | ||
![]() |
73f1d98d06 | ||
![]() |
4e75a68ad3 | ||
![]() |
c0cf71a6f5 | ||
![]() |
4b5b7e8f5a | ||
![]() |
e0dff46a28 | ||
![]() |
505b6ace2b | ||
![]() |
4845574506 | ||
![]() |
a9f69d750b | ||
![]() |
f883167acc | ||
![]() |
3d89b04ac4 | ||
![]() |
3ffa6c93e4 | ||
![]() |
6be95e9fbf | ||
![]() |
be0d4241e4 | ||
![]() |
dc0b951dfa | ||
![]() |
b92b0ec82b | ||
![]() |
774fd07ca8 | ||
![]() |
a18a1ace4d | ||
![]() |
728ac246d5 | ||
![]() |
a1b5ff5372 | ||
![]() |
80b98ca5e4 | ||
![]() |
4d22570b55 | ||
![]() |
f1a2bb6677 | ||
![]() |
b213e344fb | ||
![]() |
ed61daeb16 | ||
![]() |
4e752e95a3 | ||
![]() |
e459e07af5 | ||
![]() |
1aa82f1c80 | ||
![]() |
ae9dca08b5 | ||
![]() |
7af0ba734d | ||
![]() |
d200ee2d0d | ||
![]() |
597bde73d4 | ||
![]() |
44642e29e4 | ||
![]() |
d01091c094 | ||
![]() |
9cbe0a89a7 | ||
![]() |
09c53b975e | ||
![]() |
356d13cae7 | ||
![]() |
37a3caf5a9 | ||
![]() |
baaad3a443 | ||
![]() |
d679f40082 | ||
![]() |
9a7cd4f7ef | ||
![]() |
f94ca28f79 | ||
![]() |
f264ae11f4 | ||
![]() |
d864979a03 | ||
![]() |
0a6d3d1adb | ||
![]() |
1d9a5047ad | ||
![]() |
e3ec60d5f0 | ||
![]() |
7d13b85641 | ||
![]() |
5d507846bc | ||
![]() |
b75e141298 | ||
![]() |
884c74fbb2 | ||
![]() |
bcf4d508f1 | ||
![]() |
dc539e6820 | ||
![]() |
6f2eb61779 | ||
![]() |
9f616b80ad | ||
![]() |
054616a1cc | ||
![]() |
cec851802c | ||
![]() |
3a0185547b | ||
![]() |
3ff94c62d8 | ||
![]() |
5c389ba4f8 | ||
![]() |
17e68174e0 | ||
![]() |
e709161793 | ||
![]() |
20f73f04d0 | ||
![]() |
fbac5eca10 | ||
![]() |
cad57ed57f | ||
![]() |
4df28ca379 | ||
![]() |
1a89311bf1 | ||
![]() |
e419fbf34f | ||
![]() |
84f2ab1730 | ||
![]() |
4d0d230a2a | ||
![]() |
b1c73bfa1d | ||
![]() |
f6b5f846a0 | ||
![]() |
db0f79925b | ||
![]() |
f23ba3ef04 | ||
![]() |
8c0adc2057 | ||
![]() |
a9276c6be4 | ||
![]() |
e9239f7cd6 | ||
![]() |
3607ed03a4 | ||
![]() |
0fbc118070 | ||
![]() |
6a620a5384 | ||
![]() |
c87d215ca7 | ||
![]() |
2a31a1ff29 | ||
![]() |
cdc50c727e | ||
![]() |
73c788b92a | ||
![]() |
b14ddf04bd | ||
![]() |
2099a5a9ce | ||
![]() |
e645dde8cb | ||
![]() |
481c01a2f1 | ||
![]() |
3cbff07c4d | ||
![]() |
afff916b38 | ||
![]() |
89b4bf9ba9 | ||
![]() |
0d2c2589ff | ||
![]() |
8bad0ff2f4 | ||
![]() |
5f395cb853 | ||
![]() |
7fff5d5b81 | ||
![]() |
b025fee131 | ||
![]() |
be6bed7ef6 | ||
![]() |
14d3a5d24c | ||
![]() |
185e8c2872 | ||
![]() |
911acf5563 | ||
![]() |
06f4d4bdf6 | ||
![]() |
51df6af4d5 | ||
![]() |
2b82cfeb17 | ||
![]() |
45412a5b9f | ||
![]() |
0d02fe5357 | ||
![]() |
7173691477 | ||
![]() |
48b9a4d5dc | ||
![]() |
76dd970ded | ||
![]() |
9c963374c1 | ||
![]() |
9be077c0af | ||
![]() |
6980e289ba | ||
![]() |
4ac09f6b2b | ||
![]() |
62e9b39ac5 | ||
![]() |
f9b0d26c7f | ||
![]() |
43e8ae3bdd | ||
![]() |
695c2580cd | ||
![]() |
41a7111e19 | ||
![]() |
0338f74b45 | ||
![]() |
baa1858a95 | ||
![]() |
e011b47e58 | ||
![]() |
a97c8a0360 | ||
![]() |
976ffab80f | ||
![]() |
84a363fe4d | ||
![]() |
62d05d7f25 | ||
![]() |
66e5170691 | ||
![]() |
da16dd3eff | ||
![]() |
00f0f85cd8 | ||
![]() |
8c864ea97b | ||
![]() |
b20083c572 | ||
![]() |
c91a774849 | ||
![]() |
835be2ae12 | ||
![]() |
3831c17d45 | ||
![]() |
3b2779dacc | ||
![]() |
31a0a79438 | ||
![]() |
310e04cd63 | ||
![]() |
ebd84c318d | ||
![]() |
3b5a918176 | ||
![]() |
c878278c4c | ||
![]() |
2fb60ffdd2 | ||
![]() |
3ed09450bf | ||
![]() |
6ba9228ee1 | ||
![]() |
9cbc5fe090 | ||
![]() |
4cb605f65e | ||
![]() |
b9f7b1b927 | ||
![]() |
1ce313ef8d | ||
![]() |
92f40dee15 | ||
![]() |
349d4336c4 | ||
![]() |
d678395574 | ||
![]() |
699a08aed7 | ||
![]() |
eb17963a56 | ||
![]() |
0be2c0be9d | ||
![]() |
3e52ed7c45 | ||
![]() |
84a89d4f74 | ||
![]() |
80cc680fff | ||
![]() |
255f47483f | ||
![]() |
10f5c6b509 | ||
![]() |
6e0fac82d3 | ||
![]() |
2be2924fa0 | ||
![]() |
9fd4565e4a | ||
![]() |
4e5ea9c740 | ||
![]() |
0ab0d44cd3 | ||
![]() |
6c26b4e569 | ||
![]() |
5b0253d3a6 | ||
![]() |
28a654fccc | ||
![]() |
bda68283d3 | ||
![]() |
eb7140181d | ||
![]() |
9b93b1c568 | ||
![]() |
d32aed4330 | ||
![]() |
419f9f2a2f | ||
![]() |
86dfb3dff2 | ||
![]() |
4a650dc607 | ||
![]() |
6db2847d10 | ||
![]() |
0c24264185 | ||
![]() |
9391e8090a | ||
![]() |
c0c3ca85d7 | ||
![]() |
d183629ca4 | ||
![]() |
cd4846f8aa | ||
![]() |
997bd7f6b3 | ||
![]() |
a93a36c94a | ||
![]() |
84314ed5ec | ||
![]() |
ae09870836 | ||
![]() |
bff8aaee01 | ||
![]() |
b059d9ac0b | ||
![]() |
46c9b1d2a9 | ||
![]() |
3e36e07d80 | ||
![]() |
0080bcfce5 | ||
![]() |
0bd6ce4b47 | ||
![]() |
f4c88d5669 | ||
![]() |
b6038126c5 | ||
![]() |
5ab897112c | ||
![]() |
1b2d4e59a3 | ||
![]() |
569860cde6 | ||
![]() |
7de2e0975d | ||
![]() |
653bcd7898 | ||
![]() |
733b0b45ce | ||
![]() |
ed2c4ccbba | ||
![]() |
aab15eef3e | ||
![]() |
ce7ede5fb0 | ||
![]() |
944ffba66f | ||
![]() |
e600142ee6 | ||
![]() |
3a3e9f8701 | ||
![]() |
dfdfeae79f | ||
![]() |
a6b49fa014 | ||
![]() |
a706962edd | ||
![]() |
e69fd9f2f8 | ||
![]() |
3f7b5034e7 | ||
![]() |
fcbecec034 | ||
![]() |
e77fc90d90 | ||
![]() |
112b16d9df | ||
![]() |
dc4e625750 | ||
![]() |
a25eae3edd | ||
![]() |
7f58444112 | ||
![]() |
6527ee5a11 | ||
![]() |
52a2558d11 | ||
![]() |
21a56e4a0b | ||
![]() |
c2fdeda13e | ||
![]() |
4e0f644a55 | ||
![]() |
257ffe1c07 | ||
![]() |
5416ee4c7c | ||
![]() |
833314cf7a | ||
![]() |
359c2c2bc6 | ||
![]() |
e8e734be56 | ||
![]() |
556e38edce | ||
![]() |
aaeddcf0e1 | ||
![]() |
6139cb6d98 | ||
![]() |
aff50136ef | ||
![]() |
8a8c60adab | ||
![]() |
c09b46e964 | ||
![]() |
ed7ffe00a3 | ||
![]() |
67b8e63637 | ||
![]() |
c6a8e4de1e | ||
![]() |
59982f4d84 | ||
![]() |
ab6e573497 | ||
![]() |
dd377b2a0f | ||
![]() |
c6607c4d19 | ||
![]() |
f881d98ae7 | ||
![]() |
7a88e72f82 | ||
![]() |
d132221f9f | ||
![]() |
46a7204d41 | ||
![]() |
c46038b47e | ||
![]() |
1ec6828470 | ||
![]() |
8d5d10b352 | ||
![]() |
09585d312b | ||
![]() |
7e8b2ad87e | ||
![]() |
26c6bd8942 | ||
![]() |
d198280f6c | ||
![]() |
573adc72a2 | ||
![]() |
94dcc50578 | ||
![]() |
1b8591d297 | ||
![]() |
a3afb5551b | ||
![]() |
cda10a8aff | ||
![]() |
a0d7a0a178 | ||
![]() |
c94e69abbb | ||
![]() |
c1227e5e65 | ||
![]() |
f535d1a2c1 | ||
![]() |
a2be6780de | ||
![]() |
fc174d14c4 | ||
![]() |
69fef3808b | ||
![]() |
d63e67f1ef | ||
![]() |
bbbde64199 | ||
![]() |
136eb8bbad | ||
![]() |
220952d014 | ||
![]() |
789ccd857a | ||
![]() |
4270b2c013 | ||
![]() |
98f432ff8e | ||
![]() |
3aeb0dde1f | ||
![]() |
a6e3e4c1d9 | ||
![]() |
34defc0327 | ||
![]() |
a76508746a | ||
![]() |
624c80e80a | ||
![]() |
0f695e41e1 | ||
![]() |
13dbb4fcc5 | ||
![]() |
59d5d7e0b5 | ||
![]() |
8f6323fd38 | ||
![]() |
4f5ba867be | ||
![]() |
2ff791a31b | ||
![]() |
678b8edb87 | ||
![]() |
6e29b3ad25 | ||
![]() |
31e812c084 | ||
![]() |
4b3dd5ba71 | ||
![]() |
941219be23 | ||
![]() |
c74ee59fee | ||
![]() |
b35bcdb09e | ||
![]() |
e3fdaf7e9d | ||
![]() |
55b438017f | ||
![]() |
52a95589d6 | ||
![]() |
e1351768c8 | ||
![]() |
03867d184a | ||
![]() |
63511c149a | ||
![]() |
28aa841d8e | ||
![]() |
cbd1d07c91 | ||
![]() |
b8add8f2ed | ||
![]() |
632e9d492f | ||
![]() |
5f848e58c3 | ||
![]() |
f67c30d062 | ||
![]() |
195b05cd44 | ||
![]() |
09ebef19dd | ||
![]() |
d465b5df2a | ||
![]() |
8a88c67b60 | ||
![]() |
4f3acdee72 | ||
![]() |
dab9274cdd | ||
![]() |
b0991c13d9 | ||
![]() |
0d727dd01a | ||
![]() |
d4b6a1a89c | ||
![]() |
425736cb55 | ||
![]() |
094f35e1a1 | ||
![]() |
6f93afcbd7 | ||
![]() |
71678ed812 | ||
![]() |
d19c45d42c | ||
![]() |
9d29320bc0 | ||
![]() |
f083ba7f8d | ||
![]() |
bfa4205bf7 | ||
![]() |
c4d3cb7d4e | ||
![]() |
1fe128e53b | ||
![]() |
25f3c1486c | ||
![]() |
620f837b82 | ||
![]() |
65a58cb080 | ||
![]() |
f9d9d8eefc | ||
![]() |
5f5d8dea8a | ||
![]() |
230827cba5 | ||
![]() |
53c7487113 | ||
![]() |
d5b4287f41 | ||
![]() |
c29b7a5c7a | ||
![]() |
a56dd2a529 | ||
![]() |
14b59805a2 | ||
![]() |
e01e1be3de | ||
![]() |
d403251fd7 | ||
![]() |
d46c9d35b7 | ||
![]() |
5fa9193415 | ||
![]() |
239767be22 | ||
![]() |
7ce49d3566 | ||
![]() |
e228cca96b | ||
![]() |
92f2b0b0e1 | ||
![]() |
01d1639141 | ||
![]() |
11272a101f | ||
![]() |
d2bdb7ab0b | ||
![]() |
bc0d3b1a67 | ||
![]() |
e2451d9c7f | ||
![]() |
670887e3f3 | ||
![]() |
345e3535b4 | ||
![]() |
5eeeba0333 | ||
![]() |
df351f3b30 | ||
![]() |
29d5da3e66 | ||
![]() |
12d35d3da3 | ||
![]() |
a067dd0faa | ||
![]() |
4758d28ac1 | ||
![]() |
bfc9692ad1 | ||
![]() |
a23fef8afe | ||
![]() |
2827117eb4 | ||
![]() |
e11d71d9c1 | ||
![]() |
0aacafc032 | ||
![]() |
573e947b31 | ||
![]() |
128e5a0ef0 | ||
![]() |
114033dde4 | ||
![]() |
3cc0f2e452 | ||
![]() |
28a08686a8 | ||
![]() |
100e47307b | ||
![]() |
254e6f4d30 | ||
![]() |
d63d5a2602 | ||
![]() |
729cdc0c2b | ||
![]() |
253ac4af2d | ||
![]() |
63863f2530 | ||
![]() |
3bd04e55ab | ||
![]() |
8fd9b5c791 | ||
![]() |
3c7671f160 | ||
![]() |
16b50fad38 | ||
![]() |
a09bd51978 | ||
![]() |
829624199b | ||
![]() |
3fa3460d1a | ||
![]() |
309c1252a6 | ||
![]() |
444673a54e | ||
![]() |
0f58f4dbeb | ||
![]() |
bbb4d88c2a | ||
![]() |
ad0964116a | ||
![]() |
91a58912ce | ||
![]() |
2c8a913770 | ||
![]() |
c37b4ee3f7 | ||
![]() |
85726121a8 | ||
![]() |
1b65578c44 | ||
![]() |
24fe9663f0 | ||
![]() |
a54bccf04e | ||
![]() |
50777b6b43 | ||
![]() |
fde9dfe20f | ||
![]() |
d2ddce8012 | ||
![]() |
09a3831733 | ||
![]() |
a036419535 | ||
![]() |
e278d51713 | ||
![]() |
fcfafce65b | ||
![]() |
886eb2c588 | ||
![]() |
cfa71f5045 | ||
![]() |
f5c100ed62 | ||
![]() |
bd6f81b088 | ||
![]() |
13f2105589 | ||
![]() |
80e9e037ea | ||
![]() |
98a6ea7e3a | ||
![]() |
83b72ace65 | ||
![]() |
406fb15bd9 | ||
![]() |
a50b9be1de | ||
![]() |
71cc6a3bfd | ||
![]() |
ec99637bb2 | ||
![]() |
5d4ee260b3 | ||
![]() |
32e295ed8c | ||
![]() |
c22843a7f1 | ||
![]() |
be155a676f | ||
![]() |
7288582ef2 | ||
![]() |
e1376f3f74 | ||
![]() |
7f25d2f5b5 | ||
![]() |
e36f9f56d0 | ||
![]() |
2cf88adca9 | ||
![]() |
07ad3271b4 | ||
![]() |
6861334dad | ||
![]() |
fbc27a7fd1 | ||
![]() |
557aa9fa33 | ||
![]() |
376c7590b5 | ||
![]() |
f258ab3a4e | ||
![]() |
c4f6ff7acc | ||
![]() |
b0e7b86670 | ||
![]() |
723e1f7b78 | ||
![]() |
e6ac11c33d | ||
![]() |
fbc5f0460d | ||
![]() |
4c81264c8c | ||
![]() |
fd6750f4cf | ||
![]() |
c5fed6cdf1 | ||
![]() |
4e9f6f3e0e | ||
![]() |
b9b37c2b33 | ||
![]() |
679a293a18 | ||
![]() |
d51dca6c40 | ||
![]() |
a0f0aaab05 | ||
![]() |
5af4b4e7ce | ||
![]() |
c688d4cbe4 | ||
![]() |
c98d4795fb | ||
![]() |
c3ceace600 | ||
![]() |
3dd2ef328c | ||
![]() |
1f75307d2f | ||
![]() |
b1b1cacddf | ||
![]() |
ed564b64ec | ||
![]() |
6eb4c1e59f | ||
![]() |
4009c4d69c | ||
![]() |
c143334183 | ||
![]() |
bddacee80d | ||
![]() |
e6d5fa793d | ||
![]() |
ffc0752cba | ||
![]() |
a8de15517a | ||
![]() |
a6fb388f5d | ||
![]() |
1a61e8f98a | ||
![]() |
defbbc327c | ||
![]() |
12f5d3c516 | ||
![]() |
045f75161e | ||
![]() |
dfcbfaf155 | ||
![]() |
d233f3b60f | ||
![]() |
b4e84e1cb9 | ||
![]() |
630991b85a | ||
![]() |
2afbb7b0cd | ||
![]() |
e95f4bd56a | ||
![]() |
88b7830aa7 | ||
![]() |
035d6b8b95 | ||
![]() |
d3f1d67e82 | ||
![]() |
27b3e5ad99 | ||
![]() |
7512deca3d | ||
![]() |
66eaa610d1 | ||
![]() |
6e4dcd4829 | ||
![]() |
d24c462d1c | ||
![]() |
070b693163 | ||
![]() |
12c697a67c | ||
![]() |
8e260bb8f0 | ||
![]() |
62c096deb2 | ||
![]() |
afddbd88c6 | ||
![]() |
e907c2cb98 | ||
![]() |
4f1df9a3bf | ||
![]() |
3ac658dd26 | ||
![]() |
1c5581be0d | ||
![]() |
6a0a34e0e7 | ||
![]() |
f560a7da67 | ||
![]() |
2d4f69f83f | ||
![]() |
c124d99010 | ||
![]() |
59e5678151 | ||
![]() |
a179dd38cf | ||
![]() |
e014ffb5b3 | ||
![]() |
21453a4858 | ||
![]() |
58b78e0744 | ||
![]() |
ba14b526ab | ||
![]() |
3aacd5baba | ||
![]() |
4625f1f01a | ||
![]() |
8395c84aa9 | ||
![]() |
ee6d0a2a2c | ||
![]() |
0d91a3ee98 | ||
![]() |
5036bac094 | ||
![]() |
b82556aafd | ||
![]() |
50d04b5802 | ||
![]() |
fd917664e7 | ||
![]() |
b3da75f525 | ||
![]() |
93d61a66c1 | ||
![]() |
bed4fb9556 | ||
![]() |
7567ea5b20 | ||
![]() |
964c253fb8 | ||
![]() |
dba1ac4ac6 | ||
![]() |
f30f1e9eff | ||
![]() |
a74bbb0549 | ||
![]() |
b5cc1b4ee0 | ||
![]() |
3df2c55850 | ||
![]() |
0e0b3c62bb | ||
![]() |
ef9b51b47c | ||
![]() |
972db67e55 | ||
![]() |
e61102d919 | ||
![]() |
460ea95712 | ||
![]() |
be199f7abb | ||
![]() |
710bffc99c | ||
![]() |
dec4acdf27 | ||
![]() |
dfd70691a9 | ||
![]() |
50f7c9d484 | ||
![]() |
f755b09d0d | ||
![]() |
70db9f695d | ||
![]() |
7523ef5869 | ||
![]() |
b7bc8e43ff | ||
![]() |
21c4bac6fc | ||
![]() |
e90f450bce | ||
![]() |
816d3b4b9f | ||
![]() |
9c41187f82 | ||
![]() |
24b3f87fb3 | ||
![]() |
acdae07f35 | ||
![]() |
a8a67eee74 | ||
![]() |
88fe797d83 | ||
![]() |
d832cfdf71 | ||
![]() |
4af813173c | ||
![]() |
ceeef17c00 | ||
![]() |
90a609e7fb | ||
![]() |
4091d1ef45 | ||
![]() |
8bfbb60161 | ||
![]() |
cea8eb7c35 | ||
![]() |
339904c990 | ||
![]() |
05a087a83d | ||
![]() |
c82509b5d2 | ||
![]() |
b8ba1918c9 | ||
![]() |
f12bf79694 | ||
![]() |
0b276a371e | ||
![]() |
0902c97806 | ||
![]() |
32d460878d | ||
![]() |
9a379fb81c | ||
![]() |
2f207b561a | ||
![]() |
14304f8402 | ||
![]() |
d9421730a4 | ||
![]() |
fe71ee55c6 | ||
![]() |
8b9cfc82ea | ||
![]() |
2384ac65c3 | ||
![]() |
71d487762e | ||
![]() |
089136d6e6 | ||
![]() |
f9955e079e | ||
![]() |
1471f87845 | ||
![]() |
9467d53088 | ||
![]() |
20eec11057 | ||
![]() |
49dd1c376f | ||
![]() |
47b4cc67fc | ||
![]() |
b4707a1f9f | ||
![]() |
c79060a5a4 | ||
![]() |
3881f18b01 | ||
![]() |
a0d5fbac19 | ||
![]() |
56cb6e7b4f | ||
![]() |
2cc02f02b1 | ||
![]() |
8f790a65a6 | ||
![]() |
fcd7f8daf4 | ||
![]() |
6db4b713eb | ||
![]() |
f902a93ea1 | ||
![]() |
a5e07e633d | ||
![]() |
484a10779b | ||
![]() |
62a9a97323 | ||
![]() |
37d581abb7 | ||
![]() |
036f973fa2 | ||
![]() |
f4eeb8038e | ||
![]() |
a29c8365d9 | ||
![]() |
5d420f54a7 | ||
![]() |
35f4ab615f | ||
![]() |
043eadf13c | ||
![]() |
a24077c0be | ||
![]() |
f958c99a84 | ||
![]() |
cca43eee02 | ||
![]() |
0d6e9c9537 | ||
![]() |
9c94ea1544 | ||
![]() |
fb6804fbd3 | ||
![]() |
68c6247272 | ||
![]() |
18042a880d | ||
![]() |
ac06d58d48 | ||
![]() |
05d462a07d | ||
![]() |
ab26013d32 | ||
![]() |
2dcb0ccc1a | ||
![]() |
73f5f9ce9b | ||
![]() |
b4fcde00d1 | ||
![]() |
9d1a01d56d | ||
![]() |
f6375b77d5 | ||
![]() |
ff1c9b3405 | ||
![]() |
9465e45c97 | ||
![]() |
a573fe6b71 | ||
![]() |
98655e64d5 | ||
![]() |
d35214c11b | ||
![]() |
2f813d00ed | ||
![]() |
580724793b | ||
![]() |
fcb9b36b96 | ||
![]() |
18e37b3abf | ||
![]() |
379977edbe | ||
![]() |
bf061edb78 | ||
![]() |
616f84bc22 | ||
![]() |
a16be8b5c5 | ||
![]() |
296e50678a | ||
![]() |
a6bfa82517 | ||
![]() |
050ba515d6 | ||
![]() |
d5f559803f | ||
![]() |
d0e64458ad | ||
![]() |
7715aa6360 | ||
![]() |
12e196efee | ||
![]() |
bae2574225 | ||
![]() |
3137d222ad | ||
![]() |
4dd0273105 | ||
![]() |
b52ed3425f | ||
![]() |
9ed6b2576d | ||
![]() |
c5d0401fe1 | ||
![]() |
da352d54e3 | ||
![]() |
40acfd5a25 | ||
![]() |
469ab67b2b | ||
![]() |
94c8ca8ced | ||
![]() |
096d6cdb80 | ||
![]() |
a35abe3398 | ||
![]() |
d987d60c14 | ||
![]() |
04c2778906 | ||
![]() |
e823664f11 | ||
![]() |
e09532fd6d | ||
![]() |
26126fd2a5 | ||
![]() |
e8bdb88744 | ||
![]() |
b91cebbe52 | ||
![]() |
90082d3c0d | ||
![]() |
de6f6e26c2 | ||
![]() |
8ae8cab963 | ||
![]() |
436fe171ca | ||
![]() |
0f7f5e0f15 | ||
![]() |
3da680ad39 | ||
![]() |
91d688d60d | ||
![]() |
eea678f3c2 | ||
![]() |
87ddb85ce7 | ||
![]() |
01619e0dba | ||
![]() |
63289b016b | ||
![]() |
466bfc5808 | ||
![]() |
8509ad4ea8 | ||
![]() |
f607f3ff01 | ||
![]() |
ac4942e64a | ||
![]() |
df32b9e73c | ||
![]() |
f924b05782 | ||
![]() |
804e408f0b | ||
![]() |
5cf811f565 | ||
![]() |
b0e307af53 | ||
![]() |
2b64bea8b8 | ||
![]() |
b938a4eeca | ||
![]() |
ce1babba5a | ||
![]() |
bdedd55a41 | ||
![]() |
fefce6a17b | ||
![]() |
a9f7c54c74 | ||
![]() |
77bb5164ed | ||
![]() |
aa50fcf3e5 | ||
![]() |
101616769f | ||
![]() |
d86fc99a00 | ||
![]() |
d2d1155b18 | ||
![]() |
a96576d1b2 | ||
![]() |
6d7f05414b | ||
![]() |
f23b6e9350 | ||
![]() |
391db4a941 | ||
![]() |
eccb403da1 | ||
![]() |
cd567c3d7e | ||
![]() |
570d5c3f15 | ||
![]() |
f5a6dffa0b | ||
![]() |
7507dad1de | ||
![]() |
844b52eff6 | ||
![]() |
1ce7dedf67 | ||
![]() |
47e3460a0b | ||
![]() |
d8fbc3236e | ||
![]() |
fb78fee714 | ||
![]() |
27b7338fb8 | ||
![]() |
439319d77b | ||
![]() |
49baa6e291 | ||
![]() |
04eac5ed57 | ||
![]() |
3635c88325 | ||
![]() |
8b4fdd368a | ||
![]() |
70303a9662 | ||
![]() |
a6b5ee5f91 | ||
![]() |
fad2dc5f57 | ||
![]() |
ffb90b6adb | ||
![]() |
0c63d297bb | ||
![]() |
0066c94a74 | ||
![]() |
6c995f172c | ||
![]() |
0fd575172f | ||
![]() |
f84766f497 | ||
![]() |
2795a33335 | ||
![]() |
dc07d39761 | ||
![]() |
074ece4fa4 | ||
![]() |
affa10abe8 | ||
![]() |
68712692fb | ||
![]() |
4764f661a3 | ||
![]() |
e79d4e8ce6 | ||
![]() |
19ecdea035 | ||
![]() |
0519f6de47 | ||
![]() |
7407ae0f31 | ||
![]() |
887de107ef | ||
![]() |
1c471944d1 | ||
![]() |
69ee56a228 | ||
![]() |
5371082087 | ||
![]() |
2709ccdb56 | ||
![]() |
02361a92dc | ||
![]() |
175c931ddc | ||
![]() |
350ba84864 | ||
![]() |
9cca06ac84 | ||
![]() |
5eb4a3a448 | ||
![]() |
e2a9974791 | ||
![]() |
40322d37b6 | ||
![]() |
e42426655d | ||
![]() |
d6b0990e2d | ||
![]() |
54c1909b9e | ||
![]() |
7c8daea65b | ||
![]() |
9b6ba7ada6 | ||
![]() |
df389c37a4 | ||
![]() |
b616db2ab5 | ||
![]() |
c43d16f435 | ||
![]() |
9d9c65dcbd | ||
![]() |
3b01f2cfd4 | ||
![]() |
e29d4cbbdc | ||
![]() |
de73b0df95 | ||
![]() |
bdca46fb3f | ||
![]() |
89e7e0507f | ||
![]() |
1092769eaf | ||
![]() |
76c95fadd9 | ||
![]() |
40a945c5c6 | ||
![]() |
fc029f1581 | ||
![]() |
160e8c6f08 | ||
![]() |
52ac6020dc | ||
![]() |
966404b2fb | ||
![]() |
d151c8be9c | ||
![]() |
bf2852b46b | ||
![]() |
8d9710795b | ||
![]() |
a706856b2a | ||
![]() |
5362326d91 | ||
![]() |
a450687669 | ||
![]() |
ca31245625 | ||
![]() |
ea14136124 | ||
![]() |
69f0b70f65 | ||
![]() |
6998bef4a5 | ||
![]() |
ffbeee5e5e | ||
![]() |
55a203a413 | ||
![]() |
d6e91d4cdb | ||
![]() |
b70bc98b4b | ||
![]() |
aaf30dc34e | ||
![]() |
d44e0071f0 | ||
![]() |
790f1a65dc | ||
![]() |
f7e809c756 | ||
![]() |
4ee7585115 | ||
![]() |
5a7dd4da43 | ||
![]() |
176030fe23 | ||
![]() |
ecd17e23b8 | ||
![]() |
26df1db785 | ||
![]() |
5666a0d027 | ||
![]() |
43956387b6 | ||
![]() |
a6c594a4fa | ||
![]() |
733c5881b0 | ||
![]() |
6017f5b5a3 | ||
![]() |
db4c6f8901 | ||
![]() |
73d17e6c08 | ||
![]() |
8bc520f05f | ||
![]() |
8794935ecb | ||
![]() |
9ec989fbef | ||
![]() |
470fd2e90c | ||
![]() |
67d9217c2c | ||
![]() |
fc5f238985 | ||
![]() |
9442c0c9ed | ||
![]() |
5afbf5a459 | ||
![]() |
a8408a5344 | ||
![]() |
ecf5f2a472 | ||
![]() |
66a635eea3 | ||
![]() |
316c8b85f8 | ||
![]() |
7bfb114ca4 | ||
![]() |
8eddf43b93 | ||
![]() |
e83a7b7f93 | ||
![]() |
c370a5bfeb | ||
![]() |
0a0edfa77d | ||
![]() |
e49845e097 | ||
![]() |
0cef729b9d | ||
![]() |
59f9d60230 | ||
![]() |
8c93c24be5 | ||
![]() |
c31f1031ba | ||
![]() |
033d32f2e9 | ||
![]() |
c9723ca98c | ||
![]() |
018372206d | ||
![]() |
f55a873706 | ||
![]() |
666ad7a867 | ||
![]() |
a032634c31 | ||
![]() |
a5c07896c0 | ||
![]() |
c169aebe71 | ||
![]() |
24420ca3b0 | ||
![]() |
9ac67ea8df | ||
![]() |
23d2c69a7c | ||
![]() |
06adcc87d2 | ||
![]() |
8a0bf98c41 | ||
![]() |
e50436e88c | ||
![]() |
dc1b99c4fd | ||
![]() |
468c30792a | ||
![]() |
a9d9457f58 | ||
![]() |
fcc9f611db | ||
![]() |
ecdf65e937 | ||
![]() |
643c3da142 | ||
![]() |
0e7c995af6 | ||
![]() |
f1738bda97 | ||
![]() |
ede64dc114 | ||
![]() |
42938ac5c5 | ||
![]() |
d7125c969f | ||
![]() |
37535456c3 | ||
![]() |
afd2efafe4 | ||
![]() |
67e2a97bfb | ||
![]() |
8d4f5174b2 | ||
![]() |
271466015b | ||
![]() |
6b52e63c57 | ||
![]() |
dc5f07105f | ||
![]() |
24ea232cdc | ||
![]() |
49cac6ed74 | ||
![]() |
680b26f069 | ||
![]() |
cdc2920522 | ||
![]() |
b52ff062f8 | ||
![]() |
1aed1d64f5 | ||
![]() |
971e09ccc2 | ||
![]() |
aed3f16d61 | ||
![]() |
56a6563bb3 | ||
![]() |
18366513e4 | ||
![]() |
b7c5435de0 | ||
![]() |
778f2fd540 | ||
![]() |
4649bbd853 | ||
![]() |
572c1e3bba | ||
![]() |
772b232f70 | ||
![]() |
86ca8872f5 | ||
![]() |
2e001220c0 | ||
![]() |
edf1a62d92 | ||
![]() |
605d18b9c5 | ||
![]() |
834b254a4a | ||
![]() |
2513479221 | ||
![]() |
41bdf1634d | ||
![]() |
4ff5f9ece3 | ||
![]() |
6d730443c8 | ||
![]() |
f7d52c580e | ||
![]() |
f0bc165b62 | ||
![]() |
d6950a5f88 | ||
![]() |
07c5d52267 | ||
![]() |
bac18cb22f | ||
![]() |
f7873520c5 | ||
![]() |
1acac43403 | ||
![]() |
3b6a48e955 | ||
![]() |
ef51ebb020 | ||
![]() |
05864777df | ||
![]() |
c8b2916c4f | ||
![]() |
3f05e42578 | ||
![]() |
26e046a102 | ||
![]() |
3fc76b9927 | ||
![]() |
85c71304ab | ||
![]() |
f6ddf56967 | ||
![]() |
9a59a18033 | ||
![]() |
f55dc10b1e | ||
![]() |
7128c6ed6c | ||
![]() |
b80290f1c0 | ||
![]() |
8fb81d2140 | ||
![]() |
7a2b187b5f | ||
![]() |
f1728c7889 | ||
![]() |
97d6c921a1 | ||
![]() |
e13b82ebe1 | ||
![]() |
a72c4959b2 | ||
![]() |
739d947ea3 | ||
![]() |
20e8af7298 | ||
![]() |
6064aeea72 | ||
![]() |
122f8540a0 | ||
![]() |
6f0c6ac307 | ||
![]() |
bf26f39c8e | ||
![]() |
2d71df249f | ||
![]() |
2f96c7705e | ||
![]() |
1ea422d4b0 | ||
![]() |
fb519f5210 | ||
![]() |
5488f8f053 | ||
![]() |
3e8297b923 | ||
![]() |
324ff7e6f9 | ||
![]() |
d5ec2c5036 | ||
![]() |
2267d52345 | ||
![]() |
3adb3c3943 | ||
![]() |
33c7fb9cc3 | ||
![]() |
595d2ca370 | ||
![]() |
5f588bb474 | ||
![]() |
96a190538d | ||
![]() |
2237e6be7b | ||
![]() |
6405585cf6 | ||
![]() |
3116bc4298 | ||
![]() |
8b7572d70e | ||
![]() |
d6b0cce4c9 | ||
![]() |
f881130398 | ||
![]() |
266c4626be | ||
![]() |
38290a4887 | ||
![]() |
40b10faa7b | ||
![]() |
a17a122951 | ||
![]() |
0285684c9e | ||
![]() |
20c664d9aa | ||
![]() |
861e9fae97 | ||
![]() |
d86b27e4cd | ||
![]() |
75f127506e | ||
![]() |
4616455ce6 | ||
![]() |
255e639ea5 | ||
![]() |
1cb1325ee6 | ||
![]() |
ebe229c8ed | ||
![]() |
63f972a889 | ||
![]() |
e8e03145a3 | ||
![]() |
d2db5fb4ed | ||
![]() |
b9627e639e | ||
![]() |
9f9678337e | ||
![]() |
ba5dc117a4 | ||
![]() |
a587e9ab58 | ||
![]() |
f93f3732f3 | ||
![]() |
128bc579ce | ||
![]() |
0dd000ce7e | ||
![]() |
926d9e6884 | ||
![]() |
04c46eb668 | ||
![]() |
7e09d00d62 | ||
![]() |
7ef2a08dff | ||
![]() |
ca6d0bb98a | ||
![]() |
b70393f947 | ||
![]() |
df78472681 | ||
![]() |
22d201d2c3 | ||
![]() |
3263596166 | ||
![]() |
43302acd47 | ||
![]() |
c92fb897b0 | ||
![]() |
969cdb6fa5 | ||
![]() |
b7c273dad3 | ||
![]() |
6c392443fc | ||
![]() |
71800f2bdf | ||
![]() |
81718fb708 | ||
![]() |
6420601d00 | ||
![]() |
1ba7399adc | ||
![]() |
2eacd8be18 | ||
![]() |
f9055ae50e | ||
![]() |
26718821c0 | ||
![]() |
0339872a2c | ||
![]() |
86758895fa | ||
![]() |
6390aef437 | ||
![]() |
f782ee3f0e | ||
![]() |
971b46bb0d | ||
![]() |
cdda24b859 | ||
![]() |
bdcbdad7ed | ||
![]() |
ae955d24fa | ||
![]() |
1787fd8612 | ||
![]() |
ea8e86e4a9 | ||
![]() |
2965f7ba26 | ||
![]() |
f5e156d0d3 | ||
![]() |
e14bb1fa3d | ||
![]() |
3d8b49f913 | ||
![]() |
3e58bebc04 | ||
![]() |
69b7e9c80d | ||
![]() |
812d27cc7f | ||
![]() |
7d383e8c00 | ||
![]() |
036c8c0f59 | ||
![]() |
c08d7a771c | ||
![]() |
3bcced5d69 | ||
![]() |
9d5c90b5af | ||
![]() |
404f9328b7 | ||
![]() |
35b16fcd6d | ||
![]() |
0d83490292 | ||
![]() |
3c1c3bbdd7 | ||
![]() |
cc6e945a04 | ||
![]() |
db6ddf6b7c | ||
![]() |
faca670156 | ||
![]() |
07f39be1ff | ||
![]() |
5e18fe4c7f | ||
![]() |
a8312ec874 | ||
![]() |
cef937c880 | ||
![]() |
a8e903a3e9 | ||
![]() |
c57c830316 | ||
![]() |
5b56bfe960 | ||
![]() |
7e3eff3b23 | ||
![]() |
43f42ee9f0 | ||
![]() |
2674fae183 | ||
![]() |
17606e6180 | ||
![]() |
e5592d566e | ||
![]() |
61768c9d81 | ||
![]() |
65f4537bd0 | ||
![]() |
acd4eda04b | ||
![]() |
b51369ad97 | ||
![]() |
5b9ec81880 | ||
![]() |
91b71c8c45 | ||
![]() |
cab7c52280 | ||
![]() |
b1f1ae9570 | ||
![]() |
79dec7adc7 | ||
![]() |
6662cc6828 | ||
![]() |
8511e6d42c | ||
![]() |
aa0c056b2f | ||
![]() |
bacfe9d327 | ||
![]() |
193b40ff12 | ||
![]() |
020b88c77a | ||
![]() |
77726e6606 | ||
![]() |
1f055c0bda | ||
![]() |
0189a75ff3 | ||
![]() |
60d55753e9 | ||
![]() |
c962575aec | ||
![]() |
50da3e0873 | ||
![]() |
8b7a5511b7 | ||
![]() |
572185898c | ||
![]() |
f6ea289a0e | ||
![]() |
dc9b065689 | ||
![]() |
a448dc81c4 | ||
![]() |
d82dae0776 | ||
![]() |
0447ac624d | ||
![]() |
b9c040d9ac | ||
![]() |
fc926448ac | ||
![]() |
70d85530c7 | ||
![]() |
96b515e2fe | ||
![]() |
bced18876d | ||
![]() |
1be9d67d8a | ||
![]() |
bd21a2b57f | ||
![]() |
8cd33a127c | ||
![]() |
19f15becfe | ||
![]() |
4f0e0407f1 | ||
![]() |
dc1f76988a | ||
![]() |
8289f3e0ed | ||
![]() |
a5f517c0d4 | ||
![]() |
3c2cd278f7 | ||
![]() |
c77e248aee | ||
![]() |
2948341728 | ||
![]() |
9eaf570ae7 | ||
![]() |
5d3637d157 | ||
![]() |
955af18a01 | ||
![]() |
77d8fd5493 | ||
![]() |
40e6f4a7f2 | ||
![]() |
640d838e28 | ||
![]() |
aa02c7610e | ||
![]() |
d1179f5ada | ||
![]() |
6df2d19fbf | ||
![]() |
52ba265c71 | ||
![]() |
00d9e244c2 | ||
![]() |
ee0c58f797 | ||
![]() |
2fd1d1c371 | ||
![]() |
7a51bc7f5d | ||
![]() |
28b61d73c8 | ||
![]() |
f8af81fb65 | ||
![]() |
d474fdd404 | ||
![]() |
1afd2d4030 | ||
![]() |
94bc8dd018 | ||
![]() |
f3e85108a8 | ||
![]() |
149a7fc32b | ||
![]() |
7094c4c9b6 | ||
![]() |
e7fb9494cb | ||
![]() |
b335a96722 | ||
![]() |
18f539c709 | ||
![]() |
2bd173501c | ||
![]() |
27c47513e8 | ||
![]() |
bafd3d669b | ||
![]() |
d9a3ec2680 | ||
![]() |
b28f7b9b81 | ||
![]() |
f35b909ba9 | ||
![]() |
dbe71af81a | ||
![]() |
8a94fd0baf | ||
![]() |
b9a4bcb4fc | ||
![]() |
11f6dd674c | ||
![]() |
edfdf8ad21 | ||
![]() |
40a4928f6e | ||
![]() |
b35257ab5b | ||
![]() |
4b9f2a722d | ||
![]() |
7c88a7c48b | ||
![]() |
a8adca5d98 | ||
![]() |
ee88a62da8 | ||
![]() |
acb23a5a7a | ||
![]() |
1b63aa136b | ||
![]() |
cea3bb9764 | ||
![]() |
1840949a81 | ||
![]() |
b6678560ec | ||
![]() |
01c144de36 | ||
![]() |
5f387aeed1 | ||
![]() |
1faad458f6 | ||
![]() |
3357340772 | ||
![]() |
053606e460 | ||
![]() |
280bbc3e58 | ||
![]() |
6adf3eb827 | ||
![]() |
2259ea6e80 | ||
![]() |
51eec9d02c | ||
![]() |
6dcfe61f2e | ||
![]() |
066bcddc82 | ||
![]() |
67866e70cf | ||
![]() |
3509c2e812 | ||
![]() |
4f0ea27632 | ||
![]() |
5eb7b5a63e | ||
![]() |
865576946c | ||
![]() |
5a376e0015 | ||
![]() |
d34d43d730 | ||
![]() |
f7b4d9d424 | ||
![]() |
10b7e96c00 | ||
![]() |
a5c09041e9 | ||
![]() |
c46e268ad0 | ||
![]() |
5b6ce786a1 | ||
![]() |
70a2ccb51a | ||
![]() |
a89a072cb4 | ||
![]() |
65c040c075 | ||
![]() |
39b9e1e7a0 | ||
![]() |
cb3050294b | ||
![]() |
455eeb63b4 | ||
![]() |
be4eb1e37e | ||
![]() |
f3ff017e6f | ||
![]() |
56c18f44c7 | ||
![]() |
da2695ac35 | ||
![]() |
5614f1c62f | ||
![]() |
b7d9e49039 | ||
![]() |
4da5b9e7b6 | ||
![]() |
81958d4a3d | ||
![]() |
12a7eb4885 | ||
![]() |
dffc310969 | ||
![]() |
80ad916f4f | ||
![]() |
d216420f30 | ||
![]() |
4cf68fa98f | ||
![]() |
2996eb0bb5 | ||
![]() |
422e6ad51e | ||
![]() |
922675bf53 | ||
![]() |
d92f555e93 | ||
![]() |
48433bc9eb | ||
![]() |
db3f73c643 | ||
![]() |
ad0f8155a7 | ||
![]() |
060eee3afd | ||
![]() |
407ef4699d | ||
![]() |
1ffb358d59 | ||
![]() |
d9c4e12642 | ||
![]() |
310f561488 | ||
![]() |
dcd3636c96 | ||
![]() |
a5442f8b7a | ||
![]() |
1dc32d9e0a | ||
![]() |
a767150e87 | ||
![]() |
9691492f38 | ||
![]() |
2f52f28631 | ||
![]() |
e0bef262a7 | ||
![]() |
4cffdf216d | ||
![]() |
2628d6807c | ||
![]() |
3e1012b080 | ||
![]() |
6e575f2f8b | ||
![]() |
ec7fa95672 | ||
![]() |
753f492c86 | ||
![]() |
8b04af03a1 | ||
![]() |
8830f1138d | ||
![]() |
4567a16d65 | ||
![]() |
19eaf8240a | ||
![]() |
4a519f47b5 | ||
![]() |
edebbffe33 | ||
![]() |
156285417b | ||
![]() |
cefc312e85 | ||
![]() |
c63d5da9e1 | ||
![]() |
a934d975e7 | ||
![]() |
f391e7b11a | ||
![]() |
7f93f19314 | ||
![]() |
cb1454f8db | ||
![]() |
da2e507298 | ||
![]() |
df4660c0c1 | ||
![]() |
8751b38988 | ||
![]() |
f59b4f6af4 | ||
![]() |
afcde542f9 | ||
![]() |
4689a6b300 | ||
![]() |
0ae9b383d6 | ||
![]() |
f597bd3e01 | ||
![]() |
4987cc53d0 | ||
![]() |
d917db438e | ||
![]() |
287836fc6b | ||
![]() |
4635477f37 | ||
![]() |
89f77ea24a | ||
![]() |
3dc38ab06e | ||
![]() |
a33a400f01 | ||
![]() |
66a2dcdedc | ||
![]() |
fddea948f0 | ||
![]() |
229ab44918 | ||
![]() |
a6a958de55 | ||
![]() |
c3969dcf12 | ||
![]() |
d906fe2531 | ||
![]() |
3eaa573951 | ||
![]() |
50f98d193d | ||
![]() |
7104d7bf17 | ||
![]() |
8a162a4cb4 | ||
![]() |
9403ef8656 | ||
![]() |
c6a045d092 | ||
![]() |
092263b6b1 | ||
![]() |
6adb44ead4 | ||
![]() |
b71cbe8352 | ||
![]() |
576cb578b0 | ||
![]() |
7198a613dd | ||
![]() |
24d4eafbd0 | ||
![]() |
8b52f043d5 | ||
![]() |
6e2c5d7e82 | ||
![]() |
ae82cf1704 | ||
![]() |
1e0f54b4df | ||
![]() |
4624dd5041 | ||
![]() |
1e54ae0825 | ||
![]() |
6bfc4b7212 | ||
![]() |
81634d45e4 | ||
![]() |
cd1c9391f1 | ||
![]() |
726ccccb1c | ||
![]() |
74942af896 | ||
![]() |
89e52da2fe | ||
![]() |
fcf153e420 | ||
![]() |
c22f0896b7 | ||
![]() |
b3bf5a215f | ||
![]() |
5a0066ab52 | ||
![]() |
b85c365aa9 | ||
![]() |
7b1eaf3f48 | ||
![]() |
16cb262a26 | ||
![]() |
35e2663504 | ||
![]() |
81aaa909af | ||
![]() |
976384a0a7 | ||
![]() |
e989f1ff6c | ||
![]() |
4237138392 | ||
![]() |
192b67ee73 | ||
![]() |
7e2c567e33 | ||
![]() |
0659407aa8 | ||
![]() |
c2f5ac17a7 | ||
![]() |
ab7c9a4106 | ||
![]() |
e9e5671356 | ||
![]() |
f447cbcc9f | ||
![]() |
cea81f15a7 | ||
![]() |
bae392ce50 | ||
![]() |
5eea304b1d | ||
![]() |
dba82deef4 | ||
![]() |
14a64cad6e | ||
![]() |
bf73490b78 | ||
![]() |
50462b4956 | ||
![]() |
64087e6055 | ||
![]() |
8933a677c0 | ||
![]() |
ac487862e9 | ||
![]() |
6202f38f70 | ||
![]() |
a4779bfe73 | ||
![]() |
40874a88ef | ||
![]() |
88f6c86c9e | ||
![]() |
55bc4a6365 | ||
![]() |
3fb6ab5e45 | ||
![]() |
44d7db20e6 | ||
![]() |
cd50aa719f | ||
![]() |
92bc08207c | ||
![]() |
8ffed1b059 | ||
![]() |
5cae67c1cb | ||
![]() |
32df5502ae | ||
![]() |
bbdcd30a73 | ||
![]() |
4c04ce1c48 | ||
![]() |
a464402919 | ||
![]() |
be18f35595 | ||
![]() |
b4f9ee520a | ||
![]() |
455153d728 | ||
![]() |
1353ef62b8 | ||
![]() |
6fad1e745a | ||
![]() |
2c988961f7 | ||
![]() |
2cd9a7697f | ||
![]() |
d398528493 | ||
![]() |
59fe6e7b6d | ||
![]() |
abaf858f6c | ||
![]() |
949c5b7af1 | ||
![]() |
701eb7f9fd | ||
![]() |
07d92f720b | ||
![]() |
d16b808bb9 | ||
![]() |
b2e09157da | ||
![]() |
93ee60cd9b | ||
![]() |
9eb509e068 | ||
![]() |
89f3dfb399 | ||
![]() |
a4c9355b03 | ||
![]() |
c79cdab613 | ||
![]() |
ae3735a150 | ||
![]() |
38a5c6c01a | ||
![]() |
68caccfc2a | ||
![]() |
9482c7adc6 | ||
![]() |
ff23a04e27 | ||
![]() |
9ea989ee2a | ||
![]() |
261bff5bea | ||
![]() |
55740254ed | ||
![]() |
acb4da9f5c | ||
![]() |
78131cf48b | ||
![]() |
415d5d6f6a | ||
![]() |
0789a3db47 | ||
![]() |
7218ddea85 | ||
![]() |
e94ca414fb | ||
![]() |
2b374bfa3d | ||
![]() |
3e334a67ed | ||
![]() |
a140db6244 | ||
![]() |
07c0753d38 | ||
![]() |
26f3b861d7 | ||
![]() |
f115a98333 | ||
![]() |
f45779c41c | ||
![]() |
8446097251 | ||
![]() |
812dfcf633 | ||
![]() |
7b3dcf2f03 | ||
![]() |
aa27d5f8d1 | ||
![]() |
610ee586cb | ||
![]() |
7f56e24078 | ||
![]() |
07bb8c524e | ||
![]() |
9bed7b2086 | ||
![]() |
5c5e6b851c | ||
![]() |
ca415bca50 | ||
![]() |
7d6293d5a0 | ||
![]() |
0d35449faa | ||
![]() |
dd33aefac3 | ||
![]() |
8e137d1f72 | ||
![]() |
2888c3bb6e | ||
![]() |
aaef4e3402 | ||
![]() |
8b0a35c832 | ||
![]() |
ce2da48663 | ||
![]() |
869b89c136 | ||
![]() |
8bcbecc0ec | ||
![]() |
c7a0557e98 | ||
![]() |
c5e75439e4 | ||
![]() |
8a07b5b907 | ||
![]() |
584170eec3 | ||
![]() |
6dd2501a19 | ||
![]() |
3c3c1e9f8c | ||
![]() |
17b01dec90 | ||
![]() |
eb4d64dcd8 | ||
![]() |
dc9f20c19e | ||
![]() |
7c40b5ae3e | ||
![]() |
bda85ff7ef | ||
![]() |
68b1bd80fa | ||
![]() |
5771f5c0b7 | ||
![]() |
fbbd953f42 | ||
![]() |
8d7e8e89db | ||
![]() |
4e52529cbb | ||
![]() |
a77c7bb41a | ||
![]() |
5e2cdeb699 | ||
![]() |
4d077f7324 | ||
![]() |
7fda1f04cf | ||
![]() |
bce1d4a0e8 | ||
![]() |
cecd042357 | ||
![]() |
e4047f89c9 | ||
![]() |
1b457b3efd | ||
![]() |
99fe0df4b1 | ||
![]() |
9f3209c487 | ||
![]() |
4388c33cd4 | ||
![]() |
3e5c45f674 | ||
![]() |
85bc583696 | ||
![]() |
2d2c35b6db | ||
![]() |
1d71f84515 | ||
![]() |
550a388b2b | ||
![]() |
6b523563d8 | ||
![]() |
65bc500598 | ||
![]() |
7949aa1f1c | ||
![]() |
b3bc9b9513 | ||
![]() |
ad5c655b32 | ||
![]() |
b91e6b5e4e | ||
![]() |
06ca0a66de | ||
![]() |
f170f8dfc2 | ||
![]() |
b6b7518992 | ||
![]() |
be7b9ce46b | ||
![]() |
743ae9bef1 | ||
![]() |
1616535de5 | ||
![]() |
18471dbdc8 | ||
![]() |
2c9302b33f | ||
![]() |
f60ae05858 | ||
![]() |
b14aeed40e | ||
![]() |
28a2f33bd3 | ||
![]() |
8183c9a2b9 | ||
![]() |
449f57c0c9 | ||
![]() |
6d3f9b1da1 | ||
![]() |
a4f9983250 | ||
![]() |
cd6f1e7e25 | ||
![]() |
fc5668fed0 | ||
![]() |
6cf27e310f | ||
![]() |
1291a341f1 | ||
![]() |
989b774e9d | ||
![]() |
372ef53228 | ||
![]() |
279c446ca8 | ||
![]() |
afa96620bc | ||
![]() |
acf3847f8d | ||
![]() |
8882514053 | ||
![]() |
2eabd48630 | ||
![]() |
b4f90ab130 | ||
![]() |
e3698ae541 | ||
![]() |
4cccf1b413 | ||
![]() |
e168710885 | ||
![]() |
955892aeec | ||
![]() |
a574f4d2e9 | ||
![]() |
022b39c858 | ||
![]() |
7f1bfe6179 | ||
![]() |
de6b632fa3 | ||
![]() |
8a5de5b143 | ||
![]() |
3cae9483b0 | ||
![]() |
0c65e60ed1 | ||
![]() |
f85ccc1a88 | ||
![]() |
22d9a24ae6 | ||
![]() |
f181775371 | ||
![]() |
a5fbefdc84 | ||
![]() |
d1b10f6276 | ||
![]() |
a04a4a42ed | ||
![]() |
a18159226a | ||
![]() |
0646e4453c | ||
![]() |
2352af7196 | ||
![]() |
46a6886ac4 | ||
![]() |
3ddf1c179a | ||
![]() |
a0b2a0e4e6 | ||
![]() |
43c46052f3 | ||
![]() |
e8832fcb28 | ||
![]() |
bd02772edb | ||
![]() |
d5a38f3799 | ||
![]() |
921dca9f5b | ||
![]() |
8899453891 | ||
![]() |
7392af03a3 | ||
![]() |
7d1f4deb27 | ||
![]() |
66724ac81d | ||
![]() |
2368d34ab3 | ||
![]() |
ddc2d8d2b7 | ||
![]() |
a7012a4b8e | ||
![]() |
88db54e11c | ||
![]() |
bc85f580ec | ||
![]() |
15bfa626b4 | ||
![]() |
068895db0e | ||
![]() |
02fd58ffc8 | ||
![]() |
b7babe554b | ||
![]() |
6a385d6663 | ||
![]() |
93af0ce89d | ||
![]() |
1c025c166a | ||
![]() |
150e0cef5d | ||
![]() |
7f40852b11 | ||
![]() |
0b6fae0243 | ||
![]() |
37af96a577 | ||
![]() |
0e718265fa | ||
![]() |
16b103151b | ||
![]() |
351db6e2b9 | ||
![]() |
b2fb87cd40 | ||
![]() |
388ec9f4db | ||
![]() |
95dd7f0617 | ||
![]() |
2349b9dcfe | ||
![]() |
ddd05ff030 | ||
![]() |
4bc30fda19 | ||
![]() |
285744877c | ||
![]() |
2169ac6fba | ||
![]() |
d2b9034f5e | ||
![]() |
f1bbd3c5a0 | ||
![]() |
aec87f706b | ||
![]() |
7bd06eef1b | ||
![]() |
3be12ef66c | ||
![]() |
8dae7772b0 | ||
![]() |
f69532aebf | ||
![]() |
5f4e421001 | ||
![]() |
b350917852 | ||
![]() |
d3d1e21107 | ||
![]() |
11a7d52578 | ||
![]() |
d8a4736c01 | ||
![]() |
2a4b70e26c | ||
![]() |
353c17dc3b | ||
![]() |
f51efffd3a | ||
![]() |
2618178a93 | ||
![]() |
c57d36ba38 | ||
![]() |
6fa91ecb5c | ||
![]() |
44d61bcb62 | ||
![]() |
dfeddba61b | ||
![]() |
d691aa87ce | ||
![]() |
5a56bbb01d | ||
![]() |
675adb6f0d | ||
![]() |
4969de2250 | ||
![]() |
8f6d5b3871 | ||
![]() |
7732cd1d87 | ||
![]() |
a166aa0ac5 | ||
![]() |
51fe6a8203 | ||
![]() |
287c3fce76 | ||
![]() |
b78404bd85 | ||
![]() |
1e2cc0b6f1 | ||
![]() |
be303375c4 | ||
![]() |
8ba188e429 | ||
![]() |
10842ae839 | ||
![]() |
592b9862e1 | ||
![]() |
01b44663e3 | ||
![]() |
ce59dd6d07 | ||
![]() |
199d0eec81 | ||
![]() |
176558d6d6 | ||
![]() |
94da21229b | ||
![]() |
58298a9758 | ||
![]() |
bc53ef2017 | ||
![]() |
2892c5d77d | ||
![]() |
833e83d57d | ||
![]() |
47369822d8 | ||
![]() |
2da836df9d | ||
![]() |
c42d3700f0 | ||
![]() |
146c599e43 | ||
![]() |
950651ce0a | ||
![]() |
50ef5d664c | ||
![]() |
2382fad4f6 | ||
![]() |
1d1c6f35fa | ||
![]() |
480572f893 | ||
![]() |
68b1fdb20e | ||
![]() |
a38e82f999 | ||
![]() |
a07f6e9e3a | ||
![]() |
7e471b55eb | ||
![]() |
fd43439dab | ||
![]() |
160440b860 | ||
![]() |
84ae576ad0 | ||
![]() |
bc32a00d6a | ||
![]() |
f207bd0a1c | ||
![]() |
99601baffc | ||
![]() |
af1a3d5b76 | ||
![]() |
441b2ff2fd | ||
![]() |
941e350245 | ||
![]() |
36e56267c9 | ||
![]() |
b09b5cb5f4 | ||
![]() |
e32290e9e1 | ||
![]() |
231a806257 | ||
![]() |
782daddfcc | ||
![]() |
51ea204cb7 | ||
![]() |
dd8529743a | ||
![]() |
606979a539 | ||
![]() |
f639d44a95 | ||
![]() |
f1a51e5512 | ||
![]() |
2275532d10 | ||
![]() |
2d03d9b667 | ||
![]() |
917d5c81e3 | ||
![]() |
d67ffe20ea | ||
![]() |
5252cc6f11 | ||
![]() |
f006df974d | ||
![]() |
7bd061d996 | ||
![]() |
2a3aa69f35 | ||
![]() |
5908d8c07e | ||
![]() |
e0fce912d5 | ||
![]() |
8a77638f67 | ||
![]() |
d3c43fb648 | ||
![]() |
1974b49b52 | ||
![]() |
e729048c98 | ||
![]() |
6164bd47f9 | ||
![]() |
791fbfa2e8 | ||
![]() |
fbdbcedf33 | ||
![]() |
ca7c470793 | ||
![]() |
cf787e8b72 | ||
![]() |
998789dbe8 | ||
![]() |
1b7098b775 | ||
![]() |
152dcae5bc | ||
![]() |
42f2e1c5c3 | ||
![]() |
a986a4f0bf | ||
![]() |
f2fc6ee067 | ||
![]() |
043503a739 | ||
![]() |
83cd933987 | ||
![]() |
9efa749059 | ||
![]() |
c8fb8f0b2e | ||
![]() |
ec719c131f | ||
![]() |
90375a2f12 | ||
![]() |
6051275d0a | ||
![]() |
06f0c2388d | ||
![]() |
7051bac7d8 | ||
![]() |
44aa5856cd | ||
![]() |
374dc52601 | ||
![]() |
46345e9956 | ||
![]() |
f3c02d4298 | ||
![]() |
b4cedca7da | ||
![]() |
deaf828e84 | ||
![]() |
38a5698bc7 | ||
![]() |
70af8a3fa5 | ||
![]() |
37b172c8ca | ||
![]() |
1eef380425 | ||
![]() |
1dcf411626 | ||
![]() |
378a97553c | ||
![]() |
3246b04911 | ||
![]() |
dda48a7d6a | ||
![]() |
29153f0d8d | ||
![]() |
70c513f568 | ||
![]() |
56f75b259c | ||
![]() |
f94a3a7711 | ||
![]() |
5a166ee235 | ||
![]() |
0c199e42c8 | ||
![]() |
a377f55d7f | ||
![]() |
c4759c8ca8 | ||
![]() |
9d19230033 | ||
![]() |
5aa2ec962b | ||
![]() |
b21e7f0a70 | ||
![]() |
0056d274d5 | ||
![]() |
cf5d7f3928 | ||
![]() |
058eed953e | ||
![]() |
5feb41624d | ||
![]() |
10a74bf607 | ||
![]() |
0d1bb00270 | ||
![]() |
f5ab3e4e12 | ||
![]() |
361a9449bb | ||
![]() |
2e719ff671 | ||
![]() |
506756f332 | ||
![]() |
67239c874e | ||
![]() |
79d9dc2f8f | ||
![]() |
d6cf042764 | ||
![]() |
8e5f3ca841 | ||
![]() |
9013c84524 | ||
![]() |
47f87f4ecb | ||
![]() |
41d325fdd8 | ||
![]() |
6b6761f654 | ||
![]() |
73bf50e8a8 | ||
![]() |
fc3ae973be | ||
![]() |
0c3c4d4f22 | ||
![]() |
4df91b980a | ||
![]() |
3729104547 | ||
![]() |
48c3465b19 | ||
![]() |
adc4c9f484 | ||
![]() |
def1df2c68 | ||
![]() |
9fd896c1d1 | ||
![]() |
e6f4e1f027 | ||
![]() |
35424f62ed | ||
![]() |
70943ae461 | ||
![]() |
a976aa7123 | ||
![]() |
e58111d93d | ||
![]() |
311ebaa5a4 | ||
![]() |
b058b2dff1 | ||
![]() |
6c77e76c4c | ||
![]() |
691225f51a | ||
![]() |
d46c669f64 | ||
![]() |
7f46da315f | ||
![]() |
b7e0d18fae | ||
![]() |
f9095caa42 | ||
![]() |
de5c825d01 | ||
![]() |
5fd26d2a45 | ||
![]() |
06052b1f3e | ||
![]() |
98b3fbbc18 | ||
![]() |
fd29c8dd44 | ||
![]() |
f4e90f1888 | ||
![]() |
d9d8a39d6a | ||
![]() |
aa1a99dcf2 | ||
![]() |
01f863f268 | ||
![]() |
8779e7f36f | ||
![]() |
703cfc02e9 | ||
![]() |
d11c92ffc3 | ||
![]() |
7f381a0d02 | ||
![]() |
b8b1b9d57f | ||
![]() |
103c0e5997 | ||
![]() |
696e3d6836 | ||
![]() |
07a6b9942e | ||
![]() |
e3d2b854d7 | ||
![]() |
5ee88f6c6b | ||
![]() |
1681598c7d | ||
![]() |
8f6d92c45c | ||
![]() |
3445c340a1 | ||
![]() |
a82d3c15ff | ||
![]() |
8b44b2f2ed | ||
![]() |
3f4f9e914b | ||
![]() |
721565f3c3 | ||
![]() |
4b8060b093 | ||
![]() |
b61bbfea4e | ||
![]() |
6c88eb12d7 | ||
![]() |
5cb46022fb | ||
![]() |
afa0f83349 | ||
![]() |
a3ddac93a6 | ||
![]() |
6dd1406716 | ||
![]() |
59f11650f2 | ||
![]() |
fb6e93fe8e | ||
![]() |
b7ddf1ef3a | ||
![]() |
5ffc582f76 | ||
![]() |
d025f728cc | ||
![]() |
7172c833cb | ||
![]() |
e22350f745 | ||
![]() |
da9319411b | ||
![]() |
a3bd43f3b2 | ||
![]() |
4d0fd858be | ||
![]() |
f89325507c | ||
![]() |
3cafe68e2b | ||
![]() |
e9ea414849 | ||
![]() |
fef5b41bd9 | ||
![]() |
7a478bd508 | ||
![]() |
bba0033900 | ||
![]() |
2e773439ce | ||
![]() |
3ff668d7ed | ||
![]() |
1801d9efcb | ||
![]() |
ecd9d7444f | ||
![]() |
2dc2b31e36 | ||
![]() |
0513b647ec | ||
![]() |
a289c42830 | ||
![]() |
d67eaa8710 | ||
![]() |
50a5886455 | ||
![]() |
8d4dde8411 | ||
![]() |
b6c9bd6bfc | ||
![]() |
a5977368b1 | ||
![]() |
0c3dffb082 | ||
![]() |
0e0f34edd8 | ||
![]() |
4888d9d355 | ||
![]() |
1c39bd5781 | ||
![]() |
90ad885446 | ||
![]() |
401281d0eb | ||
![]() |
1741c234a6 | ||
![]() |
63b50b3586 | ||
![]() |
e0fefa8025 | ||
![]() |
f7df761f7c | ||
![]() |
e690653a3a | ||
![]() |
012094d32f | ||
![]() |
d426cc968d | ||
![]() |
085099b770 | ||
![]() |
c066a5d65a | ||
![]() |
2409c091ff | ||
![]() |
626fe252ab | ||
![]() |
0bb4aa58a0 | ||
![]() |
3baf081831 | ||
![]() |
cd07cae68c | ||
![]() |
f8862ff2b3 | ||
![]() |
79dd246722 | ||
![]() |
6ba4dd5d86 | ||
![]() |
10bd08ef19 | ||
![]() |
2789d08154 | ||
![]() |
da4896077b | ||
![]() |
0321ea248a | ||
![]() |
678bb2b819 | ||
![]() |
966f9aead9 | ||
![]() |
4f0a2a9593 | ||
![]() |
f3f8217125 | ||
![]() |
89bd6181f2 | ||
![]() |
c6fdac131b | ||
![]() |
0415c616b1 | ||
![]() |
740a165452 | ||
![]() |
4997624a3a | ||
![]() |
b66daae1f3 | ||
![]() |
1e7df58b5b | ||
![]() |
46da032626 | ||
![]() |
3b0593baa7 | ||
![]() |
07415a37c9 | ||
![]() |
356eb47ca3 | ||
![]() |
dd530737a2 | ||
![]() |
a4e5e46dd1 | ||
![]() |
9383e1a983 | ||
![]() |
43ad9a7da9 | ||
![]() |
b661158a85 | ||
![]() |
4d5f89fb2a | ||
![]() |
0fa5f5de4c | ||
![]() |
41cc746885 | ||
![]() |
8ead8559e0 | ||
![]() |
5245276409 | ||
![]() |
0c24a7042f | ||
![]() |
86cfeb714c | ||
![]() |
872973f145 | ||
![]() |
3ecf72a507 | ||
![]() |
1aaa400876 | ||
![]() |
65047cc2cb | ||
![]() |
c8d1cd6cc3 | ||
![]() |
558e9382ae | ||
![]() |
1ec0b25c3d | ||
![]() |
4d08a6568b | ||
![]() |
2ef35fd2f8 | ||
![]() |
cc44844029 | ||
![]() |
970903992a | ||
![]() |
ac160b6a18 | ||
![]() |
535a99a12f | ||
![]() |
6415432eb5 | ||
![]() |
30e9692158 | ||
![]() |
148bce498e | ||
![]() |
9455b0942b | ||
![]() |
a279bbb5fd | ||
![]() |
3843e24ba0 | ||
![]() |
91ada50547 | ||
![]() |
c969c730a5 | ||
![]() |
560cd5ec32 | ||
![]() |
186c3755e9 | ||
![]() |
a8113203d4 | ||
![]() |
8b993d409e | ||
![]() |
2e1da33411 | ||
![]() |
053dce7b8b | ||
![]() |
4941ffdcae | ||
![]() |
845d519fc9 | ||
![]() |
cb59638222 | ||
![]() |
1cf00f8f28 | ||
![]() |
3138e75dfe | ||
![]() |
c0fd470198 | ||
![]() |
44dd859a11 | ||
![]() |
fb207ad1d4 | ||
![]() |
238324505f | ||
![]() |
9685ac01a1 | ||
![]() |
ebf18e369b | ||
![]() |
d1663324be | ||
![]() |
37e8eb1211 | ||
![]() |
d27063eea4 | ||
![]() |
77a89d12f2 | ||
![]() |
e66d7717ab | ||
![]() |
828772e4f2 | ||
![]() |
087cfdfb9f | ||
![]() |
77a7cca80c | ||
![]() |
d24c875aeb | ||
![]() |
6c974d033b | ||
![]() |
5ee8fd7448 | ||
![]() |
23352dd668 | ||
![]() |
60524a49be | ||
![]() |
76665cc7a1 | ||
![]() |
6cd8bbf5e2 | ||
![]() |
8602bb66ab | ||
![]() |
20d40522da | ||
![]() |
44d4e069e4 | ||
![]() |
309790a707 | ||
![]() |
8a17d6287c | ||
![]() |
bea2193b82 | ||
![]() |
3819b41865 | ||
![]() |
52e7bab8e6 | ||
![]() |
7916134430 | ||
![]() |
e1944b4d45 | ||
![]() |
7fe05e26d6 | ||
![]() |
a654815139 | ||
![]() |
d7c69316f7 | ||
![]() |
a22d237f2a | ||
![]() |
f92acf6481 | ||
![]() |
c7adb02973 | ||
![]() |
ce0ed0bdd3 | ||
![]() |
3b51f9711c | ||
![]() |
ca0f7f417b | ||
![]() |
9f59e2080d | ||
![]() |
1e6778ec11 | ||
![]() |
a1cdec0995 | ||
![]() |
6593913c7e | ||
![]() |
9fb3f7d223 | ||
![]() |
8a43bc35d7 | ||
![]() |
2e65fb9441 | ||
![]() |
f0326c4a82 | ||
![]() |
1deb577b6e | ||
![]() |
60c59a9575 | ||
![]() |
fddc299b60 | ||
![]() |
1cb350b2aa | ||
![]() |
43ccc875fb | ||
![]() |
4249b7dec8 | ||
![]() |
49f15c736b | ||
![]() |
a8064ba3ee | ||
![]() |
e6c3c06c2e | ||
![]() |
892fe1917e | ||
![]() |
7979b5e11c | ||
![]() |
d19b51d4f8 | ||
![]() |
c72e853c85 | ||
![]() |
d5fba1e199 | ||
![]() |
4629c88279 | ||
![]() |
381809e1bd | ||
![]() |
a06c103aa7 | ||
![]() |
ae43f480d6 | ||
![]() |
b7535f0b0e | ||
![]() |
9d7dd3afef | ||
![]() |
6e70165e8c | ||
![]() |
b9759d2c77 | ||
![]() |
1cb3feec51 | ||
![]() |
7c0f1f42f6 | ||
![]() |
4b9dae1986 | ||
![]() |
8e13821456 | ||
![]() |
9ae10b66ab | ||
![]() |
f77fcc28d9 | ||
![]() |
20a6bdb55c | ||
![]() |
5f40a4cad4 | ||
![]() |
7f4bcbe853 | ||
![]() |
a7e2e053f3 | ||
![]() |
c870c7adab | ||
![]() |
9075584a3a | ||
![]() |
d91c98b82e | ||
![]() |
00f35af572 | ||
![]() |
24d5be9e8c | ||
![]() |
57ea4e61d1 | ||
![]() |
7b671d0d74 | ||
![]() |
6ad3077c69 | ||
![]() |
4f1d578b2f | ||
![]() |
685eb8cb50 | ||
![]() |
998331f937 | ||
![]() |
fdd3ea29e0 | ||
![]() |
f25781db38 | ||
![]() |
8bde613823 | ||
![]() |
e9db8195e0 | ||
![]() |
944f823847 | ||
![]() |
377243b07f | ||
![]() |
c86c73dd8f | ||
![]() |
4f453600db | ||
![]() |
4ff1dbc8b1 | ||
![]() |
18463b79b3 | ||
![]() |
9e19d2d166 | ||
![]() |
17e9de9a53 | ||
![]() |
4a0f4fc4ea | ||
![]() |
124d7a388f | ||
![]() |
0b34294156 | ||
![]() |
d296b12f0e | ||
![]() |
2d62da8dfe | ||
![]() |
6115b9a808 | ||
![]() |
e034410a91 | ||
![]() |
800a609b4f | ||
![]() |
ce69926f8a | ||
![]() |
e38a33b7b4 | ||
![]() |
95ab1256ba | ||
![]() |
f7a3dbd9f2 | ||
![]() |
4552db4fa1 | ||
![]() |
a33bb248a4 | ||
![]() |
2df55f0be7 | ||
![]() |
a0094fce80 | ||
![]() |
72a525063a | ||
![]() |
0484b8db51 | ||
![]() |
c63781abe3 | ||
![]() |
b496e68bf9 | ||
![]() |
d0df605151 | ||
![]() |
0796621702 | ||
![]() |
d883fa7cc0 | ||
![]() |
bf68b8b3b2 | ||
![]() |
8136962237 | ||
![]() |
b8a62be016 | ||
![]() |
f86bc0fab9 | ||
![]() |
422941f260 | ||
![]() |
b839775904 | ||
![]() |
69de41c981 | ||
![]() |
037dfadbc2 | ||
![]() |
d6a512f05b | ||
![]() |
c2f83885cb | ||
![]() |
4c8f41f312 | ||
![]() |
93d66a7958 | ||
![]() |
8dce1413b9 | ||
![]() |
e4fdab7c96 | ||
![]() |
2a748aa76a | ||
![]() |
d8049f3843 | ||
![]() |
d9edc2e30d | ||
![]() |
0808532b49 | ||
![]() |
2a0fe2584e | ||
![]() |
7bd6496854 | ||
![]() |
537f6e7f68 | ||
![]() |
6dfa89e846 | ||
![]() |
561e919a2e | ||
![]() |
20e3acf7a6 | ||
![]() |
2c0929e537 | ||
![]() |
aad8ab0123 | ||
![]() |
88a2b286c7 | ||
![]() |
f8c48e44e0 | ||
![]() |
f37bc01fe7 | ||
![]() |
ae460c569a | ||
![]() |
1e535dd779 | ||
![]() |
ea8f5a124e | ||
![]() |
93ad17baa7 | ||
![]() |
b933ddb315 | ||
![]() |
e010d12752 | ||
![]() |
3e4c51ebf9 | ||
![]() |
58df7cfe70 | ||
![]() |
b5ec37b698 | ||
![]() |
624f17bd3c | ||
![]() |
744525dcaf | ||
![]() |
05190598bc | ||
![]() |
da6f52a155 | ||
![]() |
e5d639270e | ||
![]() |
11da1e5b30 | ||
![]() |
7cfc7c7811 | ||
![]() |
cf6af5e70d | ||
![]() |
898dfff1a0 | ||
![]() |
2660e4edde | ||
![]() |
8e05ff0c32 | ||
![]() |
c4cb94bff8 | ||
![]() |
10ee9c4019 | ||
![]() |
042bcf0638 | ||
![]() |
8c30adaf10 | ||
![]() |
2fcd49791e | ||
![]() |
ae0c96b66c | ||
![]() |
c195bb0739 | ||
![]() |
6de5305e72 | ||
![]() |
ca1d426fc0 | ||
![]() |
4fd311364a | ||
![]() |
9d6180e2fd | ||
![]() |
eeeb7e4ed9 | ||
![]() |
9c248e8ae9 | ||
![]() |
d579328746 | ||
![]() |
3b3cfc391e | ||
![]() |
222fa328ba | ||
![]() |
a83498fc61 | ||
![]() |
0bd531b612 | ||
![]() |
b9b35833d3 | ||
![]() |
eac2a23d72 | ||
![]() |
641a766607 | ||
![]() |
bf38bd6620 | ||
![]() |
775e551c3d | ||
![]() |
909cc53c3a | ||
![]() |
99e5542f03 | ||
![]() |
96b8340210 | ||
![]() |
f92d025434 | ||
![]() |
bbbed516f2 | ||
![]() |
c86a878a75 | ||
![]() |
6676f24c4f | ||
![]() |
e2e336bd95 | ||
![]() |
c5ec66e585 | ||
![]() |
a0c7aae672 | ||
![]() |
95c8418dee | ||
![]() |
21f5934e44 | ||
![]() |
d6e2936bf8 | ||
![]() |
0aa1b62108 | ||
![]() |
c7c41155f6 | ||
![]() |
492a643506 | ||
![]() |
1cc8ad6ed9 | ||
![]() |
8f57c10d9b | ||
![]() |
b635b10b59 | ||
![]() |
7ebda02b81 | ||
![]() |
d6cd8b78f1 | ||
![]() |
b7d7ccc929 | ||
![]() |
f14ad61bd0 | ||
![]() |
8963baf5ad | ||
![]() |
557add5bf7 | ||
![]() |
b9cfbc2077 | ||
![]() |
a04cc707c3 | ||
![]() |
72ea8022bf | ||
![]() |
4bdeaf999b | ||
![]() |
42d8c5a040 | ||
![]() |
f299514e44 | ||
![]() |
dd220bcaea | ||
![]() |
fe75f29c15 | ||
![]() |
14845a343b | ||
![]() |
dd8a7d41e2 | ||
![]() |
fda5c6fdf7 | ||
![]() |
3d1631f375 | ||
![]() |
c7ee46e7f8 | ||
![]() |
d1e4421823 | ||
![]() |
7c9cf30909 | ||
![]() |
1e37dbd60e | ||
![]() |
f8d5c2a1b6 | ||
![]() |
23b24ea5c3 |
@@ -6,6 +6,6 @@
|
||||
!.prettierrc
|
||||
!package.json
|
||||
!public/
|
||||
!src/
|
||||
!packages/
|
||||
!tsconfig.json
|
||||
!yarn.lock
|
||||
|
@@ -7,12 +7,11 @@ VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfu
|
||||
# collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room)
|
||||
VITE_APP_WS_SERVER_URL=http://localhost:3002
|
||||
|
||||
# set this only if using the collaboration workflow we use on excalidraw.com
|
||||
VITE_APP_PORTAL_URL=
|
||||
|
||||
VITE_APP_PLUS_LP=https://plus.excalidraw.com
|
||||
VITE_APP_PLUS_APP=https://app.excalidraw.com
|
||||
|
||||
VITE_APP_AI_BACKEND=http://localhost:3015
|
||||
|
||||
VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
|
||||
|
||||
# put these in your .env.local, or make sure you don't commit!
|
||||
|
@@ -4,14 +4,13 @@ VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
||||
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||
VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
||||
|
||||
VITE_APP_PORTAL_URL=https://portal.excalidraw.com
|
||||
|
||||
VITE_APP_PLUS_LP=https://plus.excalidraw.com
|
||||
VITE_APP_PLUS_APP=https://app.excalidraw.com
|
||||
|
||||
# Fill to set socket server URL used for collaboration.
|
||||
# Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow
|
||||
VITE_APP_WS_SERVER_URL=
|
||||
VITE_APP_AI_BACKEND=https://oss-ai.excalidraw.com
|
||||
|
||||
# socket server URL used for collaboration
|
||||
VITE_APP_WS_SERVER_URL=https://oss-collab.excalidraw.com
|
||||
|
||||
VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
|
||||
|
||||
|
@@ -5,4 +5,4 @@ package-lock.json
|
||||
firebase/
|
||||
dist/
|
||||
public/workbox
|
||||
src/packages/excalidraw/types
|
||||
packages/excalidraw/types
|
||||
|
2
.github/workflows/autorelease-excalidraw.yml
vendored
@@ -23,5 +23,5 @@ jobs:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Auto release
|
||||
run: |
|
||||
yarn add @actions/core
|
||||
yarn add @actions/core -W
|
||||
yarn autorelease
|
||||
|
2
.github/workflows/autorelease-preview.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
- name: Auto release preview
|
||||
id: "autorelease"
|
||||
run: |
|
||||
yarn add @actions/core
|
||||
yarn add @actions/core -W
|
||||
yarn autorelease preview ${{ github.event.issue.number }}
|
||||
- name: Post comment post release
|
||||
if: always()
|
||||
|
2
.github/workflows/lint.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
- name: Install and lint
|
||||
run: |
|
||||
yarn --frozen-lockfile
|
||||
yarn install
|
||||
yarn test:other
|
||||
yarn test:code
|
||||
yarn test:typecheck
|
||||
|
4
.github/workflows/locales-coverage.yml
vendored
@@ -22,11 +22,11 @@ jobs:
|
||||
- name: Create report file
|
||||
run: |
|
||||
yarn locales-coverage
|
||||
FILE_CHANGED=$(git diff src/locales/percentages.json)
|
||||
FILE_CHANGED=$(git diff packages/excalidraw/locales/percentages.json)
|
||||
if [ ! -z "${FILE_CHANGED}" ]; then
|
||||
git config --global user.name 'Excalidraw Bot'
|
||||
git config --global user.email 'bot@excalidraw.com'
|
||||
git add src/locales/percentages.json
|
||||
git add packages/excalidraw/locales/percentages.json
|
||||
git commit -am "Auto commit: Calculate translation coverage"
|
||||
git push
|
||||
fi
|
||||
|
12
.github/workflows/size-limit.yml
vendored
@@ -15,16 +15,14 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
- name: Install
|
||||
run: yarn --frozen-lockfile
|
||||
- name: Install in src/packages/excalidraw
|
||||
run: yarn --frozen-lockfile
|
||||
working-directory: src/packages/excalidraw
|
||||
- name: Install in packages/excalidraw
|
||||
run: yarn
|
||||
working-directory: packages/excalidraw
|
||||
env:
|
||||
CI: true
|
||||
- uses: andresz1/size-limit-action@v1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
build_script: build:umd
|
||||
build_script: build:esm
|
||||
skip_step: install
|
||||
directory: src/packages/excalidraw
|
||||
directory: packages/excalidraw
|
||||
|
2
.github/workflows/test-coverage-pr.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
with:
|
||||
node-version: "18.x"
|
||||
- name: "Install Deps"
|
||||
run: yarn --frozen-lockfile
|
||||
run: yarn install
|
||||
- name: "Test Coverage"
|
||||
run: yarn test:coverage
|
||||
- name: "Report Coverage"
|
||||
|
2
.github/workflows/test.yml
vendored
@@ -13,5 +13,5 @@ jobs:
|
||||
node-version: 18.x
|
||||
- name: Install and test
|
||||
run: |
|
||||
yarn --frozen-lockfile
|
||||
yarn install
|
||||
yarn test:app
|
||||
|
7
.gitignore
vendored
@@ -21,10 +21,9 @@ npm-debug.log*
|
||||
package-lock.json
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
src/packages/excalidraw/types
|
||||
src/packages/excalidraw/example/public/bundle.js
|
||||
src/packages/excalidraw/example/public/excalidraw-assets-dev
|
||||
src/packages/excalidraw/example/public/excalidraw.development.js
|
||||
packages/excalidraw/types
|
||||
coverage
|
||||
dev-dist
|
||||
html
|
||||
examples/**/bundle.*
|
||||
meta*.json
|
@@ -85,7 +85,7 @@ We'll be adding these features as drop-in plugins for the npm package in the fut
|
||||
|
||||
## Quick start
|
||||
|
||||
Install the [Excalidraw npm package](https://www.npmjs.com/package/@excalidraw/excalidraw):
|
||||
**Note:** following instructions are for installing the Excalidraw [npm package](https://www.npmjs.com/package/@excalidraw/excalidraw) when integrating Excalidraw into your own app. To run the repository locally for development, please refer to our [Development Guide](https://docs.excalidraw.com/docs/introduction/development).
|
||||
|
||||
```
|
||||
npm install react react-dom @excalidraw/excalidraw
|
||||
@@ -97,7 +97,7 @@ or via yarn
|
||||
yarn add react react-dom @excalidraw/excalidraw
|
||||
```
|
||||
|
||||
Don't forget to check out our [Documentation](https://docs.excalidraw.com)!
|
||||
Check out our [documentation](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/installation) for more details!
|
||||
|
||||
## Contributing
|
||||
|
||||
|
@@ -1,3 +1,3 @@
|
||||
files:
|
||||
- source: /src/locales/en.json
|
||||
translation: /src/locales/%locale%.json
|
||||
- source: /packages/excalidraw/locales/en.json
|
||||
translation: /packages/excalidraw/locales/%locale%.json
|
||||
|
@@ -133,7 +133,7 @@ function App() {
|
||||
}
|
||||
```
|
||||
|
||||
Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/src/components/mainMenu/DefaultItems.tsx) of the default items.
|
||||
Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/components/mainMenu/DefaultItems.tsx) of the default items.
|
||||
|
||||
### MainMenu.Group
|
||||
|
||||
|
@@ -37,7 +37,7 @@ Defaults to `THEME.LIGHT` unless passed in `initialData.appState.theme`
|
||||
|
||||
### MIME_TYPES
|
||||
|
||||
[`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L101) contains all the mime types supported by `Excalidraw`.
|
||||
[`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L101) contains all the mime types supported by `Excalidraw`.
|
||||
|
||||
**How to use **
|
||||
|
||||
|
@@ -2,9 +2,9 @@
|
||||
|
||||
We support a simplified API to make it easier to generate Excalidraw elements programmatically. This API is in beta and subject to change before stable. You can check the [PR](https://github.com/excalidraw/excalidraw/pull/6546) for more details.
|
||||
|
||||
For this purpose we introduced a new type [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133). This is the simplified version of [`ExcalidrawElement`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L134) type with the minimum possible attributes so that creating elements programmatically is much easier (especially for cases like binding arrows or creating text containers).
|
||||
For this purpose we introduced a new type [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133). This is the simplified version of [`ExcalidrawElement`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L134) type with the minimum possible attributes so that creating elements programmatically is much easier (especially for cases like binding arrows or creating text containers).
|
||||
|
||||
The [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133) can be converted to fully qualified Excalidraw elements by using [`convertToExcalidrawElements`](/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton#converttoexcalidrawelements).
|
||||
The [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133) can be converted to fully qualified Excalidraw elements by using [`convertToExcalidrawElements`](/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton#converttoexcalidrawelements).
|
||||
|
||||
## convertToExcalidrawElements
|
||||
|
||||
@@ -19,7 +19,7 @@ convertToExcalidrawElements(
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `elements` | [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L137) | | The Excalidraw element Skeleton which needs to be converted to Excalidraw elements. |
|
||||
| `elements` | [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L137) | | The Excalidraw element Skeleton which needs to be converted to Excalidraw elements. |
|
||||
| `opts` | `{ regenerateIds: boolean }` | ` {regenerateIds: true}` | By default `id` will be regenerated for all the elements irrespective of whether you pass the `id` so if you don't want the ids to regenerated, you can set this attribute to `false`. |
|
||||
|
||||
**_How to use_**
|
||||
@@ -71,7 +71,7 @@ function App() {
|
||||
}
|
||||
```
|
||||
|
||||
You can pass additional [`properties`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L27) as well to decorate the shapes.
|
||||
You can pass additional [`properties`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L27) as well to decorate the shapes.
|
||||
|
||||
:::info
|
||||
|
||||
@@ -192,7 +192,7 @@ convertToExcalidrawElements([
|
||||
|
||||
### Text Containers
|
||||
|
||||
In addition to `type`, `x` and `y` properties, [`label`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L124C7-L130C59) property is required for text containers. The `text` property in `label` is required, rest of the attributes are optional.
|
||||
In addition to `type`, `x` and `y` properties, [`label`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L124C7-L130C59) property is required for text containers. The `text` property in `label` is required, rest of the attributes are optional.
|
||||
|
||||
If you don't provide the dimensions of container, we calculate it based of the label dimensions.
|
||||
|
||||
@@ -326,7 +326,7 @@ convertToExcalidrawElements([
|
||||
|
||||
### Arrow bindings
|
||||
|
||||
To bind arrow to a shape you need to specify its [`start`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L86) and [`end`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L54) properties. You need to pass either `type` or `id` property in `start` and `end` properties, rest of the attributes are optional
|
||||
To bind arrow to a shape you need to specify its [`start`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L86) and [`end`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L54) properties. You need to pass either `type` or `id` property in `start` and `end` properties, rest of the attributes are optional
|
||||
|
||||
```js
|
||||
convertToExcalidrawElements([
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
<pre>
|
||||
(api:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L616">
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L616">
|
||||
ExcalidrawAPI
|
||||
</a>
|
||||
) => void;
|
||||
@@ -17,7 +17,7 @@ export default function App() {
|
||||
}
|
||||
```
|
||||
|
||||
You can use this prop when you want to access some [Excalidraw APIs](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L616). We expose the below APIs :point_down:
|
||||
You can use this prop when you want to access some [Excalidraw APIs](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L616). We expose the below APIs :point_down:
|
||||
|
||||
| API | Signature | Usage |
|
||||
| --- | --- | --- |
|
||||
@@ -37,7 +37,7 @@ You can use this prop when you want to access some [Excalidraw APIs](https://git
|
||||
| [setActiveTool](#setactivetool) | `function` | This API can be used to set the active tool |
|
||||
| [setCursor](#setcursor) | `function` | This API can be used to set customise the mouse cursor on the canvas |
|
||||
| [resetCursor](#resetcursor) | `function` | This API can be used to reset to default mouse cursor on the canvas |
|
||||
| [toggleMenu](#togglemenu) | `function` | Toggles specific menus on/off |
|
||||
| [toggleSidebar](#toggleSidebar) | `function` | Toggles specific sidebar on/off |
|
||||
| [onChange](#onChange) | `function` | Subscribes to change events |
|
||||
| [onPointerDown](#onPointerDown) | `function` | Subscribes to `pointerdown` events |
|
||||
| [onPointerUp](#onPointerUp) | `function` | Subscribes to `pointerup` events |
|
||||
@@ -52,7 +52,7 @@ Additionally `ready` and `readyPromise` from the API have been discontinued. The
|
||||
|
||||
<pre>
|
||||
(scene:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L339">
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L339">
|
||||
sceneData
|
||||
</a>
|
||||
) => void
|
||||
@@ -62,9 +62,9 @@ You can use this function to update the scene with the sceneData. It accepts the
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `elements` | [`ImportedDataState["elements"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L38) | The `elements` to be updated in the scene |
|
||||
| `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L39) | The `appState` to be updated in the scene. |
|
||||
| `collaborators` | <code>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L37">Collaborator></a></code> | The list of collaborators to be updated in the scene. |
|
||||
| `elements` | [`ImportedDataState["elements"]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L38) | The `elements` to be updated in the scene |
|
||||
| `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L39) | The `appState` to be updated in the scene. |
|
||||
| `collaborators` | <code>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L37">Collaborator></a></code> | The list of collaborators to be updated in the scene. |
|
||||
| `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
|
||||
|
||||
```jsx live
|
||||
@@ -125,13 +125,13 @@ function App() {
|
||||
|
||||
<pre>
|
||||
(opts: { <br /> libraryItems:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L249">
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L249">
|
||||
LibraryItemsSource
|
||||
</a>
|
||||
;<br /> merge?: boolean; <br /> prompt?: boolean;
|
||||
<br /> openLibraryMenu?: boolean;
|
||||
<br /> defaultStatus?: "unpublished" | "published"; <br /> }) => Promise<
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L246">
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L246">
|
||||
LibraryItems
|
||||
</a>
|
||||
>
|
||||
@@ -141,7 +141,7 @@ You can use this function to update the library. It accepts the below attributes
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `libraryItems` | [LibraryItemsSource](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L249) | \_ | The `libraryItems` to be replaced/merged with current library |
|
||||
| `libraryItems` | [LibraryItemsSource](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L249) | \_ | The `libraryItems` to be replaced/merged with current library |
|
||||
| `merge` | boolean | `false` | Whether to merge with existing library items. |
|
||||
| `prompt` | boolean | `false` | Whether to prompt user for confirmation. |
|
||||
| `openLibraryMenu` | boolean | `false` | Keep the library menu open after library is updated. |
|
||||
@@ -189,7 +189,7 @@ function App() {
|
||||
</button>
|
||||
<Excalidraw
|
||||
ref={(api) => setExcalidrawAPI(api)}
|
||||
// initial data retrieved from https://github.com/excalidraw/excalidraw/blob/master/dev-docs/src/initialData.js
|
||||
// initial data retrieved from https://github.com/excalidraw/excalidraw/blob/master/dev-docs/packages/excalidraw/initialData.js
|
||||
initialData={{
|
||||
libraryItems: initialData.libraryItems,
|
||||
appState: { openSidebar: "library" },
|
||||
@@ -204,7 +204,7 @@ function App() {
|
||||
|
||||
<pre>
|
||||
(files:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59">
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L59">
|
||||
BinaryFileData
|
||||
</a>
|
||||
) => void
|
||||
@@ -224,7 +224,7 @@ Resets the scene. If `resetLoadingState` is passed as true then it will also for
|
||||
|
||||
<pre>
|
||||
() =>{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L115">
|
||||
ExcalidrawElement[]
|
||||
</a>
|
||||
</pre>
|
||||
@@ -235,7 +235,7 @@ Returns all the elements including the deleted in the scene.
|
||||
|
||||
<pre>
|
||||
() => NonDeleted<
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L115">
|
||||
ExcalidrawElement
|
||||
</a>
|
||||
[]>
|
||||
@@ -247,7 +247,7 @@ Returns all the elements excluding the deleted in the scene
|
||||
|
||||
<pre>
|
||||
() =>{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">
|
||||
AppState
|
||||
</a>
|
||||
</pre>
|
||||
@@ -288,7 +288,7 @@ Scroll the nearest element out of the elements supplied to the center of the vie
|
||||
|
||||
| Attribute | type | default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| target | [ExcalidrawElement](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115) | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115) | All scene elements | The element(s) to scroll to. |
|
||||
| target | [ExcalidrawElement](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L115) | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L115) | All scene elements | The element(s) to scroll to. |
|
||||
| opts.fitToContent | boolean | false | Whether to fit the elements to viewport by automatically changing zoom as needed. Note that the zoom range is between 10%-100%. |
|
||||
| opts.fitToViewport | boolean | false | Similar to fitToContent but the zoom range is not limited. If elements are smaller than the viewport, zoom will go above 100%. |
|
||||
| opts.viewportZoomFactor | number | 0.7 | when fitToViewport=true, how much screen should the content cover, between 0.1 (10%) and 1 (100%) |
|
||||
@@ -336,7 +336,7 @@ The unique id of the excalidraw component. This can be used to identify the exca
|
||||
|
||||
<pre>
|
||||
() =>{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L82">
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L82">
|
||||
files
|
||||
</a>
|
||||
</pre>
|
||||
@@ -364,7 +364,7 @@ This API has the below signature. It sets the `tool` passed in param as the acti
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `type` | [ToolType](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L91) | `selection` | The tool type which should be set as active tool. When setting `image` as active tool, the insertion onto canvas when using image tool is disabled by default, so you can enable it by setting `insertOnCanvasDirectly` to `true` |
|
||||
| `type` | [ToolType](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L91) | `selection` | The tool type which should be set as active tool. When setting `image` as active tool, the insertion onto canvas when using image tool is disabled by default, so you can enable it by setting `insertOnCanvasDirectly` to `true` |
|
||||
| `locked` | `boolean` | `false` | Indicates whether the the active tool should be locked. It behaves the same way when using the `lock` tool in the editor interface |
|
||||
|
||||
## setCursor
|
||||
|
@@ -1,18 +1,18 @@
|
||||
# initialData
|
||||
|
||||
<pre>
|
||||
{ elements?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a> }
|
||||
{ elements?: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a> }
|
||||
</pre>
|
||||
|
||||
This helps to load Excalidraw with `initialData`. It must be an object or a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) which resolves to an object containing the below optional fields.
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) | The `elements` with which `Excalidraw` should be mounted. |
|
||||
| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) | The `AppState` with which `Excalidraw` should be mounted. |
|
||||
| `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) | The `elements` with which `Excalidraw` should be mounted. |
|
||||
| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) | The `AppState` with which `Excalidraw` should be mounted. |
|
||||
| `scrollToContent` | `boolean` | This attribute indicates whether to `scroll` to the nearest element to center once `Excalidraw` is mounted. By default, it will not scroll the nearest element to the center. Make sure you pass `initialData.appState.scrollX` and `initialData.appState.scrollY` when `scrollToContent` is false so that scroll positions are retained |
|
||||
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L247) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)> | This library items with which `Excalidraw` should be mounted. |
|
||||
| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L82) | The `files` added to the scene. |
|
||||
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L247) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L200)> | This library items with which `Excalidraw` should be mounted. |
|
||||
| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L82) | The `files` added to the scene. |
|
||||
|
||||
You might want to use this when you want to load excalidraw with some initial elements and app state.
|
||||
|
||||
|
@@ -23,7 +23,7 @@ All `props` are _optional_.
|
||||
| [`libraryReturnUrl`](#libraryreturnurl) | `string` | _ | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to |
|
||||
| [`theme`](#theme) | `"light"` | `"dark"` | `"light"` | The theme of the Excalidraw component |
|
||||
| [`name`](#name) | `string` | | Name of the drawing |
|
||||
| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](#canvasactions) |
|
||||
| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](/docs/@excalidraw/excalidraw/api/props/ui-options#canvasactions) |
|
||||
| [`detectScroll`](#detectscroll) | `boolean` | `true` | Indicates whether to update the offsets when nearest ancestor is scrolled. |
|
||||
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
|
||||
| [`autoFocus`](#autofocus) | `boolean` | `false` | indicates whether to focus the Excalidraw component on page load |
|
||||
@@ -33,7 +33,7 @@ All `props` are _optional_.
|
||||
|
||||
### Storing custom data on Excalidraw elements
|
||||
|
||||
Beyond attributes that Excalidraw elements already support, you can store `custom` data on each `element` in a `customData` object. The type of the attribute is [`Record<string, any>`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L66) and is optional.
|
||||
Beyond attributes that Excalidraw elements already support, you can store `custom` data on each `element` in a `customData` object. The type of the attribute is [`Record<string, any>`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L66) and is optional.
|
||||
|
||||
You can use this to add any extra information you need to keep track of.
|
||||
|
||||
@@ -59,11 +59,11 @@ Every time component updates, this callback if passed will get triggered and has
|
||||
(excalidrawElements, appState, files) => void;
|
||||
```
|
||||
|
||||
1. `excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) in the scene.
|
||||
1. `excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) in the scene.
|
||||
|
||||
2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) of the scene.
|
||||
2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) of the scene.
|
||||
|
||||
3. `files`: The [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) which are added to the scene.
|
||||
3. `files`: The [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L64) which are added to the scene.
|
||||
|
||||
Here you can try saving the data to your backend or local storage for example.
|
||||
|
||||
@@ -79,14 +79,14 @@ This callback is triggered when mouse pointer is updated.
|
||||
|
||||
2.`button`: The position of the button. This will be one of `["down", "up"]`
|
||||
|
||||
3.`pointersMap`: [`pointers`](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L131) map of the scene
|
||||
3.`pointersMap`: [`pointers`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L131) map of the scene
|
||||
|
||||
```js
|
||||
(exportedElements, appState, canvas) => void
|
||||
```
|
||||
|
||||
1. `exportedElements`: An array of [non deleted elements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L87) which needs to be exported.
|
||||
2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) of the scene.
|
||||
1. `exportedElements`: An array of [non deleted elements](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L87) which needs to be exported.
|
||||
2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) of the scene.
|
||||
3. `canvas`: The `HTMLCanvasElement` of the scene.
|
||||
|
||||
### onPointerDown
|
||||
@@ -96,11 +96,11 @@ This prop if passed will be triggered on pointer down events and has the below s
|
||||
|
||||
<pre>
|
||||
(activeTool:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L115">
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L115">
|
||||
{" "}
|
||||
AppState["activeTool"]
|
||||
</a>
|
||||
, pointerDownState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L424">
|
||||
, pointerDownState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L424">
|
||||
PointerDownState
|
||||
</a>) => void
|
||||
</pre>
|
||||
@@ -119,7 +119,7 @@ This callback is triggered if passed when something is pasted into the scene. Yo
|
||||
|
||||
<pre>
|
||||
(data:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/clipboard.ts#L18">
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/clipboard.ts#L18">
|
||||
ClipboardData
|
||||
</a>
|
||||
, event: ClipboardEvent | null) => boolean
|
||||
@@ -135,7 +135,7 @@ This callback if supplied will get triggered when the library is updated and has
|
||||
|
||||
<pre>
|
||||
(items:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L200">
|
||||
LibraryItems
|
||||
</a>
|
||||
) => void | Promise<any>
|
||||
@@ -149,7 +149,7 @@ This prop if passed will be triggered when clicked on `link`. To handle the redi
|
||||
|
||||
<pre>
|
||||
(element:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">
|
||||
ExcalidrawElement
|
||||
</a>
|
||||
, event: CustomEvent<{ nativeEvent: MouseEvent }>) => void
|
||||
@@ -182,7 +182,7 @@ const onLinkOpen: ExcalidrawProps["onLinkOpen"] = useCallback(
|
||||
|
||||
### langCode
|
||||
|
||||
Determines the `language` of the UI. It should be one of the [available language codes](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L14). Defaults to `en` (English). We also export default language and supported languages which you can import as shown below.
|
||||
Determines the `language` of the UI. It should be one of the [available language codes](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L14). Defaults to `en` (English). We also export default language and supported languages which you can import as shown below.
|
||||
|
||||
```js
|
||||
import { defaultLang, languages } from "@excalidraw/excalidraw";
|
||||
@@ -191,7 +191,7 @@ import { defaultLang, languages } from "@excalidraw/excalidraw";
|
||||
| name | type |
|
||||
| --- | --- |
|
||||
| `defaultLang` | `string` |
|
||||
| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) |
|
||||
| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L15) |
|
||||
|
||||
### viewModeEnabled
|
||||
|
||||
|
@@ -4,7 +4,7 @@
|
||||
|
||||
<pre>
|
||||
(isMobile: boolean, appState:
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">
|
||||
AppState
|
||||
</a>) => JSX | null
|
||||
</pre>
|
||||
@@ -66,7 +66,7 @@ function App() {
|
||||
|
||||
<pre>
|
||||
(element: NonDeleted<ExcalidrawEmbeddableElement>, appState:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">
|
||||
AppState
|
||||
</a>
|
||||
) => JSX.Element | null
|
||||
|
@@ -4,7 +4,7 @@ This prop can be used to customise UI of Excalidraw. Currently we support custom
|
||||
|
||||
<pre>
|
||||
{
|
||||
<br /> canvasActions?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L372">
|
||||
<br /> canvasActions?: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L372">
|
||||
CanvasActions
|
||||
</a>, <br /> dockedSidebarBreakpoint?: number, <br /> welcomeScreen?: boolean <br />
|
||||
|
||||
@@ -55,7 +55,7 @@ If `UIOptions.canvasActions.export` is `false` the export button will not be ren
|
||||
|
||||
## dockedSidebarBreakpoint
|
||||
|
||||
This prop indicates at what point should we break to a docked, permanent sidebar. If not passed it defaults to [`MQ_RIGHT_SIDEBAR_MAX_WIDTH_PORTRAIT`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L161).
|
||||
This prop indicates at what point should we break to a docked, permanent sidebar. If not passed it defaults to [`MQ_RIGHT_SIDEBAR_MAX_WIDTH_PORTRAIT`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L161).
|
||||
If the _width_ of the _excalidraw_ container exceeds _dockedSidebarBreakpoint_, the sidebar will be `dockable` and the button to `dock` the sidebar will be shown
|
||||
If user choses to `dock` the sidebar, it will push the right part of the UI towards the left, making space for the sidebar as shown below.
|
||||
|
||||
@@ -73,9 +73,9 @@ function App() {
|
||||
|
||||
## tools
|
||||
|
||||
This `prop ` controls the visibility of the tools in the editor.
|
||||
This `prop` controls the visibility of the tools in the editor.
|
||||
Currently you can control the visibility of `image` tool via this prop.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| image | boolean | true | Decides whether `image` tool should be visible.
|
||||
| image | boolean | true | Decides whether `image` tool should be visible.
|
||||
|
@@ -20,16 +20,16 @@ exportToCanvas({<br/>
|
||||
getDimensions,<br/>
|
||||
files,<br/>
|
||||
exportPadding?: number;<br/>
|
||||
}: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">ExportOpts</a>
|
||||
}: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L21">ExportOpts</a>
|
||||
</pre>
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `elements` | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) | | The elements to be exported to canvas. |
|
||||
| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L23) | [Default App State](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L17) | The app state of the scene. |
|
||||
| `elements` | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) | | The elements to be exported to canvas. |
|
||||
| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L23) | [Default App State](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/appState.ts#L17) | The app state of the scene. |
|
||||
| [`getDimensions`](#getdimensions) | `function` | _ | A function which returns the `width`, `height`, and optionally `scale` (defaults to `1`), with which canvas is to be exported. |
|
||||
| `maxWidthOrHeight` | `number` | _ | The maximum `width` or `height` of the exported image. If provided, `getDimensions` is ignored. |
|
||||
| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59) | _ | The files added to the scene. |
|
||||
| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L59) | _ | The files added to the scene. |
|
||||
| `exportPadding` | `number` | `10` | The `padding` to be added on canvas. |
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ function App() {
|
||||
|
||||
<pre>
|
||||
exportToBlob(<br/>
|
||||
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">ExportOpts</a> & {<br/>
|
||||
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L14">ExportOpts</a> & {<br/>
|
||||
mimeType?: string,<br/>
|
||||
quality?: number,<br/>
|
||||
exportPadding?: number;<br/>
|
||||
@@ -134,16 +134,16 @@ Returns a promise which resolves with a [blob](https://developer.mozilla.org/en-
|
||||
<pre>
|
||||
exportToSvg({<br/>
|
||||
elements:
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">
|
||||
ExcalidrawElement[]
|
||||
</a>,<br/>
|
||||
appState:
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95"> AppState
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95"> AppState
|
||||
</a>,<br/>
|
||||
exportPadding: number,<br/>
|
||||
metadata: string,<br/>
|
||||
files:
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59">
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L59">
|
||||
BinaryFiles
|
||||
</a>,<br/>
|
||||
});
|
||||
@@ -151,10 +151,10 @@ exportToSvg({<br/>
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) | | The elements to exported as `svg `|
|
||||
| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L11) | The `appState` of the scene |
|
||||
| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) | | The elements to exported as `svg `|
|
||||
| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/appState.ts#L11) | The `appState` of the scene |
|
||||
| exportPadding | number | 10 | The `padding` to be added on canvas |
|
||||
| files | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) | undefined | The `files` added to the scene. |
|
||||
| files | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L64) | undefined | The `files` added to the scene. |
|
||||
|
||||
This function returns a promise which resolves to `svg` of the exported drawing.
|
||||
|
||||
@@ -164,7 +164,7 @@ This function returns a promise which resolves to `svg` of the exported drawing.
|
||||
|
||||
<pre>
|
||||
exportToClipboard(<br/>
|
||||
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">ExportOpts</a> & {<br/>
|
||||
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L21">ExportOpts</a> & {<br/>
|
||||
mimeType?: string,<br/>
|
||||
quality?: number;<br/>
|
||||
type: 'png' | 'svg' |'json'<br/>
|
||||
|
@@ -8,7 +8,7 @@ id: "restore"
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState["appState"]</a>,<br/> localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>
|
||||
restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L34">ImportedDataState["appState"]</a>,<br/> localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a>> | null): <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a>
|
||||
</pre>
|
||||
|
||||
**_How to use_**
|
||||
@@ -17,7 +17,7 @@ restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob
|
||||
import { restoreAppState } from "@excalidraw/excalidraw";
|
||||
```
|
||||
|
||||
This function will make sure all the `keys` have appropriate `values` in [appState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) and if any key is missing, it will be set to its `default` value.
|
||||
This function will make sure all the `keys` have appropriate `values` in [appState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) and if any key is missing, it will be set to its `default` value.
|
||||
|
||||
When `localAppState` is supplied, it's used in place of values that are missing (`undefined`) in `appState` instead of the defaults.
|
||||
Use this as a way to not override user's defaults if you persist them.
|
||||
@@ -29,16 +29,16 @@ You can pass `null` / `undefined` if not applicable.
|
||||
|
||||
<pre>
|
||||
restoreElements(
|
||||
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>
|
||||
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>
|
||||
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>
|
||||
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>
|
||||
opts: { refreshDimensions?: boolean, repairBindings?: boolean }<br/>
|
||||
)
|
||||
</pre>
|
||||
|
||||
| Prop | Type | Description |
|
||||
| ---- | ---- | ---- |
|
||||
| `elements` | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a> | The `elements` to be restored |
|
||||
| [`localElements`](#localelements) | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined | When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. |
|
||||
| `elements` | <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ImportedDataState["elements"]</a> | The `elements` to be restored |
|
||||
| [`localElements`](#localelements) | <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined | When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. |
|
||||
| [`opts`](#opts) | `Object` | The extra optional parameter to configure restored elements
|
||||
|
||||
#### localElements
|
||||
@@ -70,15 +70,15 @@ Parameter `refreshDimensions` indicates whether we should also `recalculate` tex
|
||||
|
||||
<pre>
|
||||
restore(
|
||||
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>
|
||||
localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>
|
||||
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a><br/>
|
||||
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L34">ImportedDataState</a>,<br/>
|
||||
localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a>> | null | undefined,<br/>
|
||||
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L4">DataState</a><br/>
|
||||
opts: { refreshDimensions?: boolean, repairBindings?: boolean }<br/>
|
||||
|
||||
)
|
||||
</pre>
|
||||
|
||||
See [`restoreAppState()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreAppState) about `localAppState`, and [`restoreElements()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreElements) about `localElements`.
|
||||
See [`restoreAppState()`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/excalidraw/README.md#restoreAppState) about `localAppState`, and [`restoreElements()`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/excalidraw/README.md#restoreElements) about `localElements`.
|
||||
|
||||
**_How to use_**
|
||||
|
||||
@@ -93,7 +93,7 @@ This function makes sure elements and state is set to appropriate values and set
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
restoreLibraryItems(libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState["libraryItems"]</a>,<br/>
|
||||
restoreLibraryItems(libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L34">ImportedDataState["libraryItems"]</a>,<br/>
|
||||
defaultStatus: "published" | "unpublished")
|
||||
</pre>
|
||||
|
||||
|
@@ -8,7 +8,7 @@ These are pure Javascript functions exported from the @excalidraw/excalidraw [`@
|
||||
|
||||
### serializeAsJSON
|
||||
|
||||
Takes the scene elements and state and returns a JSON string. `Deleted` elements as well as most properties from `AppState` are removed from the resulting JSON. (see [`serializeAsJSON()`](https://github.com/excalidraw/excalidraw/blob/master/src/data/json.ts#L42) source for details).
|
||||
Takes the scene elements and state and returns a JSON string. `Deleted` elements as well as most properties from `AppState` are removed from the resulting JSON. (see [`serializeAsJSON()`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/json.ts#L42) source for details).
|
||||
|
||||
If you want to overwrite the `source` field in the `JSON` string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value.
|
||||
|
||||
@@ -16,8 +16,8 @@ If you want to overwrite the `source` field in the `JSON` string, you can set `w
|
||||
|
||||
<pre>
|
||||
serializeAsJSON({<br/>
|
||||
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>
|
||||
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>,<br/>
|
||||
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>
|
||||
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a>,<br/>
|
||||
}): string
|
||||
</pre>
|
||||
|
||||
@@ -37,7 +37,7 @@ If you want to overwrite the source field in the JSON string, you can set `windo
|
||||
|
||||
<pre>
|
||||
serializeLibraryAsJSON(
|
||||
libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems[]</a>)
|
||||
libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L200">LibraryItems[]</a>)
|
||||
</pre>
|
||||
|
||||
**How to use**
|
||||
@@ -53,7 +53,7 @@ Returns `true` if element is invisibly small (e.g. width & height are zero).
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
isInvisiblySmallElement(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement</a>): boolean
|
||||
isInvisiblySmallElement(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement</a>): boolean
|
||||
</pre>
|
||||
|
||||
**How to use**
|
||||
@@ -80,10 +80,10 @@ excalidrawAPI.updateScene(scene);
|
||||
<pre>
|
||||
loadFromBlob(<br/>
|
||||
blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,<br/>
|
||||
localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a> | null,<br/>
|
||||
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>
|
||||
localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a> | null,<br/>
|
||||
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>
|
||||
fileHandle?: FileSystemHandle | null <br/>
|
||||
) => Promise<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/restore.ts#L61">RestoredDataState</a>>
|
||||
) => Promise<<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/restore.ts#L61">RestoredDataState</a>>
|
||||
</pre>
|
||||
|
||||
### loadLibraryFromBlob
|
||||
@@ -130,10 +130,10 @@ if (contents.type === MIME_TYPES.excalidraw) {
|
||||
<pre>
|
||||
loadSceneOrLibraryFromBlob(<br/>
|
||||
blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,<br/>
|
||||
localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a> | null,<br/>
|
||||
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>
|
||||
localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a> | null,<br/>
|
||||
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>
|
||||
fileHandle?: FileSystemHandle | null<br/>
|
||||
) => Promise<{ type: string, data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/restore.ts#L53">RestoredDataState</a> | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L33">ImportedLibraryState</a>}>
|
||||
) => Promise<{ type: string, data: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/restore.ts#L53">RestoredDataState</a> | <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L33">ImportedLibraryState</a>}>
|
||||
</pre>
|
||||
|
||||
### getFreeDrawSvgPath
|
||||
@@ -149,7 +149,7 @@ import { getFreeDrawSvgPath } from "@excalidraw/excalidraw";
|
||||
**Signature**
|
||||
|
||||
<pre>
|
||||
getFreeDrawSvgPath(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L182">ExcalidrawFreeDrawElement</a>)
|
||||
getFreeDrawSvgPath(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L182">ExcalidrawFreeDrawElement</a>)
|
||||
</pre>
|
||||
|
||||
### isLinearElement
|
||||
@@ -165,7 +165,7 @@ import { isLinearElement } from "@excalidraw/excalidraw";
|
||||
**Signature**
|
||||
|
||||
<pre>
|
||||
isLinearElement(elementType?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L80">ExcalidrawElement</a>): boolean
|
||||
isLinearElement(elementType?: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L80">ExcalidrawElement</a>): boolean
|
||||
</pre>
|
||||
|
||||
### getNonDeletedElements
|
||||
@@ -181,7 +181,7 @@ import { getNonDeletedElements } from "@excalidraw/excalidraw";
|
||||
**Signature**
|
||||
|
||||
<pre>
|
||||
getNonDeletedElements(elements:<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114"> readonly ExcalidrawElement[]</a>): as readonly <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L125">NonDeletedExcalidrawElement[]</a>
|
||||
getNonDeletedElements(elements:<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114"> readonly ExcalidrawElement[]</a>): as readonly <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L125">NonDeletedExcalidrawElement[]</a>
|
||||
</pre>
|
||||
|
||||
### mergeLibraryItems
|
||||
@@ -196,9 +196,9 @@ import { mergeLibraryItems } from "@excalidraw/excalidraw";
|
||||
|
||||
<pre>
|
||||
mergeLibraryItems(<br/>
|
||||
localItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L250">LibraryItems</a>,<br/>
|
||||
otherItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a><br/>
|
||||
): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L250">LibraryItems</a>
|
||||
localItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L250">LibraryItems</a>,<br/>
|
||||
otherItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L200">LibraryItems</a><br/>
|
||||
): <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L250">LibraryItems</a>
|
||||
</pre>
|
||||
|
||||
### parseLibraryTokensFromUrl
|
||||
@@ -239,8 +239,8 @@ export const App = () => {
|
||||
|
||||
<pre>
|
||||
useHandleLibrary(opts: {<br/>
|
||||
excalidrawAPI: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L494">ExcalidrawAPI</a>,<br/>
|
||||
getInitialLibraryItems?: () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L253">LibraryItemsSource</a><br/>
|
||||
excalidrawAPI: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L494">ExcalidrawAPI</a>,<br/>
|
||||
getInitialLibraryItems?: () => <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L253">LibraryItemsSource</a><br/>
|
||||
});
|
||||
</pre>
|
||||
|
||||
@@ -253,7 +253,7 @@ This function returns the current `scene` version.
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
getSceneVersion(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>)
|
||||
getSceneVersion(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a>)
|
||||
</pre>
|
||||
|
||||
**How to use**
|
||||
@@ -274,7 +274,7 @@ import { sceneCoordsToViewportCoords } from "@excalidraw/excalidraw";
|
||||
|
||||
<pre>
|
||||
sceneCoordsToViewportCoords({ sceneX: number, sceneY: number },<br/>
|
||||
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a><br/>): { x: number, y: number }
|
||||
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a><br/>): { x: number, y: number }
|
||||
</pre>
|
||||
|
||||
### viewportCoordsToSceneCoords
|
||||
@@ -289,7 +289,7 @@ import { viewportCoordsToSceneCoords } from "@excalidraw/excalidraw";
|
||||
|
||||
<pre>
|
||||
viewportCoordsToSceneCoords({ clientX: number, clientY: number },<br/>
|
||||
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a><br/>): {x: number, y: number}
|
||||
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a><br/>): {x: number, y: number}
|
||||
</pre>
|
||||
|
||||
### useDevice
|
||||
@@ -350,8 +350,8 @@ To help with localization, we export the following.
|
||||
| name | type |
|
||||
| --- | --- |
|
||||
| `defaultLang` | `string` |
|
||||
| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) |
|
||||
| `useI18n` | [`() => { langCode, t }`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) |
|
||||
| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L15) |
|
||||
| `useI18n` | [`() => { langCode, t }`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L15) |
|
||||
|
||||
```js
|
||||
import { defaultLang, languages, useI18n } from "@excalidraw/excalidraw";
|
||||
|
@@ -21,7 +21,7 @@ Most notably, you can customize the primary colors, by overriding these variable
|
||||
- `--color-primary-light`
|
||||
- `--color-primary-contrast-offset` — a slightly darker (in light mode), or lighter (in dark mode) `--color-primary` color to fix contrast issues (see [Chubb illusion](https://en.wikipedia.org/wiki/Chubb_illusion)). It will fall back to `--color-primary` if not present.
|
||||
|
||||
For a complete list of variables, check [theme.scss](https://github.com/excalidraw/excalidraw/blob/master/src/css/theme.scss), though most of them will not make sense to override.
|
||||
For a complete list of variables, check [theme.scss](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/css/theme.scss), though most of them will not make sense to override.
|
||||
|
||||
```css showLineNumbers
|
||||
.custom-styles .excalidraw {
|
||||
|
@@ -13,7 +13,7 @@ To start the example app using the `@excalidraw/excalidraw` package, follow the
|
||||
1. Install the dependencies
|
||||
|
||||
```bash
|
||||
cd src/packages/excalidraw && yarn
|
||||
cd packages/excalidraw && yarn
|
||||
```
|
||||
|
||||
2. Start the example app
|
||||
|
@@ -39,7 +39,7 @@ Since Vite removes env variables by default, you can update the vite config to e
|
||||
|
||||
```
|
||||
define: {
|
||||
"process.env.IS_PREACT": process.env.IS_PREACT,
|
||||
"process.env.IS_PREACT": JSON.stringify("true"),
|
||||
},
|
||||
```
|
||||
|
||||
|
@@ -32,15 +32,9 @@ function App() {
|
||||
|
||||
### Next.js
|
||||
|
||||
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
|
||||
Since Excalidraw doesn't support `server side rendering` so it should be rendered only on `client`. The way to achieve this in next.js is using `next.js dynamic import`.
|
||||
|
||||
Here are two ways on how you can render **Excalidraw** on **Next.js**.
|
||||
|
||||
|
||||
|
||||
1. Using **Next.js Dynamic** import [Recommended].
|
||||
|
||||
Since Excalidraw doesn't support server side rendering so you can also use `dynamic import` to render by setting `ssr` to `false`.
|
||||
If you want to only import `Excalidraw` component you can do :point_down:
|
||||
|
||||
```jsx showLineNumbers
|
||||
import dynamic from "next/dynamic";
|
||||
@@ -55,25 +49,88 @@ export default function App() {
|
||||
}
|
||||
```
|
||||
|
||||
Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-dynamic-k8yjq2).
|
||||
However the above component only works for named component exports. If you want to import some util / constant or something else apart from Excalidraw, then this approach will not work. Instead you can write a wrapper over Excalidraw and import the wrapper dynamically.
|
||||
|
||||
If you are using `pages router` then importing the wrapper dynamically would work, where as if you are using `app router` then you will have to also add `useClient` directive on top of the file in addition to dynamically importing the wrapper as shown :point_down:
|
||||
|
||||
2. Importing Excalidraw once **client** is rendered.
|
||||
<Tabs>
|
||||
<TabItem value="Excalidraw Wrapper" label="Excalidraw Wrapper" >
|
||||
|
||||
```jsx showLineNumbers
|
||||
import { useState, useEffect } from "react";
|
||||
export default function App() {
|
||||
const [Excalidraw, setExcalidraw] = useState(null);
|
||||
useEffect(() => {
|
||||
import("@excalidraw/excalidraw").then((comp) =>
|
||||
setExcalidraw(comp.Excalidraw),
|
||||
```jsx showLineNumbers
|
||||
"use client";
|
||||
import { Excalidraw, convertToExcalidrawElements } from "@excalidraw/excalidraw";
|
||||
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
|
||||
const ExcalidrawWrapper: React.FC = () => {
|
||||
console.info(convertToExcalidrawElements([{
|
||||
type: "rectangle",
|
||||
id: "rect-1",
|
||||
width: 186.47265625,
|
||||
height: 141.9765625,
|
||||
},]));
|
||||
return (
|
||||
<div style={{height:"500px", width:"500px"}}>
|
||||
<Excalidraw />
|
||||
</div>
|
||||
);
|
||||
}, []);
|
||||
return <>{Excalidraw && <Excalidraw />}</>;
|
||||
}
|
||||
```
|
||||
};
|
||||
export default ExcalidrawWrapper;
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pages" label="Pages router">
|
||||
|
||||
```jsx showLineNumbers
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
// Since client components get prerenderd on server as well hence importing
|
||||
// the excalidraw stuff dynamically with ssr false
|
||||
|
||||
const ExcalidrawWrapper = dynamic(
|
||||
async () => (await import("../excalidrawWrapper")).default,
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ExcalidrawWrapper />
|
||||
);
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="app" label="App router">
|
||||
|
||||
```jsx showLineNumbers
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
// Since client components get prerenderd on server as well hence importing
|
||||
// the excalidraw stuff dynamically with ssr false
|
||||
|
||||
const ExcalidrawWrapper = dynamic(
|
||||
async () => (await import("../excalidrawWrapper")).default,
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ExcalidrawWrapper />
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
Here is a [source code](https://github.com/excalidraw/excalidraw/tree/master/examples/excalidraw/with-nextjs) for the example with app and pages router. You you can try it out [here](https://excalidraw-package-example-with-nextjs-gh6smrdnq-excalidraw.vercel.app/).
|
||||
|
||||
Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-5xb3d)
|
||||
|
||||
The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm)
|
||||
|
||||
@@ -93,7 +150,7 @@ Since Vite removes env variables by default, you can update the vite config to e
|
||||
|
||||
```
|
||||
define: {
|
||||
"process.env.IS_PREACT": process.env.IS_PREACT,
|
||||
"process.env.IS_PREACT": JSON.stringify("true"),
|
||||
},
|
||||
```
|
||||
:::
|
||||
@@ -148,7 +205,7 @@ import TabItem from "@theme/TabItem";
|
||||
<h1>Excalidraw Embed Example</h1>
|
||||
<div id="app"></div>
|
||||
</div>
|
||||
<script type="text/javascript" src="src/index.js"></script>
|
||||
<script type="text/javascript" src="packages/excalidraw/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
@@ -38,9 +38,9 @@ Add the diagram type in switch case in [`parseMermaid`](https://github.com/excal
|
||||
|
||||
## Writing the Excalidraw Skeleton Convertor
|
||||
|
||||
With the completion of previous step, we have all the data, now we need to transform it so to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133) format.
|
||||
With the completion of previous step, we have all the data, now we need to transform it so to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133) format.
|
||||
|
||||
Similar to [`FlowChartToExcalidrawSkeletonConverter`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/converter/types/flowchart.ts#L24), you have to write the `{{diagramType}}ToExcalidrawSkeletonConverter` which parses the data received in previous step and returns the [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133).
|
||||
Similar to [`FlowChartToExcalidrawSkeletonConverter`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/converter/types/flowchart.ts#L24), you have to write the `{{diagramType}}ToExcalidrawSkeletonConverter` which parses the data received in previous step and returns the [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133).
|
||||
|
||||
Thats it, you have added the new diagram type 🥳, now lets test it out!
|
||||
|
||||
|
@@ -6,7 +6,7 @@ In this section we will be diving into how the [flowchart parser](https://github
|
||||
|
||||

|
||||
|
||||
We use `diagram.parser.yy` attribute to parse the data. If you want to know more about how the `diagram.parse.yy` attribute looks like, you can check it [here](https://github.com/mermaid-js/mermaid/blob/00d06c7282a701849793680c1e97da1cfdfcce62/packages/mermaid/src/diagrams/flowchart/flowDb.js#L768), however for scope of flowchart we are using **3** APIs from this parser to compute `vertices`, `edges` and `clusters` as we need these data to transform to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133C13-L133C38).
|
||||
We use `diagram.parser.yy` attribute to parse the data. If you want to know more about how the `diagram.parse.yy` attribute looks like, you can check it [here](https://github.com/mermaid-js/mermaid/blob/00d06c7282a701849793680c1e97da1cfdfcce62/packages/mermaid/src/diagrams/flowchart/flowDb.js#L768), however for scope of flowchart we are using **3** APIs from this parser to compute `vertices`, `edges` and `clusters` as we need these data to transform to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133C13-L133C38).
|
||||
|
||||
|
||||
For computing `vertices` and `edge`s lets consider the below svg generated by mermaid
|
||||
@@ -42,7 +42,7 @@ Considering the same example this is the response from the API
|
||||
}
|
||||
}
|
||||
```
|
||||
The dimensions and position is missing in this response and we need that to transform to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133C13-L133C38), for this we have our own parser [`parseVertex`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/parseMermaid.ts#L178) which takes the above response and uses the `svg` together to compute position, dimensions and cleans up the response.
|
||||
The dimensions and position is missing in this response and we need that to transform to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133C13-L133C38), for this we have our own parser [`parseVertex`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/parseMermaid.ts#L178) which takes the above response and uses the `svg` together to compute position, dimensions and cleans up the response.
|
||||
|
||||
The final output from `parseVertex` looks like :point_down:
|
||||
|
||||
|
@@ -55,11 +55,11 @@ If you want to understand how flowchart parser works, you can navigate to [Flowc
|
||||
|
||||
## Converting to ExcalidrawElementSkeleton
|
||||
|
||||
Now we have all the data, we just need to transform it to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133C13-L133C38) API so it can be rendered in Excalidraw.
|
||||
Now we have all the data, we just need to transform it to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133C13-L133C38) API so it can be rendered in Excalidraw.
|
||||
|
||||
For this we have `converters` which takes the parsed mermaid data and gives back the Excalidraw Skeleton.
|
||||
For Unsupported types, we have already mentioned above that we convert it to `dataURL` and return the ExcalidrawImageSkeleton.
|
||||
|
||||
For supported types, currently only flowchart, we have [flowchartConverter](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/converter/types/flowchart.ts#L24) which parses the data and converts to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133C13-L133C38).
|
||||
For supported types, currently only flowchart, we have [flowchartConverter](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/converter/types/flowchart.ts#L24) which parses the data and converts to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133C13-L133C38).
|
||||
|
||||

|
@@ -52,15 +52,6 @@ Make sure the title starts with a semantic prefix:
|
||||
- **chore**: Other changes that don't modify src or test files
|
||||
- **revert**: Reverts a previous commit
|
||||
|
||||
### Changelog
|
||||
|
||||
Add a brief description of your pull request to the changelog located here: [changelog](https://github.com/excalidraw/excalidraw/blob/master/CHANGELOG.md)
|
||||
|
||||
Notes:
|
||||
|
||||
- Make sure to prepend to the section corresponding with the semantic prefix you selected in the title
|
||||
- Link to your pull request - this will require updating the CHANGELOG _after_ creating the pull request
|
||||
|
||||
### Testing
|
||||
|
||||
Once you submit your pull request it will automatically be tested. Be sure to check the results of the test and fix any issues that arise.
|
||||
|
@@ -41,10 +41,7 @@ const config = {
|
||||
showLastUpdateTime: true,
|
||||
},
|
||||
theme: {
|
||||
customCss: [
|
||||
require.resolve("./src/css/custom.scss"),
|
||||
require.resolve("../src/packages/excalidraw/example/App.scss"),
|
||||
],
|
||||
customCss: [require.resolve("./src/css/custom.scss")],
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
4
dev-docs/vercel.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"outputDirectory": "build",
|
||||
"installCommand": "yarn install"
|
||||
}
|
@@ -15,14 +15,23 @@
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
.app-title {
|
||||
margin-block-start: 0.83em;
|
||||
margin-block-end: 0.83em;
|
||||
}
|
||||
}
|
||||
|
||||
.button-wrapper button {
|
||||
z-index: 1;
|
||||
height: 40px;
|
||||
max-width: 200px;
|
||||
margin: 10px;
|
||||
padding: 5px;
|
||||
.button-wrapper {
|
||||
input[type="checkbox"] {
|
||||
margin: 5px;
|
||||
}
|
||||
button {
|
||||
z-index: 1;
|
||||
height: 40px;
|
||||
max-width: 200px;
|
||||
margin: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.excalidraw .App-menu_top .buttonList {
|
@@ -1,23 +1,31 @@
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
|
||||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
useRef,
|
||||
useCallback,
|
||||
Children,
|
||||
cloneElement,
|
||||
} from "react";
|
||||
import ExampleSidebar from "./sidebar/ExampleSidebar";
|
||||
|
||||
import type * as TExcalidraw from "../index";
|
||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||
|
||||
import "./App.scss";
|
||||
import initialData from "./initialData";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import {
|
||||
resolvablePromise,
|
||||
ResolvablePromise,
|
||||
distance2d,
|
||||
fileOpen,
|
||||
withBatchedUpdates,
|
||||
withBatchedUpdatesThrottled,
|
||||
} from "../../../utils";
|
||||
import { EVENT, ROUNDNESS } from "../../../constants";
|
||||
import { distance2d } from "../../../math";
|
||||
import { fileOpen } from "../../../data/filesystem";
|
||||
import { loadSceneOrLibraryFromBlob } from "../../utils";
|
||||
import {
|
||||
} from "../utils";
|
||||
|
||||
import CustomFooter from "./CustomFooter";
|
||||
import MobileFooter from "./MobileFooter";
|
||||
import initialData from "../initialData";
|
||||
|
||||
import type {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
ExcalidrawImperativeAPI,
|
||||
@@ -25,18 +33,14 @@ import {
|
||||
Gesture,
|
||||
LibraryItems,
|
||||
PointerDownState as ExcalidrawPointerDownState,
|
||||
} from "../../../types";
|
||||
import { NonDeletedExcalidrawElement, Theme } from "../../../element/types";
|
||||
import { ImportedLibraryData } from "../../../data/types";
|
||||
import CustomFooter from "./CustomFooter";
|
||||
import MobileFooter from "./MobileFooter";
|
||||
import { KEYS } from "../../../keys";
|
||||
} from "@excalidraw/excalidraw/dist/excalidraw/types";
|
||||
import type {
|
||||
NonDeletedExcalidrawElement,
|
||||
Theme,
|
||||
} from "@excalidraw/excalidraw/dist/excalidraw/element/types";
|
||||
import type { ImportedLibraryData } from "@excalidraw/excalidraw/dist/excalidraw/data/types";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ExcalidrawLib: typeof TExcalidraw;
|
||||
}
|
||||
}
|
||||
import "./App.scss";
|
||||
|
||||
type Comment = {
|
||||
x: number;
|
||||
@@ -57,27 +61,6 @@ type PointerDownState = {
|
||||
};
|
||||
};
|
||||
|
||||
// This is so that we use the bundled excalidraw.development.js file instead
|
||||
// of the actual source code
|
||||
const {
|
||||
exportToCanvas,
|
||||
exportToSvg,
|
||||
exportToBlob,
|
||||
exportToClipboard,
|
||||
Excalidraw,
|
||||
useHandleLibrary,
|
||||
MIME_TYPES,
|
||||
sceneCoordsToViewportCoords,
|
||||
viewportCoordsToSceneCoords,
|
||||
restoreElements,
|
||||
Sidebar,
|
||||
Footer,
|
||||
WelcomeScreen,
|
||||
MainMenu,
|
||||
LiveCollaborationTrigger,
|
||||
convertToExcalidrawElements,
|
||||
} = window.ExcalidrawLib;
|
||||
|
||||
const COMMENT_ICON_DIMENSION = 32;
|
||||
const COMMENT_INPUT_HEIGHT = 50;
|
||||
const COMMENT_INPUT_WIDTH = 150;
|
||||
@@ -86,9 +69,38 @@ export interface AppProps {
|
||||
appTitle: string;
|
||||
useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void;
|
||||
customArgs?: any[];
|
||||
children: React.ReactNode;
|
||||
excalidrawLib: typeof TExcalidraw;
|
||||
}
|
||||
|
||||
export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
export default function App({
|
||||
appTitle,
|
||||
useCustom,
|
||||
customArgs,
|
||||
children,
|
||||
excalidrawLib,
|
||||
}: AppProps) {
|
||||
const {
|
||||
exportToCanvas,
|
||||
exportToSvg,
|
||||
exportToBlob,
|
||||
exportToClipboard,
|
||||
useHandleLibrary,
|
||||
MIME_TYPES,
|
||||
sceneCoordsToViewportCoords,
|
||||
viewportCoordsToSceneCoords,
|
||||
restoreElements,
|
||||
Sidebar,
|
||||
Footer,
|
||||
WelcomeScreen,
|
||||
MainMenu,
|
||||
LiveCollaborationTrigger,
|
||||
convertToExcalidrawElements,
|
||||
TTDDialog,
|
||||
TTDDialogTrigger,
|
||||
ROUNDNESS,
|
||||
loadSceneOrLibraryFromBlob,
|
||||
} = excalidrawLib;
|
||||
const appRef = useRef<any>(null);
|
||||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
||||
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
||||
@@ -150,8 +162,105 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
};
|
||||
};
|
||||
fetchData();
|
||||
}, [excalidrawAPI]);
|
||||
}, [excalidrawAPI, convertToExcalidrawElements, MIME_TYPES]);
|
||||
|
||||
const renderExcalidraw = (children: React.ReactNode) => {
|
||||
const Excalidraw: any = Children.toArray(children).find(
|
||||
(child) =>
|
||||
React.isValidElement(child) &&
|
||||
typeof child.type !== "string" &&
|
||||
//@ts-ignore
|
||||
child.type.displayName === "Excalidraw",
|
||||
);
|
||||
if (!Excalidraw) {
|
||||
return;
|
||||
}
|
||||
const newElement = cloneElement(
|
||||
Excalidraw,
|
||||
{
|
||||
excalidrawAPI: (api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api),
|
||||
initialData: initialStatePromiseRef.current.promise,
|
||||
onChange: (
|
||||
elements: NonDeletedExcalidrawElement[],
|
||||
state: AppState,
|
||||
) => {
|
||||
console.info("Elements :", elements, "State : ", state);
|
||||
},
|
||||
onPointerUpdate: (payload: {
|
||||
pointer: { x: number; y: number };
|
||||
button: "down" | "up";
|
||||
pointersMap: Gesture["pointers"];
|
||||
}) => setPointerData(payload),
|
||||
viewModeEnabled,
|
||||
zenModeEnabled,
|
||||
gridModeEnabled,
|
||||
theme,
|
||||
name: "Custom name of drawing",
|
||||
UIOptions: {
|
||||
canvasActions: {
|
||||
loadScene: false,
|
||||
},
|
||||
tools: { image: !disableImageTool },
|
||||
},
|
||||
renderTopRightUI,
|
||||
onLinkOpen,
|
||||
onPointerDown,
|
||||
onScrollChange: rerenderCommentIcons,
|
||||
validateEmbeddable: true,
|
||||
},
|
||||
<>
|
||||
{excalidrawAPI && (
|
||||
<Footer>
|
||||
<CustomFooter
|
||||
excalidrawAPI={excalidrawAPI}
|
||||
excalidrawLib={excalidrawLib}
|
||||
/>
|
||||
</Footer>
|
||||
)}
|
||||
<WelcomeScreen />
|
||||
<Sidebar name="custom">
|
||||
<Sidebar.Tabs>
|
||||
<Sidebar.Header />
|
||||
<Sidebar.Tab tab="one">Tab one!</Sidebar.Tab>
|
||||
<Sidebar.Tab tab="two">Tab two!</Sidebar.Tab>
|
||||
<Sidebar.TabTriggers>
|
||||
<Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger>
|
||||
<Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger>
|
||||
</Sidebar.TabTriggers>
|
||||
</Sidebar.Tabs>
|
||||
</Sidebar>
|
||||
<Sidebar.Trigger
|
||||
name="custom"
|
||||
tab="one"
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
bottom: "20px",
|
||||
zIndex: 9999999999999999,
|
||||
}}
|
||||
>
|
||||
Toggle Custom Sidebar
|
||||
</Sidebar.Trigger>
|
||||
{renderMenu()}
|
||||
{excalidrawAPI && (
|
||||
<TTDDialogTrigger icon={<span>😀</span>}>
|
||||
Text to diagram
|
||||
</TTDDialogTrigger>
|
||||
)}
|
||||
<TTDDialog
|
||||
onTextSubmit={async (_) => {
|
||||
console.info("submit");
|
||||
// sleep for 2s
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
throw new Error("error, go away now");
|
||||
// return "dummy";
|
||||
}}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
return newElement;
|
||||
};
|
||||
const renderTopRightUI = (isMobile: boolean) => {
|
||||
return (
|
||||
<>
|
||||
@@ -335,8 +444,8 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
pointerDownState: PointerDownState,
|
||||
) => {
|
||||
return withBatchedUpdates((event) => {
|
||||
window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.onMove);
|
||||
window.removeEventListener(EVENT.POINTER_UP, pointerDownState.onUp);
|
||||
window.removeEventListener("pointermove", pointerDownState.onMove);
|
||||
window.removeEventListener("pointerup", pointerDownState.onUp);
|
||||
excalidrawAPI?.setActiveTool({ type: "selection" });
|
||||
const distance = distance2d(
|
||||
pointerDownState.x,
|
||||
@@ -400,8 +509,8 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
onPointerMoveFromPointerDownHandler(pointerDownState);
|
||||
const onPointerUp =
|
||||
onPointerUpFromPointerDownHandler(pointerDownState);
|
||||
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
||||
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
|
||||
window.addEventListener("pointermove", onPointerMove);
|
||||
window.addEventListener("pointerup", onPointerUp);
|
||||
|
||||
pointerDownState.onMove = onPointerMove;
|
||||
pointerDownState.onUp = onPointerUp;
|
||||
@@ -493,7 +602,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
}}
|
||||
onBlur={saveComment}
|
||||
onKeyDown={(event) => {
|
||||
if (!event.shiftKey && event.key === KEYS.ENTER) {
|
||||
if (!event.shiftKey && event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
saveComment();
|
||||
}
|
||||
@@ -526,7 +635,12 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
</MainMenu.ItemCustom>
|
||||
<MainMenu.DefaultItems.Help />
|
||||
|
||||
{excalidrawAPI && <MobileFooter excalidrawAPI={excalidrawAPI} />}
|
||||
{excalidrawAPI && (
|
||||
<MobileFooter
|
||||
excalidrawLib={excalidrawLib}
|
||||
excalidrawAPI={excalidrawAPI}
|
||||
/>
|
||||
)}
|
||||
</MainMenu>
|
||||
);
|
||||
};
|
||||
@@ -675,69 +789,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="excalidraw-wrapper">
|
||||
<Excalidraw
|
||||
excalidrawAPI={(api: ExcalidrawImperativeAPI) =>
|
||||
setExcalidrawAPI(api)
|
||||
}
|
||||
initialData={initialStatePromiseRef.current.promise}
|
||||
onChange={(elements, state) => {
|
||||
console.info("Elements :", elements, "State : ", state);
|
||||
}}
|
||||
onPointerUpdate={(payload: {
|
||||
pointer: { x: number; y: number };
|
||||
button: "down" | "up";
|
||||
pointersMap: Gesture["pointers"];
|
||||
}) => setPointerData(payload)}
|
||||
viewModeEnabled={viewModeEnabled}
|
||||
zenModeEnabled={zenModeEnabled}
|
||||
gridModeEnabled={gridModeEnabled}
|
||||
theme={theme}
|
||||
name="Custom name of drawing"
|
||||
UIOptions={{
|
||||
canvasActions: {
|
||||
loadScene: false,
|
||||
},
|
||||
tools: { image: !disableImageTool },
|
||||
}}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
onLinkOpen={onLinkOpen}
|
||||
onPointerDown={onPointerDown}
|
||||
onScrollChange={rerenderCommentIcons}
|
||||
// allow all urls
|
||||
validateEmbeddable={true}
|
||||
>
|
||||
{excalidrawAPI && (
|
||||
<Footer>
|
||||
<CustomFooter excalidrawAPI={excalidrawAPI} />
|
||||
</Footer>
|
||||
)}
|
||||
<WelcomeScreen />
|
||||
<Sidebar name="custom">
|
||||
<Sidebar.Tabs>
|
||||
<Sidebar.Header />
|
||||
<Sidebar.Tab tab="one">Tab one!</Sidebar.Tab>
|
||||
<Sidebar.Tab tab="two">Tab two!</Sidebar.Tab>
|
||||
<Sidebar.TabTriggers>
|
||||
<Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger>
|
||||
<Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger>
|
||||
</Sidebar.TabTriggers>
|
||||
</Sidebar.Tabs>
|
||||
</Sidebar>
|
||||
<Sidebar.Trigger
|
||||
name="custom"
|
||||
tab="one"
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
bottom: "20px",
|
||||
zIndex: 9999999999999999,
|
||||
}}
|
||||
>
|
||||
Toggle Custom Sidebar
|
||||
</Sidebar.Trigger>
|
||||
{renderMenu()}
|
||||
</Excalidraw>
|
||||
{renderExcalidraw(children)}
|
||||
{Object.keys(commentIcons || []).length > 0 && renderCommentIcons()}
|
||||
{comment && renderComment()}
|
||||
</div>
|
@@ -1,6 +1,5 @@
|
||||
import { ExcalidrawImperativeAPI } from "../../../types";
|
||||
import { MIME_TYPES } from "../entry";
|
||||
import { Button } from "../../../components/Button";
|
||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
|
||||
|
||||
const COMMENT_SVG = (
|
||||
<svg
|
||||
@@ -18,24 +17,28 @@ const COMMENT_SVG = (
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CustomFooter = ({
|
||||
excalidrawAPI,
|
||||
excalidrawLib,
|
||||
}: {
|
||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||
excalidrawLib: typeof TExcalidraw;
|
||||
}) => {
|
||||
const { Button, MIME_TYPES } = excalidrawLib;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onSelect={() => alert("General Kenobi!")}
|
||||
className="you are a bold one"
|
||||
style={{ marginLeft: "1rem" }}
|
||||
style={{ marginLeft: "1rem", width: "auto" }}
|
||||
title="Hello there!"
|
||||
>
|
||||
{COMMENT_SVG}
|
||||
Hit me
|
||||
</Button>
|
||||
<button
|
||||
<Button
|
||||
className="custom-element"
|
||||
onClick={() => {
|
||||
onSelect={() => {
|
||||
excalidrawAPI?.setActiveTool({
|
||||
type: "custom",
|
||||
customType: "comment",
|
||||
@@ -58,15 +61,10 @@ const CustomFooter = ({
|
||||
)}`;
|
||||
excalidrawAPI?.setCursor(`url(${url}), auto`);
|
||||
}}
|
||||
title="Comments!"
|
||||
>
|
||||
{COMMENT_SVG}
|
||||
</button>
|
||||
<button
|
||||
className="custom-footer"
|
||||
onClick={() => alert("This is dummy footer")}
|
||||
>
|
||||
custom footer
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
27
examples/excalidraw/components/MobileFooter.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
|
||||
import CustomFooter from "./CustomFooter";
|
||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||
|
||||
const MobileFooter = ({
|
||||
excalidrawAPI,
|
||||
excalidrawLib,
|
||||
}: {
|
||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||
excalidrawLib: typeof TExcalidraw;
|
||||
}) => {
|
||||
const { useDevice, Footer } = excalidrawLib;
|
||||
|
||||
const device = useDevice();
|
||||
if (device.editor.isMobile) {
|
||||
return (
|
||||
<Footer>
|
||||
<CustomFooter
|
||||
excalidrawAPI={excalidrawAPI}
|
||||
excalidrawLib={excalidrawLib}
|
||||
/>
|
||||
</Footer>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
export default MobileFooter;
|
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import "./ExampleSidebar.scss";
|
||||
|
||||
export default function Sidebar({ children }: { children: React.ReactNode }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { ExcalidrawElementSkeleton } from "../../../data/transform";
|
||||
import { FileId } from "../../../element/types";
|
||||
import type { ExcalidrawElementSkeleton } from "@excalidraw/excalidraw/data/transform";
|
||||
import type { FileId } from "@excalidraw/excalidraw/element/types";
|
||||
|
||||
const elements: ExcalidrawElementSkeleton[] = [
|
||||
{
|
13
examples/excalidraw/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "examples",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"@excalidraw/excalidraw": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
3
examples/excalidraw/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig"
|
||||
}
|
146
examples/excalidraw/utils.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { unstable_batchedUpdates } from "react-dom";
|
||||
import { fileOpen as _fileOpen } from "browser-fs-access";
|
||||
import type { MIME_TYPES } from "@excalidraw/excalidraw";
|
||||
import { AbortError } from "../../packages/excalidraw/errors";
|
||||
|
||||
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
|
||||
|
||||
const INPUT_CHANGE_INTERVAL_MS = 500;
|
||||
|
||||
export type ResolvablePromise<T> = Promise<T> & {
|
||||
resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void;
|
||||
reject: (error: Error) => void;
|
||||
};
|
||||
export const resolvablePromise = <T>() => {
|
||||
let resolve!: any;
|
||||
let reject!: any;
|
||||
const promise = new Promise((_resolve, _reject) => {
|
||||
resolve = _resolve;
|
||||
reject = _reject;
|
||||
});
|
||||
(promise as any).resolve = resolve;
|
||||
(promise as any).reject = reject;
|
||||
return promise as ResolvablePromise<T>;
|
||||
};
|
||||
|
||||
export const distance2d = (x1: number, y1: number, x2: number, y2: number) => {
|
||||
const xd = x2 - x1;
|
||||
const yd = y2 - y1;
|
||||
return Math.hypot(xd, yd);
|
||||
};
|
||||
|
||||
export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
||||
extensions?: FILE_EXTENSION[];
|
||||
description: string;
|
||||
multiple?: M;
|
||||
}): Promise<M extends false | undefined ? File : File[]> => {
|
||||
// an unsafe TS hack, alas not much we can do AFAIK
|
||||
type RetType = M extends false | undefined ? File : File[];
|
||||
|
||||
const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {
|
||||
mimeTypes.push(MIME_TYPES[type]);
|
||||
|
||||
return mimeTypes;
|
||||
}, [] as string[]);
|
||||
|
||||
const extensions = opts.extensions?.reduce((acc, ext) => {
|
||||
if (ext === "jpg") {
|
||||
return acc.concat(".jpg", ".jpeg");
|
||||
}
|
||||
return acc.concat(`.${ext}`);
|
||||
}, [] as string[]);
|
||||
|
||||
return _fileOpen({
|
||||
description: opts.description,
|
||||
extensions,
|
||||
mimeTypes,
|
||||
multiple: opts.multiple ?? false,
|
||||
legacySetup: (resolve, reject, input) => {
|
||||
const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS);
|
||||
const focusHandler = () => {
|
||||
checkForFile();
|
||||
document.addEventListener("keyup", scheduleRejection);
|
||||
document.addEventListener("pointerup", scheduleRejection);
|
||||
scheduleRejection();
|
||||
};
|
||||
const checkForFile = () => {
|
||||
// this hack might not work when expecting multiple files
|
||||
if (input.files?.length) {
|
||||
const ret = opts.multiple ? [...input.files] : input.files[0];
|
||||
resolve(ret as RetType);
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(() => {
|
||||
window.addEventListener("focus", focusHandler);
|
||||
});
|
||||
const interval = window.setInterval(() => {
|
||||
checkForFile();
|
||||
}, INPUT_CHANGE_INTERVAL_MS);
|
||||
return (rejectPromise) => {
|
||||
clearInterval(interval);
|
||||
scheduleRejection.cancel();
|
||||
window.removeEventListener("focus", focusHandler);
|
||||
document.removeEventListener("keyup", scheduleRejection);
|
||||
document.removeEventListener("pointerup", scheduleRejection);
|
||||
if (rejectPromise) {
|
||||
// so that something is shown in console if we need to debug this
|
||||
console.warn("Opening the file was canceled (legacy-fs).");
|
||||
rejectPromise(new AbortError());
|
||||
}
|
||||
};
|
||||
},
|
||||
}) as Promise<RetType>;
|
||||
};
|
||||
|
||||
export const debounce = <T extends any[]>(
|
||||
fn: (...args: T) => void,
|
||||
timeout: number,
|
||||
) => {
|
||||
let handle = 0;
|
||||
let lastArgs: T | null = null;
|
||||
const ret = (...args: T) => {
|
||||
lastArgs = args;
|
||||
clearTimeout(handle);
|
||||
handle = window.setTimeout(() => {
|
||||
lastArgs = null;
|
||||
fn(...args);
|
||||
}, timeout);
|
||||
};
|
||||
ret.flush = () => {
|
||||
clearTimeout(handle);
|
||||
if (lastArgs) {
|
||||
const _lastArgs = lastArgs;
|
||||
lastArgs = null;
|
||||
fn(..._lastArgs);
|
||||
}
|
||||
};
|
||||
ret.cancel = () => {
|
||||
lastArgs = null;
|
||||
clearTimeout(handle);
|
||||
};
|
||||
return ret;
|
||||
};
|
||||
|
||||
export const withBatchedUpdates = <
|
||||
TFunction extends ((event: any) => void) | (() => void),
|
||||
>(
|
||||
func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
|
||||
) =>
|
||||
((event) => {
|
||||
unstable_batchedUpdates(func as TFunction, event);
|
||||
}) as TFunction;
|
||||
|
||||
/**
|
||||
* barches React state updates and throttles the calls to a single call per
|
||||
* animation frame
|
||||
*/
|
||||
export const withBatchedUpdatesThrottled = <
|
||||
TFunction extends ((event: any) => void) | (() => void),
|
||||
>(
|
||||
func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
|
||||
) => {
|
||||
// @ts-ignore
|
||||
return throttleRAF<Parameters<TFunction>>(((event) => {
|
||||
unstable_batchedUpdates(func, event);
|
||||
}) as TFunction);
|
||||
};
|
36
examples/excalidraw/with-nextjs/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
36
examples/excalidraw/with-nextjs/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3005) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
12
examples/excalidraw/with-nextjs/next.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
distDir: "build",
|
||||
typescript: {
|
||||
// The ts config doesn't work with `jsx: preserve" and if updated to `react-jsx` it gets ovewritten by next js throwing ts errors hence I am ignoring build errors until this is fixed.
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
// This is needed as in pages router the code for importing types throws error as its outside next js app
|
||||
transpilePackages: ["../"],
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
25
examples/excalidraw/with-nextjs/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "with-nextjs",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm",
|
||||
"dev": "yarn build:workspace && next dev -p 3005",
|
||||
"build": "yarn build:workspace && next build",
|
||||
"start": "next start -p 3006",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@excalidraw/excalidraw": "*",
|
||||
"next": "14.1",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"path2d-polyfill": "2.0.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 197 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
BIN
examples/excalidraw/with-nextjs/src/app/favicon.ico
Normal file
After Width: | Height: | Size: 25 KiB |
11
examples/excalidraw/with-nextjs/src/app/layout.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
23
examples/excalidraw/with-nextjs/src/app/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import "../common.scss";
|
||||
|
||||
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
||||
// with ssr false
|
||||
const ExcalidrawWithClientOnly = dynamic(
|
||||
async () => (await import("../excalidrawWrapper")).default,
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<a href="/excalidraw-in-pages">Switch to Pages router</a>
|
||||
<h1 className="page-title">App Router</h1>
|
||||
|
||||
{/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */}
|
||||
<ExcalidrawWithClientOnly />
|
||||
</>
|
||||
);
|
||||
}
|
15
examples/excalidraw/with-nextjs/src/common.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #1c7ed6;
|
||||
font-size: 20px;
|
||||
text-decoration: none;
|
||||
font-weight: 550;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
text-align: center;
|
||||
}
|
22
examples/excalidraw/with-nextjs/src/excalidrawWrapper.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
import * as excalidrawLib from "@excalidraw/excalidraw";
|
||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||
import App from "../../components/App";
|
||||
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
|
||||
const ExcalidrawWrapper: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<App
|
||||
appTitle={"Excalidraw with Nextjs Example"}
|
||||
useCustom={(api: any, args?: any[]) => {}}
|
||||
excalidrawLib={excalidrawLib}
|
||||
>
|
||||
<Excalidraw />
|
||||
</App>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExcalidrawWrapper;
|
@@ -0,0 +1,22 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import "../common.scss";
|
||||
|
||||
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
||||
// with ssr false
|
||||
const Excalidraw = dynamic(
|
||||
async () => (await import("../excalidrawWrapper")).default,
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<a href="/">Switch to App router</a>
|
||||
<h1 className="page-title">Pages Router</h1>
|
||||
{/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */}
|
||||
<Excalidraw />
|
||||
</>
|
||||
);
|
||||
}
|
28
examples/excalidraw/with-nextjs/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "build/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
3
examples/excalidraw/with-nextjs/vercel.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"outputDirectory": "build"
|
||||
}
|
252
examples/excalidraw/with-nextjs/yarn.lock
Normal file
@@ -0,0 +1,252 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@excalidraw/excalidraw@workspace:^":
|
||||
version "0.17.2"
|
||||
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.17.2.tgz#9a636a1e6bb3c88c5883347d3a7e75e9cce8ab96"
|
||||
integrity sha512-7pqUWD8+mPjDhF4XxG3gw4rvE2JGaLW3Vss5UZfTbITPxAtFaGEc1K081bncitnaYhUwN9ENJE0i87QB3poDwQ==
|
||||
|
||||
"@next/env@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.4.tgz#d5cda0c4a862d70ae760e58c0cd96a8899a2e49a"
|
||||
integrity sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ==
|
||||
|
||||
"@next/swc-darwin-arm64@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz#27b1854c2cd04eb1d5e75081a1a792ad91526618"
|
||||
integrity sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg==
|
||||
|
||||
"@next/swc-darwin-x64@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz#9940c449e757d0ee50bb9e792d2600cc08a3eb3b"
|
||||
integrity sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz#0eafd27c8587f68ace7b4fa80695711a8434de21"
|
||||
integrity sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==
|
||||
|
||||
"@next/swc-linux-arm64-musl@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz#2b0072adb213f36dada5394ea67d6e82069ae7dd"
|
||||
integrity sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==
|
||||
|
||||
"@next/swc-linux-x64-gnu@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz#68c67d20ebc8e3f6ced6ff23a4ba2a679dbcec32"
|
||||
integrity sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==
|
||||
|
||||
"@next/swc-linux-x64-musl@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz#67cd81b42fb2caf313f7992fcf6d978af55a1247"
|
||||
integrity sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz#be06585906b195d755ceda28f33c633e1443f1a3"
|
||||
integrity sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==
|
||||
|
||||
"@next/swc-win32-ia32-msvc@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz#e76cabefa9f2d891599c3d85928475bd8d3f6600"
|
||||
integrity sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==
|
||||
|
||||
"@next/swc-win32-x64-msvc@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz#e74892f1a9ccf41d3bf5979ad6d3d77c07b9cba1"
|
||||
integrity sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==
|
||||
|
||||
"@swc/helpers@0.5.2":
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d"
|
||||
integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@types/node@^20":
|
||||
version "20.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.0.tgz#8e0b99e70c0c1ade1a86c4a282f7b7ef87c9552f"
|
||||
integrity sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563"
|
||||
integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==
|
||||
|
||||
"@types/react-dom@^18":
|
||||
version "18.2.18"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd"
|
||||
integrity sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^18":
|
||||
version "18.2.47"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.47.tgz#85074b27ab563df01fbc3f68dc64bf7050b0af40"
|
||||
integrity sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/scheduler@*":
|
||||
version "0.16.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff"
|
||||
integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==
|
||||
|
||||
busboy@1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
|
||||
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
|
||||
dependencies:
|
||||
streamsearch "^1.1.0"
|
||||
|
||||
caniuse-lite@^1.0.30001406:
|
||||
version "1.0.30001576"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz#893be772cf8ee6056d6c1e2d07df365b9ec0a5c4"
|
||||
integrity sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==
|
||||
|
||||
client-only@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||
|
||||
csstype@^3.0.2:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
||||
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
||||
|
||||
glob-to-regexp@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
|
||||
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
|
||||
|
||||
graceful-fs@^4.1.2, graceful-fs@^4.2.11:
|
||||
version "4.2.11"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
|
||||
loose-envify@^1.1.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
dependencies:
|
||||
js-tokens "^3.0.0 || ^4.0.0"
|
||||
|
||||
nanoid@^3.3.6:
|
||||
version "3.3.7"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
||||
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||
|
||||
next@14.0.4:
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-14.0.4.tgz#bf00b6f835b20d10a5057838fa2dfced1d0d84dc"
|
||||
integrity sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==
|
||||
dependencies:
|
||||
"@next/env" "14.0.4"
|
||||
"@swc/helpers" "0.5.2"
|
||||
busboy "1.6.0"
|
||||
caniuse-lite "^1.0.30001406"
|
||||
graceful-fs "^4.2.11"
|
||||
postcss "8.4.31"
|
||||
styled-jsx "5.1.1"
|
||||
watchpack "2.4.0"
|
||||
optionalDependencies:
|
||||
"@next/swc-darwin-arm64" "14.0.4"
|
||||
"@next/swc-darwin-x64" "14.0.4"
|
||||
"@next/swc-linux-arm64-gnu" "14.0.4"
|
||||
"@next/swc-linux-arm64-musl" "14.0.4"
|
||||
"@next/swc-linux-x64-gnu" "14.0.4"
|
||||
"@next/swc-linux-x64-musl" "14.0.4"
|
||||
"@next/swc-win32-arm64-msvc" "14.0.4"
|
||||
"@next/swc-win32-ia32-msvc" "14.0.4"
|
||||
"@next/swc-win32-x64-msvc" "14.0.4"
|
||||
|
||||
path2d-polyfill@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz#24c554a738f42700d6961992bf5f1049672f2391"
|
||||
integrity sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==
|
||||
|
||||
picocolors@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||
|
||||
postcss@8.4.31:
|
||||
version "8.4.31"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
|
||||
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||
dependencies:
|
||||
nanoid "^3.3.6"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
react-dom@^18:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.0"
|
||||
|
||||
react@^18:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
scheduler@^0.23.0:
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
|
||||
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
source-map-js@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||
|
||||
streamsearch@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
|
||||
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
|
||||
|
||||
styled-jsx@5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f"
|
||||
integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==
|
||||
dependencies:
|
||||
client-only "0.0.1"
|
||||
|
||||
tslib@^2.4.0:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||
|
||||
typescript@^5:
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
|
||||
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||
|
||||
watchpack@2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
|
||||
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
|
||||
dependencies:
|
||||
glob-to-regexp "^0.4.1"
|
||||
graceful-fs "^4.1.2"
|
@@ -12,18 +12,21 @@
|
||||
<script>
|
||||
window.name = "codesandbox";
|
||||
</script>
|
||||
<link rel="stylesheet" href="/dist/browser/dev/index.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript> You need to enable JavaScript to run this app. </noscript>
|
||||
<div id="root"></div>
|
||||
<script src="https://unpkg.com/react@18.2.0/umd/react.development.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js"></script>
|
||||
|
||||
<!-- This is so that we use the bundled excalidraw.development.js file instead
|
||||
of the actual source code -->
|
||||
<script src="./excalidraw.development.js"></script>
|
||||
<script type="module">
|
||||
import * as ExcalidrawLib from "@excalidraw/excalidraw";
|
||||
|
||||
<script src="./bundle.js"></script>
|
||||
console.log(ExcalidrawLib);
|
||||
window.ExcalidrawLib = ExcalidrawLib;
|
||||
</script>
|
||||
<script type="module" src="index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
28
examples/excalidraw/with-script-in-browser/index.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import App from "../components/App";
|
||||
import React, { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ExcalidrawLib: typeof TExcalidraw;
|
||||
}
|
||||
}
|
||||
|
||||
const rootElement = document.getElementById("root")!;
|
||||
const root = createRoot(rootElement);
|
||||
const { Excalidraw } = window.ExcalidrawLib;
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<App
|
||||
appTitle={"Excalidraw Example"}
|
||||
useCustom={(api: any, args?: any[]) => {}}
|
||||
excalidrawLib={window.ExcalidrawLib}
|
||||
>
|
||||
<Excalidraw />
|
||||
</App>
|
||||
</StrictMode>,
|
||||
);
|
19
examples/excalidraw/with-script-in-browser/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "with-script-in-browser",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"@excalidraw/excalidraw": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "5.0.12",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "yarn workspace @excalidraw/excalidraw run build:esm && vite",
|
||||
"build": "yarn workspace @excalidraw/excalidraw run build:esm && vite build",
|
||||
"build:preview": "yarn build && vite preview --port 5002"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 197 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 39 KiB |
4
examples/excalidraw/with-script-in-browser/vercel.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"outputDirectory": "dist",
|
||||
"installCommand": "yarn install"
|
||||
}
|
11
examples/excalidraw/with-script-in-browser/vite.config.mts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 3001,
|
||||
// open the browser
|
||||
open: true,
|
||||
},
|
||||
publicDir: "public",
|
||||
});
|
313
examples/excalidraw/yarn.lock
Normal file
@@ -0,0 +1,313 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@esbuild/aix-ppc64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz#2acd20be6d4f0458bc8c784103495ff24f13b1d3"
|
||||
integrity sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==
|
||||
|
||||
"@esbuild/android-arm64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz#b45d000017385c9051a4f03e17078abb935be220"
|
||||
integrity sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==
|
||||
|
||||
"@esbuild/android-arm@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.11.tgz#f46f55414e1c3614ac682b29977792131238164c"
|
||||
integrity sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==
|
||||
|
||||
"@esbuild/android-x64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.11.tgz#bfc01e91740b82011ef503c48f548950824922b2"
|
||||
integrity sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==
|
||||
|
||||
"@esbuild/darwin-arm64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz#533fb7f5a08c37121d82c66198263dcc1bed29bf"
|
||||
integrity sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==
|
||||
|
||||
"@esbuild/darwin-x64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz#62f3819eff7e4ddc656b7c6815a31cf9a1e7d98e"
|
||||
integrity sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz#d478b4195aa3ca44160272dab85ef8baf4175b4a"
|
||||
integrity sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==
|
||||
|
||||
"@esbuild/freebsd-x64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz#7bdcc1917409178257ca6a1a27fe06e797ec18a2"
|
||||
integrity sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==
|
||||
|
||||
"@esbuild/linux-arm64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz#58ad4ff11685fcc735d7ff4ca759ab18fcfe4545"
|
||||
integrity sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==
|
||||
|
||||
"@esbuild/linux-arm@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz#ce82246d873b5534d34de1e5c1b33026f35e60e3"
|
||||
integrity sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==
|
||||
|
||||
"@esbuild/linux-ia32@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz#cbae1f313209affc74b80f4390c4c35c6ab83fa4"
|
||||
integrity sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==
|
||||
|
||||
"@esbuild/linux-loong64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz#5f32aead1c3ec8f4cccdb7ed08b166224d4e9121"
|
||||
integrity sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==
|
||||
|
||||
"@esbuild/linux-mips64el@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz#38eecf1cbb8c36a616261de858b3c10d03419af9"
|
||||
integrity sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==
|
||||
|
||||
"@esbuild/linux-ppc64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz#9c5725a94e6ec15b93195e5a6afb821628afd912"
|
||||
integrity sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==
|
||||
|
||||
"@esbuild/linux-riscv64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz#2dc4486d474a2a62bbe5870522a9a600e2acb916"
|
||||
integrity sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==
|
||||
|
||||
"@esbuild/linux-s390x@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz#4ad8567df48f7dd4c71ec5b1753b6f37561a65a8"
|
||||
integrity sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==
|
||||
|
||||
"@esbuild/linux-x64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz#b7390c4d5184f203ebe7ddaedf073df82a658766"
|
||||
integrity sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==
|
||||
|
||||
"@esbuild/netbsd-x64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz#d633c09492a1721377f3bccedb2d821b911e813d"
|
||||
integrity sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==
|
||||
|
||||
"@esbuild/openbsd-x64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz#17388c76e2f01125bf831a68c03a7ffccb65d1a2"
|
||||
integrity sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==
|
||||
|
||||
"@esbuild/sunos-x64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz#e320636f00bb9f4fdf3a80e548cb743370d41767"
|
||||
integrity sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==
|
||||
|
||||
"@esbuild/win32-arm64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz#c778b45a496e90b6fc373e2a2bb072f1441fe0ee"
|
||||
integrity sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==
|
||||
|
||||
"@esbuild/win32-ia32@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz#481a65fee2e5cce74ec44823e6b09ecedcc5194c"
|
||||
integrity sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==
|
||||
|
||||
"@esbuild/win32-x64@0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz#a5d300008960bb39677c46bf16f53ec70d8dee04"
|
||||
integrity sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz#b752b6c88a14ccfcbdf3f48c577ccc3a7f0e66b9"
|
||||
integrity sha512-idWaG8xeSRCfRq9KpRysDHJ/rEHBEXcHuJ82XY0yYFIWnLMjZv9vF/7DOq8djQ2n3Lk6+3qfSH8AqlmHlmi1MA==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.5.tgz#33757c3a448b9ef77b6f6292d8b0ec45c87e9c1a"
|
||||
integrity sha512-f14d7uhAMtsCGjAYwZGv6TwuS3IFaM4ZnGMUn3aCBgkcHAYErhV1Ad97WzBvS2o0aaDv4mVz+syiN0ElMyfBPg==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.5.tgz#5234ba62665a3f443143bc8bcea9df2cc58f55fb"
|
||||
integrity sha512-ndoXeLx455FffL68OIUrVr89Xu1WLzAG4n65R8roDlCoYiQcGGg6MALvs2Ap9zs7AHg8mpHtMpwC8jBBjZrT/w==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.5.tgz#981256c054d3247b83313724938d606798a919d1"
|
||||
integrity sha512-UmElV1OY2m/1KEEqTlIjieKfVwRg0Zwg4PLgNf0s3glAHXBN99KLpw5A5lrSYCa1Kp63czTpVll2MAqbZYIHoA==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.5.tgz#120678a5a2b3a283a548dbb4d337f9187a793560"
|
||||
integrity sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.5.tgz#c99d857e2372ece544b6f60b85058ad259f64114"
|
||||
integrity sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.5.tgz#3064060f568a5718c2a06858cd6e6d24f2ff8632"
|
||||
integrity sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.5.tgz#987d30b5d2b992fff07d055015991a57ff55fbad"
|
||||
integrity sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz#85946ee4d068bd12197aeeec2c6f679c94978a49"
|
||||
integrity sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.5.tgz#fe0b20f9749a60eb1df43d20effa96c756ddcbd4"
|
||||
integrity sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.5.tgz#422661ef0e16699a234465d15b2c1089ef963b2a"
|
||||
integrity sha512-aHSsMnUw+0UETB0Hlv7B/ZHOGY5bQdwMKJSzGfDfvyhnpmVxLMGnQPGNE9wgqkLUs3+gbG1Qx02S2LLfJ5GaRQ==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.5.tgz#7b73a145891c202fbcc08759248983667a035d85"
|
||||
integrity sha512-AiqiLkb9KSf7Lj/o1U3SEP9Zn+5NuVKgFdRIZkvd4N0+bYrTOovVd0+LmYCPQGbocT4kvFyK+LXCDiXPBF3fyA==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.5.tgz#10491ccf4f63c814d4149e0316541476ea603602"
|
||||
integrity sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ==
|
||||
|
||||
"@types/estree@1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
|
||||
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
|
||||
|
||||
esbuild@^0.19.3:
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.11.tgz#4a02dca031e768b5556606e1b468fe72e3325d96"
|
||||
integrity sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==
|
||||
optionalDependencies:
|
||||
"@esbuild/aix-ppc64" "0.19.11"
|
||||
"@esbuild/android-arm" "0.19.11"
|
||||
"@esbuild/android-arm64" "0.19.11"
|
||||
"@esbuild/android-x64" "0.19.11"
|
||||
"@esbuild/darwin-arm64" "0.19.11"
|
||||
"@esbuild/darwin-x64" "0.19.11"
|
||||
"@esbuild/freebsd-arm64" "0.19.11"
|
||||
"@esbuild/freebsd-x64" "0.19.11"
|
||||
"@esbuild/linux-arm" "0.19.11"
|
||||
"@esbuild/linux-arm64" "0.19.11"
|
||||
"@esbuild/linux-ia32" "0.19.11"
|
||||
"@esbuild/linux-loong64" "0.19.11"
|
||||
"@esbuild/linux-mips64el" "0.19.11"
|
||||
"@esbuild/linux-ppc64" "0.19.11"
|
||||
"@esbuild/linux-riscv64" "0.19.11"
|
||||
"@esbuild/linux-s390x" "0.19.11"
|
||||
"@esbuild/linux-x64" "0.19.11"
|
||||
"@esbuild/netbsd-x64" "0.19.11"
|
||||
"@esbuild/openbsd-x64" "0.19.11"
|
||||
"@esbuild/sunos-x64" "0.19.11"
|
||||
"@esbuild/win32-arm64" "0.19.11"
|
||||
"@esbuild/win32-ia32" "0.19.11"
|
||||
"@esbuild/win32-x64" "0.19.11"
|
||||
|
||||
fsevents@~2.3.2, fsevents@~2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
|
||||
loose-envify@^1.1.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
dependencies:
|
||||
js-tokens "^3.0.0 || ^4.0.0"
|
||||
|
||||
nanoid@^3.3.7:
|
||||
version "3.3.7"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
||||
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||
|
||||
picocolors@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||
|
||||
postcss@^8.4.32:
|
||||
version "8.4.33"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742"
|
||||
integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==
|
||||
dependencies:
|
||||
nanoid "^3.3.7"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
react-dom@18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.0"
|
||||
|
||||
react@18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
rollup@^4.2.0:
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.9.5.tgz#62999462c90f4c8b5d7c38fc7161e63b29101b05"
|
||||
integrity sha512-E4vQW0H/mbNMw2yLSqJyjtkHY9dslf/p0zuT1xehNRqUTBOFMqEjguDvqhXr7N7r/4ttb2jr4T41d3dncmIgbQ==
|
||||
dependencies:
|
||||
"@types/estree" "1.0.5"
|
||||
optionalDependencies:
|
||||
"@rollup/rollup-android-arm-eabi" "4.9.5"
|
||||
"@rollup/rollup-android-arm64" "4.9.5"
|
||||
"@rollup/rollup-darwin-arm64" "4.9.5"
|
||||
"@rollup/rollup-darwin-x64" "4.9.5"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.9.5"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.9.5"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.9.5"
|
||||
"@rollup/rollup-linux-riscv64-gnu" "4.9.5"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.9.5"
|
||||
"@rollup/rollup-linux-x64-musl" "4.9.5"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.9.5"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.9.5"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.9.5"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
scheduler@^0.23.0:
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
|
||||
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
source-map-js@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||
|
||||
vite@5.0.6:
|
||||
version "5.0.6"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.6.tgz#f9e13503a4c5ccd67312c67803dec921f3bdea7c"
|
||||
integrity sha512-MD3joyAEBtV7QZPl2JVVUai6zHms3YOmLR+BpMzLlX2Yzjfcc4gTgNi09d/Rua3F4EtC8zdwPU8eQYyib4vVMQ==
|
||||
dependencies:
|
||||
esbuild "^0.19.3"
|
||||
postcss "^8.4.32"
|
||||
rollup "^4.2.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
1122
excalidraw-app/App.tsx
Normal file
@@ -1,14 +1,14 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { debounce, getVersion, nFormatter } from "../src/utils";
|
||||
import { debounce, getVersion, nFormatter } from "../packages/excalidraw/utils";
|
||||
import {
|
||||
getElementsStorageSize,
|
||||
getTotalStorageSize,
|
||||
} from "./data/localStorage";
|
||||
import { DEFAULT_VERSION } from "../src/constants";
|
||||
import { t } from "../src/i18n";
|
||||
import { copyTextToSystemClipboard } from "../src/clipboard";
|
||||
import { NonDeletedExcalidrawElement } from "../src/element/types";
|
||||
import { UIAppState } from "../src/types";
|
||||
import { DEFAULT_VERSION } from "../packages/excalidraw/constants";
|
||||
import { t } from "../packages/excalidraw/i18n";
|
||||
import { copyTextToSystemClipboard } from "../packages/excalidraw/clipboard";
|
||||
import { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types";
|
||||
import { UIAppState } from "../packages/excalidraw/types";
|
||||
|
||||
type StorageSizes = { scene: number; total: number };
|
||||
|
||||
|
@@ -15,11 +15,17 @@ export const FILE_CACHE_MAX_AGE_SEC = 31536000;
|
||||
export const WS_EVENTS = {
|
||||
SERVER_VOLATILE: "server-volatile-broadcast",
|
||||
SERVER: "server-broadcast",
|
||||
};
|
||||
USER_FOLLOW_CHANGE: "user-follow",
|
||||
USER_FOLLOW_ROOM_CHANGE: "user-follow-room-change",
|
||||
} as const;
|
||||
|
||||
export enum WS_SCENE_EVENT_TYPES {
|
||||
export enum WS_SUBTYPES {
|
||||
INVALID_RESPONSE = "INVALID_RESPONSE",
|
||||
INIT = "SCENE_INIT",
|
||||
UPDATE = "SCENE_UPDATE",
|
||||
MOUSE_LOCATION = "MOUSE_LOCATION",
|
||||
IDLE_STATUS = "IDLE_STATUS",
|
||||
USER_VISIBLE_SCENE_BOUNDS = "USER_VISIBLE_SCENE_BOUNDS",
|
||||
}
|
||||
|
||||
export const FIREBASE_STORAGE_PREFIXES = {
|
||||
@@ -33,10 +39,14 @@ export const STORAGE_KEYS = {
|
||||
LOCAL_STORAGE_ELEMENTS: "excalidraw",
|
||||
LOCAL_STORAGE_APP_STATE: "excalidraw-state",
|
||||
LOCAL_STORAGE_COLLAB: "excalidraw-collab",
|
||||
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
||||
LOCAL_STORAGE_THEME: "excalidraw-theme",
|
||||
VERSION_DATA_STATE: "version-dataState",
|
||||
VERSION_FILES: "version-files",
|
||||
|
||||
IDB_LIBRARY: "excalidraw-library",
|
||||
|
||||
// do not use apart from migrations
|
||||
__LEGACY_LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
||||
} as const;
|
||||
|
||||
export const COOKIES = {
|
||||
|
@@ -1,36 +1,42 @@
|
||||
import throttle from "lodash.throttle";
|
||||
import { PureComponent } from "react";
|
||||
import { ExcalidrawImperativeAPI } from "../../src/types";
|
||||
import { ErrorDialog } from "../../src/components/ErrorDialog";
|
||||
import { APP_NAME, ENV, EVENT } from "../../src/constants";
|
||||
import { ImportedDataState } from "../../src/data/types";
|
||||
import {
|
||||
ExcalidrawImperativeAPI,
|
||||
SocketId,
|
||||
} from "../../packages/excalidraw/types";
|
||||
import { ErrorDialog } from "../../packages/excalidraw/components/ErrorDialog";
|
||||
import { APP_NAME, ENV, EVENT } from "../../packages/excalidraw/constants";
|
||||
import { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
InitializedExcalidrawImageElement,
|
||||
} from "../../src/element/types";
|
||||
OrderedExcalidrawElement,
|
||||
} from "../../packages/excalidraw/element/types";
|
||||
import {
|
||||
getSceneVersion,
|
||||
restoreElements,
|
||||
} from "../../src/packages/excalidraw/index";
|
||||
import { Collaborator, Gesture } from "../../src/types";
|
||||
zoomToFitBounds,
|
||||
} from "../../packages/excalidraw/index";
|
||||
import { Collaborator, Gesture } from "../../packages/excalidraw/types";
|
||||
import {
|
||||
assertNever,
|
||||
preventUnload,
|
||||
resolvablePromise,
|
||||
withBatchedUpdates,
|
||||
} from "../../src/utils";
|
||||
throttleRAF,
|
||||
} from "../../packages/excalidraw/utils";
|
||||
import {
|
||||
CURSOR_SYNC_TIMEOUT,
|
||||
FILE_UPLOAD_MAX_BYTES,
|
||||
FIREBASE_STORAGE_PREFIXES,
|
||||
INITIAL_SCENE_UPDATE_TIMEOUT,
|
||||
LOAD_IMAGES_TIMEOUT,
|
||||
WS_SCENE_EVENT_TYPES,
|
||||
WS_SUBTYPES,
|
||||
SYNC_FULL_SCENE_INTERVAL_MS,
|
||||
WS_EVENTS,
|
||||
} from "../app_constants";
|
||||
import {
|
||||
generateCollaborationLinkData,
|
||||
getCollaborationLink,
|
||||
getCollabServer,
|
||||
getSyncableElements,
|
||||
SocketUpdateDataSource,
|
||||
SyncableExcalidrawElement,
|
||||
@@ -47,42 +53,52 @@ import {
|
||||
saveUsernameToLocalStorage,
|
||||
} from "../data/localStorage";
|
||||
import Portal from "./Portal";
|
||||
import RoomDialog from "./RoomDialog";
|
||||
import { t } from "../../src/i18n";
|
||||
import { UserIdleState } from "../../src/types";
|
||||
import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../src/constants";
|
||||
import { t } from "../../packages/excalidraw/i18n";
|
||||
import { UserIdleState } from "../../packages/excalidraw/types";
|
||||
import {
|
||||
IDLE_THRESHOLD,
|
||||
ACTIVE_THRESHOLD,
|
||||
} from "../../packages/excalidraw/constants";
|
||||
import {
|
||||
encodeFilesForUpload,
|
||||
FileManager,
|
||||
updateStaleImageStatuses,
|
||||
} from "../data/FileManager";
|
||||
import { AbortError } from "../../src/errors";
|
||||
import { AbortError } from "../../packages/excalidraw/errors";
|
||||
import {
|
||||
isImageElement,
|
||||
isInitializedImageElement,
|
||||
} from "../../src/element/typeChecks";
|
||||
import { newElementWith } from "../../src/element/mutateElement";
|
||||
import {
|
||||
ReconciledElements,
|
||||
reconcileElements as _reconcileElements,
|
||||
} from "./reconciliation";
|
||||
import { decryptData } from "../../src/data/encryption";
|
||||
} from "../../packages/excalidraw/element/typeChecks";
|
||||
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
||||
import { decryptData } from "../../packages/excalidraw/data/encryption";
|
||||
import { resetBrowserStateVersions } from "../data/tabSync";
|
||||
import { LocalData } from "../data/LocalData";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { atom } from "jotai";
|
||||
import { appJotaiStore } from "../app-jotai";
|
||||
import { Mutable, ValueOf } from "../../packages/excalidraw/utility-types";
|
||||
import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds";
|
||||
import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils";
|
||||
import { collabErrorIndicatorAtom } from "./CollabError";
|
||||
import {
|
||||
ReconciledExcalidrawElement,
|
||||
RemoteExcalidrawElement,
|
||||
reconcileElements,
|
||||
} from "../../packages/excalidraw/data/reconcile";
|
||||
|
||||
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
||||
export const collabDialogShownAtom = atom(false);
|
||||
export const isCollaboratingAtom = atom(false);
|
||||
export const isOfflineAtom = atom(false);
|
||||
|
||||
interface CollabState {
|
||||
errorMessage: string;
|
||||
errorMessage: string | null;
|
||||
/** errors related to saving */
|
||||
dialogNotifiedErrors: Record<string, boolean>;
|
||||
username: string;
|
||||
activeRoomLink: string;
|
||||
activeRoomLink: string | null;
|
||||
}
|
||||
|
||||
export const activeRoomLinkAtom = atom<string | null>(null);
|
||||
|
||||
type CollabInstance = InstanceType<typeof Collab>;
|
||||
|
||||
export interface CollabAPI {
|
||||
@@ -93,32 +109,34 @@ export interface CollabAPI {
|
||||
stopCollaboration: CollabInstance["stopCollaboration"];
|
||||
syncElements: CollabInstance["syncElements"];
|
||||
fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
|
||||
setUsername: (username: string) => void;
|
||||
setUsername: CollabInstance["setUsername"];
|
||||
getUsername: CollabInstance["getUsername"];
|
||||
getActiveRoomLink: CollabInstance["getActiveRoomLink"];
|
||||
setCollabError: CollabInstance["setErrorDialog"];
|
||||
}
|
||||
|
||||
interface PublicProps {
|
||||
interface CollabProps {
|
||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||
}
|
||||
|
||||
type Props = PublicProps & { modalIsShown: boolean };
|
||||
|
||||
class Collab extends PureComponent<Props, CollabState> {
|
||||
class Collab extends PureComponent<CollabProps, CollabState> {
|
||||
portal: Portal;
|
||||
fileManager: FileManager;
|
||||
excalidrawAPI: Props["excalidrawAPI"];
|
||||
excalidrawAPI: CollabProps["excalidrawAPI"];
|
||||
activeIntervalId: number | null;
|
||||
idleTimeoutId: number | null;
|
||||
|
||||
private socketInitializationTimer?: number;
|
||||
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
||||
private collaborators = new Map<string, Collaborator>();
|
||||
private collaborators = new Map<SocketId, Collaborator>();
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: CollabProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
errorMessage: "",
|
||||
errorMessage: null,
|
||||
dialogNotifiedErrors: {},
|
||||
username: importUsernameFromLocalStorage() || "",
|
||||
activeRoomLink: "",
|
||||
activeRoomLink: null,
|
||||
};
|
||||
this.portal = new Portal(this);
|
||||
this.fileManager = new FileManager({
|
||||
@@ -151,12 +169,28 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
this.idleTimeoutId = null;
|
||||
}
|
||||
|
||||
private onUmmount: (() => void) | null = null;
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
||||
window.addEventListener("online", this.onOfflineStatusToggle);
|
||||
window.addEventListener("offline", this.onOfflineStatusToggle);
|
||||
window.addEventListener(EVENT.UNLOAD, this.onUnload);
|
||||
|
||||
const unsubOnUserFollow = this.excalidrawAPI.onUserFollow((payload) => {
|
||||
this.portal.socket && this.portal.broadcastUserFollowed(payload);
|
||||
});
|
||||
const throttledRelayUserViewportBounds = throttleRAF(
|
||||
this.relayVisibleSceneBounds,
|
||||
);
|
||||
const unsubOnScrollChange = this.excalidrawAPI.onScrollChange(() =>
|
||||
throttledRelayUserViewportBounds(),
|
||||
);
|
||||
this.onUmmount = () => {
|
||||
unsubOnUserFollow();
|
||||
unsubOnScrollChange();
|
||||
};
|
||||
|
||||
this.onOfflineStatusToggle();
|
||||
|
||||
const collabAPI: CollabAPI = {
|
||||
@@ -167,6 +201,9 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase,
|
||||
stopCollaboration: this.stopCollaboration,
|
||||
setUsername: this.setUsername,
|
||||
getUsername: this.getUsername,
|
||||
getActiveRoomLink: this.getActiveRoomLink,
|
||||
setCollabError: this.setErrorDialog,
|
||||
};
|
||||
|
||||
appJotaiStore.set(collabAPIAtom, collabAPI);
|
||||
@@ -204,6 +241,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
window.clearTimeout(this.idleTimeoutId);
|
||||
this.idleTimeoutId = null;
|
||||
}
|
||||
this.onUmmount?.();
|
||||
}
|
||||
|
||||
isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!;
|
||||
@@ -238,24 +276,39 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
syncableElements: readonly SyncableExcalidrawElement[],
|
||||
) => {
|
||||
try {
|
||||
const savedData = await saveToFirebase(
|
||||
const storedElements = await saveToFirebase(
|
||||
this.portal,
|
||||
syncableElements,
|
||||
this.excalidrawAPI.getAppState(),
|
||||
);
|
||||
|
||||
if (this.isCollaborating() && savedData && savedData.reconciledElements) {
|
||||
this.handleRemoteSceneUpdate(
|
||||
this.reconcileElements(savedData.reconciledElements),
|
||||
);
|
||||
this.resetErrorIndicator();
|
||||
|
||||
if (this.isCollaborating() && storedElements) {
|
||||
this.handleRemoteSceneUpdate(this._reconcileElements(storedElements));
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.setState({
|
||||
// firestore doesn't return a specific error code when size exceeded
|
||||
errorMessage: /is longer than.*?bytes/.test(error.message)
|
||||
? t("errors.collabSaveFailed_sizeExceeded")
|
||||
: t("errors.collabSaveFailed"),
|
||||
});
|
||||
const errorMessage = /is longer than.*?bytes/.test(error.message)
|
||||
? t("errors.collabSaveFailed_sizeExceeded")
|
||||
: t("errors.collabSaveFailed");
|
||||
|
||||
if (
|
||||
!this.state.dialogNotifiedErrors[errorMessage] ||
|
||||
!this.isCollaborating()
|
||||
) {
|
||||
this.setErrorDialog(errorMessage);
|
||||
this.setState({
|
||||
dialogNotifiedErrors: {
|
||||
...this.state.dialogNotifiedErrors,
|
||||
[errorMessage]: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (this.isCollaborating()) {
|
||||
this.setErrorIndicator(errorMessage);
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
@@ -264,6 +317,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
this.queueBroadcastAllElements.cancel();
|
||||
this.queueSaveToFirebase.cancel();
|
||||
this.loadImageFiles.cancel();
|
||||
this.resetErrorIndicator(true);
|
||||
|
||||
this.saveCollabRoomToFirebase(
|
||||
getSyncableElements(
|
||||
@@ -313,9 +367,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
this.fileManager.reset();
|
||||
if (!opts?.isUnload) {
|
||||
this.setIsCollaborating(false);
|
||||
this.setState({
|
||||
activeRoomLink: "",
|
||||
});
|
||||
this.setActiveRoomLink(null);
|
||||
this.collaborators = new Map();
|
||||
this.excalidrawAPI.updateScene({
|
||||
collaborators: this.collaborators,
|
||||
@@ -356,7 +408,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
iv: Uint8Array,
|
||||
encryptedData: ArrayBuffer,
|
||||
decryptionKey: string,
|
||||
) => {
|
||||
): Promise<ValueOf<SocketUpdateDataSource>> => {
|
||||
try {
|
||||
const decrypted = await decryptData(iv, encryptedData, decryptionKey);
|
||||
|
||||
@@ -368,7 +420,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
window.alert(t("alerts.decryptFailed"));
|
||||
console.error(error);
|
||||
return {
|
||||
type: "INVALID_RESPONSE",
|
||||
type: WS_SUBTYPES.INVALID_RESPONSE,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -377,11 +429,11 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
|
||||
startCollaboration = async (
|
||||
existingRoomLinkData: null | { roomId: string; roomKey: string },
|
||||
): Promise<ImportedDataState | null> => {
|
||||
) => {
|
||||
if (!this.state.username) {
|
||||
import("@excalidraw/random-username").then(({ getRandomUsername }) => {
|
||||
const username = getRandomUsername();
|
||||
this.onUsernameChange(username);
|
||||
this.setUsername(username);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -403,7 +455,11 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
);
|
||||
}
|
||||
|
||||
const scenePromise = resolvablePromise<ImportedDataState | null>();
|
||||
// TODO: `ImportedDataState` type here seems abused
|
||||
const scenePromise = resolvablePromise<
|
||||
| (ImportedDataState & { elements: readonly OrderedExcalidrawElement[] })
|
||||
| null
|
||||
>();
|
||||
|
||||
this.setIsCollaborating(true);
|
||||
LocalData.pauseSave("collaboration");
|
||||
@@ -423,13 +479,9 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
this.fallbackInitializationHandler = fallbackInitializationHandler;
|
||||
|
||||
try {
|
||||
const socketServerData = await getCollabServer();
|
||||
|
||||
this.portal.socket = this.portal.open(
|
||||
socketIOClient(socketServerData.url, {
|
||||
transports: socketServerData.polling
|
||||
? ["websocket", "polling"]
|
||||
: ["websocket"],
|
||||
socketIOClient(import.meta.env.VITE_APP_WS_SERVER_URL, {
|
||||
transports: ["websocket", "polling"],
|
||||
}),
|
||||
roomId,
|
||||
roomKey,
|
||||
@@ -438,7 +490,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
this.portal.socket.once("connect_error", fallbackInitializationHandler);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
this.setState({ errorMessage: error.message });
|
||||
this.setErrorDialog(error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -484,13 +536,14 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
);
|
||||
|
||||
switch (decryptedData.type) {
|
||||
case "INVALID_RESPONSE":
|
||||
case WS_SUBTYPES.INVALID_RESPONSE:
|
||||
return;
|
||||
case WS_SCENE_EVENT_TYPES.INIT: {
|
||||
case WS_SUBTYPES.INIT: {
|
||||
if (!this.portal.socketInitialized) {
|
||||
this.initializeRoom({ fetchScene: false });
|
||||
const remoteElements = decryptedData.payload.elements;
|
||||
const reconciledElements = this.reconcileElements(remoteElements);
|
||||
const reconciledElements =
|
||||
this._reconcileElements(remoteElements);
|
||||
this.handleRemoteSceneUpdate(reconciledElements, {
|
||||
init: true,
|
||||
});
|
||||
@@ -502,41 +555,75 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WS_SCENE_EVENT_TYPES.UPDATE:
|
||||
case WS_SUBTYPES.UPDATE:
|
||||
this.handleRemoteSceneUpdate(
|
||||
this.reconcileElements(decryptedData.payload.elements),
|
||||
this._reconcileElements(decryptedData.payload.elements),
|
||||
);
|
||||
break;
|
||||
case "MOUSE_LOCATION": {
|
||||
case WS_SUBTYPES.MOUSE_LOCATION: {
|
||||
const { pointer, button, username, selectedElementIds } =
|
||||
decryptedData.payload;
|
||||
|
||||
const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] =
|
||||
decryptedData.payload.socketId ||
|
||||
// @ts-ignore legacy, see #2094 (#2097)
|
||||
decryptedData.payload.socketID;
|
||||
|
||||
const collaborators = new Map(this.collaborators);
|
||||
const user = collaborators.get(socketId) || {}!;
|
||||
user.pointer = pointer;
|
||||
user.button = button;
|
||||
user.selectedElementIds = selectedElementIds;
|
||||
user.username = username;
|
||||
collaborators.set(socketId, user);
|
||||
this.updateCollaborator(socketId, {
|
||||
pointer,
|
||||
button,
|
||||
selectedElementIds,
|
||||
username,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS: {
|
||||
const { sceneBounds, socketId } = decryptedData.payload;
|
||||
|
||||
const appState = this.excalidrawAPI.getAppState();
|
||||
|
||||
// we're not following the user
|
||||
// (shouldn't happen, but could be late message or bug upstream)
|
||||
if (appState.userToFollow?.socketId !== socketId) {
|
||||
console.warn(
|
||||
`receiving remote client's (from ${socketId}) viewport bounds even though we're not subscribed to it!`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// cross-follow case, ignore updates in this case
|
||||
if (
|
||||
appState.userToFollow &&
|
||||
appState.followedBy.has(appState.userToFollow.socketId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.excalidrawAPI.updateScene({
|
||||
collaborators,
|
||||
appState: zoomToFitBounds({
|
||||
appState,
|
||||
bounds: sceneBounds,
|
||||
fitToViewport: true,
|
||||
viewportZoomFactor: 1,
|
||||
}).appState,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case WS_SUBTYPES.IDLE_STATUS: {
|
||||
const { userState, socketId, username } = decryptedData.payload;
|
||||
this.updateCollaborator(socketId, {
|
||||
userState,
|
||||
username,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "IDLE_STATUS": {
|
||||
const { userState, socketId, username } = decryptedData.payload;
|
||||
const collaborators = new Map(this.collaborators);
|
||||
const user = collaborators.get(socketId) || {}!;
|
||||
user.userState = userState;
|
||||
user.username = username;
|
||||
this.excalidrawAPI.updateScene({
|
||||
collaborators,
|
||||
});
|
||||
break;
|
||||
|
||||
default: {
|
||||
assertNever(decryptedData, null);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -553,11 +640,20 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
scenePromise.resolve(sceneData);
|
||||
});
|
||||
|
||||
this.portal.socket.on(
|
||||
WS_EVENTS.USER_FOLLOW_ROOM_CHANGE,
|
||||
(followedBy: SocketId[]) => {
|
||||
this.excalidrawAPI.updateScene({
|
||||
appState: { followedBy: new Set(followedBy) },
|
||||
});
|
||||
|
||||
this.relayVisibleSceneBounds({ force: true });
|
||||
},
|
||||
);
|
||||
|
||||
this.initializeIdleDetector();
|
||||
|
||||
this.setState({
|
||||
activeRoomLink: window.location.href,
|
||||
});
|
||||
this.setActiveRoomLink(window.location.href);
|
||||
|
||||
return scenePromise;
|
||||
};
|
||||
@@ -609,17 +705,15 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
return null;
|
||||
};
|
||||
|
||||
private reconcileElements = (
|
||||
private _reconcileElements = (
|
||||
remoteElements: readonly ExcalidrawElement[],
|
||||
): ReconciledElements => {
|
||||
): ReconciledExcalidrawElement[] => {
|
||||
const localElements = this.getSceneElementsIncludingDeleted();
|
||||
const appState = this.excalidrawAPI.getAppState();
|
||||
|
||||
remoteElements = restoreElements(remoteElements, null);
|
||||
|
||||
const reconciledElements = _reconcileElements(
|
||||
const restoredRemoteElements = restoreElements(remoteElements, null);
|
||||
const reconciledElements = reconcileElements(
|
||||
localElements,
|
||||
remoteElements,
|
||||
restoredRemoteElements as RemoteExcalidrawElement[],
|
||||
appState,
|
||||
);
|
||||
|
||||
@@ -650,7 +744,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
}, LOAD_IMAGES_TIMEOUT);
|
||||
|
||||
private handleRemoteSceneUpdate = (
|
||||
elements: ReconciledElements,
|
||||
elements: ReconciledExcalidrawElement[],
|
||||
{ init = false }: { init?: boolean } = {},
|
||||
) => {
|
||||
this.excalidrawAPI.updateScene({
|
||||
@@ -721,20 +815,39 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
document.addEventListener(EVENT.VISIBILITY_CHANGE, this.onVisibilityChange);
|
||||
};
|
||||
|
||||
setCollaborators(sockets: string[]) {
|
||||
setCollaborators(sockets: SocketId[]) {
|
||||
const collaborators: InstanceType<typeof Collab>["collaborators"] =
|
||||
new Map();
|
||||
for (const socketId of sockets) {
|
||||
if (this.collaborators.has(socketId)) {
|
||||
collaborators.set(socketId, this.collaborators.get(socketId)!);
|
||||
} else {
|
||||
collaborators.set(socketId, {});
|
||||
}
|
||||
collaborators.set(
|
||||
socketId,
|
||||
Object.assign({}, this.collaborators.get(socketId), {
|
||||
isCurrentUser: socketId === this.portal.socket?.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
this.collaborators = collaborators;
|
||||
this.excalidrawAPI.updateScene({ collaborators });
|
||||
}
|
||||
|
||||
updateCollaborator = (socketId: SocketId, updates: Partial<Collaborator>) => {
|
||||
const collaborators = new Map(this.collaborators);
|
||||
const user: Mutable<Collaborator> = Object.assign(
|
||||
{},
|
||||
collaborators.get(socketId),
|
||||
updates,
|
||||
{
|
||||
isCurrentUser: socketId === this.portal.socket?.id,
|
||||
},
|
||||
);
|
||||
collaborators.set(socketId, user);
|
||||
this.collaborators = collaborators;
|
||||
|
||||
this.excalidrawAPI.updateScene({
|
||||
collaborators,
|
||||
});
|
||||
};
|
||||
|
||||
public setLastBroadcastedOrReceivedSceneVersion = (version: number) => {
|
||||
this.lastBroadcastedOrReceivedSceneVersion = version;
|
||||
};
|
||||
@@ -760,29 +873,42 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
CURSOR_SYNC_TIMEOUT,
|
||||
);
|
||||
|
||||
relayVisibleSceneBounds = (props?: { force: boolean }) => {
|
||||
const appState = this.excalidrawAPI.getAppState();
|
||||
|
||||
if (this.portal.socket && (appState.followedBy.size > 0 || props?.force)) {
|
||||
this.portal.broadcastVisibleSceneBounds(
|
||||
{
|
||||
sceneBounds: getVisibleSceneBounds(appState),
|
||||
},
|
||||
`follow@${this.portal.socket.id}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onIdleStateChange = (userState: UserIdleState) => {
|
||||
this.portal.broadcastIdleChange(userState);
|
||||
};
|
||||
|
||||
broadcastElements = (elements: readonly ExcalidrawElement[]) => {
|
||||
broadcastElements = (elements: readonly OrderedExcalidrawElement[]) => {
|
||||
if (
|
||||
getSceneVersion(elements) >
|
||||
this.getLastBroadcastedOrReceivedSceneVersion()
|
||||
) {
|
||||
this.portal.broadcastScene(WS_SCENE_EVENT_TYPES.UPDATE, elements, false);
|
||||
this.portal.broadcastScene(WS_SUBTYPES.UPDATE, elements, false);
|
||||
this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(elements);
|
||||
this.queueBroadcastAllElements();
|
||||
}
|
||||
};
|
||||
|
||||
syncElements = (elements: readonly ExcalidrawElement[]) => {
|
||||
syncElements = (elements: readonly OrderedExcalidrawElement[]) => {
|
||||
this.broadcastElements(elements);
|
||||
this.queueSaveToFirebase();
|
||||
};
|
||||
|
||||
queueBroadcastAllElements = throttle(() => {
|
||||
this.portal.broadcastScene(
|
||||
WS_SCENE_EVENT_TYPES.UPDATE,
|
||||
WS_SUBTYPES.UPDATE,
|
||||
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||
true,
|
||||
);
|
||||
@@ -808,41 +934,49 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
{ leading: false },
|
||||
);
|
||||
|
||||
handleClose = () => {
|
||||
appJotaiStore.set(collabDialogShownAtom, false);
|
||||
};
|
||||
|
||||
setUsername = (username: string) => {
|
||||
this.setState({ username });
|
||||
};
|
||||
|
||||
onUsernameChange = (username: string) => {
|
||||
this.setUsername(username);
|
||||
saveUsernameToLocalStorage(username);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { username, errorMessage, activeRoomLink } = this.state;
|
||||
getUsername = () => this.state.username;
|
||||
|
||||
const { modalIsShown } = this.props;
|
||||
setActiveRoomLink = (activeRoomLink: string | null) => {
|
||||
this.setState({ activeRoomLink });
|
||||
appJotaiStore.set(activeRoomLinkAtom, activeRoomLink);
|
||||
};
|
||||
|
||||
getActiveRoomLink = () => this.state.activeRoomLink;
|
||||
|
||||
setErrorIndicator = (errorMessage: string | null) => {
|
||||
appJotaiStore.set(collabErrorIndicatorAtom, {
|
||||
message: errorMessage,
|
||||
nonce: Date.now(),
|
||||
});
|
||||
};
|
||||
|
||||
resetErrorIndicator = (resetDialogNotifiedErrors = false) => {
|
||||
appJotaiStore.set(collabErrorIndicatorAtom, { message: null, nonce: 0 });
|
||||
if (resetDialogNotifiedErrors) {
|
||||
this.setState({
|
||||
dialogNotifiedErrors: {},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
setErrorDialog = (errorMessage: string | null) => {
|
||||
this.setState({
|
||||
errorMessage,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { errorMessage } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
{modalIsShown && (
|
||||
<RoomDialog
|
||||
handleClose={this.handleClose}
|
||||
activeRoomLink={activeRoomLink}
|
||||
username={username}
|
||||
onUsernameChange={this.onUsernameChange}
|
||||
onRoomCreate={() => this.startCollaboration(null)}
|
||||
onRoomDestroy={this.stopCollaboration}
|
||||
setErrorMessage={(errorMessage) => {
|
||||
this.setState({ errorMessage });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{errorMessage && (
|
||||
<ErrorDialog onClose={() => this.setState({ errorMessage: "" })}>
|
||||
{errorMessage != null && (
|
||||
<ErrorDialog onClose={() => this.setErrorDialog(null)}>
|
||||
{errorMessage}
|
||||
</ErrorDialog>
|
||||
)}
|
||||
@@ -861,11 +995,6 @@ if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
||||
window.collab = window.collab || ({} as Window["collab"]);
|
||||
}
|
||||
|
||||
const _Collab: React.FC<PublicProps> = (props) => {
|
||||
const [collabDialogShown] = useAtom(collabDialogShownAtom);
|
||||
return <Collab {...props} modalIsShown={collabDialogShown} />;
|
||||
};
|
||||
|
||||
export default _Collab;
|
||||
export default Collab;
|
||||
|
||||
export type TCollabClass = Collab;
|
||||
|
35
excalidraw-app/collab/CollabError.scss
Normal file
@@ -0,0 +1,35 @@
|
||||
@import "../../packages/excalidraw/css/variables.module.scss";
|
||||
|
||||
.excalidraw {
|
||||
.collab-errors-button {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin-inline-end: 1rem;
|
||||
|
||||
color: var(--color-danger);
|
||||
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.collab-errors-button-shake {
|
||||
animation: strong-shake 0.15s 6;
|
||||
}
|
||||
|
||||
@keyframes strong-shake {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(0eg);
|
||||
}
|
||||
75% {
|
||||
transform: rotate(-10deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
}
|
54
excalidraw-app/collab/CollabError.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Tooltip } from "../../packages/excalidraw/components/Tooltip";
|
||||
import { warning } from "../../packages/excalidraw/components/icons";
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import "./CollabError.scss";
|
||||
import { atom } from "jotai";
|
||||
|
||||
type ErrorIndicator = {
|
||||
message: string | null;
|
||||
/** used to rerun the useEffect responsible for animation */
|
||||
nonce: number;
|
||||
};
|
||||
|
||||
export const collabErrorIndicatorAtom = atom<ErrorIndicator>({
|
||||
message: null,
|
||||
nonce: 0,
|
||||
});
|
||||
|
||||
const CollabError = ({ collabError }: { collabError: ErrorIndicator }) => {
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const clearAnimationRef = useRef<string | number | NodeJS.Timeout>();
|
||||
|
||||
useEffect(() => {
|
||||
setIsAnimating(true);
|
||||
clearAnimationRef.current = setTimeout(() => {
|
||||
setIsAnimating(false);
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(clearAnimationRef.current);
|
||||
};
|
||||
}, [collabError.message, collabError.nonce]);
|
||||
|
||||
if (!collabError.message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip label={collabError.message} long={true}>
|
||||
<div
|
||||
className={clsx("collab-errors-button", {
|
||||
"collab-errors-button-shake": isAnimating,
|
||||
})}
|
||||
>
|
||||
{warning}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
CollabError.displayName = "CollabError";
|
||||
|
||||
export default CollabError;
|
@@ -2,27 +2,27 @@ import {
|
||||
isSyncableElement,
|
||||
SocketUpdateData,
|
||||
SocketUpdateDataSource,
|
||||
SyncableExcalidrawElement,
|
||||
} from "../data";
|
||||
|
||||
import { TCollabClass } from "./Collab";
|
||||
|
||||
import { ExcalidrawElement } from "../../src/element/types";
|
||||
import { OrderedExcalidrawElement } from "../../packages/excalidraw/element/types";
|
||||
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
|
||||
import {
|
||||
WS_EVENTS,
|
||||
FILE_UPLOAD_TIMEOUT,
|
||||
WS_SCENE_EVENT_TYPES,
|
||||
} from "../app_constants";
|
||||
import { UserIdleState } from "../../src/types";
|
||||
import { trackEvent } from "../../src/analytics";
|
||||
OnUserFollowedPayload,
|
||||
SocketId,
|
||||
UserIdleState,
|
||||
} from "../../packages/excalidraw/types";
|
||||
import { trackEvent } from "../../packages/excalidraw/analytics";
|
||||
import throttle from "lodash.throttle";
|
||||
import { newElementWith } from "../../src/element/mutateElement";
|
||||
import { BroadcastedExcalidrawElement } from "./reconciliation";
|
||||
import { encryptData } from "../../src/data/encryption";
|
||||
import { PRECEDING_ELEMENT_KEY } from "../../src/constants";
|
||||
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
||||
import { encryptData } from "../../packages/excalidraw/data/encryption";
|
||||
import type { Socket } from "socket.io-client";
|
||||
|
||||
class Portal {
|
||||
collab: TCollabClass;
|
||||
socket: SocketIOClient.Socket | null = null;
|
||||
socket: Socket | null = null;
|
||||
socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized
|
||||
roomId: string | null = null;
|
||||
roomKey: string | null = null;
|
||||
@@ -32,7 +32,7 @@ class Portal {
|
||||
this.collab = collab;
|
||||
}
|
||||
|
||||
open(socket: SocketIOClient.Socket, id: string, key: string) {
|
||||
open(socket: Socket, id: string, key: string) {
|
||||
this.socket = socket;
|
||||
this.roomId = id;
|
||||
this.roomKey = key;
|
||||
@@ -46,12 +46,12 @@ class Portal {
|
||||
});
|
||||
this.socket.on("new-user", async (_socketId: string) => {
|
||||
this.broadcastScene(
|
||||
WS_SCENE_EVENT_TYPES.INIT,
|
||||
WS_SUBTYPES.INIT,
|
||||
this.collab.getSceneElementsIncludingDeleted(),
|
||||
/* syncAll */ true,
|
||||
);
|
||||
});
|
||||
this.socket.on("room-user-change", (clients: string[]) => {
|
||||
this.socket.on("room-user-change", (clients: SocketId[]) => {
|
||||
this.collab.setCollaborators(clients);
|
||||
});
|
||||
|
||||
@@ -83,6 +83,7 @@ class Portal {
|
||||
async _broadcastSocketData(
|
||||
data: SocketUpdateData,
|
||||
volatile: boolean = false,
|
||||
roomId?: string,
|
||||
) {
|
||||
if (this.isOpen()) {
|
||||
const json = JSON.stringify(data);
|
||||
@@ -91,7 +92,7 @@ class Portal {
|
||||
|
||||
this.socket?.emit(
|
||||
volatile ? WS_EVENTS.SERVER_VOLATILE : WS_EVENTS.SERVER,
|
||||
this.roomId,
|
||||
roomId ?? this.roomId,
|
||||
encryptedBuffer,
|
||||
iv,
|
||||
);
|
||||
@@ -130,36 +131,28 @@ class Portal {
|
||||
}, FILE_UPLOAD_TIMEOUT);
|
||||
|
||||
broadcastScene = async (
|
||||
updateType: WS_SCENE_EVENT_TYPES.INIT | WS_SCENE_EVENT_TYPES.UPDATE,
|
||||
allElements: readonly ExcalidrawElement[],
|
||||
updateType: WS_SUBTYPES.INIT | WS_SUBTYPES.UPDATE,
|
||||
elements: readonly OrderedExcalidrawElement[],
|
||||
syncAll: boolean,
|
||||
) => {
|
||||
if (updateType === WS_SCENE_EVENT_TYPES.INIT && !syncAll) {
|
||||
if (updateType === WS_SUBTYPES.INIT && !syncAll) {
|
||||
throw new Error("syncAll must be true when sending SCENE.INIT");
|
||||
}
|
||||
|
||||
// sync out only the elements we think we need to to save bandwidth.
|
||||
// periodically we'll resync the whole thing to make sure no one diverges
|
||||
// due to a dropped message (server goes down etc).
|
||||
const syncableElements = allElements.reduce(
|
||||
(acc, element: BroadcastedExcalidrawElement, idx, elements) => {
|
||||
if (
|
||||
(syncAll ||
|
||||
!this.broadcastedElementVersions.has(element.id) ||
|
||||
element.version >
|
||||
this.broadcastedElementVersions.get(element.id)!) &&
|
||||
isSyncableElement(element)
|
||||
) {
|
||||
acc.push({
|
||||
...element,
|
||||
// z-index info for the reconciler
|
||||
[PRECEDING_ELEMENT_KEY]: idx === 0 ? "^" : elements[idx - 1]?.id,
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as BroadcastedExcalidrawElement[],
|
||||
);
|
||||
const syncableElements = elements.reduce((acc, element) => {
|
||||
if (
|
||||
(syncAll ||
|
||||
!this.broadcastedElementVersions.has(element.id) ||
|
||||
element.version > this.broadcastedElementVersions.get(element.id)!) &&
|
||||
isSyncableElement(element)
|
||||
) {
|
||||
acc.push(element);
|
||||
}
|
||||
return acc;
|
||||
}, [] as SyncableExcalidrawElement[]);
|
||||
|
||||
const data: SocketUpdateDataSource[typeof updateType] = {
|
||||
type: updateType,
|
||||
@@ -183,9 +176,9 @@ class Portal {
|
||||
broadcastIdleChange = (userState: UserIdleState) => {
|
||||
if (this.socket?.id) {
|
||||
const data: SocketUpdateDataSource["IDLE_STATUS"] = {
|
||||
type: "IDLE_STATUS",
|
||||
type: WS_SUBTYPES.IDLE_STATUS,
|
||||
payload: {
|
||||
socketId: this.socket.id,
|
||||
socketId: this.socket.id as SocketId,
|
||||
userState,
|
||||
username: this.collab.state.username,
|
||||
},
|
||||
@@ -203,9 +196,9 @@ class Portal {
|
||||
}) => {
|
||||
if (this.socket?.id) {
|
||||
const data: SocketUpdateDataSource["MOUSE_LOCATION"] = {
|
||||
type: "MOUSE_LOCATION",
|
||||
type: WS_SUBTYPES.MOUSE_LOCATION,
|
||||
payload: {
|
||||
socketId: this.socket.id,
|
||||
socketId: this.socket.id as SocketId,
|
||||
pointer: payload.pointer,
|
||||
button: payload.button || "up",
|
||||
selectedElementIds:
|
||||
@@ -213,12 +206,43 @@ class Portal {
|
||||
username: this.collab.state.username,
|
||||
},
|
||||
};
|
||||
|
||||
return this._broadcastSocketData(
|
||||
data as SocketUpdateData,
|
||||
true, // volatile
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
broadcastVisibleSceneBounds = (
|
||||
payload: {
|
||||
sceneBounds: SocketUpdateDataSource["USER_VISIBLE_SCENE_BOUNDS"]["payload"]["sceneBounds"];
|
||||
},
|
||||
roomId: string,
|
||||
) => {
|
||||
if (this.socket?.id) {
|
||||
const data: SocketUpdateDataSource["USER_VISIBLE_SCENE_BOUNDS"] = {
|
||||
type: WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS,
|
||||
payload: {
|
||||
socketId: this.socket.id as SocketId,
|
||||
username: this.collab.state.username,
|
||||
sceneBounds: payload.sceneBounds,
|
||||
},
|
||||
};
|
||||
|
||||
return this._broadcastSocketData(
|
||||
data as SocketUpdateData,
|
||||
true, // volatile
|
||||
roomId,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
broadcastUserFollowed = (payload: OnUserFollowedPayload) => {
|
||||
if (this.socket?.id) {
|
||||
this.socket.emit(WS_EVENTS.USER_FOLLOW_CHANGE, payload);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default Portal;
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { useRef, useState } from "react";
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
|
||||
import { copyTextToSystemClipboard } from "../../src/clipboard";
|
||||
import { trackEvent } from "../../src/analytics";
|
||||
import { getFrame } from "../../src/utils";
|
||||
import { useI18n } from "../../src/i18n";
|
||||
import { KEYS } from "../../src/keys";
|
||||
import { copyTextToSystemClipboard } from "../../packages/excalidraw/clipboard";
|
||||
import { trackEvent } from "../../packages/excalidraw/analytics";
|
||||
import { getFrame } from "../../packages/excalidraw/utils";
|
||||
import { useI18n } from "../../packages/excalidraw/i18n";
|
||||
import { KEYS } from "../../packages/excalidraw/keys";
|
||||
|
||||
import { Dialog } from "../../src/components/Dialog";
|
||||
import { Dialog } from "../../packages/excalidraw/components/Dialog";
|
||||
import {
|
||||
copyIcon,
|
||||
playerPlayIcon,
|
||||
@@ -16,11 +16,11 @@ import {
|
||||
shareIOS,
|
||||
shareWindows,
|
||||
tablerCheckIcon,
|
||||
} from "../../src/components/icons";
|
||||
import { TextField } from "../../src/components/TextField";
|
||||
import { FilledButton } from "../../src/components/FilledButton";
|
||||
} from "../../packages/excalidraw/components/icons";
|
||||
import { TextField } from "../../packages/excalidraw/components/TextField";
|
||||
import { FilledButton } from "../../packages/excalidraw/components/FilledButton";
|
||||
|
||||
import { ReactComponent as CollabImage } from "../../src/assets/lock.svg";
|
||||
import { ReactComponent as CollabImage } from "../../packages/excalidraw/assets/lock.svg";
|
||||
import "./RoomDialog.scss";
|
||||
|
||||
const getShareIcon = () => {
|
||||
@@ -65,19 +65,18 @@ export const RoomModal = ({
|
||||
const copyRoomLink = async () => {
|
||||
try {
|
||||
await copyTextToSystemClipboard(activeRoomLink);
|
||||
|
||||
setJustCopied(true);
|
||||
|
||||
if (timerRef.current) {
|
||||
window.clearTimeout(timerRef.current);
|
||||
}
|
||||
|
||||
timerRef.current = window.setTimeout(() => {
|
||||
setJustCopied(false);
|
||||
}, 3000);
|
||||
} catch (error: any) {
|
||||
setErrorMessage(error.message);
|
||||
} catch (e) {
|
||||
setErrorMessage(t("errors.copyToSystemClipboardFailed"));
|
||||
}
|
||||
setJustCopied(true);
|
||||
|
||||
if (timerRef.current) {
|
||||
window.clearTimeout(timerRef.current);
|
||||
}
|
||||
|
||||
timerRef.current = window.setTimeout(() => {
|
||||
setJustCopied(false);
|
||||
}, 3000);
|
||||
|
||||
ref.current?.select();
|
||||
};
|
||||
@@ -120,7 +119,7 @@ export const RoomModal = ({
|
||||
size="large"
|
||||
variant="icon"
|
||||
label="Share"
|
||||
startIcon={getShareIcon()}
|
||||
icon={getShareIcon()}
|
||||
className="RoomDialog__active__share"
|
||||
onClick={shareRoomLink}
|
||||
/>
|
||||
@@ -130,7 +129,7 @@ export const RoomModal = ({
|
||||
<FilledButton
|
||||
size="large"
|
||||
label="Copy link"
|
||||
startIcon={copyIcon}
|
||||
icon={copyIcon}
|
||||
onClick={copyRoomLink}
|
||||
/>
|
||||
</Popover.Trigger>
|
||||
@@ -166,7 +165,7 @@ export const RoomModal = ({
|
||||
variant="outlined"
|
||||
color="danger"
|
||||
label={t("roomDialog.button_stopSession")}
|
||||
startIcon={playerStopFilledIcon}
|
||||
icon={playerStopFilledIcon}
|
||||
onClick={() => {
|
||||
trackEvent("share", "room closed");
|
||||
onRoomDestroy();
|
||||
@@ -195,7 +194,7 @@ export const RoomModal = ({
|
||||
<FilledButton
|
||||
size="large"
|
||||
label={t("roomDialog.button_startSession")}
|
||||
startIcon={playerPlayIcon}
|
||||
icon={playerPlayIcon}
|
||||
onClick={() => {
|
||||
trackEvent("share", "room creation", `ui (${getFrame()})`);
|
||||
onRoomCreate();
|
||||
|
@@ -1,154 +0,0 @@
|
||||
import { PRECEDING_ELEMENT_KEY } from "../../src/constants";
|
||||
import { ExcalidrawElement } from "../../src/element/types";
|
||||
import { AppState } from "../../src/types";
|
||||
import { arrayToMapWithIndex } from "../../src/utils";
|
||||
|
||||
export type ReconciledElements = readonly ExcalidrawElement[] & {
|
||||
_brand: "reconciledElements";
|
||||
};
|
||||
|
||||
export type BroadcastedExcalidrawElement = ExcalidrawElement & {
|
||||
[PRECEDING_ELEMENT_KEY]?: string;
|
||||
};
|
||||
|
||||
const shouldDiscardRemoteElement = (
|
||||
localAppState: AppState,
|
||||
local: ExcalidrawElement | undefined,
|
||||
remote: BroadcastedExcalidrawElement,
|
||||
): boolean => {
|
||||
if (
|
||||
local &&
|
||||
// local element is being edited
|
||||
(local.id === localAppState.editingElement?.id ||
|
||||
local.id === localAppState.resizingElement?.id ||
|
||||
local.id === localAppState.draggingElement?.id ||
|
||||
// local element is newer
|
||||
local.version > remote.version ||
|
||||
// resolve conflicting edits deterministically by taking the one with
|
||||
// the lowest versionNonce
|
||||
(local.version === remote.version &&
|
||||
local.versionNonce < remote.versionNonce))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const reconcileElements = (
|
||||
localElements: readonly ExcalidrawElement[],
|
||||
remoteElements: readonly BroadcastedExcalidrawElement[],
|
||||
localAppState: AppState,
|
||||
): ReconciledElements => {
|
||||
const localElementsData =
|
||||
arrayToMapWithIndex<ExcalidrawElement>(localElements);
|
||||
|
||||
const reconciledElements: ExcalidrawElement[] = localElements.slice();
|
||||
|
||||
const duplicates = new WeakMap<ExcalidrawElement, true>();
|
||||
|
||||
let cursor = 0;
|
||||
let offset = 0;
|
||||
|
||||
let remoteElementIdx = -1;
|
||||
for (const remoteElement of remoteElements) {
|
||||
remoteElementIdx++;
|
||||
|
||||
const local = localElementsData.get(remoteElement.id);
|
||||
|
||||
if (shouldDiscardRemoteElement(localAppState, local?.[0], remoteElement)) {
|
||||
if (remoteElement[PRECEDING_ELEMENT_KEY]) {
|
||||
delete remoteElement[PRECEDING_ELEMENT_KEY];
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mark duplicate for removal as it'll be replaced with the remote element
|
||||
if (local) {
|
||||
// Unless the remote and local elements are the same element in which case
|
||||
// we need to keep it as we'd otherwise discard it from the resulting
|
||||
// array.
|
||||
if (local[0] === remoteElement) {
|
||||
continue;
|
||||
}
|
||||
duplicates.set(local[0], true);
|
||||
}
|
||||
|
||||
// parent may not be defined in case the remote client is running an older
|
||||
// excalidraw version
|
||||
const parent =
|
||||
remoteElement[PRECEDING_ELEMENT_KEY] ||
|
||||
remoteElements[remoteElementIdx - 1]?.id ||
|
||||
null;
|
||||
|
||||
if (parent != null) {
|
||||
delete remoteElement[PRECEDING_ELEMENT_KEY];
|
||||
|
||||
// ^ indicates the element is the first in elements array
|
||||
if (parent === "^") {
|
||||
offset++;
|
||||
if (cursor === 0) {
|
||||
reconciledElements.unshift(remoteElement);
|
||||
localElementsData.set(remoteElement.id, [
|
||||
remoteElement,
|
||||
cursor - offset,
|
||||
]);
|
||||
} else {
|
||||
reconciledElements.splice(cursor + 1, 0, remoteElement);
|
||||
localElementsData.set(remoteElement.id, [
|
||||
remoteElement,
|
||||
cursor + 1 - offset,
|
||||
]);
|
||||
cursor++;
|
||||
}
|
||||
} else {
|
||||
let idx = localElementsData.has(parent)
|
||||
? localElementsData.get(parent)![1]
|
||||
: null;
|
||||
if (idx != null) {
|
||||
idx += offset;
|
||||
}
|
||||
if (idx != null && idx >= cursor) {
|
||||
reconciledElements.splice(idx + 1, 0, remoteElement);
|
||||
offset++;
|
||||
localElementsData.set(remoteElement.id, [
|
||||
remoteElement,
|
||||
idx + 1 - offset,
|
||||
]);
|
||||
cursor = idx + 1;
|
||||
} else if (idx != null) {
|
||||
reconciledElements.splice(cursor + 1, 0, remoteElement);
|
||||
offset++;
|
||||
localElementsData.set(remoteElement.id, [
|
||||
remoteElement,
|
||||
cursor + 1 - offset,
|
||||
]);
|
||||
cursor++;
|
||||
} else {
|
||||
reconciledElements.push(remoteElement);
|
||||
localElementsData.set(remoteElement.id, [
|
||||
remoteElement,
|
||||
reconciledElements.length - 1 - offset,
|
||||
]);
|
||||
}
|
||||
}
|
||||
// no parent z-index information, local element exists → replace in place
|
||||
} else if (local) {
|
||||
reconciledElements[local[1]] = remoteElement;
|
||||
localElementsData.set(remoteElement.id, [remoteElement, local[1]]);
|
||||
// otherwise push to the end
|
||||
} else {
|
||||
reconciledElements.push(remoteElement);
|
||||
localElementsData.set(remoteElement.id, [
|
||||
remoteElement,
|
||||
reconciledElements.length - 1 - offset,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const ret: readonly ExcalidrawElement[] = reconciledElements.filter(
|
||||
(element) => !duplicates.has(element),
|
||||
);
|
||||
|
||||
return ret as ReconciledElements;
|
||||
};
|
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Footer } from "../../src/packages/excalidraw/index";
|
||||
import { Footer } from "../../packages/excalidraw/index";
|
||||
import { EncryptedIcon } from "./EncryptedIcon";
|
||||
import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
|
||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||
|
@@ -1,12 +1,19 @@
|
||||
import React from "react";
|
||||
import { PlusPromoIcon } from "../../src/components/icons";
|
||||
import { MainMenu } from "../../src/packages/excalidraw/index";
|
||||
import {
|
||||
arrowBarToLeftIcon,
|
||||
ExcalLogo,
|
||||
} from "../../packages/excalidraw/components/icons";
|
||||
import { Theme } from "../../packages/excalidraw/element/types";
|
||||
import { MainMenu } from "../../packages/excalidraw/index";
|
||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||
import { LanguageList } from "./LanguageList";
|
||||
|
||||
export const AppMainMenu: React.FC<{
|
||||
setCollabDialogShown: (toggle: boolean) => any;
|
||||
onCollabDialogOpen: () => any;
|
||||
isCollaborating: boolean;
|
||||
isCollabEnabled: boolean;
|
||||
theme: Theme | "system";
|
||||
setTheme: (theme: Theme | "system") => void;
|
||||
}> = React.memo((props) => {
|
||||
return (
|
||||
<MainMenu>
|
||||
@@ -17,25 +24,38 @@ export const AppMainMenu: React.FC<{
|
||||
{props.isCollabEnabled && (
|
||||
<MainMenu.DefaultItems.LiveCollaborationTrigger
|
||||
isCollaborating={props.isCollaborating}
|
||||
onSelect={() => props.setCollabDialogShown(true)}
|
||||
onSelect={() => props.onCollabDialogOpen()}
|
||||
/>
|
||||
)}
|
||||
|
||||
<MainMenu.DefaultItems.CommandPalette className="highlighted" />
|
||||
<MainMenu.DefaultItems.Help />
|
||||
<MainMenu.DefaultItems.ClearCanvas />
|
||||
<MainMenu.Separator />
|
||||
<MainMenu.ItemLink
|
||||
icon={PlusPromoIcon}
|
||||
icon={ExcalLogo}
|
||||
href={`${
|
||||
import.meta.env.VITE_APP_PLUS_LP
|
||||
import.meta.env.VITE_APP_PLUS_APP
|
||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger`}
|
||||
className="ExcalidrawPlus"
|
||||
className=""
|
||||
>
|
||||
Excalidraw+
|
||||
</MainMenu.ItemLink>
|
||||
<MainMenu.DefaultItems.Socials />
|
||||
<MainMenu.ItemLink
|
||||
icon={arrowBarToLeftIcon}
|
||||
href={`${import.meta.env.VITE_APP_PLUS_APP}${
|
||||
isExcalidrawPlusSignedUser ? "" : "/sign-up"
|
||||
}?utm_source=signin&utm_medium=app&utm_content=hamburger`}
|
||||
className="highlighted"
|
||||
>
|
||||
{isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"}
|
||||
</MainMenu.ItemLink>
|
||||
<MainMenu.Separator />
|
||||
<MainMenu.DefaultItems.ToggleTheme />
|
||||
<MainMenu.DefaultItems.ToggleTheme
|
||||
allowSystemTheme
|
||||
theme={props.theme}
|
||||
onSelect={props.setTheme}
|
||||
/>
|
||||
<MainMenu.ItemCustom>
|
||||
<LanguageList style={{ width: "100%" }} />
|
||||
</MainMenu.ItemCustom>
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import React from "react";
|
||||
import { PlusPromoIcon } from "../../src/components/icons";
|
||||
import { useI18n } from "../../src/i18n";
|
||||
import { WelcomeScreen } from "../../src/packages/excalidraw/index";
|
||||
import { arrowBarToLeftIcon } from "../../packages/excalidraw/components/icons";
|
||||
import { useI18n } from "../../packages/excalidraw/i18n";
|
||||
import { WelcomeScreen } from "../../packages/excalidraw/index";
|
||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||
import { POINTER_EVENTS } from "../../src/constants";
|
||||
import { POINTER_EVENTS } from "../../packages/excalidraw/constants";
|
||||
|
||||
export const AppWelcomeScreen: React.FC<{
|
||||
setCollabDialogShown: (toggle: boolean) => any;
|
||||
onCollabDialogOpen: () => any;
|
||||
isCollabEnabled: boolean;
|
||||
}> = React.memo((props) => {
|
||||
const { t } = useI18n();
|
||||
@@ -52,7 +52,7 @@ export const AppWelcomeScreen: React.FC<{
|
||||
<WelcomeScreen.Center.MenuItemHelp />
|
||||
{props.isCollabEnabled && (
|
||||
<WelcomeScreen.Center.MenuItemLiveCollaborationTrigger
|
||||
onSelect={() => props.setCollabDialogShown(true)}
|
||||
onSelect={() => props.onCollabDialogOpen()}
|
||||
/>
|
||||
)}
|
||||
{!isExcalidrawPlusSignedUser && (
|
||||
@@ -61,9 +61,9 @@ export const AppWelcomeScreen: React.FC<{
|
||||
import.meta.env.VITE_APP_PLUS_LP
|
||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest`}
|
||||
shortcut={null}
|
||||
icon={PlusPromoIcon}
|
||||
icon={arrowBarToLeftIcon}
|
||||
>
|
||||
Try Excalidraw Plus!
|
||||
Sign up
|
||||
</WelcomeScreen.Center.MenuItemLink>
|
||||
)}
|
||||
</WelcomeScreen.Center.Menu>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { shield } from "../../src/components/icons";
|
||||
import { Tooltip } from "../../src/components/Tooltip";
|
||||
import { useI18n } from "../../src/i18n";
|
||||
import { shield } from "../../packages/excalidraw/components/icons";
|
||||
import { Tooltip } from "../../packages/excalidraw/components/Tooltip";
|
||||
import { useI18n } from "../../packages/excalidraw/i18n";
|
||||
|
||||
export const EncryptedIcon = () => {
|
||||
const { t } = useI18n();
|
||||
|
@@ -1,25 +1,36 @@
|
||||
import React from "react";
|
||||
import { Card } from "../../src/components/Card";
|
||||
import { ToolButton } from "../../src/components/ToolButton";
|
||||
import { serializeAsJSON } from "../../src/data/json";
|
||||
import { Card } from "../../packages/excalidraw/components/Card";
|
||||
import { ToolButton } from "../../packages/excalidraw/components/ToolButton";
|
||||
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
|
||||
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
|
||||
import { FileId, NonDeletedExcalidrawElement } from "../../src/element/types";
|
||||
import { AppState, BinaryFileData, BinaryFiles } from "../../src/types";
|
||||
import {
|
||||
FileId,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../../packages/excalidraw/element/types";
|
||||
import {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
BinaryFiles,
|
||||
} from "../../packages/excalidraw/types";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useI18n } from "../../src/i18n";
|
||||
import { encryptData, generateEncryptionKey } from "../../src/data/encryption";
|
||||
import { isInitializedImageElement } from "../../src/element/typeChecks";
|
||||
import { useI18n } from "../../packages/excalidraw/i18n";
|
||||
import {
|
||||
encryptData,
|
||||
generateEncryptionKey,
|
||||
} from "../../packages/excalidraw/data/encryption";
|
||||
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
||||
import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
|
||||
import { encodeFilesForUpload } from "../data/FileManager";
|
||||
import { MIME_TYPES } from "../../src/constants";
|
||||
import { trackEvent } from "../../src/analytics";
|
||||
import { getFrame } from "../../src/utils";
|
||||
import { ExcalidrawLogo } from "../../src/components/ExcalidrawLogo";
|
||||
import { MIME_TYPES } from "../../packages/excalidraw/constants";
|
||||
import { trackEvent } from "../../packages/excalidraw/analytics";
|
||||
import { getFrame } from "../../packages/excalidraw/utils";
|
||||
import { ExcalidrawLogo } from "../../packages/excalidraw/components/ExcalidrawLogo";
|
||||
|
||||
export const exportToExcalidrawPlus = async (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: Partial<AppState>,
|
||||
files: BinaryFiles,
|
||||
name: string,
|
||||
) => {
|
||||
const firebase = await loadFirebaseStorage();
|
||||
|
||||
@@ -43,7 +54,7 @@ export const exportToExcalidrawPlus = async (
|
||||
.ref(`/migrations/scenes/${id}`)
|
||||
.put(blob, {
|
||||
customMetadata: {
|
||||
data: JSON.stringify({ version: 2, name: appState.name }),
|
||||
data: JSON.stringify({ version: 2, name }),
|
||||
created: Date.now().toString(),
|
||||
},
|
||||
});
|
||||
@@ -79,9 +90,10 @@ export const ExportToExcalidrawPlus: React.FC<{
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
appState: Partial<AppState>;
|
||||
files: BinaryFiles;
|
||||
name: string;
|
||||
onError: (error: Error) => void;
|
||||
onSuccess: () => void;
|
||||
}> = ({ elements, appState, files, onError, onSuccess }) => {
|
||||
}> = ({ elements, appState, files, name, onError, onSuccess }) => {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<Card color="primary">
|
||||
@@ -107,7 +119,7 @@ export const ExportToExcalidrawPlus: React.FC<{
|
||||
onClick={async () => {
|
||||
try {
|
||||
trackEvent("export", "eplus", `ui (${getFrame()})`);
|
||||
await exportToExcalidrawPlus(elements, appState, files);
|
||||
await exportToExcalidrawPlus(elements, appState, files, name);
|
||||
onSuccess();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import oc from "open-color";
|
||||
import React from "react";
|
||||
import { THEME } from "../../src/constants";
|
||||
import { Theme } from "../../src/element/types";
|
||||
import { THEME } from "../../packages/excalidraw/constants";
|
||||
import { Theme } from "../../packages/excalidraw/element/types";
|
||||
|
||||
// https://github.com/tholman/github-corners
|
||||
export const GitHubCorner = React.memo(
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { useSetAtom } from "jotai";
|
||||
import React from "react";
|
||||
import { appLangCodeAtom } from "..";
|
||||
import { useI18n } from "../../src/i18n";
|
||||
import { languages } from "../../src/i18n";
|
||||
import { appLangCodeAtom } from "../App";
|
||||
import { useI18n } from "../../packages/excalidraw/i18n";
|
||||
import { languages } from "../../packages/excalidraw/i18n";
|
||||
|
||||
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
||||
const { t, langCode } = useI18n();
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import { t } from "../i18n";
|
||||
import Trans from "./Trans";
|
||||
import { t } from "../../packages/excalidraw/i18n";
|
||||
import Trans from "../../packages/excalidraw/components/Trans";
|
||||
|
||||
interface TopErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
@@ -67,6 +67,8 @@ export class TopErrorBoundary extends React.Component<
|
||||
|
||||
window.open(
|
||||
`https://github.com/excalidraw/excalidraw/issues/new?body=${body}`,
|
||||
"_blank",
|
||||
"noopener noreferrer",
|
||||
);
|
||||
}
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { compressData } from "../../src/data/encode";
|
||||
import { newElementWith } from "../../src/element/mutateElement";
|
||||
import { isInitializedImageElement } from "../../src/element/typeChecks";
|
||||
import { compressData } from "../../packages/excalidraw/data/encode";
|
||||
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
||||
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawImageElement,
|
||||
FileId,
|
||||
InitializedExcalidrawImageElement,
|
||||
} from "../../src/element/types";
|
||||
import { t } from "../../src/i18n";
|
||||
} from "../../packages/excalidraw/element/types";
|
||||
import { t } from "../../packages/excalidraw/i18n";
|
||||
import {
|
||||
BinaryFileData,
|
||||
BinaryFileMetadata,
|
||||
ExcalidrawImperativeAPI,
|
||||
BinaryFiles,
|
||||
} from "../../src/types";
|
||||
} from "../../packages/excalidraw/types";
|
||||
|
||||
export class FileManager {
|
||||
/** files being fetched */
|
||||
|
@@ -10,12 +10,30 @@
|
||||
* (localStorage, indexedDB).
|
||||
*/
|
||||
|
||||
import { createStore, entries, del, getMany, set, setMany } from "idb-keyval";
|
||||
import { clearAppStateForLocalStorage } from "../../src/appState";
|
||||
import { clearElementsForLocalStorage } from "../../src/element";
|
||||
import { ExcalidrawElement, FileId } from "../../src/element/types";
|
||||
import { AppState, BinaryFileData, BinaryFiles } from "../../src/types";
|
||||
import { debounce } from "../../src/utils";
|
||||
import {
|
||||
createStore,
|
||||
entries,
|
||||
del,
|
||||
getMany,
|
||||
set,
|
||||
setMany,
|
||||
get,
|
||||
} from "idb-keyval";
|
||||
import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState";
|
||||
import { LibraryPersistedData } from "../../packages/excalidraw/data/library";
|
||||
import { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
} from "../../packages/excalidraw/element/types";
|
||||
import {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
BinaryFiles,
|
||||
} from "../../packages/excalidraw/types";
|
||||
import { MaybePromise } from "../../packages/excalidraw/utility-types";
|
||||
import { debounce } from "../../packages/excalidraw/utils";
|
||||
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
|
||||
import { FileManager } from "./FileManager";
|
||||
import { Locker } from "./Locker";
|
||||
@@ -176,3 +194,52 @@ export class LocalData {
|
||||
},
|
||||
});
|
||||
}
|
||||
export class LibraryIndexedDBAdapter {
|
||||
/** IndexedDB database and store name */
|
||||
private static idb_name = STORAGE_KEYS.IDB_LIBRARY;
|
||||
/** library data store key */
|
||||
private static key = "libraryData";
|
||||
|
||||
private static store = createStore(
|
||||
`${LibraryIndexedDBAdapter.idb_name}-db`,
|
||||
`${LibraryIndexedDBAdapter.idb_name}-store`,
|
||||
);
|
||||
|
||||
static async load() {
|
||||
const IDBData = await get<LibraryPersistedData>(
|
||||
LibraryIndexedDBAdapter.key,
|
||||
LibraryIndexedDBAdapter.store,
|
||||
);
|
||||
|
||||
return IDBData || null;
|
||||
}
|
||||
|
||||
static save(data: LibraryPersistedData): MaybePromise<void> {
|
||||
return set(
|
||||
LibraryIndexedDBAdapter.key,
|
||||
data,
|
||||
LibraryIndexedDBAdapter.store,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** LS Adapter used only for migrating LS library data
|
||||
* to indexedDB */
|
||||
export class LibraryLocalStorageMigrationAdapter {
|
||||
static load() {
|
||||
const LSData = localStorage.getItem(
|
||||
STORAGE_KEYS.__LEGACY_LOCAL_STORAGE_LIBRARY,
|
||||
);
|
||||
if (LSData != null) {
|
||||
const libraryItems: ImportedDataState["libraryItems"] =
|
||||
JSON.parse(LSData);
|
||||
if (libraryItems) {
|
||||
return { libraryItems };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
static clear() {
|
||||
localStorage.removeItem(STORAGE_KEYS.__LEGACY_LOCAL_STORAGE_LIBRARY);
|
||||
}
|
||||
}
|
||||
|
@@ -1,20 +1,31 @@
|
||||
import { ExcalidrawElement, FileId } from "../../src/element/types";
|
||||
import { getSceneVersion } from "../../src/element";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
OrderedExcalidrawElement,
|
||||
} from "../../packages/excalidraw/element/types";
|
||||
import { getSceneVersion } from "../../packages/excalidraw/element";
|
||||
import Portal from "../collab/Portal";
|
||||
import { restoreElements } from "../../src/data/restore";
|
||||
import { restoreElements } from "../../packages/excalidraw/data/restore";
|
||||
import {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
BinaryFileMetadata,
|
||||
DataURL,
|
||||
} from "../../src/types";
|
||||
} from "../../packages/excalidraw/types";
|
||||
import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
|
||||
import { decompressData } from "../../src/data/encode";
|
||||
import { encryptData, decryptData } from "../../src/data/encryption";
|
||||
import { MIME_TYPES } from "../../src/constants";
|
||||
import { reconcileElements } from "../collab/reconciliation";
|
||||
import { decompressData } from "../../packages/excalidraw/data/encode";
|
||||
import {
|
||||
encryptData,
|
||||
decryptData,
|
||||
} from "../../packages/excalidraw/data/encryption";
|
||||
import { MIME_TYPES } from "../../packages/excalidraw/constants";
|
||||
import { getSyncableElements, SyncableExcalidrawElement } from ".";
|
||||
import { ResolutionType } from "../../src/utility-types";
|
||||
import { ResolutionType } from "../../packages/excalidraw/utility-types";
|
||||
import type { Socket } from "socket.io-client";
|
||||
import {
|
||||
RemoteExcalidrawElement,
|
||||
reconcileElements,
|
||||
} from "../../packages/excalidraw/data/reconcile";
|
||||
|
||||
// private
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -132,12 +143,12 @@ const decryptElements = async (
|
||||
};
|
||||
|
||||
class FirebaseSceneVersionCache {
|
||||
private static cache = new WeakMap<SocketIOClient.Socket, number>();
|
||||
static get = (socket: SocketIOClient.Socket) => {
|
||||
private static cache = new WeakMap<Socket, number>();
|
||||
static get = (socket: Socket) => {
|
||||
return FirebaseSceneVersionCache.cache.get(socket);
|
||||
};
|
||||
static set = (
|
||||
socket: SocketIOClient.Socket,
|
||||
socket: Socket,
|
||||
elements: readonly SyncableExcalidrawElement[],
|
||||
) => {
|
||||
FirebaseSceneVersionCache.cache.set(socket, getSceneVersion(elements));
|
||||
@@ -223,7 +234,7 @@ export const saveToFirebase = async (
|
||||
!socket ||
|
||||
isSavedToFirebase(portal, elements)
|
||||
) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const firebase = await loadFirestore();
|
||||
@@ -231,56 +242,59 @@ export const saveToFirebase = async (
|
||||
|
||||
const docRef = firestore.collection("scenes").doc(roomId);
|
||||
|
||||
const savedData = await firestore.runTransaction(async (transaction) => {
|
||||
const storedScene = await firestore.runTransaction(async (transaction) => {
|
||||
const snapshot = await transaction.get(docRef);
|
||||
|
||||
if (!snapshot.exists) {
|
||||
const sceneDocument = await createFirebaseSceneDocument(
|
||||
const storedScene = await createFirebaseSceneDocument(
|
||||
firebase,
|
||||
elements,
|
||||
roomKey,
|
||||
);
|
||||
|
||||
transaction.set(docRef, sceneDocument);
|
||||
transaction.set(docRef, storedScene);
|
||||
|
||||
return {
|
||||
elements,
|
||||
reconciledElements: null,
|
||||
};
|
||||
return storedScene;
|
||||
}
|
||||
|
||||
const prevDocData = snapshot.data() as FirebaseStoredScene;
|
||||
const prevElements = getSyncableElements(
|
||||
await decryptElements(prevDocData, roomKey),
|
||||
const prevStoredScene = snapshot.data() as FirebaseStoredScene;
|
||||
const prevStoredElements = getSyncableElements(
|
||||
restoreElements(await decryptElements(prevStoredScene, roomKey), null),
|
||||
);
|
||||
|
||||
const reconciledElements = getSyncableElements(
|
||||
reconcileElements(elements, prevElements, appState),
|
||||
reconcileElements(
|
||||
elements,
|
||||
prevStoredElements as OrderedExcalidrawElement[] as RemoteExcalidrawElement[],
|
||||
appState,
|
||||
),
|
||||
);
|
||||
|
||||
const sceneDocument = await createFirebaseSceneDocument(
|
||||
const storedScene = await createFirebaseSceneDocument(
|
||||
firebase,
|
||||
reconciledElements,
|
||||
roomKey,
|
||||
);
|
||||
|
||||
transaction.update(docRef, sceneDocument);
|
||||
return {
|
||||
elements,
|
||||
reconciledElements,
|
||||
};
|
||||
transaction.update(docRef, storedScene);
|
||||
|
||||
// Return the stored elements as the in memory `reconciledElements` could have mutated in the meantime
|
||||
return storedScene;
|
||||
});
|
||||
|
||||
FirebaseSceneVersionCache.set(socket, savedData.elements);
|
||||
const storedElements = getSyncableElements(
|
||||
restoreElements(await decryptElements(storedScene, roomKey), null),
|
||||
);
|
||||
|
||||
return { reconciledElements: savedData.reconciledElements };
|
||||
FirebaseSceneVersionCache.set(socket, storedElements);
|
||||
|
||||
return storedElements;
|
||||
};
|
||||
|
||||
export const loadFromFirebase = async (
|
||||
roomId: string,
|
||||
roomKey: string,
|
||||
socket: SocketIOClient.Socket | null,
|
||||
): Promise<readonly ExcalidrawElement[] | null> => {
|
||||
socket: Socket | null,
|
||||
): Promise<readonly SyncableExcalidrawElement[] | null> => {
|
||||
const firebase = await loadFirestore();
|
||||
const db = firebase.firestore();
|
||||
|
||||
@@ -291,14 +305,14 @@ export const loadFromFirebase = async (
|
||||
}
|
||||
const storedScene = doc.data() as FirebaseStoredScene;
|
||||
const elements = getSyncableElements(
|
||||
await decryptElements(storedScene, roomKey),
|
||||
restoreElements(await decryptElements(storedScene, roomKey), null),
|
||||
);
|
||||
|
||||
if (socket) {
|
||||
FirebaseSceneVersionCache.set(socket, elements);
|
||||
}
|
||||
|
||||
return restoreElements(elements, null);
|
||||
return elements;
|
||||
};
|
||||
|
||||
export const loadFilesFromFirebase = async (
|
||||
|
@@ -1,37 +1,47 @@
|
||||
import { compressData, decompressData } from "../../src/data/encode";
|
||||
import {
|
||||
compressData,
|
||||
decompressData,
|
||||
} from "../../packages/excalidraw/data/encode";
|
||||
import {
|
||||
decryptData,
|
||||
generateEncryptionKey,
|
||||
IV_LENGTH_BYTES,
|
||||
} from "../../src/data/encryption";
|
||||
import { serializeAsJSON } from "../../src/data/json";
|
||||
import { restore } from "../../src/data/restore";
|
||||
import { ImportedDataState } from "../../src/data/types";
|
||||
import { isInvisiblySmallElement } from "../../src/element/sizeHelpers";
|
||||
import { isInitializedImageElement } from "../../src/element/typeChecks";
|
||||
import { ExcalidrawElement, FileId } from "../../src/element/types";
|
||||
import { t } from "../../src/i18n";
|
||||
} from "../../packages/excalidraw/data/encryption";
|
||||
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
|
||||
import { restore } from "../../packages/excalidraw/data/restore";
|
||||
import { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||
import { SceneBounds } from "../../packages/excalidraw/element/bounds";
|
||||
import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers";
|
||||
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
OrderedExcalidrawElement,
|
||||
} from "../../packages/excalidraw/element/types";
|
||||
import { t } from "../../packages/excalidraw/i18n";
|
||||
import {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
BinaryFiles,
|
||||
SocketId,
|
||||
UserIdleState,
|
||||
} from "../../src/types";
|
||||
import { bytesToHexString } from "../../src/utils";
|
||||
} from "../../packages/excalidraw/types";
|
||||
import { MakeBrand } from "../../packages/excalidraw/utility-types";
|
||||
import { bytesToHexString } from "../../packages/excalidraw/utils";
|
||||
import {
|
||||
DELETED_ELEMENT_TIMEOUT,
|
||||
FILE_UPLOAD_MAX_BYTES,
|
||||
ROOM_ID_BYTES,
|
||||
WS_SUBTYPES,
|
||||
} from "../app_constants";
|
||||
import { encodeFilesForUpload } from "./FileManager";
|
||||
import { saveFilesToFirebase } from "./firebase";
|
||||
|
||||
export type SyncableExcalidrawElement = ExcalidrawElement & {
|
||||
_brand: "SyncableExcalidrawElement";
|
||||
};
|
||||
export type SyncableExcalidrawElement = OrderedExcalidrawElement &
|
||||
MakeBrand<"SyncableExcalidrawElement">;
|
||||
|
||||
export const isSyncableElement = (
|
||||
element: ExcalidrawElement,
|
||||
element: OrderedExcalidrawElement,
|
||||
): element is SyncableExcalidrawElement => {
|
||||
if (element.isDeleted) {
|
||||
if (element.updated > Date.now() - DELETED_ELEMENT_TIMEOUT) {
|
||||
@@ -42,7 +52,9 @@ export const isSyncableElement = (
|
||||
return !isInvisiblySmallElement(element);
|
||||
};
|
||||
|
||||
export const getSyncableElements = (elements: readonly ExcalidrawElement[]) =>
|
||||
export const getSyncableElements = (
|
||||
elements: readonly OrderedExcalidrawElement[],
|
||||
) =>
|
||||
elements.filter((element) =>
|
||||
isSyncableElement(element),
|
||||
) as SyncableExcalidrawElement[];
|
||||
@@ -56,67 +68,49 @@ const generateRoomId = async () => {
|
||||
return bytesToHexString(buffer);
|
||||
};
|
||||
|
||||
/**
|
||||
* Right now the reason why we resolve connection params (url, polling...)
|
||||
* from upstream is to allow changing the params immediately when needed without
|
||||
* having to wait for clients to update the SW.
|
||||
*
|
||||
* If REACT_APP_WS_SERVER_URL env is set, we use that instead (useful for forks)
|
||||
*/
|
||||
export const getCollabServer = async (): Promise<{
|
||||
url: string;
|
||||
polling: boolean;
|
||||
}> => {
|
||||
if (import.meta.env.VITE_APP_WS_SERVER_URL) {
|
||||
return {
|
||||
url: import.meta.env.VITE_APP_WS_SERVER_URL,
|
||||
polling: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`${import.meta.env.VITE_APP_PORTAL_URL}/collab-server`,
|
||||
);
|
||||
return await resp.json();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error(t("errors.cannotResolveCollabServer"));
|
||||
}
|
||||
};
|
||||
|
||||
export type EncryptedData = {
|
||||
data: ArrayBuffer;
|
||||
iv: Uint8Array;
|
||||
};
|
||||
|
||||
export type SocketUpdateDataSource = {
|
||||
INVALID_RESPONSE: {
|
||||
type: WS_SUBTYPES.INVALID_RESPONSE;
|
||||
};
|
||||
SCENE_INIT: {
|
||||
type: "SCENE_INIT";
|
||||
type: WS_SUBTYPES.INIT;
|
||||
payload: {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
};
|
||||
};
|
||||
SCENE_UPDATE: {
|
||||
type: "SCENE_UPDATE";
|
||||
type: WS_SUBTYPES.UPDATE;
|
||||
payload: {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
};
|
||||
};
|
||||
MOUSE_LOCATION: {
|
||||
type: "MOUSE_LOCATION";
|
||||
type: WS_SUBTYPES.MOUSE_LOCATION;
|
||||
payload: {
|
||||
socketId: string;
|
||||
socketId: SocketId;
|
||||
pointer: { x: number; y: number; tool: "pointer" | "laser" };
|
||||
button: "down" | "up";
|
||||
selectedElementIds: AppState["selectedElementIds"];
|
||||
username: string;
|
||||
};
|
||||
};
|
||||
IDLE_STATUS: {
|
||||
type: "IDLE_STATUS";
|
||||
USER_VISIBLE_SCENE_BOUNDS: {
|
||||
type: WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS;
|
||||
payload: {
|
||||
socketId: string;
|
||||
socketId: SocketId;
|
||||
username: string;
|
||||
sceneBounds: SceneBounds;
|
||||
};
|
||||
};
|
||||
IDLE_STATUS: {
|
||||
type: WS_SUBTYPES.IDLE_STATUS;
|
||||
payload: {
|
||||
socketId: SocketId;
|
||||
userState: UserIdleState;
|
||||
username: string;
|
||||
};
|
||||
@@ -124,10 +118,7 @@ export type SocketUpdateDataSource = {
|
||||
};
|
||||
|
||||
export type SocketUpdateDataIncoming =
|
||||
| SocketUpdateDataSource[keyof SocketUpdateDataSource]
|
||||
| {
|
||||
type: "INVALID_RESPONSE";
|
||||
};
|
||||
SocketUpdateDataSource[keyof SocketUpdateDataSource];
|
||||
|
||||
export type SocketUpdateData =
|
||||
SocketUpdateDataSource[keyof SocketUpdateDataSource] & {
|
||||
|
@@ -1,12 +1,11 @@
|
||||
import { ExcalidrawElement } from "../../src/element/types";
|
||||
import { AppState } from "../../src/types";
|
||||
import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
|
||||
import { AppState } from "../../packages/excalidraw/types";
|
||||
import {
|
||||
clearAppStateForLocalStorage,
|
||||
getDefaultAppState,
|
||||
} from "../../src/appState";
|
||||
import { clearElementsForLocalStorage } from "../../src/element";
|
||||
} from "../../packages/excalidraw/appState";
|
||||
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
|
||||
import { STORAGE_KEYS } from "../app_constants";
|
||||
import { ImportedDataState } from "../../src/data/types";
|
||||
|
||||
export const saveUsernameToLocalStorage = (username: string) => {
|
||||
try {
|
||||
@@ -88,28 +87,13 @@ export const getTotalStorageSize = () => {
|
||||
try {
|
||||
const appState = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_APP_STATE);
|
||||
const collab = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB);
|
||||
const library = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
|
||||
|
||||
const appStateSize = appState?.length || 0;
|
||||
const collabSize = collab?.length || 0;
|
||||
const librarySize = library?.length || 0;
|
||||
|
||||
return appStateSize + collabSize + librarySize + getElementsStorageSize();
|
||||
return appStateSize + collabSize + getElementsStorageSize();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const getLibraryItemsFromStorage = () => {
|
||||
try {
|
||||
const libraryItems: ImportedDataState["libraryItems"] = JSON.parse(
|
||||
localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY) as string,
|
||||
);
|
||||
|
||||
return libraryItems || [];
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
3
excalidraw-app/global.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
interface Window {
|
||||
__EXCALIDRAW_SHA__: string | undefined;
|
||||
}
|
@@ -64,12 +64,30 @@
|
||||
<!-- to minimize white flash on load when user has dark mode enabled -->
|
||||
<script>
|
||||
try {
|
||||
//
|
||||
const theme = window.localStorage.getItem("excalidraw-theme");
|
||||
if (theme === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
function setTheme(theme) {
|
||||
if (theme === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
function getTheme() {
|
||||
const theme = window.localStorage.getItem("excalidraw-theme");
|
||||
|
||||
if (theme && theme === "system") {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
} else {
|
||||
return theme || "light";
|
||||
}
|
||||
}
|
||||
|
||||
setTheme(getTheme());
|
||||
} catch (e) {
|
||||
console.error("Error setting dark mode", e);
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
html.dark {
|
||||
@@ -78,7 +96,7 @@
|
||||
}
|
||||
</style>
|
||||
<!------------------------------------------------------------------------->
|
||||
<% if ("%PROD%" === "true") { %>
|
||||
<% if (typeof PROD != 'undefined' && PROD == true) { %>
|
||||
<script>
|
||||
// Redirect Excalidraw+ users which have auto-redirect enabled.
|
||||
//
|
||||
@@ -121,8 +139,9 @@
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
|
||||
<link rel="stylesheet" href="/fonts.css" type="text/css" />
|
||||
<% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%"==="true" ) { %>
|
||||
<link rel="stylesheet" href="/fonts/fonts.css" type="text/css" />
|
||||
<% if (typeof VITE_APP_DEV_DISABLE_LIVE_RELOAD != 'undefined' &&
|
||||
VITE_APP_DEV_DISABLE_LIVE_RELOAD == true) { %>
|
||||
<script>
|
||||
{
|
||||
const _WebSocket = window.WebSocket;
|
||||
@@ -195,8 +214,7 @@
|
||||
<h1 class="visually-hidden">Excalidraw</h1>
|
||||
</header>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
<% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%" !== 'true') { %>
|
||||
<script type="module" src="index.tsx"></script>
|
||||
<!-- 100% privacy friendly analytics -->
|
||||
<script>
|
||||
// need to load this script dynamically bcs. of iframe embed tracking
|
||||
@@ -229,6 +247,5 @@
|
||||
}
|
||||
</script>
|
||||
<!-- end LEGACY GOOGLE ANALYTICS -->
|
||||
<% } %>
|
||||
</body>
|
||||
</html>
|
@@ -4,6 +4,13 @@
|
||||
&.theme--dark {
|
||||
--color-primary-contrast-offset: #726dff; // to offset Chubb illusion
|
||||
}
|
||||
|
||||
.top-right-ui {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.footer-center {
|
||||
justify-content: flex-end;
|
||||
margin-top: auto;
|
||||
@@ -31,7 +38,7 @@
|
||||
background-color: #ecfdf5;
|
||||
color: #064e3c;
|
||||
}
|
||||
&.ExcalidrawPlus {
|
||||
&.highlighted {
|
||||
color: var(--color-promo);
|
||||
}
|
||||
}
|
||||
|
@@ -1,811 +1,15 @@
|
||||
import polyfill from "../src/polyfill";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { trackEvent } from "../src/analytics";
|
||||
import { getDefaultAppState } from "../src/appState";
|
||||
import { ErrorDialog } from "../src/components/ErrorDialog";
|
||||
import { TopErrorBoundary } from "../src/components/TopErrorBoundary";
|
||||
import {
|
||||
APP_NAME,
|
||||
EVENT,
|
||||
THEME,
|
||||
TITLE_TIMEOUT,
|
||||
VERSION_TIMEOUT,
|
||||
} from "../src/constants";
|
||||
import { loadFromBlob } from "../src/data/blob";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
NonDeletedExcalidrawElement,
|
||||
Theme,
|
||||
} from "../src/element/types";
|
||||
import { useCallbackRefState } from "../src/hooks/useCallbackRefState";
|
||||
import { t } from "../src/i18n";
|
||||
import {
|
||||
Excalidraw,
|
||||
defaultLang,
|
||||
LiveCollaborationTrigger,
|
||||
} from "../src/packages/excalidraw/index";
|
||||
import {
|
||||
AppState,
|
||||
LibraryItems,
|
||||
ExcalidrawImperativeAPI,
|
||||
BinaryFiles,
|
||||
ExcalidrawInitialDataState,
|
||||
UIAppState,
|
||||
} from "../src/types";
|
||||
import {
|
||||
debounce,
|
||||
getVersion,
|
||||
getFrame,
|
||||
isTestEnv,
|
||||
preventUnload,
|
||||
ResolvablePromise,
|
||||
resolvablePromise,
|
||||
isRunningInIframe,
|
||||
} from "../src/utils";
|
||||
import {
|
||||
FIREBASE_STORAGE_PREFIXES,
|
||||
STORAGE_KEYS,
|
||||
SYNC_BROWSER_TABS_TIMEOUT,
|
||||
} from "./app_constants";
|
||||
import Collab, {
|
||||
CollabAPI,
|
||||
collabAPIAtom,
|
||||
collabDialogShownAtom,
|
||||
isCollaboratingAtom,
|
||||
isOfflineAtom,
|
||||
} from "./collab/Collab";
|
||||
import {
|
||||
exportToBackend,
|
||||
getCollaborationLinkData,
|
||||
isCollaborationLink,
|
||||
loadScene,
|
||||
} from "./data";
|
||||
import {
|
||||
getLibraryItemsFromStorage,
|
||||
importFromLocalStorage,
|
||||
importUsernameFromLocalStorage,
|
||||
} from "./data/localStorage";
|
||||
import CustomStats from "./CustomStats";
|
||||
import {
|
||||
restore,
|
||||
restoreAppState,
|
||||
RestoredDataState,
|
||||
} from "../src/data/restore";
|
||||
import {
|
||||
ExportToExcalidrawPlus,
|
||||
exportToExcalidrawPlus,
|
||||
} from "./components/ExportToExcalidrawPlus";
|
||||
import { updateStaleImageStatuses } from "./data/FileManager";
|
||||
import { newElementWith } from "../src/element/mutateElement";
|
||||
import { isInitializedImageElement } from "../src/element/typeChecks";
|
||||
import { loadFilesFromFirebase } from "./data/firebase";
|
||||
import { LocalData } from "./data/LocalData";
|
||||
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
||||
import clsx from "clsx";
|
||||
import { reconcileElements } from "./collab/reconciliation";
|
||||
import {
|
||||
parseLibraryTokensFromUrl,
|
||||
useHandleLibrary,
|
||||
} from "../src/data/library";
|
||||
import { AppMainMenu } from "./components/AppMainMenu";
|
||||
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
||||
import { AppFooter } from "./components/AppFooter";
|
||||
import { atom, Provider, useAtom, useAtomValue } from "jotai";
|
||||
import { useAtomWithInitialValue } from "../src/jotai";
|
||||
import { appJotaiStore } from "./app-jotai";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import ExcalidrawApp from "./App";
|
||||
import { registerSW } from "virtual:pwa-register";
|
||||
|
||||
import "./index.scss";
|
||||
import { ResolutionType } from "../src/utility-types";
|
||||
import { ShareableLinkDialog } from "../src/components/ShareableLinkDialog";
|
||||
import { openConfirmModal } from "../src/components/OverwriteConfirm/OverwriteConfirmState";
|
||||
import { OverwriteConfirmDialog } from "../src/components/OverwriteConfirm/OverwriteConfirm";
|
||||
import Trans from "../src/components/Trans";
|
||||
|
||||
polyfill();
|
||||
|
||||
window.EXCALIDRAW_THROTTLE_RENDER = true;
|
||||
|
||||
let isSelfEmbedding = false;
|
||||
|
||||
if (window.self !== window.top) {
|
||||
try {
|
||||
const parentUrl = new URL(document.referrer);
|
||||
const currentUrl = new URL(window.location.href);
|
||||
if (parentUrl.origin === currentUrl.origin) {
|
||||
isSelfEmbedding = true;
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const languageDetector = new LanguageDetector();
|
||||
languageDetector.init({
|
||||
languageUtils: {},
|
||||
});
|
||||
|
||||
const shareableLinkConfirmDialog = {
|
||||
title: t("overwriteConfirm.modal.shareableLink.title"),
|
||||
description: (
|
||||
<Trans
|
||||
i18nKey="overwriteConfirm.modal.shareableLink.description"
|
||||
bold={(text) => <strong>{text}</strong>}
|
||||
br={() => <br />}
|
||||
/>
|
||||
),
|
||||
actionLabel: t("overwriteConfirm.modal.shareableLink.button"),
|
||||
color: "danger",
|
||||
} as const;
|
||||
|
||||
const initializeScene = async (opts: {
|
||||
collabAPI: CollabAPI | null;
|
||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||
}): Promise<
|
||||
{ scene: ExcalidrawInitialDataState | null } & (
|
||||
| { isExternalScene: true; id: string; key: string }
|
||||
| { isExternalScene: false; id?: null; key?: null }
|
||||
)
|
||||
> => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const id = searchParams.get("id");
|
||||
const jsonBackendMatch = window.location.hash.match(
|
||||
/^#json=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/,
|
||||
);
|
||||
const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/);
|
||||
|
||||
const localDataState = importFromLocalStorage();
|
||||
|
||||
let scene: RestoredDataState & {
|
||||
scrollToContent?: boolean;
|
||||
} = await loadScene(null, null, localDataState);
|
||||
|
||||
let roomLinkData = getCollaborationLinkData(window.location.href);
|
||||
const isExternalScene = !!(id || jsonBackendMatch || roomLinkData);
|
||||
if (isExternalScene) {
|
||||
if (
|
||||
// don't prompt if scene is empty
|
||||
!scene.elements.length ||
|
||||
// don't prompt for collab scenes because we don't override local storage
|
||||
roomLinkData ||
|
||||
// otherwise, prompt whether user wants to override current scene
|
||||
(await openConfirmModal(shareableLinkConfirmDialog))
|
||||
) {
|
||||
if (jsonBackendMatch) {
|
||||
scene = await loadScene(
|
||||
jsonBackendMatch[1],
|
||||
jsonBackendMatch[2],
|
||||
localDataState,
|
||||
);
|
||||
}
|
||||
scene.scrollToContent = true;
|
||||
if (!roomLinkData) {
|
||||
window.history.replaceState({}, APP_NAME, window.location.origin);
|
||||
}
|
||||
} else {
|
||||
// https://github.com/excalidraw/excalidraw/issues/1919
|
||||
if (document.hidden) {
|
||||
return new Promise((resolve, reject) => {
|
||||
window.addEventListener(
|
||||
"focus",
|
||||
() => initializeScene(opts).then(resolve).catch(reject),
|
||||
{
|
||||
once: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
roomLinkData = null;
|
||||
window.history.replaceState({}, APP_NAME, window.location.origin);
|
||||
}
|
||||
} else if (externalUrlMatch) {
|
||||
window.history.replaceState({}, APP_NAME, window.location.origin);
|
||||
|
||||
const url = externalUrlMatch[1];
|
||||
try {
|
||||
const request = await fetch(window.decodeURIComponent(url));
|
||||
const data = await loadFromBlob(await request.blob(), null, null);
|
||||
if (
|
||||
!scene.elements.length ||
|
||||
(await openConfirmModal(shareableLinkConfirmDialog))
|
||||
) {
|
||||
return { scene: data, isExternalScene };
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
scene: {
|
||||
appState: {
|
||||
errorMessage: t("alerts.invalidSceneUrl"),
|
||||
},
|
||||
},
|
||||
isExternalScene,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (roomLinkData && opts.collabAPI) {
|
||||
const { excalidrawAPI } = opts;
|
||||
|
||||
const scene = await opts.collabAPI.startCollaboration(roomLinkData);
|
||||
|
||||
return {
|
||||
// when collaborating, the state may have already been updated at this
|
||||
// point (we may have received updates from other clients), so reconcile
|
||||
// elements and appState with existing state
|
||||
scene: {
|
||||
...scene,
|
||||
appState: {
|
||||
...restoreAppState(
|
||||
{
|
||||
...scene?.appState,
|
||||
theme: localDataState?.appState?.theme || scene?.appState?.theme,
|
||||
},
|
||||
excalidrawAPI.getAppState(),
|
||||
),
|
||||
// necessary if we're invoking from a hashchange handler which doesn't
|
||||
// go through App.initializeScene() that resets this flag
|
||||
isLoading: false,
|
||||
},
|
||||
elements: reconcileElements(
|
||||
scene?.elements || [],
|
||||
excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||
excalidrawAPI.getAppState(),
|
||||
),
|
||||
},
|
||||
isExternalScene: true,
|
||||
id: roomLinkData.roomId,
|
||||
key: roomLinkData.roomKey,
|
||||
};
|
||||
} else if (scene) {
|
||||
return isExternalScene && jsonBackendMatch
|
||||
? {
|
||||
scene,
|
||||
isExternalScene,
|
||||
id: jsonBackendMatch[1],
|
||||
key: jsonBackendMatch[2],
|
||||
}
|
||||
: { scene, isExternalScene: false };
|
||||
}
|
||||
return { scene: null, isExternalScene: false };
|
||||
};
|
||||
|
||||
const detectedLangCode = languageDetector.detect() || defaultLang.code;
|
||||
export const appLangCodeAtom = atom(
|
||||
Array.isArray(detectedLangCode) ? detectedLangCode[0] : detectedLangCode,
|
||||
import "../excalidraw-app/sentry";
|
||||
window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA;
|
||||
const rootElement = document.getElementById("root")!;
|
||||
const root = createRoot(rootElement);
|
||||
registerSW();
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<ExcalidrawApp />
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
const ExcalidrawWrapper = () => {
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
|
||||
const isCollabDisabled = isRunningInIframe();
|
||||
|
||||
// initial state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const initialStatePromiseRef = useRef<{
|
||||
promise: ResolvablePromise<ExcalidrawInitialDataState | null>;
|
||||
}>({ promise: null! });
|
||||
if (!initialStatePromiseRef.current.promise) {
|
||||
initialStatePromiseRef.current.promise =
|
||||
resolvablePromise<ExcalidrawInitialDataState | null>();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
trackEvent("load", "frame", getFrame());
|
||||
// Delayed so that the app has a time to load the latest SW
|
||||
setTimeout(() => {
|
||||
trackEvent("load", "version", getVersion());
|
||||
}, VERSION_TIMEOUT);
|
||||
}, []);
|
||||
|
||||
const [excalidrawAPI, excalidrawRefCallback] =
|
||||
useCallbackRefState<ExcalidrawImperativeAPI>();
|
||||
|
||||
const [collabAPI] = useAtom(collabAPIAtom);
|
||||
const [, setCollabDialogShown] = useAtom(collabDialogShownAtom);
|
||||
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
|
||||
return isCollaborationLink(window.location.href);
|
||||
});
|
||||
|
||||
useHandleLibrary({
|
||||
excalidrawAPI,
|
||||
getInitialLibraryItems: getLibraryItemsFromStorage,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadImages = (
|
||||
data: ResolutionType<typeof initializeScene>,
|
||||
isInitialLoad = false,
|
||||
) => {
|
||||
if (!data.scene) {
|
||||
return;
|
||||
}
|
||||
if (collabAPI?.isCollaborating()) {
|
||||
if (data.scene.elements) {
|
||||
collabAPI
|
||||
.fetchImageFilesFromFirebase({
|
||||
elements: data.scene.elements,
|
||||
forceFetchFiles: true,
|
||||
})
|
||||
.then(({ loadedFiles, erroredFiles }) => {
|
||||
excalidrawAPI.addFiles(loadedFiles);
|
||||
updateStaleImageStatuses({
|
||||
excalidrawAPI,
|
||||
erroredFiles,
|
||||
elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const fileIds =
|
||||
data.scene.elements?.reduce((acc, element) => {
|
||||
if (isInitializedImageElement(element)) {
|
||||
return acc.concat(element.fileId);
|
||||
}
|
||||
return acc;
|
||||
}, [] as FileId[]) || [];
|
||||
|
||||
if (data.isExternalScene) {
|
||||
loadFilesFromFirebase(
|
||||
`${FIREBASE_STORAGE_PREFIXES.shareLinkFiles}/${data.id}`,
|
||||
data.key,
|
||||
fileIds,
|
||||
).then(({ loadedFiles, erroredFiles }) => {
|
||||
excalidrawAPI.addFiles(loadedFiles);
|
||||
updateStaleImageStatuses({
|
||||
excalidrawAPI,
|
||||
erroredFiles,
|
||||
elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||
});
|
||||
});
|
||||
} else if (isInitialLoad) {
|
||||
if (fileIds.length) {
|
||||
LocalData.fileStorage
|
||||
.getFiles(fileIds)
|
||||
.then(({ loadedFiles, erroredFiles }) => {
|
||||
if (loadedFiles.length) {
|
||||
excalidrawAPI.addFiles(loadedFiles);
|
||||
}
|
||||
updateStaleImageStatuses({
|
||||
excalidrawAPI,
|
||||
erroredFiles,
|
||||
elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||
});
|
||||
});
|
||||
}
|
||||
// on fresh load, clear unused files from IDB (from previous
|
||||
// session)
|
||||
LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => {
|
||||
loadImages(data, /* isInitialLoad */ true);
|
||||
initialStatePromiseRef.current.promise.resolve(data.scene);
|
||||
});
|
||||
|
||||
const onHashChange = async (event: HashChangeEvent) => {
|
||||
event.preventDefault();
|
||||
const libraryUrlTokens = parseLibraryTokensFromUrl();
|
||||
if (!libraryUrlTokens) {
|
||||
if (
|
||||
collabAPI?.isCollaborating() &&
|
||||
!isCollaborationLink(window.location.href)
|
||||
) {
|
||||
collabAPI.stopCollaboration(false);
|
||||
}
|
||||
excalidrawAPI.updateScene({ appState: { isLoading: true } });
|
||||
|
||||
initializeScene({ collabAPI, excalidrawAPI }).then((data) => {
|
||||
loadImages(data);
|
||||
if (data.scene) {
|
||||
excalidrawAPI.updateScene({
|
||||
...data.scene,
|
||||
...restore(data.scene, null, null, { repairBindings: true }),
|
||||
commitToHistory: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const titleTimeout = setTimeout(
|
||||
() => (document.title = APP_NAME),
|
||||
TITLE_TIMEOUT,
|
||||
);
|
||||
|
||||
const syncData = debounce(() => {
|
||||
if (isTestEnv()) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!document.hidden &&
|
||||
((collabAPI && !collabAPI.isCollaborating()) || isCollabDisabled)
|
||||
) {
|
||||
// don't sync if local state is newer or identical to browser state
|
||||
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
|
||||
const localDataState = importFromLocalStorage();
|
||||
const username = importUsernameFromLocalStorage();
|
||||
let langCode = languageDetector.detect() || defaultLang.code;
|
||||
if (Array.isArray(langCode)) {
|
||||
langCode = langCode[0];
|
||||
}
|
||||
setLangCode(langCode);
|
||||
excalidrawAPI.updateScene({
|
||||
...localDataState,
|
||||
});
|
||||
excalidrawAPI.updateLibrary({
|
||||
libraryItems: getLibraryItemsFromStorage(),
|
||||
});
|
||||
collabAPI?.setUsername(username || "");
|
||||
}
|
||||
|
||||
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) {
|
||||
const elements = excalidrawAPI.getSceneElementsIncludingDeleted();
|
||||
const currFiles = excalidrawAPI.getFiles();
|
||||
const fileIds =
|
||||
elements?.reduce((acc, element) => {
|
||||
if (
|
||||
isInitializedImageElement(element) &&
|
||||
// only load and update images that aren't already loaded
|
||||
!currFiles[element.fileId]
|
||||
) {
|
||||
return acc.concat(element.fileId);
|
||||
}
|
||||
return acc;
|
||||
}, [] as FileId[]) || [];
|
||||
if (fileIds.length) {
|
||||
LocalData.fileStorage
|
||||
.getFiles(fileIds)
|
||||
.then(({ loadedFiles, erroredFiles }) => {
|
||||
if (loadedFiles.length) {
|
||||
excalidrawAPI.addFiles(loadedFiles);
|
||||
}
|
||||
updateStaleImageStatuses({
|
||||
excalidrawAPI,
|
||||
erroredFiles,
|
||||
elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, SYNC_BROWSER_TABS_TIMEOUT);
|
||||
|
||||
const onUnload = () => {
|
||||
LocalData.flushSave();
|
||||
};
|
||||
|
||||
const visibilityChange = (event: FocusEvent | Event) => {
|
||||
if (event.type === EVENT.BLUR || document.hidden) {
|
||||
LocalData.flushSave();
|
||||
}
|
||||
if (
|
||||
event.type === EVENT.VISIBILITY_CHANGE ||
|
||||
event.type === EVENT.FOCUS
|
||||
) {
|
||||
syncData();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener(EVENT.HASHCHANGE, onHashChange, false);
|
||||
window.addEventListener(EVENT.UNLOAD, onUnload, false);
|
||||
window.addEventListener(EVENT.BLUR, visibilityChange, false);
|
||||
document.addEventListener(EVENT.VISIBILITY_CHANGE, visibilityChange, false);
|
||||
window.addEventListener(EVENT.FOCUS, visibilityChange, false);
|
||||
return () => {
|
||||
window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false);
|
||||
window.removeEventListener(EVENT.UNLOAD, onUnload, false);
|
||||
window.removeEventListener(EVENT.BLUR, visibilityChange, false);
|
||||
window.removeEventListener(EVENT.FOCUS, visibilityChange, false);
|
||||
document.removeEventListener(
|
||||
EVENT.VISIBILITY_CHANGE,
|
||||
visibilityChange,
|
||||
false,
|
||||
);
|
||||
clearTimeout(titleTimeout);
|
||||
};
|
||||
}, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode]);
|
||||
|
||||
useEffect(() => {
|
||||
const unloadHandler = (event: BeforeUnloadEvent) => {
|
||||
LocalData.flushSave();
|
||||
|
||||
if (
|
||||
excalidrawAPI &&
|
||||
LocalData.fileStorage.shouldPreventUnload(
|
||||
excalidrawAPI.getSceneElements(),
|
||||
)
|
||||
) {
|
||||
preventUnload(event);
|
||||
}
|
||||
};
|
||||
window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
|
||||
return () => {
|
||||
window.removeEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
|
||||
};
|
||||
}, [excalidrawAPI]);
|
||||
|
||||
useEffect(() => {
|
||||
languageDetector.cacheUserLanguage(langCode);
|
||||
}, [langCode]);
|
||||
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() =>
|
||||
(localStorage.getItem(
|
||||
STORAGE_KEYS.LOCAL_STORAGE_THEME,
|
||||
) as Theme | null) ||
|
||||
// FIXME migration from old LS scheme. Can be removed later. #5660
|
||||
importFromLocalStorage().appState?.theme ||
|
||||
THEME.LIGHT,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_THEME, theme);
|
||||
// currently only used for body styling during init (see public/index.html),
|
||||
// but may change in the future
|
||||
document.documentElement.classList.toggle("dark", theme === THEME.DARK);
|
||||
}, [theme]);
|
||||
|
||||
const onChange = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
files: BinaryFiles,
|
||||
) => {
|
||||
if (collabAPI?.isCollaborating()) {
|
||||
collabAPI.syncElements(elements);
|
||||
}
|
||||
|
||||
setTheme(appState.theme);
|
||||
|
||||
// this check is redundant, but since this is a hot path, it's best
|
||||
// not to evaludate the nested expression every time
|
||||
if (!LocalData.isSavePaused()) {
|
||||
LocalData.save(elements, appState, files, () => {
|
||||
if (excalidrawAPI) {
|
||||
let didChange = false;
|
||||
|
||||
const elements = excalidrawAPI
|
||||
.getSceneElementsIncludingDeleted()
|
||||
.map((element) => {
|
||||
if (
|
||||
LocalData.fileStorage.shouldUpdateImageElementStatus(element)
|
||||
) {
|
||||
const newElement = newElementWith(element, { status: "saved" });
|
||||
if (newElement !== element) {
|
||||
didChange = true;
|
||||
}
|
||||
return newElement;
|
||||
}
|
||||
return element;
|
||||
});
|
||||
|
||||
if (didChange) {
|
||||
excalidrawAPI.updateScene({
|
||||
elements,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const [latestShareableLink, setLatestShareableLink] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const onExportToBackend = async (
|
||||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: Partial<AppState>,
|
||||
files: BinaryFiles,
|
||||
canvas: HTMLCanvasElement,
|
||||
) => {
|
||||
if (exportedElements.length === 0) {
|
||||
throw new Error(t("alerts.cannotExportEmptyCanvas"));
|
||||
}
|
||||
if (canvas) {
|
||||
try {
|
||||
const { url, errorMessage } = await exportToBackend(
|
||||
exportedElements,
|
||||
{
|
||||
...appState,
|
||||
viewBackgroundColor: appState.exportBackground
|
||||
? appState.viewBackgroundColor
|
||||
: getDefaultAppState().viewBackgroundColor,
|
||||
},
|
||||
files,
|
||||
);
|
||||
|
||||
if (errorMessage) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (url) {
|
||||
setLatestShareableLink(url);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.name !== "AbortError") {
|
||||
const { width, height } = canvas;
|
||||
console.error(error, { width, height });
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderCustomStats = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: UIAppState,
|
||||
) => {
|
||||
return (
|
||||
<CustomStats
|
||||
setToast={(message) => excalidrawAPI!.setToast({ message })}
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const onLibraryChange = async (items: LibraryItems) => {
|
||||
if (!items.length) {
|
||||
localStorage.removeItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
|
||||
return;
|
||||
}
|
||||
const serializedItems = JSON.stringify(items);
|
||||
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
|
||||
};
|
||||
|
||||
const isOffline = useAtomValue(isOfflineAtom);
|
||||
|
||||
// browsers generally prevent infinite self-embedding, there are
|
||||
// cases where it still happens, and while we disallow self-embedding
|
||||
// by not whitelisting our own origin, this serves as an additional guard
|
||||
if (isSelfEmbedding) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
textAlign: "center",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<h1>I'm not a pretzel!</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ height: "100%" }}
|
||||
className={clsx("excalidraw-app", {
|
||||
"is-collaborating": isCollaborating,
|
||||
})}
|
||||
>
|
||||
<Excalidraw
|
||||
excalidrawAPI={excalidrawRefCallback}
|
||||
onChange={onChange}
|
||||
initialData={initialStatePromiseRef.current.promise}
|
||||
isCollaborating={isCollaborating}
|
||||
onPointerUpdate={collabAPI?.onPointerUpdate}
|
||||
UIOptions={{
|
||||
canvasActions: {
|
||||
toggleTheme: true,
|
||||
export: {
|
||||
onExportToBackend,
|
||||
renderCustomUI: (elements, appState, files) => {
|
||||
return (
|
||||
<ExportToExcalidrawPlus
|
||||
elements={elements}
|
||||
appState={appState}
|
||||
files={files}
|
||||
onError={(error) => {
|
||||
excalidrawAPI?.updateScene({
|
||||
appState: {
|
||||
errorMessage: error.message,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onSuccess={() => {
|
||||
excalidrawAPI?.updateScene({
|
||||
appState: { openDialog: null },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
langCode={langCode}
|
||||
renderCustomStats={renderCustomStats}
|
||||
detectScroll={false}
|
||||
handleKeyboardGlobally={true}
|
||||
onLibraryChange={onLibraryChange}
|
||||
autoFocus={true}
|
||||
theme={theme}
|
||||
renderTopRightUI={(isMobile) => {
|
||||
if (isMobile || !collabAPI || isCollabDisabled) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<LiveCollaborationTrigger
|
||||
isCollaborating={isCollaborating}
|
||||
onSelect={() => setCollabDialogShown(true)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
>
|
||||
<AppMainMenu
|
||||
setCollabDialogShown={setCollabDialogShown}
|
||||
isCollaborating={isCollaborating}
|
||||
isCollabEnabled={!isCollabDisabled}
|
||||
/>
|
||||
<AppWelcomeScreen
|
||||
setCollabDialogShown={setCollabDialogShown}
|
||||
isCollabEnabled={!isCollabDisabled}
|
||||
/>
|
||||
<OverwriteConfirmDialog>
|
||||
<OverwriteConfirmDialog.Actions.ExportToImage />
|
||||
<OverwriteConfirmDialog.Actions.SaveToDisk />
|
||||
{excalidrawAPI && (
|
||||
<OverwriteConfirmDialog.Action
|
||||
title={t("overwriteConfirm.action.excalidrawPlus.title")}
|
||||
actionLabel={t("overwriteConfirm.action.excalidrawPlus.button")}
|
||||
onClick={() => {
|
||||
exportToExcalidrawPlus(
|
||||
excalidrawAPI.getSceneElements(),
|
||||
excalidrawAPI.getAppState(),
|
||||
excalidrawAPI.getFiles(),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t("overwriteConfirm.action.excalidrawPlus.description")}
|
||||
</OverwriteConfirmDialog.Action>
|
||||
)}
|
||||
</OverwriteConfirmDialog>
|
||||
<AppFooter />
|
||||
{isCollaborating && isOffline && (
|
||||
<div className="collab-offline-warning">
|
||||
{t("alerts.collabOfflineWarning")}
|
||||
</div>
|
||||
)}
|
||||
{latestShareableLink && (
|
||||
<ShareableLinkDialog
|
||||
link={latestShareableLink}
|
||||
onCloseRequest={() => setLatestShareableLink(null)}
|
||||
setErrorMessage={setErrorMessage}
|
||||
/>
|
||||
)}
|
||||
{excalidrawAPI && !isCollabDisabled && (
|
||||
<Collab excalidrawAPI={excalidrawAPI} />
|
||||
)}
|
||||
{errorMessage && (
|
||||
<ErrorDialog onClose={() => setErrorMessage("")}>
|
||||
{errorMessage}
|
||||
</ErrorDialog>
|
||||
)}
|
||||
</Excalidraw>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ExcalidrawApp = () => {
|
||||
return (
|
||||
<TopErrorBoundary>
|
||||
<Provider unstable_createStore={() => appJotaiStore}>
|
||||
<ExcalidrawWrapper />
|
||||
</Provider>
|
||||
</TopErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExcalidrawApp;
|
||||
|
42
excalidraw-app/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "excalidraw-app",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"homepage": ".",
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all",
|
||||
"not safari < 12",
|
||||
"not kaios <= 2.5",
|
||||
"not edge < 79",
|
||||
"not chrome < 70",
|
||||
"not and_uc < 13",
|
||||
"not samsung < 10"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"vite-plugin-html": "3.2.2"
|
||||
},
|
||||
"prettier": "@excalidraw/prettier-config",
|
||||
"scripts": {
|
||||
"build-node": "node ./scripts/build-node.js",
|
||||
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true VITE_APP_DISABLE_TRACKING=true vite build",
|
||||
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
|
||||
"build:version": "node ../scripts/build-version.js",
|
||||
"build": "yarn build:app && yarn build:version",
|
||||
"start": "yarn && vite",
|
||||
"start:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
|
||||
"build:preview": "yarn build && vite preview --port 5000"
|
||||
}
|
||||
}
|