Compare commits

...

156 Commits

Author SHA1 Message Date
4ed3a2e7be lint 2023-06-07 15:30:57 +05:30
faec098e30 fix offset in editor 2023-06-07 15:27:39 +05:30
65e849804d Merge remote-tracking branch 'origin/master' into aakansha-no-trailing-space-render-text-container 2023-06-06 15:07:42 +05:30
ae7ff76126 fix: cleanup textWysiwyg and getAdjustedDimensions (#6520)
* fix: cleanup textWysiwyg and getAdjustedDimensions

* fix lint

* fix test
2023-06-06 14:36:18 +05:30
7c0f783cbc Merge remote-tracking branch 'origin/master' into aakansha-no-trailing-space-render-text-container 2023-06-06 13:27:27 +05:30
952aa63f86 chore: Update translations from Crowdin (#6625) 2023-06-02 17:41:05 +02:00
a065ec67a9 fix: eye dropper not accounting for offsets (#6640) 2023-06-02 17:35:25 +02:00
079aa72475 feat: eye dropper (#6615) 2023-06-02 17:06:11 +02:00
644685a5a8 fix: color picker input closing problem (#6599) 2023-06-01 23:17:22 +02:00
Are
7bf4de5892 feat: redesign of Live Collaboration dialog (#6635)
* feat: redesiged Live Collaboration dialog

* fix: address lints

* fix: inactive dialog dark mode improvements

* fix: follow styleguide with event parameter, add FilledButton size prop

* fix: change timer to be imperative

* fix: add spacing after emoji

* fix: remove unused useEffect

* fix: change margin into whitespace

* fix: add share button check back
2023-05-31 18:27:29 +02:00
253c5c7866 perf: memoize rendering of library (#6622)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-05-31 15:37:13 +02:00
82d8d02697 test: Add coverage script (#6634)
Add coverage script
2023-05-31 17:30:14 +05:30
1e3c94a37a feat: recover scrolled position after Library re-opening (#6624)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-05-31 10:22:02 +02:00
a91e401554 feat: clearing library cache (#6621)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-05-29 16:01:44 +02:00
Are
08563e7d7b feat: update design of ImageExportDialog (#6614)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-05-26 16:16:55 +02:00
6459ccda6a feat: add flipping for multiple elements (#5578)
* feat: add flipping when resizing multiple elements

* fix: image elements not flipping its content

* test: fix accidental resizing in grouping test

* fix: angles not flipping vertically when resizing

* feat: add flipping multiple elements with a command

* revert: image elements not flipping its content

This reverts commit cb989a6c66e62a02a8c04ce41f12507806c8d0a0.

* fix: add special cases for flipping text & images

* fix: a few corner cases for flipping

* fix: remove angle flip

* fix: bound text scaling when resizing

* fix: linear elements drifting away after multiple flips

* revert: fix linear elements drifting away after multiple flips

This reverts commit bffc33dd3f.

* fix: linear elements unstable bounds

* revert: linear elements unstable bounds

This reverts commit 22ae9b02c4.

* fix: hand-drawn lines shift after flipping

* test: fix flipping tests

* test: fix the number of context menu items

* fix: incorrect scaling due to ignoring bound text when finding selection bounds

* fix: bound text coordinates not being updated

* fix: lines bound text rotation

* fix: incorrect placement of bound lines on flip

* remove redundant predicates in actionFlip

* update test

* refactor resizeElement with some renaming and comments

* fix grouped bounded text elements not being flipped correctly

* combine mutation for bounded text element

* remove incorrect return

* fix: linear elements bindings after flipping

* revert: remove incorrect return

This reverts commit e6b205ca90.

* fix: minimum size for all elements in selection

---------

Co-authored-by: Ryan Di <ryan.weihao.di@gmail.com>
2023-05-25 16:27:41 +02:00
75bea48b54 fix: export dialog shortcut toggles console on firefox (#6620) 2023-05-24 22:52:21 +02:00
13780f390a fix: add react v17 useTransition polyfill (#6618) 2023-05-24 15:24:54 +00:00
fecbde3f5c chore: Update translations from Crowdin (#6598)
* New translations en.json (Greek)

* New translations en.json (Slovenian)

* New translations en.json (Portuguese, Brazilian)

* Auto commit: Calculate translation coverage

* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (Korean)

* New translations en.json (Chinese Traditional)

* Auto commit: Calculate translation coverage

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (Indonesian)

* New translations en.json (Indonesian)

* Auto commit: Calculate translation coverage

* New translations en.json (Indonesian)

* Auto commit: Calculate translation coverage

* New translations en.json (Indonesian)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* Auto commit: Calculate translation coverage

* New translations en.json (Russian)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovak)

* Auto commit: Calculate translation coverage

* New translations en.json (Italian)

* Auto commit: Calculate translation coverage

* New translations en.json (Italian)

* Auto commit: Calculate translation coverage

* New translations en.json (Kurdish)

* New translations en.json (Swedish)

* Auto commit: Calculate translation coverage

* New translations en.json (Portuguese)

* Auto commit: Calculate translation coverage

* New translations en.json (Portuguese)

* Auto commit: Calculate translation coverage

* New translations en.json (Khmer)

* New translations en.json (Khmer)

* Auto commit: Calculate translation coverage

* New translations en.json (Khmer)

* Auto commit: Calculate translation coverage

* New translations en.json (Khmer)

* Auto commit: Calculate translation coverage

* New translations en.json (Khmer)

* Auto commit: Calculate translation coverage

* New translations en.json (Khmer)

* Auto commit: Calculate translation coverage

* New translations en.json (Khmer)

* New translations en.json (Khmer)

* Auto commit: Calculate translation coverage

* New translations en.json (Khmer)

* Auto commit: Calculate translation coverage

* New translations en.json (Khmer)

* Auto commit: Calculate translation coverage
2023-05-24 16:50:11 +02:00
7340c70a06 perf: improve rendering performance for Library (#6587)
* perf: improve rendering performance for Library

* fix: return onDrag and onToggle functionality to Library Items

* perf: cache exportToSvg output

* fix: lint warning

* fix: add onClick handler into LibraryUnit

* feat: better spinner

* fix: useCallback for getInsertedElements to fix linter error

* feat: different batch size when svgs are cached

* fix: library items alignment in row

* feat: skeleton instead of spinner

* fix: remove unused variables

* feat: use css vars instead of hadcoded colors

* feat: reverting skeleton, removing spinner

* cleanup and unrelated refactor

* change ROWS_RENDERED_PER_BATCH to 6

---------

Co-authored-by: dwelle <luzar.david@gmail.com>
2023-05-24 14:40:20 +00:00
a4f05339aa fix: Library dropdown visibility issue for mobile (#6613)
Fix: Library dropdown visibility issue for mobile

Co-authored-by: Rounik Prashar <rounik.prashar@increff.com>
2023-05-23 22:37:19 +02:00
a8f0a14610 fix: withInternalFallback leaking state in multi-instance scenarios (#6602) 2023-05-19 15:47:01 +02:00
a89952e32f chore: Update translations from Crowdin (#6589) 2023-05-18 16:23:09 +02:00
5b7596582f feat: color picker redesign (#6216)
Co-authored-by: Maielo <maielo.mv@gmail.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2023-05-18 16:06:27 +02:00
6977c32631 style: Removes extra spaces (#6558)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-05-13 21:09:16 +00:00
f6f9ed0396 refactor: simplify ImageExportDialog (#6578) 2023-05-13 22:58:35 +02:00
b1b325b9a7 feat: add "unlock all elements" to canvas contextMenu (#5894) 2023-05-13 22:52:03 +02:00
5bf27a463c fix: language list containing duplicate en lang (#6583) 2023-05-13 22:50:14 +02:00
306e133651 fix: garbled text displayed on avatars (#6575)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-05-13 19:49:09 +02:00
e0f2869374 fix: assign the original text to text editor only during init (#6580) 2023-05-13 19:17:29 +02:00
2c511e30cd chore: Update translations from Crowdin (#6571)
* New translations en.json (Karakalpak)

* New translations en.json (Karakalpak)

* Auto commit: Calculate translation coverage

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* New translations en.json (Spanish)

* New translations en.json (Italian)

* New translations en.json (Slovak)

* New translations en.json (Bengali)

* New translations en.json (Khmer)

* New translations en.json (Karakalpak)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Basque)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Kurdish)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Punjabi)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Galician)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Tamil)

* New translations en.json (Marathi)

* New translations en.json (Thai)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Kazakh)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Burmese)

* New translations en.json (Chinese Traditional, Hong Kong)

* New translations en.json (Sinhala)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Occitan)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* New translations en.json (Spanish)

* New translations en.json (Italian)

* New translations en.json (Slovak)

* New translations en.json (Bengali)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Basque)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Kurdish)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Punjabi)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Galician)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Tamil)

* New translations en.json (Marathi)

* New translations en.json (Thai)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Burmese)

* New translations en.json (Chinese Traditional, Hong Kong)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Occitan)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* New translations en.json (Ukrainian)

* Auto commit: Calculate translation coverage

* New translations en.json (Ukrainian)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* New translations en.json (Hindi)

* New translations en.json (Hindi)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Ukrainian)

* New translations en.json (Khmer)

* New translations en.json (Khmer)

* Auto commit: Calculate translation coverage

* New translations en.json (German)

* New translations en.json (Punjabi)

* Auto commit: Calculate translation coverage

---------

Co-authored-by: dwelle <luzar.david@gmail.com>
2023-05-13 17:16:58 +00:00
fff9d1522a feat: library sidebar design tweaks (#6582) 2023-05-13 13:18:14 +02:00
e619e06055 fix: i18n: Apply Trans component to publish library dialogue (#6564) 2023-05-10 10:39:21 +02:00
d8965ee823 chore: Update translations from Crowdin (#6530)
* New translations en.json (Bengali)

* New translations en.json (Bengali)

* Auto commit: Calculate translation coverage

* New translations en.json (Khmer)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovak)

* Auto commit: Calculate translation coverage

* New translations en.json (Karakalpak)

* Auto commit: Calculate translation coverage

* New translations en.json (Karakalpak)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* New translations en.json (Spanish)

* New translations en.json (Italian)

* New translations en.json (Slovak)

* New translations en.json (Bengali)

* New translations en.json (Khmer)

* New translations en.json (Karakalpak)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Basque)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Kurdish)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Punjabi)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Galician)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Tamil)

* New translations en.json (Marathi)

* New translations en.json (Thai)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Kazakh)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Burmese)

* New translations en.json (Chinese Traditional, Hong Kong)

* New translations en.json (Sinhala)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Occitan)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* New translations en.json (Korean)

* New translations en.json (Russian)

* New translations en.json (Slovenian)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* New translations en.json (German)

* New translations en.json (Chinese Traditional)

* Auto commit: Calculate translation coverage

* New translations en.json (Karakalpak)

* Auto commit: Calculate translation coverage

* New translations en.json (Karakalpak)

* Auto commit: Calculate translation coverage

* New translations en.json (Vietnamese)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* New translations en.json (Spanish)

* New translations en.json (Italian)

* New translations en.json (Slovak)

* New translations en.json (Bengali)

* New translations en.json (Khmer)

* New translations en.json (Karakalpak)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Basque)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Kurdish)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Punjabi)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Galician)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Tamil)

* New translations en.json (Marathi)

* New translations en.json (Thai)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Kazakh)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Burmese)

* New translations en.json (Chinese Traditional, Hong Kong)

* New translations en.json (Sinhala)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Occitan)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Kurdish)

* New translations en.json (Slovenian)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* Auto commit: Calculate translation coverage

* New translations en.json (Russian)

* Auto commit: Calculate translation coverage

* New translations en.json (Korean)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* Auto commit: Calculate translation coverage

* New translations en.json (Swedish)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovak)

* New translations en.json (Persian)

* Auto commit: Calculate translation coverage
2023-05-09 18:15:27 +05:30
560231d365 perf: use UIAppState where possible to reduce UI rerenders (#6560) 2023-05-08 10:14:02 +02:00
026949204d fix: fix brave error i18n string and remove unused (#6561) 2023-05-06 10:36:42 +02:00
1184a8c0e9 feat: Add Trans component for interpolating JSX in translations (#6534)
* feat: add Trans component

* Add comments

* tweak

* Move brave to trans component

* fix test and tweaks

* remove any

* fix

* fix

* comment

* replace render function type

* Use tags for Trans

* Fix a typo

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

* Cleanup, add comments, add support for kebab case

* tweaks

---------

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-05-05 21:35:18 +05:30
e9cae918a7 feat: sidebar tabs support (#6213)
* feat: Sidebar tabs support [wip]

* tab trigger styling tweaks

* add `:hover` & `:active` states

* replace `@dwelle/tunnel-rat` with `tunnel-rat`

* make stuff more explicit

- remove `Sidebar.Header` fallback (host apps need to render manually), and stop tunneling it (render in place)
- make `docked` state explicit
- stop tunneling `Sidebar.TabTriggers` (render in place)

* redesign sidebar / library as per latest spec

* support no label on `Sidebar.Trigger`

* add Sidebar `props.onStateChange`

* style fixes

* make `appState.isSidebarDocked` into a soft user preference

* px -> rem & refactor

* remove `props.renderSidebar`

* update tests

* remove

* refactor

* rename constants

* tab triggers styling fixes

* factor out library-related logic from generic sidebar trigger

* change `props.onClose` to `onToggle`

* rename `props.value` -> `props.tab`

* add displayNames

* allow HTMLAttributes on applicable compos

* fix example App

* more styling tweaks and fixes

* fix not setting `dockable`

* more style fixes

* fix and align sidebar header button styling

* make DefaultSidebar dockable on if host apps supplies `onDock`

* stop `Sidebar.Trigger` hiding label on mobile

this should be only the default sidebar trigger behavior, and for that we don't need to use `device` hook as we handle in CSS

* fix `dockable` prop of defaultSidebar

* remove extra `typescript` dep

* remove `defaultTab` prop

in favor of explicit `tab` value in `<Sidebar.Trigger/>` and `toggleSidebar()`, to reduce API surface area and solve inconsistency of `appState.openSidebar.tab` not reflecting actual UI value if `defaultTab` was supported (without additional syncing logic which feels like the wrong solution).

* remove `onToggle` in favor of `onStateChange`

reducing API surface area

* fix restore

* comment no longer applies

* reuse `Button` component in sidebar buttons

* fix tests

* split Sidebar sub-components into files

* remove `props.dockable` in favor of `props.onDock` only

* split tests

* fix sidebar showing dock button if no `props.docked` supplied & add more tests

* reorder and group sidebar tests

* clarify

* rename classes & dedupe css

* refactor tests

* update changelog

* update changelog

---------

Co-authored-by: barnabasmolnar <barnabas@excalidraw.com>
2023-05-04 17:33:31 +00:00
b1311a407a fix: Revert add version tags to Docker build (#6540)
Revert "build: Add version tags to Docker build (#6508)"

This reverts commit 1815cf3213.
2023-05-02 12:49:11 +05:30
2a39d0b9a7 chore: Update translations from Crowdin (#6471) 2023-04-27 19:27:36 +02:00
6b0218b012 feat: testing simple analytics and fathom analytics for better privacy of the users (#6529)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-04-27 19:11:42 +02:00
45a57d70de fix: don't refresh dimensions for text containers on font load (#6523) 2023-04-26 21:35:06 +05:30
da8dd389a9 fix: cleanup getMaxContainerHeight and getMaxContainerWidth (#6519)
* fix: cleanup getMaxContainerHeight and getMaxContainerWidth

* rename getMaxContainerWidth -> getBoundTextMaxMaxWidth and getMaxContainerHeight -> getBoundTextMaxHeight

* add specs
2023-04-25 18:06:23 +05:30
dae81c0a2c fix: cleanup redrawTextBoundingBox (#6518)
* chore: cleanup redrawTextBoundingBox

* fix
2023-04-25 17:57:53 +05:30
1e9943323a style: fix font family inconsistencies (#6501)
style: font fix for four components

The browser default font was showing up in various locations. Fixed them to show the desired ui font.
2023-04-25 17:20:19 +05:30
1815cf3213 build: Add version tags to Docker build (#6508)
ci: Add version tags
2023-04-25 16:51:25 +05:30
d35386755f feat: retain seed on shift-paste (#6509)
thanks for the review 👍
2023-04-24 10:26:21 +02:00
9d5cfbbfb7 fix: text jumps when editing on Android Chrome (#6503)
* debug logging

* debug

* debugging

* Update textWysiwyg.tsx

* Update textWysiwyg.tsx

* extended debug information

* debug

* debug

* trace

* further debug

* don't drag while editing

* removing all console.logs

* revert all changes to textWysiwyt.tsx

* updated comment
2023-04-22 14:17:13 +02:00
fee760d38c feat: allow avif, jfif, webp, bmp, ico image types (#6500
* feat: allow `avif`, `jfif`, `webp`, `bmp`, `ico` image types

* dedupe for SSOT

* more SSOT
2023-04-21 22:53:49 +02:00
fd379c2897 add comment 2023-04-21 13:20:18 +05:30
97929c07d6 fix tests 2023-04-21 12:20:01 +05:30
ba22646c22 fix 2023-04-20 21:48:51 +05:30
c21fecde40 fix element width 2023-04-20 21:45:59 +05:30
caf0a904db fix: preserve trailing spaces in word wrap 2023-04-20 20:13:01 +05:30
2a4799d8c8 chore: upgrade excalidraw version to 0.15.2 (#6496)
chore: updragde excalidraw version to 0.15.2
2023-04-20 17:40:08 +05:30
c4445c181b docs: release @excalidraw/excalidraw@0.15.2 🎉 (#6495) 2023-04-20 17:34:06 +05:30
d12a9fdd40 build(deps): bump ua-parser-js from 0.7.31 to 0.7.33 in /dev-docs (#6164)
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.31 to 0.7.33.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.31...0.7.33)

---
updated-dependencies:
- dependency-name: ua-parser-js
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-20 17:07:56 +05:30
9368a9ce3e build(deps): bump webpack from 5.75.0 to 5.76.1 (#6357)
Bumps [webpack](https://github.com/webpack/webpack) from 5.75.0 to 5.76.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.75.0...v5.76.1)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-20 17:05:03 +05:30
851b9b7aec fix: rotate the text element when binding to a rotated container (#6477)
* Updated logic to update the bound child angle from the parent

* update angle when generating text element

* add test

* remove

* fix

---------

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2023-04-20 16:34:39 +05:30
5ddb28d378 fix: support breaking words containing hyphen - (#6014)
* fix: support breaking words containing hyphen -

* fix

* add spec

* fix

* fix

* fix

* fix and add spec

* improve code and add more tests
2023-04-20 11:10:46 +05:30
404a79e241 chore: typo (collab) - reconciliation.ts (#6447) 2023-04-19 19:18:03 +02:00
eea30da05a fix: incorrect background fill button active state (#6491) 2023-04-19 16:23:24 +02:00
98a77d7426 chore: show bounding box only when flag is true (#6490) 2023-04-19 17:02:20 +05:30
ff3c2e5a16 docs: fix docs link in readme (#6486)
* docs: fix docs link in readme

* update changelog
2023-04-18 21:52:08 +05:30
b64beaf5ba docs: release @excalidraw/excalidraw@0.15.1 🎉 (#6485) 2023-04-18 21:32:33 +05:30
89304c9f66 fix: add readme back to npm package which was mistakenly removed (#6484)
* fix: remove update readme script from release

* update docs

* remove

* fix
2023-04-18 21:23:47 +05:30
1d0653ce50 docs: update docs for next version (#6251)
* update docs for opts param inr estore utils

* docs: add docs for `useI18n` hook (#6269)

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

* upgrade excal

---------

Co-authored-by: David Luzar <luzar.david@gmail.com>
2023-04-18 21:03:17 +05:30
c9c79646c5 docs: release @excalidraw/excalidraw@0.15.0 🎉 (#6481) 2023-04-18 20:48:30 +05:30
979312f779 fix: center align text when wrapped in container via context menu (#6480)
* rename action to wrapTextInContainer

* fix: center align text when wrapped in container via context menu

* revert translation key

* fix tests
2023-04-18 19:44:14 +05:30
4d0d844e39 feat: constrain export dialog preview size (#6475) 2023-04-18 15:27:51 +02:00
801412bf6b fix: restore original container height when unbinding text which was binded via context menu (#6444)
* fix: restore original container height when unbinding text which was binded via context menu

* remove flag

* comment
2023-04-18 18:50:25 +05:30
21726e22cc fix: mark more props as optional for element (#6448)
* fix: mark more props as optional for element

* fix
2023-04-18 18:42:48 +05:30
c3e8ddaf58 fix: improperly cache-busting on canvas scale instead of zoom (#6473) 2023-04-17 11:41:27 +02:00
f640ddc2aa fix: incorrectly duplicating items on paste/library insert (#6467
* fix: incorrectly duplicating items on paste/library insert

* fix: deduplicate element ids on restore

* tests
2023-04-16 17:22:16 +02:00
e7e54814e7 chore: Update translations from Crowdin (#6290)
* New translations en.json (Occitan)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Slovenian)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (Dutch)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Traditional)

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Galician)

* Auto commit: Calculate translation coverage

* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (Marathi)

* New translations en.json (Hindi)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Basque)

* Auto commit: Calculate translation coverage

* New translations en.json (Swedish)

* Auto commit: Calculate translation coverage

* New translations en.json (Spanish)

* Auto commit: Calculate translation coverage

* New translations en.json (Hebrew)

* Auto commit: Calculate translation coverage

* New translations en.json (Hebrew)

* Auto commit: Calculate translation coverage

* New translations en.json (Hebrew)

* New translations en.json (Chinese Simplified)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovak)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Turkish)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* New translations en.json (Spanish)

* New translations en.json (Catalan)

* New translations en.json (Basque)

* New translations en.json (Finnish)

* New translations en.json (Portuguese)

* New translations en.json (Slovak)

* New translations en.json (Chinese Simplified)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Tamil)

* New translations en.json (Marathi)

* New translations en.json (Hindi)

* New translations en.json (French)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Kurdish)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Punjabi)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Galician)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Bengali)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Kazakh)

* New translations en.json (Latvian)

* New translations en.json (Burmese)

* New translations en.json (Chinese Traditional, Hong Kong)

* New translations en.json (Sinhala)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Occitan)

* New translations en.json (Kabyle)

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* New translations en.json (Greek)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Traditional)

* Auto commit: Calculate translation coverage

* New translations en.json (Portuguese)

* Auto commit: Calculate translation coverage

* New translations en.json (Portuguese)

* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (Galician)

* Auto commit: Calculate translation coverage

* New translations en.json (Galician)

* Auto commit: Calculate translation coverage

* New translations en.json (Hebrew)

* Auto commit: Calculate translation coverage

* New translations en.json (Hebrew)

* New translations en.json (French)

* Auto commit: Calculate translation coverage

* New translations en.json (Indonesian)

* Auto commit: Calculate translation coverage

* New translations en.json (Indonesian)

* Auto commit: Calculate translation coverage

* New translations en.json (Italian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Marathi)

* New translations en.json (Hindi)

* Auto commit: Calculate translation coverage

* New translations en.json (Hindi)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovak)

* Auto commit: Calculate translation coverage

* New translations en.json (Spanish)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Japanese)

* Auto commit: Calculate translation coverage

* New translations en.json (Dutch)

* Auto commit: Calculate translation coverage

* New translations en.json (Dutch)

* Auto commit: Calculate translation coverage

* New translations en.json (Basque)

* Auto commit: Calculate translation coverage

* New translations en.json (Basque)

* Auto commit: Calculate translation coverage

* New translations en.json (Marathi)

* Auto commit: Calculate translation coverage

* New translations en.json (Marathi)

* Auto commit: Calculate translation coverage

* New translations en.json (Marathi)

* New translations en.json (Hindi)

* Auto commit: Calculate translation coverage

* New translations en.json (Polish)

* Auto commit: Calculate translation coverage

* New translations en.json (Polish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* Auto commit: Calculate translation coverage

* New translations en.json (Vietnamese)

* Auto commit: Calculate translation coverage

* New translations en.json (Polish)

* Auto commit: Calculate translation coverage

* New translations en.json (Korean)

* Auto commit: Calculate translation coverage

* New translations en.json (Dutch)

* New translations en.json (Basque)

* New translations en.json (Marathi)

* New translations en.json (Hindi)

* New translations en.json (Polish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Korean)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Kurdish)

* New translations en.json (Lithuanian)

* New translations en.json (Punjabi)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Galician)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Tamil)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Kazakh)

* New translations en.json (Latvian)

* New translations en.json (Burmese)

* New translations en.json (Chinese Traditional, Hong Kong)

* New translations en.json (Sinhala)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Occitan)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovenian)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (Japanese)

* Auto commit: Calculate translation coverage

* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Traditional)

* Auto commit: Calculate translation coverage

* New translations en.json (Marathi)

* New translations en.json (Hindi)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* Auto commit: Calculate translation coverage

* New translations en.json (Italian)

* Auto commit: Calculate translation coverage

* New translations en.json (Turkish)

* Auto commit: Calculate translation coverage
2023-04-16 17:12:37 +02:00
e9064a4a87 fix: library ids cross-contamination on multiple insert (#6466) 2023-04-16 17:09:51 +02:00
034113772d fix: color picker keyboard handling not working (#6464) 2023-04-16 15:33:30 +02:00
d34cd3072f fix: abort freedraw line if second touch is detected (#6440) 2023-04-16 15:33:16 +02:00
e31230f78c refactor: inline SingleLibraryItem into PublishLibrary (#6462
refactor: inline `SingleLibraryItem` into `PublishLibrary` to reduce api surface area
2023-04-16 11:57:13 +02:00
399c92d882 fix: utils leaking Scene state (#6461
* fix: utils leaking Scene state

* remove debug

* doc

* add tests for group duplicating

* tweaks
2023-04-16 11:56:25 +02:00
b0b23353cf fix: split "Edit selected shape" shortcut (#6457)
Co-authored-by: David Luzar <luzar.david@gmail.com>
2023-04-14 21:34:26 +00:00
6164b5273c fix: center align text when bind to container via context menu (#6451) 2023-04-14 15:22:39 +02:00
ca3be2c678 fix: exporting labelled arrows via export utils (#6443)
* fix: exporting labelled arrows via export utils

* add comments

* lint

* update changelog

* fix lint

* initialize scene in the utils so it can be availabe in the helper functions

* fix library rendering

* add comments
2023-04-13 17:19:46 +05:30
13b27afe0f fix: update coords when text unbinded from its container (#6445)
* fix: update coords when text unbinded from its container

* Add specs
2023-04-13 11:45:58 +05:30
372743f59f fix: autoredirect to plus in prod only (#6446) 2023-04-12 10:57:00 +02:00
fc601347cf fix: fixing popover overflow on small screen (#6433)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-04-11 23:23:36 +02:00
e4d8ba226f feat: zigzag fill easter egg (#6439) 2023-04-10 15:38:50 +02:00
ec215362a1 fix: introduce baseline to fix the layout shift when switching to text editor (#6397)
* fix: introduce baseline to fix the layout shift when switching to text editor

* uncomment

* change offset to 8pixels

* [debug]

* introduce DOM baseline in canvas rendering instead

* introduce baseline in element making it backward compat

* fix

* lint

* fix

* update baseline when resizing text element

* fix safari backward compat

* fix for safari

* lint

* reduce safari LS

* floor line height and height when dom height increases than canvas height

* Revert "floor line height and height when dom height increases than canvas height"

This reverts commit 8de6516823.

* cleanup

* use DOM height only for safari to fix LS

* Revert "use DOM height only for safari to fix LS"

This reverts commit d75889238d.

* fix lint and test

* fix

* calculate line height by rounding off instead of DOM

* cleanup

---------

Co-authored-by: dwelle <luzar.david@gmail.com>
2023-04-10 18:52:46 +05:30
0b8fc4f4b6 fix: don't refresh dimensions for deleted text elements (#6438) 2023-04-10 16:31:58 +05:30
c170403b13 docs: fix closing tag in readme (#6424
Update README.md
2023-04-07 17:43:50 +02:00
705ac9c1ab fix: element vanishes when zoomed in (#6417)
Co-authored-by: David Luzar <luzar.david@gmail.com>
2023-04-07 15:30:44 +00:00
68692b9d4c feat: add container to multiple text elements (#6428)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-04-07 13:50:36 +00:00
d61b3cf83d fix: don't jump text to end when out of viewport in safari (#6416) 2023-04-05 12:17:18 +05:30
d2b8f4d2f8 fix: getDefaultLineHeight should return default font family line height for unknown font (#6399)
* fix(getDefaultLineHeight): make getDefaultLineHeight always has a default value

* test: add getDefaultLineHeight test case when using unknown font

* test: add getDefaultLineHeight test case when using unknown font

* Revert "test: add getDefaultLineHeight test case when using unknown font"

This reverts commit d41da5493b6edab9e599a13a23c387d38345bf03.

* test: add getDefaultLineHeight test case when using unknown font

* newline

* newline

* tweaks

* trigger action

* trigger action

* fix

---------

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2023-03-30 00:46:23 +05:30
f8e65bb77e fix: Revert use ideographic textBaseline to improve layout shift when editing text" (#6400)
Revert "fix: use `ideographic` textBaseline to improve layout shift when editing text (#6384)"

This reverts commit 9e52c30ce8.
2023-03-29 18:53:03 +05:30
3030e96d62 feat: starting migration from GA to Matomo for better privacy (#6398)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-03-29 11:13:06 +02:00
44453b725d fix: call stack size exceeded when paste large text (#6373) (#6396)
* fix: call stack size exceeded when paste large text (#6373)

* fix: add test case for paste multi-line text

* fix

* tweak

* add missing assertion

* add comments

* lint

---------

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2023-03-29 13:45:30 +05:30
25bb6738ea feat: Add fitToContent and animate to scrollToContent (#6319)
Co-authored-by: Brice Leroy <brice@brigalabs.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-03-25 23:26:58 +01:00
9e52c30ce8 fix: use ideographic textBaseline to improve layout shift when editing text (#6384) 2023-03-22 18:33:37 +01:00
83383977f5 feat: add line height attribute to text element (#6360)
* feat: add line height attribute to text element

* lint

* update line height when redrawing text bounding box

* fix tests

* retain line height when pasting styles

* fix test

* create a util for calculating ling height using old algo

* update line height when resizing multiple text elements

* make line height backward compatible

* udpate line height for older element when font size updated

* remove logs

* Add specs

* lint

* review fixes

* simplify by changing `lineHeight` from px to unitless

* make param non-optional

* update comment

* fix: jumping text due to font size being calculated incorrectly

* update line height when font family is updated

* lint

* Add spec

* more specs

* rename to getDefaultLineHeight

* fix getting lineHeight for potentially undefined fontFamily

* reduce duplication

* fix fallback

* refactor and comment tweaks

* fix

---------

Co-authored-by: dwelle <luzar.david@gmail.com>
2023-03-22 11:32:38 +05:30
ac4c8b3ca7 fix: chrome crashing when embedding scene on chrome arm (#6383) 2023-03-21 18:48:49 +01:00
5c8941467d fix: division by zero in findFocusPointForEllipse leads to infinite loop in wrapText freezing Excalidraw (#6377)
* Update collision.ts

* Update textElement.ts

* Update textElement.ts

* tweak

* fix

* remove unnecessary `Math.sign`

* change check and add doc

* Add a case for negative max width and specs

* fix

---------

Co-authored-by: dwelle <luzar.david@gmail.com>
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2023-03-20 17:50:09 +05:30
0726911fa6 fix: containerizing text incorrectly updates arrow bindings (#6369)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-03-18 15:00:28 +00:00
7e330c8ee1 build(deps-dev): bump webpack from 5.73.0 to 5.76.0 in /src/packages/utils (#6354)
build(deps-dev): bump webpack in /src/packages/utils

Bumps [webpack](https://github.com/webpack/webpack) from 5.73.0 to 5.76.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.73.0...v5.76.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-15 11:04:13 +05:30
7d21747644 build(deps-dev): bump webpack from 5.73.0 to 5.76.0 in /src/packages/excalidraw (#6355)
build(deps-dev): bump webpack in /src/packages/excalidraw

Bumps [webpack](https://github.com/webpack/webpack) from 5.73.0 to 5.76.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.73.0...v5.76.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-15 11:04:01 +05:30
e718136aea build(deps): bump webpack from 5.74.0 to 5.76.1 in /dev-docs (#6356)
Bumps [webpack](https://github.com/webpack/webpack) from 5.74.0 to 5.76.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.74.0...v5.76.1)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-15 11:03:40 +05:30
fe83e2922d build: move TS and types to devDependencies (#6346) 2023-03-14 12:06:57 +00:00
20edddcd4e fix: ensure export preview is centered (#6337) 2023-03-14 13:03:55 +01:00
f6e8be399e fix: hide text align for labelled arrows (#6339)
* fix: hide text align for labelled arrows

* lintttt

* since we fetch seledcted Elements including the bound text hence this block can be removed

* fix
2023-03-14 17:21:46 +05:30
ab49cad6b1 perf: break early if the line width <= max width of the container (#6347)
* fix: break early if the line width <= max width of the container

* Remove dead code

* remove dead code

* lint

* remove
2023-03-14 17:18:16 +05:30
6aeb18b784 fix: refresh dimensions when elements loaded from shareable link and blob (#6333)
* fix: refresh dimensions when elements loaded from shareable link

* refresh text dimensions when loading from file

* remove log
2023-03-14 17:08:23 +05:30
023313e92f fix: show error message when measureText API breaks in brave (#6336)
* fix: show error message when measureText API breaks in brave

* Add docs

* Add assets

* tweak message

* fix

* tweak message

* add translations

* lint

* fix

* fix

* lint

* lint

* lint please work now

* tweak doc

* fix

* split error component to new file

* add specs

* tweaks

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

* wrap in div with a width of 30rem

* fix spec

* fix

* Fix typo

---------

Co-authored-by: David Luzar <luzar.david@gmail.com>
Co-authored-by: Daniel J. Geiger <1852529+DanielJGeiger@users.noreply.github.com>
2023-03-13 19:46:09 +05:30
1eee488dab feat: add thai lang support (#6314) 2023-03-11 22:15:52 +01:00
dd4c333925 fix: add an offset of 0.5px for text editor in containers (#6328)
* fix: add an offset of 0.5px for text editor in containers

* fix specs and lint
2023-03-09 13:07:36 +05:30
8542c95a7a fix: move utility types out of .d.ts file to fix exported declaration files (#6315) 2023-03-04 19:21:57 +01:00
cef6094d4c fix: more jotai scopes missing (#6313) 2023-03-03 16:19:02 +01:00
3322f0fa6f build(deps): bump @sideway/formula from 3.0.0 to 3.0.1 in /dev-docs (#6309)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-03 14:45:29 +01:00
34a7d48b95 fix: provide HelpButton title prop (#6209)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-03-03 12:50:18 +00:00
5c0b15ce2b fix: respect text align when wrapping in a container (#6310)
* fix: respect text align when wrapping in a container

* fix
2023-03-03 18:07:26 +05:30
9f9666110e chore: Add debug flag to enable text container bounding box (#6296)
* debug: Add debug flag to enable text container bounding box

* newline

* fix
2023-03-03 18:01:55 +05:30
05ffce62ef build(deps): bump dns-packet from 5.3.1 to 5.4.0 in /src/packages/excalidraw (#6305)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-03 13:20:53 +01:00
0f06fa3851 feat: create bound container from text (#6301)
* feat: create container from text

* fix lint and spec

* fix

* round off dims

* ceil

* review fixes

* fix

* Add specs

* fix

* fix z-index and type

* consider group

* consider linear bindings

* lint
2023-03-03 17:40:42 +05:30
1ce933d2f5 fix: compute bounding box correctly for text element when multiple element resizing (#6307) 2023-03-03 17:34:11 +05:30
15655acb5a fix: use jotai scope for editor-specific atoms (#6308) 2023-03-03 11:58:36 +00:00
d5b264c2d2 build(deps): bump http-cache-semantics from 4.1.0 to 4.1.1 in /dev-docs (#6192)
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-01 14:51:36 +05:30
bd4424bbe3 fix: consider arrow for bound text element (#6297)
* fix: consider arrow for bound text element

* add spec
2023-02-28 19:53:30 +05:30
38fc51b4e3 fix: text never goes beyond max width for unbound text elements (#6288) 2023-02-27 16:54:29 +05:30
e1dc748aef fix: svg text baseline (#6285
* fix: svg text baseline

* fix for multiline
2023-02-26 12:51:44 +01:00
0e95e2b386 fix: compute container height from bound text correctly (#6273)
* fix: compute container height from bound text correctly

* fix specs

* Add tests
2023-02-23 17:39:02 +05:30
9659254fd6 feat: improve text measurements in bound containers (#6187)
* feat: move to canvas measureText

* calcualte height with better heuristic

* improve heuristic more

* remove vertical offset as its not needed

* lint

* calculate width of individual char and ceil to calculate width and remove adjustment factor

* push the word if equal to max width

* update height when text overflows for vertical alignment top/bottom

* remove the hack of updating height when line mismatch as its not needed

* remove scroll height and calculate the height instead

* remove unused code

* fix

* remove

* use math.ceil for whole width instead of individual chars

* fix tests

* fix

* fix

* redraw text bounding box instead when font loaded to fix alignment as well

* fix

* fix

* fix

* Add a 0.05px extra only for firefox

* Add spec

* stop taking ceil and increase firefox editor width by 0.05px

* Ad 0.05px in safari too

* lint

* lint

* remove baseline from measureFontSizeFromWH

* don't redraw on font load

* lint

* refactor name and signature
2023-02-23 16:33:10 +05:30
39b96cb011 fix: fit mobile toolbar and make scrollable (#6270)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-02-23 11:24:04 +01:00
04a8c22f39 fix: rerender i18n in host components on lang change (#6224) 2023-02-22 14:01:23 +00:00
e4506be3e8 chore: Update translations from Crowdin (#6191) 2023-02-22 11:23:10 +00:00
1e816e87bf fix: indenting via tab clashing with IME compositor (#6258) 2023-02-22 12:10:29 +01:00
5368ddef74 fix: improve text wrapping inside rhombus and more fixes (#6265)
* fix: improve text wrapping inside rhombus

* Add comments

* specs

* fix: shift resize and multiple element regression for ellipse and rhombus

* use container width for scaling font size

* fix

* fix multiple resize

* lint

* redraw on submit

* redraw only newly pasted elements

* no padding when center

* fix tests

* fix

* dont add padding in rhombus when aligning

* refactor

* fix

* move getMaxContainerHeight and getMaxContainerWidth to textElement.ts

* Add specs
2023-02-22 16:28:12 +05:30
88ff32e9b3 fix: improve text wrapping in ellipse and alignment (#6172)
* fix: improve text wrapping in ellipse

* compute height when font properties updated

* fix alignment

* fix alignment when resizing

* fix

* ad padding

* always compute height when redrawing bounding box and refactor

* lint

* fix specs

* fix

* redraw text bounding box when pasted or refreshed

* fix

* Add specs

* fix

* restore on font load

* add comments
2023-02-21 12:36:43 +05:30
0fcbddda8e docs: Fix outdated link in README.md (#6263) 2023-02-20 09:44:25 +00:00
b9ba407f96 feat: Bind text to container if double clicked on filled shape or stroke (#6250)
* feat: bind text to container when clicked on filled shape or element stroke

* Bind if double clicked on stroke as well

* remove

* specs

* remove

* shuffle

* fix

* back to normal
2023-02-16 20:46:51 +05:30
5acb99777a docs: fix typo (#6252) 2023-02-16 19:45:41 +05:30
b107c9af2a docs: fix next.js example (#6241) 2023-02-15 15:14:15 +01:00
c587b85b4e docs: new readme (#6240)
Co-authored-by: David Luzar <luzar.david@gmail.com>
2023-02-15 14:45:06 +01:00
9686141113 docs: Fixed broken codesandbox link in the dev-docs (#6229)
fixed broken link
2023-02-15 05:31:07 +00:00
0d7ee891e0 feat: Make repair and refreshDimensions configurable in restoreElements (#6238)
* fix: don't repair during reconcilation

* Add opts to restoreElement and enable refreshDimensions and repair via config

* remove

* update changelog

* fix tests

* rename to repairBindings
2023-02-15 10:41:11 +05:30
71fb60394a docs: enable Algolia for search (#6230) 2023-02-13 17:39:11 +05:30
c9d18ecab6 fix: don't allow blank space in collab name (#6211)
* don't allow blank space in collab name

* add spec

* prevent blank
2023-02-09 15:51:49 +05:30
8c1168ef33 refactor: Make the example React app reusable without duplication (#6188) 2023-02-07 12:41:20 +05:30
c3c45a8c37 fix: docker build architecture:linux/amd64 error occur on linux/arm64 instance (#6197)
fix docker build
when in linux/arm64 use docker buildx plugin to build linux/amd64 image, a build error will occur causing the build to break
2023-02-07 11:44:31 +05:30
a8e6028c33 feat: show error message when not connected to internet while collabo… (#6165)
Co-authored-by: dwelle <luzar.david@gmail.com>
Resolves https://github.com/excalidraw/excalidraw/issues/5994
2023-02-04 15:03:39 +01:00
11e2f90ca1 feat: shortcut for clearCanvas confirmDialog (#6114)
Co-authored-by: dwelle <luzar.david@gmail.com>
resolve https://github.com/excalidraw/excalidraw/issues/5818
2023-02-04 13:33:40 +01:00
4db87a0b6a chore: Update translations from Crowdin (#6150) 2023-02-04 10:04:15 +01:00
4414069617 feat: disable canvas smoothing (antialiasing) for right-angled elements (#6186)Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com>
* feat: disable canvas smoothing for text and other types

* disable smoothing for all right-angled elements

* Update src/renderer/renderElement.ts

Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com>

* Update src/renderer/renderElement.ts

Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com>

* fix lint

* always enable smoothing while zooming

---------

Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com>
2023-02-03 17:07:14 +01:00
a9c5bdb878 fix: sort bound text elements to fix text duplication z-index error (#5130)
* fix: sort bound text elements to fix text duplication z-index error

* improve & sort groups & add tests

* fix backtracking and discontiguous groups

---------

Co-authored-by: dwelle <luzar.david@gmail.com>
2023-02-02 16:23:39 +08:00
5a0334f37f fix: hide welcome screen on mobile once user interacts (#6185)
* fix: hide welcome screen on mobile once started drawing

* Add specs
2023-02-02 12:58:45 +05:30
d8a4ca6911 docs: show last updated time and author (#6183)
docs:show last updated time and author
2023-02-01 21:09:23 +05:30
eb9eeefc63 fix: edit link in docs (#6182) 2023-02-01 20:27:31 +05:30
f23cdc47ee docs: migrating dev docs to docusaurus :) (#6073)
* docs: migrating existing docs to docosaraus :)

* log broken links

* lint :p

* fix

* divide the doc into diff categories

* fix

* order sidebars and more

* fix lint

* point to installation

* making docs better :)

* fix

* renaming git

* renaming git

* fix links

* fix

* update readme

* fix

* resolve duplicate url and make /docs as base url

* fix

* move main docs as well

* making docs better

* support mdx

* update og

* fix title

* upgrade docusarus to stable version

* use draculla theme

* fix

* make entire sidebar collapsable

* live editor for footer wohoo

* render excalidraw only on client to fix the prod build

* migrate MainMenu to live editor too :)

* lint :p

* cleanup integration and use live editor and tabs

* fix

* Add welcome screen doc

* Live Collaboration comp docs

* Add collaborator example

* Add example

* add more

* remove isCollaborating

* Rewrite ref and move to sidebar

* change color of links inside pre

* add initial data

* fix lint

* Add styling

* fix lint

* Add example for customizing styles

* fix lint

* fix

* fix lint

* Add link to livecollabtrigger

* fix

* rewrite UIOptions to sidebar

* move initialdata to sidebar

* move render props to sidebar and rewrite renderTopRightUI and renderCustomStats

* rewrite renderSidebar

* update og

* update url for testing

* fix url

* update readme

* fix style

* tweaks

* Add highlight comp to highlight text

* Add bash syntax highlight

* fix

* tweaks

* fix

* rewrite export utilities

* fix restore

* rewrite utils

* move constants to sidebar

* update readme

* add copyright

* fix links style

* Add linkedin

* tweaks

* rename package to @excalidraw/excalidraw

* enable algolia with dummy creds

* tweaks to integration doc

* tweak WelcomeScreen docs to reflect upcoming API changes

* tweak components intro

* tweak nomenclature

* fix admonition

* rename `components` sidebar item and change order of components list

* uncollapse package section in sidebar

* show level 4 haeadings in TOC

* remove algolia

* remove unused assets

* capitalize C

* tweak

* rename components to App

* rename components -> children-components in the routes

* move notable used tools to intro

* update MainMenu docs with `onSelect` preventDefault behavior

* change sidebar label for children components

* use code

* tweak README & docs intro

* tweak package development doc

* make scrollbar gutter stable

* tweak api intro

* add admonition for export utils

* use next

* wip

* wip

* make excalidraw examples use current color theme & prefer system

* fix welcomescreen docs

* use latest temp release

* fix component order

* revert wip changes

* use next

* tweak

* increase height to fix welcome screen hint

* tweak editor height

* update excal version

* wrap Excal with forwardRef to fix refs

* migrate contributing.md

* fix broken links

---------

Co-authored-by: dwelle <luzar.david@gmail.com>
2023-02-01 19:57:54 +05:30
348 changed files with 26074 additions and 14553 deletions

View File

@ -22,3 +22,13 @@ REACT_APP_DEV_ENABLE_SW=
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
FAST_REFRESH=false
# MATOMO
REACT_APP_MATOMO_URL=
REACT_APP_CDN_MATOMO_TRACKER_URL=
REACT_APP_MATOMO_SITE_ID=
#Debug flags
# To enable bounding box for text containers
REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=

View File

@ -12,6 +12,13 @@ REACT_APP_WS_SERVER_URL=
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
# production-only vars
# GOOGLE ANALYTICS
REACT_APP_GOOGLE_ANALYTICS_ID=UA-387204-13
# MATOMO
REACT_APP_MATOMO_URL=https://excalidraw.matomo.cloud/
REACT_APP_CDN_MATOMO_TRACKER_URL=//cdn.matomo.cloud/excalidraw.matomo.cloud/matomo.js
REACT_APP_MATOMO_SITE_ID=1
REACT_APP_PLUS_APP=https://app.excalidraw.com

1
.gitignore vendored
View File

@ -25,3 +25,4 @@ 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
coverage

1
.npmrc
View File

@ -1 +1,2 @@
save-exact=true
legacy-peer-deps=true

View File

@ -1,63 +1,3 @@
# Contributing
## Setup
### Option 1 - Manual
1. Fork and clone the repo
1. Run `yarn` to install dependencies
1. Create a branch for your PR with `git checkout -b your-branch-name`
> To keep `master` branch pointing to remote repository and make pull requests from branches on your fork. To do this, run:
>
> ```sh
> git remote add upstream https://github.com/excalidraw/excalidraw.git
> git fetch upstream
> git branch --set-upstream-to=upstream/master master
> ```
### Option 2 - CodeSandbox
1. Go to https://codesandbox.io/s/github/excalidraw/excalidraw
1. Connect your GitHub account
1. Go to Git tab on left side
1. Tap on `Fork Sandbox`
1. Write your code
1. Commit and PR automatically
## Pull Request Guidelines
Don't worry if you get any of the below wrong, or if you don't know how. We'll gladly help out.
### Title
Make sure the title starts with a semantic prefix:
- **feat**: A new feature
- **fix**: A bug fix
- **docs**: Documentation only changes
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
- **refactor**: A code change that neither fixes a bug nor adds a feature
- **perf**: A code change that improves performance
- **test**: Adding missing tests or correcting existing tests
- **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
- **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
- **chore**: Other changes that don't modify src or test files
- **revert**: Reverts a previous commit
### Changelog
Add a brief description of your pull request to the changelog located here: [`src/packages/excalidraw/CHANGELOG.md`](src/packages/excalidraw/CHANGELOG.md)
Notes:
- Make sure to prepend to the section corresponding with the semantic prefix you selected in the title
- Link to your pull request - this will require updating the CHANGELOG _after_ creating the pull request
### Testing
Once you submit your pull request it will automatically be tested. Be sure to check the results of the test and fix any issues that arise.
It's also a good idea to consider if your change should include additional tests. This is highly recommended for new features or bug-fixes. For example, it's good practice to create a test for each bug you fix which ensures that we don't regress the code in the future.
Finally - always manually test your changes using the convenient staging environment deployed for each pull request. As much as local development attempts to replicate production, there can still be subtle differences in behavior. For larger features consider testing your change in multiple browsers as well.
Head over to the [docs](https://docs.excalidraw.com/docs/introduction/contributing)

View File

@ -3,7 +3,7 @@ FROM node:14-alpine AS build
WORKDIR /opt/node_app
COPY package.json yarn.lock ./
RUN yarn --ignore-optional
RUN yarn --ignore-optional --network-timeout 600000
ARG NODE_ENV=production

301
README.md
View File

@ -1,201 +1,126 @@
<div align="center" style="display:flex;flex-direction:column;">
<a href="https://excalidraw.com">
<img width="540" src="./public/og-image-sm.png" alt="Excalidraw logo: Sketch handrawn like diagrams." />
</a>
<h3>Virtual whiteboard for sketching hand-drawn like diagrams.<br>Collaborative and end-to-end encrypted.</h3>
<p>
<a href="https://twitter.com/Excalidraw">
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+excalidraw&style=social&logo=twitter">
</a>
<a target="_blank" href="https://crowdin.com/project/excalidraw">
<img src="https://badges.crowdin.net/excalidraw/localized.svg">
</a>
</p>
<p>Ask questions or hang out on our <a target="_blank" href="https://discord.gg/UexuTaE">discord.gg/UexuTaE</a>.</p>
<a href="https://excalidraw.com/" target="_blank" rel="noopener">
<picture>
<source media="(prefers-color-scheme: dark)" alt="Excalidraw" srcset="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/github%2FExcalidraw_Github_cover_dark.png" />
<img alt="Excalidraw" src="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/github%2FExcalidraw_Github_cover.png" />
</picture>
</a>
<h4 align="center">
<a href="https://excalidraw.com">Excalidraw Editor</a> |
<a href="https://blog.excalidraw.com">Blog</a> |
<a href="https://docs.excalidraw.com">Documentation</a> |
<a href="https://plus.excalidraw.com">Excalidraw+</a>
</h4>
<div align="center">
<h2>
An open source virtual hand-drawn style whiteboard. </br>
Collaborative and end-to-end encrypted. </br>
<br />
</h2>
</div>
## Try it now
<br />
<p align="center">
<a href="https://github.com/excalidraw/excalidraw/blob/master/LICENSE">
<img alt="Excalidraw is released under the MIT license." src="https://img.shields.io/badge/license-MIT-blue.svg" />
</a>
<a href="https://docs.excalidraw.com/docs/introduction/contributing">
<img alt="PRs welcome!" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" />
</a>
<a href="https://discord.gg/UexuTaE">
<img alt="Chat on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widge=false"/>
</a>
<a href="https://twitter.com/excalidraw">
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/>
</a>
</p>
Go to [excalidraw.com](https://excalidraw.com) to start sketching.
<div align="center">
<figure>
<a href="https://excalidraw.com" target="_blank" rel="noopener">
<img src="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/github%2Fproduct_showcase.png" alt="Product showcase" />
</a>
<figcaption>
<p align="center">
Create beautiful hand-drawn like diagrams, wireframes, or whatever you like.
</p>
</figcaption>
</figure>
</div>
Read the latest news and updates on our [blog](https://blog.excalidraw.com). A good start is to see all the updates of [One Year of Excalidraw](https://blog.excalidraw.com/one-year-of-excalidraw/).
## Features
## Supporting Excalidraw
The Excalidraw editor (npm package) supports:
If you like the project, you can become a sponsor at [Open Collective](https://opencollective.com/excalidraw).
- 💯&nbsp;Free & open-source.
- 🎨&nbsp;Infinite, canvas-based whiteboard.
- ✍️&nbsp;Hand-drawn like style.
- 🌓&nbsp;Dark mode.
- 🏗️&nbsp;Customizable.
- 📷&nbsp;Image support.
- 😀&nbsp;Shape libraries support.
- 👅&nbsp;Localization (i18n) support.
- 🖼️&nbsp;Export to PNG, SVG & clipboard.
- 💾&nbsp;Open format - export drawings as an `.excalidraw` json file.
- ⚒️&nbsp;Wide range of tools - rectangle, circle, diamond, arrow, line, free-draw, eraser...
- ➡️&nbsp;Arrow-binding & labeled arrows.
- 🔙&nbsp;Undo / Redo.
- 🔍&nbsp;Zoom and panning support.
[<img src="https://opencollective.com/excalidraw/tiers/sponsors/0/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/0/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/1/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/1/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/2/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/2/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/3/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/3/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/4/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/4/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/5/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/5/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/6/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/6/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/7/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/7/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/8/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/8/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/9/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/9/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/10/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/10/website)
## Excalidraw.com
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/src/excalidraw-app) is part of this repository as well, and the app features:
- 📡&nbsp;PWA support (works offline).
- 🤼&nbsp;Real-time collaboration.
- 🔒&nbsp;End-to-end encryption.
- 💾&nbsp;Local-first support (autosaves to the browser).
- 🔗&nbsp;Shareable links (export to a readonly link you can share with others).
We'll be adding these features as drop-in plugins for the npm package in the future.
## Quick start
Install the [Excalidraw npm package](https://www.npmjs.com/package/@excalidraw/excalidraw):
```
npm install react react-dom @excalidraw/excalidraw
```
or via yarn
```
yarn add react react-dom @excalidraw/excalidraw
```
Don't forget to check out our [Documentation](https://docs.excalidraw.com)!
## Contributing
- Missing something or found a bug? [Report here](https://github.com/excalidraw/excalidraw/issues).
- Want to contribute? Check out our [contribution guide](https://docs.excalidraw.com/docs/introduction/contributing) or let us know on [Discord](https://discord.gg/UexuTaE).
- Want to help with translations? See the [translation guide](https://docs.excalidraw.com/docs/introduction/contributing#translating).
## Integrations
- [VScode extension](https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor)
- [npm package](https://www.npmjs.com/package/@excalidraw/excalidraw)
## Who's integrating Excalidraw
[Google Cloud](https://googlecloudcheatsheet.withgoogle.com/architecture) • [Meta](https://meta.com/) • [CodeSandbox](https://codesandbox.io/) • [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) • [Replit](https://replit.com/) • [Slite](https://slite.com/) • [Notion](https://notion.so/) • [HackerRank](https://www.hackerrank.com/) • and many others
## Sponsors & support
If you like the project, you can become a sponsor at [Open Collective](https://opencollective.com/excalidraw) or use [Excalidraw+](https://plus.excalidraw.com/).
## Thank you for supporting Excalidraw
[<img src="https://opencollective.com/excalidraw/tiers/sponsors/0/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/0/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/1/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/1/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/2/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/2/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/3/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/3/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/4/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/4/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/5/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/5/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/6/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/6/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/7/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/7/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/8/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/8/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/9/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/9/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/10/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/10/website)
<a href="https://opencollective.com/excalidraw#category-CONTRIBUTE" target="_blank"><img src="https://opencollective.com/excalidraw/tiers/backers.svg?avatarHeight=32"/></a>
Last but not least, we're thankful to these companies for offering their services for free:
[![Vercel](./.github/assets/vercel.svg)](https://vercel.com) [![Sentry](./.github/assets/sentry.svg)](https://sentry.io) [![Crowdin](./.github/assets/crowdin.svg)](https://crowdin.com)
## Who's integrating Excalidraw
[Google Cloud](https://googlecloudcheatsheet.withgoogle.com/architecture) • [Meta](https://meta.com/) • [CodeSandbox](https://codesandbox.io/) • [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) • [Replit](https://replit.com/) • [Slite](https://slite.com/) • [Notion](https://notion.so/) • [HackerRank](https://www.hackerrank.com/) •
## Documentation
### Shortcuts
You can almost do anything with shortcuts. Click on the help icon on the bottom right corner to see them all.
### Curved lines and arrows
Choose line or arrow and click click click instead of drag.
### Charts
You can easily create charts by copy pasting data from Excel or just plain comma separated text.
### Translating
To translate Excalidraw into other languages, please visit [our Crowdin page](https://crowdin.com/project/excalidraw). To add a new language, [open an issue](https://github.com/excalidraw/excalidraw/issues/new) so we can get things set up on our end first.
Translations will be available on the app if they exceed a certain threshold of completion (currently 85%).
### Create a collaboration session manually
In order to create a session manually, you just need to generate a link of this form:
```
https://excalidraw.com/#room=[0-9a-f]{20},[a-zA-Z0-9_-]{22}
```
#### Example
```
https://excalidraw.com/#room=91bd46ae3aa84dff9d20,pfLqgEoY1c2ioq8LmGwsFA
```
The first set of digits is the room. This is visible from the server thats going to dispatch messages to everyone that knows this number.
The second set of digits is the encryption key. The Excalidraw server doesnt know about it. This is what all the participants use to encrypt/decrypt the messages.
> Note: Please ensure that the encryption key is 22 characters long.
## Shape libraries
Find a growing list of libraries containing assets for your drawings at [libraries.excalidraw.com](https://libraries.excalidraw.com).
## Embedding Excalidraw in your App?
Try out [`@excalidraw/excalidraw`](https://www.npmjs.com/package/@excalidraw/excalidraw). This package allows you to easily embed Excalidraw as a React component into your apps.
## Development
### Code Sandbox
- Go to https://codesandbox.io/p/github/excalidraw/excalidraw
- You may need to sign in with GitHub and reload the page
- You can start coding instantly, and even send PRs from there!
### Local Installation
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
#### Requirements
- [Node.js](https://nodejs.org/en/)
- [Yarn](https://yarnpkg.com/getting-started/install) (v1 or v2.4.2+)
- [Git](https://git-scm.com/downloads)
#### Clone the repo
```bash
git clone https://github.com/excalidraw/excalidraw.git
```
#### Install the dependencies
```bash
yarn
```
#### Start the server
```bash
yarn start
```
Now you can open [http://localhost:3000](http://localhost:3000) and start coding in your favorite code editor.
#### Collaboration
For collaboration, you will need to set up [collab server](https://github.com/excalidraw/excalidraw-room) in local.
#### Commands
##### Install the dependencies
```
yarn
```
##### Run the project
```
yarn start
```
##### Reformat all files with Prettier
```
yarn fix
```
##### Run tests
```
yarn test
```
##### Update test snapshots
```
yarn test:update
```
##### Test for formatting with Prettier
```
yarn test:code
```
#### Docker Compose
You can use docker-compose to work on Excalidraw locally if you don't want to setup a Node.js env.
```sh
docker-compose up --build -d
```
### Self-hosting
We publish a Docker image with the Excalidraw client at [excalidraw/excalidraw](https://hub.docker.com/r/excalidraw/excalidraw). You can use it to self-host your own client under your own domain, on Kubernetes, AWS ECS, etc.
```sh
docker build -t excalidraw/excalidraw .
docker run --rm -dit --name excalidraw -p 5000:80 excalidraw/excalidraw:latest
```
The Docker image is free of analytics and other tracking libraries.
**At the moment, self-hosting your own instance doesn't support sharing or collaboration features.**
We are working towards providing a full-fledged solution for self-hosting your own Excalidraw.
## Contributing
Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/excalidraw/issues/new) first to discuss what you would like to change.
## Notable used tools
- [Create React App](https://github.com/facebook/create-react-app)
- [Rough.js](https://roughjs.com)
- [TypeScript](https://www.typescriptlang.org)
- [Vercel](https://vercel.com)
And the main source of inspiration for starting the project is the awesome [Zwibbler](https://zwibbler.com/demo/) app.

View File

@ -0,0 +1,11 @@
---
slug: /@excalidraw/excalidraw/api
---
# API
Currently the **API** is divided into 3 broad categories 👇
- [Props](/docs/@excalidraw/excalidraw/api/props) - The `props` you can pass to the `Excalidraw` component.
- [Children components](/docs/@excalidraw/excalidraw/api/children-components) - Official components you can use to customize the UI.
- [Utils](/docs/@excalidraw/excalidraw/api/utils) - Utilities and helpers you can use to export, restore and more.

View File

@ -0,0 +1,21 @@
---
sidebar_label: Children Components
slug: /@excalidraw/excalidraw/api/children-components
---
# `<Excalidraw/>` children
We expose several components you can render as children of the `<Excalidraw/>` component to customize the UI.
:::info
We have only recently started migrating to this type of component API. Some UI components are still using render props, and some UI customization isn't supported yet (such as the toolbar or the element properties panel). Stay tuned for more updates!
:::
Below are the currently supported components:
- [MainMenu](/docs/@excalidraw/excalidraw/api/children-components/main-menu)
- [WelcomeScreen](/docs/@excalidraw/excalidraw/api/children-components/welcome-screen)
- [Footer](/docs/@excalidraw/excalidraw/api/children-components/footer)
- [LiveCollaborationTrigger](/docs/@excalidraw/excalidraw/api/children-components/live-collaboration-trigger)

View File

@ -0,0 +1,68 @@
# Footer
Earlier we were using `renderFooter` prop to render custom footer which was removed in [#5970](https://github.com/excalidraw/excalidraw/pull/5970). Now you can pass a `Footer` component instead to render the custom UI for footer.
You will need to import the `Footer` component from the package and wrap your component with the Footer component. The `Footer` should a valid React Node.
**Usage**
```jsx live
function App() {
return (
<div style={{ height: "500px"}}>
<Excalidraw>
<Footer>
<button
className="custom-footer"
onClick={() => alert("This is dummy footer")}
>
custom footer
</button>
</Footer>
</Excalidraw>
</div>
);
}
```
This will only for `Desktop` devices.
For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
Open the `Menu` in the below playground and you will see the `custom footer` rendered.
```jsx live noInline
const MobileFooter = ({}) => {
const device = useDevice();
if (device.isMobile) {
return (
<Footer>
<button
className="custom-footer"
style= {{ marginLeft: '20px', height: '2rem'}}
onClick={() => alert("This is custom footer in mobile menu")}
>
custom footer
</button>
</Footer>
);
}
return null;
};
const App = () => (
<div style={{ height: "400px" }}>
<Excalidraw>
<MainMenu>
<MainMenu.Item> Item1 </MainMenu.Item>
<MainMenu.Item> Item 2 </MainMenu.Item>
<MobileFooter />
</MainMenu>
</Excalidraw>
</div>
);
// Need to render when code is span across multiple components
// in Live Code blocks editor
render(<App />);
```

View File

@ -0,0 +1,62 @@
# LiveCollaborationTrigger
If you implement live collaboration support and want to expose the same UI button as on [excalidraw.com](https://excalidraw.com), you can render the `<LiveCollaborationTrigger/>` component using the [renderTopRightUI](/docs/@excalidraw/excalidraw/api/props#rendertoprightui) prop.
You'll need to supply `onSelect()` to handle opening of your collaboration dialog, but the button will display `appState.collaborators` count provided you have supplied it.
| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `onSelect` | `function` | Yes | | Handler called when the user clicks on the button |
| `isCollaborating` | `boolean` | Yes | false | Whether live collaboration session is in effect. Modifies button style. |
```tsx live
function App() {
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
const [isCollaborating, setIsCollaborating] = useState(false);
return (
<div style={{ height: "500px" }}>
<p style={{ fontSize: "16px" }}>
Selecting the checkbox to see the collaborator count
</p>
<label style={{ fontSize: "16px", fontWeight: "bold" }}>
<input
type="checkbox"
checked={isCollaborating}
onChange={() => {
if (!isCollaborating) {
const collaborators = new Map();
collaborators.set("id1", {
username: "Doremon",
avatarUrl: "../../../../img/doremon.png",
});
collaborators.set("id3", {
username: "Pika",
avatarUrl: "../../../../img/pika.jpeg",
});
excalidrawAPI.updateScene({ collaborators });
} else {
excalidrawAPI.updateScene({
collaborators: new Map(),
});
}
setIsCollaborating(!isCollaborating);
}}
/>
Show Collaborators
</label>
<Excalidraw
ref={(api) => setExcalidrawAPI(api)}
renderTopRightUI={() => (
<LiveCollaborationTrigger
isCollaborating={isCollaborating}
onSelect={() => {
window.alert("You clicked on collab button");
setIsCollaborating(true);
}}
/>
)}
></Excalidraw>
</div>
);
}
```

View File

@ -0,0 +1,167 @@
# MainMenu
By default Excalidraw will render the `MainMenu` with default options. If you want to customise the `MainMenu`, you can pass the `MainMenu` component with the list options.
**Usage**
```jsx live
function App() {
return (
<div style={{ height: "300px" }}>
<Excalidraw>
<MainMenu>
<MainMenu.Item onSelect={() => window.alert("Item1")}>
Item1
</MainMenu.Item>
<MainMenu.Item onSelect={() => window.alert("Item2")}>
Item 2
</MainMenu.Item>
</MainMenu>
</Excalidraw>
</div>
);
}
```
### `<MainMenu>`
This is the `MainMenu` component. If you render it, you will need to populate the menu with your own items as we will not render any ourselves at that point.
| Prop | Type | Required | Default | Description |
| --- | --- | :-: | :-: | --- |
| `onSelect` | `function` | No | - | Triggered when any item is selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. |
### MainMenu.Item
To render an item, its recommended to use `MainMenu.Item`.
| Prop | Type | Required | Default | Description |
| --- | --- | :-: | :-: | --- |
| `onSelect` | `function` | Yes | - | Triggered when selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. |
| `children` | `React.ReactNode` | Yes | - | The content of the menu item |
| `icon` | `JSX.Element` | No | - | The icon used in the menu item |
| `shortcut` | `string` | No | - | The shortcut to be shown for the menu item |
### MainMenu.ItemLink
To render an item as a link, its recommended to use `MainMenu.ItemLink`.
**Usage**
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw>
<MainMenu>
<MainMenu.ItemLink href="https://google.com">
Google
</MainMenu.ItemLink>
<MainMenu.ItemLink href="https://excalidraw.com">
Excalidraw
</MainMenu.ItemLink>
</MainMenu>
</Excalidraw>
</div>
);
}
```
| Prop | Type | Required | Default | Description |
| --- | --- | :-: | :-: | --- |
| `onSelect` | `function` | No | - | Triggered when selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. |
| `href` | `string` | Yes | - | The `href` attribute to be added to the `anchor` element. |
| `children` | `React.ReactNode` | Yes | - | The content of the menu item |
| `icon` | `JSX.Element` | No | - | The icon used in the menu item |
| `shortcut` | `string` | No | - | The shortcut to be shown for the menu item |
### MainMenu.ItemCustom
To render a custom item, you can use `MainMenu.ItemCustom`.
**Usage**
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw>
<MainMenu>
<MainMenu.ItemCustom>
<button
style={{ height: "2rem" }}
onClick={() => window.alert("custom menu item")}
>
custom item
</button>
</MainMenu.ItemCustom>
</MainMenu>
</Excalidraw>
</div>
);
}
```
| Prop | Type | Required | Default | Description |
| --- | --- | :-: | :-: | --- |
| `children` | `React.ReactNode` | Yes | - | The content of the menu item |
### MainMenu.DefaultItems
For the items which are shown in the menu in [excalidraw.com](https://excalidraw.com), you can use `MainMenu.DefaultItems`
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw>
<MainMenu>
<MainMenu.DefaultItems.Socials />
<MainMenu.DefaultItems.Export />
<MainMenu.Item onSelect={() => window.alert("Item1")}>
Item1
</MainMenu.Item>
<MainMenu.Item onSelect={() => window.alert("Item2")}>
Item 2
</MainMenu.Item>
</MainMenu>
</Excalidraw>
</div>
);
}
```
Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/src/components/mainMenu/DefaultItems.tsx) of the default items.
### MainMenu.Group
To Group item in the main menu, you can use `MainMenu.Group`
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw>
<MainMenu>
<MainMenu.Group title="Excalidraw items">
<MainMenu.DefaultItems.Socials />
<MainMenu.DefaultItems.Export />
</MainMenu.Group>
<MainMenu.Group title="custom items">
<MainMenu.Item onSelect={() => window.alert("Item1")}>
Item1
</MainMenu.Item>
<MainMenu.Item onSelect={() => window.alert("Item2")}>
Item 2
</MainMenu.Item>
</MainMenu.Group>
</MainMenu>
</Excalidraw>
</div>
);
}
```
| Prop | Type | Required | Default | Description |
| --- | --- | :-: | :-: | --- |
| `children ` | `React.ReactNode` | Yes | - | The content of the `Menu Group` |

View File

@ -0,0 +1,140 @@
# WelcomeScreen
When the canvas is empty, Excalidraw can show a welcome _splash_ screen with a logo, a few quick action items, and hints explaining what some of the UI buttons do. Once the user picks a tool, or has created an element on the canvas, the welcome screen will disappear.
You can enable this behavior by rendering a `WelcomeScreen` component like this:
```jsx live
function App() {
return (
<div style={{ height: "46rem" }}>
<Excalidraw>
<WelcomeScreen />
</Excalidraw>
</div>
);
}
```
You can also customize the welcome screen by passing children to the `WelcomeScreen` component. See below.
## <WelcomeScreen/>
This is the main component. If you render it without any children, we will render the default welcome screen.
You can customize which welcome screen subcomponents are rendered by passing them as children.
The welcome screen consists of two main groups of subcomponents:
1. [WelcomeScreen.Center](#welcomescreencenterlogo).
2. [WeelcomeScreen.Hints](#welcomescreenhints).
<img
src={require("@site/static/img/welcome-screen-overview.png").default}
alt="Excalidraw logo: Sketch handrawn like diagrams."
/>
### Center
`<WelcomeScreen.Center/>` subcomponent is the center piece of the welcome screen, containing the `logo`, `heading`, and `menu`. All three subcomponents are optional, and you can render whatever you wish into the center component.
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw>
<WelcomeScreen>
<WelcomeScreen.Center>
<WelcomeScreen.Center.Logo />
<WelcomeScreen.Center.Heading>
Welcome Screen Heading!
</WelcomeScreen.Center.Heading>
<WelcomeScreen.Center.Menu>
<WelcomeScreen.Center.MenuItemLink href="https://github.com/excalidraw/excalidraw">
Excalidraw GitHub
</WelcomeScreen.Center.MenuItemLink>
<WelcomeScreen.Center.MenuItemHelp />
</WelcomeScreen.Center.Menu>
</WelcomeScreen.Center>
</WelcomeScreen>
</Excalidraw>
</div>
);
}
```
#### Logo
Use the `<WelcomeScreen.Center.Logo/>` to render a logo. By default it renders the Excalidraw logo and name. Supply `children` to customize.
#### Heading
Use the `<WelcomeScreen.Center.Heading/>` to render a heading below the logo. Supply `children` to change the default message.
#### Menu
`<WelcomeScreen.Center.Menu/>` is a wrapper component for the menu items. You can build your menu using the `<WelcomeScreen.Center.MenuItem>` and `<WelcomeScreen.Center.MenuItemLink>` components, render your own, or render one of the default menu items.
The default menu items are:
- `<WelcomeScreen.Center.MenuItemHelp/>` - opens the help dialog.
- `<WelcomeScreen.Center.MenuItemLoadScene/>` - open the load file dialog.
- `<WelcomeScreen.Center.MenuItemLiveCollaborationTrigger/>` - intended to open the live collaboration dialog. Works similarly to [`<LiveCollaborationTrigger>`](/docs/@excalidraw/excalidraw/api/children-components/live-collaboration-trigger) and you must supply `onSelect()` handler to integrate with your collaboration implementation.
#### MenuItem
The `<WelcomeScreen.Center.MenuItem/>` component can be used to render a menu item.
| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `onSelect` | `function` | Yes | | The handler is triggered when the item is selected. |
| `children` | `React.ReactNode` | Yes | | The content of the menu item |
| `icon` | `JSX.Element` | No | | The icon used in the menu item |
| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) |
**WelcomeScreen.Center.MenuItemLink**
To render an external link in a menu item, you can use this component.
| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `href` | `string` | Yes | | The `href` attribute to be added to the `anchor` element. |
| `children` | `React.ReactNode` | Yes | | The content of the menu item |
| `icon` | `JSX.Element` | No | | The icon used in the menu item |
| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) |
### Hints
These `<WelcomeScreen.Hints.*>` subcomponents render the UI hints. Text of each hint can be customized by supplying `children`.
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw>
<WelcomeScreen>
<WelcomeScreen.Hints.ToolbarHint>
<p> ToolBar Hints </p>
</WelcomeScreen.Hints.ToolbarHint>
<WelcomeScreen.Hints.MenuHint />
<WelcomeScreen.Hints.HelpHint />
</WelcomeScreen>
</Excalidraw>
</div>
);
}
```
#### MenuHint
`<WelcomeScreen.Hints.MenuHint>` hint subcomponent for the main menu. Supply `children` to customize the hint text.
#### ToolbarHint
`<WelcomeScreen.Hints.ToolbarHint>` hint subcomponent for the toolbar. Supply `children` to customize the hint text.
#### Help
`<WelcomeScreen.Hints.Help>` hint subcomponent for the help dialog. Supply `children` to customize the hint text.

View File

@ -0,0 +1,46 @@
# Constants
### FONT_FAMILY
**How to use**
```js
import { FONT_FAMILY } from "@excalidraw/excalidraw";
```
`FONT_FAMILY` contains all the font families used in `Excalidraw` as explained below
| Font Family | Description |
| ----------- | ---------------------- |
| `Virgil` | The `handwritten` font |
| `Helvetica` | The `Normal` Font |
| `Cascadia` | The `Code` Font |
Defaults to `FONT_FAMILY.Virgil` unless passed in `initialData.appState.currentItemFontFamily`.
### THEME
**How to use**
```js
import { THEME } from "@excalidraw/excalidraw";
```
`THEME` contains all the themes supported by `Excalidraw` as explained below
| Theme | Description |
| ------- | ----------------- |
| `LIGHT` | The `light` theme |
| `DARK` | The `Dark` theme |
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`.
**How to use **
```js
import { MIME_TYPES } from "@excalidraw/excalidraw";
```

View File

@ -0,0 +1,55 @@
# initialData
<pre>
&#123; 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> &#125;
</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. |
| `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) &#124; Promise&lt;[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)&gt; | 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. |
You might want to use this when you want to load excalidraw with some initial elements and app state.
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw
initialData={{
elements: [
{
type: "rectangle",
version: 141,
versionNonce: 361174001,
isDeleted: false,
id: "oDVXy8D6rom3H1-LLH2-f",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
angle: 0,
x: 100.50390625,
y: 93.67578125,
strokeColor: "#000000",
backgroundColor: "transparent",
width: 186.47265625,
height: 141.9765625,
seed: 1968410350,
groupIds: [],
},
],
appState: { zenModeEnabled: true, viewBackgroundColor: "#a5d8ff" },
scrollToContent: true
}}
/>
</div>
);
}
```

View File

@ -0,0 +1,230 @@
# Props
All `props` are *optional*.
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` &#124; `null` &#124; <code>Promise<object &#124; null></code> | `null` | The initial data with which app loads. |
| [`ref`](/docs/@excalidraw/excalidraw/api/props/ref) | `object` | _ | `Ref` to be passed to Excalidraw |
| [`isCollaborating`](#iscollaborating) | `boolean` | _ | This indicates if the app is in `collaboration` mode |
| [`onChange`](#onchange) | `function` | _ | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw `elements` and the current `app state`. |
| [`onPointerUpdate`](#onpointerupdate) | `function` | _ | Callback triggered when mouse pointer is updated. |
| [`onPointerDown`](#onpointerdown) | `function` | _ | This prop if passed gets triggered on pointer down evenets |
| [`onScrollChange`](#onscrollchange) | `function` | _ | This prop if passed gets triggered when scrolling the canvas. |
| [`onPaste`](#onpaste) | `function` | _ | Callback to be triggered if passed when the something is pasted in to the scene |
| [`onLibraryChange`](#onlibrarychange) | `function` | _ | The callback if supplied is triggered when the library is updated and receives the library items. |
| [`onLinkOpen`](#onlinkopen) | `function` | _ | The callback if supplied is triggered when any link is opened. |
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
| [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | _ | Render function that renders custom UI in top right corner |
| [`renderCustomStats`](/docs/@excalidraw/excalidraw/api/props/render-props#rendercustomstats) | `function` | _ | Render function that can be used to render custom stats on the stats dialog. |
| [`renderSidebar`](/docs/@excalidraw/excalidraw/api/props/render-props#rendersidebar) | `function` | _ | Render function that renders custom sidebar. |
| [`viewModeEnabled`](#viewmodeenabled) | `boolean` | _ | This indicates if the app is in `view` mode. |
| [`zenModeEnabled`](#zenmodeenabled) | `boolean` | _ | This indicates if the `zen` mode is enabled |
| [`gridModeEnabled`](#gridmodeenabled) | `boolean` | _ | This indicates if the `grid` mode is enabled |
| [`libraryReturnUrl`](#libraryreturnurl) | `string` | _ | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to |
| [`theme`](#theme) | `"light"` &#124; `"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) |
| [`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 |
| [`generateIdForFile`](#generateidforfile) | `function` | _ | Allows you to override `id` generation for files added on canvas |
### 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.
You can use this to add any extra information you need to keep track of.
You can add `customData` to elements when passing them as [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata), or using [`updateScene`](/docs/@excalidraw/excalidraw/api/props/ref#updatescene) / [`updateLibrary`](/docs/@excalidraw/excalidraw/api/props/ref#updatelibrary) afterwards.
```js showLineNumbers
{
type: "rectangle",
id: "oDVXy8D6rom3H1-LLH2-f",
customData: {customId: '162'},
}
```
### isCollaborating
This prop indicates if the app is in `collaboration` mode.
### onChange
Every time component updates, this callback if passed will get triggered and has the below signature.
```js
(excalidrawElements, appState, files) => void;
```
1. `excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) in the scene.
2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/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.
Here you can try saving the data to your backend or local storage for example.
### onPointerUpdate
This callback is triggered when mouse pointer is updated.
```js
({ x, y }, button, pointersMap}) => void;
```
1.`{x, y}`: Pointer coordinates
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
```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.
3. `canvas`: The `HTMLCanvasElement` of the scene.
### onPointerDown
This prop if passed will be triggered on pointer down events and has the below signature.
<pre>
(activeTool: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L115"> AppState["activeTool"]</a>, pointerDownState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L424">PointerDownState</a>) => void
</pre>
### onScrollChange
This prop if passed will be triggered when canvas is scrolled and has the below signature.
```ts
(scrollX: number, scrollY: number) => void
```
### onPaste
This callback is triggered if passed when something is pasted into the scene. You can use this callback in case you want to do something additional when the paste event occurs.
<pre>
(data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/clipboard.ts#L18">ClipboardData</a>, event: ClipboardEvent &#124; null) => boolean
</pre>
This callback must return a `boolean` value or a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) which resolves to a boolean value.
In case you want to prevent the excalidraw paste action you must return `false`, it will stop the native excalidraw clipboard management flow (nothing will be pasted into the scene).
### onLibraryChange
This callback if supplied will get triggered when the library is updated and has the below signature.
<pre>
(items:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">
LibraryItems
</a>
) => void | Promise&lt;any&gt;
</pre>
It is invoked with empty items when user clears the library. You can use this callback when you want to do something additional when library is updated for example persisting it to local storage.
### onLinkOpen
This prop if passed will be triggered when clicked on `link`. To handle the redirect yourself (such as when using your own router for internal links), you must call `event.preventDefault()`.
<pre>
(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement</a>,
event: CustomEvent&lt;&#123; nativeEvent: MouseEvent }&gt;) => void
</pre>
Example:
```js showLineNumbers
const history = useHistory();
// open internal links using the app's router, but opens external links in
// a new tab/window
const onLinkOpen: ExcalidrawProps["onLinkOpen"] = useCallback(
(element, event) => {
const link = element.link;
const { nativeEvent } = event.detail;
const isNewTab = nativeEvent.ctrlKey || nativeEvent.metaKey;
const isNewWindow = nativeEvent.shiftKey;
const isInternalLink =
link.startsWith("/") || link.includes(window.location.origin);
if (isInternalLink && !isNewTab && !isNewWindow) {
history.push(link.replace(window.location.origin, ""));
// signal that we're handling the redirect ourselves
event.preventDefault();
}
},
[history],
);
```
### 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.
```js
import { defaultLang, languages } from "@excalidraw/excalidraw";
```
| name | type |
| --- | --- |
| `defaultLang` | `string` |
| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) |
### viewModeEnabled
This prop indicates whether the app is in `view mode`. When supplied, the value takes precedence over *intialData.appState.viewModeEnabled*, the `view mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app.
### zenModeEnabled
This prop indicates whether the app is in `zen mode`. When supplied, the value takes precedence over *intialData.appState.zenModeEnabled*, the `zen mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app.
### gridModeEnabled
This prop indicates whether the shows the grid. When supplied, the value takes precedence over *intialData.appState.gridModeEnabled*, the grid will be fully controlled by the host app, and users won't be able to toggle it from within the app.
### libraryReturnUrl
If supplied, this URL will be used when user tries to install a library from [libraries.excalidraw.com](https://libraries.excalidraw.com).
Defaults to *window.location.origin + window.location.pathname*. To install the libraries in the same tab from which it was opened, you need to set `window.name` (to any alphanumeric string) — if it's not set it will open in a new tab.
### theme
This prop controls Excalidraw's theme. When supplied, the value takes precedence over *intialData.appState.theme*, the theme will be fully controlled by the host app, and users won't be able to toggle it from within the app unless *UIOptions.canvasActions.toggleTheme* is set to `true`, in which case the `theme` prop will control Excalidraw's default theme with ability to allow theme switching (you must take care of updating the `theme` prop when you detect a change to `appState.theme` from the [onChange](#onchange) callback).
You can use [`THEME`](/docs/@excalidraw/excalidraw/api/utils#theme) to specify the theme.
### name
This prop sets the `name` of the drawing which will be used when exporting the drawing. When supplied, the value takes precedence over *intialData.appState.name*, the `name` will be fully controlled by host app and the users won't be able to edit from within Excalidraw.
### detectScroll
Indicates whether Excalidraw should listen for `scroll` event on the nearest scrollable container in the DOM tree and recompute the coordinates (e.g. to correctly handle the cursor) when the component's position changes. You can disable this when you either know this doesn't affect your app or you want to take care of it yourself (calling the [`refresh()`](#ref) method).
### handleKeyboardGlobally
Indicates whether to bind keyboard events to `document`. Disabled by default, meaning the keyboard events are bound to the Excalidraw component. This allows for multiple Excalidraw components to live on the same page, and ensures that Excalidraw keyboard handling doesn't collide with your app's (or the browser) when the component isn't focused.
Enable this if you want Excalidraw to handle keyboard even if the component isn't focused (e.g. a user is interacting with the navbar, sidebar, or similar).
### autoFocus
This prop indicates whether to `focus` the Excalidraw component on page load. Defaults to false.
### generateIdForFile
Allows you to override `id` generation for files added on canvas (images). By default, an SHA-1 digest of the file is used.
```tsx
(file: File) => string | Promise<string>
```

View File

@ -0,0 +1,421 @@
# ref
<pre>
<a href="https://reactjs.org/docs/refs-and-the-dom.html#creating-refs">
createRef
</a>{" "}
&#124;{" "}
<a href="https://reactjs.org/docs/hooks-reference.html#useref">useRef</a>{" "}
&#124;{" "}
<a href="https://reactjs.org/docs/refs-and-the-dom.html#callback-refs">
callbackRef
</a>{" "}
&#124; <br />
&#123; current: &#123; readyPromise: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L460">
resolvablePromise
</a> } }
</pre>
You can pass a `ref` when you want to access some excalidraw APIs. We expose the below APIs:
| API | Signature | Usage |
| --- | --- | --- |
| ready | `boolean` | This is set to true once Excalidraw is rendered |
| [readyPromise](#readypromise) | `function` | This promise will be resolved with the api once excalidraw has rendered. This will be helpful when you want do some action on the host app once this promise resolves. For this to work you will have to pass ref as shown [here](#readypromise) |
| [updateScene](#updatescene) | `function` | updates the scene with the sceneData |
| [updateLibrary](#updatelibrary) | `function` | updates the scene with the sceneData |
| [addFiles](#addfiles) | `function` | add files data to the appState |
| [resetScene](#resetscene) | `function` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. |
| [getSceneElementsIncludingDeleted](#getsceneelementsincludingdeleted) | `function` | Returns all the elements including the deleted in the scene |
| [getSceneElements](#getsceneelements) | `function` | Returns all the elements excluding the deleted in the scene |
| [getAppState](#getappstate) | `function` | Returns current appState |
| [history](#history) | `object` | This is the history API. `history.clear()` will clear the history |
| [scrollToContent](#scrolltocontent) | `function` | Scroll the nearest element out of the elements supplied to the center. Defaults to the elements on the scene. |
| [refresh](#refresh) | `function` | Updates the offsets for the Excalidraw component so that the coordinates are computed correctly (for example the cursor position). |
| [setToast](#settoast) | `function` | This API can be used to show the toast with custom message. |
| [id](#id) | `string` | Unique ID for the excalidraw component. |
| [getFiles](#getfiles) | `function` | This API can be used to get the files present in the scene. |
| [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 |
## readyPromise
<pre>
const excalidrawRef = &#123; current:&#123; readyPromise:
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L460">
&nbsp;resolvablePromise
</a>
&nbsp;&#125; &#125;
</pre>
Since plain object is passed as a `ref`, the `readyPromise` is resolved as soon as the component is mounted. Most of the time you will not need this unless you have a specific use case where you can't pass the `ref` in the react way and want to do some action on the host when this promise resolves.
```jsx showLineNumbers
const resolvablePromise = () => {
let resolve;
let reject;
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
promise.resolve = resolve;
promise.reject = reject;
return promise;
};
const App = () => {
const excalidrawRef = useMemo(
() => ({
current: {
readyPromise: resolvablePromise(),
},
}),
[],
);
useEffect(() => {
excalidrawRef.current.readyPromise.then((api) => {
console.log("loaded", api);
});
}, [excalidrawRef]);
return (
<div style={{ height: "500px" }}>
<Excalidraw ref={excalidrawRef} />
</div>
);
};
```
## updateScene
<pre>
(scene:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L339">
sceneData
</a>
) => void
</pre>
You can use this function to update the scene with the sceneData. It accepts the below attributes.
| 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. |
| `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
```jsx live
function App() {
const updateScene = () => {
const sceneData = {
elements: [
{
type: "rectangle",
version: 141,
versionNonce: 361174001,
isDeleted: false,
id: "oDVXy8D6rom3H1-LLH2-f",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
angle: 0,
x: 100.50390625,
y: 93.67578125,
strokeColor: "#c92a2a",
backgroundColor: "transparent",
width: 186.47265625,
height: 141.9765625,
seed: 1968410350,
groupIds: [],
boundElements: null,
locked: false,
link: null,
updated: 1,
roundness: {
type: 3,
value: 32,
},
},
],
appState: {
viewBackgroundColor: "#edf2ff",
},
};
excalidrawAPI.updateScene(sceneData);
};
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
return (
<div style={{ height: "500px" }}>
<p style={{ fontSize: "16px" }}> Click to update the scene</p>
<button className="custom-button" onClick={updateScene}>
Update Scene
</button>
<Excalidraw ref={(api) => setExcalidrawAPI(api)} />
</div>
);
}
```
### updateLibrary
<pre>
(opts: &#123; <br /> libraryItems:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L249">
LibraryItemsSource
</a>
;<br /> merge?: boolean; <br /> prompt?: boolean;
<br /> openLibraryMenu?: boolean;
<br /> defaultStatus?: "unpublished" | "published"; <br /> &#125;) => Promise&lt;
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L246">
LibraryItems
</a>
&gt;
</pre>
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 |
| `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. |
| `defaultStatus` | <code>"unpublished" &#124; "published"</code> | `"unpublished"` | Default library item's `status` if not present. |
```tsx live
function App() {
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
useEffect(() => {
if (!excalidrawAPI) {
return;
}
// to open the library sidebar
excalidrawAPI.updateScene({ appState: { openSidebar: "library" } });
}, [excalidrawAPI]);
return (
<div style={{ height: "500px" }}>
<p style={{ fontSize: "16px" }}> Click to update the library items</p>
<button
className="custom-button"
onClick={() => {
const libraryItems = [
{
status: "published",
id: "1",
created: 1,
elements: initialData.libraryItems[1],
},
{
status: "unpublished",
id: "2",
created: 2,
elements: initialData.libraryItems[1],
},
];
excalidrawAPI.updateLibrary({
libraryItems,
openLibraryMenu: true,
});
}}
>
Update Library
</button>
<Excalidraw
ref={(api) => setExcalidrawAPI(api)}
// initial data retrieved from https://github.com/excalidraw/excalidraw/blob/master/dev-docs/src/initialData.js
initialData={{
libraryItems: initialData.libraryItems,
appState: { openSidebar: "library" },
}}
/>
</div>
);
}
```
### addFiles
<pre>
(files:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59">
BinaryFileData
</a>
) => void
</pre>
Adds supplied files data to the `appState.files` cache on top of existing files present in the cache.
## resetScene
```tsx
(opts?: { resetLoadingState: boolean }) => void
```
Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false.
## getSceneElementsIncludingDeleted
<pre>
() =>{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
ExcalidrawElement[]
</a>
</pre>
Returns all the elements including the deleted in the scene.
## getSceneElements
<pre>
() => NonDeleted&#60;
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
ExcalidrawElement
</a>
[]&#62;
</pre>
Returns all the elements excluding the deleted in the scene
## getAppState
<pre>
() =>{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
AppState
</a>
</pre>
Returns current appState.
## history
```tsx
{
clear: () => void
}
```
This is the history API. history.clear() will clear the history.
## scrollToContent
<pre>
(<br />
{" "}
target?:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
ExcalidrawElement
</a>{" "}
&#124;{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
ExcalidrawElement
</a>
[],
<br />
{" "}opts?: &#123; fitToContent?: boolean; animate?: boolean; duration?: number
&#125;
<br />) => void
</pre>
Scroll the nearest element out of the elements supplied to the center of the viewport. Defaults to the elements on the scene.
| Attribute | type | default | Description |
| --- | --- | --- | --- |
| target | <code>ExcalidrawElement &#124; ExcalidrawElement[]</code> | 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. |
| opts.animate | boolean | false | Whether to animate between starting and ending position. Note that for larger scenes the animation may not be smooth due to performance issues. |
| opts.duration | number | 500 | Duration of the animation if `opts.animate` is `true`. |
## refresh
```tsx
() => void
```
Updates the `offsets` for the `Excalidraw` component so that the coordinates are computed correctly (for example the cursor position).
You don't have to call this when the position is changed on page scroll or when the excalidraw container resizes (we handle that ourselves).
For any other cases if the position of excalidraw is updated (example due to scroll on parent container and not page scroll) you should call this API.
## setToast
This API can be used to show the toast with custom message.
```tsx
({ message: string, closable?:boolean,duration?:number
} | null) => void
```
| Attribute | type | Description |
| --- | --- | --- |
| message | string | The message to be shown on the toast. |
| closable | boolean | Indicates whether to show the closable button on toast to dismiss the toast. |
| duration | number | Determines the duration after which the toast should auto dismiss. To prevent autodimiss you can pass `Infinity`. |
To dismiss an existing toast you can simple pass `null`
```js
setToast(null);
```
## id
The unique id of the excalidraw component. This can be used to identify the excalidraw component, for example importing the library items to the excalidraw component from where it was initiated when you have multiple excalidraw components rendered on the same page as shown in [multiple excalidraw demo](https://codesandbox.io/s/multiple-excalidraw-k1xx5).
## getFiles
<pre>
() =>{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L82">
files
</a>
</pre>
This API can be used to get the files present in the scene. It may contain files that aren't referenced by any element, so if you're persisting the files to a storage, you should compare them against stored elements.
## setActiveTool
This API has the below signature. It sets the `tool` passed in param as the active tool.
<pre>
(tool: <br /> &#123; type:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L15">
SHAPES
</a>
[number]["value"]&#124; "eraser" &#125; &#124;
<br /> &#123; type: "custom"; customType: string &#125;) => void
</pre>
## setCursor
This API can be used to customise the mouse cursor on the canvas and has the below signature. It sets the mouse cursor to the cursor passed in param.
```tsx
(cursor: string) => void
```
## toggleMenu
```tsx
(type: "library" | "customSidebar", force?: boolean) => boolean;
```
This API can be used to toggle a specific menu (currently only the sidebars), and returns whether the menu was toggled on or off. If the `force` flag passed, it will force the menu to be toggled either on/off based on the `boolean` passed.
This API is especially useful when you render a custom sidebar using [`renderSidebar`](#rendersidebar) prop, and you want to toggle it from your app based on a user action.
## resetCursor
```tsx
() => void
```
This API can be used to reset to default mouse cursor.

View File

@ -0,0 +1,123 @@
# Render Props
## renderTopRightUI
<pre>
(isMobile: boolean, appState:
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
AppState
</a>
) => JSX | null
</pre>
A function returning `JSX` to render `custom` UI in the top right corner of the app.
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw
renderTopRightUI={() => {
return (
<button
style={{
background: "#70b1ec",
border: "none",
color: "#fff",
width: "max-content",
fontWeight: "bold",
}}
onClick={() => window.alert("This is dummy top right UI")}
>
Click me
</button>
);
}}
/>
</div>
);
}
```
## renderCustomStats
A function that can be used to render custom stats (returns JSX) in the `nerd stats` dialog.
![Nerd Stats](../../../../assets/nerd-stats.png)
For example you can use this prop to render the size of the elements in the storage as do in [excalidraw.com](https://excalidraw.com).
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw
renderCustomStats={() => (
<p style={{ color: "#70b1ec", fontWeight: "bold" }}>
Dummy stats will be shown here
</p>
)}
/>
</div>
);
}
```
## renderSidebar
```tsx
() => JSX | null;
```
You can render `custom sidebar` using this prop. This sidebar is the same that the library menu sidebar is using, and can be used for any purposes your app needs.
You need to import the `Sidebar` component from `excalidraw` package and pass your content as its `children`. The function `renderSidebar` should return the `Sidebar` instance.
### Sidebar
The `<Sidebar>` component takes these props (all are optional except `children`):
| Prop | Type | Description |
| --- | --- | --- |
| `children` | `React.ReactNode` | Content you want to render inside the `sidebar`. |
| `onClose` | `function` | Invoked when the component is closed (by user, or the editor). No need to act on this event, as the editor manages the sidebar open state on its own. |
| `onDock` | `function` | Invoked when the user toggles the `dock` button. The callback recieves a `boolean` parameter `isDocked` which indicates whether the sidebar is `docked` |
| `docked` | `boolean` | Indicates whether the sidebar is`docked`. By default, the sidebar is `undocked`. If passed, the docking becomes controlled, and you are responsible for updating the `docked` state by listening on `onDock` callback. To decide the breakpoint for docking you can use [UIOptions.dockedSidebarBreakpoint](/docs/@excalidraw/excalidraw/api/props/ui-options#dockedsidebarbreakpoint) for more info on docking. |
| `dockable` | `boolean` | Indicates whether to show the `dock` button so that user can `dock` the sidebar. If `false`, you can still dock programmatically by passing `docked` as `true`. |
The sidebar will always include a header with `close / dock` buttons (when applicable).
You can also add custom content to the header, by rendering `<Sidebar.Header>` as a child of the `<Sidebar>` component. Note that the custom header will still include the default buttons.
### Sidebar.Header
| name | type | description |
| --- | --- | --- |
| children | `React.ReactNode` | Content you want to render inside the sidebar header as a sibling of `close` / `dock` buttons. |
To control the visibility of the sidebar you can use [`toggleMenu("customSidebar")`](/docs/@excalidraw/excalidraw/api/props/ref#togglemenu) api available via `ref`.
```tsx live
function App() {
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
return (
<div style={{ height: "500px" }}>
<button className="custom-button" onClick={() => excalidrawAPI.toggleMenu("customSidebar")}>
Toggle Custom Sidebar
</button>
<Excalidraw
UIOptions={{ dockedSidebarBreakpoint: 100 }}
ref={(api) => setExcalidrawAPI(api)}
renderSidebar={() => {
return (
<Sidebar dockable={true}>
<Sidebar.Header>Custom Sidebar Header </Sidebar.Header>
<p style={{ padding: "1rem" }}> custom Sidebar Content </p>
</Sidebar>
);
}}
/>
</div>
);
}
```

View File

@ -0,0 +1,72 @@
# UIOptions
This prop can be used to customise UI of Excalidraw. Currently we support customising [`canvasActions`](#canvasactions), [`dockedSidebarBreakpoint`](#dockedsidebarbreakpoint) and [`welcomeScreen`](#welcmescreen).
<pre>
&#123;
<br /> canvasActions?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L372">
CanvasActions
</a>, <br /> dockedSidebarBreakpoint?: number, <br /> welcomeScreen?: boolean <br />
}
</pre>
## canvasActions
This `prop` controls the visibility of the canvas actions inside the `menu`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `changeViewBackgroundColor` | `boolean` | `true` | Indicates whether to show `Background color picker`. |
| `clearCanvas` | `boolean` | `true` | Indicates whether to show `Clear canvas` button. |
| `export` | `false` &#124; [`exportOpts`](#exportopts) | `object` | This prop allows to customize the UI inside the export dialog. By default it shows the `save file to disk`. For more details visit [`exportOpts`](#exportopts). |
| `loadScene` | `boolean` | `true` | Indicates whether to show `Load` button. |
| `saveToActiveFile` | `boolean` | `true` | Indicates whether to show `Save` button to save to current file. |
| `toggleTheme` | `boolean` &#124; `null` | `null` | Indicates whether to show `Theme toggle`. When defined as `boolean`, takes precedence over [`props.theme`](/docs/@excalidraw/excalidraw/api/props#theme) to show `Theme toggle`. |
| `saveAsImage` | `boolean` | `true` | Indicates whether to show `Save as image` button. |
```tsx live
function App() {
const UIOptions = {
canvasActions: {
changeViewBackgroundColor: false,
clearCanvas: false,
loadScene: false,
},
};
return (
<div style={{ height: "500px" }}>
<Excalidraw UIOptions={UIOptions} />
</div>
);
}
```
### exportOpts
The below attributes can be set in `UIOptions.canvasActions.export` to customize the export dialog.
If `UIOptions.canvasActions.export` is `false` the export button will not be rendered.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `saveFileToDisk` | `boolean` | `true` | Indicates whether `save file to disk` button should be shown |
| `onExportToBackend` | `object` | \_ | This callback is triggered when the shareable-link button is clicked in the export dialog. The link button will only be shown if this callback is passed. |
| `renderCustomUI` | `object` | \_ | This callback should be supplied if you want to render custom UI in the export dialog. |
## 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).
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.
![image](https://user-images.githubusercontent.com/11256141/174664866-c698c3fa-197b-43ff-956c-d79852c7b326.png)
```tsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw UIOptions={{dockedSidebarBreakpoint: 200}}/>
</div>
);
}
```

View File

@ -0,0 +1,196 @@
---
title: Export Utilities
id: "export"
---
:::info
We're working on much improved export utilities. Stay tuned!
:::
### exportToCanvas
**_Signature_**
<pre>
exportToCanvas(&#123;<br/>&nbsp;
elements,<br/>&nbsp;
appState<br/>&nbsp;
getDimensions,<br/>&nbsp;
files,<br/>&nbsp;
exportPadding?: number;<br/>
&#125;: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/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. |
| [`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. |
| `exportPadding` | `number` | `10` | The `padding` to be added on canvas. |
#### getDimensions
```tsx
(width: number, height: number) => {
width: number,
height: number,
scale?: number
}
```
A function which returns the `width`, `height`, and optionally `scale` (defaults to `1`), with which canvas is to be exported.
**How to use**
```js
import { exportToCanvas } from "@excalidraw/excalidraw";
```
This function returns the canvas with the exported elements, appState and dimensions.
```jsx live
function App() {
const [canvasUrl, setCanvasUrl] = useState("");
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
return (
<>
<button
className="custom-button"
onClick={async () => {
if (!excalidrawAPI) {
return
}
const elements = excalidrawAPI.getSceneElements();
if (!elements || !elements.length) {
return
}
const canvas = await exportToCanvas({
elements,
appState: {
...initialData.appState,
exportWithDarkMode: false,
},
files: excalidrawAPI.getFiles(),
getDimensions: () => { return {width: 350, height: 350}}
});
const ctx = canvas.getContext("2d");
ctx.font = "30px Virgil";
ctx.strokeText("My custom text", 50, 60);
setCanvasUrl(canvas.toDataURL());
}}
>
Export to Canvas
</button>
<div className="export export-canvas">
<img src={canvasUrl} alt="" />
</div>
<div style={{ height: "400px" }}>
<Excalidraw ref={(api) => setExcalidrawAPI(api)}
/>
</div>
</>
)
}
```
### exportToBlob
**_Signature_**
<pre>
exportToBlob(<br/>&nbsp;
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">ExportOpts</a> & &#123;<br/>&nbsp;
mimeType?: string,<br/>&nbsp;
quality?: number,<br/>&nbsp;
exportPadding?: number;<br/>
})
</pre>
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `opts` | `object` | _ | This param is passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exporttocanvas) |
| `mimeType` | `string` | `image/png` | Indicates the image format. |
| `quality` | `number` | `0.92` | A value between `0` and `1` indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg`/`image/webp` MIME types. |
| `exportPadding` | `number` | `10` | The padding to be added on canvas. |
**How to use**
```js
import { exportToBlob } from "@excalidraw/excalidraw";
```
Returns a promise which resolves with a [blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob). It internally uses [canvas.ToBlob](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob).
### exportToSvg
**_Signature_**
<pre>
exportToSvg(&#123;<br/>&nbsp;
elements:&nbsp;
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
ExcalidrawElement[]
</a>,<br/>&nbsp;
appState:
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95"> AppState
</a>,<br/>&nbsp;
exportPadding: number,<br/>&nbsp;
metadata: string,<br/>&nbsp;
files:&nbsp;
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59">
BinaryFiles
</a>,<br/>
&#125;);
</pre>
| 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 |
| 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. |
This function returns a promise which resolves to `svg` of the exported drawing.
### exportToClipboard
**_Signature_**
<pre>
exportToClipboard(<br/>&nbsp;
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">ExportOpts</a> & &#123;<br/>&nbsp;
mimeType?: string,<br/>&nbsp;
quality?: number;<br/>&nbsp;
type: 'png' | 'svg' |'json'<br/>
})
</pre>
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `opts` | | | This param is same as the params passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exporttocanvas). |
| `mimeType` | `string` | `image/png` | Indicates the image format, this will be used when exporting as `png`. |
| `quality` | `number` | `0.92` | A value between `0` and `1` indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg` / `image/webp` MIME types. This will be used when exporting as `png`. |
| `type` | 'png' &#124; 'svg' &#124; 'json' | _ | This determines the format to which the scene data should be `exported`. |
**How to use**
```js
import { exportToClipboard } from "@excalidraw/excalidraw";
```
Copies the scene data in the specified format (determined by `type`) to clipboard.
### Additional attributes of appState for export\* APIs
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `exportBackground` | `boolean` | `true` | Indicates whether `background` should be exported |
| `viewBackgroundColor` | `string` | `#fff` | The default background color |
| `exportWithDarkMode` | `boolean` | `false` | Indicates whether to export with `dark` mode |
| `exportEmbedScene` | `boolean` | `false` | Indicates whether scene data should be embedded in `svg/png`. This will increase the image size. |

View File

@ -0,0 +1,108 @@
---
title: Restore Utilities
id: "restore"
---
### restoreAppState
**_Signature_**
<pre>
restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState["appState"]</a>,<br/>&nbsp; localAppState: Partial&lt;<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>
</pre>
**_How to use_**
```js
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.
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.
You can pass `null` / `undefined` if not applicable.
### restoreElements
**_Signature_**
<pre>
restoreElements(
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>&nbsp;
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/>&nbsp;
opts: &#123; 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> &#124; null &#124; 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
When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`.
Use this when you `import` elements which may already be present in the scene to ensure that you do not disregard the newly imported elements if you're using element version to detect the update
#### opts
The extra optional parameter to configure restored elements. It has the following attributes
| Prop | Type | Description|
| --- | --- | ------|
| `refreshDimensions` | `boolean` | Indicates whether we should also `recalculate` text element dimensions. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration. |
| `repairBindings` |`boolean` | Indicates whether the `bindings` for the elements should be repaired. This is to make sure there are no containers with non existent bound text element id and no bound text elements with non existent container id. |
**_How to use_**
```js
import { restoreElements } from "@excalidraw/excalidraw";
```
This function will make sure all properties of element is correctly set and if any attribute is missing, it will be set to its default value.
Parameter `refreshDimensions` indicates whether we should also `recalculate` text element dimensions. Defaults to `false`. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration.
### restore
**_Signature_**
<pre>
restore(
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>&nbsp;
localAppState: Partial&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>&nbsp;
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/>
opts: &#123; 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`.
**_How to use_**
```js
import { restore } from "@excalidraw/excalidraw";
```
This function makes sure elements and state is set to appropriate values and set to default value if not present. It is a combination of [restoreElements](#restoreelements) and [restoreAppState](#restoreappstate).
### restoreLibraryItems
**_Signature_**
<pre>
restoreLibraryItems(libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState["libraryItems"]</a>,<br/>&nbsp;
defaultStatus: "published" | "unpublished")
</pre>
**_How to use_**
```js
import { restoreLibraryItems } from "@excalidraw/excalidraw";
restoreLibraryItems(libraryItems, "unpublished");
```
This function normalizes library items elements, adding missing values when needed.

View File

@ -0,0 +1,385 @@
---
slug: /@excalidraw/excalidraw/api/utils
---
# Utils
These are pure Javascript functions exported from the @excalidraw/excalidraw [`@excalidraw/excalidraw`](https://npmjs.com/@excalidraw/excalidraw). If you want to export your drawings in different formats eg `png`, `svg` and more you can check out [Export Utilities](/docs/@excalidraw/excalidraw/API/utils/export). If you want to restore your drawings you can check out [Restore Utilities](/docs/@excalidraw/excalidraw/API/utils/restore).
### 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).
If you want to overwrite the `source` field in the `JSON` string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value.
**_Signature_**
<pre>
serializeAsJSON(&#123;<br/>&nbsp;
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>&nbsp;
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>,<br/>
}): string
</pre>
**How to use**
```js
import { serializeAsJSON } from "@excalidraw/excalidraw";
```
### serializeLibraryAsJSON
Takes the `library` items and returns a `JSON` string.
If you want to overwrite the source field in the JSON string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value.
**_Signature_**
<pre>
serializeLibraryAsJSON(
libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems[]</a>)
</pre>
**How to use**
```js
import { serializeLibraryAsJSON } from "@excalidraw/excalidraw";
```
#### isInvisiblySmallElement
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
</pre>
**How to use**
```js
import { isInvisiblySmallElement } from "@excalidraw/excalidraw";
```
### loadFromBlob
This function loads the scene data from the blob (or file). If you pass `localAppState`, `localAppState` value will be preferred over the `appState` derived from `blob`. Throws if blob doesn't contain valid scene data.
**How to use**
```js
import { loadFromBlob } from "@excalidraw/excalidraw";
const scene = await loadFromBlob(file, null, null);
excalidrawAPI.updateScene(scene);
```
**Signature**
<pre>
loadFromBlob(<br/>&nbsp;
blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,<br/>&nbsp;
localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a> | null,<br/>&nbsp;
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>&nbsp;
fileHandle?: FileSystemHandle | null <br/>
) => Promise&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/restore.ts#L61">RestoredDataState</a>>
</pre>
### loadLibraryFromBlob
This function loads the library from the blob. Additonally takes `defaultStatus` param which sets the default status for library item if not present, defaults to `unpublished`.
**How to use **
```js
import { loadLibraryFromBlob } from "@excalidraw/excalidraw";
```
**_Signature_**
<pre>
loadLibraryFromBlob(blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>, defaultStatus: "published" | "unpublished")
</pre>
### loadSceneOrLibraryFromBlob
This function loads either scene or library data from the supplied blob. If the blob contains scene data, and you pass `localAppState`, `localAppState` value will be preferred over the `appState` derived from `blob`.
:::caution
Throws if blob doesn't contain valid `scene` data or `library` data.
:::
**How to use**
```js showLineNumbers
import { loadSceneOrLibraryFromBlob, MIME_TYPES } from "@excalidraw/excalidraw";
const contents = await loadSceneOrLibraryFromBlob(file, null, null);
if (contents.type === MIME_TYPES.excalidraw) {
excalidrawAPI.updateScene(contents.data);
} else if (contents.type === MIME_TYPES.excalidrawlib) {
excalidrawAPI.updateLibrary(contents.data);
}
```
**_Signature_**
<pre>
loadSceneOrLibraryFromBlob(<br/>&nbsp;
blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,
localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a> | null,<br/>&nbsp;
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>&nbsp;
fileHandle?: FileSystemHandle | null<br/>
) => Promise&lt;&#123; 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>}>
</pre>
### getFreeDrawSvgPath
This function returns the `free draw` svg path for the element.
**How to use**
```js
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>)
</pre>
### isLinearElement
This function returns true if the element is `linear` type (`arrow` |`line`) else returns `false`.
**How to use**
```js
import { isLinearElement } from "@excalidraw/excalidraw";
```
**Signature**
```tsx
isLinearElement(elementType?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L80">ExcalidrawElement</a>): boolean
```
### getNonDeletedElements
This function returns an array of `deleted` elements.
**How to use**
```js
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>
</pre>
### mergeLibraryItems
This function merges two `LibraryItems` arrays, where unique items from `otherItems` are sorted first in the returned array.
```js
import { mergeLibraryItems } from "@excalidraw/excalidraw";
```
**_Signature_**
<pre>
mergeLibraryItems(localItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L250">LibraryItems</a>,<br/>&nbsp;
otherItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>) => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L250">LibraryItems</a>
</pre>
### parseLibraryTokensFromUrl
Parses library parameters from URL if present (expects the `#addLibrary` hash key), and returns an object with the `libraryUrl` and `idToken`. Returns `null` if `#addLibrary` hash key not found.
**How to use**
```js
import { parseLibraryTokensFromUrl } from "@excalidraw/excalidraw";
```
**Signature**
```tsx
parseLibraryTokensFromUrl(): {
libraryUrl: string;
idToken: string | null;
} | null
```
### useHandleLibrary
A hook that automatically imports library from url if `#addLibrary` hash key exists on initial load, or when it changes during the editing session (e.g. when a user installs a new library), and handles initial library load if `getInitialLibraryItems` getter is supplied.
**How to use**
```js
import { useHandleLibrary } from "@excalidraw/excalidraw";
export const App = () => {
// ...
useHandleLibrary({ excalidrawAPI });
};
```
**Signature**
<pre>
useHandleLibrary(opts: &#123;<br/>&nbsp;
excalidrawAPI: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L494">ExcalidrawAPI</a>,<br/>&nbsp;
getInitialLibraryItems?: () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L253">LibraryItemsSource</a><br/>
});
</pre>
In the future, we will be adding support for handling `library` persistence to `browser storage` (or elsewhere).
### getSceneVersion
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>)
</pre>
**How to use**
```js
import { getSceneVersion } from "@excalidraw/excalidraw";
```
### sceneCoordsToViewportCoords
This function returns equivalent `viewport` coords for the provided `scene` coords in params.
```js
import { sceneCoordsToViewportCoords } from "@excalidraw/excalidraw";
```
**_Signature_**
<pre>
sceneCoordsToViewportCoords(&#123; sceneX: number, sceneY: number },<br/>&nbsp;
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a><br/>): &#123; x: number, y: number }
</pre>
### viewportCoordsToSceneCoords
This function returns equivalent `scene` coords for the provided `viewport` coords in params.
```js
import { viewportCoordsToSceneCoords } from "@excalidraw/excalidraw";
```
**_Signature_**
<pre>
viewportCoordsToSceneCoords(&#123; clientX: number, clientY: number },<br/>&nbsp;
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a><br/>): &#123;x: number, y: number}
</pre>
### useDevice
This hook can be used to check the type of device which is being used. It can only be used inside the `children` of `Excalidraw` component.
Open the `main menu` in the below example to view the footer.
```jsx live noInline
const MobileFooter = ({}) => {
const device = useDevice();
if (device.isMobile) {
return (
<Footer>
<button
className="custom-footer"
style={{ marginLeft: "20px", height: "2rem" }}
onClick={() => alert("This is custom footer in mobile menu")}
>
custom footer
</button>
</Footer>
);
}
return null;
};
const App = () => (
<div style={{ height: "400px" }}>
<Excalidraw>
<MainMenu>
<MainMenu.Item> Item1 </MainMenu.Item>
<MainMenu.Item> Item 2 </MainMenu.Item>
<MobileFooter />
</MainMenu>
</Excalidraw>
</div>
);
// Need to render when code is span across multiple components
// in Live Code blocks editor
render(<App />);
```
The `device` has the following `attributes`
| Name | Type | Description |
| --- | --- | --- |
| `isSmScreen` | `boolean` | Set to `true` when the device small screen is small (Width < `640px` ) |
| `isMobile` | `boolean` | Set to `true` when the device is `mobile` |
| `isTouchScreen` | `boolean` | Set to `true` for `touch` devices |
| `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` |
### i18n
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) |
```js
import { defaultLang, languages, useI18n } from "@excalidraw/excalidraw";
```
#### defaultLang
Default language code, `en`.
#### languages
List of supported language codes. You can pass any of these to `Excalidraw`'s [`langCode` prop](/docs/@excalidraw/excalidraw/api/props/#langcode).
#### useI18n
A hook that returns the current language code and translation helper function. You can use this to translate strings in the components you render as children of `<Excalidraw>`.
```jsx live
function App() {
const { t } = useI18n();
return (
<div style={{ height: "500px" }}>
<Excalidraw>
<button
style={{ position: "absolute", zIndex: 10, height: "2rem" }}
onClick={() => window.alert(t("labels.madeWithExcalidraw"))}
>
{t("buttons.confirm")}
</button>
</Excalidraw>
</div>
);
}
```

View File

@ -0,0 +1,49 @@
# Customizing Styles
Excalidraw is using CSS variables to style certain components. To override them, you should set your own on the `.excalidraw` and `.excalidraw.theme--dark` (for dark mode variables) selectors.
Make sure the selector has higher specificity, e.g. by prefixing it with your app's selector:
```css
.your-app .excalidraw {
--color-primary: red;
}
.your-app .excalidraw.theme--dark {
--color-primary: pink;
}
```
Most notably, you can customize the primary colors, by overriding these variables:
- `--color-primary`
- `--color-primary-darker`
- `--color-primary-darkest`
- `--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.
```css showLineNumbers
.custom-styles .excalidraw {
--color-primary: #fcc6d9;
--color-primary-darker: #f783ac;
--color-primary-darkest: #e64980;
--color-primary-light: #f2a9c4;
}
.custom-styles .excalidraw.theme--dark {
--color-primary: #d494aa;
--color-primary-darker: #d64c7e;
--color-primary-darkest: #e86e99;
--color-primary-light: #dcbec9;
}
```
```tsx live
function App() {
return (
<div style={{ height: "500px" }} className="custom-styles">
<Excalidraw />
</div>
);
}
```

View File

@ -0,0 +1,59 @@
---
pagination_prev: "@excalidraw/excalidraw/installation"
---
# Development
This page relates to developing the `@excalidraw/excalidraw` package itself.
## Example app
To start the example app using the `@excalidraw/excalidraw` package, follow the below steps:
1. Install the dependencies
```bash
cd src/packages/excalidraw && yarn
```
2. Start the example app
```bash
yarn start
```
[http://localhost:3001](http://localhost:3001) will open in your default browser.
The example is same as the [codesandbox example](https://ehlz3.csb.app/)
## Releasing
### Create a test release
You can create a test release by posting the below comment in your pull request:
```bash
@excalibot trigger release
```
Once the version is released `@excalibot` will post a comment with the release version.
### Creating a production release
To release the next stable version follow the below steps:
```bash
yarn prerelease version
```
You need to pass the `version` for which you want to create the release. This will make the changes needed before making the release like updating `package.json`, `changelog` and more.
The next step is to run the `release` script:
```bash
yarn release
```
This will publish the package.
Right now there are two steps to create a production release but once this works fine these scripts will be combined and more automation will be done.

View File

@ -0,0 +1,37 @@
# FAQ
### Does this package support collaboration ?
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same.
### Turning off Aggressive Anti-Fingerprinting in Brave browser
When *Aggressive Anti-Fingerprinting* is turned on, the `measureText` API breaks which in turn breaks the Text Elements in your drawings. Here is more [info](https://github.com/excalidraw/excalidraw/pull/6336) on the same.
We strongly recommend turning it off. You can follow the steps below on how to do so.
1. Open [excalidraw.com](https://excalidraw.com) in Brave and click on the **Shield** button
![Shield button](../../assets/brave-shield.png)
<div style={{width:'30rem'}}>
2. Once opened, look for **Aggressively Block Fingerprinting**
![Aggresive block fingerprinting](../../assets/aggressive-block-fingerprint.png)
3. Switch to **Block Fingerprinting**
![Block filtering](../../assets/block-fingerprint.png)
4. Thats all. All text elements should be fixed now 🎉
</div>
If disabling this setting doesn't fix the display of text elements, please consider opening an [issue](https://github.com/excalidraw/excalidraw/issues/new) on our GitHub, or message us on [Discord](https://discord.gg/UexuTaE).
## Need help?
Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw).

View File

@ -0,0 +1,43 @@
# Installation
**Excalidraw** is published to npm as a component you can directly embed in your projects.
Using `npm`:
```bash
npm install react react-dom @excalidraw/excalidraw
```
or `yarn`:
```bash
yarn add react react-dom @excalidraw/excalidraw
```
:::tip
**If you don't want to wait for the next stable release and try out the unreleased changes you can use `@excalidraw/excalidraw@next`.**
:::
### Static assets
Excalidraw depends on assets such as localization files (if you opt to use them), fonts, and others.
By default these assets are loaded from a public CDN [`https://unpkg.com/@excalidraw/excalidraw/dist/`](https://unpkg.com/@excalidraw/excalidraw/dist), so you don't need to do anything on your end.
However, if you want to host these files yourself, you can find them in your `node_modules/@excalidraw/excalidraw/dist` directory, in folders `excalidraw-assets` (for production) and `excalidraw-assets-dev` (for development).
Copy these folders to your static assets directory, and add a `window.EXCALIDRAW_ASSET_PATH` variable in your `index.html` or `index.js` entry file pointing to your public assets path (relative). For example, if you serve your assets from the root of your hostname, you would do:
```js
window.EXCALIDRAW_ASSET_PATH = "/";
```
### Dimensions of Excalidraw
Excalidraw takes _100%_ of `width` and `height` of the containing block so make sure the container in which you render Excalidraw has non zero dimensions.
### Demo
[Try here](https://codesandbox.io/s/excalidraw-ehlz3).

View File

@ -0,0 +1,131 @@
# Integration
## Module bundler
If you are using a module bundler (for instance, Webpack), you can import it as an ES6 module as shown below
```js
import { Excalidraw } from "@excalidraw/excalidraw";
```
:::info
Throughout the documentation we use live, editable Excalidraw examples like the one shown below.
While we aim for the examples to closely reflect what you'd get if you rendered it yourself, we actually initialize it with some props behind the scenes.
For example, we're passing a `theme` prop to it based on the current color theme of the docs you're just reading.
:::
```jsx live
function App() {
return (
<>
<h1 style={{ textAlign: "center" }}>Excalidraw Example</h1>
<div style={{ height: "500px" }}>
<Excalidraw />
</div>
</>
);
}
```
### Rendering Excalidraw only on client
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
The following worfklow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon.
```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));
}, []);
return <>{Excalidraw && <Excalidraw />}</>;
}
```
The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm)
## Browser
To use it in a browser directly:
For development use :point_down:
```js
<script
type="text/javascript"
src="https://unpkg.com/@excalidraw/excalidraw/dist/excalidraw.development.js"
></script>
```
For production use :point_down:
```js
<script
type="text/javascript"
src="https://unpkg.com/@excalidraw/excalidraw/dist/excalidraw.production.min.js"
></script>
```
You will need to make sure `react`, `react-dom` is available as shown in the below example. For prod please use the production versions of `react`, `react-dom`.
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
<Tabs>
<TabItem value="html" label="html">
```html
<!DOCTYPE html>
<html>
<head>
<title>Excalidraw in browser</title>
<meta charset="UTF-8" />
<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>
<script
type="text/javascript"
src="https://unpkg.com/@excalidraw/excalidraw/dist/excalidraw.development.js"
></script>
</head>
<body>
<div class="container">
<h1>Excalidraw Embed Example</h1>
<div id="app"></div>
</div>
<script type="text/javascript" src="src/index.js"></script>
</body>
</html>
```
</TabItem>
<TabItem value="js" label="Javascript">
```js showLineNumbers
const App = () => {
return React.createElement(
React.Fragment,
null,
React.createElement(
"div",
{
style: { height: "500px" },
},
React.createElement(ExcalidrawLib.Excalidraw),
),
);
};
const excalidrawWrapper = document.getElementById("app");
const root = ReactDOM.createRoot(excalidrawWrapper);
root.render(React.createElement(App));
```
</TabItem>
</Tabs>

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@ -1,6 +0,0 @@
---
sidebar_position: 1
title: Overview
---
In development. For now, refer to [excalidraw Readme](https://github.com/excalidraw/excalidraw/blob/master/README.md).

View File

@ -1,8 +0,0 @@
---
sidebar_position: 1
title: Introduction
---
Want to integrate Excalidraw into your app? Head over to the [package docs](/docs/package/overview).
If you're looking into the Excalidraw codebase itself, start [here](/docs/codebase/overview).

View File

@ -0,0 +1,72 @@
# Contributing
Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/excalidraw/issues/new) first to discuss what you would like to change.
## Setup
### Option 1 - Manual
1. Fork and clone the repo
1. Run `yarn` to install dependencies
1. Create a branch for your PR with `git checkout -b your-branch-name`
> To keep `master` branch pointing to remote repository and make pull requests from branches on your fork. To do this, run:
>
> ```bash
> git remote add upstream https://github.com/excalidraw/excalidraw.git
> git fetch upstream
> git branch --set-upstream-to=upstream/master master
> ```
### Option 2 - CodeSandbox
1. Go to https://codesandbox.io/p/github/excalidraw/excalidraw
1. Connect your GitHub account
1. Go to Git tab on left side
1. Tap on `Fork Sandbox`
1. Write your code
1. Commit and PR automatically
## Pull Request Guidelines
Don't worry if you get any of the below wrong, or if you don't know how. We'll gladly help out.
### Title
Make sure the title starts with a semantic prefix:
- **feat**: A new feature
- **fix**: A bug fix
- **docs**: Documentation only changes
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
- **refactor**: A code change that neither fixes a bug nor adds a feature
- **perf**: A code change that improves performance
- **test**: Adding missing tests or correcting existing tests
- **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
- **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
- **chore**: Other changes that don't modify src or test files
- **revert**: Reverts a previous commit
### Changelog
Add a brief description of your pull request to the changelog located here: [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.
It's also a good idea to consider if your change should include additional tests. This is highly recommended for new features or bug-fixes. For example, it's good practice to create a test for each bug you fix which ensures that we don't regress the code in the future.
Finally - always manually test your changes using the convenient staging environment deployed for each pull request. As much as local development attempts to replicate production, there can still be subtle differences in behavior. For larger features consider testing your change in multiple browsers as well.
## Translating
To translate Excalidraw into other languages, please visit [our Crowdin page](https://crowdin.com/project/excalidraw). To add a new language, [open an issue](https://github.com/excalidraw/excalidraw/issues/new) so we can get things set up on our end first.
Translations will be available on the app if they exceed a certain threshold of completion (currently **85%**).

View File

@ -0,0 +1,102 @@
# Development
## Code Sandbox
- Go to https://codesandbox.io/p/github/excalidraw/excalidraw
- You may need to sign in with GitHub and reload the page
- You can start coding instantly, and even send PRs from there!
## Local Installation
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
### Requirements
- [Node.js](https://nodejs.org/en/)
- [Yarn](https://yarnpkg.com/getting-started/install) (v1 or v2.4.2+)
- [Git](https://git-scm.com/downloads)
### Clone the repo
```bash
git clone https://github.com/excalidraw/excalidraw.git
```
### Install the dependencies
```bash
yarn
```
### Start the server
```bash
yarn start
```
Now you can open [http://localhost:3000](http://localhost:3000) and start coding in your favorite code editor.
## Collaboration
For collaboration, you will need to set up [collab server](https://github.com/excalidraw/excalidraw-room) in local.
## Commands
### Install the dependencies
```bash
yarn
```
### Run the project
```bash
yarn start
```
### Reformat all files with Prettier
```bash
yarn fix
```
### Run tests
```bash
yarn test
```
### Update test snapshots
```bash
yarn test:update
```
### Test for formatting with Prettier
```bash
yarn test:code
```
### Docker Compose
You can use docker-compose to work on Excalidraw locally if you don't want to setup a Node.js env.
```bash
docker-compose up --build -d
```
## Self-hosting
We publish a Docker image with the Excalidraw client at [excalidraw/excalidraw](https://hub.docker.com/r/excalidraw/excalidraw). You can use it to self-host your own client under your own domain, on Kubernetes, AWS ECS, etc.
```bash
docker build -t excalidraw/excalidraw .
docker run --rm -dit --name excalidraw -p 5000:80 excalidraw/excalidraw:latest
```
The Docker image is free of analytics and other tracking libraries.
**At the moment, self-hosting your own instance doesn't support sharing or collaboration features.**
We are working towards providing a full-fledged solution for self-hosting your own Excalidraw.

View File

@ -0,0 +1,16 @@
---
title: Introduction
slug: ../
---
## Try now
Go to [excalidraw.com](https://excalidraw.com) to start sketching.
## How are these docs structured
These docs are focused on developers, and structured in the following way:
- [Introduction](/docs/) — development setup and introduction.
- [@excalidraw/excalidraw](/docs/@excalidraw/excalidraw/installation) — docs for the npm package to help you integrate Excalidraw into your own app.
- Editor — IN PROGRESS. Docs describing the internals of the Excalidraw editor to help in contributing to the codebase.

View File

@ -1,6 +0,0 @@
---
sidebar_position: 1
title: Overview
---
In development. For now, refer to [excalidraw package readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md).

View File

@ -1,15 +1,12 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
const lightCodeTheme = require("prism-react-renderer/themes/github");
const darkCodeTheme = require("prism-react-renderer/themes/dracula");
/** @type {import('@docusaurus/types').Config} */
const config = {
title: "Excalidraw developer docs",
tagline:
"For Excalidraw contributors or those integrating the Excalidraw editor",
url: "https://docs.excalidraw.com.com",
url: "https://docs.excalidraw.com",
baseUrl: "/",
onBrokenLinks: "throw",
onBrokenMarkdownLinks: "warn",
@ -33,10 +30,16 @@ const config = {
docs: {
sidebarPath: require.resolve("./sidebars.js"),
// Please change this to your repo.
editUrl: "https://github.com/excalidraw/docs/tree/master/",
editUrl:
"https://github.com/excalidraw/excalidraw/tree/master/dev-docs/",
showLastUpdateAuthor: true,
showLastUpdateTime: true,
},
theme: {
customCss: require.resolve("./src/css/custom.css"),
customCss: [
require.resolve("./src/css/custom.scss"),
require.resolve("../src/packages/excalidraw/example/App.scss"),
],
},
}),
],
@ -45,18 +48,20 @@ const config = {
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
colorMode: {
respectPrefersColorScheme: true,
},
navbar: {
title: "Excalidraw Docs",
title: "Excalidraw",
logo: {
alt: "Excalidraw Logo",
src: "img/logo.svg",
},
items: [
{
type: "doc",
docId: "get-started",
to: "/docs",
position: "left",
label: "Get started",
label: "Docs",
},
{
to: "https://blog.excalidraw.com",
@ -78,7 +83,7 @@ const config = {
items: [
{
label: "Get Started",
to: "/docs/get-started",
to: "/docs",
},
],
},
@ -93,6 +98,10 @@ const config = {
label: "Twitter",
href: "https://twitter.com/excalidraw",
},
{
label: "Linkedin",
href: "https://www.linkedin.com/company/excalidraw",
},
],
},
{
@ -109,13 +118,28 @@ const config = {
],
},
],
copyright: `Made with ❤️ Built with Docusaurus`,
copyright: `Copyright © 2023 Excalidraw community. Built with Docusaurus ❤️`,
},
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
theme: require("prism-react-renderer/themes/dracula"),
},
image: "img/og-image.png",
docs: {
sidebar: {
hideable: true,
},
},
tableOfContents: {
maxHeadingLevel: 4,
},
algolia: {
appId: "8FEAOD28DI",
apiKey: "4b07cca33ff2d2919bc95ff98f148e9e",
indexName: "excalidraw",
},
}),
themes: ["@docusaurus/theme-live-codeblock"],
plugins: ["docusaurus-plugin-sass"],
};
module.exports = config;

View File

@ -15,13 +15,17 @@
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "2.0.0-rc.1",
"@docusaurus/preset-classic": "2.0.0-rc.1",
"@docusaurus/core": "2.2.0",
"@docusaurus/preset-classic": "2.2.0",
"@docusaurus/theme-live-codeblock": "2.2.0",
"@excalidraw/excalidraw": "0.15.2",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"docusaurus-plugin-sass": "0.2.3",
"prism-react-renderer": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react-dom": "^17.0.2",
"sass": "1.57.1"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-rc.1",

View File

@ -13,19 +13,86 @@
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
// By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [{ type: "autogenerated", dirName: "." }],
// But you can create a sidebar manually
/*
tutorialSidebar: [
docs: [
{
type: 'category',
label: 'Tutorial',
items: ['hello'],
type: "category",
label: "Introduction",
link: {
type: "doc",
id: "introduction/get-started",
},
items: ["introduction/development", "introduction/contributing"],
},
{
type: "category",
label: "@excalidraw/excalidraw",
collapsed: false,
items: [
"@excalidraw/excalidraw/installation",
"@excalidraw/excalidraw/integration",
"@excalidraw/excalidraw/customizing-styles",
{
type: "category",
label: "API",
link: {
type: "doc",
id: "@excalidraw/excalidraw/api/api-intro",
},
items: [
{
type: "category",
label: "Props",
link: {
type: "doc",
id: "@excalidraw/excalidraw/api/props/props",
},
items: [
"@excalidraw/excalidraw/api/props/initialdata",
"@excalidraw/excalidraw/api/props/ref",
"@excalidraw/excalidraw/api/props/render-props",
"@excalidraw/excalidraw/api/props/ui-options",
],
},
{
type: "category",
label: "Children Components",
link: {
type: "doc",
id: "@excalidraw/excalidraw/api/children-components/children-components-intro",
},
items: [
"@excalidraw/excalidraw/api/children-components/main-menu",
"@excalidraw/excalidraw/api/children-components/welcome-screen",
"@excalidraw/excalidraw/api/children-components/footer",
"@excalidraw/excalidraw/api/children-components/live-collaboration-trigger",
],
},
{
type: "category",
label: "Utils",
link: {
type: "doc",
id: "@excalidraw/excalidraw/api/utils/utils-intro",
},
items: [
"@excalidraw/excalidraw/api/utils/export",
"@excalidraw/excalidraw/api/utils/restore",
],
},
{
type: "category",
label: "Constants",
link: { type: "doc", id: "@excalidraw/excalidraw/api/constants" },
items: [],
},
],
},
"@excalidraw/excalidraw/faq",
"@excalidraw/excalidraw/development",
],
},
],
*/
};
module.exports = sidebars;

View File

@ -0,0 +1,15 @@
import React from "react";
export default function Highlight({ children }) {
return (
<span
style={{
backgroundColor: "#7874e8",
borderRadius: "2px",
color: "#fff",
padding: "0.2rem",
}}
>
{children}
</span>
);
}

View File

@ -14,11 +14,13 @@
--ifm-color-primary-lighter: #5b57d1;
--ifm-color-primary-lightest: #5b57d1;
--ifm-code-font-size: 95%;
scrollbar-gutter: stable;
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme="dark"] {
--ifm-color-primary: #5650f0;
--ifm-color-primary: #8784e3;
--ifm-color-primary-dark: #4b46d8;
--ifm-color-primary-darker: #4b46d8;
--ifm-color-primary-darkest: #3e39be;
@ -41,3 +43,59 @@
[data-theme="dark"] .navbar__logo {
filter: invert(93%) hue-rotate(180deg);
}
pre a {
color: #5dccff;
&:hover {
color: #8fd3f3;
}
}
.custom-button {
height: 40px;
max-width: 200px;
margin: 10px 0;
padding: 5px;
background: #70b1ec;
color: white;
font-weight: bold;
border: none;
}
.custom-styles .excalidraw {
--color-primary: #fcc6d9;
--color-primary-darker: #f783ac;
--color-primary-darkest: #e64980;
--color-primary-light: #f2a9c4;
}
.custom-styles .excalidraw.theme--dark {
--color-primary: #d494aa;
--color-primary-darker: #d64c7e;
--color-primary-darkest: #e86e99;
--color-primary-light: #dcbec9;
}
/* The global css conflicts with Excal css hence overriding */
.excalidraw .context-menu-item__shortcut {
background-color: transparent;
border: none;
box-shadow: none;
padding: 0;
}
.excalidraw .Stats table td,
.excalidraw .Stats table th,
.excalidraw .Stats table tr {
border: none;
background: none;
padding: 0;
}
.excalidraw .Stats .close {
padding: 0;
}
.excalidraw .Stats table {
display: table;
}

1230
dev-docs/src/initialData.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +0,0 @@
import React from "react";
import clsx from "clsx";
import Layout from "@theme/Layout";
import Link from "@docusaurus/Link";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import styles from "./index.module.css";
import HomepageFeatures from "@site/src/components/Homepage";
function HomepageHeader() {
const { siteConfig } = useDocusaurusContext();
return (
<header className={clsx("hero hero--primary", styles.heroBanner)}>
<div className="container">
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="/docs/get-started"
>
Get started
</Link>
</div>
</div>
</header>
);
}
export default function Home() {
const { siteConfig } = useDocusaurusContext();
return (
<Layout
title={`Hello from ${siteConfig.title}`}
description="Description will go into a meta tag in <head />"
>
<HomepageHeader />
<main>
<HomepageFeatures />
</main>
</Layout>
);
}

View File

@ -14,10 +14,7 @@ function HomepageHeader() {
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="/docs/get-started"
>
<Link className="button button--secondary button--lg" to="/docs">
Get started
</Link>
</div>
@ -27,12 +24,8 @@ function HomepageHeader() {
}
export default function Home() {
const { siteConfig } = useDocusaurusContext();
return (
<Layout
title={`Hello from ${siteConfig.title}`}
description="Description will go into a meta tag in <head />"
>
<Layout description="For Excalidraw contributors or those integrating the Excalidraw editor">
<HomepageHeader />
<main>
<HomepageFeatures />

View File

@ -0,0 +1,11 @@
// Import the original mapper
import MDXComponents from "@theme-original/MDXComponents";
import Highlight from "@site/src/components/Highlight";
export default {
// Re-use the default mapping
...MDXComponents,
// Map the "highlight" tag to our <Highlight /> component!
// `Highlight` will receive all props that were passed to `highlight` in MDX
highlight: Highlight,
};

View File

@ -0,0 +1,30 @@
import React from "react";
import ExecutionEnvironment from "@docusaurus/ExecutionEnvironment";
import initialData from "@site/src/initialData";
import { useColorMode } from "@docusaurus/theme-common";
let ExcalidrawComp = {};
if (ExecutionEnvironment.canUseDOM) {
ExcalidrawComp = require("@excalidraw/excalidraw");
}
const Excalidraw = React.forwardRef((props, ref) => {
const { colorMode } = useColorMode();
return <ExcalidrawComp.Excalidraw theme={colorMode} {...props} ref={ref} />;
});
// Add react-live imports you need here
const ExcalidrawScope = {
React,
...React,
Excalidraw,
Footer: ExcalidrawComp.Footer,
useDevice: ExcalidrawComp.useDevice,
MainMenu: ExcalidrawComp.MainMenu,
WelcomeScreen: ExcalidrawComp.WelcomeScreen,
LiveCollaborationTrigger: ExcalidrawComp.LiveCollaborationTrigger,
Sidebar: ExcalidrawComp.Sidebar,
exportToCanvas: ExcalidrawComp.exportToCanvas,
initialData,
useI18n: ExcalidrawComp.useI18n,
};
export default ExcalidrawScope;

View File

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

@ -1191,10 +1191,10 @@
"@docsearch/css" "3.1.1"
algoliasearch "^4.0.0"
"@docusaurus/core@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-2.0.0-rc.1.tgz#828d93d241171565d8947a9ab404091e04759141"
integrity sha512-b9FX0Z+EddfQ6wAiNh+Wx4fysKfcvEcWJrZ5USROn3C+EVU5P4luaa8mwWK//O+hTwD9ur7/A44IZ/tWCTAoLQ==
"@docusaurus/core@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-2.2.0.tgz#64c9ee31502c23b93c869f8188f73afaf5fd4867"
integrity sha512-Vd6XOluKQqzG12fEs9prJgDtyn6DPok9vmUWDR2E6/nV5Fl9SVkhEQOBxwObjk3kQh7OY7vguFaLh0jqdApWsA==
dependencies:
"@babel/core" "^7.18.6"
"@babel/generator" "^7.18.7"
@ -1206,13 +1206,13 @@
"@babel/runtime" "^7.18.6"
"@babel/runtime-corejs3" "^7.18.6"
"@babel/traverse" "^7.18.8"
"@docusaurus/cssnano-preset" "2.0.0-rc.1"
"@docusaurus/logger" "2.0.0-rc.1"
"@docusaurus/mdx-loader" "2.0.0-rc.1"
"@docusaurus/cssnano-preset" "2.2.0"
"@docusaurus/logger" "2.2.0"
"@docusaurus/mdx-loader" "2.2.0"
"@docusaurus/react-loadable" "5.5.2"
"@docusaurus/utils" "2.0.0-rc.1"
"@docusaurus/utils-common" "2.0.0-rc.1"
"@docusaurus/utils-validation" "2.0.0-rc.1"
"@docusaurus/utils" "2.2.0"
"@docusaurus/utils-common" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
"@slorber/static-site-generator-webpack-plugin" "^4.0.7"
"@svgr/webpack" "^6.2.1"
autoprefixer "^10.4.7"
@ -1268,33 +1268,33 @@
webpack-merge "^5.8.0"
webpackbar "^5.0.2"
"@docusaurus/cssnano-preset@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-2.0.0-rc.1.tgz#76bbd7f6912779a0667f8f2fd8fc1a05618a6148"
integrity sha512-9/KmQvF+eTlMqUqG6UcXbRgxbGed/8bQInXuKEs+95/jI6jO/3xSzuRwuHHHP0naUvSVWjnNI9jngPrQerXE5w==
"@docusaurus/cssnano-preset@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-2.2.0.tgz#fc05044659051ae74ab4482afcf4a9936e81d523"
integrity sha512-mAAwCo4n66TMWBH1kXnHVZsakW9VAXJzTO4yZukuL3ro4F+JtkMwKfh42EG75K/J/YIFQG5I/Bzy0UH/hFxaTg==
dependencies:
cssnano-preset-advanced "^5.3.8"
postcss "^8.4.14"
postcss-sort-media-queries "^4.2.1"
tslib "^2.4.0"
"@docusaurus/logger@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-2.0.0-rc.1.tgz#db95e9b15bc243695830a5b791c0eff705ef1b54"
integrity sha512-daa3g+SXuO9K60PVMiSUmDEK9Vro+Ed7i7uF8CH6QQJLcNZy/zJc0Xz62eH7ip1x77fmeb6Rg4Us1TqTFc9AbQ==
"@docusaurus/logger@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-2.2.0.tgz#ea2f7feda7b8675485933b87f06d9c976d17423f"
integrity sha512-DF3j1cA5y2nNsu/vk8AG7xwpZu6f5MKkPPMaaIbgXLnWGfm6+wkOeW7kNrxnM95YOhKUkJUophX69nGUnLsm0A==
dependencies:
chalk "^4.1.2"
tslib "^2.4.0"
"@docusaurus/mdx-loader@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-2.0.0-rc.1.tgz#e78d7d416aacc289f2427c5ccdb9145820acb0cb"
integrity sha512-8Fg0c/ceu39knmr7w0dutm7gq3YxKYCqWVS2cB/cPATzChCCNH/AGLfBT6sz/Z4tjVXE+NyREq2pfOFvkhjVXg==
"@docusaurus/mdx-loader@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-2.2.0.tgz#fd558f429e5d9403d284bd4214e54d9768b041a0"
integrity sha512-X2bzo3T0jW0VhUU+XdQofcEeozXOTmKQMvc8tUnWRdTnCvj4XEcBVdC3g+/jftceluiwSTNRAX4VBOJdNt18jA==
dependencies:
"@babel/parser" "^7.18.8"
"@babel/traverse" "^7.18.8"
"@docusaurus/logger" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
"@docusaurus/logger" "2.2.0"
"@docusaurus/utils" "2.2.0"
"@mdx-js/mdx" "^1.6.22"
escape-html "^1.0.3"
file-loader "^6.2.0"
@ -1323,18 +1323,32 @@
react-helmet-async "*"
react-loadable "npm:@docusaurus/react-loadable@5.5.2"
"@docusaurus/plugin-content-blog@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.0.0-rc.1.tgz#8ae5d5ec2da08c583a057bf2754a5b9278b3eb08"
integrity sha512-BVVrAGZujpjS/0rarY2o24rlylRRh2NZuM65kg0JNkkViF79SeEHsepog7IuHyoqGWPm1N/I7LpEp7k+gowZzQ==
"@docusaurus/module-type-aliases@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-2.2.0.tgz#1e23e54a1bbb6fde1961e4fa395b1b69f4803ba5"
integrity sha512-wDGW4IHKoOr9YuJgy7uYuKWrDrSpsUSDHLZnWQYM9fN7D5EpSmYHjFruUpKWVyxLpD/Wh0rW8hYZwdjJIQUQCQ==
dependencies:
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/logger" "2.0.0-rc.1"
"@docusaurus/mdx-loader" "2.0.0-rc.1"
"@docusaurus/types" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
"@docusaurus/utils-common" "2.0.0-rc.1"
"@docusaurus/utils-validation" "2.0.0-rc.1"
"@docusaurus/react-loadable" "5.5.2"
"@docusaurus/types" "2.2.0"
"@types/history" "^4.7.11"
"@types/react" "*"
"@types/react-router-config" "*"
"@types/react-router-dom" "*"
react-helmet-async "*"
react-loadable "npm:@docusaurus/react-loadable@5.5.2"
"@docusaurus/plugin-content-blog@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.2.0.tgz#dc55982e76771f4e678ac10e26d10e1da2011dc1"
integrity sha512-0mWBinEh0a5J2+8ZJXJXbrCk1tSTNf7Nm4tYAl5h2/xx+PvH/Bnu0V+7mMljYm/1QlDYALNIIaT/JcoZQFUN3w==
dependencies:
"@docusaurus/core" "2.2.0"
"@docusaurus/logger" "2.2.0"
"@docusaurus/mdx-loader" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/utils" "2.2.0"
"@docusaurus/utils-common" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
cheerio "^1.0.0-rc.12"
feed "^4.2.2"
fs-extra "^10.1.0"
@ -1345,18 +1359,18 @@
utility-types "^3.10.0"
webpack "^5.73.0"
"@docusaurus/plugin-content-docs@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.0.0-rc.1.tgz#2dda88166bf21b0eeb3821ef748059b20c8c49f7"
integrity sha512-Yk5Hu6uaw3tRplzJnbDygwRhmZ3PCzEXD4SJpBA6cPC73ylfqOEh6qhiU+BWhMTtDXNhY+athk5Kycfk3DW1aQ==
"@docusaurus/plugin-content-docs@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.2.0.tgz#0fcb85226fcdb80dc1e2d4a36ef442a650dcc84d"
integrity sha512-BOazBR0XjzsHE+2K1wpNxz5QZmrJgmm3+0Re0EVPYFGW8qndCWGNtXW/0lGKhecVPML8yyFeAmnUCIs7xM2wPw==
dependencies:
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/logger" "2.0.0-rc.1"
"@docusaurus/mdx-loader" "2.0.0-rc.1"
"@docusaurus/module-type-aliases" "2.0.0-rc.1"
"@docusaurus/types" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
"@docusaurus/utils-validation" "2.0.0-rc.1"
"@docusaurus/core" "2.2.0"
"@docusaurus/logger" "2.2.0"
"@docusaurus/mdx-loader" "2.2.0"
"@docusaurus/module-type-aliases" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/utils" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
"@types/react-router-config" "^5.0.6"
combine-promises "^1.1.0"
fs-extra "^10.1.0"
@ -1367,84 +1381,84 @@
utility-types "^3.10.0"
webpack "^5.73.0"
"@docusaurus/plugin-content-pages@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.0.0-rc.1.tgz#2be82f53d6c77e6aa66787726c30dc60b210e6f8"
integrity sha512-FdO79WC5hfWDQu3/CTFLRQzTNc0e5n+HNzavm2MNkSzGV08BFJ6RAkbPbtra5CWef+6iXZav6D/tzv2jDPvLzA==
"@docusaurus/plugin-content-pages@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.2.0.tgz#e3f40408787bbe229545dd50595f87e1393bc3ae"
integrity sha512-+OTK3FQHk5WMvdelz8v19PbEbx+CNT6VSpx7nVOvMNs5yJCKvmqBJBQ2ZSxROxhVDYn+CZOlmyrC56NSXzHf6g==
dependencies:
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/mdx-loader" "2.0.0-rc.1"
"@docusaurus/types" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
"@docusaurus/utils-validation" "2.0.0-rc.1"
"@docusaurus/core" "2.2.0"
"@docusaurus/mdx-loader" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/utils" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
fs-extra "^10.1.0"
tslib "^2.4.0"
webpack "^5.73.0"
"@docusaurus/plugin-debug@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-2.0.0-rc.1.tgz#73c06ad08d66810941e456d50b07be008f5235cb"
integrity sha512-aOsyYrPMbnsyqHwsVZ+0frrMRtnYqm4eaJpG4sC/6LYAJ07IDRQ9j3GOku2dKr5GsFK1Vx7VlE6ZLwe0MaGstg==
"@docusaurus/plugin-debug@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-2.2.0.tgz#b38741d2c492f405fee01ee0ef2e0029cedb689a"
integrity sha512-p9vOep8+7OVl6r/NREEYxf4HMAjV8JMYJ7Bos5fCFO0Wyi9AZEo0sCTliRd7R8+dlJXZEgcngSdxAUo/Q+CJow==
dependencies:
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/types" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
"@docusaurus/core" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/utils" "2.2.0"
fs-extra "^10.1.0"
react-json-view "^1.21.3"
tslib "^2.4.0"
"@docusaurus/plugin-google-analytics@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.0.0-rc.1.tgz#0136cc7534573ca56e023178ec2bda5c1e89ce71"
integrity sha512-f+G8z5OJWfg5QqWDLIdcN2SDoK5J5Gg8HMrqCI6Pfl+rxPb5I1niA+/UkAM+kMCpnekvhSt5AWz2fgkRenkPLA==
"@docusaurus/plugin-google-analytics@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.2.0.tgz#63c7137eff5a1208d2059fea04b5207c037d7954"
integrity sha512-+eZVVxVeEnV5nVQJdey9ZsfyEVMls6VyWTIj8SmX0k5EbqGvnIfET+J2pYEuKQnDIHxy+syRMoRM6AHXdHYGIg==
dependencies:
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/types" "2.0.0-rc.1"
"@docusaurus/utils-validation" "2.0.0-rc.1"
"@docusaurus/core" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
tslib "^2.4.0"
"@docusaurus/plugin-google-gtag@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.0.0-rc.1.tgz#61698fdc41a4ace912fb8f6c834efd288edad3c0"
integrity sha512-yE1Et9hhhX9qMRnMJzpNq0854qIYiSEc2dZaXNk537HN7Q0rKkr/YONUHz2iqNYwPX2hGOY4LdpTxlMP88uVhA==
"@docusaurus/plugin-google-gtag@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.2.0.tgz#7b086d169ac5fe9a88aca10ab0fd2bf00c6c6b12"
integrity sha512-6SOgczP/dYdkqUMGTRqgxAS1eTp6MnJDAQMy8VCF1QKbWZmlkx4agHDexihqmYyCujTYHqDAhm1hV26EET54NQ==
dependencies:
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/types" "2.0.0-rc.1"
"@docusaurus/utils-validation" "2.0.0-rc.1"
"@docusaurus/core" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
tslib "^2.4.0"
"@docusaurus/plugin-sitemap@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.0.0-rc.1.tgz#0b638e774b253d90e9f2d11663e961250f557bc4"
integrity sha512-5JmbNpssUF03odFM4ArvIsrO9bv7HnAJ0VtefXhh0WBpaFs8NgI3rTkCTFimvtRQjDR9U2bh23fXz2vjQQz6oA==
"@docusaurus/plugin-sitemap@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.2.0.tgz#876da60937886032d63143253d420db6a4b34773"
integrity sha512-0jAmyRDN/aI265CbWZNZuQpFqiZuo+5otk2MylU9iVrz/4J7gSc+ZJ9cy4EHrEsW7PV8s1w18hIEsmcA1YgkKg==
dependencies:
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/logger" "2.0.0-rc.1"
"@docusaurus/types" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
"@docusaurus/utils-common" "2.0.0-rc.1"
"@docusaurus/utils-validation" "2.0.0-rc.1"
"@docusaurus/core" "2.2.0"
"@docusaurus/logger" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/utils" "2.2.0"
"@docusaurus/utils-common" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
fs-extra "^10.1.0"
sitemap "^7.1.1"
tslib "^2.4.0"
"@docusaurus/preset-classic@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-2.0.0-rc.1.tgz#5e5b1cf80b3dd4e2c3f824c78a111f105858d853"
integrity sha512-5jjTVZkhArjyoNHwCI9x4PSG0zPmBJILjZLVrxPcHpm/K0ltkYcp6J3GxYpf5EbMuOh5+yCWM63cSshGcNOo3Q==
"@docusaurus/preset-classic@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-2.2.0.tgz#bece5a043eeb74430f7c6c7510000b9c43669eb7"
integrity sha512-yKIWPGNx7BT8v2wjFIWvYrS+nvN04W+UameSFf8lEiJk6pss0kL6SG2MRvyULiI3BDxH+tj6qe02ncpSPGwumg==
dependencies:
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/plugin-content-blog" "2.0.0-rc.1"
"@docusaurus/plugin-content-docs" "2.0.0-rc.1"
"@docusaurus/plugin-content-pages" "2.0.0-rc.1"
"@docusaurus/plugin-debug" "2.0.0-rc.1"
"@docusaurus/plugin-google-analytics" "2.0.0-rc.1"
"@docusaurus/plugin-google-gtag" "2.0.0-rc.1"
"@docusaurus/plugin-sitemap" "2.0.0-rc.1"
"@docusaurus/theme-classic" "2.0.0-rc.1"
"@docusaurus/theme-common" "2.0.0-rc.1"
"@docusaurus/theme-search-algolia" "2.0.0-rc.1"
"@docusaurus/types" "2.0.0-rc.1"
"@docusaurus/core" "2.2.0"
"@docusaurus/plugin-content-blog" "2.2.0"
"@docusaurus/plugin-content-docs" "2.2.0"
"@docusaurus/plugin-content-pages" "2.2.0"
"@docusaurus/plugin-debug" "2.2.0"
"@docusaurus/plugin-google-analytics" "2.2.0"
"@docusaurus/plugin-google-gtag" "2.2.0"
"@docusaurus/plugin-sitemap" "2.2.0"
"@docusaurus/theme-classic" "2.2.0"
"@docusaurus/theme-common" "2.2.0"
"@docusaurus/theme-search-algolia" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2":
version "5.5.2"
@ -1454,23 +1468,23 @@
"@types/react" "*"
prop-types "^15.6.2"
"@docusaurus/theme-classic@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-2.0.0-rc.1.tgz#4ab30745e6b03b0f277837debae786a0a83aee6a"
integrity sha512-qNiz7ieeq3AC+V8TbW6S63pWLJph1CbzWDDPTqxDLHgA8VQaNaSmJM8S92pH+yKALRb9u14ogjjYYc75Nj2JmQ==
"@docusaurus/theme-classic@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-2.2.0.tgz#a048bb1bc077dee74b28bec25f4b84b481863742"
integrity sha512-kjbg/qJPwZ6H1CU/i9d4l/LcFgnuzeiGgMQlt6yPqKo0SOJIBMPuz7Rnu3r/WWbZFPi//o8acclacOzmXdUUEg==
dependencies:
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/mdx-loader" "2.0.0-rc.1"
"@docusaurus/module-type-aliases" "2.0.0-rc.1"
"@docusaurus/plugin-content-blog" "2.0.0-rc.1"
"@docusaurus/plugin-content-docs" "2.0.0-rc.1"
"@docusaurus/plugin-content-pages" "2.0.0-rc.1"
"@docusaurus/theme-common" "2.0.0-rc.1"
"@docusaurus/theme-translations" "2.0.0-rc.1"
"@docusaurus/types" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
"@docusaurus/utils-common" "2.0.0-rc.1"
"@docusaurus/utils-validation" "2.0.0-rc.1"
"@docusaurus/core" "2.2.0"
"@docusaurus/mdx-loader" "2.2.0"
"@docusaurus/module-type-aliases" "2.2.0"
"@docusaurus/plugin-content-blog" "2.2.0"
"@docusaurus/plugin-content-docs" "2.2.0"
"@docusaurus/plugin-content-pages" "2.2.0"
"@docusaurus/theme-common" "2.2.0"
"@docusaurus/theme-translations" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/utils" "2.2.0"
"@docusaurus/utils-common" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
"@mdx-js/react" "^1.6.22"
clsx "^1.2.1"
copy-text-to-clipboard "^3.0.1"
@ -1485,17 +1499,17 @@
tslib "^2.4.0"
utility-types "^3.10.0"
"@docusaurus/theme-common@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-2.0.0-rc.1.tgz#ea5d9732a16b03b488555e50107161bfa2abad98"
integrity sha512-1r9ZLKD9SeoCYVzWzcdR79Dia4ANlrlRjNl6uzETOEybjK6FF7yEa9Yra8EJcOCbi3coyYz5xFh/r1YHFTFHug==
"@docusaurus/theme-common@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-2.2.0.tgz#2303498d80448aafdd588b597ce9d6f4cfa930e4"
integrity sha512-R8BnDjYoN90DCL75gP7qYQfSjyitXuP9TdzgsKDmSFPNyrdE3twtPNa2dIN+h+p/pr+PagfxwWbd6dn722A1Dw==
dependencies:
"@docusaurus/mdx-loader" "2.0.0-rc.1"
"@docusaurus/module-type-aliases" "2.0.0-rc.1"
"@docusaurus/plugin-content-blog" "2.0.0-rc.1"
"@docusaurus/plugin-content-docs" "2.0.0-rc.1"
"@docusaurus/plugin-content-pages" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
"@docusaurus/mdx-loader" "2.2.0"
"@docusaurus/module-type-aliases" "2.2.0"
"@docusaurus/plugin-content-blog" "2.2.0"
"@docusaurus/plugin-content-docs" "2.2.0"
"@docusaurus/plugin-content-pages" "2.2.0"
"@docusaurus/utils" "2.2.0"
"@types/history" "^4.7.11"
"@types/react" "*"
"@types/react-router-config" "*"
@ -1505,19 +1519,34 @@
tslib "^2.4.0"
utility-types "^3.10.0"
"@docusaurus/theme-search-algolia@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.0.0-rc.1.tgz#e78c0aeaea6a3717ae3a6ecd75a8652bd7c8e974"
integrity sha512-H5yq6V/B4qo6GZrDKMbeSpk3T9e9K2MliDzLonRu0w3QHW9orVGe0c/lZvRbGlDZjnsOo7XGddhXXIDWGwnpaA==
"@docusaurus/theme-live-codeblock@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-live-codeblock/-/theme-live-codeblock-2.2.0.tgz#a507e496a1a74d261beee30ad072e4341310809a"
integrity sha512-4XRFxfZGcyqmbLmNbnbZ2ZOsoY7FYCJUZKsYW5yzhZYjmjGg7lkdJH5trt9otUoKBsZopBpPWvcDZwCu1SENYg==
dependencies:
"@docusaurus/core" "2.2.0"
"@docusaurus/theme-common" "2.2.0"
"@docusaurus/theme-translations" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
"@philpl/buble" "^0.19.7"
clsx "^1.2.1"
fs-extra "^10.1.0"
react-live "2.2.3"
tslib "^2.4.0"
"@docusaurus/theme-search-algolia@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.2.0.tgz#77fd9f7a600917e6024fe3ac7fb6cfdf2ce84737"
integrity sha512-2h38B0tqlxgR2FZ9LpAkGrpDWVdXZ7vltfmTdX+4RsDs3A7khiNsmZB+x/x6sA4+G2V2CvrsPMlsYBy5X+cY1w==
dependencies:
"@docsearch/react" "^3.1.1"
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/logger" "2.0.0-rc.1"
"@docusaurus/plugin-content-docs" "2.0.0-rc.1"
"@docusaurus/theme-common" "2.0.0-rc.1"
"@docusaurus/theme-translations" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
"@docusaurus/utils-validation" "2.0.0-rc.1"
"@docusaurus/core" "2.2.0"
"@docusaurus/logger" "2.2.0"
"@docusaurus/plugin-content-docs" "2.2.0"
"@docusaurus/theme-common" "2.2.0"
"@docusaurus/theme-translations" "2.2.0"
"@docusaurus/utils" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
algoliasearch "^4.13.1"
algoliasearch-helper "^3.10.0"
clsx "^1.2.1"
@ -1527,10 +1556,10 @@
tslib "^2.4.0"
utility-types "^3.10.0"
"@docusaurus/theme-translations@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-2.0.0-rc.1.tgz#bd647f78c741ee7f6c6d2cbbd3e3f282ef2f89ad"
integrity sha512-JLhNdlnbQhxVQzOnLyiCaTzKFa1lpVrM3nCrkGQKscoG2rY6ARGYMgMN2DkoH6hm7TflQ8+PE1S5MzzASeLs4Q==
"@docusaurus/theme-translations@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-2.2.0.tgz#5fbd4693679806f80c26eeae1381e1f2c23d83e7"
integrity sha512-3T140AG11OjJrtKlY4pMZ5BzbGRDjNs2co5hJ6uYJG1bVWlhcaFGqkaZ5lCgKflaNHD7UHBHU9Ec5f69jTdd6w==
dependencies:
fs-extra "^10.1.0"
tslib "^2.4.0"
@ -1549,30 +1578,44 @@
webpack "^5.73.0"
webpack-merge "^5.8.0"
"@docusaurus/utils-common@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-2.0.0-rc.1.tgz#3e233a28794325d5d9d3af3f7b1c22b59aa8b847"
integrity sha512-+iZICpeFPZJ9oGJXuG92WTWee6WRnVx5BdzlcfuKf/f5KQX8PvwXR2tDME78FGGhShB8zr+vjuNEXuLvXT7j2A==
"@docusaurus/types@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-2.2.0.tgz#02c577a4041ab7d058a3c214ccb13647e21a9857"
integrity sha512-b6xxyoexfbRNRI8gjblzVOnLr4peCJhGbYGPpJ3LFqpi5nsFfoK4mmDLvWdeah0B7gmJeXabN7nQkFoqeSdmOw==
dependencies:
"@types/history" "^4.7.11"
"@types/react" "*"
commander "^5.1.0"
joi "^17.6.0"
react-helmet-async "^1.3.0"
utility-types "^3.10.0"
webpack "^5.73.0"
webpack-merge "^5.8.0"
"@docusaurus/utils-common@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-2.2.0.tgz#a401c1b93a8697dd566baf6ac64f0fdff1641a78"
integrity sha512-qebnerHp+cyovdUseDQyYFvMW1n1nv61zGe5JJfoNQUnjKuApch3IVsz+/lZ9a38pId8kqehC1Ao2bW/s0ntDA==
dependencies:
tslib "^2.4.0"
"@docusaurus/utils-validation@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-2.0.0-rc.1.tgz#dded12f036cda8a54a19e01694b35859fe0cf1d5"
integrity sha512-lj36gm9Ksu4tt/EUeLDWoMbXe3sfBxeIPIUUdqYcBYkF/rpQkh+uL/dncjNGiw6uvBOqXhOfsFVP045HtgShVw==
"@docusaurus/utils-validation@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-2.2.0.tgz#04d4d103137ad0145883971d3aa497f4a1315f25"
integrity sha512-I1hcsG3yoCkasOL5qQAYAfnmVoLei7apugT6m4crQjmDGxq+UkiRrq55UqmDDyZlac/6ax/JC0p+usZ6W4nVyg==
dependencies:
"@docusaurus/logger" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
"@docusaurus/logger" "2.2.0"
"@docusaurus/utils" "2.2.0"
joi "^17.6.0"
js-yaml "^4.1.0"
tslib "^2.4.0"
"@docusaurus/utils@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-2.0.0-rc.1.tgz#53584b800df9e13864d5ef1a76aa7655a90ec86e"
integrity sha512-ym9I1OwIYbKs1LGaUajaA/vDG8VweJj/6YoZjHp+eDQHhTRIrHXiYoGDqorafRhftKwnA1EnyomuXpNd9bq8Gg==
"@docusaurus/utils@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-2.2.0.tgz#3d6f9b7a69168d5c92d371bf21c556a4f50d1da6"
integrity sha512-oNk3cjvx7Tt1Lgh/aeZAmFpGV2pDr5nHKrBVx6hTkzGhrnMuQqLt6UPlQjdYQ3QHXwyF/ZtZMO1D5Pfi0lu7SA==
dependencies:
"@docusaurus/logger" "2.0.0-rc.1"
"@docusaurus/logger" "2.2.0"
"@svgr/webpack" "^6.2.1"
file-loader "^6.2.0"
fs-extra "^10.1.0"
@ -1588,6 +1631,11 @@
url-loader "^4.1.1"
webpack "^5.73.0"
"@excalidraw/excalidraw@0.15.2":
version "0.15.2"
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2.tgz#7dba4f6e10c52015a007efb75a9fc1afe598574c"
integrity sha512-rTI02kgWSTXiUdIkBxt9u/581F3eXcqQgJdIxmz54TFtG3ughoxO5fr4t7Fr2LZIturBPqfocQHGKZ0t2KLKgw==
"@hapi/hoek@^9.0.0":
version "9.3.0"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb"
@ -1709,6 +1757,21 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@philpl/buble@^0.19.7":
version "0.19.7"
resolved "https://registry.yarnpkg.com/@philpl/buble/-/buble-0.19.7.tgz#27231e6391393793b64bc1c982fc7b593198b893"
integrity sha512-wKTA2DxAGEW+QffRQvOhRQ0VBiYU2h2p8Yc1oBNlqSKws48/8faxqKNIuub0q4iuyTuLwtB8EkwiKwhlfV1PBA==
dependencies:
acorn "^6.1.1"
acorn-class-fields "^0.2.1"
acorn-dynamic-import "^4.0.0"
acorn-jsx "^5.0.1"
chalk "^2.4.2"
magic-string "^0.25.2"
minimist "^1.2.0"
os-homedir "^1.0.1"
regexpu-core "^4.5.4"
"@polka/url@^1.0.0-next.20":
version "1.0.0-next.21"
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1"
@ -1722,9 +1785,9 @@
"@hapi/hoek" "^9.0.0"
"@sideway/formula@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
version "3.0.1"
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f"
integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==
"@sideway/pinpoint@^2.0.0":
version "2.0.0"
@ -2242,16 +2305,36 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8:
mime-types "~2.1.34"
negotiator "0.6.3"
acorn-class-fields@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/acorn-class-fields/-/acorn-class-fields-0.2.1.tgz#748058bceeb0ef25164bbc671993984083f5a085"
integrity sha512-US/kqTe0H8M4LN9izoL+eykVAitE68YMuYZ3sHn3i1fjniqR7oQ3SPvuMK/VT1kjOQHrx5Q88b90TtOKgAv2hQ==
acorn-dynamic-import@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948"
integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==
acorn-import-assertions@^1.7.6:
version "1.8.0"
resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9"
integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==
acorn-jsx@^5.0.1:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn-walk@^8.0.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
acorn@^6.1.1:
version "6.4.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
acorn@^8.0.4, acorn@^8.5.0, acorn@^8.7.1:
version "8.8.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
@ -2618,6 +2701,18 @@ browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4
node-releases "^2.0.6"
update-browserslist-db "^1.0.4"
buble@0.19.6:
version "0.19.6"
resolved "https://registry.yarnpkg.com/buble/-/buble-0.19.6.tgz#915909b6bd5b11ee03b1c885ec914a8b974d34d3"
integrity sha512-9kViM6nJA1Q548Jrd06x0geh+BG2ru2+RMDkIHHgJY/8AcyCs34lTHwra9BX7YdPrZXd5aarkpr/SY8bmPgPdg==
dependencies:
chalk "^2.4.1"
magic-string "^0.25.1"
minimist "^1.2.0"
os-homedir "^1.0.1"
regexpu-core "^4.2.0"
vlq "^1.0.0"
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
@ -2697,7 +2792,7 @@ ccount@^1.0.0:
resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043"
integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==
chalk@^2.0.0:
chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@ -2754,7 +2849,7 @@ cheerio@^1.0.0-rc.12:
parse5 "^7.0.0"
parse5-htmlparser2-tree-adapter "^7.0.0"
chokidar@^3.4.2, chokidar@^3.5.3:
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.2, chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
@ -2905,6 +3000,16 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
component-props@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/component-props/-/component-props-1.1.1.tgz#f9b7df9b9927b6e6d97c9bd272aa867670f34944"
integrity sha512-69pIRJs9fCCHRqCz3390YF2LV1Lu6iEMZ5zuVqqUn+G20V9BNXlMs0cWawWeW9g4Ynmg29JmkG6R7/lUJoGd1Q==
component-xor@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/component-xor/-/component-xor-0.0.4.tgz#c55d83ccc1b94cd5089a4e93fa7891c7263e59aa"
integrity sha512-ZIt6sla8gfo+AFVRZoZOertcnD5LJaY2T9CKE2j13NJxQt/mUafD69Bl7/Y4AnpI2LGjiXH7cOfJDx/n2G9edA==
compressible@~2.0.16:
version "2.0.18"
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
@ -3016,6 +3121,11 @@ core-js-pure@^3.20.2:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.24.0.tgz#10eeb90dbf0d670a6b22b081aecc7deb2faec7e1"
integrity sha512-uzMmW8cRh7uYw4JQtzqvGWRyC2T5+4zipQLQdi2FmiRqP83k3d6F3stv2iAlNhOs6cXN401FCD5TL0vvleuHgA==
core-js@^2.4.1:
version "2.6.12"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-js@^3.23.3:
version "3.24.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.24.0.tgz#4928d4e99c593a234eb1a1f9abd3122b04d3ac57"
@ -3345,6 +3455,13 @@ dns-packet@^5.2.2:
dependencies:
"@leichtgewicht/ip-codec" "^2.0.1"
docusaurus-plugin-sass@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/docusaurus-plugin-sass/-/docusaurus-plugin-sass-0.2.3.tgz#5b61f7e560d236cfc1531ed497ac32fc166fc5e2"
integrity sha512-FbaE06K8NF8SPUYTwiG+83/jkXrwHJ/Afjqz3SUIGon6QvFwSSoKOcoxGQmUBnjTOk+deUONDx8jNWsegFJcBQ==
dependencies:
sass-loader "^10.1.1"
dom-converter@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
@ -3352,6 +3469,14 @@ dom-converter@^0.2.0:
dependencies:
utila "~0.4"
dom-iterator@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/dom-iterator/-/dom-iterator-1.0.0.tgz#9c09899846ec41c2d257adc4d6015e4759ef05ad"
integrity sha512-7dsMOQI07EMU98gQM8NSB3GsAiIeBYIPKpnxR3c9xOvdvBjChAcOM0iJ222I3p5xyiZO9e5oggkNaCusuTdYig==
dependencies:
component-props "1.1.1"
component-xor "0.0.4"
dom-serializer@^1.0.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
@ -4251,9 +4376,9 @@ htmlparser2@^8.0.1:
entities "^4.3.0"
http-cache-semantics@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
version "4.1.1"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
http-deceiver@^1.2.7:
version "1.2.7"
@ -4340,6 +4465,11 @@ immer@^9.0.7:
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.15.tgz#0b9169e5b1d22137aba7d43f8a81a495dd1b62dc"
integrity sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==
immutable@^4.0.0:
version "4.2.2"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.2.tgz#2da9ff4384a4330c36d4d1bc88e90f9e0b0ccd16"
integrity sha512-fTMKDwtbvO5tldky9QZ2fMX7slR0mYpY5nbnFWYp0fOzDhHqhgIw9KoYgxLWsoNTS9ZHGauHj18DTyEw6BK3Og==
import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
@ -4722,7 +4852,7 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
klona@^2.0.5:
klona@^2.0.4, klona@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc"
integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==
@ -4851,6 +4981,13 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
magic-string@^0.25.1, magic-string@^0.25.2:
version "0.25.9"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==
dependencies:
sourcemap-codec "^1.4.8"
make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
@ -5208,6 +5345,11 @@ opener@^1.5.2:
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
os-homedir@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
integrity sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==
p-cancelable@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
@ -5737,7 +5879,7 @@ pretty-time@^1.1.0:
resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e"
integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==
prism-react-renderer@^1.3.5:
prism-react-renderer@^1.0.1, prism-react-renderer@^1.3.5:
version "1.3.5"
resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.5.tgz#786bb69aa6f73c32ba1ee813fbe17a0115435085"
integrity sha512-IJ+MSwBWKG+SM3b2SUfdrhC+gu01QkV2KmRQgREThBfSQRoufqRfxfHUxpG1WcaFjP+kojcFyO9Qqtpgt3qLCg==
@ -5767,7 +5909,7 @@ prompts@^2.4.2:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -5967,6 +6109,19 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-live@2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/react-live/-/react-live-2.2.3.tgz#260f99194213799f0005e473e7a4154c699d6a7c"
integrity sha512-tpKruvfytNETuzO3o1mrQUj180GVrq35IE8F5gH1NJVPt4szYCx83/dOSCOyjgRhhc3gQvl0pQ3k/CjOjwJkKQ==
dependencies:
buble "0.19.6"
core-js "^2.4.1"
dom-iterator "^1.0.0"
prism-react-renderer "^1.0.1"
prop-types "^15.5.8"
react-simple-code-editor "^0.10.0"
unescape "^1.0.1"
react-loadable-ssr-addon-v5-slorber@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz#2cdc91e8a744ffdf9e3556caabeb6e4278689883"
@ -6010,6 +6165,11 @@ react-router@5.3.3, react-router@^5.3.3:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-simple-code-editor@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/react-simple-code-editor/-/react-simple-code-editor-0.10.0.tgz#73e7ac550a928069715482aeb33ccba36efe2373"
integrity sha512-bL5W5mAxSW6+cLwqqVWY47Silqgy2DKDTR4hDBrLrUqC5BXc29YVx17l2IZk5v36VcDEq1Bszu2oHm1qBwKqBA==
react-textarea-autosize@^8.3.2:
version "8.3.4"
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.3.4.tgz#270a343de7ad350534141b02c9cb78903e553524"
@ -6082,6 +6242,13 @@ regenerate-unicode-properties@^10.0.1:
dependencies:
regenerate "^1.4.2"
regenerate-unicode-properties@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326"
integrity sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA==
dependencies:
regenerate "^1.4.2"
regenerate@^1.4.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
@ -6099,6 +6266,18 @@ regenerator-transform@^0.15.0:
dependencies:
"@babel/runtime" "^7.8.4"
regexpu-core@^4.2.0, regexpu-core@^4.5.4:
version "4.8.0"
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.8.0.tgz#e5605ba361b67b1718478501327502f4479a98f0"
integrity sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg==
dependencies:
regenerate "^1.4.2"
regenerate-unicode-properties "^9.0.0"
regjsgen "^0.5.2"
regjsparser "^0.7.0"
unicode-match-property-ecmascript "^2.0.0"
unicode-match-property-value-ecmascript "^2.0.0"
regexpu-core@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.1.0.tgz#2f8504c3fd0ebe11215783a41541e21c79942c6d"
@ -6125,11 +6304,23 @@ registry-url@^5.0.0:
dependencies:
rc "^1.2.8"
regjsgen@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733"
integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==
regjsgen@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.6.0.tgz#83414c5354afd7d6627b16af5f10f41c4e71808d"
integrity sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA==
regjsparser@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.7.0.tgz#a6b667b54c885e18b52554cb4960ef71187e9968"
integrity sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ==
dependencies:
jsesc "~0.5.0"
regjsparser@^0.8.2:
version "0.8.4"
resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.8.4.tgz#8a14285ffcc5de78c5b95d62bbf413b6bc132d5f"
@ -6317,6 +6508,26 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sass-loader@^10.1.1:
version "10.4.1"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.4.1.tgz#bea4e173ddf512c9d7f53e9ec686186146807cbf"
integrity sha512-aX/iJZTTpNUNx/OSYzo2KsjIUQHqvWsAhhUijFjAPdZTEhstjZI9zTNvkTTwsx+uNUJqUwOw5gacxQMx4hJxGQ==
dependencies:
klona "^2.0.4"
loader-utils "^2.0.0"
neo-async "^2.6.2"
schema-utils "^3.0.0"
semver "^7.3.2"
sass@1.57.1:
version "1.57.1"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.57.1.tgz#dfafd46eb3ab94817145e8825208ecf7281119b5"
integrity sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
sax@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
@ -6594,7 +6805,7 @@ sort-css-media-queries@2.0.4:
resolved "https://registry.yarnpkg.com/sort-css-media-queries/-/sort-css-media-queries-2.0.4.tgz#b2badfa519cb4a938acbc6d3aaa913d4949dc908"
integrity sha512-PAIsEK/XupCQwitjv7XxoMvYhT7EAfyzI3hsy/MyDgTvc+Ft55ctdkctJLOy6cQejaIC+zjpUL4djFVm2ivOOw==
source-map-js@^1.0.2:
"source-map-js@>=0.6.2 <2.0.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==
@ -6617,6 +6828,11 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
sourcemap-codec@^1.4.8:
version "1.4.8"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
space-separated-tokens@^1.0.0:
version "1.1.5"
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899"
@ -6943,9 +7159,16 @@ typescript@^4.7.4:
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
ua-parser-js@^0.7.30:
version "0.7.31"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"
integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==
version "0.7.33"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532"
integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==
unescape@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/unescape/-/unescape-1.0.1.tgz#956e430f61cad8a4d57d82c518f5e6cc5d0dda96"
integrity sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==
dependencies:
extend-shallow "^2.0.1"
unherit@^1.0.4:
version "1.1.3"
@ -7203,6 +7426,11 @@ vfile@^4.0.0:
unist-util-stringify-position "^2.0.0"
vfile-message "^2.0.0"
vlq@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468"
integrity sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==
wait-on@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-6.0.1.tgz#16bbc4d1e4ebdd41c5b4e63a2e16dbd1f4e5601e"
@ -7314,9 +7542,9 @@ webpack-sources@^3.2.2, webpack-sources@^3.2.3:
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@^5.73.0:
version "5.74.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.74.0.tgz#02a5dac19a17e0bb47093f2be67c695102a55980"
integrity sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==
version "5.76.1"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.1.tgz#7773de017e988bccb0f13c7d75ec245f377d295c"
integrity sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==
dependencies:
"@types/eslint-scope" "^3.7.3"
"@types/estree" "^0.0.51"

View File

@ -19,17 +19,13 @@
]
},
"dependencies": {
"@dwelle/tunnel-rat": "0.1.1",
"@radix-ui/react-popover": "1.0.3",
"@radix-ui/react-tabs": "1.0.2",
"@sentry/browser": "6.2.5",
"@sentry/integrations": "6.2.5",
"@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.5",
"@tldraw/vec": "1.7.1",
"@types/jest": "27.4.0",
"@types/pica": "5.1.3",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"@types/socket.io-client": "1.4.36",
"browser-fs-access": "0.29.1",
"clsx": "1.1.1",
"cross-env": "7.0.3",
@ -38,7 +34,7 @@
"i18next-browser-languagedetector": "6.1.4",
"idb-keyval": "6.0.3",
"image-blob-reduce": "3.0.1",
"jotai": "1.6.4",
"jotai": "1.13.1",
"lodash.throttle": "4.1.1",
"nanoid": "3.3.3",
"open-color": "1.9.1",
@ -56,8 +52,7 @@
"roughjs": "4.5.2",
"sass": "1.51.0",
"socket.io-client": "2.3.1",
"tunnel-rat": "0.1.0",
"typescript": "4.9.4",
"tunnel-rat": "0.1.2",
"workbox-background-sync": "^6.5.4",
"workbox-broadcast-update": "^6.5.4",
"workbox-cacheable-response": "^6.5.4",
@ -75,9 +70,14 @@
"@excalidraw/eslint-config": "1.0.0",
"@excalidraw/prettier-config": "1.0.2",
"@types/chai": "4.3.0",
"@types/jest": "27.4.0",
"@types/lodash.throttle": "4.1.7",
"@types/pako": "1.0.3",
"@types/pica": "5.1.3",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"@types/resize-observer-browser": "0.1.7",
"@types/socket.io-client": "1.4.36",
"chai": "4.3.6",
"dotenv": "16.0.1",
"eslint-config-prettier": "8.5.0",
@ -88,13 +88,23 @@
"lint-staged": "12.3.7",
"pepjs": "0.5.3",
"prettier": "2.6.2",
"rewire": "6.0.0"
"rewire": "6.0.0",
"typescript": "4.9.4"
},
"engines": {
"node": ">=14.0.0"
},
"homepage": ".",
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}"
],
"coveragePathIgnorePatterns": [
"<rootDir>/locales",
"<rootDir>/src/packages/excalidraw/dist/",
"<rootDir>/src/packages/excalidraw/types",
"<rootDir>/src/packages/excalidraw/example"
],
"transformIgnorePatterns": [
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access)/)"
],
@ -127,6 +137,7 @@
"test:typecheck": "tsc",
"test:update": "yarn test:app --updateSnapshot --watchAll=false",
"test": "yarn test:app",
"test:coverage": "react-scripts test --passWithNoTests --coverage --watchAll",
"autorelease": "node scripts/autorelease.js",
"prerelease": "node scripts/prerelease.js",
"release": "node scripts/release.js"

View File

@ -79,6 +79,7 @@
</style>
<!------------------------------------------------------------------------->
<% if (process.env.NODE_ENV === "production") { %>
<script>
// Redirect Excalidraw+ users which have auto-redirect enabled.
//
@ -97,6 +98,7 @@
window.location.href = "https://app.excalidraw.com";
}
</script>
<% } %>
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
@ -146,8 +148,18 @@
// setting this so that libraries installation reuses this window tab.
window.name = "_excalidraw";
</script>
<% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true' &&
process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
<% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %>
<!-- Fathom - privacy-friendly analytics -->
<script
src="https://cdn.usefathom.com/script.js"
data-site="VMSBUEYA"
defer
></script>
<!-- / Fathom -->
<!-- LEGACY GOOGLE ANALYTICS -->
<% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
<script
async
src="https://www.googletagmanager.com/gtag/js?id=%REACT_APP_GOOGLE_ANALYTICS_ID%"
@ -161,6 +173,8 @@
gtag("config", "%REACT_APP_GOOGLE_ANALYTICS_ID%");
</script>
<% } %>
<!-- end LEGACY GOOGLE ANALYTICS -->
<% } %>
<!-- FIXME: remove this when we update CRA (fix SW caching) -->
<style>
@ -213,5 +227,17 @@
<h1 class="visually-hidden">Excalidraw</h1>
</header>
<div id="root"></div>
<!-- 100% privacy friendly analytics -->
<script
async
defer
src="https://scripts.simpleanalyticscdn.com/latest.js"
></script>
<noscript
><img
src="https://queue.simpleanalyticscdn.com/noscript.gif"
alt=""
referrerpolicy="no-referrer-when-downgrade"
/></noscript>
</body>
</html>

View File

@ -2,6 +2,9 @@ const fs = require("fs");
const THRESSHOLD = 85;
// we're using BCP 47 language tags as keys
// e.g. https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1
const crowdinMap = {
"ar-SA": "en-ar",
"bg-BG": "en-bg",
@ -52,6 +55,7 @@ const crowdinMap = {
"kk-KZ": "en-kk",
"vi-VN": "en-vi",
"mr-IN": "en-mr",
"th-TH": "en-th",
};
const flags = {
@ -104,6 +108,7 @@ const flags = {
"eu-ES": "🇪🇦",
"vi-VN": "🇻🇳",
"mr-IN": "🇮🇳",
"th-TH": "🇹🇭",
};
const languages = {
@ -156,6 +161,7 @@ const languages = {
"zh-TW": "繁體中文",
"vi-VN": "Tiếng Việt",
"mr-IN": "मराठी",
"th-TH": "ภาษาไทย",
};
const percentages = fs.readFileSync(

View File

@ -1,22 +1,9 @@
const fs = require("fs");
const { execSync } = require("child_process");
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
const pkg = require(excalidrawPackage);
const originalReadMe = fs.readFileSync(`${excalidrawDir}/README.md`, "utf8");
const updateReadme = () => {
const excalidrawIndex = originalReadMe.indexOf("### Excalidraw");
// remove note for stable readme
const data = originalReadMe.slice(excalidrawIndex);
// update readme
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
};
const publish = () => {
try {
execSync(`yarn --frozen-lockfile`);
@ -30,15 +17,8 @@ const publish = () => {
};
const release = () => {
updateReadme();
console.info("Note for stable readme removed");
publish();
console.info(`Published ${pkg.version}!`);
// revert readme after release
fs.writeFileSync(`${excalidrawDir}/README.md`, originalReadMe, "utf8");
console.info("Readme reverted");
};
release();

View File

@ -1,7 +1,14 @@
import { VERTICAL_ALIGN } from "../constants";
import { getNonDeletedElements, isTextElement } from "../element";
import {
BOUND_TEXT_PADDING,
ROUNDNESS,
VERTICAL_ALIGN,
TEXT_ALIGN,
} from "../constants";
import { getNonDeletedElements, isTextElement, newElement } from "../element";
import { mutateElement } from "../element/mutateElement";
import {
computeBoundTextPosition,
computeContainerDimensionForBoundText,
getBoundTextElement,
measureText,
redrawTextBoundingBox,
@ -9,16 +16,21 @@ import {
import {
getOriginalContainerHeightFromCache,
resetOriginalContainerCache,
updateOriginalContainerCache,
} from "../element/textWysiwyg";
import {
hasBoundTextElement,
isTextBindableContainer,
isUsingAdaptiveRadius,
} from "../element/typeChecks";
import {
ExcalidrawElement,
ExcalidrawLinearElement,
ExcalidrawTextContainer,
ExcalidrawTextElement,
} from "../element/types";
import { getSelectedElements } from "../scene";
import { AppState } from "../types";
import { getFontString } from "../utils";
import { register } from "./register";
@ -28,6 +40,7 @@ export const actionUnbindText = register({
trackEvent: { category: "element" },
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
return selectedElements.some((element) => hasBoundTextElement(element));
},
perform: (elements, appState) => {
@ -41,18 +54,21 @@ export const actionUnbindText = register({
const { width, height, baseline } = measureText(
boundTextElement.originalText,
getFontString(boundTextElement),
boundTextElement.lineHeight,
);
const originalContainerHeight = getOriginalContainerHeightFromCache(
element.id,
);
resetOriginalContainerCache(element.id);
const { x, y } = computeBoundTextPosition(element, boundTextElement);
mutateElement(boundTextElement as ExcalidrawTextElement, {
containerId: null,
width,
height,
baseline,
text: boundTextElement.originalText,
x,
y,
});
mutateElement(element, {
boundElements: element.boundElements?.filter(
@ -122,6 +138,7 @@ export const actionBindText = register({
mutateElement(textElement, {
containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
textAlign: TEXT_ALIGN.CENTER,
});
mutateElement(container, {
boundElements: (container.boundElements || []).concat({
@ -129,20 +146,168 @@ export const actionBindText = register({
id: textElement.id,
}),
});
const originalContainerHeight = container.height;
redrawTextBoundingBox(textElement, container);
const updatedElements = elements.slice();
const textElementIndex = updatedElements.findIndex(
(ele) => ele.id === textElement.id,
);
updatedElements.splice(textElementIndex, 1);
const containerIndex = updatedElements.findIndex(
(ele) => ele.id === container.id,
);
updatedElements.splice(containerIndex + 1, 0, textElement);
// overwritting the cache with original container height so
// it can be restored when unbind
updateOriginalContainerCache(container.id, originalContainerHeight);
return {
elements: updatedElements,
elements: pushTextAboveContainer(elements, container, textElement),
appState: { ...appState, selectedElementIds: { [container.id]: true } },
commitToHistory: true,
};
},
});
const pushTextAboveContainer = (
elements: readonly ExcalidrawElement[],
container: ExcalidrawElement,
textElement: ExcalidrawTextElement,
) => {
const updatedElements = elements.slice();
const textElementIndex = updatedElements.findIndex(
(ele) => ele.id === textElement.id,
);
updatedElements.splice(textElementIndex, 1);
const containerIndex = updatedElements.findIndex(
(ele) => ele.id === container.id,
);
updatedElements.splice(containerIndex + 1, 0, textElement);
return updatedElements;
};
const pushContainerBelowText = (
elements: readonly ExcalidrawElement[],
container: ExcalidrawElement,
textElement: ExcalidrawTextElement,
) => {
const updatedElements = elements.slice();
const containerIndex = updatedElements.findIndex(
(ele) => ele.id === container.id,
);
updatedElements.splice(containerIndex, 1);
const textElementIndex = updatedElements.findIndex(
(ele) => ele.id === textElement.id,
);
updatedElements.splice(textElementIndex, 0, container);
return updatedElements;
};
export const actionWrapTextInContainer = register({
name: "wrapTextInContainer",
contextItemLabel: "labels.createContainerFromText",
trackEvent: { category: "element" },
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
const areTextElements = selectedElements.every((el) => isTextElement(el));
return selectedElements.length > 0 && areTextElements;
},
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
let updatedElements: readonly ExcalidrawElement[] = elements.slice();
const containerIds: AppState["selectedElementIds"] = {};
for (const textElement of selectedElements) {
if (isTextElement(textElement)) {
const container = newElement({
type: "rectangle",
backgroundColor: appState.currentItemBackgroundColor,
boundElements: [
...(textElement.boundElements || []),
{ id: textElement.id, type: "text" },
],
angle: textElement.angle,
fillStyle: appState.currentItemFillStyle,
strokeColor: appState.currentItemStrokeColor,
roughness: appState.currentItemRoughness,
strokeWidth: appState.currentItemStrokeWidth,
strokeStyle: appState.currentItemStrokeStyle,
roundness:
appState.currentItemRoundness === "round"
? {
type: isUsingAdaptiveRadius("rectangle")
? ROUNDNESS.ADAPTIVE_RADIUS
: ROUNDNESS.PROPORTIONAL_RADIUS,
}
: null,
opacity: 100,
locked: false,
x: textElement.x - BOUND_TEXT_PADDING,
y: textElement.y - BOUND_TEXT_PADDING,
width: computeContainerDimensionForBoundText(
textElement.width,
"rectangle",
),
height: computeContainerDimensionForBoundText(
textElement.height,
"rectangle",
),
groupIds: textElement.groupIds,
});
// update bindings
if (textElement.boundElements?.length) {
const linearElementIds = textElement.boundElements
.filter((ele) => ele.type === "arrow")
.map((el) => el.id);
const linearElements = updatedElements.filter((ele) =>
linearElementIds.includes(ele.id),
) as ExcalidrawLinearElement[];
linearElements.forEach((ele) => {
let startBinding = ele.startBinding;
let endBinding = ele.endBinding;
if (startBinding?.elementId === textElement.id) {
startBinding = {
...startBinding,
elementId: container.id,
};
}
if (endBinding?.elementId === textElement.id) {
endBinding = { ...endBinding, elementId: container.id };
}
if (startBinding || endBinding) {
mutateElement(ele, { startBinding, endBinding }, false);
}
});
}
mutateElement(
textElement,
{
containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
boundElements: null,
textAlign: TEXT_ALIGN.CENTER,
},
false,
);
redrawTextBoundingBox(textElement, container);
updatedElements = pushContainerBelowText(
[...updatedElements, container],
container,
textElement,
);
containerIds[container.id] = true;
}
}
return {
elements: updatedElements,
appState: {
...appState,
selectedElementIds: containerIds,
},
commitToHistory: true,
};
},
});

View File

@ -1,4 +1,4 @@
import { ColorPicker } from "../components/ColorPicker";
import { ColorPicker } from "../components/ColorPicker/ColorPicker";
import { ZoomInIcon, ZoomOutIcon } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { CURSOR_TYPE, MIN_ZOOM, THEME, ZOOM_STEP } from "../constants";
@ -19,6 +19,7 @@ import {
isEraserActive,
isHandToolActive,
} from "../appState";
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor",
@ -35,24 +36,21 @@ export const actionChangeViewBackgroundColor = register({
commitToHistory: !!value.viewBackgroundColor,
};
},
PanelComponent: ({ elements, appState, updateData }) => {
PanelComponent: ({ elements, appState, updateData, appProps }) => {
// FIXME move me to src/components/mainMenu/DefaultItems.tsx
return (
<div style={{ position: "relative" }}>
<ColorPicker
label={t("labels.canvasBackground")}
type="canvasBackground"
color={appState.viewBackgroundColor}
onChange={(color) => updateData({ viewBackgroundColor: color })}
isActive={appState.openPopup === "canvasColorPicker"}
setActive={(active) =>
updateData({ openPopup: active ? "canvasColorPicker" : null })
}
data-testid="canvas-background-picker"
elements={elements}
appState={appState}
/>
</div>
<ColorPicker
palette={null}
topPicks={DEFAULT_CANVAS_BACKGROUND_PICKS}
label={t("labels.canvasBackground")}
type="canvasBackground"
color={appState.viewBackgroundColor}
onChange={(color) => updateData({ viewBackgroundColor: color })}
data-testid="canvas-background-picker"
elements={elements}
appState={appState}
updateData={updateData}
/>
);
},
});
@ -226,7 +224,7 @@ const zoomValueToFitBoundsOnViewport = (
return clampedZoomValueToFitElements as NormalizedZoomValue;
};
const zoomToFitElements = (
export const zoomToFitElements = (
elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>,
zoomToSelection: boolean,

View File

@ -18,7 +18,7 @@ export const actionCopy = register({
perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState, true);
copyToClipboard(selectedElements, appState, app.files);
copyToClipboard(selectedElements, app.files);
return {
commitToHistory: false,

View File

@ -154,7 +154,9 @@ export const actionDeleteSelected = register({
};
},
contextItemLabel: "labels.delete",
keyTest: (event) => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
keyTest: (event, appState, elements) =>
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) &&
!event[KEYS.CTRL_OR_CMD],
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
type="button"

View File

@ -16,8 +16,12 @@ import { AppState } from "../types";
import { fixBindingsAfterDuplication } from "../element/binding";
import { ActionResult } from "./types";
import { GRID_SIZE } from "../constants";
import { bindTextToShapeAfterDuplication } from "../element/textElement";
import {
bindTextToShapeAfterDuplication,
getBoundTextElement,
} from "../element/textElement";
import { isBoundToContainer } from "../element/typeChecks";
import { normalizeElementOrder } from "../element/sortElements";
import { DuplicateIcon } from "../components/icons";
export const actionDuplicateSelection = register({
@ -64,6 +68,11 @@ const duplicateElements = (
elements: readonly ExcalidrawElement[],
appState: AppState,
): Partial<ActionResult> => {
// ---------------------------------------------------------------------------
// step (1)
const sortedElements = normalizeElementOrder(elements);
const groupIdMap = new Map();
const newElements: ExcalidrawElement[] = [];
const oldElements: ExcalidrawElement[] = [];
@ -85,42 +94,112 @@ const duplicateElements = (
return newElement;
};
const finalElements: ExcalidrawElement[] = [];
let index = 0;
const selectedElementIds = arrayToMap(
getSelectedElements(elements, appState, true),
getSelectedElements(sortedElements, appState, true),
);
while (index < elements.length) {
const element = elements[index];
// Ids of elements that have already been processed so we don't push them
// into the array twice if we end up backtracking when retrieving
// discontiguous group of elements (can happen due to a bug, or in edge
// cases such as a group containing deleted elements which were not selected).
//
// This is not enough to prevent duplicates, so we do a second loop afterwards
// to remove them.
//
// For convenience we mark even the newly created ones even though we don't
// loop over them.
const processedIds = new Map<ExcalidrawElement["id"], true>();
const markAsProcessed = (elements: ExcalidrawElement[]) => {
for (const element of elements) {
processedIds.set(element.id, true);
}
return elements;
};
const elementsWithClones: ExcalidrawElement[] = [];
let index = -1;
while (++index < sortedElements.length) {
const element = sortedElements[index];
if (processedIds.get(element.id)) {
continue;
}
const boundTextElement = getBoundTextElement(element);
if (selectedElementIds.get(element.id)) {
if (element.groupIds.length) {
// if a group or a container/bound-text, duplicate atomically
if (element.groupIds.length || boundTextElement) {
const groupId = getSelectedGroupForElement(appState, element);
// if group selected, duplicate it atomically
if (groupId) {
const groupElements = getElementsInGroup(elements, groupId);
finalElements.push(
...groupElements,
...groupElements.map((element) =>
duplicateAndOffsetElement(element),
),
const groupElements = getElementsInGroup(sortedElements, groupId);
elementsWithClones.push(
...markAsProcessed([
...groupElements,
...groupElements.map((element) =>
duplicateAndOffsetElement(element),
),
]),
);
continue;
}
if (boundTextElement) {
elementsWithClones.push(
...markAsProcessed([
element,
boundTextElement,
duplicateAndOffsetElement(element),
duplicateAndOffsetElement(boundTextElement),
]),
);
index = index + groupElements.length;
continue;
}
}
finalElements.push(element, duplicateAndOffsetElement(element));
elementsWithClones.push(
...markAsProcessed([element, duplicateAndOffsetElement(element)]),
);
} else {
finalElements.push(element);
elementsWithClones.push(...markAsProcessed([element]));
}
index++;
}
// step (2)
// second pass to remove duplicates. We loop from the end as it's likelier
// that the last elements are in the correct order (contiguous or otherwise).
// Thus we need to reverse as the last step (3).
const finalElementsReversed: ExcalidrawElement[] = [];
const finalElementIds = new Map<ExcalidrawElement["id"], true>();
index = elementsWithClones.length;
while (--index >= 0) {
const element = elementsWithClones[index];
if (!finalElementIds.get(element.id)) {
finalElementIds.set(element.id, true);
finalElementsReversed.push(element);
}
}
// step (3)
const finalElements = finalElementsReversed.reverse();
// ---------------------------------------------------------------------------
bindTextToShapeAfterDuplication(
finalElements,
elementsWithClones,
oldElements,
oldIdToDuplicatedId,
);
fixBindingsAfterDuplication(
elementsWithClones,
oldElements,
oldIdToDuplicatedId,
);
fixBindingsAfterDuplication(finalElements, oldElements, oldIdToDuplicatedId);
return {
elements: finalElements,

View File

@ -0,0 +1,68 @@
import { Excalidraw } from "../packages/excalidraw/index";
import { queryByTestId, fireEvent } from "@testing-library/react";
import { render } from "../tests/test-utils";
import { Pointer, UI } from "../tests/helpers/ui";
import { API } from "../tests/helpers/api";
const { h } = window;
const mouse = new Pointer("mouse");
describe("element locking", () => {
it("should not show unlockAllElements action in contextMenu if no elements locked", async () => {
await render(<Excalidraw />);
mouse.rightClickAt(0, 0);
const item = queryByTestId(UI.queryContextMenu()!, "unlockAllElements");
expect(item).toBe(null);
});
it("should unlock all elements and select them when using unlockAllElements action in contextMenu", async () => {
await render(
<Excalidraw
initialData={{
elements: [
API.createElement({
x: 100,
y: 100,
width: 100,
height: 100,
locked: true,
}),
API.createElement({
x: 100,
y: 100,
width: 100,
height: 100,
locked: true,
}),
API.createElement({
x: 100,
y: 100,
width: 100,
height: 100,
locked: false,
}),
],
}}
/>,
);
mouse.rightClickAt(0, 0);
expect(Object.keys(h.state.selectedElementIds).length).toBe(0);
expect(h.elements.map((el) => el.locked)).toEqual([true, true, false]);
const item = queryByTestId(UI.queryContextMenu()!, "unlockAllElements");
expect(item).not.toBe(null);
fireEvent.click(item!.querySelector("button")!);
expect(h.elements.map((el) => el.locked)).toEqual([false, false, false]);
// should select the unlocked elements
expect(h.state.selectedElementIds).toEqual({
[h.elements[0].id]: true,
[h.elements[1].id]: true,
});
});
});

View File

@ -5,8 +5,11 @@ import { getSelectedElements } from "../scene";
import { arrayToMap } from "../utils";
import { register } from "./register";
export const actionToggleLock = register({
name: "toggleLock",
const shouldLock = (elements: readonly ExcalidrawElement[]) =>
elements.every((el) => !el.locked);
export const actionToggleElementLock = register({
name: "toggleElementLock",
trackEvent: { category: "element" },
perform: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState, true);
@ -15,20 +18,21 @@ export const actionToggleLock = register({
return false;
}
const operation = getOperation(selectedElements);
const nextLockState = shouldLock(selectedElements);
const selectedElementsMap = arrayToMap(selectedElements);
const lock = operation === "lock";
return {
elements: elements.map((element) => {
if (!selectedElementsMap.has(element.id)) {
return element;
}
return newElementWith(element, { locked: lock });
return newElementWith(element, { locked: nextLockState });
}),
appState: {
...appState,
selectedLinearElement: lock ? null : appState.selectedLinearElement,
selectedLinearElement: nextLockState
? null
: appState.selectedLinearElement,
},
commitToHistory: true,
};
@ -41,7 +45,7 @@ export const actionToggleLock = register({
: "labels.elementLock.lock";
}
return getOperation(selected) === "lock"
return shouldLock(selected)
? "labels.elementLock.lockAll"
: "labels.elementLock.unlockAll";
},
@ -55,6 +59,31 @@ export const actionToggleLock = register({
},
});
const getOperation = (
elements: readonly ExcalidrawElement[],
): "lock" | "unlock" => (elements.some((el) => !el.locked) ? "lock" : "unlock");
export const actionUnlockAllElements = register({
name: "unlockAllElements",
trackEvent: { category: "canvas" },
viewMode: false,
predicate: (elements) => {
return elements.some((element) => element.locked);
},
perform: (elements, appState) => {
const lockedElements = elements.filter((el) => el.locked);
return {
elements: elements.map((element) => {
if (element.locked) {
return newElementWith(element, { locked: false });
}
return element;
}),
appState: {
...appState,
selectedElementIds: Object.fromEntries(
lockedElements.map((el) => [el.id, true]),
),
},
commitToHistory: true,
};
},
contextItemLabel: "labels.elementLock.unlockAll",
});

View File

@ -26,7 +26,7 @@ export const actionChangeProjectName = register({
perform: (_elements, appState, value) => {
return { appState: { ...appState, name: value }, commitToHistory: false };
},
PanelComponent: ({ appState, updateData, appProps }) => (
PanelComponent: ({ appState, updateData, appProps, data }) => (
<ProjectName
label={t("labels.fileTitle")}
value={appState.name || "Unnamed"}
@ -34,6 +34,7 @@ export const actionChangeProjectName = register({
isNameEditable={
typeof appProps.name === "undefined" && !appState.viewModeEnabled
}
ignoreFocus={data?.ignoreFocus ?? false}
/>
),
});

View File

@ -1,42 +1,17 @@
import { register } from "./register";
import { getSelectedElements } from "../scene";
import { getNonDeletedElements } from "../element";
import { mutateElement } from "../element/mutateElement";
import { ExcalidrawElement, NonDeleted } from "../element/types";
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
import { AppState } from "../types";
import { getTransformHandles } from "../element/transformHandles";
import { updateBoundElements } from "../element/binding";
import { resizeMultipleElements } from "../element/resizeElements";
import { AppState, PointerDownState } from "../types";
import { arrayToMap } from "../utils";
import { CODES, KEYS } from "../keys";
import { getCommonBoundingBox } from "../element/bounds";
import {
getElementAbsoluteCoords,
getElementPointsCoords,
} from "../element/bounds";
import { isLinearElement } from "../element/typeChecks";
import { LinearElementEditor } from "../element/linearElementEditor";
import { KEYS } from "../keys";
const enableActionFlipHorizontal = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const eligibleElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return eligibleElements.length === 1 && eligibleElements[0].type !== "text";
};
const enableActionFlipVertical = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const eligibleElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return eligibleElements.length === 1;
};
bindOrUnbindSelectedElements,
isBindingEnabled,
unbindLinearElements,
} from "../element/binding";
export const actionFlipHorizontal = register({
name: "flipHorizontal",
@ -48,10 +23,8 @@ export const actionFlipHorizontal = register({
commitToHistory: true,
};
},
keyTest: (event) => event.shiftKey && event.code === "KeyH",
keyTest: (event) => event.shiftKey && event.code === CODES.H,
contextItemLabel: "labels.flipHorizontal",
predicate: (elements, appState) =>
enableActionFlipHorizontal(elements, appState),
});
export const actionFlipVertical = register({
@ -65,10 +38,8 @@ export const actionFlipVertical = register({
};
},
keyTest: (event) =>
event.shiftKey && event.code === "KeyV" && !event[KEYS.CTRL_OR_CMD],
event.shiftKey && event.code === CODES.V && !event[KEYS.CTRL_OR_CMD],
contextItemLabel: "labels.flipVertical",
predicate: (elements, appState) =>
enableActionFlipVertical(elements, appState),
});
const flipSelectedElements = (
@ -81,11 +52,6 @@ const flipSelectedElements = (
appState,
);
// remove once we allow for groups of elements to be flipped
if (selectedElements.length > 1) {
return elements;
}
const updatedElements = flipElements(
selectedElements,
appState,
@ -104,144 +70,20 @@ const flipElements = (
appState: AppState,
flipDirection: "horizontal" | "vertical",
): ExcalidrawElement[] => {
elements.forEach((element) => {
flipElement(element, appState);
// If vertical flip, rotate an extra 180
if (flipDirection === "vertical") {
rotateElement(element, Math.PI);
}
});
const { minX, minY, maxX, maxY } = getCommonBoundingBox(elements);
resizeMultipleElements(
{ originalElements: arrayToMap(elements) } as PointerDownState,
elements,
"nw",
true,
flipDirection === "horizontal" ? maxX : minX,
flipDirection === "horizontal" ? minY : maxY,
);
(isBindingEnabled(appState)
? bindOrUnbindSelectedElements
: unbindLinearElements)(elements);
return elements;
};
const flipElement = (
element: NonDeleted<ExcalidrawElement>,
appState: AppState,
) => {
const originalX = element.x;
const originalY = element.y;
const width = element.width;
const height = element.height;
const originalAngle = normalizeAngle(element.angle);
// Rotate back to zero, if necessary
mutateElement(element, {
angle: normalizeAngle(0),
});
// Flip unrotated by pulling TransformHandle to opposite side
const transformHandles = getTransformHandles(element, appState.zoom);
let usingNWHandle = true;
let nHandle = transformHandles.nw;
if (!nHandle) {
// Use ne handle instead
usingNWHandle = false;
nHandle = transformHandles.ne;
if (!nHandle) {
mutateElement(element, {
angle: originalAngle,
});
return;
}
}
let finalOffsetX = 0;
if (isLinearElement(element) && element.points.length < 3) {
finalOffsetX =
element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
element.width;
}
let initialPointsCoords;
if (isLinearElement(element)) {
initialPointsCoords = getElementPointsCoords(element, element.points);
}
const initialElementAbsoluteCoords = getElementAbsoluteCoords(element);
if (isLinearElement(element) && element.points.length < 3) {
for (let index = 1; index < element.points.length; index++) {
LinearElementEditor.movePoints(element, [
{
index,
point: [-element.points[index][0], element.points[index][1]],
},
]);
}
LinearElementEditor.normalizePoints(element);
} else {
const elWidth = initialPointsCoords
? initialPointsCoords[2] - initialPointsCoords[0]
: initialElementAbsoluteCoords[2] - initialElementAbsoluteCoords[0];
const startPoint = initialPointsCoords
? [initialPointsCoords[0], initialPointsCoords[1]]
: [initialElementAbsoluteCoords[0], initialElementAbsoluteCoords[1]];
resizeSingleElement(
new Map().set(element.id, element),
false,
element,
usingNWHandle ? "nw" : "ne",
true,
usingNWHandle ? startPoint[0] + elWidth : startPoint[0] - elWidth,
startPoint[1],
);
}
// Rotate by (360 degrees - original angle)
let angle = normalizeAngle(2 * Math.PI - originalAngle);
if (angle < 0) {
// check, probably unnecessary
angle = normalizeAngle(angle + 2 * Math.PI);
}
mutateElement(element, {
angle,
});
// Move back to original spot to appear "flipped in place"
mutateElement(element, {
x: originalX + finalOffsetX,
y: originalY,
width,
height,
});
updateBoundElements(element);
if (initialPointsCoords && isLinearElement(element)) {
// Adjusting origin because when a beizer curve path exceeds min/max points it offsets the origin.
// There's still room for improvement since when the line roughness is > 1
// we still have a small offset of the origin when fliipping the element.
const finalPointsCoords = getElementPointsCoords(element, element.points);
const topLeftCoordsDiff = initialPointsCoords[0] - finalPointsCoords[0];
const topRightCoordDiff = initialPointsCoords[2] - finalPointsCoords[2];
const coordsDiff = topLeftCoordsDiff + topRightCoordDiff;
mutateElement(element, {
x: element.x + coordsDiff * 0.5,
y: element.y,
width,
height,
});
}
};
const rotateElement = (element: ExcalidrawElement, rotationAngle: number) => {
const originalX = element.x;
const originalY = element.y;
let angle = normalizeAngle(element.angle + rotationAngle);
if (angle < 0) {
// check, probably unnecessary
angle = normalizeAngle(2 * Math.PI + angle);
}
mutateElement(element, {
angle,
});
// Move back to original spot
mutateElement(element, {
x: originalX,
y: originalY,
});
};

View File

@ -1,6 +1,13 @@
import { AppState } from "../../src/types";
import {
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
DEFAULT_ELEMENT_BACKGROUND_PICKS,
DEFAULT_ELEMENT_STROKE_COLOR_PALETTE,
DEFAULT_ELEMENT_STROKE_PICKS,
} from "../colors";
import { trackEvent } from "../analytics";
import { ButtonIconSelect } from "../components/ButtonIconSelect";
import { ColorPicker } from "../components/ColorPicker";
import { ColorPicker } from "../components/ColorPicker/ColorPicker";
import { IconPicker } from "../components/IconPicker";
// TODO barnabasmolnar/editor-redesign
// TextAlignTopIcon, TextAlignBottomIcon,TextAlignMiddleIcon,
@ -37,6 +44,7 @@ import {
TextAlignLeftIcon,
TextAlignCenterIcon,
TextAlignRightIcon,
FillZigZagIcon,
} from "../components/icons";
import {
DEFAULT_FONT_FAMILY,
@ -54,6 +62,7 @@ import { mutateElement, newElementWith } from "../element/mutateElement";
import {
getBoundTextElement,
getContainerElement,
getDefaultLineHeight,
} from "../element/textElement";
import {
isBoundToContainer,
@ -81,7 +90,7 @@ import {
isSomeElementSelected,
} from "../scene";
import { hasStrokeColor } from "../scene/comparisons";
import { arrayToMap } from "../utils";
import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
@ -110,8 +119,8 @@ const getFormValue = function <T>(
elements: readonly ExcalidrawElement[],
appState: AppState,
getAttribute: (element: ExcalidrawElement) => T,
defaultValue?: T,
): T | null {
defaultValue: T,
): T {
const editingElement = appState.editingElement;
const nonDeletedElements = getNonDeletedElements(elements);
return (
@ -123,7 +132,7 @@ const getFormValue = function <T>(
getAttribute,
)
: defaultValue) ??
null
defaultValue
);
};
@ -223,10 +232,12 @@ export const actionChangeStrokeColor = register({
commitToHistory: !!value.currentItemStrokeColor,
};
},
PanelComponent: ({ elements, appState, updateData }) => (
PanelComponent: ({ elements, appState, updateData, appProps }) => (
<>
<h3 aria-hidden="true">{t("labels.stroke")}</h3>
<ColorPicker
topPicks={DEFAULT_ELEMENT_STROKE_PICKS}
palette={DEFAULT_ELEMENT_STROKE_COLOR_PALETTE}
type="elementStroke"
label={t("labels.stroke")}
color={getFormValue(
@ -236,12 +247,9 @@ export const actionChangeStrokeColor = register({
appState.currentItemStrokeColor,
)}
onChange={(color) => updateData({ currentItemStrokeColor: color })}
isActive={appState.openPopup === "strokeColorPicker"}
setActive={(active) =>
updateData({ openPopup: active ? "strokeColorPicker" : null })
}
elements={elements}
appState={appState}
updateData={updateData}
/>
</>
),
@ -266,10 +274,12 @@ export const actionChangeBackgroundColor = register({
commitToHistory: !!value.currentItemBackgroundColor,
};
},
PanelComponent: ({ elements, appState, updateData }) => (
PanelComponent: ({ elements, appState, updateData, appProps }) => (
<>
<h3 aria-hidden="true">{t("labels.background")}</h3>
<ColorPicker
topPicks={DEFAULT_ELEMENT_BACKGROUND_PICKS}
palette={DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE}
type="elementBackground"
label={t("labels.background")}
color={getFormValue(
@ -279,12 +289,9 @@ export const actionChangeBackgroundColor = register({
appState.currentItemBackgroundColor,
)}
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
isActive={appState.openPopup === "backgroundColorPicker"}
setActive={(active) =>
updateData({ openPopup: active ? "backgroundColorPicker" : null })
}
elements={elements}
appState={appState}
updateData={updateData}
/>
</>
),
@ -293,7 +300,12 @@ export const actionChangeBackgroundColor = register({
export const actionChangeFillStyle = register({
name: "changeFillStyle",
trackEvent: false,
perform: (elements, appState, value) => {
perform: (elements, appState, value, app) => {
trackEvent(
"element",
"changeFillStyle",
`${value} (${app.device.isMobile ? "mobile" : "desktop"})`,
);
return {
elements: changeProperty(elements, appState, (el) =>
newElementWith(el, {
@ -304,40 +316,57 @@ export const actionChangeFillStyle = register({
commitToHistory: true,
};
},
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
<legend>{t("labels.fill")}</legend>
<ButtonIconSelect
options={[
{
value: "hachure",
text: t("labels.hachure"),
icon: FillHachureIcon,
},
{
value: "cross-hatch",
text: t("labels.crossHatch"),
icon: FillCrossHatchIcon,
},
{
value: "solid",
text: t("labels.solid"),
icon: FillSolidIcon,
},
]}
group="fill"
value={getFormValue(
elements,
appState,
(element) => element.fillStyle,
appState.currentItemFillStyle,
)}
onChange={(value) => {
updateData(value);
}}
/>
</fieldset>
),
PanelComponent: ({ elements, appState, updateData }) => {
const selectedElements = getSelectedElements(elements, appState);
const allElementsZigZag =
selectedElements.length > 0 &&
selectedElements.every((el) => el.fillStyle === "zigzag");
return (
<fieldset>
<legend>{t("labels.fill")}</legend>
<ButtonIconSelect
type="button"
options={[
{
value: "hachure",
text: `${
allElementsZigZag ? t("labels.zigzag") : t("labels.hachure")
} (${getShortcutKey("Alt-Click")})`,
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
active: allElementsZigZag ? true : undefined,
},
{
value: "cross-hatch",
text: t("labels.crossHatch"),
icon: FillCrossHatchIcon,
},
{
value: "solid",
text: t("labels.solid"),
icon: FillSolidIcon,
},
]}
value={getFormValue(
elements,
appState,
(element) => element.fillStyle,
appState.currentItemFillStyle,
)}
onClick={(value, event) => {
const nextValue =
event.altKey &&
value === "hachure" &&
selectedElements.every((el) => el.fillStyle === "hachure")
? "zigzag"
: value;
updateData(nextValue);
}}
/>
</fieldset>
);
},
});
export const actionChangeStrokeWidth = register({
@ -637,6 +666,7 @@ export const actionChangeFontFamily = register({
oldElement,
{
fontFamily: value,
lineHeight: getDefaultLineHeight(value),
},
);
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
@ -745,16 +775,19 @@ export const actionChangeTextAlign = register({
value: "left",
text: t("labels.left"),
icon: TextAlignLeftIcon,
testId: "align-left",
},
{
value: "center",
text: t("labels.center"),
icon: TextAlignCenterIcon,
testId: "align-horizontal-center",
},
{
value: "right",
text: t("labels.right"),
icon: TextAlignRightIcon,
testId: "align-right",
},
]}
value={getFormValue(
@ -778,6 +811,7 @@ export const actionChangeTextAlign = register({
);
},
});
export const actionChangeVerticalAlign = register({
name: "changeVerticalAlign",
trackEvent: { category: "element" },
@ -832,16 +866,21 @@ export const actionChangeVerticalAlign = register({
testId: "align-bottom",
},
]}
value={getFormValue(elements, appState, (element) => {
if (isTextElement(element) && element.containerId) {
return element.verticalAlign;
}
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
return boundTextElement.verticalAlign;
}
return null;
})}
value={getFormValue(
elements,
appState,
(element) => {
if (isTextElement(element) && element.containerId) {
return element.verticalAlign;
}
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
return boundTextElement.verticalAlign;
}
return null;
},
VERTICAL_ALIGN.MIDDLE,
)}
onChange={(value) => updateData(value)}
/>
</fieldset>

View File

@ -1,9 +1,14 @@
import ExcalidrawApp from "../excalidraw-app";
import { t } from "../i18n";
import { CODES } from "../keys";
import { API } from "../tests/helpers/api";
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
import { fireEvent, render, screen } from "../tests/test-utils";
import {
act,
fireEvent,
render,
screen,
togglePopover,
} from "../tests/test-utils";
import { copiedStyles } from "./actionStyles";
const { h } = window;
@ -14,7 +19,14 @@ describe("actionStyles", () => {
beforeEach(async () => {
await render(<ExcalidrawApp />);
});
it("should copy & paste styles via keyboard", () => {
afterEach(async () => {
// https://github.com/floating-ui/floating-ui/issues/1908#issuecomment-1301553793
// affects node v16+
await act(async () => {});
});
it("should copy & paste styles via keyboard", async () => {
UI.clickTool("rectangle");
mouse.down(10, 10);
mouse.up(20, 20);
@ -24,10 +36,10 @@ describe("actionStyles", () => {
mouse.up(20, 20);
// Change some styles of second rectangle
UI.clickLabeledElement("Stroke");
UI.clickLabeledElement(t("colors.c92a2a"));
UI.clickLabeledElement("Background");
UI.clickLabeledElement(t("colors.e64980"));
togglePopover("Stroke");
UI.clickOnTestId("color-red");
togglePopover("Background");
UI.clickOnTestId("color-blue");
// Fill style
fireEvent.click(screen.getByTitle("Cross-hatch"));
// Stroke width
@ -60,8 +72,8 @@ describe("actionStyles", () => {
const firstRect = API.getSelectedElement();
expect(firstRect.id).toBe(h.elements[0].id);
expect(firstRect.strokeColor).toBe("#c92a2a");
expect(firstRect.backgroundColor).toBe("#e64980");
expect(firstRect.strokeColor).toBe("#e03131");
expect(firstRect.backgroundColor).toBe("#a5d8ff");
expect(firstRect.fillStyle).toBe("cross-hatch");
expect(firstRect.strokeWidth).toBe(2); // Bold: 2
expect(firstRect.strokeStyle).toBe("dotted");

View File

@ -12,7 +12,10 @@ import {
DEFAULT_FONT_FAMILY,
DEFAULT_TEXT_ALIGN,
} from "../constants";
import { getBoundTextElement } from "../element/textElement";
import {
getBoundTextElement,
getDefaultLineHeight,
} from "../element/textElement";
import {
hasBoundTextElement,
canApplyRoundnessTypeToElement,
@ -92,12 +95,18 @@ export const actionPasteStyles = register({
});
if (isTextElement(newElement)) {
const fontSize =
elementStylesToCopyFrom?.fontSize || DEFAULT_FONT_SIZE;
const fontFamily =
elementStylesToCopyFrom?.fontFamily || DEFAULT_FONT_FAMILY;
newElement = newElementWith(newElement, {
fontSize: elementStylesToCopyFrom?.fontSize || DEFAULT_FONT_SIZE,
fontFamily:
elementStylesToCopyFrom?.fontFamily || DEFAULT_FONT_FAMILY,
fontSize,
fontFamily,
textAlign:
elementStylesToCopyFrom?.textAlign || DEFAULT_TEXT_ALIGN,
lineHeight:
elementStylesToCopyFrom.lineHeight ||
getDefaultLineHeight(fontFamily),
});
let container = null;
if (newElement.containerId) {

View File

@ -84,5 +84,5 @@ export { actionToggleZenMode } from "./actionToggleZenMode";
export { actionToggleStats } from "./actionToggleStats";
export { actionUnbindText, actionBindText } from "./actionBoundText";
export { actionLink } from "../element/Hyperlink";
export { actionToggleLock } from "./actionToggleLock";
export { actionToggleElementLock } from "./actionElementLock";
export { actionToggleLinearEditor } from "./actionLinearEditor";

View File

@ -118,10 +118,13 @@ export class ActionManager {
return true;
}
executeAction(action: Action, source: ActionSource = "api") {
executeAction(
action: Action,
source: ActionSource = "api",
value: any = null,
) {
const elements = this.getElementsIncludingDeleted();
const appState = this.getAppState();
const value = null;
trackAction(action, source, appState, elements, this.app, value);

View File

@ -1,5 +1,6 @@
import { isDarwin } from "../constants";
import { t } from "../i18n";
import { SubtypeOf } from "../utility-types";
import { getShortcutKey } from "../utils";
import { ActionName } from "./types";
@ -8,6 +9,7 @@ export type ShortcutName =
ActionName,
| "toggleTheme"
| "loadScene"
| "clearCanvas"
| "cut"
| "copy"
| "paste"
@ -32,7 +34,7 @@ export type ShortcutName =
| "flipHorizontal"
| "flipVertical"
| "hyperlink"
| "toggleLock"
| "toggleElementLock"
>
| "saveScene"
| "imageExport";
@ -41,6 +43,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
toggleTheme: [getShortcutKey("Shift+Alt+D")],
saveScene: [getShortcutKey("CtrlOrCmd+S")],
loadScene: [getShortcutKey("CtrlOrCmd+O")],
clearCanvas: [getShortcutKey("CtrlOrCmd+Delete")],
imageExport: [getShortcutKey("CtrlOrCmd+Shift+E")],
cut: [getShortcutKey("CtrlOrCmd+X")],
copy: [getShortcutKey("CtrlOrCmd+C")],
@ -77,7 +80,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
flipVertical: [getShortcutKey("Shift+V")],
viewMode: [getShortcutKey("Alt+R")],
hyperlink: [getShortcutKey("CtrlOrCmd+K")],
toggleLock: [getShortcutKey("CtrlOrCmd+Shift+L")],
toggleElementLock: [getShortcutKey("CtrlOrCmd+Shift+L")],
};
export const getShortcutFromShortcutName = (name: ShortcutName) => {

View File

@ -6,6 +6,7 @@ import {
ExcalidrawProps,
BinaryFiles,
} from "../types";
import { MarkOptional } from "../utility-types";
export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api";
@ -110,10 +111,12 @@ export type ActionName =
| "unbindText"
| "hyperlink"
| "bindText"
| "toggleLock"
| "unlockAllElements"
| "toggleElementLock"
| "toggleLinearEditor"
| "toggleEraserTool"
| "toggleHandTool";
| "toggleHandTool"
| "wrapTextInContainer";
export type PanelComponentProps = {
elements: readonly ExcalidrawElement[];

View File

@ -1,22 +1,41 @@
export const trackEvent =
typeof process !== "undefined" &&
process.env?.REACT_APP_GOOGLE_ANALYTICS_ID &&
typeof window !== "undefined" &&
window.gtag
? (category: string, action: string, label?: string, value?: number) => {
try {
window.gtag("event", action, {
event_category: category,
event_label: label,
value,
});
} catch (error) {
console.error("error logging to ga", error);
}
}
: typeof process !== "undefined" && process.env?.JEST_WORKER_ID
? (category: string, action: string, label?: string, value?: number) => {}
: (category: string, action: string, label?: string, value?: number) => {
// Uncomment the next line to track locally
// console.log("Track Event", { category, action, label, value });
};
export const trackEvent = (
category: string,
action: string,
label?: string,
value?: number,
) => {
try {
// Uncomment the next line to track locally
// console.log("Track Event", { category, action, label, value });
if (typeof window === "undefined" || process.env.JEST_WORKER_ID) {
return;
}
if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID && window.gtag) {
window.gtag("event", action, {
event_category: category,
event_label: label,
value,
});
}
if (window.sa_event) {
window.sa_event(action, {
category,
label,
value,
});
}
if (window.fathom) {
window.fathom.trackEvent(action, {
category,
label,
value,
});
}
} catch (error) {
console.error("error during analytics", error);
}
};

View File

@ -1,5 +1,6 @@
import oc from "open-color";
import { COLOR_PALETTE } from "./colors";
import {
DEFAULT_ELEMENT_PROPS,
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
DEFAULT_TEXT_ALIGN,
@ -23,18 +24,18 @@ export const getDefaultAppState = (): Omit<
theme: THEME.LIGHT,
collaborators: new Map(),
currentChartType: "bar",
currentItemBackgroundColor: "transparent",
currentItemBackgroundColor: DEFAULT_ELEMENT_PROPS.backgroundColor,
currentItemEndArrowhead: "arrow",
currentItemFillStyle: "hachure",
currentItemFillStyle: DEFAULT_ELEMENT_PROPS.fillStyle,
currentItemFontFamily: DEFAULT_FONT_FAMILY,
currentItemFontSize: DEFAULT_FONT_SIZE,
currentItemOpacity: 100,
currentItemRoughness: 1,
currentItemOpacity: DEFAULT_ELEMENT_PROPS.opacity,
currentItemRoughness: DEFAULT_ELEMENT_PROPS.roughness,
currentItemStartArrowhead: null,
currentItemStrokeColor: oc.black,
currentItemStrokeColor: DEFAULT_ELEMENT_PROPS.strokeColor,
currentItemRoundness: "round",
currentItemStrokeStyle: "solid",
currentItemStrokeWidth: 1,
currentItemStrokeStyle: DEFAULT_ELEMENT_PROPS.strokeStyle,
currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
cursorButton: "up",
draggingElement: null,
@ -44,7 +45,7 @@ export const getDefaultAppState = (): Omit<
activeTool: {
type: "selection",
customType: null,
locked: false,
locked: DEFAULT_ELEMENT_PROPS.locked,
lastActiveTool: null,
},
penMode: false,
@ -57,7 +58,7 @@ export const getDefaultAppState = (): Omit<
fileHandle: null,
gridSize: null,
isBindingEnabled: true,
isSidebarDocked: false,
defaultSidebarDockedPreference: false,
isLoading: false,
isResizing: false,
isRotating: false,
@ -83,7 +84,7 @@ export const getDefaultAppState = (): Omit<
startBoundElement: null,
suggestedBindings: [],
toast: null,
viewBackgroundColor: oc.white,
viewBackgroundColor: COLOR_PALETTE.white,
zenModeEnabled: false,
zoom: {
value: 1 as NormalizedZoomValue,
@ -149,7 +150,11 @@ const APP_STATE_STORAGE_CONF = (<
gridSize: { browser: true, export: true, server: true },
height: { browser: false, export: false, server: false },
isBindingEnabled: { browser: false, export: false, server: false },
isSidebarDocked: { browser: true, export: false, server: false },
defaultSidebarDockedPreference: {
browser: true,
export: false,
server: false,
},
isLoading: { browser: false, export: false, server: false },
isResizing: { browser: false, export: false, server: false },
isRotating: { browser: false, export: false, server: false },

20
src/assets/lock.svg Normal file
View File

@ -0,0 +1,20 @@
<svg width="178" height="162" viewBox="0 0 178 162" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.3329 54.3823L38.5547 94.3134L39.7731 111.754L40.1282 118.907L41.0832 123.59L44.3502 131.942L48.9438 137.693L52.5472 143.333L58.5544 147.755L62.5364 150.239L72.3634 154.486L83.15 156.361L91.1212 158.708L101.174 157.525L110.808 156.719L115.983 154.049L124.511 151.377L129.276 148.71L133.701 143.947L139.666 135.877L142.001 128.136L145.746 118.192L145.188 111.065L145.489 94.3675L145.873 75.2546L143.227 59.7779L142.022 47.4695L138.595 46.8345L102.952 45.4703L56.9173 46.7498L46.0719 49.1207L41.9323 50.6825L39.5684 53.4297" fill="#E3E2FE"/>
<path d="M41.0014 54.2859C41.0861 64.8796 38.3765 102.581 40.9779 117.876C43.5793 133.17 48.2646 139.346 56.6121 146.047C64.9596 152.746 79.1214 157.662 91.0653 158.078C103.009 158.492 119.347 155.242 128.277 148.543C137.206 141.842 142.112 133.527 144.641 117.874C147.169 102.221 146.061 66.4132 143.446 54.6222C140.83 42.8289 143.97 48.2857 128.948 47.1238C113.925 45.9619 67.9608 46.477 53.3051 47.6483C38.6493 48.8197 43.2053 53.0675 41.0155 54.1518M40.5263 53.9801C40.5404 64.6138 37.9249 103.418 40.5921 118.587C43.257 133.755 48.147 138.325 56.5251 144.991C64.9008 151.655 78.7263 157.935 90.8536 158.577C102.981 159.221 120.212 155.413 129.289 148.844C138.368 142.277 142.872 134.995 145.321 119.168C147.767 103.343 146.805 65.8698 143.977 53.8837C141.148 41.8975 143.615 48.4292 128.348 47.2508C113.081 46.0724 67.14 45.6726 52.3737 46.8157C37.6074 47.9564 41.7776 53.091 39.7524 54.1024" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.7935 45.726L66.36 36.6964L65.5979 34.4501L67.3384 30.7668L70.4197 26.5048L74.8157 21.0598L81.9095 16.9131L89.9419 14.4951L95.6127 12.8534L97.7555 13.2133L103.819 15.269L106.552 18.5807L109.967 22.3793L114.45 27.2387L114.904 34.3937L117.153 38.9214L116.031 44.5005L116.765 45.5448L118.863 47.3559L126.164 47.6782L127.744 46.7797L128.76 44.7052L123.882 25.8227L120.641 20.4835L116.351 15.8758L112.809 11.0729L108.617 7.36601L101.352 5.43025L93.263 3.31104L84.2451 5.1433L77.7299 7.86229L76.0011 8.0975L62.1168 19.8532L59.5366 24.5597L55.7733 32.3215L55.0371 39.5282L56.0626 44.8933L56.5636 47.1725L59.6401 46.8644L66.1648 46.0388" fill="#E3E2FE"/>
<path d="M65.0037 46.0106C65.1166 43.8231 64.9237 36.8492 66.1421 33.2412C67.3605 29.6307 69.0799 27.2857 72.314 24.3527C75.5481 21.422 81.273 17.6093 85.5444 15.6453C89.8157 13.6813 94.3317 12.1148 97.9421 12.5664C101.555 13.0157 104.544 15.857 107.219 18.3478C109.893 20.8387 112.356 24.0869 113.986 27.5115C115.618 30.9338 116.389 35.8684 117.006 38.8861C117.622 41.9038 115.992 44.2888 117.69 45.6178C119.388 46.949 125.617 48.1721 127.195 46.8667C128.773 45.5613 127.717 41.1888 127.157 37.7877C126.597 34.3866 125.494 30.1247 123.838 26.4601C122.183 22.7956 119.746 18.9029 117.222 15.7982C114.698 12.6934 112.791 9.9086 108.696 7.83642C104.601 5.76189 97.8081 3.42863 92.6547 3.35337C87.5013 3.2781 81.5529 5.74308 77.7708 7.38717C73.991 9.03127 72.8879 10.6166 69.9666 13.218C67.043 15.8217 62.4306 19.6768 60.2384 23.0026C58.0486 26.3284 57.4818 29.252 56.8185 33.1753C56.1529 37.0962 54.6499 44.39 56.2517 46.5327C57.8511 48.6731 64.7756 45.98 66.4267 46.0247M65.9704 45.5096C65.9845 43.348 64.2652 37.5525 65.5423 33.8456C66.8172 30.1364 70.2959 26.4789 73.6264 23.259C76.9546 20.039 81.3177 16.3015 85.5208 14.5281C89.7216 12.7523 95.1079 11.7903 98.8383 12.6111C102.569 13.432 105.283 16.8072 107.903 19.4486C110.526 22.09 113.146 25.3029 114.567 28.4664C115.987 31.6276 116.03 35.4051 116.425 38.4204C116.82 41.4334 115.124 45.1426 116.937 46.5515C118.751 47.9604 125.539 48.2968 127.31 46.8761C129.081 45.4578 127.978 41.4428 127.562 38.0347C127.145 34.6265 126.501 30.1646 124.81 26.4296C123.116 22.6921 120.195 18.7594 117.413 15.6194C114.63 12.4818 112.247 9.39349 108.117 7.58945C103.987 5.78541 97.5776 5.02099 92.6335 4.79519C87.6895 4.56939 82.3503 4.78813 78.4505 6.23466C74.5484 7.68118 72.0882 10.6542 69.228 13.4696C66.3679 16.2851 63.4725 19.7873 61.2898 23.1319C59.1071 26.4789 56.9761 29.4896 56.1293 33.5469C55.285 37.6043 54.577 45.2132 56.2117 47.4759C57.8487 49.7409 64.2675 47.3418 65.9445 47.1301" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
<path d="M140.37 54.8958C137.884 58.1322 127.704 71.2286 125.185 74.5427M139.697 54.209C137.098 57.5466 127.005 71.7884 124.51 75.3565" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
<path d="M141.663 63.1765C139.661 66.0413 131.311 77.1501 129.077 79.9726M141.065 62.5908C139.021 65.2792 130.631 76.1364 128.717 78.8625" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
<path d="M141.888 72.9917C139.475 75.8589 130.268 86.8478 127.966 89.7455M141.02 72.726C138.503 75.6496 129.775 87.2476 127.58 90.3242" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
<path d="M141.948 82.215C139.815 85.1057 130.308 96.8214 127.961 99.7709M141.459 81.7375C139.298 84.4119 129.816 95.9888 127.479 98.8606" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
<path d="M141.357 91.7838C138.885 95.2484 128.808 108.535 126.428 111.76M142.474 91.4757C139.917 94.7921 128.38 107.493 125.781 110.883" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
<path d="M142.568 101.479C140.028 104.403 129.867 115.528 127.195 118.356M141.811 101.018C139.212 104.055 129.477 115.975 126.828 118.97" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
<path d="M141.023 112.172C138.591 114.775 128.028 125.905 125.422 128.664M140.51 113.465C138.008 116.147 127.36 125.233 124.742 127.582" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
<path d="M139.004 123.69C136.501 126.275 125.952 137.248 123.287 140.108M138.343 124.817C135.805 127.454 125.487 138.261 122.848 140.75" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
<path d="M132.192 139.862C129.854 141.624 120.87 148.168 118.574 150.012M131.39 139.496C128.97 141.333 120.524 148.89 118.322 150.621" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M82.6351 92.3124L78.2767 89.0148L78.6718 88.8784L75.6282 79.6865L74.4922 76.0525L75.0379 74.1074L78.6248 69.5444L83.6182 65.186L86.6924 64.0711L93.7768 63.9864L99.9181 63.9276L103.905 64.4215L106.038 66.068L109.333 67.6392L110.251 69.4479L112.438 73.1877L112.702 81.928L111.674 82.93L110.907 85.5573L107.828 89.2336L101.273 92.9193L102.785 120.401L99.5488 125.521L98.0059 127.838L96.1313 129.414L93.17 130.237L92.2198 130.033L90.1358 129.233L88.8328 126.594L87.8378 95.2549L88.9386 93.3215L86.0409 91.294L80.9533 91.1552" fill="white"/>
<path d="M82.8214 92.0607C82.0664 91.4327 79.291 90.7201 77.8539 88.2033C76.4167 85.6866 73.5284 80.4438 74.1964 76.9581C74.862 73.4723 78.6959 69.6384 81.8524 67.2887C85.0089 64.939 88.9227 63.1138 93.1353 62.8574C97.3478 62.6034 103.957 63.9888 107.132 65.7575C110.31 67.5263 111.416 70.5651 112.196 73.4747C112.977 76.3842 112.606 80.6626 111.82 83.2122C111.035 85.7642 109.078 87.1661 107.481 88.7749C105.883 90.3837 103.106 91.2751 102.233 92.8651C101.363 94.4551 102.327 95.3254 102.25 98.3125C102.172 101.3 101.76 107.227 101.767 110.788C101.772 114.349 102.487 116.981 102.285 119.676C102.085 122.374 101.52 125.126 100.556 126.965C99.5917 128.805 98.077 130.256 96.5011 130.715C94.9275 131.171 92.4485 130.36 91.1101 129.713C89.7742 129.066 89.0144 128.341 88.4805 126.836C87.9489 125.331 87.9678 123.964 87.9137 120.681C87.8596 117.397 88.1159 111.599 88.1583 107.14C88.203 102.68 89.2779 96.445 88.1724 93.9236C87.0693 91.4022 82.7791 92.4347 81.5325 92.0137M82.0194 91.6068C81.222 90.7624 78.4536 89.7886 77.3623 87.1567C76.2733 84.5247 74.6621 79.3125 75.4783 75.815C76.2921 72.3151 79.1428 68.3166 82.2522 66.1597C85.3617 64.0029 90.1693 63.062 94.1302 62.8739C98.0911 62.6857 102.925 63.0832 106.02 65.0331C109.118 66.9853 111.834 71.5836 112.705 74.5801C113.572 77.5743 111.949 80.7731 111.234 83.0076C110.519 85.2444 109.835 86.3711 108.417 87.9916C107.001 89.6146 103.738 90.9623 102.732 92.7358C101.725 94.5092 102.351 95.6382 102.377 98.6301C102.405 101.62 102.866 106.949 102.894 110.682C102.922 114.417 102.955 118.291 102.544 121.038C102.13 123.783 101.408 125.54 100.42 127.161C99.4318 128.781 98.1005 130.233 96.6163 130.759C95.1322 131.286 92.9353 130.893 91.51 130.322C90.0846 129.753 88.7769 128.889 88.0618 127.335C87.3468 125.78 87.0128 124.317 87.2198 120.998C87.4268 117.68 89.0874 112.046 89.299 107.422C89.5107 102.798 89.8494 95.9322 88.4946 93.2509C87.1398 90.5695 82.4804 91.4845 81.1679 91.3316" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
<path d="M28.1943 139.31C26.7936 139.432 25.332 140.402 23.8703 140.523C23.1395 140.766 22.5914 140.16 21.9824 139.735C21.5561 139.553 21.008 138.461 20.8253 138.219C20.5817 136.884 19.9118 134.276 20.0336 133.002C19.7291 131.364 21.5561 129.787 23.0786 129.727C23.2613 129.727 23.8094 129.787 23.8703 129.787C25.7583 130.151 27.5853 131.546 29.5341 131.728C29.595 131.728 29.6559 131.728 29.6559 131.668C30.4476 130.333 30.204 126.937 30.813 125.542C30.813 125.36 31.1784 123.54 31.1784 123.237C31.6048 122.327 32.1529 121.781 33.1273 122.084C33.7972 122.266 34.6498 122.388 34.6498 123.237V128.635C34.8325 129.242 36.1114 128.999 36.5986 128.999C38.7911 128.028 40.8617 127.422 43.3586 127.058C45.6729 127.179 46.7082 129.242 46.5864 131.304C46.6473 132.396 45.4293 133.245 44.6985 133.973C44.4549 134.094 43.4804 134.519 43.115 134.397C42.2624 133.791 41.1662 134.033 40.1309 134.094C40.1309 134.155 40.0091 134.337 40.07 134.397C41.288 135.853 43.5413 136.096 45.0639 137.066C46.1601 138.34 47.4999 138.643 47.1345 140.341C47.0736 141.191 47.1345 142.1 46.221 142.404C45.9774 142.586 44.5767 142.828 44.2722 142.828C43.9677 142.768 43.3586 142.343 43.115 142.04C40.9835 141.13 38.6693 140.402 36.2332 140.159V145.133C35.9896 146.468 35.6851 147.923 34.6498 148.955C34.2844 149.015 33.1273 149.015 32.7619 148.955C32.4574 148.773 31.4221 147.741 31.1784 147.438C30.5694 145.133 30.4476 142.404 29.6559 140.159C29.1687 139.553 28.986 139.25 28.1943 139.31Z" fill="#6965DB"/>
<path d="M59.5964 139.31C58.1956 139.432 56.734 140.402 55.2724 140.523C54.5416 140.766 53.9935 140.16 53.3845 139.735C52.9582 139.553 52.41 138.461 52.2273 138.219C51.9837 136.884 51.3138 134.276 51.4356 133.002C51.1311 131.364 52.9582 129.787 54.4807 129.727C54.6634 129.727 55.2115 129.787 55.2724 129.787C57.1603 130.151 58.9874 131.546 60.9362 131.728C60.9971 131.728 61.058 131.728 61.058 131.668C61.8497 130.333 61.6061 126.937 62.2151 125.542C62.2151 125.36 62.5805 123.54 62.5805 123.237C63.0068 122.327 63.5549 121.781 64.5293 122.084C65.1992 122.266 66.0519 122.388 66.0519 123.237V128.635C66.2346 129.242 67.5135 128.999 68.0007 128.999C70.1931 128.028 72.2638 127.422 74.7607 127.058C77.0749 127.179 78.1103 129.242 77.9885 131.304C78.0494 132.396 76.8313 133.245 76.1005 133.973C75.8569 134.094 74.8825 134.519 74.5171 134.397C73.6645 133.791 72.5683 134.033 71.5329 134.094C71.5329 134.155 71.4112 134.337 71.4721 134.397C72.6901 135.853 74.9434 136.096 76.4659 137.066C77.5621 138.34 78.902 138.643 78.5366 140.341C78.4757 141.191 78.5366 142.1 77.623 142.404C77.3794 142.586 75.9787 142.828 75.6742 142.828C75.3697 142.768 74.7607 142.343 74.5171 142.04C72.3856 141.13 70.0713 140.402 67.6353 140.159V145.133C67.3917 146.468 67.0872 147.923 66.0519 148.955C65.6865 149.015 64.5293 149.015 64.1639 148.955C63.8594 148.773 62.8241 147.741 62.5805 147.438C61.9715 145.133 61.8497 142.404 61.058 140.159C60.5708 139.553 60.3881 139.25 59.5964 139.31Z" fill="#6965DB"/>
<path d="M90.9984 139.31C89.5977 139.432 88.1361 140.402 86.6745 140.523C85.9436 140.766 85.3955 140.16 84.7865 139.735C84.3602 139.553 83.8121 138.461 83.6294 138.219C83.3858 136.884 82.7159 134.276 82.8377 133.002C82.5332 131.364 84.3602 129.787 85.8827 129.727C86.0654 129.727 86.6136 129.787 86.6745 129.787C88.5624 130.151 90.3894 131.546 92.3382 131.728C92.3991 131.728 92.46 131.728 92.46 131.668C93.2518 130.333 93.0082 126.937 93.6172 125.542C93.6172 125.36 93.9826 123.54 93.9826 123.237C94.4089 122.327 94.957 121.781 95.9314 122.084C96.6013 122.266 97.4539 122.388 97.4539 123.237V128.635C97.6366 129.242 98.9155 128.999 99.4028 128.999C101.595 128.028 103.666 127.422 106.163 127.058C108.477 127.179 109.512 129.242 109.391 131.304C109.451 132.396 108.233 133.245 107.503 133.973C107.259 134.094 106.285 134.519 105.919 134.397C105.067 133.791 103.97 134.033 102.935 134.094C102.935 134.155 102.813 134.337 102.874 134.397C104.092 135.853 106.345 136.096 107.868 137.066C108.964 138.34 110.304 138.643 109.939 140.341C109.878 141.191 109.939 142.1 109.025 142.404C108.782 142.586 107.381 142.828 107.076 142.828C106.772 142.768 106.163 142.343 105.919 142.04C103.788 141.13 101.473 140.402 99.0373 140.159V145.133C98.7937 146.468 98.4892 147.923 97.4539 148.955C97.0885 149.015 95.9314 149.015 95.566 148.955C95.2615 148.773 94.2262 147.741 93.9826 147.438C93.3736 145.133 93.2518 142.404 92.46 140.159C91.9728 139.553 91.7901 139.25 90.9984 139.31Z" fill="#6965DB"/>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,4 +1,8 @@
import colors from "./colors";
import {
COLOR_PALETTE,
DEFAULT_CHART_COLOR_INDEX,
getAllColorsSpecificShade,
} from "./colors";
import {
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
@ -158,10 +162,7 @@ export const tryParseSpreadsheet = (text: string): ParseSpreadsheetResult => {
return result;
};
const bgColors = colors.elementBackground.slice(
2,
colors.elementBackground.length,
);
const bgColors = getAllColorsSpecificShade(DEFAULT_CHART_COLOR_INDEX);
// Put all the common properties here so when the whole chart is selected
// the properties dialog shows the correct selected values
@ -171,7 +172,7 @@ const commonProps = {
fontSize: DEFAULT_FONT_SIZE,
opacity: 100,
roughness: 1,
strokeColor: colors.elementStroke[0],
strokeColor: COLOR_PALETTE.black,
roundness: null,
strokeStyle: "solid",
strokeWidth: 1,
@ -323,7 +324,6 @@ const chartBaseElements = (
x: x + chartWidth / 2,
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
roundness: null,
strokeStyle: "solid",
textAlign: "center",
})
: null;
@ -338,7 +338,7 @@ const chartBaseElements = (
y: y - chartHeight,
width: chartWidth,
height: chartHeight,
strokeColor: colors.elementStroke[0],
strokeColor: COLOR_PALETTE.black,
fillStyle: "solid",
opacity: 6,
})

View File

@ -1,6 +1,17 @@
import colors from "./colors";
import {
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
getAllColorsSpecificShade,
} from "./colors";
import { AppState } from "./types";
const BG_COLORS = getAllColorsSpecificShade(
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
);
const STROKE_COLORS = getAllColorsSpecificShade(
DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
);
export const getClientColors = (clientId: string, appState: AppState) => {
if (appState?.collaborators) {
const currentUser = appState.collaborators.get(clientId);
@ -11,18 +22,19 @@ export const getClientColors = (clientId: string, appState: AppState) => {
// Naive way of getting an integer out of the clientId
const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0);
// Skip transparent & gray colors
const backgrounds = colors.elementBackground.slice(3);
const strokes = colors.elementStroke.slice(3);
return {
background: backgrounds[sum % backgrounds.length],
stroke: strokes[sum % strokes.length],
background: BG_COLORS[sum % BG_COLORS.length],
stroke: STROKE_COLORS[sum % STROKE_COLORS.length],
};
};
export const getClientInitials = (userName?: string | null) => {
if (!userName) {
return "?";
}
return userName.trim()[0].toUpperCase();
/**
* returns first char, capitalized
*/
export const getNameInitial = (name?: string | null) => {
// first char can be a surrogate pair, hence using codePointAt
const firstCodePoint = name?.trim()?.codePointAt(0);
return (
firstCodePoint ? String.fromCodePoint(firstCodePoint) : "?"
).toUpperCase();
};

View File

@ -2,12 +2,12 @@ import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
} from "./element/types";
import { AppState, BinaryFiles } from "./types";
import { BinaryFiles } from "./types";
import { SVG_EXPORT_TAG } from "./scene/export";
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
import { isInitializedImageElement } from "./element/typeChecks";
import { isPromiseLike } from "./utils";
import { isPromiseLike, isTestEnv } from "./utils";
type ElementsClipboard = {
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
@ -55,24 +55,40 @@ const clipboardContainsElements = (
export const copyToClipboard = async (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
files: BinaryFiles | null,
) => {
let foundFile = false;
const _files = elements.reduce((acc, element) => {
if (isInitializedImageElement(element)) {
foundFile = true;
if (files && files[element.fileId]) {
acc[element.fileId] = files[element.fileId];
}
}
return acc;
}, {} as BinaryFiles);
if (foundFile && !files) {
console.warn(
"copyToClipboard: attempting to file element(s) without providing associated `files` object.",
);
}
// select binded text elements when copying
const contents: ElementsClipboard = {
type: EXPORT_DATA_TYPES.excalidrawClipboard,
elements,
files: files
? elements.reduce((acc, element) => {
if (isInitializedImageElement(element) && files[element.fileId]) {
acc[element.fileId] = files[element.fileId];
}
return acc;
}, {} as BinaryFiles)
: undefined,
files: files ? _files : undefined,
};
const json = JSON.stringify(contents);
if (isTestEnv()) {
return json;
}
CLIPBOARD = json;
try {
PREFER_APP_CLIPBOARD = false;
await copyTextToSystemClipboard(json);

View File

@ -1,22 +1,170 @@
import oc from "open-color";
import { Merge } from "./utility-types";
const shades = (index: number) => [
oc.red[index],
oc.pink[index],
oc.grape[index],
oc.violet[index],
oc.indigo[index],
oc.blue[index],
oc.cyan[index],
oc.teal[index],
oc.green[index],
oc.lime[index],
oc.yellow[index],
oc.orange[index],
];
export default {
canvasBackground: [oc.white, oc.gray[0], oc.gray[1], ...shades(0)],
elementBackground: ["transparent", oc.gray[4], oc.gray[6], ...shades(6)],
elementStroke: [oc.black, oc.gray[8], oc.gray[7], ...shades(9)],
// FIXME can't put to utils.ts rn because of circular dependency
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
source: R,
keys: K,
) => {
return keys.reduce((acc, key: K[number]) => {
if (key in source) {
acc[key] = source[key];
}
return acc;
}, {} as Pick<R, K[number]>) as Pick<R, K[number]>;
};
export type ColorPickerColor =
| Exclude<keyof oc, "indigo" | "lime">
| "transparent"
| "bronze";
export type ColorTuple = readonly [string, string, string, string, string];
export type ColorPalette = Merge<
Record<ColorPickerColor, ColorTuple>,
{ black: string; white: string; transparent: string }
>;
// used general type instead of specific type (ColorPalette) to support custom colors
export type ColorPaletteCustom = { [key: string]: ColorTuple | string };
export type ColorShadesIndexes = [number, number, number, number, number];
export const MAX_CUSTOM_COLORS_USED_IN_CANVAS = 5;
export const COLORS_PER_ROW = 5;
export const DEFAULT_CHART_COLOR_INDEX = 4;
export const DEFAULT_ELEMENT_STROKE_COLOR_INDEX = 4;
export const DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX = 1;
export const ELEMENTS_PALETTE_SHADE_INDEXES = [0, 2, 4, 6, 8] as const;
export const CANVAS_PALETTE_SHADE_INDEXES = [0, 1, 2, 3, 4] as const;
export const getSpecificColorShades = (
color: Exclude<
ColorPickerColor,
"transparent" | "white" | "black" | "bronze"
>,
indexArr: Readonly<ColorShadesIndexes>,
) => {
return indexArr.map((index) => oc[color][index]) as any as ColorTuple;
};
export const COLOR_PALETTE = {
transparent: "transparent",
black: "#1e1e1e",
white: "#ffffff",
// open-colors
gray: getSpecificColorShades("gray", ELEMENTS_PALETTE_SHADE_INDEXES),
red: getSpecificColorShades("red", ELEMENTS_PALETTE_SHADE_INDEXES),
pink: getSpecificColorShades("pink", ELEMENTS_PALETTE_SHADE_INDEXES),
grape: getSpecificColorShades("grape", ELEMENTS_PALETTE_SHADE_INDEXES),
violet: getSpecificColorShades("violet", ELEMENTS_PALETTE_SHADE_INDEXES),
blue: getSpecificColorShades("blue", ELEMENTS_PALETTE_SHADE_INDEXES),
cyan: getSpecificColorShades("cyan", ELEMENTS_PALETTE_SHADE_INDEXES),
teal: getSpecificColorShades("teal", ELEMENTS_PALETTE_SHADE_INDEXES),
green: getSpecificColorShades("green", ELEMENTS_PALETTE_SHADE_INDEXES),
yellow: getSpecificColorShades("yellow", ELEMENTS_PALETTE_SHADE_INDEXES),
orange: getSpecificColorShades("orange", ELEMENTS_PALETTE_SHADE_INDEXES),
// radix bronze shades 3,5,7,9,11
bronze: ["#f8f1ee", "#eaddd7", "#d2bab0", "#a18072", "#846358"],
} as ColorPalette;
const COMMON_ELEMENT_SHADES = pick(COLOR_PALETTE, [
"cyan",
"blue",
"violet",
"grape",
"pink",
"green",
"teal",
"yellow",
"orange",
"red",
]);
// -----------------------------------------------------------------------------
// quick picks defaults
// -----------------------------------------------------------------------------
// ORDER matters for positioning in quick picker
export const DEFAULT_ELEMENT_STROKE_PICKS = [
COLOR_PALETTE.black,
COLOR_PALETTE.red[DEFAULT_ELEMENT_STROKE_COLOR_INDEX],
COLOR_PALETTE.green[DEFAULT_ELEMENT_STROKE_COLOR_INDEX],
COLOR_PALETTE.blue[DEFAULT_ELEMENT_STROKE_COLOR_INDEX],
COLOR_PALETTE.yellow[DEFAULT_ELEMENT_STROKE_COLOR_INDEX],
] as ColorTuple;
// ORDER matters for positioning in quick picker
export const DEFAULT_ELEMENT_BACKGROUND_PICKS = [
COLOR_PALETTE.transparent,
COLOR_PALETTE.red[DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX],
COLOR_PALETTE.green[DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX],
COLOR_PALETTE.blue[DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX],
COLOR_PALETTE.yellow[DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX],
] as ColorTuple;
// ORDER matters for positioning in quick picker
export const DEFAULT_CANVAS_BACKGROUND_PICKS = [
COLOR_PALETTE.white,
// radix slate2
"#f8f9fa",
// radix blue2
"#f5faff",
// radix yellow2
"#fffce8",
// radix bronze2
"#fdf8f6",
] as ColorTuple;
// -----------------------------------------------------------------------------
// palette defaults
// -----------------------------------------------------------------------------
export const DEFAULT_ELEMENT_STROKE_COLOR_PALETTE = {
// 1st row
transparent: COLOR_PALETTE.transparent,
white: COLOR_PALETTE.white,
gray: COLOR_PALETTE.gray,
black: COLOR_PALETTE.black,
bronze: COLOR_PALETTE.bronze,
// rest
...COMMON_ELEMENT_SHADES,
} as const;
// ORDER matters for positioning in pallete (5x3 grid)s
export const DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE = {
transparent: COLOR_PALETTE.transparent,
white: COLOR_PALETTE.white,
gray: COLOR_PALETTE.gray,
black: COLOR_PALETTE.black,
bronze: COLOR_PALETTE.bronze,
...COMMON_ELEMENT_SHADES,
} as const;
// -----------------------------------------------------------------------------
// helpers
// -----------------------------------------------------------------------------
// !!!MUST BE WITHOUT GRAY, TRANSPARENT AND BLACK!!!
export const getAllColorsSpecificShade = (index: 0 | 1 | 2 | 3 | 4) =>
[
// 2nd row
COLOR_PALETTE.cyan[index],
COLOR_PALETTE.blue[index],
COLOR_PALETTE.violet[index],
COLOR_PALETTE.grape[index],
COLOR_PALETTE.pink[index],
// 3rd row
COLOR_PALETTE.green[index],
COLOR_PALETTE.teal[index],
COLOR_PALETTE.yellow[index],
COLOR_PALETTE.orange[index],
COLOR_PALETTE.red[index],
] as const;
export const rgbToHex = (r: number, g: number, b: number) =>
`#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
// -----------------------------------------------------------------------------

View File

@ -14,7 +14,7 @@ import {
hasText,
} from "../scene";
import { SHAPES } from "../shapes";
import { AppState, Zoom } from "../types";
import { UIAppState, Zoom } from "../types";
import {
capitalizeString,
isTransparent,
@ -28,16 +28,20 @@ import { trackEvent } from "../analytics";
import { hasBoundTextElement } from "../element/typeChecks";
import clsx from "clsx";
import { actionToggleZenMode } from "../actions";
import "./Actions.scss";
import { Tooltip } from "./Tooltip";
import { shouldAllowVerticalAlign } from "../element/textElement";
import {
shouldAllowVerticalAlign,
suppportsHorizontalAlign,
} from "../element/textElement";
import "./Actions.scss";
export const SelectedShapeActions = ({
appState,
elements,
renderAction,
}: {
appState: AppState;
appState: UIAppState;
elements: readonly ExcalidrawElement[];
renderAction: ActionManager["renderAction"];
}) => {
@ -122,7 +126,8 @@ export const SelectedShapeActions = ({
{renderAction("changeFontFamily")}
{renderAction("changeTextAlign")}
{suppportsHorizontalAlign(targetElements) &&
renderAction("changeTextAlign")}
</>
)}
@ -211,10 +216,10 @@ export const ShapesSwitcher = ({
appState,
}: {
canvas: HTMLCanvasElement | null;
activeTool: AppState["activeTool"];
setAppState: React.Component<any, AppState>["setState"];
activeTool: UIAppState["activeTool"];
setAppState: React.Component<any, UIAppState>["setState"];
onImageAction: (data: { pointerType: PointerType | null }) => void;
appState: AppState;
appState: UIAppState;
}) => (
<>
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {

View File

@ -1,6 +1,7 @@
import { atom, useAtom } from "jotai";
import { actionClearCanvas } from "../actions";
import { t } from "../i18n";
import { jotaiScope } from "../jotai";
import { useExcalidrawActionManager } from "./App";
import ConfirmDialog from "./ConfirmDialog";
@ -9,6 +10,7 @@ export const activeConfirmDialogAtom = atom<"clearCanvas" | null>(null);
export const ActiveConfirmDialog = () => {
const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
activeConfirmDialogAtom,
jotaiScope,
);
const actionManager = useExcalidrawActionManager();

View File

@ -0,0 +1,45 @@
import ReactDOM from "react-dom";
import * as Renderer from "../renderer/renderScene";
import { reseed } from "../random";
import { render, queryByTestId } from "../tests/test-utils";
import ExcalidrawApp from "../excalidraw-app";
const renderScene = jest.spyOn(Renderer, "renderScene");
describe("Test <App/>", () => {
beforeEach(async () => {
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
localStorage.clear();
renderScene.mockClear();
reseed(7);
});
it("should show error modal when using brave and measureText API is not working", async () => {
(global.navigator as any).brave = {
isBrave: {
name: "isBrave",
},
};
const originalContext = global.HTMLCanvasElement.prototype.getContext("2d");
//@ts-ignore
global.HTMLCanvasElement.prototype.getContext = (contextId) => {
return {
...originalContext,
measureText: () => ({
width: 0,
}),
};
};
await render(<ExcalidrawApp />);
expect(
queryByTestId(
document.querySelector(".excalidraw-modal-container")!,
"brave-measure-text-error",
),
).toMatchSnapshot();
});
});

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
import "./Avatar.scss";
import React, { useState } from "react";
import { getClientInitials } from "../clients";
import { getNameInitial } from "../clients";
type AvatarProps = {
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
@ -12,7 +12,7 @@ type AvatarProps = {
};
export const Avatar = ({ color, onClick, name, src }: AvatarProps) => {
const shortName = getClientInitials(name);
const shortName = getNameInitial(name);
const [error, setError] = useState(false);
const loadImg = !error && src;
const style = loadImg ? undefined : { background: color };

View File

@ -0,0 +1,43 @@
import Trans from "./Trans";
const BraveMeasureTextError = () => {
return (
<div data-testid="brave-measure-text-error">
<p>
<Trans
i18nKey="errors.brave_measure_text_error.line1"
bold={(el) => <span style={{ fontWeight: 600 }}>{el}</span>}
/>
</p>
<p>
<Trans
i18nKey="errors.brave_measure_text_error.line2"
bold={(el) => <span style={{ fontWeight: 600 }}>{el}</span>}
/>
</p>
<p>
<Trans
i18nKey="errors.brave_measure_text_error.line3"
link={(el) => (
<a href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser">
{el}
</a>
)}
/>
</p>
<p>
<Trans
i18nKey="errors.brave_measure_text_error.line4"
issueLink={(el) => (
<a href="https://github.com/excalidraw/excalidraw/issues/new">
{el}
</a>
)}
discordLink={(el) => <a href="https://discord.gg/UexuTaE">{el}.</a>}
/>
</p>
</div>
);
};
export default BraveMeasureTextError;

View File

@ -1,8 +1,12 @@
import clsx from "clsx";
import { composeEventHandlers } from "../utils";
import "./Button.scss";
interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
type?: "button" | "submit" | "reset";
onSelect: () => any;
/** whether button is in active state */
selected?: boolean;
children: React.ReactNode;
className?: string;
}
@ -15,18 +19,18 @@ interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
export const Button = ({
type = "button",
onSelect,
selected,
children,
className = "",
...rest
}: ButtonProps) => {
return (
<button
onClick={(event) => {
onClick={composeEventHandlers(rest.onClick, (event) => {
onSelect();
rest.onClick?.(event);
}}
})}
type={type}
className={`excalidraw-button ${className}`}
className={clsx("excalidraw-button", className, { selected })}
{...rest}
>
{children}

View File

@ -1,33 +1,59 @@
import clsx from "clsx";
// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />
export const ButtonIconSelect = <T extends Object>({
options,
value,
onChange,
group,
}: {
options: { value: T; text: string; icon: JSX.Element; testId?: string }[];
value: T | null;
onChange: (value: T) => void;
group: string;
}) => (
export const ButtonIconSelect = <T extends Object>(
props: {
options: {
value: T;
text: string;
icon: JSX.Element;
testId?: string;
/** if not supplied, defaults to value identity check */
active?: boolean;
}[];
value: T | null;
type?: "radio" | "button";
} & (
| { type?: "radio"; group: string; onChange: (value: T) => void }
| {
type: "button";
onClick: (
value: T,
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
) => void;
}
),
) => (
<div className="buttonList buttonListIcon">
{options.map((option) => (
<label
key={option.text}
className={clsx({ active: value === option.value })}
title={option.text}
>
<input
type="radio"
name={group}
onChange={() => onChange(option.value)}
checked={value === option.value}
{props.options.map((option) =>
props.type === "button" ? (
<button
key={option.text}
onClick={(event) => props.onClick(option.value, event)}
className={clsx({
active: option.active ?? props.value === option.value,
})}
data-testid={option.testId}
/>
{option.icon}
</label>
))}
title={option.text}
>
{option.icon}
</button>
) : (
<label
key={option.text}
className={clsx({ active: props.value === option.value })}
title={option.text}
>
<input
type="radio"
name={props.group}
onChange={() => props.onChange(option.value)}
checked={props.value === option.value}
data-testid={option.testId}
/>
{option.icon}
</label>
),
)}
</div>
);

View File

@ -1,430 +0,0 @@
import React from "react";
import { Popover } from "./Popover";
import { isTransparent } from "../utils";
import "./ColorPicker.scss";
import { isArrowKey, KEYS } from "../keys";
import { t, getLanguage } from "../i18n";
import { isWritableElement } from "../utils";
import colors from "../colors";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
const MAX_CUSTOM_COLORS = 5;
const MAX_DEFAULT_COLORS = 15;
export const getCustomColors = (
elements: readonly ExcalidrawElement[],
type: "elementBackground" | "elementStroke",
) => {
const customColors: string[] = [];
const updatedElements = elements
.filter((element) => !element.isDeleted)
.sort((ele1, ele2) => ele2.updated - ele1.updated);
let index = 0;
const elementColorTypeMap = {
elementBackground: "backgroundColor",
elementStroke: "strokeColor",
};
const colorType = elementColorTypeMap[type] as
| "backgroundColor"
| "strokeColor";
while (
index < updatedElements.length &&
customColors.length < MAX_CUSTOM_COLORS
) {
const element = updatedElements[index];
if (
customColors.length < MAX_CUSTOM_COLORS &&
isCustomColor(element[colorType], type) &&
!customColors.includes(element[colorType])
) {
customColors.push(element[colorType]);
}
index++;
}
return customColors;
};
const isCustomColor = (
color: string,
type: "elementBackground" | "elementStroke",
) => {
return !colors[type].includes(color);
};
const isValidColor = (color: string) => {
const style = new Option().style;
style.color = color;
return !!style.color;
};
const getColor = (color: string): string | null => {
if (isTransparent(color)) {
return color;
}
// testing for `#` first fixes a bug on Electron (more specfically, an
// Obsidian popout window), where a hex color without `#` is (incorrectly)
// considered valid
return isValidColor(`#${color}`)
? `#${color}`
: isValidColor(color)
? color
: null;
};
// This is a narrow reimplementation of the awesome react-color Twitter component
// https://github.com/casesandberg/react-color/blob/master/src/components/twitter/Twitter.js
// Unfortunately, we can't detect keyboard layout in the browser. So this will
// only work well for QWERTY but not AZERTY or others...
const keyBindings = [
["1", "2", "3", "4", "5"],
["q", "w", "e", "r", "t"],
["a", "s", "d", "f", "g"],
["z", "x", "c", "v", "b"],
].flat();
const Picker = ({
colors,
color,
onChange,
onClose,
label,
showInput = true,
type,
elements,
}: {
colors: string[];
color: string | null;
onChange: (color: string) => void;
onClose: () => void;
label: string;
showInput: boolean;
type: "canvasBackground" | "elementBackground" | "elementStroke";
elements: readonly ExcalidrawElement[];
}) => {
const firstItem = React.useRef<HTMLButtonElement>();
const activeItem = React.useRef<HTMLButtonElement>();
const gallery = React.useRef<HTMLDivElement>();
const colorInput = React.useRef<HTMLInputElement>();
const [customColors] = React.useState(() => {
if (type === "canvasBackground") {
return [];
}
return getCustomColors(elements, type);
});
React.useEffect(() => {
// After the component is first mounted focus on first input
if (activeItem.current) {
activeItem.current.focus();
} else if (colorInput.current) {
colorInput.current.focus();
} else if (gallery.current) {
gallery.current.focus();
}
}, []);
const handleKeyDown = (event: React.KeyboardEvent) => {
let handled = false;
if (isArrowKey(event.key)) {
handled = true;
const { activeElement } = document;
const isRTL = getLanguage().rtl;
let isCustom = false;
let index = Array.prototype.indexOf.call(
gallery.current!.querySelector(".color-picker-content--default")
?.children,
activeElement,
);
if (index === -1) {
index = Array.prototype.indexOf.call(
gallery.current!.querySelector(".color-picker-content--canvas-colors")
?.children,
activeElement,
);
if (index !== -1) {
isCustom = true;
}
}
const parentElement = isCustom
? gallery.current?.querySelector(".color-picker-content--canvas-colors")
: gallery.current?.querySelector(".color-picker-content--default");
if (parentElement && index !== -1) {
const length = parentElement.children.length - (showInput ? 1 : 0);
const nextIndex =
event.key === (isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT)
? (index + 1) % length
: event.key === (isRTL ? KEYS.ARROW_RIGHT : KEYS.ARROW_LEFT)
? (length + index - 1) % length
: !isCustom && event.key === KEYS.ARROW_DOWN
? (index + 5) % length
: !isCustom && event.key === KEYS.ARROW_UP
? (length + index - 5) % length
: index;
(parentElement.children[nextIndex] as HTMLElement | undefined)?.focus();
}
event.preventDefault();
} else if (
keyBindings.includes(event.key.toLowerCase()) &&
!event[KEYS.CTRL_OR_CMD] &&
!event.altKey &&
!isWritableElement(event.target)
) {
handled = true;
const index = keyBindings.indexOf(event.key.toLowerCase());
const isCustom = index >= MAX_DEFAULT_COLORS;
const parentElement = isCustom
? gallery?.current?.querySelector(
".color-picker-content--canvas-colors",
)
: gallery?.current?.querySelector(".color-picker-content--default");
const actualIndex = isCustom ? index - MAX_DEFAULT_COLORS : index;
(
parentElement?.children[actualIndex] as HTMLElement | undefined
)?.focus();
event.preventDefault();
} else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
handled = true;
event.preventDefault();
onClose();
}
if (handled) {
event.nativeEvent.stopImmediatePropagation();
event.stopPropagation();
}
};
const renderColors = (colors: Array<string>, custom: boolean = false) => {
return colors.map((_color, i) => {
const _colorWithoutHash = _color.replace("#", "");
const keyBinding = custom
? keyBindings[i + MAX_DEFAULT_COLORS]
: keyBindings[i];
const label = custom
? _colorWithoutHash
: t(`colors.${_colorWithoutHash}`);
return (
<button
className="color-picker-swatch"
onClick={(event) => {
(event.currentTarget as HTMLButtonElement).focus();
onChange(_color);
}}
title={`${label}${
!isTransparent(_color) ? ` (${_color})` : ""
}${keyBinding.toUpperCase()}`}
aria-label={label}
aria-keyshortcuts={keyBindings[i]}
style={{ color: _color }}
key={_color}
ref={(el) => {
if (!custom && el && i === 0) {
firstItem.current = el;
}
if (el && _color === color) {
activeItem.current = el;
}
}}
onFocus={() => {
onChange(_color);
}}
>
{isTransparent(_color) ? (
<div className="color-picker-transparent"></div>
) : undefined}
<span className="color-picker-keybinding">{keyBinding}</span>
</button>
);
});
};
return (
<div
className={`color-picker color-picker-type-${type}`}
role="dialog"
aria-modal="true"
aria-label={t("labels.colorPicker")}
onKeyDown={handleKeyDown}
>
<div className="color-picker-triangle color-picker-triangle-shadow"></div>
<div className="color-picker-triangle"></div>
<div
className="color-picker-content"
ref={(el) => {
if (el) {
gallery.current = el;
}
}}
// to allow focusing by clicking but not by tabbing
tabIndex={-1}
>
<div className="color-picker-content--default">
{renderColors(colors)}
</div>
{!!customColors.length && (
<div className="color-picker-content--canvas">
<span className="color-picker-content--canvas-title">
{t("labels.canvasColors")}
</span>
<div className="color-picker-content--canvas-colors">
{renderColors(customColors, true)}
</div>
</div>
)}
{showInput && (
<ColorInput
color={color}
label={label}
onChange={(color) => {
onChange(color);
}}
ref={colorInput}
/>
)}
</div>
</div>
);
};
const ColorInput = React.forwardRef(
(
{
color,
onChange,
label,
}: {
color: string | null;
onChange: (color: string) => void;
label: string;
},
ref,
) => {
const [innerValue, setInnerValue] = React.useState(color);
const inputRef = React.useRef(null);
React.useEffect(() => {
setInnerValue(color);
}, [color]);
React.useImperativeHandle(ref, () => inputRef.current);
const changeColor = React.useCallback(
(inputValue: string) => {
const value = inputValue.toLowerCase();
const color = getColor(value);
if (color) {
onChange(color);
}
setInnerValue(value);
},
[onChange],
);
return (
<label className="color-input-container">
<div className="color-picker-hash">#</div>
<input
spellCheck={false}
className="color-picker-input"
aria-label={label}
onChange={(event) => changeColor(event.target.value)}
value={(innerValue || "").replace(/^#/, "")}
onBlur={() => setInnerValue(color)}
ref={inputRef}
/>
</label>
);
},
);
ColorInput.displayName = "ColorInput";
export const ColorPicker = ({
type,
color,
onChange,
label,
isActive,
setActive,
elements,
appState,
}: {
type: "canvasBackground" | "elementBackground" | "elementStroke";
color: string | null;
onChange: (color: string) => void;
label: string;
isActive: boolean;
setActive: (active: boolean) => void;
elements: readonly ExcalidrawElement[];
appState: AppState;
}) => {
const pickerButton = React.useRef<HTMLButtonElement>(null);
const coords = pickerButton.current?.getBoundingClientRect();
return (
<div>
<div className="color-picker-control-container">
<div className="color-picker-label-swatch-container">
<button
className="color-picker-label-swatch"
aria-label={label}
style={color ? { "--swatch-color": color } : undefined}
onClick={() => setActive(!isActive)}
ref={pickerButton}
/>
</div>
<ColorInput
color={color}
label={label}
onChange={(color) => {
onChange(color);
}}
/>
</div>
<React.Suspense fallback="">
{isActive ? (
<div
className="color-picker-popover-container"
style={{
position: "fixed",
top: coords?.top,
left: coords?.right,
zIndex: 1,
}}
>
<Popover
onCloseRequest={(event) =>
event.target !== pickerButton.current && setActive(false)
}
>
<Picker
colors={colors[type]}
color={color || null}
onChange={(changedColor) => {
onChange(changedColor);
}}
onClose={() => {
setActive(false);
pickerButton.current?.focus();
}}
label={label}
showInput={false}
type={type}
elements={elements}
/>
</Popover>
</div>
) : null}
</React.Suspense>
</div>
);
};

View File

@ -0,0 +1,126 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { getColor } from "./ColorPicker";
import { useAtom } from "jotai";
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
import { eyeDropperIcon } from "../icons";
import { jotaiScope } from "../../jotai";
import { KEYS } from "../../keys";
import { activeEyeDropperAtom } from "../EyeDropper";
import clsx from "clsx";
import { t } from "../../i18n";
import { useDevice } from "../App";
import { getShortcutKey } from "../../utils";
interface ColorInputProps {
color: string;
onChange: (color: string) => void;
label: string;
}
export const ColorInput = ({ color, onChange, label }: ColorInputProps) => {
const device = useDevice();
const [innerValue, setInnerValue] = useState(color);
const [activeSection, setActiveColorPickerSection] = useAtom(
activeColorPickerSectionAtom,
);
useEffect(() => {
setInnerValue(color);
}, [color]);
const changeColor = useCallback(
(inputValue: string) => {
const value = inputValue.toLowerCase();
const color = getColor(value);
if (color) {
onChange(color);
}
setInnerValue(value);
},
[onChange],
);
const inputRef = useRef<HTMLInputElement>(null);
const eyeDropperTriggerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, [activeSection]);
const [eyeDropperState, setEyeDropperState] = useAtom(
activeEyeDropperAtom,
jotaiScope,
);
useEffect(() => {
return () => {
setEyeDropperState(null);
};
}, [setEyeDropperState]);
return (
<div className="color-picker__input-label">
<div className="color-picker__input-hash">#</div>
<input
ref={activeSection === "hex" ? inputRef : undefined}
style={{ border: 0, padding: 0 }}
spellCheck={false}
className="color-picker-input"
aria-label={label}
onChange={(event) => {
changeColor(event.target.value);
}}
value={(innerValue || "").replace(/^#/, "")}
onBlur={() => {
setInnerValue(color);
}}
tabIndex={-1}
onFocus={() => setActiveColorPickerSection("hex")}
onKeyDown={(event) => {
if (event.key === KEYS.TAB) {
return;
} else if (event.key === KEYS.ESCAPE) {
eyeDropperTriggerRef.current?.focus();
}
event.stopPropagation();
}}
/>
{/* TODO reenable on mobile with a better UX */}
{!device.isMobile && (
<>
<div
style={{
width: "1px",
height: "1.25rem",
backgroundColor: "var(--default-border-color)",
}}
/>
<div
ref={eyeDropperTriggerRef}
className={clsx("excalidraw-eye-dropper-trigger", {
selected: eyeDropperState,
})}
onClick={() =>
setEyeDropperState((s) =>
s
? null
: {
keepOpenOnAlt: false,
onSelect: (color) => onChange(color),
},
)
}
title={`${t(
"labels.eyeDropper",
)}${KEYS.I.toLocaleUpperCase()} or ${getShortcutKey("Alt")} `}
>
{eyeDropperIcon}
</div>
</>
)}
</div>
);
};

View File

@ -1,6 +1,134 @@
@import "../css/variables.module";
@import "../../css/variables.module";
.excalidraw {
.focus-visible-none {
&:focus-visible {
outline: none !important;
}
}
.color-picker__heading {
padding: 0 0.5rem;
font-size: 0.75rem;
text-align: left;
}
.color-picker-container {
display: grid;
grid-template-columns: 1fr 20px 1.625rem;
padding: 0.25rem 0px;
align-items: center;
@include isMobile {
max-width: 175px;
}
}
.color-picker__top-picks {
display: flex;
justify-content: space-between;
}
.color-picker__button {
--radius: 0.25rem;
padding: 0;
margin: 0;
width: 1.35rem;
height: 1.35rem;
border: 1px solid var(--color-gray-30);
border-radius: var(--radius);
filter: var(--theme-filter);
background-color: var(--swatch-color);
background-position: left center;
position: relative;
font-family: inherit;
box-sizing: border-box;
&:hover {
&::after {
content: "";
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
box-shadow: 0 0 0 1px var(--color-gray-30);
border-radius: calc(var(--radius) + 1px);
filter: var(--theme-filter);
}
}
&.active {
.color-picker__button-outline {
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
box-shadow: 0 0 0 1px var(--color-primary-darkest);
z-index: 1; // due hover state so this has preference
border-radius: calc(var(--radius) + 1px);
filter: var(--theme-filter);
}
}
&:focus-visible {
outline: none;
&::after {
content: "";
position: absolute;
top: -4px;
right: -4px;
bottom: -4px;
left: -4px;
border: 3px solid var(--focus-highlight-color);
border-radius: calc(var(--radius) + 1px);
}
&.active {
.color-picker__button-outline {
display: none;
}
}
}
&--large {
--radius: 0.5rem;
width: 1.875rem;
height: 1.875rem;
}
&.is-transparent {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==");
}
&--no-focus-visible {
border: 0;
&::after {
display: none;
}
&:focus-visible {
outline: none !important;
}
}
&.active-color {
border-radius: calc(var(--radius) + 1px);
width: 1.625rem;
height: 1.625rem;
}
}
.color-picker__button__hotkey-label {
position: absolute;
right: 4px;
bottom: 4px;
filter: none;
font-size: 11px;
}
.color-picker {
background: var(--popup-bg-color);
border: 0 solid transparentize($oc-white, 0.75);
@ -72,11 +200,18 @@
}
}
.color-picker-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
outline: none;
}
.color-picker-content--default {
padding: 0.5rem;
display: grid;
grid-template-columns: repeat(5, auto);
grid-gap: 0.5rem;
grid-template-columns: repeat(5, 1.875rem);
grid-gap: 0.25rem;
border-radius: 4px;
&:focus {
@ -178,11 +313,33 @@
}
}
.color-picker__input-label {
display: grid;
grid-template-columns: auto 1fr auto auto;
gap: 8px;
align-items: center;
border: 1px solid var(--default-border-color);
border-radius: 8px;
padding: 0 12px;
margin: 8px;
box-sizing: border-box;
&:focus-within {
box-shadow: 0 0 0 1px var(--color-primary-darkest);
border-radius: var(--border-radius-lg);
}
}
.color-picker__input-hash {
padding: 0 0.25rem;
}
.color-picker-input {
box-sizing: border-box;
width: 100%;
margin: 0;
font-size: 0.875rem;
font-family: inherit;
background-color: transparent;
color: var(--text-primary-color);
border: 0;

View File

@ -0,0 +1,290 @@
import { isTransparent, isWritableElement } from "../../utils";
import { ExcalidrawElement } from "../../element/types";
import { AppState } from "../../types";
import { TopPicks } from "./TopPicks";
import { Picker } from "./Picker";
import * as Popover from "@radix-ui/react-popover";
import { useAtom } from "jotai";
import {
activeColorPickerSectionAtom,
ColorPickerType,
} from "./colorPickerUtils";
import { useDevice, useExcalidrawContainer } from "../App";
import { ColorTuple, COLOR_PALETTE, ColorPaletteCustom } from "../../colors";
import PickerHeading from "./PickerHeading";
import { t } from "../../i18n";
import clsx from "clsx";
import { jotaiScope } from "../../jotai";
import { ColorInput } from "./ColorInput";
import { useRef } from "react";
import { activeEyeDropperAtom } from "../EyeDropper";
import "./ColorPicker.scss";
const isValidColor = (color: string) => {
const style = new Option().style;
style.color = color;
return !!style.color;
};
export const getColor = (color: string): string | null => {
if (isTransparent(color)) {
return color;
}
// testing for `#` first fixes a bug on Electron (more specfically, an
// Obsidian popout window), where a hex color without `#` is (incorrectly)
// considered valid
return isValidColor(`#${color}`)
? `#${color}`
: isValidColor(color)
? color
: null;
};
interface ColorPickerProps {
type: ColorPickerType;
color: string;
onChange: (color: string) => void;
label: string;
elements: readonly ExcalidrawElement[];
appState: AppState;
palette?: ColorPaletteCustom | null;
topPicks?: ColorTuple;
updateData: (formData?: any) => void;
}
const ColorPickerPopupContent = ({
type,
color,
onChange,
label,
elements,
palette = COLOR_PALETTE,
updateData,
}: Pick<
ColorPickerProps,
| "type"
| "color"
| "onChange"
| "label"
| "elements"
| "palette"
| "updateData"
>) => {
const [, setActiveColorPickerSection] = useAtom(activeColorPickerSectionAtom);
const [eyeDropperState, setEyeDropperState] = useAtom(
activeEyeDropperAtom,
jotaiScope,
);
const { container } = useExcalidrawContainer();
const { isMobile, isLandscape } = useDevice();
const colorInputJSX = (
<div>
<PickerHeading>{t("colorPicker.hexCode")}</PickerHeading>
<ColorInput
color={color}
label={label}
onChange={(color) => {
onChange(color);
}}
/>
</div>
);
const popoverRef = useRef<HTMLDivElement>(null);
const focusPickerContent = () => {
popoverRef.current
?.querySelector<HTMLDivElement>(".color-picker-content")
?.focus();
};
return (
<Popover.Portal container={container}>
<Popover.Content
ref={popoverRef}
className="focus-visible-none"
data-prevent-outside-click
onFocusOutside={(event) => {
focusPickerContent();
event.preventDefault();
}}
onPointerDownOutside={(event) => {
if (eyeDropperState) {
// prevent from closing if we click outside the popover
// while eyedropping (e.g. click when clicking the sidebar;
// the eye-dropper-backdrop is prevented downstream)
event.preventDefault();
}
}}
onCloseAutoFocus={(e) => {
e.preventDefault();
e.stopPropagation();
// return focus to excalidraw container
if (container) {
container.focus();
}
updateData({ openPopup: null });
setActiveColorPickerSection(null);
}}
side={isMobile && !isLandscape ? "bottom" : "right"}
align={isMobile && !isLandscape ? "center" : "start"}
alignOffset={-16}
sideOffset={20}
style={{
zIndex: 9999,
backgroundColor: "var(--popup-bg-color)",
maxWidth: "208px",
maxHeight: window.innerHeight,
padding: "12px",
borderRadius: "8px",
boxSizing: "border-box",
overflowY: "auto",
boxShadow:
"0px 7px 14px rgba(0, 0, 0, 0.05), 0px 0px 3.12708px rgba(0, 0, 0, 0.0798), 0px 0px 0.931014px rgba(0, 0, 0, 0.1702)",
}}
>
{palette ? (
<Picker
palette={palette}
color={color}
onChange={(changedColor) => {
onChange(changedColor);
}}
onEyeDropperToggle={(force) => {
setEyeDropperState((state) => {
if (force) {
state = state || {
keepOpenOnAlt: true,
onSelect: onChange,
};
state.keepOpenOnAlt = true;
return state;
}
return force === false || state
? null
: {
keepOpenOnAlt: false,
onSelect: onChange,
};
});
}}
onEscape={(event) => {
if (eyeDropperState) {
setEyeDropperState(null);
} else if (isWritableElement(event.target)) {
focusPickerContent();
} else {
updateData({ openPopup: null });
}
}}
label={label}
type={type}
elements={elements}
updateData={updateData}
>
{colorInputJSX}
</Picker>
) : (
colorInputJSX
)}
<Popover.Arrow
width={20}
height={10}
style={{
fill: "var(--popup-bg-color)",
filter: "drop-shadow(rgba(0, 0, 0, 0.05) 0px 3px 2px)",
}}
/>
</Popover.Content>
</Popover.Portal>
);
};
const ColorPickerTrigger = ({
label,
color,
type,
}: {
color: string;
label: string;
type: ColorPickerType;
}) => {
return (
<Popover.Trigger
type="button"
className={clsx("color-picker__button active-color", {
"is-transparent": color === "transparent" || !color,
})}
aria-label={label}
style={color ? { "--swatch-color": color } : undefined}
title={
type === "elementStroke"
? t("labels.showStroke")
: t("labels.showBackground")
}
>
<div className="color-picker__button-outline" />
</Popover.Trigger>
);
};
export const ColorPicker = ({
type,
color,
onChange,
label,
elements,
palette = COLOR_PALETTE,
topPicks,
updateData,
appState,
}: ColorPickerProps) => {
return (
<div>
<div role="dialog" aria-modal="true" className="color-picker-container">
<TopPicks
activeColor={color}
onChange={onChange}
type={type}
topPicks={topPicks}
/>
<div
style={{
width: 1,
height: "100%",
backgroundColor: "var(--default-border-color)",
margin: "0 auto",
}}
/>
<Popover.Root
open={appState.openPopup === type}
onOpenChange={(open) => {
updateData({ openPopup: open ? type : null });
}}
>
{/* serves as an active color indicator as well */}
<ColorPickerTrigger color={color} label={label} type={type} />
{/* popup content */}
{appState.openPopup === type && (
<ColorPickerPopupContent
type={type}
color={color}
onChange={onChange}
label={label}
elements={elements}
palette={palette}
updateData={updateData}
/>
)}
</Popover.Root>
</div>
</div>
);
};

View File

@ -0,0 +1,63 @@
import clsx from "clsx";
import { useAtom } from "jotai";
import { useEffect, useRef } from "react";
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
import HotkeyLabel from "./HotkeyLabel";
interface CustomColorListProps {
colors: string[];
color: string;
onChange: (color: string) => void;
label: string;
}
export const CustomColorList = ({
colors,
color,
onChange,
label,
}: CustomColorListProps) => {
const [activeColorPickerSection, setActiveColorPickerSection] = useAtom(
activeColorPickerSectionAtom,
);
const btnRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (btnRef.current) {
btnRef.current.focus();
}
}, [color, activeColorPickerSection]);
return (
<div className="color-picker-content--default">
{colors.map((c, i) => {
return (
<button
ref={color === c ? btnRef : undefined}
tabIndex={-1}
type="button"
className={clsx(
"color-picker__button color-picker__button--large",
{
active: color === c,
"is-transparent": c === "transparent" || !c,
},
)}
onClick={() => {
onChange(c);
setActiveColorPickerSection("custom");
}}
title={c}
aria-label={label}
style={{ "--swatch-color": c }}
key={i}
>
<div className="color-picker__button-outline" />
<HotkeyLabel color={c} keyLabel={i + 1} isCustomColor />
</button>
);
})}
</div>
);
};

View File

@ -0,0 +1,29 @@
import React from "react";
import { getContrastYIQ } from "./colorPickerUtils";
interface HotkeyLabelProps {
color: string;
keyLabel: string | number;
isCustomColor?: boolean;
isShade?: boolean;
}
const HotkeyLabel = ({
color,
keyLabel,
isCustomColor = false,
isShade = false,
}: HotkeyLabelProps) => {
return (
<div
className="color-picker__button__hotkey-label"
style={{
color: getContrastYIQ(color, isCustomColor),
}}
>
{isShade && "⇧"}
{keyLabel}
</div>
);
};
export default HotkeyLabel;

View File

@ -0,0 +1,178 @@
import React, { useEffect, useState } from "react";
import { t } from "../../i18n";
import { ExcalidrawElement } from "../../element/types";
import { ShadeList } from "./ShadeList";
import PickerColorList from "./PickerColorList";
import { useAtom } from "jotai";
import { CustomColorList } from "./CustomColorList";
import { colorPickerKeyNavHandler } from "./keyboardNavHandlers";
import PickerHeading from "./PickerHeading";
import {
ColorPickerType,
activeColorPickerSectionAtom,
getColorNameAndShadeFromColor,
getMostUsedCustomColors,
isCustomColor,
} from "./colorPickerUtils";
import {
ColorPaletteCustom,
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
} from "../../colors";
import { KEYS } from "../../keys";
import { EVENT } from "../../constants";
interface PickerProps {
color: string;
onChange: (color: string) => void;
label: string;
type: ColorPickerType;
elements: readonly ExcalidrawElement[];
palette: ColorPaletteCustom;
updateData: (formData?: any) => void;
children?: React.ReactNode;
onEyeDropperToggle: (force?: boolean) => void;
onEscape: (event: React.KeyboardEvent | KeyboardEvent) => void;
}
export const Picker = ({
color,
onChange,
label,
type,
elements,
palette,
updateData,
children,
onEyeDropperToggle,
onEscape,
}: PickerProps) => {
const [customColors] = React.useState(() => {
if (type === "canvasBackground") {
return [];
}
return getMostUsedCustomColors(elements, type, palette);
});
const [activeColorPickerSection, setActiveColorPickerSection] = useAtom(
activeColorPickerSectionAtom,
);
const colorObj = getColorNameAndShadeFromColor({
color,
palette,
});
useEffect(() => {
if (!activeColorPickerSection) {
const isCustom = isCustomColor({ color, palette });
const isCustomButNotInList = isCustom && !customColors.includes(color);
setActiveColorPickerSection(
isCustomButNotInList
? "hex"
: isCustom
? "custom"
: colorObj?.shade != null
? "shades"
: "baseColors",
);
}
}, [
activeColorPickerSection,
color,
palette,
setActiveColorPickerSection,
colorObj,
customColors,
]);
const [activeShade, setActiveShade] = useState(
colorObj?.shade ??
(type === "elementBackground"
? DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX
: DEFAULT_ELEMENT_STROKE_COLOR_INDEX),
);
useEffect(() => {
if (colorObj?.shade != null) {
setActiveShade(colorObj.shade);
}
const keyup = (event: KeyboardEvent) => {
if (event.key === KEYS.ALT) {
onEyeDropperToggle(false);
}
};
document.addEventListener(EVENT.KEYUP, keyup, { capture: true });
return () => {
document.removeEventListener(EVENT.KEYUP, keyup, { capture: true });
};
}, [colorObj, onEyeDropperToggle]);
const pickerRef = React.useRef<HTMLDivElement>(null);
return (
<div role="dialog" aria-modal="true" aria-label={t("labels.colorPicker")}>
<div
ref={pickerRef}
onKeyDown={(event) => {
const handled = colorPickerKeyNavHandler({
event,
activeColorPickerSection,
palette,
color,
onChange,
onEyeDropperToggle,
customColors,
setActiveColorPickerSection,
updateData,
activeShade,
onEscape,
});
if (handled) {
event.preventDefault();
event.stopPropagation();
}
}}
className="color-picker-content"
// to allow focusing by clicking but not by tabbing
tabIndex={-1}
>
{!!customColors.length && (
<div>
<PickerHeading>
{t("colorPicker.mostUsedCustomColors")}
</PickerHeading>
<CustomColorList
colors={customColors}
color={color}
label={t("colorPicker.mostUsedCustomColors")}
onChange={onChange}
/>
</div>
)}
<div>
<PickerHeading>{t("colorPicker.colors")}</PickerHeading>
<PickerColorList
color={color}
label={label}
palette={palette}
onChange={onChange}
activeShade={activeShade}
/>
</div>
<div>
<PickerHeading>{t("colorPicker.shades")}</PickerHeading>
<ShadeList hex={color} onChange={onChange} palette={palette} />
</div>
{children}
</div>
</div>
);
};

View File

@ -0,0 +1,86 @@
import clsx from "clsx";
import { useAtom } from "jotai";
import { useEffect, useRef } from "react";
import {
activeColorPickerSectionAtom,
colorPickerHotkeyBindings,
getColorNameAndShadeFromColor,
} from "./colorPickerUtils";
import HotkeyLabel from "./HotkeyLabel";
import { ColorPaletteCustom } from "../../colors";
import { t } from "../../i18n";
interface PickerColorListProps {
palette: ColorPaletteCustom;
color: string;
onChange: (color: string) => void;
label: string;
activeShade: number;
}
const PickerColorList = ({
palette,
color,
onChange,
label,
activeShade,
}: PickerColorListProps) => {
const colorObj = getColorNameAndShadeFromColor({
color: color || "transparent",
palette,
});
const [activeColorPickerSection, setActiveColorPickerSection] = useAtom(
activeColorPickerSectionAtom,
);
const btnRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (btnRef.current && activeColorPickerSection === "baseColors") {
btnRef.current.focus();
}
}, [colorObj?.colorName, activeColorPickerSection]);
return (
<div className="color-picker-content--default">
{Object.entries(palette).map(([key, value], index) => {
const color =
(Array.isArray(value) ? value[activeShade] : value) || "transparent";
const keybinding = colorPickerHotkeyBindings[index];
const label = t(`colors.${key.replace(/\d+/, "")}`, null, "");
return (
<button
ref={colorObj?.colorName === key ? btnRef : undefined}
tabIndex={-1}
type="button"
className={clsx(
"color-picker__button color-picker__button--large",
{
active: colorObj?.colorName === key,
"is-transparent": color === "transparent" || !color,
},
)}
onClick={() => {
onChange(color);
setActiveColorPickerSection("baseColors");
}}
title={`${label}${
color.startsWith("#") ? ` ${color}` : ""
}${keybinding}`}
aria-label={`${label}${keybinding}`}
style={color ? { "--swatch-color": color } : undefined}
data-testid={`color-${key}`}
key={key}
>
<div className="color-picker__button-outline" />
<HotkeyLabel color={color} keyLabel={keybinding} />
</button>
);
})}
</div>
);
};
export default PickerColorList;

View File

@ -0,0 +1,7 @@
import { ReactNode } from "react";
const PickerHeading = ({ children }: { children: ReactNode }) => (
<div className="color-picker__heading">{children}</div>
);
export default PickerHeading;

View File

@ -0,0 +1,105 @@
import clsx from "clsx";
import { useAtom } from "jotai";
import { useEffect, useRef } from "react";
import {
activeColorPickerSectionAtom,
getColorNameAndShadeFromColor,
} from "./colorPickerUtils";
import HotkeyLabel from "./HotkeyLabel";
import { t } from "../../i18n";
import { ColorPaletteCustom } from "../../colors";
interface ShadeListProps {
hex: string;
onChange: (color: string) => void;
palette: ColorPaletteCustom;
}
export const ShadeList = ({ hex, onChange, palette }: ShadeListProps) => {
const colorObj = getColorNameAndShadeFromColor({
color: hex || "transparent",
palette,
});
const [activeColorPickerSection, setActiveColorPickerSection] = useAtom(
activeColorPickerSectionAtom,
);
const btnRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (btnRef.current && activeColorPickerSection === "shades") {
btnRef.current.focus();
}
}, [colorObj, activeColorPickerSection]);
if (colorObj) {
const { colorName, shade } = colorObj;
const shades = palette[colorName];
if (Array.isArray(shades)) {
return (
<div className="color-picker-content--default shades">
{shades.map((color, i) => (
<button
ref={
i === shade && activeColorPickerSection === "shades"
? btnRef
: undefined
}
tabIndex={-1}
key={i}
type="button"
className={clsx(
"color-picker__button color-picker__button--large",
{ active: i === shade },
)}
aria-label="Shade"
title={`${colorName} - ${i + 1}`}
style={color ? { "--swatch-color": color } : undefined}
onClick={() => {
onChange(color);
setActiveColorPickerSection("shades");
}}
>
<div className="color-picker__button-outline" />
<HotkeyLabel color={color} keyLabel={i + 1} isShade />
</button>
))}
</div>
);
}
}
return (
<div
className="color-picker-content--default"
style={{ position: "relative" }}
tabIndex={-1}
>
<button
type="button"
tabIndex={-1}
className="color-picker__button color-picker__button--large color-picker__button--no-focus-visible"
/>
<div
tabIndex={-1}
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
textAlign: "center",
fontSize: "0.75rem",
}}
>
{t("colorPicker.noShades")}
</div>
</div>
);
};

View File

@ -0,0 +1,64 @@
import clsx from "clsx";
import { ColorPickerType } from "./colorPickerUtils";
import {
DEFAULT_CANVAS_BACKGROUND_PICKS,
DEFAULT_ELEMENT_BACKGROUND_PICKS,
DEFAULT_ELEMENT_STROKE_PICKS,
} from "../../colors";
interface TopPicksProps {
onChange: (color: string) => void;
type: ColorPickerType;
activeColor: string;
topPicks?: readonly string[];
}
export const TopPicks = ({
onChange,
type,
activeColor,
topPicks,
}: TopPicksProps) => {
let colors;
if (type === "elementStroke") {
colors = DEFAULT_ELEMENT_STROKE_PICKS;
}
if (type === "elementBackground") {
colors = DEFAULT_ELEMENT_BACKGROUND_PICKS;
}
if (type === "canvasBackground") {
colors = DEFAULT_CANVAS_BACKGROUND_PICKS;
}
// this one can overwrite defaults
if (topPicks) {
colors = topPicks;
}
if (!colors) {
console.error("Invalid type for TopPicks");
return null;
}
return (
<div className="color-picker__top-picks">
{colors.map((color: string) => (
<button
className={clsx("color-picker__button", {
active: color === activeColor,
"is-transparent": color === "transparent" || !color,
})}
style={{ "--swatch-color": color }}
key={color}
type="button"
title={color}
onClick={() => onChange(color)}
>
<div className="color-picker__button-outline" />
</button>
))}
</div>
);
};

View File

@ -0,0 +1,136 @@
import { ExcalidrawElement } from "../../element/types";
import { atom } from "jotai";
import {
ColorPickerColor,
ColorPaletteCustom,
MAX_CUSTOM_COLORS_USED_IN_CANVAS,
} from "../../colors";
export const getColorNameAndShadeFromColor = ({
palette,
color,
}: {
palette: ColorPaletteCustom;
color: string;
}): {
colorName: ColorPickerColor;
shade: number | null;
} | null => {
for (const [colorName, colorVal] of Object.entries(palette)) {
if (Array.isArray(colorVal)) {
const shade = colorVal.indexOf(color);
if (shade > -1) {
return { colorName: colorName as ColorPickerColor, shade };
}
} else if (colorVal === color) {
return { colorName: colorName as ColorPickerColor, shade: null };
}
}
return null;
};
export const colorPickerHotkeyBindings = [
["q", "w", "e", "r", "t"],
["a", "s", "d", "f", "g"],
["z", "x", "c", "v", "b"],
].flat();
export const isCustomColor = ({
color,
palette,
}: {
color: string;
palette: ColorPaletteCustom;
}) => {
const paletteValues = Object.values(palette).flat();
return !paletteValues.includes(color);
};
export const getMostUsedCustomColors = (
elements: readonly ExcalidrawElement[],
type: "elementBackground" | "elementStroke",
palette: ColorPaletteCustom,
) => {
const elementColorTypeMap = {
elementBackground: "backgroundColor",
elementStroke: "strokeColor",
};
const colors = elements.filter((element) => {
if (element.isDeleted) {
return false;
}
const color =
element[elementColorTypeMap[type] as "backgroundColor" | "strokeColor"];
return isCustomColor({ color, palette });
});
const colorCountMap = new Map<string, number>();
colors.forEach((element) => {
const color =
element[elementColorTypeMap[type] as "backgroundColor" | "strokeColor"];
if (colorCountMap.has(color)) {
colorCountMap.set(color, colorCountMap.get(color)! + 1);
} else {
colorCountMap.set(color, 1);
}
});
return [...colorCountMap.entries()]
.sort((a, b) => b[1] - a[1])
.map((c) => c[0])
.slice(0, MAX_CUSTOM_COLORS_USED_IN_CANVAS);
};
export type ActiveColorPickerSectionAtomType =
| "custom"
| "baseColors"
| "shades"
| "hex"
| null;
export const activeColorPickerSectionAtom =
atom<ActiveColorPickerSectionAtomType>(null);
const calculateContrast = (r: number, g: number, b: number) => {
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
return yiq >= 160 ? "black" : "white";
};
// inspiration from https://stackoverflow.com/a/11868398
export const getContrastYIQ = (bgHex: string, isCustomColor: boolean) => {
if (isCustomColor) {
const style = new Option().style;
style.color = bgHex;
if (style.color) {
const rgb = style.color
.replace(/^(rgb|rgba)\(/, "")
.replace(/\)$/, "")
.replace(/\s/g, "")
.split(",");
const r = parseInt(rgb[0]);
const g = parseInt(rgb[1]);
const b = parseInt(rgb[2]);
return calculateContrast(r, g, b);
}
}
// TODO: ? is this wanted?
if (bgHex === "transparent") {
return "black";
}
const r = parseInt(bgHex.substring(1, 3), 16);
const g = parseInt(bgHex.substring(3, 5), 16);
const b = parseInt(bgHex.substring(5, 7), 16);
return calculateContrast(r, g, b);
};
export type ColorPickerType =
| "canvasBackground"
| "elementBackground"
| "elementStroke";

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