mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
update to latest master (#6286)
* fix: fonts not rendered on init if `loadingdone` not fired (#5923) * fix: fonts not rendered on init if `loadingdone` not fired * remove unnecessary check * fix: Always bind to container selected by user (#5880) * fix: Always bind to container selected by user * Don't bind to container when using text tool * adjust z-index for bound text * fix * Add spec * Add test * Allow double click on transparent container and add spec * fix spec * adjust z-index only when binding * update index * fix * add index check * Update src/scene/Scene.ts Co-authored-by: dwelle <luzar.david@gmail.com> * feat: changed text copy/paste behaviour (#5786) Co-authored-by: dwelle <luzar.david@gmail.com> Co-authored-by: Antonio Della Fortuna <a.dellafortuna00@gmail.com> * feat: Don't add midpoint until dragged beyond a threshold (#5927) * Don't add midpoint until dragged beyond a threshold * remove unnecessary code * fix tests * fix * add spec * remove isMidpoint * cleanup * fix threshold for zoom * split into shouldAddMidpoint and addMidpoint * wrap in flushSync for synchronous updates * remove threshold for line editor and add spec * [unrelated] fix stack overflow state update * fix tests * don't drag arrow when dragging to add mid point * add specs Co-authored-by: dwelle <luzar.david@gmail.com> * refactor: remove unnecessary code (#5933) * fix: scale font correctly when using shift (#5935) * fix: scale font correctly when using shift * fix * Empty-Commit * Add spec * fix * fix: Dedupe boundElement ids when container duplicated with alt+drag (#5938) * Dedupe boundElement ids when container duplicated with alt+drag and add spec * set to null by default * fix: bindings do not survive history serialization (#5942) * fix: don't allow whitespaces for bound text (#5939) * fix: don't allow whitespaces for bound text * fix * remove * remove empty else * fix * fix * fix * feat: Support labels for arrow 🔥 (#5723) * feat: support arrow with text * render arrow -> clear rect-> render text * move bound text when linear elements move * fix centering cursor when linear element rotated * fix y coord when new line added and container has 3 points * update text position when 2nd point moved * support adding label on top of 2nd point when 3 points are present * change linear element editor shortcut to cmd+enter and fix tests * scale bound text points when resizing via bounding box * ohh yeah rotation works :) * fix coords when updating text properties * calculate new position after rotation always from original position * rotate the bound text by same angle as parent * don't rotate text and make sure dimensions and coords are always calculated from original point * hardcoding the text width for now * Move the linear element when bound text hit * Rotation working yaay * consider text element angle when editing * refactor * update x2 coords if needed when text updated * simplify * consider bound text to be part of bounding box when hit * show bounding box correctly when multiple element selected * fix typo * support rotating multiple elements * support multiple element resizing * shift bound text to mid point when odd points * Always render linear element handles inside editor after element rendered so point is visible for bound text * Delete bound text when point attached to it deleted * move bound to mid segement mid point when points are even * shift bound text when points nearby deleted and handle segment deletion * Resize working :) * more resize fixes * don't update cache-its breaking delete points, look for better soln * update mid point cache for bound elements when updated * introduce wrapping when resizing * wrap when resize for 2 pointer linear elements * support adding text for linear elements with more than 3 points * export to svg working :) * clip from nearest enclosing element with non transparent color if present when exporting and fill with correct color in canvas * fix snap * use visible elements * Make export to svg work with Mask :) * remove id * mask canvas linear element area where label is added * decide the position of bound text during render * fix coords when editing * fix multiple resize * update cache when bound text version changes * fix masking when rotated * render text in correct position in preview * remove unnecessary code * fix masking when rotating linear element * fix masking with zoom * fix mask in preview for export * fix offsets in export view * fix coords on svg export * fix mask when element rotated in svg * enable double-click to enter text * fix hint * Position cursor correctly and text dimensiosn when height of element is negative * don't allow 2 pointer linear element with bound text width to go beyond min width * code cleanup * fix freedraw * Add padding * don't show vertical align action for linear element containers * Add specs for getBoundTextElementPosition * more specs * move some utils to linearElementEditor.ts * remove only :p * check absoulte coods in test * Add test to hide vertical align for linear eleemnt with bound text * improve export preview * support labels only for arrows * spec * fix large texts * fix tests * fix zooming * enter line editor with cmd+double click * Allow points to move beyond min width/height for 2 pointer arrow with bound text * fix hint for line editing * attempt to fix arrow getting deselected * fix hint and shortcut * Add padding of 5px when creating bound text and add spec * Wrap bound text when arrow binding containers moved * Add spec * remove * set boundTextElementVersion to null if not present * dont use cache when version mismatch * Add a padding of 5px vertically when creating text * Add box sizing content box * Set bound elements when text element created to fix the padding * fix zooming in editor * fix zoom in export * remove globalCompositeOperation and use clearRect instead of fillRect * fix: repair element bindings on restore (#5956) * fix: repair element bindings on restore * fix dropping non-text bound elements * be more conservative * build: move release scripts to use release branch (#5958) * fix: renderFooter styling (#5962) * fix: `ExcalidrawArrowElement` rather than `ExcalidrawArrowEleement` (#5955) * fix: Galego and Kurdî missing in languages plus two locale typos (#5954) * fix: remove blank space (#5950) * fix: remove editor onpaste handler (#5971) * feat: better default radius sizes for rectangles (#5553) Co-authored-by: Ryan <diweihao@bytedance.com> Co-authored-by: dwelle <luzar.david@gmail.com> * chore: add display name to context providers (#5974) * chore: add display name to context providers * fix typo * fix: apply the right type of roundness when pasting styles (#5979) * fix: only paste roundness when target and source elements are of the same type * apply roundness when pasting across different types * simplify Co-authored-by: dwelle <luzar.david@gmail.com> * feat: allow readonly actions to be used in viewMode (#5982) * fix: chart pasting not working due to removing tab characters (#5987) * fix: Avatar outline on safari & center (#5997) * fix: not properly restoring element stroke and bg colors (#6002) * fix: PWA not working after CRA@5 update (#6012) * fix: PWA not working after CRA@5 update * fix: fallback to default locale when fetch fails * fix: resize sometimes throwing on missing null-checks (#6013) * fix: showing `grabbing` cursor when holding `spacebar` (#6015) * fix: don't push whitespace to next line when exceeding max width during wrapping and make sure to use same width of text editor on DOM when measuring dimensions (#5996) * fix: don't push whitespace to next line when exceeding max width during wrapping * add a helper function and never push empty line * use width same as in text area so dimensions are same * add tests * make sure dom element has exact same width as text editor * feat: render footer as a component instead of render prop (#5970) * feat: render footer as a component instead of render prop * Export FooterCenter as footer * remove useDevice export * revert some changes * remove * add spec * update specs * parse children into a dictionary * factor app footer components into a single file * Add docs * split app footer components Co-authored-by: dwelle <luzar.david@gmail.com> * feat: move contextMenu into the component tree and control via appState (#6021) * fix: ColorPicker getColor (#5949) Co-authored-by: dwelle <luzar.david@gmail.com> * chore: bump typescript @ 4.9.4 (#6024) * feat: support shrinking text containers to original height when text removed (#6025) * fix:cache bind text containers height so that it could autoshrink to original height when text deleted * revert * rename * reset cache when resized * safe check * restore original containr height when text is unbind * update cache when redrawing bounding box * reset cache when unbind * make type-safe * add specs * skip one test * remoe mock * fix Co-authored-by: dwelle <luzar.david@gmail.com> * fix: restoring deleted bindings (#6029) * fix: restoring deleted bindings * add tests * add one more test * merge restore tests files * fix: use canvas measureText to calculate width in measureText (#6030) * fix: use canvas measureText to calculate width in measureText * calculate multiline width correctly using canvas measure text and rename functions * set correct width when pasting in bound container * take existing value + new pasted * remove debugger :p * fix snaps * fix: remove background from wysiwyg when editing arrow label (#6033) Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com> * fix: use displayName since name gets stripped off when uglifying/minifiyng in production (#6036) fix: use displayName since name gets stripped off when uglifying/minifiy in production * feat: Scroll using PageUp and PageDown (#6038) * feat: Scroll using PageUp and PageDown * support x-axis via `shift` & enable in viewMode * tweak test Co-authored-by: dwelle <luzar.david@gmail.com> * chore: Update translations from Crowdin (#5807) Co-authored-by: David Luzar <luzar.david@gmail.com> * fix: remove ga from docker build (#6059) * fix: remove ga from docker build * lint * fix debug * fix: show error message on collab save failure (#6063) * fix: show error message on collab save failure * comment * feat: new Menu Component API (#6034) * feat: new Menu Component API * allow valid children types * introduce menu group to group items * Add lang footer * use display name * displayName * define types inside * fix default menu * add json export to menu * fix * simplify expression * put open menu into own compo to optimize perf So that we don't rerun `useOutsideClickHook` (and rebind event listeners all the time) * naming tweaks * rename MenuComponents->MenuDefaultItems and export default items from Menu.Items * import Menu.scss in Menu.tsx * move menu scss to excal app * Don't filter children inside menu group * move E+ out of socials * support style prop for MenuItem and MenuGroup * Support header in menu group and add Excalidraw links header for default items in social section * rename header to title * fix padding for lang * render menu in mobile * review fixes * tweaks * Export collaborators and show in mobile menu * revert .env * lint :p * again lint * show correct actions in view mode for mobile * Whitelist Collaborators Comp * mobile styling * padding * don't show nerds when menu open in mobile * lint :( * hide shortcuts * refactor userlist to support mobile and keep a wrapper comp for excal app * use only UserList * render only on mobile for default items * remove unused hooks * Show collab button in menu when onCollabButtonClick present and hide export when UIOptions.canvasActions.export is false * fix tests * lint * inject userlist inside menu on mobile * revert userlist * move menu socials to default menu * fix collab * use meny in library * Make Menu generic and create hamburgemenu for public excal menu and use menu in library as well * use appState.openMenu for mobile * fix tests * styling fixes and support style and class name in menu content * fix test * rename MenuDefaultItems->DefaultItems * move footer css to its own comp * rename HamburgerMenu -> MainMenu * rename menu -> dropdownMenu and update classes, onClick->onToggle * close main menu when dialog closes * by bye filtering * update docs * fix lint * update example, docs for useDevice and footer in mobile, rename menu ->DropDownMenu everywhere * spec * remove isMenuOpenAtom and set openMenu as canvas for main menu, render decreases in specs :) * [temp] remove cyclic depenedency to fix build * hack- update appstate to sync lang change * Add more specs * wip: rewrite MainMenu footer * fix margin * fix snaps * not needed as lang list no more imported * simplify custom footer rendering * Add DropdownMenuItemLink and DropdownMenuItemCustom and update API, docs * fix `MainMenu.ItemCustom` * naming * use onSelect and base class for custom items * fix lint * fix snap * use custom item for lang * update docs * fix * properly use `MainMenu.ItemCustom` for `LanguageList` * add margin top to custom items * flex Co-authored-by: dwelle <luzar.david@gmail.com> * fix: HelpDialog (#6072) * chore: Update translations from Crowdin (#6052) * New translations en.json (German) * Auto commit: Calculate translation coverage * New translations en.json (Hindi) * New translations en.json (Marathi) * New translations en.json (Hindi) * Auto commit: Calculate translation coverage * New translations en.json (Galician) * Auto commit: Calculate translation coverage * 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 (Basque) * 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 (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 (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 Simplified) * 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 (Bengali) * New translations en.json (Marathi) * 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) * Auto commit: Calculate translation coverage * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Traditional) * New translations en.json (Norwegian Bokmal) * Auto commit: Calculate translation coverage * New translations en.json (Latvian) * Auto commit: Calculate translation coverage * New translations en.json (Romanian) * Auto commit: Calculate translation coverage * New translations en.json (Slovenian) * Auto commit: Calculate translation coverage * New translations en.json (Spanish) * New translations en.json (Russian) * Auto commit: Calculate translation coverage * New translations en.json (German) * Auto commit: Calculate translation coverage * New translations en.json (Vietnamese) * Auto commit: Calculate translation coverage * New translations en.json (Hindi) * Auto commit: Calculate translation coverage * New translations en.json (Dutch) * Auto commit: Calculate translation coverage * New translations en.json (Marathi) * Auto commit: Calculate translation coverage * New translations en.json (Latvian) * New translations en.json (French) * Auto commit: Calculate translation coverage * New translations en.json (French) * Auto commit: Calculate translation coverage * New translations en.json (Portuguese, Brazilian) * Auto commit: Calculate translation coverage * New translations en.json (Japanese) * Auto commit: Calculate translation coverage * build(deps): bump json5 from 2.2.1 to 2.2.3 in /src/packages/excalidraw (#6062) Bumps [json5](https://github.com/json5/json5) from 2.2.1 to 2.2.3. - [Release notes](https://github.com/json5/json5/releases) - [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md) - [Commits](https://github.com/json5/json5/compare/v2.2.1...v2.2.3) --- updated-dependencies: - dependency-name: json5 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump json5 from 2.2.1 to 2.2.3 in /src/packages/utils (#6061) Bumps [json5](https://github.com/json5/json5) from 2.2.1 to 2.2.3. - [Release notes](https://github.com/json5/json5/releases) - [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md) - [Commits](https://github.com/json5/json5/compare/v2.2.1...v2.2.3) --- updated-dependencies: - dependency-name: json5 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump json5 from 2.2.1 to 2.2.3 in /dev-docs (#6060) Bumps [json5](https://github.com/json5/json5) from 2.2.1 to 2.2.3. - [Release notes](https://github.com/json5/json5/releases) - [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md) - [Commits](https://github.com/json5/json5/compare/v2.2.1...v2.2.3) --- updated-dependencies: - dependency-name: json5 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump decode-uri-component from 0.2.0 to 0.2.2 (#5963) Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2. - [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases) - [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2) --- updated-dependencies: - dependency-name: decode-uri-component dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump json5 from 1.0.1 to 1.0.2 (#6076) Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2. - [Release notes](https://github.com/json5/json5/releases) - [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md) - [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2) --- updated-dependencies: - dependency-name: json5 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump loader-utils from 2.0.3 to 2.0.4 (#5905) Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.3 to 2.0.4. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v2.0.3...v2.0.4) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump loader-utils from 2.0.3 to 2.0.4 in /src/packages/excalidraw (#5892) build(deps): bump loader-utils in /src/packages/excalidraw Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.3 to 2.0.4. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v2.0.3...v2.0.4) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix: stale appState of MainMenu defaultItems rendered from Actions (#6074) * fix: png-exporting does not preserve angles correctly for flipped images (#6085) * fix: png-exporting does not preserve angles correctly for flipped images * refactor related code * simplify further and comment * fix: image horizontal flip fix + improved tests (#5799) Co-authored-by: Antonio Della Fortuna <a.dellafortuna00@gmail.com> Co-authored-by: dwelle <luzar.david@gmail.com> fixes https://github.com/excalidraw/excalidraw/issues/5784 * fix: React.memo resolvers not accounting for all props (#6042) * fix: use position absolute for mobile misc tools (#6099) * feat: generic button export (#6092) Co-authored-by: dwelle <luzar.david@gmail.com> * feat: render unknown supplied children to UI (#6096) * feat: support WelcomeScreen customization API (#6048) * fix: renamed folder MainMenu->main-menu and support rest props (#6103) * renamed folder MainMenu -> main-menu * rename ariaLabel -> aria-label and dataTestId -> data-testid * allow rest props * fix * lint * add ts check * ts for div * fix * fix * fix * feat: new Live Collaboration Component API (#6104) * feat: new Live Collaboration Component API * namespace export icons into `icons` dictionary and lowercase * update readme and changelog * review fixes * fix * fix * update docs * remove * allow button rest props * update docs * docs * add `WelcomeScreen.Center.MenuItemLiveCollaborationTrigger` * fix lint * update changelog Co-authored-by: dwelle <luzar.david@gmail.com> * fix: mobile tools positioning (#6107) * fix: mobile tools positioning * add var for padding * use css var * new line * stupid mistake * lint * fix: remove overflow hidden from button (#6110) remove overflow hidden from button * docs: release @excalidraw/excalidraw@0.14.0 🎉 (#6109) * docs: release @excalidraw/excalidraw@0.14.1 🎉 (#6112) * build: temporarily disable pre-commit (#6132) * chore: Update translations from Crowdin (#6077) * feat: show copy-as-png export button on firefox and show steps how to enable it (#6125) * feat: hide copy-as-png shortcut from help dialog if not supported * fix: support firefox if clipboard.write supported * show shrotcut in firefox and instead show error message how to enable the flag support * widen to TypeError because minification * show copy-as-png on firefox even if it will throw * style: change in ExportButton style (#6147) (#6148) Co-authored-by: David Luzar <luzar.david@gmail.com> * fix: button background and svg sizes (#6155) * fix: button background color fallback * fix svg width/height * feat: add hand/panning tool (#6141) * feat: add hand/panning tool * move hand tool right of tool lock separator * tweak i18n * rename `panning` -> `hand` * toggle between last tool and hand on `H` shortcut * hide properties sidebar when `hand` active * revert to rendering HandButton manually due to mobile toolbar * feat: close MainMenu and Library dropdown on item select (#6152) * fix: declare css variable for font in excalidraw so its available in host (#6160) declar css variable for font in excalidraw so its available in host * fix: 🐛 broken emojis when wrap text (#6153) * fix: 🐛 broken emojis when wrap text * refactor: Delete unnecessary "else" (reduce indentation) * fix: remove code block that causes the emojis to disappear * Apply suggestions from code review Co-authored-by: David Luzar <luzar.david@gmail.com> * fix: 🚑 possibly undefined value * Add spec Co-authored-by: David Luzar <luzar.david@gmail.com> Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com> * fix: set the width correctly using measureText in editor (#6162) * fix: quick typo fix (#6167) * fix: add 1px width to the container to calculate more accurately (#6174) * fix: add 1px width to the container to calculate accurately * fix tests * feat: rewrite public UI component rendering using tunnels (#6117) * feat: rewrite public UI component rendering using tunnels * factor out into components * comments * fix variable naming * fix not hiding welcomeScreen * factor out AppFooter and memoize components * remove `UIOptions.welcomeScreen` and render only from host app * factor out tunnels into own file * update changelog. Keep `UIOptions.welcomeScreen` as deprecated * update changelog * lint --------- Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com> * fix: make tunnels work in multi-instance scenarios (#6178) * fix: make tunnels work in multi-instance scenarios * factor tunnels out * use tunnel-rat fork until upsteam updated * fix: horizontal padding when aligning bound text containers (#6180) * fix: horizontal padding when aligning bound text containers * Add specs * fix * docs: release @excalidraw/excalidraw@0.14.2 🎉 (#6181) * 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> * fix: edit link in docs (#6182) * docs: show last updated time and author (#6183) docs:show last updated time and author * fix: hide welcome screen on mobile once user interacts (#6185) * fix: hide welcome screen on mobile once started drawing * Add specs * 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> * 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> * chore: Update translations from Crowdin (#6150) * feat: shortcut for clearCanvas confirmDialog (#6114) Co-authored-by: dwelle <luzar.david@gmail.com> resolve https://github.com/excalidraw/excalidraw/issues/5818 * 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 * 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 * refactor: Make the example React app reusable without duplication (#6188) * fix: don't allow blank space in collab name (#6211) * don't allow blank space in collab name * add spec * prevent blank * docs: enable Algolia for search (#6230) * 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 * docs: Fixed broken codesandbox link in the dev-docs (#6229) fixed broken link * docs: new readme (#6240) Co-authored-by: David Luzar <luzar.david@gmail.com> * docs: fix next.js example (#6241) * docs: fix typo (#6252) * 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 * docs: Fix outdated link in README.md (#6263) * 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 * 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 * fix: indenting via `tab` clashing with IME compositor (#6258) * chore: Update translations from Crowdin (#6191) * fix: rerender i18n in host components on lang change (#6224) * fix: fit mobile toolbar and make scrollable (#6270) Co-authored-by: dwelle <luzar.david@gmail.com> * 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 * fix: compute container height from bound text correctly (#6273) * fix: compute container height from bound text correctly * fix specs * Add tests * fix: svg text baseline (#6285 * fix: svg text baseline * fix for multiline --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: David Luzar <luzar.david@gmail.com> Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com> Co-authored-by: Antonio Della Fortuna <50418432+adarkforce@users.noreply.github.com> Co-authored-by: Antonio Della Fortuna <a.dellafortuna00@gmail.com> Co-authored-by: DanielJGeiger <1852529+DanielJGeiger@users.noreply.github.com> Co-authored-by: Fer <63980689+1fbr@users.noreply.github.com> Co-authored-by: fennghuang <89014758+fennghuang@users.noreply.github.com> Co-authored-by: Ryan Di <ryan.weihao.di@gmail.com> Co-authored-by: Ryan <diweihao@bytedance.com> Co-authored-by: Excalidraw Bot <77840495+excalibot@users.noreply.github.com> Co-authored-by: EternalWill43 <70084418+EternalWill43@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Barnabás Molnár <38168628+barnabasmolnar@users.noreply.github.com> Co-authored-by: Nishant-l <61119157+Nishant-l@users.noreply.github.com> Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com> Co-authored-by: JUNYI OU <49964599+Irvingouj@users.noreply.github.com> Co-authored-by: Jang Min <jangmin.dev@gmail.com> Co-authored-by: Matthieu Rossignon <51274353+Mattross45@users.noreply.github.com> Co-authored-by: Dejavu Moe <jialong.vip@gmail.com> Co-authored-by: Luka Hietala <95122845+LukaHietala@users.noreply.github.com> Co-authored-by: Milos Vetesnik <maielo.mv@gmail.com> Co-authored-by: Jan Klass <kissaki@posteo.de> Co-authored-by: Hikaru Yoshino <57059705+osushicrusher@users.noreply.github.com> Co-authored-by: Tengku Farhan <109069184+tfarhan00@users.noreply.github.com>
This commit is contained in:
parent
82e56919ca
commit
1d5e865da1
305 changed files with 20140 additions and 9184 deletions
|
@ -5,7 +5,7 @@ import { ExcalidrawElement, PointerType } from "../element/types";
|
|||
import { t } from "../i18n";
|
||||
import { useDevice } from "../components/App";
|
||||
import {
|
||||
canChangeSharpness,
|
||||
canChangeRoundness,
|
||||
canHaveArrowheads,
|
||||
getTargetElements,
|
||||
hasBackground,
|
||||
|
@ -25,11 +25,12 @@ import Stack from "./Stack";
|
|||
import { ToolButton } from "./ToolButton";
|
||||
import { hasStrokeColor } from "../scene/comparisons";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks";
|
||||
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";
|
||||
|
||||
export const SelectedShapeActions = ({
|
||||
appState,
|
||||
|
@ -109,9 +110,9 @@ export const SelectedShapeActions = ({
|
|||
</>
|
||||
)}
|
||||
|
||||
{(canChangeSharpness(appState.activeTool.type) ||
|
||||
targetElements.some((element) => canChangeSharpness(element.type))) && (
|
||||
<>{renderAction("changeSharpness")}</>
|
||||
{(canChangeRoundness(appState.activeTool.type) ||
|
||||
targetElements.some((element) => canChangeRoundness(element.type))) && (
|
||||
<>{renderAction("changeRoundness")}</>
|
||||
)}
|
||||
|
||||
{(hasText(appState.activeTool.type) ||
|
||||
|
@ -125,10 +126,8 @@ export const SelectedShapeActions = ({
|
|||
</>
|
||||
)}
|
||||
|
||||
{targetElements.some(
|
||||
(element) =>
|
||||
hasBoundTextElement(element) || isBoundToContainer(element),
|
||||
) && renderAction("changeVerticalAlign")}
|
||||
{shouldAllowVerticalAlign(targetElements) &&
|
||||
renderAction("changeVerticalAlign")}
|
||||
{(canHaveArrowheads(appState.activeTool.type) ||
|
||||
targetElements.some((element) => canHaveArrowheads(element.type))) && (
|
||||
<>{renderAction("changeArrowhead")}</>
|
||||
|
@ -220,9 +219,10 @@ export const ShapesSwitcher = ({
|
|||
<>
|
||||
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
||||
const label = t(`toolBar.${value}`);
|
||||
const letter = key && (typeof key === "string" ? key : key[0]);
|
||||
const letter =
|
||||
key && capitalizeString(typeof key === "string" ? key : key[0]);
|
||||
const shortcut = letter
|
||||
? `${capitalizeString(letter)} ${t("helpDialog.or")} ${numericKey}`
|
||||
? `${letter} ${t("helpDialog.or")} ${numericKey}`
|
||||
: `${numericKey}`;
|
||||
return (
|
||||
<ToolButton
|
||||
|
@ -233,7 +233,7 @@ export const ShapesSwitcher = ({
|
|||
checked={activeTool.type === value}
|
||||
name="editor-current-shape"
|
||||
title={`${capitalizeString(label)} — ${shortcut}`}
|
||||
keyBindingLabel={numericKey}
|
||||
keyBindingLabel={numericKey || letter}
|
||||
aria-label={capitalizeString(label)}
|
||||
aria-keyshortcuts={shortcut}
|
||||
data-testid={`toolbar-${value}`}
|
||||
|
|
35
src/components/ActiveConfirmDialog.tsx
Normal file
35
src/components/ActiveConfirmDialog.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { atom, useAtom } from "jotai";
|
||||
import { actionClearCanvas } from "../actions";
|
||||
import { t } from "../i18n";
|
||||
import { useExcalidrawActionManager } from "./App";
|
||||
import ConfirmDialog from "./ConfirmDialog";
|
||||
|
||||
export const activeConfirmDialogAtom = atom<"clearCanvas" | null>(null);
|
||||
|
||||
export const ActiveConfirmDialog = () => {
|
||||
const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
|
||||
activeConfirmDialogAtom,
|
||||
);
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
if (!activeConfirmDialog) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (activeConfirmDialog === "clearCanvas") {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
onConfirm={() => {
|
||||
actionManager.executeAction(actionClearCanvas);
|
||||
setActiveConfirmDialog(null);
|
||||
}}
|
||||
onCancel={() => setActiveConfirmDialog(null)}
|
||||
title={t("clearCanvasDialog.title")}
|
||||
>
|
||||
<p className="clear-canvas__content"> {t("alerts.clearReset")}</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -1,23 +0,0 @@
|
|||
// TODO barnabasmolnar/editor-redesign
|
||||
// this icon is not great
|
||||
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
||||
import { save } from "../components/icons";
|
||||
import { t } from "../i18n";
|
||||
|
||||
import "./ActiveFile.scss";
|
||||
import MenuItem from "./MenuItem";
|
||||
|
||||
type ActiveFileProps = {
|
||||
fileName?: string;
|
||||
onSave: () => void;
|
||||
};
|
||||
|
||||
export const ActiveFile = ({ fileName, onSave }: ActiveFileProps) => (
|
||||
<MenuItem
|
||||
label={`${t("buttons.save")}`}
|
||||
shortcut={getShortcutFromShortcutName("saveScene")}
|
||||
dataTestId="save-button"
|
||||
onClick={onSave}
|
||||
icon={save}
|
||||
/>
|
||||
);
|
File diff suppressed because it is too large
Load diff
|
@ -4,8 +4,8 @@
|
|||
.Avatar {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
position: relative;
|
||||
border-radius: 100%;
|
||||
outline: 2px solid var(--avatar-border-color);
|
||||
outline-offset: 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -21,5 +21,16 @@
|
|||
height: 100%;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: -3px;
|
||||
bottom: -3px;
|
||||
left: -3px;
|
||||
border: 1px solid var(--avatar-border-color);
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
7
src/components/Button.scss
Normal file
7
src/components/Button.scss
Normal file
|
@ -0,0 +1,7 @@
|
|||
@import "../css/theme";
|
||||
|
||||
.excalidraw {
|
||||
.excalidraw-button {
|
||||
@include outlineButtonStyles;
|
||||
}
|
||||
}
|
35
src/components/Button.tsx
Normal file
35
src/components/Button.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import "./Button.scss";
|
||||
|
||||
interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
type?: "button" | "submit" | "reset";
|
||||
onSelect: () => any;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic button component that follows Excalidraw's design system.
|
||||
* Style can be customised using `className` or `style` prop.
|
||||
* Accepts all props that a regular `button` element accepts.
|
||||
*/
|
||||
export const Button = ({
|
||||
type = "button",
|
||||
onSelect,
|
||||
children,
|
||||
className = "",
|
||||
...rest
|
||||
}: ButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
onClick={(event) => {
|
||||
onSelect();
|
||||
rest.onClick?.(event);
|
||||
}}
|
||||
type={type}
|
||||
className={`excalidraw-button ${className}`}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
|
@ -1,39 +0,0 @@
|
|||
import { useState } from "react";
|
||||
import { t } from "../i18n";
|
||||
import { TrashIcon } from "./icons";
|
||||
|
||||
import ConfirmDialog from "./ConfirmDialog";
|
||||
import MenuItem from "./MenuItem";
|
||||
|
||||
const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const toggleDialog = () => {
|
||||
setShowDialog(!showDialog);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem
|
||||
label={t("buttons.clearReset")}
|
||||
icon={TrashIcon}
|
||||
onClick={toggleDialog}
|
||||
dataTestId="clear-canvas-button"
|
||||
/>
|
||||
|
||||
{showDialog && (
|
||||
<ConfirmDialog
|
||||
onConfirm={() => {
|
||||
onConfirm();
|
||||
toggleDialog();
|
||||
}}
|
||||
onCancel={toggleDialog}
|
||||
title={t("clearCanvasDialog.title")}
|
||||
>
|
||||
<p className="clear-canvas__content"> {t("alerts.clearReset")}</p>
|
||||
</ConfirmDialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClearCanvas;
|
|
@ -1,49 +0,0 @@
|
|||
import { t } from "../i18n";
|
||||
import { UsersIcon } from "./icons";
|
||||
|
||||
import "./CollabButton.scss";
|
||||
import MenuItem from "./MenuItem";
|
||||
import clsx from "clsx";
|
||||
|
||||
const CollabButton = ({
|
||||
isCollaborating,
|
||||
collaboratorCount,
|
||||
onClick,
|
||||
isInHamburgerMenu = true,
|
||||
}: {
|
||||
isCollaborating: boolean;
|
||||
collaboratorCount: number;
|
||||
onClick: () => void;
|
||||
isInHamburgerMenu?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{isInHamburgerMenu ? (
|
||||
<MenuItem
|
||||
label={t("labels.liveCollaboration")}
|
||||
dataTestId="collab-button"
|
||||
icon={UsersIcon}
|
||||
onClick={onClick}
|
||||
isCollaborating={isCollaborating}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
className={clsx("collab-button", { active: isCollaborating })}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
style={{ position: "relative" }}
|
||||
title={t("labels.liveCollaboration")}
|
||||
>
|
||||
{UsersIcon}
|
||||
{collaboratorCount > 0 && (
|
||||
<div className="CollabButton-collaborators">
|
||||
{collaboratorCount}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollabButton;
|
|
@ -66,10 +66,13 @@ const getColor = (color: string): string | null => {
|
|||
return color;
|
||||
}
|
||||
|
||||
return isValidColor(color)
|
||||
? color
|
||||
: isValidColor(`#${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;
|
||||
};
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@ import { Dialog, DialogProps } from "./Dialog";
|
|||
|
||||
import "./ConfirmDialog.scss";
|
||||
import DialogActionButton from "./DialogActionButton";
|
||||
import { isMenuOpenAtom } from "./App";
|
||||
import { isDropdownOpenAtom } from "./App";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
||||
import { useExcalidrawSetAppState } from "./App";
|
||||
|
||||
interface Props extends Omit<DialogProps, "onCloseRequest"> {
|
||||
onConfirm: () => void;
|
||||
|
@ -23,9 +23,8 @@ const ConfirmDialog = (props: Props) => {
|
|||
className = "",
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const setIsMenuOpen = useSetAtom(isMenuOpenAtom);
|
||||
const setIsDropdownOpen = useSetAtom(isDropdownOpenAtom);
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
@ -39,16 +38,16 @@ const ConfirmDialog = (props: Props) => {
|
|||
<DialogActionButton
|
||||
label={cancelText}
|
||||
onClick={() => {
|
||||
setIsMenuOpen(false);
|
||||
setIsDropdownOpen(false);
|
||||
setAppState({ openMenu: null });
|
||||
setIsLibraryMenuOpen(false);
|
||||
onCancel();
|
||||
}}
|
||||
/>
|
||||
<DialogActionButton
|
||||
label={confirmText}
|
||||
onClick={() => {
|
||||
setIsMenuOpen(false);
|
||||
setIsDropdownOpen(false);
|
||||
setAppState({ openMenu: null });
|
||||
setIsLibraryMenuOpen(false);
|
||||
onConfirm();
|
||||
}}
|
||||
actionType="danger"
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
color: var(--popup-text-color);
|
||||
}
|
||||
|
||||
.context-menu-option {
|
||||
.context-menu-item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 9.5rem;
|
||||
|
@ -43,16 +43,16 @@
|
|||
}
|
||||
|
||||
&.dangerous {
|
||||
.context-menu-option__label {
|
||||
.context-menu-item__label {
|
||||
color: $oc-red-7;
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-option__label {
|
||||
.context-menu-item__label {
|
||||
justify-self: start;
|
||||
margin-inline-end: 20px;
|
||||
}
|
||||
.context-menu-option__shortcut {
|
||||
.context-menu-item__shortcut {
|
||||
justify-self: end;
|
||||
opacity: 0.6;
|
||||
font-family: inherit;
|
||||
|
@ -60,37 +60,37 @@
|
|||
}
|
||||
}
|
||||
|
||||
.context-menu-option:hover {
|
||||
.context-menu-item:hover {
|
||||
color: var(--popup-bg-color);
|
||||
background-color: var(--select-highlight-color);
|
||||
|
||||
&.dangerous {
|
||||
.context-menu-option__label {
|
||||
.context-menu-item__label {
|
||||
color: var(--popup-bg-color);
|
||||
}
|
||||
background-color: $oc-red-6;
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-option:focus {
|
||||
.context-menu-item:focus {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@include isMobile {
|
||||
.context-menu-option {
|
||||
.context-menu-item {
|
||||
display: block;
|
||||
|
||||
.context-menu-option__label {
|
||||
.context-menu-item__label {
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
.context-menu-option__shortcut {
|
||||
.context-menu-item__shortcut {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-option-separator {
|
||||
.context-menu-item-separator {
|
||||
border: none;
|
||||
border-top: 1px solid $oc-gray-5;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { createRoot, Root } from "react-dom/client";
|
||||
import clsx from "clsx";
|
||||
import { Popover } from "./Popover";
|
||||
import { t } from "../i18n";
|
||||
|
@ -10,135 +9,116 @@ import {
|
|||
} from "../actions/shortcuts";
|
||||
import { Action } from "../actions/types";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { AppState } from "../types";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import {
|
||||
useExcalidrawAppState,
|
||||
useExcalidrawElements,
|
||||
useExcalidrawSetAppState,
|
||||
} from "./App";
|
||||
import React from "react";
|
||||
|
||||
export type ContextMenuOption = "separator" | Action;
|
||||
export type ContextMenuItem = typeof CONTEXT_MENU_SEPARATOR | Action;
|
||||
|
||||
export type ContextMenuItems = (ContextMenuItem | false | null | undefined)[];
|
||||
|
||||
type ContextMenuProps = {
|
||||
options: ContextMenuOption[];
|
||||
onCloseRequest?(): void;
|
||||
actionManager: ActionManager;
|
||||
items: ContextMenuItems;
|
||||
top: number;
|
||||
left: number;
|
||||
actionManager: ActionManager;
|
||||
appState: Readonly<AppState>;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
};
|
||||
|
||||
const ContextMenu = ({
|
||||
options,
|
||||
onCloseRequest,
|
||||
top,
|
||||
left,
|
||||
actionManager,
|
||||
appState,
|
||||
elements,
|
||||
}: ContextMenuProps) => {
|
||||
return (
|
||||
<Popover
|
||||
onCloseRequest={onCloseRequest}
|
||||
top={top}
|
||||
left={left}
|
||||
fitInViewport={true}
|
||||
offsetLeft={appState.offsetLeft}
|
||||
offsetTop={appState.offsetTop}
|
||||
viewportWidth={appState.width}
|
||||
viewportHeight={appState.height}
|
||||
>
|
||||
<ul
|
||||
className="context-menu"
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
>
|
||||
{options.map((option, idx) => {
|
||||
if (option === "separator") {
|
||||
return <hr key={idx} className="context-menu-option-separator" />;
|
||||
}
|
||||
export const CONTEXT_MENU_SEPARATOR = "separator";
|
||||
|
||||
const actionName = option.name;
|
||||
let label = "";
|
||||
if (option.contextItemLabel) {
|
||||
if (typeof option.contextItemLabel === "function") {
|
||||
label = t(option.contextItemLabel(elements, appState));
|
||||
} else {
|
||||
label = t(option.contextItemLabel);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<li key={idx} data-testid={actionName} onClick={onCloseRequest}>
|
||||
<button
|
||||
className={clsx("context-menu-option", {
|
||||
dangerous: actionName === "deleteSelectedElements",
|
||||
checkmark: option.checked?.(appState),
|
||||
})}
|
||||
onClick={() =>
|
||||
actionManager.executeAction(option, "contextMenu")
|
||||
}
|
||||
>
|
||||
<div className="context-menu-option__label">{label}</div>
|
||||
<kbd className="context-menu-option__shortcut">
|
||||
{actionName
|
||||
? getShortcutFromShortcutName(actionName as ShortcutName)
|
||||
: ""}
|
||||
</kbd>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
export const ContextMenu = React.memo(
|
||||
({ actionManager, items, top, left }: ContextMenuProps) => {
|
||||
const appState = useExcalidrawAppState();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
const elements = useExcalidrawElements();
|
||||
|
||||
const contextMenuRoots = new WeakMap<HTMLElement, Root>();
|
||||
|
||||
const getContextMenuRoot = (container: HTMLElement): Root => {
|
||||
let contextMenuRoot = contextMenuRoots.get(container);
|
||||
if (contextMenuRoot) {
|
||||
return contextMenuRoot;
|
||||
}
|
||||
contextMenuRoot = createRoot(
|
||||
container.querySelector(".excalidraw-contextMenuContainer")!,
|
||||
);
|
||||
contextMenuRoots.set(container, contextMenuRoot);
|
||||
return contextMenuRoot;
|
||||
};
|
||||
|
||||
const handleClose = (container: HTMLElement) => {
|
||||
const contextMenuRoot = contextMenuRoots.get(container);
|
||||
if (contextMenuRoot) {
|
||||
contextMenuRoot.unmount();
|
||||
contextMenuRoots.delete(container);
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
push(params: {
|
||||
options: (ContextMenuOption | false | null | undefined)[];
|
||||
top: ContextMenuProps["top"];
|
||||
left: ContextMenuProps["left"];
|
||||
actionManager: ContextMenuProps["actionManager"];
|
||||
appState: Readonly<AppState>;
|
||||
container: HTMLElement;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
}) {
|
||||
const options = Array.of<ContextMenuOption>();
|
||||
params.options.forEach((option) => {
|
||||
if (option) {
|
||||
options.push(option);
|
||||
const filteredItems = items.reduce((acc: ContextMenuItem[], item) => {
|
||||
if (
|
||||
item &&
|
||||
(item === CONTEXT_MENU_SEPARATOR ||
|
||||
!item.predicate ||
|
||||
item.predicate(
|
||||
elements,
|
||||
appState,
|
||||
actionManager.app.props,
|
||||
actionManager.app,
|
||||
))
|
||||
) {
|
||||
acc.push(item);
|
||||
}
|
||||
});
|
||||
if (options.length) {
|
||||
getContextMenuRoot(params.container).render(
|
||||
<ContextMenu
|
||||
top={params.top}
|
||||
left={params.left}
|
||||
options={options}
|
||||
onCloseRequest={() => handleClose(params.container)}
|
||||
actionManager={params.actionManager}
|
||||
appState={params.appState}
|
||||
elements={params.elements}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
onCloseRequest={() => setAppState({ contextMenu: null })}
|
||||
top={top}
|
||||
left={left}
|
||||
fitInViewport={true}
|
||||
offsetLeft={appState.offsetLeft}
|
||||
offsetTop={appState.offsetTop}
|
||||
viewportWidth={appState.width}
|
||||
viewportHeight={appState.height}
|
||||
>
|
||||
<ul
|
||||
className="context-menu"
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
>
|
||||
{filteredItems.map((item, idx) => {
|
||||
if (item === CONTEXT_MENU_SEPARATOR) {
|
||||
if (
|
||||
!filteredItems[idx - 1] ||
|
||||
filteredItems[idx - 1] === CONTEXT_MENU_SEPARATOR
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return <hr key={idx} className="context-menu-item-separator" />;
|
||||
}
|
||||
|
||||
const actionName = item.name;
|
||||
let label = "";
|
||||
if (item.contextItemLabel) {
|
||||
if (typeof item.contextItemLabel === "function") {
|
||||
label = t(item.contextItemLabel(elements, appState));
|
||||
} else {
|
||||
label = t(item.contextItemLabel);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
key={idx}
|
||||
data-testid={actionName}
|
||||
onClick={() => {
|
||||
// we need update state before executing the action in case
|
||||
// the action uses the appState it's being passed (that still
|
||||
// contains a defined contextMenu) to return the next state.
|
||||
setAppState({ contextMenu: null }, () => {
|
||||
actionManager.executeAction(item, "contextMenu");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<button
|
||||
className={clsx("context-menu-item", {
|
||||
dangerous: actionName === "deleteSelectedElements",
|
||||
checkmark: item.checked?.(appState),
|
||||
})}
|
||||
>
|
||||
<div className="context-menu-item__label">{label}</div>
|
||||
<kbd className="context-menu-item__shortcut">
|
||||
{actionName
|
||||
? getShortcutFromShortcutName(actionName as ShortcutName)
|
||||
: ""}
|
||||
</kbd>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</Popover>
|
||||
);
|
||||
},
|
||||
};
|
||||
);
|
||||
|
|
|
@ -2,7 +2,11 @@ import clsx from "clsx";
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
||||
import { t } from "../i18n";
|
||||
import { useExcalidrawContainer, useDevice } from "../components/App";
|
||||
import {
|
||||
useExcalidrawContainer,
|
||||
useDevice,
|
||||
useExcalidrawSetAppState,
|
||||
} from "../components/App";
|
||||
import { KEYS } from "../keys";
|
||||
import "./Dialog.scss";
|
||||
import { back, CloseIcon } from "./icons";
|
||||
|
@ -10,8 +14,8 @@ import { Island } from "./Island";
|
|||
import { Modal } from "./Modal";
|
||||
import { AppState } from "../types";
|
||||
import { queryFocusableElements } from "../utils";
|
||||
import { isMenuOpenAtom, isDropdownOpenAtom } from "./App";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
||||
|
||||
export interface DialogProps {
|
||||
children: React.ReactNode;
|
||||
|
@ -67,12 +71,12 @@ export const Dialog = (props: DialogProps) => {
|
|||
return () => islandNode.removeEventListener("keydown", handleKeyDown);
|
||||
}, [islandNode, props.autofocus]);
|
||||
|
||||
const setIsMenuOpen = useSetAtom(isMenuOpenAtom);
|
||||
const setIsDropdownOpen = useSetAtom(isDropdownOpenAtom);
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
|
||||
|
||||
const onClose = () => {
|
||||
setIsMenuOpen(false);
|
||||
setIsDropdownOpen(false);
|
||||
setAppState({ openMenu: null });
|
||||
setIsLibraryMenuOpen(false);
|
||||
(lastActiveElement as HTMLElement).focus();
|
||||
props.onCloseRequest();
|
||||
};
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import { t } from "../i18n";
|
||||
import { shield } from "./icons";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
|
||||
const EncryptedIcon = () => (
|
||||
<a
|
||||
className="encrypted-icon tooltip"
|
||||
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t("encrypted.link")}
|
||||
>
|
||||
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
||||
{shield}
|
||||
</Tooltip>
|
||||
</a>
|
||||
);
|
||||
|
||||
export default EncryptedIcon;
|
|
@ -96,6 +96,10 @@
|
|||
width: 5rem;
|
||||
height: 5rem;
|
||||
margin: 0 0.2em;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-radius: 1rem;
|
||||
background-color: var(--button-color);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.FixedSideContainer {
|
||||
position: absolute;
|
||||
|
@ -9,10 +11,10 @@
|
|||
}
|
||||
|
||||
.FixedSideContainer_side_top {
|
||||
left: 1rem;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
left: var(--editor-container-padding);
|
||||
top: var(--editor-container-padding);
|
||||
right: var(--editor-container-padding);
|
||||
bottom: var(--editor-container-padding);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
|
|
32
src/components/HandButton.tsx
Normal file
32
src/components/HandButton.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import "./ToolIcon.scss";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { handIcon } from "./icons";
|
||||
import { KEYS } from "../keys";
|
||||
|
||||
type LockIconProps = {
|
||||
title?: string;
|
||||
name?: string;
|
||||
checked: boolean;
|
||||
onChange?(): void;
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
export const HandButton = (props: LockIconProps) => {
|
||||
return (
|
||||
<ToolButton
|
||||
className={clsx("Shape", { fillable: false })}
|
||||
type="radio"
|
||||
icon={handIcon}
|
||||
name="editor-current-shape"
|
||||
checked={props.checked}
|
||||
title={`${props.title} — H`}
|
||||
keyBindingLabel={!props.isMobile ? KEYS.H.toLocaleUpperCase() : undefined}
|
||||
aria-label={`${props.title} — H`}
|
||||
aria-keyshortcuts={KEYS.H}
|
||||
data-testid={`toolbar-hand`}
|
||||
onChange={() => props.onChange?.()}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,10 +1,12 @@
|
|||
import React from "react";
|
||||
import { t } from "../i18n";
|
||||
import { isDarwin, isWindows, KEYS } from "../keys";
|
||||
import { KEYS } from "../keys";
|
||||
import { Dialog } from "./Dialog";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import "./HelpDialog.scss";
|
||||
import { ExternalLinkIcon } from "./icons";
|
||||
import { probablySupportsClipboardBlob } from "../clipboard";
|
||||
import { isDarwin, isFirefox, isWindows } from "../constants";
|
||||
|
||||
const Header = () => (
|
||||
<div className="HelpDialog__header">
|
||||
|
@ -67,6 +69,10 @@ function* intersperse(as: JSX.Element[][], delim: string | null) {
|
|||
}
|
||||
}
|
||||
|
||||
const upperCaseSingleChars = (str: string) => {
|
||||
return str.replace(/\b[a-z]\b/, (c) => c.toUpperCase());
|
||||
};
|
||||
|
||||
const Shortcut = ({
|
||||
label,
|
||||
shortcuts,
|
||||
|
@ -81,7 +87,9 @@ const Shortcut = ({
|
|||
? [...shortcut.slice(0, -2).split("+"), "+"]
|
||||
: shortcut.split("+");
|
||||
|
||||
return keys.map((key) => <ShortcutKey key={key}>{key}</ShortcutKey>);
|
||||
return keys.map((key) => (
|
||||
<ShortcutKey key={key}>{upperCaseSingleChars(key)}</ShortcutKey>
|
||||
));
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -118,6 +126,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||
className="HelpDialog__island--tools"
|
||||
caption={t("helpDialog.tools")}
|
||||
>
|
||||
<Shortcut label={t("toolBar.hand")} shortcuts={[KEYS.H]} />
|
||||
<Shortcut
|
||||
label={t("toolBar.selection")}
|
||||
shortcuts={[KEYS.V, KEYS["1"]]}
|
||||
|
@ -140,11 +149,11 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||
/>
|
||||
<Shortcut
|
||||
label={t("toolBar.line")}
|
||||
shortcuts={[KEYS.P, KEYS["6"]]}
|
||||
shortcuts={[KEYS.L, KEYS["6"]]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("toolBar.freedraw")}
|
||||
shortcuts={["Shift + P", KEYS["7"]]}
|
||||
shortcuts={[KEYS.P, KEYS["7"]]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("toolBar.text")}
|
||||
|
@ -157,7 +166,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||
/>
|
||||
<Shortcut
|
||||
label={t("helpDialog.editSelectedShape")}
|
||||
shortcuts={[getShortcutKey("Enter"), t("helpDialog.doubleClick")]}
|
||||
shortcuts={[
|
||||
getShortcutKey("CtrlOrCmd+Enter"),
|
||||
getShortcutKey(`CtrlOrCmd + ${t("helpDialog.doubleClick")}`),
|
||||
]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("helpDialog.textNewLine")}
|
||||
|
@ -227,6 +239,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||
label={t("helpDialog.zoomToSelection")}
|
||||
shortcuts={["Shift+2"]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("helpDialog.movePageUpDown")}
|
||||
shortcuts={["PgUp/PgDn"]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("helpDialog.movePageLeftRight")}
|
||||
shortcuts={["Shift+PgUp/PgDn"]}
|
||||
/>
|
||||
<Shortcut label={t("buttons.fullScreen")} shortcuts={["F"]} />
|
||||
<Shortcut
|
||||
label={t("buttons.zenMode")}
|
||||
|
@ -253,6 +273,38 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||
className="HelpDialog__island--editor"
|
||||
caption={t("helpDialog.editor")}
|
||||
>
|
||||
<Shortcut
|
||||
label={t("labels.moveCanvas")}
|
||||
shortcuts={[
|
||||
getShortcutKey(`Space+${t("helpDialog.drag")}`),
|
||||
getShortcutKey(`Wheel+${t("helpDialog.drag")}`),
|
||||
]}
|
||||
isOr={true}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("buttons.clearReset")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Delete")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.delete")}
|
||||
shortcuts={[getShortcutKey("Delete")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.cut")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+X")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.copy")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+C")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.paste")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+V")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.pasteAsPlaintext")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+V")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.selectAll")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+A")]}
|
||||
|
@ -269,30 +321,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||
label={t("helpDialog.deepBoxSelect")}
|
||||
shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`)]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.moveCanvas")}
|
||||
shortcuts={[
|
||||
getShortcutKey(`Space+${t("helpDialog.drag")}`),
|
||||
getShortcutKey(`Wheel+${t("helpDialog.drag")}`),
|
||||
]}
|
||||
isOr={true}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.cut")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+X")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.copy")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+C")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.paste")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+V")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.copyAsPng")}
|
||||
shortcuts={[getShortcutKey("Shift+Alt+C")]}
|
||||
/>
|
||||
{/* firefox supports clipboard API under a flag, so we'll
|
||||
show users what they can do in the error message */}
|
||||
{(probablySupportsClipboardBlob || isFirefox) && (
|
||||
<Shortcut
|
||||
label={t("labels.copyAsPng")}
|
||||
shortcuts={[getShortcutKey("Shift+Alt+C")]}
|
||||
/>
|
||||
)}
|
||||
<Shortcut
|
||||
label={t("labels.copyStyles")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]}
|
||||
|
@ -301,10 +337,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||
label={t("labels.pasteStyles")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Alt+V")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.delete")}
|
||||
shortcuts={[getShortcutKey("Delete")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.sendToBack")}
|
||||
shortcuts={[
|
||||
|
|
|
@ -12,7 +12,7 @@ import Stack from "./Stack";
|
|||
import "./ExportDialog.scss";
|
||||
import OpenColor from "open-color";
|
||||
import { CheckboxItem } from "./CheckboxItem";
|
||||
import { DEFAULT_EXPORT_PADDING } from "../constants";
|
||||
import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants";
|
||||
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
|
||||
|
@ -190,7 +190,9 @@ const ImageExportModal = ({
|
|||
>
|
||||
SVG
|
||||
</ExportButton>
|
||||
{probablySupportsClipboardBlob && (
|
||||
{/* firefox supports clipboard API under a flag,
|
||||
so let's throw and tell people what they can do */}
|
||||
{(probablySupportsClipboardBlob || isFirefox) && (
|
||||
<ExportButton
|
||||
title={t("buttons.copyPngToClipboard")}
|
||||
onClick={() => onExportToClipboard(exportedElements)}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
|
||||
import { AppState, ExportOpts, BinaryFiles } from "../types";
|
||||
import { Dialog } from "./Dialog";
|
||||
import { ExportIcon, exportToFileIcon, LinkIcon } from "./icons";
|
||||
import { exportToFileIcon, LinkIcon } from "./icons";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { actionSaveFileToDisk } from "../actions/actionExport";
|
||||
import { Card } from "./Card";
|
||||
|
@ -14,7 +14,6 @@ import { nativeFileSystemSupported } from "../data/filesystem";
|
|||
import { trackEvent } from "../analytics";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { getFrame } from "../utils";
|
||||
import MenuItem from "./MenuItem";
|
||||
|
||||
export type ExportCB = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
|
@ -94,6 +93,7 @@ export const JSONExportDialog = ({
|
|||
actionManager,
|
||||
exportOpts,
|
||||
canvas,
|
||||
setAppState,
|
||||
}: {
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
appState: AppState;
|
||||
|
@ -101,24 +101,15 @@ export const JSONExportDialog = ({
|
|||
actionManager: ActionManager;
|
||||
exportOpts: ExportOpts;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
}) => {
|
||||
const [modalIsShown, setModalIsShown] = useState(false);
|
||||
|
||||
const handleClose = React.useCallback(() => {
|
||||
setModalIsShown(false);
|
||||
}, []);
|
||||
setAppState({ openDialog: null });
|
||||
}, [setAppState]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem
|
||||
icon={ExportIcon}
|
||||
label={t("buttons.export")}
|
||||
onClick={() => {
|
||||
setModalIsShown(true);
|
||||
}}
|
||||
dataTestId="json-export-button"
|
||||
/>
|
||||
{modalIsShown && (
|
||||
{appState.openDialog === "jsonExport" && (
|
||||
<Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
|
||||
<JSONExportModal
|
||||
elements={elements}
|
||||
|
|
|
@ -80,12 +80,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.layer-ui__wrapper__footer-center {
|
||||
pointer-events: none;
|
||||
& > * {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
.layer-ui__wrapper__footer-left,
|
||||
.layer-ui__wrapper__footer-right,
|
||||
.disable-zen-mode--visible {
|
||||
|
|
|
@ -9,9 +9,8 @@ import { Language, t } from "../i18n";
|
|||
import { calculateScrollCenter } from "../scene";
|
||||
import { ExportType } from "../scene/types";
|
||||
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
|
||||
import { muteFSAbortError } from "../utils";
|
||||
import { isShallowEqual, muteFSAbortError } from "../utils";
|
||||
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
||||
import CollabButton from "./CollabButton";
|
||||
import { ErrorDialog } from "./ErrorDialog";
|
||||
import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
|
||||
import { FixedSideContainer } from "./FixedSideContainer";
|
||||
|
@ -35,26 +34,18 @@ import "./LayerUI.scss";
|
|||
import "./Toolbar.scss";
|
||||
import { PenModeButton } from "./PenModeButton";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { isMenuOpenAtom, useDevice } from "../components/App";
|
||||
import { useDevice } from "../components/App";
|
||||
import { Stats } from "./Stats";
|
||||
import { actionToggleStats } from "../actions/actionToggleStats";
|
||||
import Footer from "./Footer";
|
||||
import {
|
||||
ExportImageIcon,
|
||||
HamburgerMenuIcon,
|
||||
WelcomeScreenMenuArrow,
|
||||
WelcomeScreenTopToolbarArrow,
|
||||
} from "./icons";
|
||||
import { MenuLinks, Separator } from "./MenuUtils";
|
||||
import { useOutsideClickHook } from "../hooks/useOutsideClick";
|
||||
import WelcomeScreen from "./WelcomeScreen";
|
||||
import Footer from "./footer/Footer";
|
||||
import { hostSidebarCountersAtom } from "./Sidebar/Sidebar";
|
||||
import { jotaiScope } from "../jotai";
|
||||
import { useAtom } from "jotai";
|
||||
import { LanguageList } from "../excalidraw-app/components/LanguageList";
|
||||
import WelcomeScreenDecor from "./WelcomeScreenDecor";
|
||||
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
||||
import MenuItem from "./MenuItem";
|
||||
import { Provider, useAtom } from "jotai";
|
||||
import MainMenu from "./main-menu/MainMenu";
|
||||
import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
|
||||
import { HandButton } from "./HandButton";
|
||||
import { isHandToolActive } from "../appState";
|
||||
import { TunnelsContext, useInitializeTunnels } from "./context/tunnels";
|
||||
|
||||
interface LayerUIProps {
|
||||
actionManager: ActionManager;
|
||||
|
@ -63,15 +54,13 @@ interface LayerUIProps {
|
|||
canvas: HTMLCanvasElement | null;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
onCollabButtonClick?: () => void;
|
||||
onLockToggle: () => void;
|
||||
onHandToolToggle: () => void;
|
||||
onPenModeToggle: () => void;
|
||||
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||
showExitZenModeBtn: boolean;
|
||||
langCode: Language["code"];
|
||||
isCollaborating: boolean;
|
||||
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
||||
renderCustomFooter?: ExcalidrawProps["renderFooter"];
|
||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||
renderCustomSidebar?: ExcalidrawProps["renderSidebar"];
|
||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||
|
@ -81,7 +70,35 @@ interface LayerUIProps {
|
|||
id: string;
|
||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||
renderWelcomeScreen: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const DefaultMainMenu: React.FC<{
|
||||
UIOptions: AppProps["UIOptions"];
|
||||
}> = ({ UIOptions }) => {
|
||||
return (
|
||||
<MainMenu __fallback>
|
||||
<MainMenu.DefaultItems.LoadScene />
|
||||
<MainMenu.DefaultItems.SaveToActiveFile />
|
||||
{/* FIXME we should to test for this inside the item itself */}
|
||||
{UIOptions.canvasActions.export && <MainMenu.DefaultItems.Export />}
|
||||
{/* FIXME we should to test for this inside the item itself */}
|
||||
{UIOptions.canvasActions.saveAsImage && (
|
||||
<MainMenu.DefaultItems.SaveAsImage />
|
||||
)}
|
||||
<MainMenu.DefaultItems.Help />
|
||||
<MainMenu.DefaultItems.ClearCanvas />
|
||||
<MainMenu.Separator />
|
||||
<MainMenu.Group title="Excalidraw links">
|
||||
<MainMenu.DefaultItems.Socials />
|
||||
</MainMenu.Group>
|
||||
<MainMenu.Separator />
|
||||
<MainMenu.DefaultItems.ToggleTheme />
|
||||
<MainMenu.DefaultItems.ChangeCanvasBackground />
|
||||
</MainMenu>
|
||||
);
|
||||
};
|
||||
|
||||
const LayerUI = ({
|
||||
actionManager,
|
||||
appState,
|
||||
|
@ -89,14 +106,12 @@ const LayerUI = ({
|
|||
setAppState,
|
||||
elements,
|
||||
canvas,
|
||||
onCollabButtonClick,
|
||||
onLockToggle,
|
||||
onHandToolToggle,
|
||||
onPenModeToggle,
|
||||
onInsertElements,
|
||||
showExitZenModeBtn,
|
||||
isCollaborating,
|
||||
renderTopRightUI,
|
||||
renderCustomFooter,
|
||||
renderCustomStats,
|
||||
renderCustomSidebar,
|
||||
libraryReturnUrl,
|
||||
|
@ -106,8 +121,10 @@ const LayerUI = ({
|
|||
id,
|
||||
onImageAction,
|
||||
renderWelcomeScreen,
|
||||
children,
|
||||
}: LayerUIProps) => {
|
||||
const device = useDevice();
|
||||
const tunnels = useInitializeTunnels();
|
||||
|
||||
const renderJSONExportDialog = () => {
|
||||
if (!UIOptions.canvasActions.export) {
|
||||
|
@ -122,6 +139,7 @@ const LayerUI = ({
|
|||
actionManager={actionManager}
|
||||
exportOpts={UIOptions.canvasActions.export}
|
||||
canvas={canvas}
|
||||
setAppState={setAppState}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -175,100 +193,12 @@ const LayerUI = ({
|
|||
);
|
||||
};
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useAtom(isMenuOpenAtom);
|
||||
const menuRef = useOutsideClickHook(() => setIsMenuOpen(false));
|
||||
|
||||
const renderCanvasActions = () => (
|
||||
<div style={{ position: "relative" }}>
|
||||
<WelcomeScreenDecor
|
||||
shouldRender={renderWelcomeScreen && !appState.isLoading}
|
||||
>
|
||||
<div className="virgil WelcomeScreen-decor WelcomeScreen-decor--menu-pointer">
|
||||
{WelcomeScreenMenuArrow}
|
||||
<div>{t("welcomeScreen.menuHints")}</div>
|
||||
</div>
|
||||
</WelcomeScreenDecor>
|
||||
|
||||
<button
|
||||
data-prevent-outside-click
|
||||
className={clsx("menu-button", "zen-mode-transition", {
|
||||
"transition-left": appState.zenModeEnabled,
|
||||
})}
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
type="button"
|
||||
data-testid="menu-button"
|
||||
>
|
||||
{HamburgerMenuIcon}
|
||||
</button>
|
||||
|
||||
{isMenuOpen && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
style={{ position: "absolute", top: "100%", marginTop: ".25rem" }}
|
||||
>
|
||||
<Section heading="canvasActions">
|
||||
{/* the zIndex ensures this menu has higher stacking order,
|
||||
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
||||
<Island
|
||||
className="menu-container"
|
||||
padding={2}
|
||||
style={{ zIndex: 1 }}
|
||||
>
|
||||
{!appState.viewModeEnabled &&
|
||||
actionManager.renderAction("loadScene")}
|
||||
{/* // TODO barnabasmolnar/editor-redesign */}
|
||||
{/* is this fine here? */}
|
||||
{appState.fileHandle &&
|
||||
actionManager.renderAction("saveToActiveFile")}
|
||||
{renderJSONExportDialog()}
|
||||
{UIOptions.canvasActions.saveAsImage && (
|
||||
<MenuItem
|
||||
label={t("buttons.exportImage")}
|
||||
icon={ExportImageIcon}
|
||||
dataTestId="image-export-button"
|
||||
onClick={() => setAppState({ openDialog: "imageExport" })}
|
||||
shortcut={getShortcutFromShortcutName("imageExport")}
|
||||
/>
|
||||
)}
|
||||
{onCollabButtonClick && (
|
||||
<CollabButton
|
||||
isCollaborating={isCollaborating}
|
||||
collaboratorCount={appState.collaborators.size}
|
||||
onClick={onCollabButtonClick}
|
||||
/>
|
||||
)}
|
||||
{actionManager.renderAction("toggleShortcuts", undefined, true)}
|
||||
{!appState.viewModeEnabled &&
|
||||
actionManager.renderAction("clearCanvas")}
|
||||
<Separator />
|
||||
<MenuLinks />
|
||||
<Separator />
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
rowGap: ".5rem",
|
||||
}}
|
||||
>
|
||||
<div>{actionManager.renderAction("toggleTheme")}</div>
|
||||
<div style={{ padding: "0 0.625rem" }}>
|
||||
<LanguageList style={{ width: "100%" }} />
|
||||
</div>
|
||||
{!appState.viewModeEnabled && (
|
||||
<div>
|
||||
<div style={{ fontSize: ".75rem", marginBottom: ".5rem" }}>
|
||||
{t("labels.canvasBackground")}
|
||||
</div>
|
||||
<div style={{ padding: "0 0.625rem" }}>
|
||||
{actionManager.renderAction("changeViewBackgroundColor")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Island>
|
||||
</Section>
|
||||
</div>
|
||||
)}
|
||||
{/* wrapping to Fragment stops React from occasionally complaining
|
||||
about identical Keys */}
|
||||
<tunnels.mainMenuTunnel.Out />
|
||||
{renderWelcomeScreen && <tunnels.welcomeScreenMenuHintTunnel.Out />}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
@ -305,9 +235,6 @@ const LayerUI = ({
|
|||
|
||||
return (
|
||||
<FixedSideContainer side="top">
|
||||
{renderWelcomeScreen && !appState.isLoading && (
|
||||
<WelcomeScreen appState={appState} actionManager={actionManager} />
|
||||
)}
|
||||
<div className="App-menu App-menu_top">
|
||||
<Stack.Col
|
||||
gap={6}
|
||||
|
@ -322,17 +249,9 @@ const LayerUI = ({
|
|||
<Section heading="shapes" className="shapes-section">
|
||||
{(heading: React.ReactNode) => (
|
||||
<div style={{ position: "relative" }}>
|
||||
<WelcomeScreenDecor
|
||||
shouldRender={renderWelcomeScreen && !appState.isLoading}
|
||||
>
|
||||
<div className="virgil WelcomeScreen-decor WelcomeScreen-decor--top-toolbar-pointer">
|
||||
<div className="WelcomeScreen-decor--top-toolbar-pointer__label">
|
||||
{t("welcomeScreen.toolbarHints")}
|
||||
</div>
|
||||
{WelcomeScreenTopToolbarArrow}
|
||||
</div>
|
||||
</WelcomeScreenDecor>
|
||||
|
||||
{renderWelcomeScreen && (
|
||||
<tunnels.welcomeScreenToolbarHintTunnel.Out />
|
||||
)}
|
||||
<Stack.Col gap={4} align="start">
|
||||
<Stack.Row
|
||||
gap={1}
|
||||
|
@ -362,13 +281,20 @@ const LayerUI = ({
|
|||
penDetected={appState.penDetected}
|
||||
/>
|
||||
<LockButton
|
||||
zenModeEnabled={appState.zenModeEnabled}
|
||||
checked={appState.activeTool.locked}
|
||||
onChange={() => onLockToggle()}
|
||||
onChange={onLockToggle}
|
||||
title={t("toolBar.lock")}
|
||||
/>
|
||||
|
||||
<div className="App-toolbar__divider"></div>
|
||||
|
||||
<HandButton
|
||||
checked={isHandToolActive(appState)}
|
||||
onChange={() => onHandToolToggle()}
|
||||
title={t("toolBar.hand")}
|
||||
isMobile
|
||||
/>
|
||||
|
||||
<ShapesSwitcher
|
||||
appState={appState}
|
||||
canvas={canvas}
|
||||
|
@ -380,9 +306,6 @@ const LayerUI = ({
|
|||
});
|
||||
}}
|
||||
/>
|
||||
{/* {actionManager.renderAction("eraser", {
|
||||
// size: "small",
|
||||
})} */}
|
||||
</Stack.Row>
|
||||
</Island>
|
||||
</Stack.Row>
|
||||
|
@ -399,18 +322,7 @@ const LayerUI = ({
|
|||
},
|
||||
)}
|
||||
>
|
||||
<UserList
|
||||
collaborators={appState.collaborators}
|
||||
actionManager={actionManager}
|
||||
/>
|
||||
{onCollabButtonClick && (
|
||||
<CollabButton
|
||||
isInHamburgerMenu={false}
|
||||
isCollaborating={isCollaborating}
|
||||
collaboratorCount={appState.collaborators.size}
|
||||
onClick={onCollabButtonClick}
|
||||
/>
|
||||
)}
|
||||
<UserList collaborators={appState.collaborators} />
|
||||
{renderTopRightUI?.(device.isMobile, appState)}
|
||||
{!appState.viewModeEnabled && (
|
||||
<LibraryButton appState={appState} setAppState={setAppState} />
|
||||
|
@ -438,8 +350,18 @@ const LayerUI = ({
|
|||
|
||||
const [hostSidebarCounters] = useAtom(hostSidebarCountersAtom, jotaiScope);
|
||||
|
||||
return (
|
||||
const layerUIJSX = (
|
||||
<>
|
||||
{/* ------------------------- tunneled UI ---------------------------- */}
|
||||
{/* make sure we render host app components first so that we can detect
|
||||
them first on initial render to optimize layout shift */}
|
||||
{children}
|
||||
{/* render component fallbacks. Can be rendered anywhere as they'll be
|
||||
tunneled away. We only render tunneled components that actually
|
||||
have defaults when host do not render anything. */}
|
||||
<DefaultMainMenu UIOptions={UIOptions} />
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
|
||||
{appState.isLoading && <LoadingMessage delay={250} />}
|
||||
{appState.errorMessage && (
|
||||
<ErrorDialog
|
||||
|
@ -454,7 +376,9 @@ const LayerUI = ({
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
<ActiveConfirmDialog />
|
||||
{renderImageExportDialog()}
|
||||
{renderJSONExportDialog()}
|
||||
{appState.pasteDialog.shown && (
|
||||
<PasteChartDialog
|
||||
setAppState={setAppState}
|
||||
|
@ -469,24 +393,22 @@ const LayerUI = ({
|
|||
)}
|
||||
{device.isMobile && (
|
||||
<MobileMenu
|
||||
renderWelcomeScreen={renderWelcomeScreen}
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
actionManager={actionManager}
|
||||
renderJSONExportDialog={renderJSONExportDialog}
|
||||
renderImageExportDialog={renderImageExportDialog}
|
||||
setAppState={setAppState}
|
||||
onCollabButtonClick={onCollabButtonClick}
|
||||
onLockToggle={() => onLockToggle()}
|
||||
onLockToggle={onLockToggle}
|
||||
onHandToolToggle={onHandToolToggle}
|
||||
onPenModeToggle={onPenModeToggle}
|
||||
canvas={canvas}
|
||||
isCollaborating={isCollaborating}
|
||||
renderCustomFooter={renderCustomFooter}
|
||||
onImageAction={onImageAction}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderCustomStats={renderCustomStats}
|
||||
renderSidebars={renderSidebars}
|
||||
device={device}
|
||||
renderWelcomeScreen={renderWelcomeScreen}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
@ -509,13 +431,13 @@ const LayerUI = ({
|
|||
: {}
|
||||
}
|
||||
>
|
||||
{renderWelcomeScreen && <tunnels.welcomeScreenCenterTunnel.Out />}
|
||||
{renderFixedSideContainer()}
|
||||
<Footer
|
||||
renderWelcomeScreen={renderWelcomeScreen}
|
||||
appState={appState}
|
||||
actionManager={actionManager}
|
||||
renderCustomFooter={renderCustomFooter}
|
||||
showExitZenModeBtn={showExitZenModeBtn}
|
||||
renderWelcomeScreen={renderWelcomeScreen}
|
||||
/>
|
||||
{appState.showStats && (
|
||||
<Stats
|
||||
|
@ -546,31 +468,49 @@ const LayerUI = ({
|
|||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const areEqual = (prev: LayerUIProps, next: LayerUIProps) => {
|
||||
const getNecessaryObj = (appState: AppState): Partial<AppState> => {
|
||||
const {
|
||||
suggestedBindings,
|
||||
startBoundElement: boundElement,
|
||||
...ret
|
||||
} = appState;
|
||||
return ret;
|
||||
};
|
||||
const prevAppState = getNecessaryObj(prev.appState);
|
||||
const nextAppState = getNecessaryObj(next.appState);
|
||||
|
||||
const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
|
||||
|
||||
return (
|
||||
prev.renderCustomFooter === next.renderCustomFooter &&
|
||||
prev.renderTopRightUI === next.renderTopRightUI &&
|
||||
prev.renderCustomStats === next.renderCustomStats &&
|
||||
prev.renderCustomSidebar === next.renderCustomSidebar &&
|
||||
prev.langCode === next.langCode &&
|
||||
prev.elements === next.elements &&
|
||||
prev.files === next.files &&
|
||||
keys.every((key) => prevAppState[key] === nextAppState[key])
|
||||
<Provider scope={tunnels.jotaiScope}>
|
||||
<TunnelsContext.Provider value={tunnels}>
|
||||
{layerUIJSX}
|
||||
</TunnelsContext.Provider>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const stripIrrelevantAppStateProps = (
|
||||
appState: AppState,
|
||||
): Partial<AppState> => {
|
||||
const { suggestedBindings, startBoundElement, cursorButton, ...ret } =
|
||||
appState;
|
||||
return ret;
|
||||
};
|
||||
|
||||
const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => {
|
||||
// short-circuit early
|
||||
if (prevProps.children !== nextProps.children) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const {
|
||||
canvas: _prevCanvas,
|
||||
// not stable, but shouldn't matter in our case
|
||||
onInsertElements: _prevOnInsertElements,
|
||||
appState: prevAppState,
|
||||
...prev
|
||||
} = prevProps;
|
||||
const {
|
||||
canvas: _nextCanvas,
|
||||
onInsertElements: _nextOnInsertElements,
|
||||
appState: nextAppState,
|
||||
...next
|
||||
} = nextProps;
|
||||
|
||||
return (
|
||||
isShallowEqual(
|
||||
stripIrrelevantAppStateProps(prevAppState),
|
||||
stripIrrelevantAppStateProps(nextAppState),
|
||||
) && isShallowEqual(prev, next)
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -129,4 +129,27 @@
|
|||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.layer-ui__sidebar__header .dropdown-menu {
|
||||
&.dropdown-menu--mobile {
|
||||
top: 100%;
|
||||
}
|
||||
.dropdown-menu-container {
|
||||
--gap: 0;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
|
||||
:root[dir="rtl"] & {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
width: 196px;
|
||||
box-shadow: var(--library-dropdown-shadow);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,14 +13,15 @@ import {
|
|||
import { ToolButton } from "./ToolButton";
|
||||
import { fileOpen } from "../data/filesystem";
|
||||
import { muteFSAbortError } from "../utils";
|
||||
import { useAtom } from "jotai";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { jotaiScope } from "../jotai";
|
||||
import ConfirmDialog from "./ConfirmDialog";
|
||||
import PublishLibrary from "./PublishLibrary";
|
||||
import { Dialog } from "./Dialog";
|
||||
import { useOutsideClickHook } from "../hooks/useOutsideClick";
|
||||
import MenuItem from "./MenuItem";
|
||||
import { isDropdownOpenAtom } from "./App";
|
||||
|
||||
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
||||
|
||||
export const isLibraryMenuOpenAtom = atom(false);
|
||||
|
||||
const getSelectedItems = (
|
||||
libraryItems: LibraryItems,
|
||||
|
@ -45,7 +46,9 @@ export const LibraryMenuHeader: React.FC<{
|
|||
appState,
|
||||
}) => {
|
||||
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
||||
|
||||
const [isLibraryMenuOpen, setIsLibraryMenuOpen] = useAtom(
|
||||
isLibraryMenuOpenAtom,
|
||||
);
|
||||
const renderRemoveLibAlert = useCallback(() => {
|
||||
const content = selectedItems.length
|
||||
? t("alerts.removeItemsFromsLibrary", { count: selectedItems.length })
|
||||
|
@ -173,85 +176,87 @@ export const LibraryMenuHeader: React.FC<{
|
|||
});
|
||||
};
|
||||
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useAtom(isDropdownOpenAtom);
|
||||
const dropdownRef = useOutsideClickHook(() => setIsDropdownOpen(false));
|
||||
|
||||
const renderLibraryMenu = () => {
|
||||
return (
|
||||
<DropdownMenu open={isLibraryMenuOpen}>
|
||||
<DropdownMenu.Trigger
|
||||
className="Sidebar__dropdown-btn"
|
||||
onToggle={() => setIsLibraryMenuOpen(!isLibraryMenuOpen)}
|
||||
>
|
||||
{DotsIcon}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
onClickOutside={() => setIsLibraryMenuOpen(false)}
|
||||
onSelect={() => setIsLibraryMenuOpen(false)}
|
||||
className="library-menu"
|
||||
>
|
||||
{!itemsSelected && (
|
||||
<DropdownMenu.Item
|
||||
onSelect={onLibraryImport}
|
||||
icon={LoadIcon}
|
||||
data-testid="lib-dropdown--load"
|
||||
>
|
||||
{t("buttons.load")}
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
{!!items.length && (
|
||||
<DropdownMenu.Item
|
||||
onSelect={onLibraryExport}
|
||||
icon={ExportIcon}
|
||||
data-testid="lib-dropdown--export"
|
||||
>
|
||||
{t("buttons.export")}
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
{!!items.length && (
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => setShowRemoveLibAlert(true)}
|
||||
icon={TrashIcon}
|
||||
>
|
||||
{resetLabel}
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
{itemsSelected && (
|
||||
<DropdownMenu.Item
|
||||
icon={publishIcon}
|
||||
onSelect={() => setShowPublishLibraryDialog(true)}
|
||||
data-testid="lib-dropdown--remove"
|
||||
>
|
||||
{t("buttons.publishLibrary")}
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<button
|
||||
type="button"
|
||||
className="Sidebar__dropdown-btn"
|
||||
data-prevent-outside-click
|
||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||
>
|
||||
{DotsIcon}
|
||||
</button>
|
||||
|
||||
{renderLibraryMenu()}
|
||||
{selectedItems.length > 0 && (
|
||||
<div className="library-actions-counter">{selectedItems.length}</div>
|
||||
)}
|
||||
|
||||
{isDropdownOpen && (
|
||||
<div
|
||||
className="Sidebar__dropdown-content menu-container"
|
||||
ref={dropdownRef}
|
||||
>
|
||||
{!itemsSelected && (
|
||||
<MenuItem
|
||||
label={t("buttons.load")}
|
||||
icon={LoadIcon}
|
||||
dataTestId="lib-dropdown--load"
|
||||
onClick={onLibraryImport}
|
||||
/>
|
||||
{showRemoveLibAlert && renderRemoveLibAlert()}
|
||||
{showPublishLibraryDialog && (
|
||||
<PublishLibrary
|
||||
onClose={() => setShowPublishLibraryDialog(false)}
|
||||
libraryItems={getSelectedItems(
|
||||
libraryItemsData.libraryItems,
|
||||
selectedItems,
|
||||
)}
|
||||
{showRemoveLibAlert && renderRemoveLibAlert()}
|
||||
{showPublishLibraryDialog && (
|
||||
<PublishLibrary
|
||||
onClose={() => setShowPublishLibraryDialog(false)}
|
||||
libraryItems={getSelectedItems(
|
||||
libraryItemsData.libraryItems,
|
||||
selectedItems,
|
||||
)}
|
||||
appState={appState}
|
||||
onSuccess={(data) =>
|
||||
onPublishLibSuccess(data, libraryItemsData.libraryItems)
|
||||
}
|
||||
onError={(error) => window.alert(error)}
|
||||
updateItemsInStorage={() =>
|
||||
library.setLibrary(libraryItemsData.libraryItems)
|
||||
}
|
||||
onRemove={(id: string) =>
|
||||
onSelectItems(selectedItems.filter((_id) => _id !== id))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{publishLibSuccess && renderPublishSuccess()}
|
||||
{!!items.length && (
|
||||
<>
|
||||
<MenuItem
|
||||
label={t("buttons.export")}
|
||||
icon={ExportIcon}
|
||||
onClick={onLibraryExport}
|
||||
dataTestId="lib-dropdown--export"
|
||||
/>
|
||||
<MenuItem
|
||||
label={resetLabel}
|
||||
icon={TrashIcon}
|
||||
onClick={() => setShowRemoveLibAlert(true)}
|
||||
dataTestId="lib-dropdown--remove"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{itemsSelected && (
|
||||
<MenuItem
|
||||
label={t("buttons.publishLibrary")}
|
||||
icon={publishIcon}
|
||||
dataTestId="lib-dropdown--publish"
|
||||
onClick={() => setShowPublishLibraryDialog(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
appState={appState}
|
||||
onSuccess={(data) =>
|
||||
onPublishLibSuccess(data, libraryItemsData.libraryItems)
|
||||
}
|
||||
onError={(error) => window.alert(error)}
|
||||
updateItemsInStorage={() =>
|
||||
library.setLibrary(libraryItemsData.libraryItems)
|
||||
}
|
||||
onRemove={(id: string) =>
|
||||
onSelectItems(selectedItems.filter((_id) => _id !== id))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{publishLibSuccess && renderPublishSuccess()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,7 +9,6 @@ type LockIconProps = {
|
|||
name?: string;
|
||||
checked: boolean;
|
||||
onChange?(): void;
|
||||
zenModeEnabled?: boolean;
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.menu-container {
|
||||
background-color: #fff !important;
|
||||
max-height: calc(100vh - 150px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.menu-button {
|
||||
@include outlineButtonStyles;
|
||||
background-color: var(--island-bg-color);
|
||||
width: var(--lg-button-size);
|
||||
height: var(--lg-button-size);
|
||||
|
||||
svg {
|
||||
width: var(--lg-icon-size);
|
||||
height: var(--lg-icon-size);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
align-items: center;
|
||||
padding: 0 0.625rem;
|
||||
height: 2rem;
|
||||
column-gap: 0.625rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-gray-100);
|
||||
cursor: pointer;
|
||||
border-radius: var(--border-radius-md);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-weight: normal;
|
||||
font-family: inherit;
|
||||
|
||||
@media screen and (min-width: 1921px) {
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
&__text {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__shortcut {
|
||||
margin-inline-start: auto;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--button-hover);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.active-collab {
|
||||
background-color: #ecfdf5;
|
||||
color: #064e3c;
|
||||
}
|
||||
}
|
||||
|
||||
&.theme--dark {
|
||||
.menu-item {
|
||||
color: var(--color-gray-40);
|
||||
|
||||
&.active-collab {
|
||||
background-color: #064e3c;
|
||||
color: #ecfdf5;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-container {
|
||||
background-color: var(--color-gray-90) !important;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
import clsx from "clsx";
|
||||
import "./Menu.scss";
|
||||
|
||||
interface MenuProps {
|
||||
icon: JSX.Element;
|
||||
onClick: () => void;
|
||||
label: string;
|
||||
dataTestId: string;
|
||||
shortcut?: string;
|
||||
isCollaborating?: boolean;
|
||||
}
|
||||
|
||||
const MenuItem = ({
|
||||
icon,
|
||||
onClick,
|
||||
label,
|
||||
dataTestId,
|
||||
shortcut,
|
||||
isCollaborating,
|
||||
}: MenuProps) => {
|
||||
return (
|
||||
<button
|
||||
className={clsx("menu-item", { "active-collab": isCollaborating })}
|
||||
aria-label={label}
|
||||
onClick={onClick}
|
||||
data-testid={dataTestId}
|
||||
title={label}
|
||||
type="button"
|
||||
>
|
||||
<div className="menu-item__icon">{icon}</div>
|
||||
<div className="menu-item__text">{label}</div>
|
||||
{shortcut && <div className="menu-item__shortcut">{shortcut}</div>}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuItem;
|
|
@ -1,53 +0,0 @@
|
|||
import { GithubIcon, DiscordIcon, PlusPromoIcon, TwitterIcon } from "./icons";
|
||||
|
||||
export const MenuLinks = () => (
|
||||
<>
|
||||
<a
|
||||
href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="menu-item"
|
||||
style={{ color: "var(--color-promo)" }}
|
||||
>
|
||||
<div className="menu-item__icon">{PlusPromoIcon}</div>
|
||||
<div className="menu-item__text">Excalidraw+</div>
|
||||
</a>
|
||||
<a
|
||||
className="menu-item"
|
||||
href="https://github.com/excalidraw/excalidraw"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="menu-item__icon">{GithubIcon}</div>
|
||||
<div className="menu-item__text">GitHub</div>
|
||||
</a>
|
||||
<a
|
||||
className="menu-item"
|
||||
target="_blank"
|
||||
href="https://discord.gg/UexuTaE"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="menu-item__icon">{DiscordIcon}</div>
|
||||
<div className="menu-item__text">Discord</div>
|
||||
</a>
|
||||
<a
|
||||
className="menu-item"
|
||||
target="_blank"
|
||||
href="https://twitter.com/excalidraw"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="menu-item__icon">{TwitterIcon}</div>
|
||||
<div className="menu-item__text">Twitter</div>
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
|
||||
export const Separator = () => (
|
||||
<div
|
||||
style={{
|
||||
height: "1px",
|
||||
backgroundColor: "var(--default-border-color)",
|
||||
margin: ".5rem 0",
|
||||
}}
|
||||
/>
|
||||
);
|
|
@ -11,18 +11,15 @@ import { HintViewer } from "./HintViewer";
|
|||
import { calculateScrollCenter } from "../scene";
|
||||
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
||||
import { Section } from "./Section";
|
||||
import CollabButton from "./CollabButton";
|
||||
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
||||
import { LockButton } from "./LockButton";
|
||||
import { UserList } from "./UserList";
|
||||
import { LibraryButton } from "./LibraryButton";
|
||||
import { PenModeButton } from "./PenModeButton";
|
||||
import { Stats } from "./Stats";
|
||||
import { actionToggleStats } from "../actions";
|
||||
import { MenuLinks, Separator } from "./MenuUtils";
|
||||
import WelcomeScreen from "./WelcomeScreen";
|
||||
import MenuItem from "./MenuItem";
|
||||
import { ExportImageIcon } from "./icons";
|
||||
import { HandButton } from "./HandButton";
|
||||
import { isHandToolActive } from "../appState";
|
||||
import { useTunnels } from "./context/tunnels";
|
||||
|
||||
type MobileMenuProps = {
|
||||
appState: AppState;
|
||||
|
@ -31,15 +28,11 @@ type MobileMenuProps = {
|
|||
renderImageExportDialog: () => React.ReactNode;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
onCollabButtonClick?: () => void;
|
||||
onLockToggle: () => void;
|
||||
onHandToolToggle: () => void;
|
||||
onPenModeToggle: () => void;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
isCollaborating: boolean;
|
||||
renderCustomFooter?: (
|
||||
isMobile: boolean,
|
||||
appState: AppState,
|
||||
) => JSX.Element | null;
|
||||
|
||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||
renderTopRightUI?: (
|
||||
isMobile: boolean,
|
||||
|
@ -48,22 +41,18 @@ type MobileMenuProps = {
|
|||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||
renderSidebars: () => JSX.Element | null;
|
||||
device: Device;
|
||||
renderWelcomeScreen?: boolean;
|
||||
renderWelcomeScreen: boolean;
|
||||
};
|
||||
|
||||
export const MobileMenu = ({
|
||||
appState,
|
||||
elements,
|
||||
actionManager,
|
||||
renderJSONExportDialog,
|
||||
renderImageExportDialog,
|
||||
setAppState,
|
||||
onCollabButtonClick,
|
||||
onLockToggle,
|
||||
onHandToolToggle,
|
||||
onPenModeToggle,
|
||||
canvas,
|
||||
isCollaborating,
|
||||
renderCustomFooter,
|
||||
onImageAction,
|
||||
renderTopRightUI,
|
||||
renderCustomStats,
|
||||
|
@ -71,12 +60,11 @@ export const MobileMenu = ({
|
|||
device,
|
||||
renderWelcomeScreen,
|
||||
}: MobileMenuProps) => {
|
||||
const { welcomeScreenCenterTunnel, mainMenuTunnel } = useTunnels();
|
||||
const renderToolbar = () => {
|
||||
return (
|
||||
<FixedSideContainer side="top" className="App-top-bar">
|
||||
{renderWelcomeScreen && !appState.isLoading && (
|
||||
<WelcomeScreen appState={appState} actionManager={actionManager} />
|
||||
)}
|
||||
{renderWelcomeScreen && <welcomeScreenCenterTunnel.Out />}
|
||||
<Section heading="shapes">
|
||||
{(heading: React.ReactNode) => (
|
||||
<Stack.Col gap={4} align="center">
|
||||
|
@ -84,20 +72,6 @@ export const MobileMenu = ({
|
|||
<Island padding={1} className="App-toolbar App-toolbar--mobile">
|
||||
{heading}
|
||||
<Stack.Row gap={1}>
|
||||
{/* <PenModeButton
|
||||
checked={appState.penMode}
|
||||
onChange={onPenModeToggle}
|
||||
title={t("toolBar.penMode")}
|
||||
isMobile
|
||||
penDetected={appState.penDetected}
|
||||
/>
|
||||
<LockButton
|
||||
checked={appState.activeTool.locked}
|
||||
onChange={onLockToggle}
|
||||
title={t("toolBar.lock")}
|
||||
isMobile
|
||||
/>
|
||||
<div className="App-toolbar__divider"></div> */}
|
||||
<ShapesSwitcher
|
||||
appState={appState}
|
||||
canvas={canvas}
|
||||
|
@ -113,20 +87,6 @@ export const MobileMenu = ({
|
|||
</Island>
|
||||
{renderTopRightUI && renderTopRightUI(true, appState)}
|
||||
<div className="mobile-misc-tools-container">
|
||||
<PenModeButton
|
||||
checked={appState.penMode}
|
||||
onChange={onPenModeToggle}
|
||||
title={t("toolBar.penMode")}
|
||||
isMobile
|
||||
penDetected={appState.penDetected}
|
||||
// penDetected={true}
|
||||
/>
|
||||
<LockButton
|
||||
checked={appState.activeTool.locked}
|
||||
onChange={onLockToggle}
|
||||
title={t("toolBar.lock")}
|
||||
isMobile
|
||||
/>
|
||||
{!appState.viewModeEnabled && (
|
||||
<LibraryButton
|
||||
appState={appState}
|
||||
|
@ -134,6 +94,25 @@ export const MobileMenu = ({
|
|||
isMobile
|
||||
/>
|
||||
)}
|
||||
<PenModeButton
|
||||
checked={appState.penMode}
|
||||
onChange={onPenModeToggle}
|
||||
title={t("toolBar.penMode")}
|
||||
isMobile
|
||||
penDetected={appState.penDetected}
|
||||
/>
|
||||
<LockButton
|
||||
checked={appState.activeTool.locked}
|
||||
onChange={onLockToggle}
|
||||
title={t("toolBar.lock")}
|
||||
isMobile
|
||||
/>
|
||||
<HandButton
|
||||
checked={isHandToolActive(appState)}
|
||||
onChange={() => onHandToolToggle()}
|
||||
title={t("toolBar.hand")}
|
||||
isMobile
|
||||
/>
|
||||
</div>
|
||||
</Stack.Row>
|
||||
</Stack.Col>
|
||||
|
@ -153,14 +132,14 @@ export const MobileMenu = ({
|
|||
if (appState.viewModeEnabled) {
|
||||
return (
|
||||
<div className="App-toolbar-content">
|
||||
{actionManager.renderAction("toggleCanvasMenu")}
|
||||
<mainMenuTunnel.Out />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="App-toolbar-content">
|
||||
{actionManager.renderAction("toggleCanvasMenu")}
|
||||
<mainMenuTunnel.Out />
|
||||
{actionManager.renderAction("toggleEditMenu")}
|
||||
{actionManager.renderAction("undo")}
|
||||
{actionManager.renderAction("redo")}
|
||||
|
@ -172,58 +151,6 @@ export const MobileMenu = ({
|
|||
);
|
||||
};
|
||||
|
||||
const renderCanvasActions = () => {
|
||||
if (appState.viewModeEnabled) {
|
||||
return (
|
||||
<>
|
||||
{renderJSONExportDialog()}
|
||||
<MenuItem
|
||||
label={t("buttons.exportImage")}
|
||||
icon={ExportImageIcon}
|
||||
dataTestId="image-export-button"
|
||||
onClick={() => setAppState({ openDialog: "imageExport" })}
|
||||
/>
|
||||
{renderImageExportDialog()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{!appState.viewModeEnabled && actionManager.renderAction("loadScene")}
|
||||
{renderJSONExportDialog()}
|
||||
{renderImageExportDialog()}
|
||||
<MenuItem
|
||||
label={t("buttons.exportImage")}
|
||||
icon={ExportImageIcon}
|
||||
dataTestId="image-export-button"
|
||||
onClick={() => setAppState({ openDialog: "imageExport" })}
|
||||
/>
|
||||
{onCollabButtonClick && (
|
||||
<CollabButton
|
||||
isCollaborating={isCollaborating}
|
||||
collaboratorCount={appState.collaborators.size}
|
||||
onClick={onCollabButtonClick}
|
||||
/>
|
||||
)}
|
||||
{actionManager.renderAction("toggleShortcuts", undefined, true)}
|
||||
{!appState.viewModeEnabled && actionManager.renderAction("clearCanvas")}
|
||||
<Separator />
|
||||
<MenuLinks />
|
||||
<Separator />
|
||||
{!appState.viewModeEnabled && (
|
||||
<div style={{ marginBottom: ".5rem" }}>
|
||||
<div style={{ fontSize: ".75rem", marginBottom: ".5rem" }}>
|
||||
{t("labels.canvasBackground")}
|
||||
</div>
|
||||
<div style={{ padding: "0 0.625rem" }}>
|
||||
{actionManager.renderAction("changeViewBackgroundColor")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{actionManager.renderAction("toggleTheme")}
|
||||
</>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{renderSidebars()}
|
||||
|
@ -248,28 +175,9 @@ export const MobileMenu = ({
|
|||
}}
|
||||
>
|
||||
<Island padding={0}>
|
||||
{appState.openMenu === "canvas" ? (
|
||||
<Section className="App-mobile-menu" heading="canvasActions">
|
||||
<div className="panelColumn">
|
||||
<Stack.Col gap={2}>
|
||||
{renderCanvasActions()}
|
||||
{renderCustomFooter?.(true, appState)}
|
||||
{appState.collaborators.size > 0 && (
|
||||
<fieldset>
|
||||
<legend>{t("labels.collaborators")}</legend>
|
||||
<UserList
|
||||
mobile
|
||||
collaborators={appState.collaborators}
|
||||
actionManager={actionManager}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
</Stack.Col>
|
||||
</div>
|
||||
</Section>
|
||||
) : appState.openMenu === "shape" &&
|
||||
!appState.viewModeEnabled &&
|
||||
showSelectedShapeActions(appState, elements) ? (
|
||||
{appState.openMenu === "shape" &&
|
||||
!appState.viewModeEnabled &&
|
||||
showSelectedShapeActions(appState, elements) ? (
|
||||
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
||||
<SelectedShapeActions
|
||||
appState={appState}
|
||||
|
|
|
@ -3,24 +3,6 @@
|
|||
|
||||
.excalidraw {
|
||||
.Sidebar {
|
||||
&__dropdown-content {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
|
||||
:root[dir="rtl"] & {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
margin-top: 0.25rem;
|
||||
width: 180px;
|
||||
box-shadow: var(--library-dropdown-shadow);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
&__close-btn,
|
||||
&__pin-btn,
|
||||
&__dropdown-btn {
|
||||
|
|
|
@ -19,7 +19,7 @@ type ToolButtonBaseProps = {
|
|||
name?: string;
|
||||
id?: string;
|
||||
size?: ToolButtonSize;
|
||||
keyBindingLabel?: string;
|
||||
keyBindingLabel?: string | null;
|
||||
showAriaLabel?: boolean;
|
||||
hidden?: boolean;
|
||||
visible?: boolean;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
|
||||
&:empty {
|
||||
|
|
|
@ -4,16 +4,16 @@ import React from "react";
|
|||
import clsx from "clsx";
|
||||
import { AppState, Collaborator } from "../types";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { useExcalidrawActionManager } from "./App";
|
||||
|
||||
export const UserList: React.FC<{
|
||||
className?: string;
|
||||
mobile?: boolean;
|
||||
collaborators: AppState["collaborators"];
|
||||
actionManager: ActionManager;
|
||||
}> = ({ className, mobile, collaborators, actionManager }) => {
|
||||
const uniqueCollaborators = new Map<string, Collaborator>();
|
||||
}> = ({ className, mobile, collaborators }) => {
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
const uniqueCollaborators = new Map<string, Collaborator>();
|
||||
collaborators.forEach((collaborator, socketId) => {
|
||||
uniqueCollaborators.set(
|
||||
// filter on user id, else fall back on unique socketId
|
||||
|
@ -44,26 +44,6 @@ export const UserList: React.FC<{
|
|||
);
|
||||
});
|
||||
|
||||
// TODO barnabasmolnar/editor-redesign
|
||||
// probably remove before shipping :)
|
||||
// 20 fake collaborators; for easy, convenient debug purposes ˇˇ
|
||||
// const avatars = Array.from({ length: 20 }).map((_, index) => {
|
||||
// const avatarJSX = actionManager.renderAction("goToCollaborator", [
|
||||
// index.toString(),
|
||||
// {
|
||||
// username: `User ${index}`,
|
||||
// },
|
||||
// ]);
|
||||
|
||||
// return mobile ? (
|
||||
// <Tooltip label={`User ${index}`} key={index}>
|
||||
// {avatarJSX}
|
||||
// </Tooltip>
|
||||
// ) : (
|
||||
// <React.Fragment key={index}>{avatarJSX}</React.Fragment>
|
||||
// );
|
||||
// });
|
||||
|
||||
return (
|
||||
<div className={clsx("UserList", className, { UserList_mobile: mobile })}>
|
||||
{avatars}
|
||||
|
|
|
@ -1,141 +0,0 @@
|
|||
import { useAtom } from "jotai";
|
||||
import { actionLoadScene, actionShortcuts } from "../actions";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
||||
import { COOKIES } from "../constants";
|
||||
import { collabDialogShownAtom } from "../excalidraw-app/collab/Collab";
|
||||
import { t } from "../i18n";
|
||||
import { AppState } from "../types";
|
||||
import {
|
||||
ExcalLogo,
|
||||
HelpIcon,
|
||||
LoadIcon,
|
||||
PlusPromoIcon,
|
||||
UsersIcon,
|
||||
} from "./icons";
|
||||
import "./WelcomeScreen.scss";
|
||||
|
||||
const isExcalidrawPlusSignedUser = document.cookie.includes(
|
||||
COOKIES.AUTH_STATE_COOKIE,
|
||||
);
|
||||
|
||||
const WelcomeScreenItem = ({
|
||||
label,
|
||||
shortcut,
|
||||
onClick,
|
||||
icon,
|
||||
link,
|
||||
}: {
|
||||
label: string;
|
||||
shortcut: string | null;
|
||||
onClick?: () => void;
|
||||
icon: JSX.Element;
|
||||
link?: string;
|
||||
}) => {
|
||||
if (link) {
|
||||
return (
|
||||
<a
|
||||
className="WelcomeScreen-item"
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className="WelcomeScreen-item__label">
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button className="WelcomeScreen-item" type="button" onClick={onClick}>
|
||||
<div className="WelcomeScreen-item__label">
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
{shortcut && (
|
||||
<div className="WelcomeScreen-item__shortcut">{shortcut}</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const WelcomeScreen = ({
|
||||
appState,
|
||||
actionManager,
|
||||
}: {
|
||||
appState: AppState;
|
||||
actionManager: ActionManager;
|
||||
}) => {
|
||||
const [, setCollabDialogShown] = useAtom(collabDialogShownAtom);
|
||||
|
||||
let subheadingJSX;
|
||||
|
||||
if (isExcalidrawPlusSignedUser) {
|
||||
subheadingJSX = t("welcomeScreen.switchToPlusApp")
|
||||
.split(/(Excalidraw\+)/)
|
||||
.map((bit, idx) => {
|
||||
if (bit === "Excalidraw+") {
|
||||
return (
|
||||
<a
|
||||
style={{ pointerEvents: "all" }}
|
||||
href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
|
||||
key={idx}
|
||||
>
|
||||
Excalidraw+
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return bit;
|
||||
});
|
||||
} else {
|
||||
subheadingJSX = t("welcomeScreen.data");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="WelcomeScreen-container">
|
||||
<div className="WelcomeScreen-logo virgil WelcomeScreen-decor">
|
||||
{ExcalLogo} Excalidraw
|
||||
</div>
|
||||
<div className="virgil WelcomeScreen-decor WelcomeScreen-decor--subheading">
|
||||
{subheadingJSX}
|
||||
</div>
|
||||
<div className="WelcomeScreen-items">
|
||||
{!appState.viewModeEnabled && (
|
||||
<WelcomeScreenItem
|
||||
// TODO barnabasmolnar/editor-redesign
|
||||
// do we want the internationalized labels here that are currently
|
||||
// in use elsewhere or new ones?
|
||||
label={t("buttons.load")}
|
||||
onClick={() => actionManager.executeAction(actionLoadScene)}
|
||||
shortcut={getShortcutFromShortcutName("loadScene")}
|
||||
icon={LoadIcon}
|
||||
/>
|
||||
)}
|
||||
<WelcomeScreenItem
|
||||
label={t("labels.liveCollaboration")}
|
||||
shortcut={null}
|
||||
onClick={() => setCollabDialogShown(true)}
|
||||
icon={UsersIcon}
|
||||
/>
|
||||
<WelcomeScreenItem
|
||||
onClick={() => actionManager.executeAction(actionShortcuts)}
|
||||
label={t("helpDialog.title")}
|
||||
shortcut="?"
|
||||
icon={HelpIcon}
|
||||
/>
|
||||
{!isExcalidrawPlusSignedUser && (
|
||||
<WelcomeScreenItem
|
||||
link="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
||||
label="Try Excalidraw Plus!"
|
||||
shortcut={null}
|
||||
icon={PlusPromoIcon}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WelcomeScreen;
|
|
@ -1,11 +0,0 @@
|
|||
import { ReactNode } from "react";
|
||||
|
||||
const WelcomeScreenDecor = ({
|
||||
children,
|
||||
shouldRender,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
shouldRender: boolean;
|
||||
}) => (shouldRender ? <>{children}</> : null);
|
||||
|
||||
export default WelcomeScreenDecor;
|
32
src/components/context/tunnels.ts
Normal file
32
src/components/context/tunnels.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import React from "react";
|
||||
import tunnel from "@dwelle/tunnel-rat";
|
||||
|
||||
type Tunnel = ReturnType<typeof tunnel>;
|
||||
|
||||
type TunnelsContextValue = {
|
||||
mainMenuTunnel: Tunnel;
|
||||
welcomeScreenMenuHintTunnel: Tunnel;
|
||||
welcomeScreenToolbarHintTunnel: Tunnel;
|
||||
welcomeScreenHelpHintTunnel: Tunnel;
|
||||
welcomeScreenCenterTunnel: Tunnel;
|
||||
footerCenterTunnel: Tunnel;
|
||||
jotaiScope: symbol;
|
||||
};
|
||||
|
||||
export const TunnelsContext = React.createContext<TunnelsContextValue>(null!);
|
||||
|
||||
export const useTunnels = () => React.useContext(TunnelsContext);
|
||||
|
||||
export const useInitializeTunnels = () => {
|
||||
return React.useMemo((): TunnelsContextValue => {
|
||||
return {
|
||||
mainMenuTunnel: tunnel(),
|
||||
welcomeScreenMenuHintTunnel: tunnel(),
|
||||
welcomeScreenToolbarHintTunnel: tunnel(),
|
||||
welcomeScreenHelpHintTunnel: tunnel(),
|
||||
welcomeScreenCenterTunnel: tunnel(),
|
||||
footerCenterTunnel: tunnel(),
|
||||
jotaiScope: Symbol(),
|
||||
};
|
||||
}, []);
|
||||
};
|
127
src/components/dropdownMenu/DropdownMenu.scss
Normal file
127
src/components/dropdownMenu/DropdownMenu.scss
Normal file
|
@ -0,0 +1,127 @@
|
|||
@import "../../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
margin-top: 0.25rem;
|
||||
|
||||
&--mobile {
|
||||
bottom: 55px;
|
||||
top: auto;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 0.75rem;
|
||||
|
||||
.dropdown-menu-container {
|
||||
padding: 8px 8px;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--island-bg-color);
|
||||
box-shadow: var(--shadow-island);
|
||||
border-radius: var(--border-radius-lg);
|
||||
position: relative;
|
||||
transition: box-shadow 0.5s ease-in-out;
|
||||
|
||||
&.zen-mode {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-container {
|
||||
background-color: #fff !important;
|
||||
max-height: calc(100vh - 150px);
|
||||
overflow-y: auto;
|
||||
--gap: 2;
|
||||
}
|
||||
|
||||
.dropdown-menu-item-base {
|
||||
display: flex;
|
||||
padding: 0 0.625rem;
|
||||
column-gap: 0.625rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-gray-100);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-weight: normal;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.dropdown-menu-item {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
cursor: pointer;
|
||||
border-radius: var(--border-radius-md);
|
||||
|
||||
@media screen and (min-width: 1921px) {
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
&__text {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__shortcut {
|
||||
margin-inline-start: auto;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--button-hover-bg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-item-custom {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-menu-group-title {
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
margin: 10px 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
&.theme--dark {
|
||||
.dropdown-menu-item {
|
||||
color: var(--color-gray-40);
|
||||
}
|
||||
|
||||
.dropdown-menu-container {
|
||||
background-color: var(--color-gray-90) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-button {
|
||||
@include outlineButtonStyles;
|
||||
background-color: var(--island-bg-color);
|
||||
width: var(--lg-button-size);
|
||||
height: var(--lg-button-size);
|
||||
|
||||
svg {
|
||||
width: var(--lg-icon-size);
|
||||
height: var(--lg-icon-size);
|
||||
}
|
||||
|
||||
&--mobile {
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: var(--default-button-size);
|
||||
height: var(--default-button-size);
|
||||
}
|
||||
}
|
||||
}
|
43
src/components/dropdownMenu/DropdownMenu.tsx
Normal file
43
src/components/dropdownMenu/DropdownMenu.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import React from "react";
|
||||
import DropdownMenuTrigger from "./DropdownMenuTrigger";
|
||||
import DropdownMenuItem from "./DropdownMenuItem";
|
||||
import MenuSeparator from "./DropdownMenuSeparator";
|
||||
import DropdownMenuGroup from "./DropdownMenuGroup";
|
||||
import DropdownMenuContent from "./DropdownMenuContent";
|
||||
import DropdownMenuItemLink from "./DropdownMenuItemLink";
|
||||
import DropdownMenuItemCustom from "./DropdownMenuItemCustom";
|
||||
import {
|
||||
getMenuContentComponent,
|
||||
getMenuTriggerComponent,
|
||||
} from "./dropdownMenuUtils";
|
||||
|
||||
import "./DropdownMenu.scss";
|
||||
|
||||
const DropdownMenu = ({
|
||||
children,
|
||||
open,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
open: boolean;
|
||||
}) => {
|
||||
const MenuTriggerComp = getMenuTriggerComponent(children);
|
||||
const MenuContentComp = getMenuContentComponent(children);
|
||||
return (
|
||||
<>
|
||||
{MenuTriggerComp}
|
||||
{open && MenuContentComp}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
DropdownMenu.Trigger = DropdownMenuTrigger;
|
||||
DropdownMenu.Content = DropdownMenuContent;
|
||||
DropdownMenu.Item = DropdownMenuItem;
|
||||
DropdownMenu.ItemLink = DropdownMenuItemLink;
|
||||
DropdownMenu.ItemCustom = DropdownMenuItemCustom;
|
||||
DropdownMenu.Group = DropdownMenuGroup;
|
||||
DropdownMenu.Separator = MenuSeparator;
|
||||
|
||||
export default DropdownMenu;
|
||||
|
||||
DropdownMenu.displayName = "DropdownMenu";
|
62
src/components/dropdownMenu/DropdownMenuContent.tsx
Normal file
62
src/components/dropdownMenu/DropdownMenuContent.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { useOutsideClickHook } from "../../hooks/useOutsideClick";
|
||||
import { Island } from "../Island";
|
||||
|
||||
import { useDevice } from "../App";
|
||||
import clsx from "clsx";
|
||||
import Stack from "../Stack";
|
||||
import React from "react";
|
||||
import { DropdownMenuContentPropsContext } from "./common";
|
||||
|
||||
const MenuContent = ({
|
||||
children,
|
||||
onClickOutside,
|
||||
className = "",
|
||||
onSelect,
|
||||
style,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
onClickOutside?: () => void;
|
||||
className?: string;
|
||||
/**
|
||||
* Called when any menu item is selected (clicked on).
|
||||
*/
|
||||
onSelect?: (event: Event) => void;
|
||||
style?: React.CSSProperties;
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
const menuRef = useOutsideClickHook(() => {
|
||||
onClickOutside?.();
|
||||
});
|
||||
|
||||
const classNames = clsx(`dropdown-menu ${className}`, {
|
||||
"dropdown-menu--mobile": device.isMobile,
|
||||
}).trim();
|
||||
|
||||
return (
|
||||
<DropdownMenuContentPropsContext.Provider value={{ onSelect }}>
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={classNames}
|
||||
style={style}
|
||||
data-testid="dropdown-menu"
|
||||
>
|
||||
{/* the zIndex ensures this menu has higher stacking order,
|
||||
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
||||
{device.isMobile ? (
|
||||
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
|
||||
) : (
|
||||
<Island
|
||||
className="dropdown-menu-container"
|
||||
padding={2}
|
||||
style={{ zIndex: 1 }}
|
||||
>
|
||||
{children}
|
||||
</Island>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContentPropsContext.Provider>
|
||||
);
|
||||
};
|
||||
MenuContent.displayName = "DropdownMenuContent";
|
||||
|
||||
export default MenuContent;
|
23
src/components/dropdownMenu/DropdownMenuGroup.tsx
Normal file
23
src/components/dropdownMenu/DropdownMenuGroup.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import React from "react";
|
||||
|
||||
const MenuGroup = ({
|
||||
children,
|
||||
className = "",
|
||||
style,
|
||||
title,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
title?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className={`dropdown-menu-group ${className}`} style={style}>
|
||||
{title && <p className="dropdown-menu-group-title">{title}</p>}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuGroup;
|
||||
MenuGroup.displayName = "DropdownMenuGroup";
|
40
src/components/dropdownMenu/DropdownMenuItem.tsx
Normal file
40
src/components/dropdownMenu/DropdownMenuItem.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import React from "react";
|
||||
import {
|
||||
getDropdownMenuItemClassName,
|
||||
useHandleDropdownMenuItemClick,
|
||||
} from "./common";
|
||||
import MenuItemContent from "./DropdownMenuItemContent";
|
||||
|
||||
const DropdownMenuItem = ({
|
||||
icon,
|
||||
onSelect,
|
||||
children,
|
||||
shortcut,
|
||||
className,
|
||||
...rest
|
||||
}: {
|
||||
icon?: JSX.Element;
|
||||
onSelect: (event: Event) => void;
|
||||
children: React.ReactNode;
|
||||
shortcut?: string;
|
||||
className?: string;
|
||||
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
|
||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||
|
||||
return (
|
||||
<button
|
||||
{...rest}
|
||||
onClick={handleClick}
|
||||
type="button"
|
||||
className={getDropdownMenuItemClassName(className)}
|
||||
title={rest.title ?? rest["aria-label"]}
|
||||
>
|
||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||
{children}
|
||||
</MenuItemContent>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownMenuItem;
|
||||
DropdownMenuItem.displayName = "DropdownMenuItem";
|
23
src/components/dropdownMenu/DropdownMenuItemContent.tsx
Normal file
23
src/components/dropdownMenu/DropdownMenuItemContent.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { useDevice } from "../App";
|
||||
|
||||
const MenuItemContent = ({
|
||||
icon,
|
||||
shortcut,
|
||||
children,
|
||||
}: {
|
||||
icon?: JSX.Element;
|
||||
shortcut?: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
return (
|
||||
<>
|
||||
<div className="dropdown-menu-item__icon">{icon}</div>
|
||||
<div className="dropdown-menu-item__text">{children}</div>
|
||||
{shortcut && !device.isMobile && (
|
||||
<div className="dropdown-menu-item__shortcut">{shortcut}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default MenuItemContent;
|
21
src/components/dropdownMenu/DropdownMenuItemCustom.tsx
Normal file
21
src/components/dropdownMenu/DropdownMenuItemCustom.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import React from "react";
|
||||
|
||||
const DropdownMenuItemCustom = ({
|
||||
children,
|
||||
className = "",
|
||||
...rest
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
} & React.HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
className={`dropdown-menu-item-base dropdown-menu-item-custom ${className}`.trim()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownMenuItemCustom;
|
44
src/components/dropdownMenu/DropdownMenuItemLink.tsx
Normal file
44
src/components/dropdownMenu/DropdownMenuItemLink.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import MenuItemContent from "./DropdownMenuItemContent";
|
||||
import React from "react";
|
||||
import {
|
||||
getDropdownMenuItemClassName,
|
||||
useHandleDropdownMenuItemClick,
|
||||
} from "./common";
|
||||
|
||||
const DropdownMenuItemLink = ({
|
||||
icon,
|
||||
shortcut,
|
||||
href,
|
||||
children,
|
||||
onSelect,
|
||||
className = "",
|
||||
...rest
|
||||
}: {
|
||||
href: string;
|
||||
icon?: JSX.Element;
|
||||
children: React.ReactNode;
|
||||
shortcut?: string;
|
||||
className?: string;
|
||||
onSelect?: (event: Event) => void;
|
||||
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||
|
||||
return (
|
||||
<a
|
||||
{...rest}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={getDropdownMenuItemClassName(className)}
|
||||
title={rest.title ?? rest["aria-label"]}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||
{children}
|
||||
</MenuItemContent>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownMenuItemLink;
|
||||
DropdownMenuItemLink.displayName = "DropdownMenuItemLink";
|
14
src/components/dropdownMenu/DropdownMenuSeparator.tsx
Normal file
14
src/components/dropdownMenu/DropdownMenuSeparator.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import React from "react";
|
||||
|
||||
const MenuSeparator = () => (
|
||||
<div
|
||||
style={{
|
||||
height: "1px",
|
||||
backgroundColor: "var(--default-border-color)",
|
||||
margin: ".5rem 0",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export default MenuSeparator;
|
||||
MenuSeparator.displayName = "DropdownMenuSeparator";
|
37
src/components/dropdownMenu/DropdownMenuTrigger.tsx
Normal file
37
src/components/dropdownMenu/DropdownMenuTrigger.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import clsx from "clsx";
|
||||
import { useDevice, useExcalidrawAppState } from "../App";
|
||||
|
||||
const MenuTrigger = ({
|
||||
className = "",
|
||||
children,
|
||||
onToggle,
|
||||
}: {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
onToggle: () => void;
|
||||
}) => {
|
||||
const appState = useExcalidrawAppState();
|
||||
const device = useDevice();
|
||||
const classNames = clsx(
|
||||
`dropdown-menu-button ${className}`,
|
||||
"zen-mode-transition",
|
||||
{
|
||||
"transition-left": appState.zenModeEnabled,
|
||||
"dropdown-menu-button--mobile": device.isMobile,
|
||||
},
|
||||
).trim();
|
||||
return (
|
||||
<button
|
||||
data-prevent-outside-click
|
||||
className={classNames}
|
||||
onClick={onToggle}
|
||||
type="button"
|
||||
data-testid="dropdown-menu-button"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuTrigger;
|
||||
MenuTrigger.displayName = "DropdownMenuTrigger";
|
31
src/components/dropdownMenu/common.ts
Normal file
31
src/components/dropdownMenu/common.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React, { useContext } from "react";
|
||||
import { EVENT } from "../../constants";
|
||||
import { composeEventHandlers } from "../../utils";
|
||||
|
||||
export const DropdownMenuContentPropsContext = React.createContext<{
|
||||
onSelect?: (event: Event) => void;
|
||||
}>({});
|
||||
|
||||
export const getDropdownMenuItemClassName = (className = "") => {
|
||||
return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim();
|
||||
};
|
||||
|
||||
export const useHandleDropdownMenuItemClick = (
|
||||
origOnClick:
|
||||
| React.MouseEventHandler<HTMLAnchorElement | HTMLButtonElement>
|
||||
| undefined,
|
||||
onSelect: ((event: Event) => void) | undefined,
|
||||
) => {
|
||||
const DropdownMenuContentProps = useContext(DropdownMenuContentPropsContext);
|
||||
|
||||
return composeEventHandlers(origOnClick, (event) => {
|
||||
const itemSelectEvent = new CustomEvent(EVENT.MENU_ITEM_SELECT, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
onSelect?.(itemSelectEvent);
|
||||
if (!itemSelectEvent.defaultPrevented) {
|
||||
DropdownMenuContentProps.onSelect?.(itemSelectEvent);
|
||||
}
|
||||
});
|
||||
};
|
35
src/components/dropdownMenu/dropdownMenuUtils.ts
Normal file
35
src/components/dropdownMenu/dropdownMenuUtils.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import React from "react";
|
||||
|
||||
export const getMenuTriggerComponent = (children: React.ReactNode) => {
|
||||
const comp = React.Children.toArray(children).find(
|
||||
(child) =>
|
||||
React.isValidElement(child) &&
|
||||
typeof child.type !== "string" &&
|
||||
//@ts-ignore
|
||||
child?.type.displayName &&
|
||||
//@ts-ignore
|
||||
child.type.displayName === "DropdownMenuTrigger",
|
||||
);
|
||||
if (!comp) {
|
||||
return null;
|
||||
}
|
||||
//@ts-ignore
|
||||
return comp;
|
||||
};
|
||||
|
||||
export const getMenuContentComponent = (children: React.ReactNode) => {
|
||||
const comp = React.Children.toArray(children).find(
|
||||
(child) =>
|
||||
React.isValidElement(child) &&
|
||||
typeof child.type !== "string" &&
|
||||
//@ts-ignore
|
||||
child?.type.displayName &&
|
||||
//@ts-ignore
|
||||
child.type.displayName === "DropdownMenuContent",
|
||||
);
|
||||
if (!comp) {
|
||||
return null;
|
||||
}
|
||||
//@ts-ignore
|
||||
return comp;
|
||||
};
|
|
@ -1,35 +1,36 @@
|
|||
import clsx from "clsx";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { t } from "../i18n";
|
||||
import { AppState, ExcalidrawProps } from "../types";
|
||||
import { actionShortcuts } from "../../actions";
|
||||
import { ActionManager } from "../../actions/manager";
|
||||
import { AppState } from "../../types";
|
||||
import {
|
||||
ExitZenModeAction,
|
||||
FinalizeAction,
|
||||
UndoRedoActions,
|
||||
ZoomActions,
|
||||
} from "./Actions";
|
||||
import { useDevice } from "./App";
|
||||
import { WelcomeScreenHelpArrow } from "./icons";
|
||||
import { Section } from "./Section";
|
||||
import Stack from "./Stack";
|
||||
import WelcomeScreenDecor from "./WelcomeScreenDecor";
|
||||
} from "../Actions";
|
||||
import { useDevice } from "../App";
|
||||
import { useTunnels } from "../context/tunnels";
|
||||
import { HelpButton } from "../HelpButton";
|
||||
import { Section } from "../Section";
|
||||
import Stack from "../Stack";
|
||||
|
||||
const Footer = ({
|
||||
appState,
|
||||
actionManager,
|
||||
renderCustomFooter,
|
||||
showExitZenModeBtn,
|
||||
renderWelcomeScreen,
|
||||
}: {
|
||||
appState: AppState;
|
||||
actionManager: ActionManager;
|
||||
renderCustomFooter?: ExcalidrawProps["renderFooter"];
|
||||
showExitZenModeBtn: boolean;
|
||||
renderWelcomeScreen: boolean;
|
||||
}) => {
|
||||
const { footerCenterTunnel, welcomeScreenHelpHintTunnel } = useTunnels();
|
||||
|
||||
const device = useDevice();
|
||||
const showFinalize =
|
||||
!appState.viewModeEnabled && appState.multiElement && device.isTouchScreen;
|
||||
|
||||
return (
|
||||
<footer
|
||||
role="contentinfo"
|
||||
|
@ -69,33 +70,17 @@ const Footer = ({
|
|||
</Section>
|
||||
</Stack.Col>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
"layer-ui__wrapper__footer-center zen-mode-transition",
|
||||
{
|
||||
"layer-ui__wrapper__footer-left--transition-bottom":
|
||||
appState.zenModeEnabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{renderCustomFooter?.(false, appState)}
|
||||
</div>
|
||||
<footerCenterTunnel.Out />
|
||||
<div
|
||||
className={clsx("layer-ui__wrapper__footer-right zen-mode-transition", {
|
||||
"transition-right disable-pointerEvents": appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
<div style={{ position: "relative" }}>
|
||||
<WelcomeScreenDecor
|
||||
shouldRender={renderWelcomeScreen && !appState.isLoading}
|
||||
>
|
||||
<div className="virgil WelcomeScreen-decor WelcomeScreen-decor--help-pointer">
|
||||
<div>{t("welcomeScreen.helpHints")}</div>
|
||||
{WelcomeScreenHelpArrow}
|
||||
</div>
|
||||
</WelcomeScreenDecor>
|
||||
|
||||
{actionManager.renderAction("toggleShortcuts")}
|
||||
{renderWelcomeScreen && <welcomeScreenHelpHintTunnel.Out />}
|
||||
<HelpButton
|
||||
onClick={() => actionManager.executeAction(actionShortcuts)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ExitZenModeAction
|
||||
|
@ -107,3 +92,4 @@ const Footer = ({
|
|||
};
|
||||
|
||||
export default Footer;
|
||||
Footer.displayName = "Footer";
|
10
src/components/footer/FooterCenter.scss
Normal file
10
src/components/footer/FooterCenter.scss
Normal file
|
@ -0,0 +1,10 @@
|
|||
.footer-center {
|
||||
pointer-events: none;
|
||||
& > * {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
24
src/components/footer/FooterCenter.tsx
Normal file
24
src/components/footer/FooterCenter.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import clsx from "clsx";
|
||||
import { useExcalidrawAppState } from "../App";
|
||||
import { useTunnels } from "../context/tunnels";
|
||||
import "./FooterCenter.scss";
|
||||
|
||||
const FooterCenter = ({ children }: { children?: React.ReactNode }) => {
|
||||
const { footerCenterTunnel } = useTunnels();
|
||||
const appState = useExcalidrawAppState();
|
||||
return (
|
||||
<footerCenterTunnel.In>
|
||||
<div
|
||||
className={clsx("footer-center zen-mode-transition", {
|
||||
"layer-ui__wrapper__footer-left--transition-bottom":
|
||||
appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</footerCenterTunnel.In>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterCenter;
|
||||
FooterCenter.displayName = "FooterCenter";
|
52
src/components/hoc/withInternalFallback.tsx
Normal file
52
src/components/hoc/withInternalFallback.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { atom, useAtom } from "jotai";
|
||||
import React, { useLayoutEffect } from "react";
|
||||
import { useTunnels } from "../context/tunnels";
|
||||
|
||||
export const withInternalFallback = <P,>(
|
||||
componentName: string,
|
||||
Component: React.FC<P>,
|
||||
) => {
|
||||
const counterAtom = atom(0);
|
||||
// flag set on initial render to tell the fallback component to skip the
|
||||
// render until mount counter are initialized. This is because the counter
|
||||
// is initialized in an effect, and thus we could end rendering both
|
||||
// components at the same time until counter is initialized.
|
||||
let preferHost = false;
|
||||
|
||||
const WrapperComponent: React.FC<
|
||||
P & {
|
||||
__fallback?: boolean;
|
||||
}
|
||||
> = (props) => {
|
||||
const { jotaiScope } = useTunnels();
|
||||
const [counter, setCounter] = useAtom(counterAtom, jotaiScope);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setCounter((counter) => counter + 1);
|
||||
return () => {
|
||||
setCounter((counter) => counter - 1);
|
||||
};
|
||||
}, [setCounter]);
|
||||
|
||||
if (!props.__fallback) {
|
||||
preferHost = true;
|
||||
}
|
||||
|
||||
// ensure we don't render fallback and host components at the same time
|
||||
if (
|
||||
// either before the counters are initialized
|
||||
(!counter && props.__fallback && preferHost) ||
|
||||
// or after the counters are initialized, and both are rendered
|
||||
// (this is the default when host renders as well)
|
||||
(counter > 1 && props.__fallback)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Component {...props} />;
|
||||
};
|
||||
|
||||
WrapperComponent.displayName = componentName;
|
||||
|
||||
return WrapperComponent;
|
||||
};
|
|
@ -883,7 +883,7 @@ export const CenterHorizontallyIcon = createIcon(
|
|||
modifiedTablerIconProps,
|
||||
);
|
||||
|
||||
export const UsersIcon = createIcon(
|
||||
export const usersIcon = createIcon(
|
||||
<g strokeWidth="1.5">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
|
@ -1532,3 +1532,14 @@ export const publishIcon = createIcon(
|
|||
export const eraser = createIcon(
|
||||
<path d="M480 416C497.7 416 512 430.3 512 448C512 465.7 497.7 480 480 480H150.6C133.7 480 117.4 473.3 105.4 461.3L25.37 381.3C.3786 356.3 .3786 315.7 25.37 290.7L258.7 57.37C283.7 32.38 324.3 32.38 349.3 57.37L486.6 194.7C511.6 219.7 511.6 260.3 486.6 285.3L355.9 416H480zM265.4 416L332.7 348.7L195.3 211.3L70.63 336L150.6 416L265.4 416z" />,
|
||||
);
|
||||
|
||||
export const handIcon = createIcon(
|
||||
<g strokeWidth={1.25}>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M8 13v-7.5a1.5 1.5 0 0 1 3 0v6.5"></path>
|
||||
<path d="M11 5.5v-2a1.5 1.5 0 1 1 3 0v8.5"></path>
|
||||
<path d="M14 5.5a1.5 1.5 0 0 1 3 0v6.5"></path>
|
||||
<path d="M17 7.5a1.5 1.5 0 0 1 3 0v8.5a6 6 0 0 1 -6 6h-2h.208a6 6 0 0 1 -5.012 -2.7a69.74 69.74 0 0 1 -.196 -.3c-.312 -.479 -1.407 -2.388 -3.286 -5.728a1.5 1.5 0 0 1 .536 -2.022a1.867 1.867 0 0 1 2.28 .28l1.47 1.47"></path>
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
|
|
@ -1,30 +1,23 @@
|
|||
@import "../css/variables.module";
|
||||
@import "../../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.collab-button {
|
||||
@include outlineButtonStyles;
|
||||
width: var(--lg-button-size);
|
||||
height: var(--lg-button-size);
|
||||
--button-bg: var(--color-primary);
|
||||
--button-color: white;
|
||||
--button-border: var(--color-primary);
|
||||
|
||||
--button-width: var(--lg-button-size);
|
||||
--button-height: var(--lg-button-size);
|
||||
|
||||
--button-hover-bg: var(--color-primary-darker);
|
||||
--button-hover-border: var(--color-primary-darker);
|
||||
|
||||
--button-active-bg: var(--color-primary-darker);
|
||||
|
||||
svg {
|
||||
width: var(--lg-icon-size);
|
||||
height: var(--lg-icon-size);
|
||||
}
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-darker);
|
||||
border-color: var(--color-primary-darker);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--color-primary-darker);
|
||||
}
|
||||
|
||||
&.active {
|
||||
// double .active to force specificity
|
||||
&.active.active {
|
||||
background-color: #0fb884;
|
||||
border-color: #0fb884;
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { t } from "../../i18n";
|
||||
import { usersIcon } from "../icons";
|
||||
import { Button } from "../Button";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { useExcalidrawAppState } from "../App";
|
||||
|
||||
import "./LiveCollaborationTrigger.scss";
|
||||
|
||||
const LiveCollaborationTrigger = ({
|
||||
isCollaborating,
|
||||
onSelect,
|
||||
...rest
|
||||
}: {
|
||||
isCollaborating: boolean;
|
||||
onSelect: () => void;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
const appState = useExcalidrawAppState();
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...rest}
|
||||
className={clsx("collab-button", { active: isCollaborating })}
|
||||
type="button"
|
||||
onSelect={onSelect}
|
||||
style={{ position: "relative" }}
|
||||
title={t("labels.liveCollaboration")}
|
||||
>
|
||||
{usersIcon}
|
||||
{appState.collaborators.size > 0 && (
|
||||
<div className="CollabButton-collaborators">
|
||||
{appState.collaborators.size}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default LiveCollaborationTrigger;
|
||||
LiveCollaborationTrigger.displayName = "LiveCollaborationTrigger";
|
257
src/components/main-menu/DefaultItems.tsx
Normal file
257
src/components/main-menu/DefaultItems.tsx
Normal file
|
@ -0,0 +1,257 @@
|
|||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||
import { useI18n } from "../../i18n";
|
||||
import {
|
||||
useExcalidrawAppState,
|
||||
useExcalidrawSetAppState,
|
||||
useExcalidrawActionManager,
|
||||
} from "../App";
|
||||
import {
|
||||
ExportIcon,
|
||||
ExportImageIcon,
|
||||
HelpIcon,
|
||||
LoadIcon,
|
||||
MoonIcon,
|
||||
save,
|
||||
SunIcon,
|
||||
TrashIcon,
|
||||
usersIcon,
|
||||
} from "../icons";
|
||||
import { GithubIcon, DiscordIcon, TwitterIcon } from "../icons";
|
||||
import DropdownMenuItem from "../dropdownMenu/DropdownMenuItem";
|
||||
import DropdownMenuItemLink from "../dropdownMenu/DropdownMenuItemLink";
|
||||
import {
|
||||
actionClearCanvas,
|
||||
actionLoadScene,
|
||||
actionSaveToActiveFile,
|
||||
actionShortcuts,
|
||||
actionToggleTheme,
|
||||
} from "../../actions";
|
||||
|
||||
import "./DefaultItems.scss";
|
||||
import clsx from "clsx";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
||||
|
||||
export const LoadScene = () => {
|
||||
const { t } = useI18n();
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
if (!actionManager.isActionEnabled(actionLoadScene)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
icon={LoadIcon}
|
||||
onSelect={() => actionManager.executeAction(actionLoadScene)}
|
||||
data-testid="load-button"
|
||||
shortcut={getShortcutFromShortcutName("loadScene")}
|
||||
aria-label={t("buttons.load")}
|
||||
>
|
||||
{t("buttons.load")}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
};
|
||||
LoadScene.displayName = "LoadScene";
|
||||
|
||||
export const SaveToActiveFile = () => {
|
||||
const { t } = useI18n();
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
if (!actionManager.isActionEnabled(actionSaveToActiveFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
shortcut={getShortcutFromShortcutName("saveScene")}
|
||||
data-testid="save-button"
|
||||
onSelect={() => actionManager.executeAction(actionSaveToActiveFile)}
|
||||
icon={save}
|
||||
aria-label={`${t("buttons.save")}`}
|
||||
>{`${t("buttons.save")}`}</DropdownMenuItem>
|
||||
);
|
||||
};
|
||||
SaveToActiveFile.displayName = "SaveToActiveFile";
|
||||
|
||||
export const SaveAsImage = () => {
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
icon={ExportImageIcon}
|
||||
data-testid="image-export-button"
|
||||
onSelect={() => setAppState({ openDialog: "imageExport" })}
|
||||
shortcut={getShortcutFromShortcutName("imageExport")}
|
||||
aria-label={t("buttons.exportImage")}
|
||||
>
|
||||
{t("buttons.exportImage")}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
};
|
||||
SaveAsImage.displayName = "SaveAsImage";
|
||||
|
||||
export const Help = () => {
|
||||
const { t } = useI18n();
|
||||
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
data-testid="help-menu-item"
|
||||
icon={HelpIcon}
|
||||
onSelect={() => actionManager.executeAction(actionShortcuts)}
|
||||
shortcut="?"
|
||||
aria-label={t("helpDialog.title")}
|
||||
>
|
||||
{t("helpDialog.title")}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
};
|
||||
Help.displayName = "Help";
|
||||
|
||||
export const ClearCanvas = () => {
|
||||
const { t } = useI18n();
|
||||
|
||||
const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom);
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
if (!actionManager.isActionEnabled(actionClearCanvas)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
icon={TrashIcon}
|
||||
onSelect={() => setActiveConfirmDialog("clearCanvas")}
|
||||
data-testid="clear-canvas-button"
|
||||
aria-label={t("buttons.clearReset")}
|
||||
>
|
||||
{t("buttons.clearReset")}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
};
|
||||
ClearCanvas.displayName = "ClearCanvas";
|
||||
|
||||
export const ToggleTheme = () => {
|
||||
const { t } = useI18n();
|
||||
const appState = useExcalidrawAppState();
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
if (!actionManager.isActionEnabled(actionToggleTheme)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onSelect={(event) => {
|
||||
// do not close the menu when changing theme
|
||||
event.preventDefault();
|
||||
return actionManager.executeAction(actionToggleTheme);
|
||||
}}
|
||||
icon={appState.theme === "dark" ? SunIcon : MoonIcon}
|
||||
data-testid="toggle-dark-mode"
|
||||
shortcut={getShortcutFromShortcutName("toggleTheme")}
|
||||
aria-label={
|
||||
appState.theme === "dark"
|
||||
? t("buttons.lightMode")
|
||||
: t("buttons.darkMode")
|
||||
}
|
||||
>
|
||||
{appState.theme === "dark"
|
||||
? t("buttons.lightMode")
|
||||
: t("buttons.darkMode")}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
};
|
||||
ToggleTheme.displayName = "ToggleTheme";
|
||||
|
||||
export const ChangeCanvasBackground = () => {
|
||||
const { t } = useI18n();
|
||||
const appState = useExcalidrawAppState();
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
if (appState.viewModeEnabled) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div style={{ marginTop: "0.5rem" }}>
|
||||
<div style={{ fontSize: ".75rem", marginBottom: ".5rem" }}>
|
||||
{t("labels.canvasBackground")}
|
||||
</div>
|
||||
<div style={{ padding: "0 0.625rem" }}>
|
||||
{actionManager.renderAction("changeViewBackgroundColor")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
ChangeCanvasBackground.displayName = "ChangeCanvasBackground";
|
||||
|
||||
export const Export = () => {
|
||||
const { t } = useI18n();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
icon={ExportIcon}
|
||||
onSelect={() => {
|
||||
setAppState({ openDialog: "jsonExport" });
|
||||
}}
|
||||
data-testid="json-export-button"
|
||||
aria-label={t("buttons.export")}
|
||||
>
|
||||
{t("buttons.export")}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
};
|
||||
Export.displayName = "Export";
|
||||
|
||||
export const Socials = () => (
|
||||
<>
|
||||
<DropdownMenuItemLink
|
||||
icon={GithubIcon}
|
||||
href="https://github.com/excalidraw/excalidraw"
|
||||
aria-label="GitHub"
|
||||
>
|
||||
GitHub
|
||||
</DropdownMenuItemLink>
|
||||
<DropdownMenuItemLink
|
||||
icon={DiscordIcon}
|
||||
href="https://discord.gg/UexuTaE"
|
||||
aria-label="Discord"
|
||||
>
|
||||
Discord
|
||||
</DropdownMenuItemLink>
|
||||
<DropdownMenuItemLink
|
||||
icon={TwitterIcon}
|
||||
href="https://twitter.com/excalidraw"
|
||||
aria-label="Twitter"
|
||||
>
|
||||
Twitter
|
||||
</DropdownMenuItemLink>
|
||||
</>
|
||||
);
|
||||
Socials.displayName = "Socials";
|
||||
|
||||
export const LiveCollaborationTrigger = ({
|
||||
onSelect,
|
||||
isCollaborating,
|
||||
}: {
|
||||
onSelect: () => void;
|
||||
isCollaborating: boolean;
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
data-testid="collab-button"
|
||||
icon={usersIcon}
|
||||
className={clsx({
|
||||
"active-collab": isCollaborating,
|
||||
})}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
{t("labels.liveCollaboration")}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
LiveCollaborationTrigger.displayName = "LiveCollaborationTrigger";
|
84
src/components/main-menu/MainMenu.tsx
Normal file
84
src/components/main-menu/MainMenu.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import React from "react";
|
||||
import {
|
||||
useDevice,
|
||||
useExcalidrawAppState,
|
||||
useExcalidrawSetAppState,
|
||||
} from "../App";
|
||||
import DropdownMenu from "../dropdownMenu/DropdownMenu";
|
||||
|
||||
import * as DefaultItems from "./DefaultItems";
|
||||
|
||||
import { UserList } from "../UserList";
|
||||
import { t } from "../../i18n";
|
||||
import { HamburgerMenuIcon } from "../icons";
|
||||
import { withInternalFallback } from "../hoc/withInternalFallback";
|
||||
import { composeEventHandlers } from "../../utils";
|
||||
import { useTunnels } from "../context/tunnels";
|
||||
|
||||
const MainMenu = Object.assign(
|
||||
withInternalFallback(
|
||||
"MainMenu",
|
||||
({
|
||||
children,
|
||||
onSelect,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
/**
|
||||
* Called when any menu item is selected (clicked on).
|
||||
*/
|
||||
onSelect?: (event: Event) => void;
|
||||
}) => {
|
||||
const { mainMenuTunnel } = useTunnels();
|
||||
const device = useDevice();
|
||||
const appState = useExcalidrawAppState();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
const onClickOutside = device.isMobile
|
||||
? undefined
|
||||
: () => setAppState({ openMenu: null });
|
||||
|
||||
return (
|
||||
<mainMenuTunnel.In>
|
||||
<DropdownMenu open={appState.openMenu === "canvas"}>
|
||||
<DropdownMenu.Trigger
|
||||
onToggle={() => {
|
||||
setAppState({
|
||||
openMenu: appState.openMenu === "canvas" ? null : "canvas",
|
||||
});
|
||||
}}
|
||||
>
|
||||
{HamburgerMenuIcon}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
onClickOutside={onClickOutside}
|
||||
onSelect={composeEventHandlers(onSelect, () => {
|
||||
setAppState({ openMenu: null });
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
{device.isMobile && appState.collaborators.size > 0 && (
|
||||
<fieldset className="UserList-Wrapper">
|
||||
<legend>{t("labels.collaborators")}</legend>
|
||||
<UserList
|
||||
mobile={true}
|
||||
collaborators={appState.collaborators}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
</mainMenuTunnel.In>
|
||||
);
|
||||
},
|
||||
),
|
||||
{
|
||||
Trigger: DropdownMenu.Trigger,
|
||||
Item: DropdownMenu.Item,
|
||||
ItemLink: DropdownMenu.ItemLink,
|
||||
ItemCustom: DropdownMenu.ItemCustom,
|
||||
Group: DropdownMenu.Group,
|
||||
Separator: DropdownMenu.Separator,
|
||||
DefaultItems,
|
||||
},
|
||||
);
|
||||
|
||||
export default MainMenu;
|
196
src/components/welcome-screen/WelcomeScreen.Center.tsx
Normal file
196
src/components/welcome-screen/WelcomeScreen.Center.tsx
Normal file
|
@ -0,0 +1,196 @@
|
|||
import { actionLoadScene, actionShortcuts } from "../../actions";
|
||||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||
import { t, useI18n } from "../../i18n";
|
||||
import {
|
||||
useDevice,
|
||||
useExcalidrawActionManager,
|
||||
useExcalidrawAppState,
|
||||
} from "../App";
|
||||
import { useTunnels } from "../context/tunnels";
|
||||
import { ExcalLogo, HelpIcon, LoadIcon, usersIcon } from "../icons";
|
||||
|
||||
const WelcomeScreenMenuItemContent = ({
|
||||
icon,
|
||||
shortcut,
|
||||
children,
|
||||
}: {
|
||||
icon?: JSX.Element;
|
||||
shortcut?: string | null;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
return (
|
||||
<>
|
||||
<div className="welcome-screen-menu-item__icon">{icon}</div>
|
||||
<div className="welcome-screen-menu-item__text">{children}</div>
|
||||
{shortcut && !device.isMobile && (
|
||||
<div className="welcome-screen-menu-item__shortcut">{shortcut}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
WelcomeScreenMenuItemContent.displayName = "WelcomeScreenMenuItemContent";
|
||||
|
||||
const WelcomeScreenMenuItem = ({
|
||||
onSelect,
|
||||
children,
|
||||
icon,
|
||||
shortcut,
|
||||
className = "",
|
||||
...props
|
||||
}: {
|
||||
onSelect: () => void;
|
||||
children: React.ReactNode;
|
||||
icon?: JSX.Element;
|
||||
shortcut?: string | null;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className={`welcome-screen-menu-item ${className}`}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<WelcomeScreenMenuItemContent icon={icon} shortcut={shortcut}>
|
||||
{children}
|
||||
</WelcomeScreenMenuItemContent>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
WelcomeScreenMenuItem.displayName = "WelcomeScreenMenuItem";
|
||||
|
||||
const WelcomeScreenMenuItemLink = ({
|
||||
children,
|
||||
href,
|
||||
icon,
|
||||
shortcut,
|
||||
className = "",
|
||||
...props
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
href: string;
|
||||
icon?: JSX.Element;
|
||||
shortcut?: string | null;
|
||||
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
return (
|
||||
<a
|
||||
{...props}
|
||||
className={`welcome-screen-menu-item ${className}`}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<WelcomeScreenMenuItemContent icon={icon} shortcut={shortcut}>
|
||||
{children}
|
||||
</WelcomeScreenMenuItemContent>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
WelcomeScreenMenuItemLink.displayName = "WelcomeScreenMenuItemLink";
|
||||
|
||||
const Center = ({ children }: { children?: React.ReactNode }) => {
|
||||
const { welcomeScreenCenterTunnel } = useTunnels();
|
||||
return (
|
||||
<welcomeScreenCenterTunnel.In>
|
||||
<div className="welcome-screen-center">
|
||||
{children || (
|
||||
<>
|
||||
<Logo />
|
||||
<Heading>{t("welcomeScreen.defaults.center_heading")}</Heading>
|
||||
<Menu>
|
||||
<MenuItemLoadScene />
|
||||
<MenuItemHelp />
|
||||
</Menu>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</welcomeScreenCenterTunnel.In>
|
||||
);
|
||||
};
|
||||
Center.displayName = "Center";
|
||||
|
||||
const Logo = ({ children }: { children?: React.ReactNode }) => {
|
||||
return (
|
||||
<div className="welcome-screen-center__logo virgil welcome-screen-decor">
|
||||
{children || <>{ExcalLogo} Excalidraw</>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Logo.displayName = "Logo";
|
||||
|
||||
const Heading = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<div className="welcome-screen-center__heading welcome-screen-decor virgil">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Heading.displayName = "Heading";
|
||||
|
||||
const Menu = ({ children }: { children?: React.ReactNode }) => {
|
||||
return <div className="welcome-screen-menu">{children}</div>;
|
||||
};
|
||||
Menu.displayName = "Menu";
|
||||
|
||||
const MenuItemHelp = () => {
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
return (
|
||||
<WelcomeScreenMenuItem
|
||||
onSelect={() => actionManager.executeAction(actionShortcuts)}
|
||||
shortcut="?"
|
||||
icon={HelpIcon}
|
||||
>
|
||||
{t("helpDialog.title")}
|
||||
</WelcomeScreenMenuItem>
|
||||
);
|
||||
};
|
||||
MenuItemHelp.displayName = "MenuItemHelp";
|
||||
|
||||
const MenuItemLoadScene = () => {
|
||||
const appState = useExcalidrawAppState();
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
if (appState.viewModeEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<WelcomeScreenMenuItem
|
||||
onSelect={() => actionManager.executeAction(actionLoadScene)}
|
||||
shortcut={getShortcutFromShortcutName("loadScene")}
|
||||
icon={LoadIcon}
|
||||
>
|
||||
{t("buttons.load")}
|
||||
</WelcomeScreenMenuItem>
|
||||
);
|
||||
};
|
||||
MenuItemLoadScene.displayName = "MenuItemLoadScene";
|
||||
|
||||
const MenuItemLiveCollaborationTrigger = ({
|
||||
onSelect,
|
||||
}: {
|
||||
onSelect: () => any;
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<WelcomeScreenMenuItem shortcut={null} onSelect={onSelect} icon={usersIcon}>
|
||||
{t("labels.liveCollaboration")}
|
||||
</WelcomeScreenMenuItem>
|
||||
);
|
||||
};
|
||||
MenuItemLiveCollaborationTrigger.displayName =
|
||||
"MenuItemLiveCollaborationTrigger";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
Center.Logo = Logo;
|
||||
Center.Heading = Heading;
|
||||
Center.Menu = Menu;
|
||||
Center.MenuItem = WelcomeScreenMenuItem;
|
||||
Center.MenuItemLink = WelcomeScreenMenuItemLink;
|
||||
Center.MenuItemHelp = MenuItemHelp;
|
||||
Center.MenuItemLoadScene = MenuItemLoadScene;
|
||||
Center.MenuItemLiveCollaborationTrigger = MenuItemLiveCollaborationTrigger;
|
||||
|
||||
export { Center };
|
52
src/components/welcome-screen/WelcomeScreen.Hints.tsx
Normal file
52
src/components/welcome-screen/WelcomeScreen.Hints.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { t } from "../../i18n";
|
||||
import { useTunnels } from "../context/tunnels";
|
||||
import {
|
||||
WelcomeScreenHelpArrow,
|
||||
WelcomeScreenMenuArrow,
|
||||
WelcomeScreenTopToolbarArrow,
|
||||
} from "../icons";
|
||||
|
||||
const MenuHint = ({ children }: { children?: React.ReactNode }) => {
|
||||
const { welcomeScreenMenuHintTunnel } = useTunnels();
|
||||
return (
|
||||
<welcomeScreenMenuHintTunnel.In>
|
||||
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--menu">
|
||||
{WelcomeScreenMenuArrow}
|
||||
<div className="welcome-screen-decor-hint__label">
|
||||
{children || t("welcomeScreen.defaults.menuHint")}
|
||||
</div>
|
||||
</div>
|
||||
</welcomeScreenMenuHintTunnel.In>
|
||||
);
|
||||
};
|
||||
MenuHint.displayName = "MenuHint";
|
||||
|
||||
const ToolbarHint = ({ children }: { children?: React.ReactNode }) => {
|
||||
const { welcomeScreenToolbarHintTunnel } = useTunnels();
|
||||
return (
|
||||
<welcomeScreenToolbarHintTunnel.In>
|
||||
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--toolbar">
|
||||
<div className="welcome-screen-decor-hint__label">
|
||||
{children || t("welcomeScreen.defaults.toolbarHint")}
|
||||
</div>
|
||||
{WelcomeScreenTopToolbarArrow}
|
||||
</div>
|
||||
</welcomeScreenToolbarHintTunnel.In>
|
||||
);
|
||||
};
|
||||
ToolbarHint.displayName = "ToolbarHint";
|
||||
|
||||
const HelpHint = ({ children }: { children?: React.ReactNode }) => {
|
||||
const { welcomeScreenHelpHintTunnel } = useTunnels();
|
||||
return (
|
||||
<welcomeScreenHelpHintTunnel.In>
|
||||
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--help">
|
||||
<div>{children || t("welcomeScreen.defaults.helpHint")}</div>
|
||||
{WelcomeScreenHelpArrow}
|
||||
</div>
|
||||
</welcomeScreenHelpHintTunnel.In>
|
||||
);
|
||||
};
|
||||
HelpHint.displayName = "HelpHint";
|
||||
|
||||
export { HelpHint, MenuHint, ToolbarHint };
|
|
@ -3,29 +3,39 @@
|
|||
font-family: "Virgil";
|
||||
}
|
||||
|
||||
.WelcomeScreen-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 0.75rem;
|
||||
font-size: 2.25rem;
|
||||
// WelcomeSreen common
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
svg {
|
||||
width: 1.625rem;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.WelcomeScreen-decor {
|
||||
.welcome-screen-decor {
|
||||
pointer-events: none;
|
||||
|
||||
color: var(--color-gray-40);
|
||||
}
|
||||
|
||||
&--subheading {
|
||||
font-size: 1.125rem;
|
||||
text-align: center;
|
||||
&.theme--dark {
|
||||
.welcome-screen-decor {
|
||||
color: var(--color-gray-60);
|
||||
}
|
||||
}
|
||||
|
||||
// WelcomeScreen.Hints
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.welcome-screen-decor-hint {
|
||||
@media (max-height: 599px) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&--help-pointer {
|
||||
@media (max-width: 1024px), (max-width: 800px) {
|
||||
.welcome-screen-decor {
|
||||
&--help,
|
||||
&--menu {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--help {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
|
@ -49,7 +59,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&--top-toolbar-pointer {
|
||||
&--toolbar {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
|
@ -58,7 +68,7 @@
|
|||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
&__label {
|
||||
.welcome-screen-decor-hint__label {
|
||||
width: 120px;
|
||||
position: relative;
|
||||
top: -0.5rem;
|
||||
|
@ -74,7 +84,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&--menu-pointer {
|
||||
&--menu {
|
||||
position: absolute;
|
||||
width: 320px;
|
||||
font-size: 1rem;
|
||||
|
@ -95,10 +105,19 @@
|
|||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.welcome-screen-decor-hint__label {
|
||||
max-width: 160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.WelcomeScreen-container {
|
||||
// WelcomeSreen.Center
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.welcome-screen-center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
|
@ -112,7 +131,24 @@
|
|||
bottom: 1rem;
|
||||
}
|
||||
|
||||
.WelcomeScreen-items {
|
||||
.welcome-screen-center__logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 0.75rem;
|
||||
font-size: 2.25rem;
|
||||
|
||||
svg {
|
||||
width: 1.625rem;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-screen-center__heading {
|
||||
font-size: 1.125rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.welcome-screen-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
@ -120,7 +156,7 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.WelcomeScreen-item {
|
||||
.welcome-screen-menu-item {
|
||||
box-sizing: border-box;
|
||||
|
||||
pointer-events: all;
|
||||
|
@ -128,8 +164,10 @@
|
|||
color: var(--color-gray-50);
|
||||
font-size: 0.875rem;
|
||||
|
||||
width: 100%;
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
max-width: 400px;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
|
@ -140,44 +178,49 @@
|
|||
|
||||
border-radius: var(--border-radius-md);
|
||||
|
||||
&__label {
|
||||
grid-template-columns: calc(var(--default-icon-size) + 0.5rem) 1fr 3rem;
|
||||
|
||||
&__text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: auto;
|
||||
text-align: left;
|
||||
column-gap: 0.5rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: var(--default-icon-size);
|
||||
height: var(--default-icon-size);
|
||||
}
|
||||
&__icon {
|
||||
width: var(--default-icon-size);
|
||||
height: var(--default-icon-size);
|
||||
}
|
||||
|
||||
&__shortcut {
|
||||
margin-left: auto;
|
||||
color: var(--color-gray-40);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:active) .WelcomeScreen-item:hover {
|
||||
&:not(:active) .welcome-screen-menu-item:hover {
|
||||
text-decoration: none;
|
||||
background: var(--color-gray-10);
|
||||
|
||||
.WelcomeScreen-item__shortcut {
|
||||
.welcome-screen-menu-item__shortcut {
|
||||
color: var(--color-gray-50);
|
||||
}
|
||||
|
||||
.WelcomeScreen-item__label {
|
||||
.welcome-screen-menu-item__text {
|
||||
color: var(--color-gray-100);
|
||||
}
|
||||
}
|
||||
|
||||
.WelcomeScreen-item:active {
|
||||
.welcome-screen-menu-item:active {
|
||||
background: var(--color-gray-20);
|
||||
|
||||
.WelcomeScreen-item__shortcut {
|
||||
.welcome-screen-menu-item__shortcut {
|
||||
color: var(--color-gray-50);
|
||||
}
|
||||
|
||||
.WelcomeScreen-item__label {
|
||||
.welcome-screen-menu-item__text {
|
||||
color: var(--color-gray-100);
|
||||
}
|
||||
|
||||
|
@ -185,7 +228,7 @@
|
|||
color: var(--color-promo) !important;
|
||||
|
||||
&:hover {
|
||||
.WelcomeScreen-item__label {
|
||||
.welcome-screen-menu-item__text {
|
||||
color: var(--color-promo) !important;
|
||||
}
|
||||
}
|
||||
|
@ -193,11 +236,7 @@
|
|||
}
|
||||
|
||||
&.theme--dark {
|
||||
.WelcomeScreen-decor {
|
||||
color: var(--color-gray-60);
|
||||
}
|
||||
|
||||
.WelcomeScreen-item {
|
||||
.welcome-screen-menu-item {
|
||||
color: var(--color-gray-60);
|
||||
|
||||
&__shortcut {
|
||||
|
@ -205,69 +244,41 @@
|
|||
}
|
||||
}
|
||||
|
||||
&:not(:active) .WelcomeScreen-item:hover {
|
||||
&:not(:active) .welcome-screen-menu-item:hover {
|
||||
background: var(--color-gray-85);
|
||||
|
||||
.WelcomeScreen-item__shortcut {
|
||||
.welcome-screen-menu-item__shortcut {
|
||||
color: var(--color-gray-50);
|
||||
}
|
||||
|
||||
.WelcomeScreen-item__label {
|
||||
.welcome-screen-menu-item__text {
|
||||
color: var(--color-gray-10);
|
||||
}
|
||||
}
|
||||
|
||||
.WelcomeScreen-item:active {
|
||||
.welcome-screen-menu-item:active {
|
||||
background-color: var(--color-gray-90);
|
||||
.WelcomeScreen-item__label {
|
||||
.welcome-screen-menu-item__text {
|
||||
color: var(--color-gray-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Can tweak these values but for an initial effort, it looks OK to me
|
||||
@media (max-width: 1024px) {
|
||||
.WelcomeScreen-decor {
|
||||
&--help-pointer,
|
||||
&--menu-pointer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @media (max-height: 400px) {
|
||||
// .WelcomeScreen-container {
|
||||
// margin-top: 0;
|
||||
// }
|
||||
// }
|
||||
@media (max-height: 599px) {
|
||||
.WelcomeScreen-container {
|
||||
.welcome-screen-center {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
@media (min-height: 600px) and (max-height: 900px) {
|
||||
.WelcomeScreen-container {
|
||||
.welcome-screen-center {
|
||||
margin-top: 8rem;
|
||||
}
|
||||
}
|
||||
@media (max-height: 630px) {
|
||||
.WelcomeScreen-decor--top-toolbar-pointer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media (max-height: 500px) {
|
||||
.WelcomeScreen-container {
|
||||
@media (max-height: 500px), (max-width: 320px) {
|
||||
.welcome-screen-center {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// @media (max-height: 740px) {
|
||||
// .WelcomeScreen-decor {
|
||||
// &--help-pointer,
|
||||
// &--top-toolbar-pointer,
|
||||
// &--menu-pointer {
|
||||
// display: none;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ---------------------------------------------------------------------------
|
||||
}
|
26
src/components/welcome-screen/WelcomeScreen.tsx
Normal file
26
src/components/welcome-screen/WelcomeScreen.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { Center } from "./WelcomeScreen.Center";
|
||||
import { MenuHint, ToolbarHint, HelpHint } from "./WelcomeScreen.Hints";
|
||||
|
||||
import "./WelcomeScreen.scss";
|
||||
|
||||
const WelcomeScreen = (props: { children?: React.ReactNode }) => {
|
||||
return (
|
||||
<>
|
||||
{props.children || (
|
||||
<>
|
||||
<Center />
|
||||
<MenuHint />
|
||||
<ToolbarHint />
|
||||
<HelpHint />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
WelcomeScreen.displayName = "WelcomeScreen";
|
||||
|
||||
WelcomeScreen.Center = Center;
|
||||
WelcomeScreen.Hints = { MenuHint, ToolbarHint, HelpHint };
|
||||
|
||||
export default WelcomeScreen;
|
Loading…
Add table
Add a link
Reference in a new issue