Parcourir la source

feat: editor redesign 🔥 (#5780)

* Placed eraser into shape switcher (top toolbar).
Redesigned top toolbar.

* Redesigned zoom and undo-redo buttons.

* Started redesigning left toolbar.

* Redesigned help dialog.

* Colour picker now somewhat in line with new design

* [WIP] Changed a bunch of icons.
TODO: organise new icons.

* [WIP] Organised a bunch of icons. Still some to do

* [WIP] Started working on hamburger menu.

* Fixed some bugs with hamburger menu.

* Menu and left toolbar positioning.

* Added some more items to hamburger menu.

* Changed some icons.

* Modal/dialog styling & bunch of fixes.

* Some more dialog improvements & fixes.

* Mobile menu changes.

* Menu can now be closed with outside click.

* Collab avatars and button changes.

* Icon sizing. Left toolbar positioning.

* Implemented welcome screen rendering logic.

* [WIP] Welcome screen content + design.

* Some more welcome screen content and design.

* Merge fixes.

* Tweaked icon set.

* Welcome screen darkmode fix.

* Content updates.

* Various small fixes & adjustments.
Moved language selection into menu.
Fixed some problematic icons.
Slightly moved encryption icon.

* Sidebar header redesign.

* Libraries content rendering logic + some styling.

* Somem more library sidebar styling.

* Publish library dialog styling.

* scroll-back-to-content btn styling

* ColorPicker positioning.

* Library button styling.

* ColorPicker positioning "fix".

* Misc adjustments.

* PenMode button changes.

* Trying to make mobile somewhat usable.

* Added a couple of icons.

* Added some shortcuts.

* Prevent welcome screen flickering.
Fix issue with welcome screen interactivity.
Don't show sidebar button when docked.

* Icon sizing on smaller screens.

* Sidebar styling changes.

* Alignment button... well... alignments.

* Fix inconsistent padding in left toolbar.

* HintViewer changes.

* Hamburger menu changes.

* Move encryption badge back to its original pos.

* Arrowhead changes.
Active state, colours + stronger shadow.

* Added new custom font.

* Fixed bug with library button not rendering.

* Fixed issue with lang selection colours.

* Add tooltips for undo, redo.

* Address some dark mode contrast issues.

* (Re)introduce counter for selectedItems in sidebar

* [WIP] Tweaked bounding box colour & padding.

* Dashed bounding box for remote clients.

* Some more bounding box tweaks.

* Removed docking animation for now...

* Address some RTL issues.

* Welcome screen responsiveness.

* use lighter selection color in dark mode & align naming

* use rounded corners for transform handles

* use lighter gray for welcomeScreen text in dark mode

* disable selection on dialog buttons

* change selection button icon

* fix library item width being flexible

* library: visually align spinner with first section heading

* lint

* fix scrollbar color in dark mode & make thinner

* adapt properties panel max-height

* add shrotcut label to save-to-current-file

* fix unrelated `useOutsideClick` firing for active modal

* add promo color to e+ menu item

* fix type

* lowered button size

* fix transform handles raidus not accounting for zoom

* attempt fix for excal logo on safari

* final fix for excal logo on safari

* fixing fhd resolution button sized

* remove TODO shortcut

* Collab related styling changes.
Expanding avatar list no longer offsets top toolbar.
Added active state & collaborator count badge for collab button.

* Tweaked collab button active colours.

* Added active style to collab btn in hamburger menu

* Remove unnecessary comment.

* Added back promo link for non (signed in) E+ users

* Go to E+ button added for signed in E+ users.

* Close menu & dropdown on modal close.

* tweak icons & fix rendering on smaller sizes [part one]

* align welcomeScreen icons with other UI

* switch icon resize mq to `device-width`

* disable welcomeScreen items `:hover` when selecting on canvas

* change selection box color and style

* reduce selection padding and fix group selection styling

* improve collab cursor styling

- make name borders round
- hide status when "active"
- remove black/gray colors

* add Twitter to hamburger menu

* align collab button

* add shortcut for image export dialog

* revert yarn.lock

* fix more tabler icons

* slightly better-looking penMode button

* change penMode button & tooltip

* revert hamburger menu icon

* align padding on lang picker & canvas bg

* updated robot txt to allow twitter bot and fb bot

* added new OG and tweaked the OG state

* add tooltip to collab button

* align style for scroll-to-content button

* fix pointer-events around toolbar

* fix decor arrow positioning and RTL

* fix welcomeScreen-item active state in dark mode

* change `load` button copy

* prevent shadow anim when opening a docked sidebar

* update E+ links ga params

* show redirect-to-eplus welcomeScreen subheading for signed-in users

* make more generic

* add ga for eplus redirect button

* change copy and icons for hamburger export buttons

* update snaps

* trim the username to account for trailing spaces

* tweaks around decor breakpoints

* fix linear element editor test

* remove .env change

* remove `it.only`

Co-authored-by: dwelle <luzar.david@gmail.com>
Co-authored-by: Maielo <maielo.mv@gmail.com>
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
Barnabás Molnár il y a 2 ans
Parent
commit
6334bd832f
100 fichiers modifiés avec 3236 ajouts et 1916 suppressions
  1. BIN
      public/Assistant-Bold.woff2
  2. BIN
      public/Assistant-Medium.woff2
  3. BIN
      public/Assistant-Regular.woff2
  4. BIN
      public/Assistant-SemiBold.woff2
  5. 25 0
      public/fonts.css
  6. 34 26
      public/index.html
  7. BIN
      public/og-fb-v1.png
  8. BIN
      public/og-general-v1.png
  9. BIN
      public/og-twitter-v1.png
  10. 6 0
      public/robots.txt
  11. 6 6
      src/actions/actionAlign.tsx
  12. 27 16
      src/actions/actionCanvas.tsx
  13. 2 2
      src/actions/actionDeleteSelected.tsx
  14. 2 2
      src/actions/actionDistribute.tsx
  15. 2 2
      src/actions/actionDuplicateSelection.tsx
  16. 8 8
      src/actions/actionExport.tsx
  17. 3 3
      src/actions/actionHistory.tsx
  18. 18 8
      src/actions/actionMenu.tsx
  19. 57 61
      src/actions/actionProperties.tsx
  20. 6 6
      src/actions/actionZindex.tsx
  21. 7 1
      src/actions/manager.tsx
  22. 37 28
      src/actions/shortcuts.ts
  23. 3 1
      src/actions/types.ts
  24. 4 2
      src/appState.ts
  25. 6 15
      src/clients.ts
  26. 92 0
      src/components/Actions.scss
  27. 37 17
      src/components/Actions.tsx
  28. 12 17
      src/components/ActiveFile.tsx
  29. 30 1
      src/components/App.tsx
  30. 7 4
      src/components/Avatar.scss
  31. 2 4
      src/components/Avatar.tsx
  32. 0 12
      src/components/BackgroundPickerAndDarkModeToggle.tsx
  33. 2 0
      src/components/CheckboxItem.scss
  34. 6 10
      src/components/ClearCanvas.tsx
  35. 48 3
      src/components/CollabButton.scss
  36. 29 19
      src/components/CollabButton.tsx
  37. 76 62
      src/components/ColorPicker.scss
  38. 40 27
      src/components/ColorPicker.tsx
  39. 1 27
      src/components/ConfirmDialog.scss
  40. 21 13
      src/components/ConfirmDialog.tsx
  41. 5 62
      src/components/Dialog.scss
  42. 9 2
      src/components/Dialog.tsx
  43. 47 0
      src/components/DialogActionButton.scss
  44. 46 0
      src/components/DialogActionButton.tsx
  45. 19 0
      src/components/EncryptedIcon.tsx
  46. 2 0
      src/components/ExportDialog.scss
  47. 4 3
      src/components/FixedSideContainer.scss
  48. 28 25
      src/components/Footer.tsx
  49. 4 4
      src/components/HelpButton.tsx
  50. 104 45
      src/components/HelpDialog.scss
  51. 313 346
      src/components/HelpDialog.tsx
  52. 11 7
      src/components/HintViewer.scss
  53. 2 2
      src/components/IconPicker.scss
  54. 6 3
      src/components/IconPicker.tsx
  55. 6 19
      src/components/ImageExportDialog.tsx
  56. 1 0
      src/components/Island.scss
  57. 8 10
      src/components/JSONExportDialog.tsx
  58. 2 12
      src/components/LayerUI.scss
  59. 183 113
      src/components/LayerUI.tsx
  60. 32 0
      src/components/LibraryButton.scss
  61. 17 21
      src/components/LibraryButton.tsx
  62. 31 93
      src/components/LibraryMenu.scss
  63. 20 17
      src/components/LibraryMenu.tsx
  64. 31 0
      src/components/LibraryMenuBrowseButton.tsx
  65. 86 87
      src/components/LibraryMenuHeaderContent.tsx
  66. 52 0
      src/components/LibraryMenuItems.scss
  67. 52 40
      src/components/LibraryMenuItems.tsx
  68. 45 26
      src/components/LibraryUnit.scss
  69. 2 14
      src/components/LibraryUnit.tsx
  70. 4 23
      src/components/LockButton.tsx
  71. 85 0
      src/components/Menu.scss
  72. 37 0
      src/components/MenuItem.tsx
  73. 53 0
      src/components/MenuUtils.tsx
  74. 76 35
      src/components/MobileMenu.tsx
  75. 18 8
      src/components/Modal.scss
  76. 1 0
      src/components/Modal.tsx
  77. 5 50
      src/components/PenModeButton.tsx
  78. 3 3
      src/components/PublishLibrary.scss
  79. 4 11
      src/components/PublishLibrary.tsx
  80. 93 36
      src/components/Sidebar/Sidebar.scss
  81. 15 3
      src/components/Sidebar/Sidebar.tsx
  82. 16 22
      src/components/Sidebar/SidebarHeader.tsx
  83. 1 0
      src/components/Sidebar/common.ts
  84. 3 3
      src/components/SingleLibraryItem.tsx
  85. 2 2
      src/components/Stats.tsx
  86. 2 2
      src/components/Toast.tsx
  87. 35 96
      src/components/ToolIcon.scss
  88. 7 88
      src/components/Toolbar.scss
  89. 12 5
      src/components/UserList.scss
  90. 20 0
      src/components/UserList.tsx
  91. 273 0
      src/components/WelcomeScreen.scss
  92. 131 0
      src/components/WelcomeScreen.tsx
  93. 11 0
      src/components/WelcomeScreenDecor.tsx
  94. 207 3
      src/components/icons.tsx
  95. 124 139
      src/css/styles.scss
  96. 95 18
      src/css/theme.scss
  97. 69 9
      src/css/variables.module.scss
  98. 4 4
      src/element/Hyperlink.tsx
  99. 2 2
      src/element/transformHandles.ts
  100. 4 0
      src/excalidraw-app/collab/RoomDialog.scss

BIN
public/Assistant-Bold.woff2


BIN
public/Assistant-Medium.woff2


BIN
public/Assistant-Regular.woff2


BIN
public/Assistant-SemiBold.woff2


+ 25 - 0
public/fonts.css

@@ -11,3 +11,28 @@
   src: url("Cascadia.woff2");
   font-display: swap;
 }
+
+@font-face {
+  font-family: "Assistant";
+  src: url("Assistant-Regular.woff2");
+  font-display: swap;
+  font-weight: 400;
+}
+@font-face {
+  font-family: "Assistant";
+  src: url("Assistant-Medium.woff2");
+  font-display: swap;
+  font-weight: 500;
+}
+@font-face {
+  font-family: "Assistant";
+  src: url("Assistant-SemiBold.woff2");
+  font-display: swap;
+  font-weight: 600;
+}
+@font-face {
+  font-family: "Assistant";
+  src: url("Assistant-Bold.woff2");
+  font-display: swap;
+  font-weight: 700;
+}

+ 34 - 26
public/index.html

@@ -8,48 +8,56 @@
       content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no"
     />
     <meta name="referrer" content="origin" />
-
     <meta name="mobile-web-app-capable" content="yes" />
+    <meta name="theme-color" content="#121212" />
 
-    <meta name="theme-color" content="#000" />
-
-    <!-- General tags -->
+    <!-- Primary Meta Tags -->
+    <meta
+      name="title"
+      content="Excalidraw — Collaborative whiteboarding made easy"
+    />
     <meta
       name="description"
       content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
     />
-    <meta name="image" content="og-image.png" />
+    <meta name="image" content="https://excalidraw.com/og-general-v1.png" />
 
-    <!-- OpenGraph tags -->
-    <meta property="og:url" content="https://excalidraw.com" />
+    <!-- Open Graph / Facebook -->
     <meta property="og:site_name" content="Excalidraw" />
     <meta property="og:type" content="website" />
-    <meta property="og:title" content="Excalidraw" />
+    <meta property="og:url" content="https://excalidraw.com" />
+    <meta
+      property="og:title"
+      content="Excalidraw — Collaborative whiteboarding made easy"
+    />
+    <meta property="og:image:alt" content="Excalidraw logo" />
     <meta
       property="og:description"
-      content="Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
+      content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
+    />
+    <meta property="og:image" content="https://excalidraw.com/og-fb-v1.png" />
+
+    <!-- Twitter -->
+    <meta property="twitter:card" content="summary_large_image" />
+    <meta property="twitter:site" content="@excalidraw" />
+    <meta property="twitter:url" content="https://excalidraw.com" />
+    <meta
+      property="twitter:title"
+      content="Excalidraw — Collaborative whiteboarding made easy"
     />
-    <!-- OG tags require an absolute url for images -->
     <meta
-      property="og:image"
-      name="twitter:image"
-      content="https://excalidraw.com/og-image.png"
+      property="twitter:description"
+      content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
     />
     <meta
-      property="og:image:secure_url"
-      name="twitter:image"
-      content="https://excalidraw.com/og-image.png"
+      property="twitter:image"
+      content="https://excalidraw.com/og-twitter-v1.png"
     />
-    <meta property="og:image:width" content="1280" />
-    <meta property="og:image:height" content="669" />
-    <meta property="og:image:alt" content="Excalidraw logo with byline." />
 
-    <!-- Twitter Card tags -->
-    <meta name="twitter:card" content="summary_large_image" />
-    <meta name="twitter:title" content="Excalidraw" />
+    <!-- General tags -->
     <meta
-      name="twitter:description"
-      content="Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
+      name="description"
+      content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
     />
 
     <!------------------------------------------------------------------------->
@@ -158,8 +166,8 @@
       body,
       html {
         margin: 0;
-        --ui-font: system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
-          Roboto, Helvetica, Arial, sans-serif;
+        --ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system,
+          Segoe UI, Roboto, Helvetica, Arial, sans-serif;
         font-family: var(--ui-font);
         -webkit-text-size-adjust: 100%;
 

BIN
public/og-fb-v1.png


BIN
public/og-general-v1.png


BIN
public/og-twitter-v1.png


+ 6 - 0
public/robots.txt

@@ -1,3 +1,9 @@
+User-agent: Twitterbot
+Disallow:
+
+User-agent: facebookexternalhit
+Disallow:
+
 user-agent: *
 Allow: /$
 Disallow: /

+ 6 - 6
src/actions/actionAlign.tsx

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

+ 27 - 16
src/actions/actionCanvas.tsx

@@ -1,7 +1,12 @@
 import { ColorPicker } from "../components/ColorPicker";
-import { eraser, zoomIn, zoomOut } from "../components/icons";
+import {
+  eraser,
+  MoonIcon,
+  SunIcon,
+  ZoomInIcon,
+  ZoomOutIcon,
+} from "../components/icons";
 import { ToolButton } from "../components/ToolButton";
-import { DarkModeToggle } from "../components/DarkModeToggle";
 import { MIN_ZOOM, THEME, ZOOM_STEP } from "../constants";
 import { getCommonBounds, getNonDeletedElements } from "../element";
 import { ExcalidrawElement } from "../element/types";
@@ -18,6 +23,8 @@ import { newElementWith } from "../element/mutateElement";
 import { getDefaultAppState, isEraserActive } from "../appState";
 import ClearCanvas from "../components/ClearCanvas";
 import clsx from "clsx";
+import MenuItem from "../components/MenuItem";
+import { getShortcutFromShortcutName } from "./shortcuts";
 
 export const actionChangeViewBackgroundColor = register({
   name: "changeViewBackgroundColor",
@@ -103,13 +110,13 @@ export const actionZoomIn = register({
   PanelComponent: ({ updateData }) => (
     <ToolButton
       type="button"
-      icon={zoomIn}
+      className="zoom-in-button zoom-button"
+      icon={ZoomInIcon}
       title={`${t("buttons.zoomIn")} — ${getShortcutKey("CtrlOrCmd++")}`}
       aria-label={t("buttons.zoomIn")}
       onClick={() => {
         updateData(null);
       }}
-      size="small"
     />
   ),
   keyTest: (event) =>
@@ -139,13 +146,13 @@ export const actionZoomOut = register({
   PanelComponent: ({ updateData }) => (
     <ToolButton
       type="button"
-      icon={zoomOut}
+      className="zoom-out-button zoom-button"
+      icon={ZoomOutIcon}
       title={`${t("buttons.zoomOut")} — ${getShortcutKey("CtrlOrCmd+-")}`}
       aria-label={t("buttons.zoomOut")}
       onClick={() => {
         updateData(null);
       }}
-      size="small"
     />
   ),
   keyTest: (event) =>
@@ -176,13 +183,12 @@ export const actionResetZoom = register({
     <Tooltip label={t("buttons.resetZoom")} style={{ height: "100%" }}>
       <ToolButton
         type="button"
-        className="reset-zoom-button"
+        className="reset-zoom-button zoom-button"
         title={t("buttons.resetZoom")}
         aria-label={t("buttons.resetZoom")}
         onClick={() => {
           updateData(null);
         }}
-        size="small"
       >
         {(appState.zoom.value * 100).toFixed(0)}%
       </ToolButton>
@@ -288,14 +294,19 @@ export const actionToggleTheme = register({
     };
   },
   PanelComponent: ({ appState, updateData }) => (
-    <div style={{ marginInlineStart: "0.25rem" }}>
-      <DarkModeToggle
-        value={appState.theme}
-        onChange={(theme) => {
-          updateData(theme);
-        }}
-      />
-    </div>
+    <MenuItem
+      label={
+        appState.theme === "dark"
+          ? t("buttons.lightMode")
+          : t("buttons.darkMode")
+      }
+      onClick={() => {
+        updateData(appState.theme === THEME.LIGHT ? THEME.DARK : THEME.LIGHT);
+      }}
+      icon={appState.theme === "dark" ? SunIcon : MoonIcon}
+      dataTestId="toggle-dark-mode"
+      shortcut={getShortcutFromShortcutName("toggleTheme")}
+    />
   ),
   keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D,
 });

+ 2 - 2
src/actions/actionDeleteSelected.tsx

@@ -1,7 +1,6 @@
 import { isSomeElementSelected } from "../scene";
 import { KEYS } from "../keys";
 import { ToolButton } from "../components/ToolButton";
-import { trash } from "../components/icons";
 import { t } from "../i18n";
 import { register } from "./register";
 import { getNonDeletedElements } from "../element";
@@ -13,6 +12,7 @@ import { LinearElementEditor } from "../element/linearElementEditor";
 import { fixBindingsAfterDeletion } from "../element/binding";
 import { isBoundToContainer } from "../element/typeChecks";
 import { updateActiveTool } from "../utils";
+import { TrashIcon } from "../components/icons";
 
 const deleteSelectedElements = (
   elements: readonly ExcalidrawElement[],
@@ -149,7 +149,7 @@ export const actionDeleteSelected = register({
   PanelComponent: ({ elements, appState, updateData }) => (
     <ToolButton
       type="button"
-      icon={trash}
+      icon={TrashIcon}
       title={t("labels.delete")}
       aria-label={t("labels.delete")}
       onClick={() => updateData(null)}

+ 2 - 2
src/actions/actionDistribute.tsx

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

+ 2 - 2
src/actions/actionDuplicateSelection.tsx

@@ -4,7 +4,6 @@ import { ExcalidrawElement } from "../element/types";
 import { duplicateElement, getNonDeletedElements } from "../element";
 import { getSelectedElements, isSomeElementSelected } from "../scene";
 import { ToolButton } from "../components/ToolButton";
-import { clone } from "../components/icons";
 import { t } from "../i18n";
 import { arrayToMap, getShortcutKey } from "../utils";
 import { LinearElementEditor } from "../element/linearElementEditor";
@@ -19,6 +18,7 @@ import { ActionResult } from "./types";
 import { GRID_SIZE } from "../constants";
 import { bindTextToShapeAfterDuplication } from "../element/textElement";
 import { isBoundToContainer } from "../element/typeChecks";
+import { DuplicateIcon } from "../components/icons";
 
 export const actionDuplicateSelection = register({
   name: "duplicateSelection",
@@ -49,7 +49,7 @@ export const actionDuplicateSelection = register({
   PanelComponent: ({ elements, appState, updateData }) => (
     <ToolButton
       type="button"
-      icon={clone}
+      icon={DuplicateIcon}
       title={`${t("labels.duplicateSelection")} — ${getShortcutKey(
         "CtrlOrCmd+D",
       )}`}

+ 8 - 8
src/actions/actionExport.tsx

@@ -1,4 +1,4 @@
-import { load, questionCircle, saveAs } from "../components/icons";
+import { LoadIcon, questionCircle, saveAs } from "../components/icons";
 import { ProjectName } from "../components/ProjectName";
 import { ToolButton } from "../components/ToolButton";
 import "../components/ToolIcon.scss";
@@ -19,6 +19,8 @@ import { ActiveFile } from "../components/ActiveFile";
 import { isImageFileHandle } from "../data/blob";
 import { nativeFileSystemSupported } from "../data/filesystem";
 import { Theme } from "../element/types";
+import MenuItem from "../components/MenuItem";
+import { getShortcutFromShortcutName } from "./shortcuts";
 
 export const actionChangeProjectName = register({
   name: "changeProjectName",
@@ -245,14 +247,12 @@ export const actionLoadScene = register({
   },
   keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
   PanelComponent: ({ updateData }) => (
-    <ToolButton
-      type="button"
-      icon={load}
-      title={t("buttons.load")}
-      aria-label={t("buttons.load")}
-      showAriaLabel={useDevice().isMobile}
+    <MenuItem
+      label={t("buttons.load")}
+      icon={LoadIcon}
       onClick={updateData}
-      data-testid="load-button"
+      dataTestId="load-button"
+      shortcut={getShortcutFromShortcutName("loadScene")}
     />
   ),
 });

+ 3 - 3
src/actions/actionHistory.tsx

@@ -1,5 +1,5 @@
 import { Action, ActionResult } from "./types";
-import { undo, redo } from "../components/icons";
+import { UndoIcon, RedoIcon } from "../components/icons";
 import { ToolButton } from "../components/ToolButton";
 import { t } from "../i18n";
 import History, { HistoryEntry } from "../history";
@@ -72,7 +72,7 @@ export const createUndoAction: ActionCreator = (history) => ({
   PanelComponent: ({ updateData, data }) => (
     <ToolButton
       type="button"
-      icon={undo}
+      icon={UndoIcon}
       aria-label={t("buttons.undo")}
       onClick={updateData}
       size={data?.size || "medium"}
@@ -94,7 +94,7 @@ export const createRedoAction: ActionCreator = (history) => ({
   PanelComponent: ({ updateData, data }) => (
     <ToolButton
       type="button"
-      icon={redo}
+      icon={RedoIcon}
       aria-label={t("buttons.redo")}
       onClick={updateData}
       size={data?.size || "medium"}

+ 18 - 8
src/actions/actionMenu.tsx

@@ -1,11 +1,12 @@
-import { menu, palette } from "../components/icons";
+import { HamburgerMenuIcon, HelpIcon, palette } from "../components/icons";
 import { ToolButton } from "../components/ToolButton";
 import { t } from "../i18n";
 import { showSelectedShapeActions, getNonDeletedElements } from "../element";
 import { register } from "./register";
 import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
 import { KEYS } from "../keys";
-import { HelpIcon } from "../components/HelpIcon";
+import { HelpButton } from "../components/HelpButton";
+import MenuItem from "../components/MenuItem";
 
 export const actionToggleCanvasMenu = register({
   name: "toggleCanvasMenu",
@@ -20,7 +21,7 @@ export const actionToggleCanvasMenu = register({
   PanelComponent: ({ appState, updateData }) => (
     <ToolButton
       type="button"
-      icon={menu}
+      icon={HamburgerMenuIcon}
       aria-label={t("buttons.menu")}
       onClick={updateData}
       selected={appState.openMenu === "canvas"}
@@ -74,19 +75,28 @@ export const actionShortcuts = register({
   name: "toggleShortcuts",
   trackEvent: { category: "menu", action: "toggleHelpDialog" },
   perform: (_elements, appState, _, { focusContainer }) => {
-    if (appState.showHelpDialog) {
+    if (appState.openDialog === "help") {
       focusContainer();
     }
     return {
       appState: {
         ...appState,
-        showHelpDialog: !appState.showHelpDialog,
+        openDialog: appState.openDialog === "help" ? null : "help",
       },
       commitToHistory: false,
     };
   },
-  PanelComponent: ({ updateData }) => (
-    <HelpIcon title={t("helpDialog.title")} onClick={updateData} />
-  ),
+  PanelComponent: ({ updateData, isInHamburgerMenu }) =>
+    isInHamburgerMenu ? (
+      <MenuItem
+        label={t("helpDialog.title")}
+        dataTestId="help-menu-item"
+        icon={HelpIcon}
+        onClick={updateData}
+        shortcut="?"
+      />
+    ) : (
+      <HelpButton title={t("helpDialog.title")} onClick={updateData} />
+    ),
   keyTest: (event) => event.key === KEYS.QUESTION_MARK,
 });

+ 57 - 61
src/actions/actionProperties.tsx

@@ -2,37 +2,41 @@ import { AppState } from "../../src/types";
 import { ButtonIconSelect } from "../components/ButtonIconSelect";
 import { ColorPicker } from "../components/ColorPicker";
 import { IconPicker } from "../components/IconPicker";
+// TODO barnabasmolnar/editor-redesign
+// TextAlignTopIcon, TextAlignBottomIcon,TextAlignMiddleIcon,
+// ArrowHead icons
 import {
   ArrowheadArrowIcon,
   ArrowheadBarIcon,
   ArrowheadDotIcon,
   ArrowheadTriangleIcon,
   ArrowheadNoneIcon,
-  EdgeRoundIcon,
-  EdgeSharpIcon,
-  FillCrossHatchIcon,
+  StrokeStyleDashedIcon,
+  StrokeStyleDottedIcon,
+  TextAlignTopIcon,
+  TextAlignBottomIcon,
+  TextAlignMiddleIcon,
   FillHachureIcon,
+  FillCrossHatchIcon,
   FillSolidIcon,
-  FontFamilyCodeIcon,
-  FontFamilyHandDrawnIcon,
-  FontFamilyNormalIcon,
-  FontSizeExtraLargeIcon,
-  FontSizeLargeIcon,
-  FontSizeMediumIcon,
-  FontSizeSmallIcon,
   SloppinessArchitectIcon,
   SloppinessArtistIcon,
   SloppinessCartoonistIcon,
-  StrokeStyleDashedIcon,
-  StrokeStyleDottedIcon,
-  StrokeStyleSolidIcon,
-  StrokeWidthIcon,
-  TextAlignCenterIcon,
+  StrokeWidthBaseIcon,
+  StrokeWidthBoldIcon,
+  StrokeWidthExtraBoldIcon,
+  FontSizeSmallIcon,
+  FontSizeMediumIcon,
+  FontSizeLargeIcon,
+  FontSizeExtraLargeIcon,
+  EdgeSharpIcon,
+  EdgeRoundIcon,
+  FreedrawIcon,
+  FontFamilyNormalIcon,
+  FontFamilyCodeIcon,
   TextAlignLeftIcon,
+  TextAlignCenterIcon,
   TextAlignRightIcon,
-  TextAlignTopIcon,
-  TextAlignBottomIcon,
-  TextAlignMiddleIcon,
 } from "../components/icons";
 import {
   DEFAULT_FONT_FAMILY,
@@ -307,17 +311,17 @@ export const actionChangeFillStyle = register({
           {
             value: "hachure",
             text: t("labels.hachure"),
-            icon: <FillHachureIcon theme={appState.theme} />,
+            icon: FillHachureIcon,
           },
           {
             value: "cross-hatch",
             text: t("labels.crossHatch"),
-            icon: <FillCrossHatchIcon theme={appState.theme} />,
+            icon: FillCrossHatchIcon,
           },
           {
             value: "solid",
             text: t("labels.solid"),
-            icon: <FillSolidIcon theme={appState.theme} />,
+            icon: FillSolidIcon,
           },
         ]}
         group="fill"
@@ -358,17 +362,17 @@ export const actionChangeStrokeWidth = register({
           {
             value: 1,
             text: t("labels.thin"),
-            icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={2} />,
+            icon: StrokeWidthBaseIcon,
           },
           {
             value: 2,
             text: t("labels.bold"),
-            icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={6} />,
+            icon: StrokeWidthBoldIcon,
           },
           {
             value: 4,
             text: t("labels.extraBold"),
-            icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={10} />,
+            icon: StrokeWidthExtraBoldIcon,
           },
         ]}
         value={getFormValue(
@@ -407,17 +411,17 @@ export const actionChangeSloppiness = register({
           {
             value: 0,
             text: t("labels.architect"),
-            icon: <SloppinessArchitectIcon theme={appState.theme} />,
+            icon: SloppinessArchitectIcon,
           },
           {
             value: 1,
             text: t("labels.artist"),
-            icon: <SloppinessArtistIcon theme={appState.theme} />,
+            icon: SloppinessArtistIcon,
           },
           {
             value: 2,
             text: t("labels.cartoonist"),
-            icon: <SloppinessCartoonistIcon theme={appState.theme} />,
+            icon: SloppinessCartoonistIcon,
           },
         ]}
         value={getFormValue(
@@ -455,17 +459,17 @@ export const actionChangeStrokeStyle = register({
           {
             value: "solid",
             text: t("labels.strokeStyle_solid"),
-            icon: <StrokeStyleSolidIcon theme={appState.theme} />,
+            icon: StrokeWidthBaseIcon,
           },
           {
             value: "dashed",
             text: t("labels.strokeStyle_dashed"),
-            icon: <StrokeStyleDashedIcon theme={appState.theme} />,
+            icon: StrokeStyleDashedIcon,
           },
           {
             value: "dotted",
             text: t("labels.strokeStyle_dotted"),
-            icon: <StrokeStyleDottedIcon theme={appState.theme} />,
+            icon: StrokeStyleDottedIcon,
           },
         ]}
         value={getFormValue(
@@ -535,25 +539,25 @@ export const actionChangeFontSize = register({
           {
             value: 16,
             text: t("labels.small"),
-            icon: <FontSizeSmallIcon theme={appState.theme} />,
+            icon: FontSizeSmallIcon,
             testId: "fontSize-small",
           },
           {
             value: 20,
             text: t("labels.medium"),
-            icon: <FontSizeMediumIcon theme={appState.theme} />,
+            icon: FontSizeMediumIcon,
             testId: "fontSize-medium",
           },
           {
             value: 28,
             text: t("labels.large"),
-            icon: <FontSizeLargeIcon theme={appState.theme} />,
+            icon: FontSizeLargeIcon,
             testId: "fontSize-large",
           },
           {
             value: 36,
             text: t("labels.veryLarge"),
-            icon: <FontSizeExtraLargeIcon theme={appState.theme} />,
+            icon: FontSizeExtraLargeIcon,
             testId: "fontSize-veryLarge",
           },
         ]}
@@ -658,17 +662,17 @@ export const actionChangeFontFamily = register({
       {
         value: FONT_FAMILY.Virgil,
         text: t("labels.handDrawn"),
-        icon: <FontFamilyHandDrawnIcon theme={appState.theme} />,
+        icon: FreedrawIcon,
       },
       {
         value: FONT_FAMILY.Helvetica,
         text: t("labels.normal"),
-        icon: <FontFamilyNormalIcon theme={appState.theme} />,
+        icon: FontFamilyNormalIcon,
       },
       {
         value: FONT_FAMILY.Cascadia,
         text: t("labels.code"),
-        icon: <FontFamilyCodeIcon theme={appState.theme} />,
+        icon: FontFamilyCodeIcon,
       },
     ];
 
@@ -739,17 +743,17 @@ export const actionChangeTextAlign = register({
             {
               value: "left",
               text: t("labels.left"),
-              icon: <TextAlignLeftIcon theme={appState.theme} />,
+              icon: TextAlignLeftIcon,
             },
             {
               value: "center",
               text: t("labels.center"),
-              icon: <TextAlignCenterIcon theme={appState.theme} />,
+              icon: TextAlignCenterIcon,
             },
             {
               value: "right",
               text: t("labels.right"),
-              icon: <TextAlignRightIcon theme={appState.theme} />,
+              icon: TextAlignRightIcon,
             },
           ]}
           value={getFormValue(
@@ -882,12 +886,12 @@ export const actionChangeSharpness = register({
           {
             value: "sharp",
             text: t("labels.sharp"),
-            icon: <EdgeSharpIcon theme={appState.theme} />,
+            icon: EdgeSharpIcon,
           },
           {
             value: "round",
             text: t("labels.round"),
-            icon: <EdgeRoundIcon theme={appState.theme} />,
+            icon: EdgeRoundIcon,
           },
         ]}
         value={getFormValue(
@@ -949,42 +953,38 @@ export const actionChangeArrowhead = register({
     return (
       <fieldset>
         <legend>{t("labels.arrowheads")}</legend>
-        <div className="iconSelectList">
+        <div className="iconSelectList buttonList">
           <IconPicker
             label="arrowhead_start"
             options={[
               {
                 value: null,
                 text: t("labels.arrowhead_none"),
-                icon: <ArrowheadNoneIcon theme={appState.theme} />,
+                icon: ArrowheadNoneIcon,
                 keyBinding: "q",
               },
               {
                 value: "arrow",
                 text: t("labels.arrowhead_arrow"),
-                icon: (
-                  <ArrowheadArrowIcon theme={appState.theme} flip={!isRTL} />
-                ),
+                icon: <ArrowheadArrowIcon flip={!isRTL} />,
                 keyBinding: "w",
               },
               {
                 value: "bar",
                 text: t("labels.arrowhead_bar"),
-                icon: <ArrowheadBarIcon theme={appState.theme} flip={!isRTL} />,
+                icon: <ArrowheadBarIcon flip={!isRTL} />,
                 keyBinding: "e",
               },
               {
                 value: "dot",
                 text: t("labels.arrowhead_dot"),
-                icon: <ArrowheadDotIcon theme={appState.theme} flip={!isRTL} />,
+                icon: <ArrowheadDotIcon flip={!isRTL} />,
                 keyBinding: "r",
               },
               {
                 value: "triangle",
                 text: t("labels.arrowhead_triangle"),
-                icon: (
-                  <ArrowheadTriangleIcon theme={appState.theme} flip={!isRTL} />
-                ),
+                icon: <ArrowheadTriangleIcon flip={!isRTL} />,
                 keyBinding: "t",
               },
             ]}
@@ -1007,34 +1007,30 @@ export const actionChangeArrowhead = register({
                 value: null,
                 text: t("labels.arrowhead_none"),
                 keyBinding: "q",
-                icon: <ArrowheadNoneIcon theme={appState.theme} />,
+                icon: ArrowheadNoneIcon,
               },
               {
                 value: "arrow",
                 text: t("labels.arrowhead_arrow"),
                 keyBinding: "w",
-                icon: (
-                  <ArrowheadArrowIcon theme={appState.theme} flip={isRTL} />
-                ),
+                icon: <ArrowheadArrowIcon flip={isRTL} />,
               },
               {
                 value: "bar",
                 text: t("labels.arrowhead_bar"),
                 keyBinding: "e",
-                icon: <ArrowheadBarIcon theme={appState.theme} flip={isRTL} />,
+                icon: <ArrowheadBarIcon flip={isRTL} />,
               },
               {
                 value: "dot",
                 text: t("labels.arrowhead_dot"),
                 keyBinding: "r",
-                icon: <ArrowheadDotIcon theme={appState.theme} flip={isRTL} />,
+                icon: <ArrowheadDotIcon flip={isRTL} />,
               },
               {
                 value: "triangle",
                 text: t("labels.arrowhead_triangle"),
-                icon: (
-                  <ArrowheadTriangleIcon theme={appState.theme} flip={isRTL} />
-                ),
+                icon: <ArrowheadTriangleIcon flip={isRTL} />,
                 keyBinding: "t",
               },
             ]}

+ 6 - 6
src/actions/actionZindex.tsx

@@ -10,10 +10,10 @@ import { t } from "../i18n";
 import { getShortcutKey } from "../utils";
 import { register } from "./register";
 import {
-  SendBackwardIcon,
+  BringForwardIcon,
   BringToFrontIcon,
+  SendBackwardIcon,
   SendToBackIcon,
-  BringForwardIcon,
 } from "../components/icons";
 
 export const actionSendBackward = register({
@@ -39,7 +39,7 @@ export const actionSendBackward = register({
       onClick={() => updateData(null)}
       title={`${t("labels.sendBackward")} — ${getShortcutKey("CtrlOrCmd+[")}`}
     >
-      <SendBackwardIcon theme={appState.theme} />
+      {SendBackwardIcon}
     </button>
   ),
 });
@@ -67,7 +67,7 @@ export const actionBringForward = register({
       onClick={() => updateData(null)}
       title={`${t("labels.bringForward")} — ${getShortcutKey("CtrlOrCmd+]")}`}
     >
-      <BringForwardIcon theme={appState.theme} />
+      {BringForwardIcon}
     </button>
   ),
 });
@@ -102,7 +102,7 @@ export const actionSendToBack = register({
           : getShortcutKey("CtrlOrCmd+Shift+[")
       }`}
     >
-      <SendToBackIcon theme={appState.theme} />
+      {SendToBackIcon}
     </button>
   ),
 });
@@ -138,7 +138,7 @@ export const actionBringToFront = register({
           : getShortcutKey("CtrlOrCmd+Shift+]")
       }`}
     >
-      <BringToFrontIcon theme={appState.theme} />
+      {BringToFrontIcon}
     </button>
   ),
 });

+ 7 - 1
src/actions/manager.tsx

@@ -135,8 +135,13 @@ export class ActionManager {
   /**
    * @param data additional data sent to the PanelComponent
    */
-  renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => {
+  renderAction = (
+    name: ActionName,
+    data?: PanelComponentProps["data"],
+    isInHamburgerMenu = false,
+  ) => {
     const canvasActions = this.app.props.UIOptions.canvasActions;
+
     if (
       this.actions[name] &&
       "PanelComponent" in this.actions[name] &&
@@ -169,6 +174,7 @@ export class ActionManager {
           updateData={updateData}
           appProps={this.app.props}
           data={data}
+          isInHamburgerMenu={isInHamburgerMenu}
         />
       );
     }

+ 37 - 28
src/actions/shortcuts.ts

@@ -3,36 +3,45 @@ import { isDarwin } from "../keys";
 import { getShortcutKey } from "../utils";
 import { ActionName } from "./types";
 
-export type ShortcutName = SubtypeOf<
-  ActionName,
-  | "cut"
-  | "copy"
-  | "paste"
-  | "copyStyles"
-  | "pasteStyles"
-  | "selectAll"
-  | "deleteSelectedElements"
-  | "duplicateSelection"
-  | "sendBackward"
-  | "bringForward"
-  | "sendToBack"
-  | "bringToFront"
-  | "copyAsPng"
-  | "copyAsSvg"
-  | "group"
-  | "ungroup"
-  | "gridMode"
-  | "zenMode"
-  | "stats"
-  | "addToLibrary"
-  | "viewMode"
-  | "flipHorizontal"
-  | "flipVertical"
-  | "hyperlink"
-  | "toggleLock"
->;
+export type ShortcutName =
+  | SubtypeOf<
+      ActionName,
+      | "toggleTheme"
+      | "loadScene"
+      | "cut"
+      | "copy"
+      | "paste"
+      | "copyStyles"
+      | "pasteStyles"
+      | "selectAll"
+      | "deleteSelectedElements"
+      | "duplicateSelection"
+      | "sendBackward"
+      | "bringForward"
+      | "sendToBack"
+      | "bringToFront"
+      | "copyAsPng"
+      | "copyAsSvg"
+      | "group"
+      | "ungroup"
+      | "gridMode"
+      | "zenMode"
+      | "stats"
+      | "addToLibrary"
+      | "viewMode"
+      | "flipHorizontal"
+      | "flipVertical"
+      | "hyperlink"
+      | "toggleLock"
+    >
+  | "saveScene"
+  | "imageExport";
 
 const shortcutMap: Record<ShortcutName, string[]> = {
+  toggleTheme: [getShortcutKey("Shit+Alt+D")],
+  saveScene: [getShortcutKey("CtrlOrCmd+S")],
+  loadScene: [getShortcutKey("CtrlOrCmd+O")],
+  imageExport: [getShortcutKey("CtrlOrCmd+Shift+E")],
   cut: [getShortcutKey("CtrlOrCmd+X")],
   copy: [getShortcutKey("CtrlOrCmd+C")],
   paste: [getShortcutKey("CtrlOrCmd+V")],

+ 3 - 1
src/actions/types.ts

@@ -124,7 +124,9 @@ export type PanelComponentProps = {
 
 export interface Action {
   name: ActionName;
-  PanelComponent?: React.FC<PanelComponentProps>;
+  PanelComponent?: React.FC<
+    PanelComponentProps & { isInHamburgerMenu: boolean }
+  >;
   perform: ActionFn;
   keyPriority?: number;
   keyTest?: (

+ 4 - 2
src/appState.ts

@@ -19,6 +19,7 @@ export const getDefaultAppState = (): Omit<
   "offsetTop" | "offsetLeft" | "width" | "height"
 > => {
   return {
+    showWelcomeScreen: false,
     theme: THEME.LIGHT,
     collaborators: new Map(),
     currentChartType: "bar",
@@ -67,6 +68,7 @@ export const getDefaultAppState = (): Omit<
     openMenu: null,
     openPopup: null,
     openSidebar: null,
+    openDialog: null,
     pasteDialog: { shown: false, data: null },
     previousSelectedElementIds: {},
     resizingElement: null,
@@ -77,7 +79,6 @@ export const getDefaultAppState = (): Omit<
     selectedGroupIds: {},
     selectionElement: null,
     shouldCacheIgnoreZoom: false,
-    showHelpDialog: false,
     showStats: false,
     startBoundElement: null,
     suggestedBindings: [],
@@ -110,6 +111,7 @@ const APP_STATE_STORAGE_CONF = (<
   T extends Record<keyof AppState, Values>,
 >(config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }) =>
   config)({
+  showWelcomeScreen: { browser: true, export: false, server: false },
   theme: { browser: true, export: false, server: false },
   collaborators: { browser: false, export: false, server: false },
   currentChartType: { browser: true, export: false, server: false },
@@ -160,6 +162,7 @@ const APP_STATE_STORAGE_CONF = (<
   openMenu: { browser: true, export: false, server: false },
   openPopup: { browser: false, export: false, server: false },
   openSidebar: { browser: true, export: false, server: false },
+  openDialog: { browser: false, export: false, server: false },
   pasteDialog: { browser: false, export: false, server: false },
   previousSelectedElementIds: { browser: true, export: false, server: false },
   resizingElement: { browser: false, export: false, server: false },
@@ -170,7 +173,6 @@ const APP_STATE_STORAGE_CONF = (<
   selectedGroupIds: { browser: true, export: false, server: false },
   selectionElement: { browser: false, export: false, server: false },
   shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
-  showHelpDialog: { browser: false, export: false, server: false },
   showStats: { browser: true, export: false, server: false },
   startBoundElement: { browser: false, export: false, server: false },
   suggestedBindings: { browser: false, export: false, server: false },

+ 6 - 15
src/clients.ts

@@ -11,27 +11,18 @@ export const getClientColors = (clientId: string, appState: AppState) => {
   // Naive way of getting an integer out of the clientId
   const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0);
 
-  // Skip transparent background.
-  const backgrounds = colors.elementBackground.slice(1);
-  const strokes = colors.elementStroke.slice(1);
+  // Skip transparent & gray colors
+  const backgrounds = colors.elementBackground.slice(3);
+  const strokes = colors.elementStroke.slice(3);
   return {
     background: backgrounds[sum % backgrounds.length],
     stroke: strokes[sum % strokes.length],
   };
 };
 
-export const getClientInitials = (username?: string | null) => {
-  if (!username) {
+export const getClientInitials = (userName?: string | null) => {
+  if (!userName) {
     return "?";
   }
-  const names = username.trim().split(" ");
-
-  if (names.length < 2) {
-    return names[0].substring(0, 2).toUpperCase();
-  }
-
-  const firstName = names[0];
-  const lastName = names[names.length - 1];
-
-  return (firstName[0] + lastName[0]).toUpperCase();
+  return userName.trim()[0].toUpperCase();
 };

+ 92 - 0
src/components/Actions.scss

@@ -0,0 +1,92 @@
+.zoom-actions,
+.undo-redo-buttons {
+  background-color: var(--island-bg-color);
+  border-radius: var(--border-radius-lg);
+}
+
+.zoom-button,
+.undo-redo-buttons button {
+  border: 1px solid var(--default-border-color) !important;
+  border-radius: 0 !important;
+  background-color: transparent !important;
+  font-size: 0.875rem !important;
+  width: var(--lg-button-size);
+  height: var(--lg-button-size);
+  svg {
+    width: var(--lg-icon-size) !important;
+    height: var(--lg-icon-size) !important;
+  }
+
+  .ToolIcon__icon {
+    width: 100%;
+    height: 100%;
+  }
+}
+
+.reset-zoom-button {
+  border-left: 0 !important;
+  border-right: 0 !important;
+  padding: 0 0.625rem !important;
+  width: 3.75rem !important;
+  justify-content: center;
+  color: var(--text-primary-color);
+}
+
+.zoom-out-button {
+  border-top-left-radius: var(--border-radius-lg) !important;
+  border-bottom-left-radius: var(--border-radius-lg) !important;
+
+  :root[dir="rtl"] & {
+    transform: scaleX(-1);
+  }
+
+  .ToolIcon__icon {
+    border-top-right-radius: 0 !important;
+    border-bottom-right-radius: 0 !important;
+  }
+}
+
+.zoom-in-button {
+  border-top-right-radius: var(--border-radius-lg) !important;
+  border-bottom-right-radius: var(--border-radius-lg) !important;
+
+  :root[dir="rtl"] & {
+    transform: scaleX(-1);
+  }
+
+  .ToolIcon__icon {
+    border-top-left-radius: 0 !important;
+    border-bottom-left-radius: 0 !important;
+  }
+}
+
+.undo-redo-buttons {
+  .undo-button-container button {
+    border-top-left-radius: var(--border-radius-lg) !important;
+    border-bottom-left-radius: var(--border-radius-lg) !important;
+    border-right: 0 !important;
+
+    :root[dir="rtl"] & {
+      transform: scaleX(-1);
+    }
+
+    .ToolIcon__icon {
+      border-top-right-radius: 0 !important;
+      border-bottom-right-radius: 0 !important;
+    }
+  }
+
+  .redo-button-container button {
+    border-top-right-radius: var(--border-radius-lg) !important;
+    border-bottom-right-radius: var(--border-radius-lg) !important;
+
+    :root[dir="rtl"] & {
+      transform: scaleX(-1);
+    }
+
+    .ToolIcon__icon {
+      border-top-left-radius: 0 !important;
+      border-bottom-left-radius: 0 !important;
+    }
+  }
+}

+ 37 - 17
src/components/Actions.tsx

@@ -28,6 +28,8 @@ import { trackEvent } from "../analytics";
 import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks";
 import clsx from "clsx";
 import { actionToggleZenMode } from "../actions";
+import "./Actions.scss";
+import { Tooltip } from "./Tooltip";
 
 export const SelectedShapeActions = ({
   appState,
@@ -79,12 +81,16 @@ export const SelectedShapeActions = ({
 
   return (
     <div className="panelColumn">
-      {((hasStrokeColor(appState.activeTool.type) &&
-        appState.activeTool.type !== "image" &&
-        commonSelectedType !== "image") ||
-        targetElements.some((element) => hasStrokeColor(element.type))) &&
-        renderAction("changeStrokeColor")}
-      {showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
+      <div>
+        {((hasStrokeColor(appState.activeTool.type) &&
+          appState.activeTool.type !== "image" &&
+          commonSelectedType !== "image") ||
+          targetElements.some((element) => hasStrokeColor(element.type))) &&
+          renderAction("changeStrokeColor")}
+      </div>
+      {showChangeBackgroundIcons && (
+        <div>{renderAction("changeBackgroundColor")}</div>
+      )}
       {showFillIcons && renderAction("changeFillStyle")}
 
       {(hasStrokeWidth(appState.activeTool.type) ||
@@ -163,7 +169,16 @@ export const SelectedShapeActions = ({
             )}
             {targetElements.length > 2 &&
               renderAction("distributeHorizontally")}
-            <div className="iconRow">
+            {/* breaks the row ˇˇ */}
+            <div style={{ flexBasis: "100%", height: 0 }} />
+            <div
+              style={{
+                display: "flex",
+                flexWrap: "wrap",
+                gap: ".5rem",
+                marginTop: "-0.5rem",
+              }}
+            >
               {renderAction("alignTop")}
               {renderAction("alignVerticallyCentered")}
               {renderAction("alignBottom")}
@@ -203,22 +218,23 @@ export const ShapesSwitcher = ({
   appState: AppState;
 }) => (
   <>
-    {SHAPES.map(({ value, icon, key }, index) => {
+    {SHAPES.map(({ value, icon, key, fillable }, index) => {
+      const numberKey = value === "eraser" ? 0 : index + 1;
       const label = t(`toolBar.${value}`);
       const letter = key && (typeof key === "string" ? key : key[0]);
       const shortcut = letter
-        ? `${capitalizeString(letter)} ${t("helpDialog.or")} ${index + 1}`
-        : `${index + 1}`;
+        ? `${capitalizeString(letter)} ${t("helpDialog.or")} ${numberKey}`
+        : `${numberKey}`;
       return (
         <ToolButton
-          className="Shape"
+          className={clsx("Shape", { fillable })}
           key={value}
           type="radio"
           icon={icon}
           checked={activeTool.type === value}
           name="editor-current-shape"
           title={`${capitalizeString(label)} — ${shortcut}`}
-          keyBindingLabel={`${index + 1}`}
+          keyBindingLabel={`${numberKey}`}
           aria-label={capitalizeString(label)}
           aria-keyshortcuts={shortcut}
           data-testid={value}
@@ -263,11 +279,11 @@ export const ZoomActions = ({
   renderAction: ActionManager["renderAction"];
   zoom: Zoom;
 }) => (
-  <Stack.Col gap={1}>
-    <Stack.Row gap={1} align="center">
+  <Stack.Col gap={1} className="zoom-actions">
+    <Stack.Row align="center">
       {renderAction("zoomOut")}
-      {renderAction("zoomIn")}
       {renderAction("resetZoom")}
+      {renderAction("zoomIn")}
     </Stack.Row>
   </Stack.Col>
 );
@@ -280,8 +296,12 @@ export const UndoRedoActions = ({
   className?: string;
 }) => (
   <div className={`undo-redo-buttons ${className}`}>
-    {renderAction("undo", { size: "small" })}
-    {renderAction("redo", { size: "small" })}
+    <div className="undo-button-container">
+      <Tooltip label={t("buttons.undo")}>{renderAction("undo")}</Tooltip>
+    </div>
+    <div className="redo-button-container">
+      <Tooltip label={t("buttons.redo")}> {renderAction("redo")}</Tooltip>
+    </div>
   </div>
 );
 

+ 12 - 17
src/components/ActiveFile.tsx

@@ -1,9 +1,11 @@
-import Stack from "../components/Stack";
-import { ToolButton } from "../components/ToolButton";
-import { save, file } from "../components/icons";
+// 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;
@@ -11,18 +13,11 @@ type ActiveFileProps = {
 };
 
 export const ActiveFile = ({ fileName, onSave }: ActiveFileProps) => (
-  <Stack.Row className="ActiveFile" gap={1} align="center">
-    <span className="ActiveFile__fileName">
-      {file}
-      <span>{fileName}</span>
-    </span>
-    <ToolButton
-      type="icon"
-      icon={save}
-      title={t("buttons.save")}
-      aria-label={t("buttons.save")}
-      onClick={onSave}
-      data-testid="save-button"
-    />
-  </Stack.Row>
+  <MenuItem
+    label={`${t("buttons.save")}`}
+    shortcut={getShortcutFromShortcutName("saveScene")}
+    dataTestId="save-button"
+    onClick={onSave}
+    icon={save}
+  />
 );

+ 30 - 1
src/components/App.tsx

@@ -266,6 +266,10 @@ import {
   isLocalLink,
 } from "../element/Hyperlink";
 import { shouldShowBoundingBox } from "../element/transformHandles";
+import { atom } from "jotai";
+
+export const isMenuOpenAtom = atom(false);
+export const isDropdownOpenAtom = atom(false);
 
 const deviceContextInitialValue = {
   isSmScreen: false,
@@ -571,6 +575,11 @@ class App extends React.Component<AppProps, AppState> {
                     library={this.library}
                     id={this.id}
                     onImageAction={this.onImageAction}
+                    renderWelcomeScreen={
+                      this.state.showWelcomeScreen &&
+                      this.state.activeTool.type === "selection" &&
+                      !this.scene.getElementsIncludingDeleted().length
+                    }
                   />
                   <div className="excalidraw-textEditorContainer" />
                   <div className="excalidraw-contextMenuContainer" />
@@ -1086,6 +1095,13 @@ class App extends React.Component<AppProps, AppState> {
 
   componentDidUpdate(prevProps: AppProps, prevState: AppState) {
     if (
+      !this.state.showWelcomeScreen &&
+      !this.scene.getElementsIncludingDeleted().length
+    ) {
+      this.setState({ showWelcomeScreen: true });
+    }
+
+    if (
       this.excalidrawContainerRef.current &&
       prevProps.UIOptions.dockedSidebarBreakpoint !==
         this.props.UIOptions.dockedSidebarBreakpoint
@@ -1276,6 +1292,10 @@ class App extends React.Component<AppProps, AppState> {
         );
       });
 
+    const selectionColor = getComputedStyle(
+      document.querySelector(".excalidraw")!,
+    ).getPropertyValue("--color-selection");
+
     renderScene(
       {
         elements: renderingElements,
@@ -1284,6 +1304,7 @@ class App extends React.Component<AppProps, AppState> {
         rc: this.rc!,
         canvas: this.canvas!,
         renderConfig: {
+          selectionColor,
           scrollX: this.state.scrollX,
           scrollY: this.state.scrollY,
           viewBackgroundColor: this.state.viewBackgroundColor,
@@ -1867,8 +1888,16 @@ class App extends React.Component<AppProps, AppState> {
 
       if (event.key === KEYS.QUESTION_MARK) {
         this.setState({
-          showHelpDialog: true,
+          openDialog: "help",
         });
+        return;
+      } else if (
+        event.key.toLowerCase() === KEYS.E &&
+        event.shiftKey &&
+        event[KEYS.CTRL_OR_CMD]
+      ) {
+        this.setState({ openDialog: "imageExport" });
+        return;
       }
 
       if (this.actionManager.handleKeyDown(event)) {

+ 7 - 4
src/components/Avatar.scss

@@ -2,16 +2,19 @@
 
 .excalidraw {
   .Avatar {
-    width: 2.5rem;
-    height: 2.5rem;
-    border-radius: 1.25rem;
+    width: 1.25rem;
+    height: 1.25rem;
+    border-radius: 100%;
+    outline: 2px solid var(--avatar-border-color);
+    outline-offset: 2px;
     display: flex;
     justify-content: center;
     align-items: center;
     color: $oc-white;
     cursor: pointer;
-    font-size: 0.8rem;
+    font-size: 0.625rem;
     font-weight: 500;
+    line-height: 1;
 
     &-img {
       width: 100%;

+ 2 - 4
src/components/Avatar.tsx

@@ -11,13 +11,11 @@ type AvatarProps = {
   src?: string;
 };
 
-export const Avatar = ({ color, border, onClick, name, src }: AvatarProps) => {
+export const Avatar = ({ color, onClick, name, src }: AvatarProps) => {
   const shortName = getClientInitials(name);
   const [error, setError] = useState(false);
   const loadImg = !error && src;
-  const style = loadImg
-    ? undefined
-    : { background: color, border: `1px solid ${border}` };
+  const style = loadImg ? undefined : { background: color };
   return (
     <div className="Avatar" style={style} onClick={onClick}>
       {loadImg ? (

+ 0 - 12
src/components/BackgroundPickerAndDarkModeToggle.tsx

@@ -1,12 +0,0 @@
-import { ActionManager } from "../actions/manager";
-
-export const BackgroundPickerAndDarkModeToggle = ({
-  actionManager,
-}: {
-  actionManager: ActionManager;
-}) => (
-  <div style={{ display: "flex" }}>
-    {actionManager.renderAction("changeViewBackgroundColor")}
-    {actionManager.renderAction("toggleTheme")}
-  </div>
-);

+ 2 - 0
src/components/CheckboxItem.scss

@@ -64,6 +64,8 @@
 
       color: #{$oc-blue-7};
 
+      border: 0;
+
       &:focus {
         box-shadow: 0 0 0 3px #{$oc-blue-7};
       }

+ 6 - 10
src/components/ClearCanvas.tsx

@@ -1,10 +1,9 @@
 import { useState } from "react";
 import { t } from "../i18n";
-import { useDevice } from "./App";
-import { trash } from "./icons";
-import { ToolButton } from "./ToolButton";
+import { TrashIcon } from "./icons";
 
 import ConfirmDialog from "./ConfirmDialog";
+import MenuItem from "./MenuItem";
 
 const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => {
   const [showDialog, setShowDialog] = useState(false);
@@ -14,14 +13,11 @@ const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => {
 
   return (
     <>
-      <ToolButton
-        type="button"
-        icon={trash}
-        title={t("buttons.clearReset")}
-        aria-label={t("buttons.clearReset")}
-        showAriaLabel={useDevice().isMobile}
+      <MenuItem
+        label={t("buttons.clearReset")}
+        icon={TrashIcon}
         onClick={toggleDialog}
-        data-testid="clear-canvas-button"
+        dataTestId="clear-canvas-button"
       />
 
       {showDialog && (

+ 48 - 3
src/components/CollabButton.scss

@@ -1,6 +1,51 @@
 @import "../css/variables.module";
 
 .excalidraw {
+  .collab-button {
+    @include outlineButtonStyles;
+    width: var(--lg-button-size);
+    height: var(--lg-button-size);
+
+    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 {
+      background-color: #0fb884;
+      border-color: #0fb884;
+
+      svg {
+        color: #fff;
+      }
+
+      &:hover,
+      &:active {
+        background-color: #0fb884;
+        border-color: #0fb884;
+      }
+    }
+  }
+
+  &.theme--dark {
+    .collab-button {
+      color: var(--color-gray-90);
+    }
+  }
+
   .CollabButton.is-collaborating {
     background-color: var(--button-special-active-bg-color);
 
@@ -24,9 +69,9 @@
     bottom: -5px;
     padding: 3px;
     border-radius: 50%;
-    background-color: $oc-green-6;
-    color: $oc-white;
-    font-size: 0.6em;
+    background-color: $oc-green-2;
+    color: $oc-green-9;
+    font-size: 0.6rem;
     font-family: "Cascadia";
   }
 }

+ 29 - 19
src/components/CollabButton.tsx

@@ -1,37 +1,47 @@
-import clsx from "clsx";
-import { ToolButton } from "./ToolButton";
 import { t } from "../i18n";
-import { useDevice } from "../components/App";
-import { users } from "./icons";
+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 (
     <>
-      <ToolButton
-        className={clsx("CollabButton", {
-          "is-collaborating": isCollaborating,
-        })}
-        onClick={onClick}
-        icon={users}
-        type="button"
-        title={t("labels.liveCollaboration")}
-        aria-label={t("labels.liveCollaboration")}
-        showAriaLabel={useDevice().isMobile}
-      >
-        {isCollaborating && (
-          <div className="CollabButton-collaborators">{collaboratorCount}</div>
-        )}
-      </ToolButton>
+      {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>
+      )}
     </>
   );
 };

+ 76 - 62
src/components/ColorPicker.scss

@@ -21,6 +21,23 @@
     display: grid;
     grid-template-columns: auto 1fr;
     align-items: center;
+    column-gap: 0.5rem;
+  }
+
+  .color-picker-control-container + .popover {
+    position: static;
+  }
+
+  .color-picker-popover-container {
+    margin-top: -0.25rem;
+
+    :root[dir="ltr"] & {
+      margin-left: 0.5rem;
+    }
+
+    :root[dir="rtl"] & {
+      margin-left: -3rem;
+    }
   }
 
   .color-picker-triangle {
@@ -30,20 +47,29 @@
     border-width: 0 9px 10px;
     border-color: transparent transparent var(--popup-bg-color);
     position: absolute;
-    top: -10px;
+    top: 10px;
 
     :root[dir="ltr"] & {
-      left: 12px;
+      transform: rotate(270deg);
+      left: -14px;
     }
 
     :root[dir="rtl"] & {
-      right: 12px;
+      transform: rotate(90deg);
+      right: -14px;
     }
   }
 
   .color-picker-triangle-shadow {
     border-color: transparent transparent transparentize($oc-black, 0.9);
-    top: -11px;
+
+    :root[dir="ltr"] & {
+      left: -14px;
+    }
+
+    :root[dir="rtl"] & {
+      right: -16px;
+    }
   }
 
   .color-picker-content--default {
@@ -119,16 +145,21 @@
   }
 
   .color-picker-hash {
-    background: var(--input-border-color);
-    height: 1.875rem;
-    width: 1.875rem;
+    height: var(--default-button-size);
+    flex-shrink: 0;
+    padding: 0.5rem 0.5rem 0.5rem 0.75rem;
+    border: 1px solid var(--default-border-color);
+    border-right: 0;
+    box-sizing: border-box;
 
     :root[dir="ltr"] & {
-      border-radius: 4px 0 0 4px;
+      border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg);
     }
 
     :root[dir="rtl"] & {
-      border-radius: 0 4px 4px 0;
+      border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0;
+      border-right: 1px solid var(--default-border-color);
+      border-left: 0;
     }
 
     color: var(--input-label-color);
@@ -138,81 +169,64 @@
     position: relative;
   }
 
-  .color-input-container:focus-within .color-picker-hash {
-    box-shadow: 0 0 0 2px var(--focus-highlight-color);
-  }
-
-  .color-input-container:focus-within .color-picker-hash::before,
-  .color-input-container:focus-within .color-picker-hash::after {
-    content: "";
-    width: 1px;
-    height: 100%;
-    position: absolute;
-    top: 0;
-  }
-
-  .color-input-container:focus-within .color-picker-hash::before {
-    background: var(--input-border-color);
-
-    :root[dir="ltr"] & {
-      right: -1px;
-    }
-
-    :root[dir="rtl"] & {
-      left: -1px;
-    }
-  }
-
-  .color-input-container:focus-within .color-picker-hash::after {
-    background: var(--input-bg-color);
-
-    :root[dir="ltr"] & {
-      right: -2px;
-    }
-
-    :root[dir="rtl"] & {
-      left: -2px;
-    }
-  }
-
   .color-input-container {
     display: flex;
+
+    &:focus-within {
+      box-shadow: 0 0 0 1px var(--color-primary-darkest);
+      border-radius: var(--border-radius-lg);
+    }
   }
 
   .color-picker-input {
-    width: 11ch; /* length of `transparent` */
+    box-sizing: border-box;
+    width: 100%;
     margin: 0;
-    font-size: 1rem;
-    background-color: var(--input-bg-color);
+    font-size: 0.875rem;
+    background-color: transparent;
     color: var(--text-primary-color);
     border: 0;
     outline: none;
-    height: 1.75em;
-    box-shadow: var(--input-border-color) 0 0 0 1px inset;
+    height: var(--default-button-size);
+    border: 1px solid var(--default-border-color);
+    border-left: 0;
+    letter-spacing: 0.4px;
 
     :root[dir="ltr"] & {
-      border-radius: 0 4px 4px 0;
+      border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0;
     }
 
     :root[dir="rtl"] & {
-      border-radius: 4px 0 0 4px;
+      border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg);
+      border-left: 1px solid var(--default-border-color);
+      border-right: 0;
     }
 
-    float: left;
-    padding: 1px;
-    padding-inline-start: 0.5em;
+    padding: 0.5rem;
+    padding-left: 0.25rem;
     appearance: none;
+
+    &:focus-visible {
+      box-shadow: none;
+    }
+  }
+
+  .color-picker-label-swatch-container {
+    border: 1px solid var(--default-border-color);
+    border-radius: var(--border-radius-lg);
+    width: var(--default-button-size);
+    height: var(--default-button-size);
+    box-sizing: border-box;
+    overflow: hidden;
   }
 
   .color-picker-label-swatch {
-    height: 1.875rem;
-    width: 1.875rem;
-    margin-inline-end: 0.25rem;
-    border: 1px solid $oc-gray-3;
-    position: relative;
+    @include outlineButtonStyles;
+    background-color: var(--swatch-color) !important;
     overflow: hidden;
-    background-color: transparent !important;
+    position: relative;
     filter: var(--theme-filter);
+    border: 0 !important;
 
     &:after {
       content: "";

+ 40 - 27
src/components/ColorPicker.tsx

@@ -365,17 +365,20 @@ export const ColorPicker = ({
   appState: AppState;
 }) => {
   const pickerButton = React.useRef<HTMLButtonElement>(null);
+  const coords = pickerButton.current?.getBoundingClientRect();
 
   return (
     <div>
       <div className="color-picker-control-container">
-        <button
-          className="color-picker-label-swatch"
-          aria-label={label}
-          style={color ? { "--swatch-color": color } : undefined}
-          onClick={() => setActive(!isActive)}
-          ref={pickerButton}
-        />
+        <div className="color-picker-label-swatch-container">
+          <button
+            className="color-picker-label-swatch"
+            aria-label={label}
+            style={color ? { "--swatch-color": color } : undefined}
+            onClick={() => setActive(!isActive)}
+            ref={pickerButton}
+          />
+        </div>
         <ColorInput
           color={color}
           label={label}
@@ -386,27 +389,37 @@ export const ColorPicker = ({
       </div>
       <React.Suspense fallback="">
         {isActive ? (
-          <Popover
-            onCloseRequest={(event) =>
-              event.target !== pickerButton.current && setActive(false)
-            }
+          <div
+            className="color-picker-popover-container"
+            style={{
+              position: "fixed",
+              top: coords?.top,
+              left: coords?.right,
+              zIndex: 1,
+            }}
           >
-            <Picker
-              colors={colors[type]}
-              color={color || null}
-              onChange={(changedColor) => {
-                onChange(changedColor);
-              }}
-              onClose={() => {
-                setActive(false);
-                pickerButton.current?.focus();
-              }}
-              label={label}
-              showInput={false}
-              type={type}
-              elements={elements}
-            />
-          </Popover>
+            <Popover
+              onCloseRequest={(event) =>
+                event.target !== pickerButton.current && setActive(false)
+              }
+            >
+              <Picker
+                colors={colors[type]}
+                color={color || null}
+                onChange={(changedColor) => {
+                  onChange(changedColor);
+                }}
+                onClose={() => {
+                  setActive(false);
+                  pickerButton.current?.focus();
+                }}
+                label={label}
+                showInput={false}
+                type={type}
+                elements={elements}
+              />
+            </Popover>
+          </div>
         ) : null}
       </React.Suspense>
     </div>

+ 1 - 27
src/components/ConfirmDialog.scss

@@ -4,34 +4,8 @@
   .confirm-dialog {
     &-buttons {
       display: flex;
-      padding: 0.2rem 0;
+      column-gap: 0.5rem;
       justify-content: flex-end;
     }
-    .ToolIcon__icon {
-      min-width: 2.5rem;
-      width: auto;
-      font-size: 1rem;
-    }
-
-    .ToolIcon_type_button {
-      margin-left: 0.8rem;
-      padding: 0 0.5rem;
-    }
-
-    &__content {
-      font-size: 1rem;
-    }
-
-    &--confirm.ToolIcon_type_button {
-      background-color: $oc-red-6;
-
-      &:hover {
-        background-color: $oc-red-8;
-      }
-
-      .ToolIcon__icon {
-        color: $oc-white;
-      }
-    }
   }
 }

+ 21 - 13
src/components/ConfirmDialog.tsx

@@ -1,8 +1,11 @@
 import { t } from "../i18n";
 import { Dialog, DialogProps } from "./Dialog";
-import { ToolButton } from "./ToolButton";
 
 import "./ConfirmDialog.scss";
+import DialogActionButton from "./DialogActionButton";
+import { isMenuOpenAtom } from "./App";
+import { isDropdownOpenAtom } from "./App";
+import { useSetAtom } from "jotai";
 
 interface Props extends Omit<DialogProps, "onCloseRequest"> {
   onConfirm: () => void;
@@ -20,6 +23,10 @@ const ConfirmDialog = (props: Props) => {
     className = "",
     ...rest
   } = props;
+
+  const setIsMenuOpen = useSetAtom(isMenuOpenAtom);
+  const setIsDropdownOpen = useSetAtom(isDropdownOpenAtom);
+
   return (
     <Dialog
       onCloseRequest={onCancel}
@@ -29,21 +36,22 @@ const ConfirmDialog = (props: Props) => {
     >
       {children}
       <div className="confirm-dialog-buttons">
-        <ToolButton
-          type="button"
-          title={cancelText}
-          aria-label={cancelText}
+        <DialogActionButton
           label={cancelText}
-          onClick={onCancel}
-          className="confirm-dialog--cancel"
+          onClick={() => {
+            setIsMenuOpen(false);
+            setIsDropdownOpen(false);
+            onCancel();
+          }}
         />
-        <ToolButton
-          type="button"
-          title={confirmText}
-          aria-label={confirmText}
+        <DialogActionButton
           label={confirmText}
-          onClick={onConfirm}
-          className="confirm-dialog--confirm"
+          onClick={() => {
+            setIsMenuOpen(false);
+            setIsDropdownOpen(false);
+            onConfirm();
+          }}
+          actionType="danger"
         />
       </div>
     </Dialog>

+ 5 - 62
src/components/Dialog.scss

@@ -7,68 +7,11 @@
   }
 
   .Dialog__title {
-    display: grid;
-    align-items: center;
-    margin-top: 0;
-    grid-template-columns: 1fr calc(var(--space-factor) * 7);
-    grid-gap: var(--metric);
-    padding: calc(var(--space-factor) * 2);
-    text-align: center;
-    font-variant: small-caps;
-    font-size: 1.2em;
-  }
-
-  .Dialog__titleContent {
-    flex: 1;
-  }
-
-  .Dialog .Modal__close {
-    color: var(--icon-fill-color);
     margin: 0;
-  }
-
-  .Dialog__content {
-    padding: 0 16px 16px;
-  }
-
-  @include isMobile {
-    .Dialog {
-      --metric: calc(var(--space-factor) * 4);
-      --inset-left: #{"max(var(--metric), var(--sal))"};
-      --inset-right: #{"max(var(--metric), var(--sar))"};
-    }
-
-    .Dialog__title {
-      grid-template-columns: calc(var(--space-factor) * 7) 1fr calc(
-          var(--space-factor) * 7
-        );
-      position: sticky;
-      top: 0;
-      padding: calc(var(--space-factor) * 2);
-      background: var(--island-bg-color);
-      font-size: 1.25em;
-
-      box-sizing: border-box;
-      border-bottom: 1px solid var(--button-gray-2);
-      z-index: 1;
-    }
-
-    .Dialog__titleContent {
-      text-align: center;
-    }
-
-    .Dialog .Island {
-      width: 100vw;
-      height: 100%;
-      box-sizing: border-box;
-      overflow-y: auto;
-      padding-left: #{"max(calc(var(--padding) * var(--space-factor)), var(--sal))"};
-      padding-right: #{"max(calc(var(--padding) * var(--space-factor)), var(--sar))"};
-      padding-bottom: #{"max(calc(var(--padding) * var(--space-factor)), var(--sab))"};
-    }
-
-    .Dialog .Modal__close {
-      order: -1;
-    }
+    text-align: left;
+    font-size: 1.25rem;
+    border-bottom: 1px solid var(--dialog-border-color);
+    padding: 0 0 0.75rem;
+    margin-bottom: 1.5rem;
   }
 }

+ 9 - 2
src/components/Dialog.tsx

@@ -5,11 +5,13 @@ import { t } from "../i18n";
 import { useExcalidrawContainer, useDevice } from "../components/App";
 import { KEYS } from "../keys";
 import "./Dialog.scss";
-import { back, close } from "./icons";
+import { back, CloseIcon } from "./icons";
 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";
 
 export interface DialogProps {
   children: React.ReactNode;
@@ -65,7 +67,12 @@ export const Dialog = (props: DialogProps) => {
     return () => islandNode.removeEventListener("keydown", handleKeyDown);
   }, [islandNode, props.autofocus]);
 
+  const setIsMenuOpen = useSetAtom(isMenuOpenAtom);
+  const setIsDropdownOpen = useSetAtom(isDropdownOpenAtom);
+
   const onClose = () => {
+    setIsMenuOpen(false);
+    setIsDropdownOpen(false);
     (lastActiveElement as HTMLElement).focus();
     props.onCloseRequest();
   };
@@ -88,7 +95,7 @@ export const Dialog = (props: DialogProps) => {
             title={t("buttons.close")}
             aria-label={t("buttons.close")}
           >
-            {useDevice().isMobile ? back : close}
+            {useDevice().isMobile ? back : CloseIcon}
           </button>
         </h2>
         <div className="Dialog__content">{props.children}</div>

+ 47 - 0
src/components/DialogActionButton.scss

@@ -0,0 +1,47 @@
+.excalidraw {
+  .Dialog__action-button {
+    position: relative;
+    display: flex;
+    column-gap: 0.5rem;
+    align-items: center;
+    padding: 0.5rem 1.5rem;
+    border: 1px solid var(--default-border-color);
+    background-color: transparent;
+    height: 3rem;
+    border-radius: var(--border-radius-lg);
+    letter-spacing: 0.4px;
+    color: inherit;
+    font-family: inherit;
+    font-size: 0.875rem;
+    font-weight: 600;
+    user-select: none;
+
+    svg {
+      display: block;
+      width: 1rem;
+      height: 1rem;
+    }
+
+    &--danger {
+      background-color: var(--color-danger);
+      border-color: var(--color-danger);
+      color: #fff;
+    }
+
+    &--primary {
+      background-color: var(--color-primary);
+      border-color: var(--color-primary);
+      color: #fff;
+    }
+  }
+
+  &.theme--dark {
+    .Dialog__action-button--danger {
+      color: var(--color-gray-100);
+    }
+
+    .Dialog__action-button--primary {
+      color: var(--color-gray-100);
+    }
+  }
+}

+ 46 - 0
src/components/DialogActionButton.tsx

@@ -0,0 +1,46 @@
+import clsx from "clsx";
+import { ReactNode } from "react";
+import "./DialogActionButton.scss";
+import Spinner from "./Spinner";
+
+interface DialogActionButtonProps {
+  label: string;
+  children?: ReactNode;
+  actionType?: "primary" | "danger";
+  isLoading?: boolean;
+}
+
+const DialogActionButton = ({
+  label,
+  onClick,
+  className,
+  children,
+  actionType,
+  type = "button",
+  isLoading,
+  ...rest
+}: DialogActionButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
+  const cs = actionType ? `Dialog__action-button--${actionType}` : "";
+
+  return (
+    <button
+      className={clsx("Dialog__action-button", cs, className)}
+      type={type}
+      aria-label={label}
+      onClick={onClick}
+      {...rest}
+    >
+      {children && (
+        <div style={isLoading ? { visibility: "hidden" } : {}}>{children}</div>
+      )}
+      <div style={isLoading ? { visibility: "hidden" } : {}}>{label}</div>
+      {isLoading && (
+        <div style={{ position: "absolute", inset: 0 }}>
+          <Spinner />
+        </div>
+      )}
+    </button>
+  );
+};
+
+export default DialogActionButton;

+ 19 - 0
src/components/EncryptedIcon.tsx

@@ -0,0 +1,19 @@
+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;

+ 2 - 0
src/components/ExportDialog.scss

@@ -91,6 +91,8 @@
   }
 
   button.ExportDialog-imageExportButton {
+    border: 0;
+
     width: 5rem;
     height: 5rem;
     margin: 0 0.2em;

+ 4 - 3
src/components/FixedSideContainer.scss

@@ -9,9 +9,10 @@
   }
 
   .FixedSideContainer_side_top {
-    left: var(--space-factor);
-    top: var(--space-factor);
-    right: var(--space-factor);
+    left: 1rem;
+    top: 1rem;
+    right: 1rem;
+    bottom: 1rem;
     z-index: 2;
   }
 

+ 28 - 25
src/components/Footer.tsx

@@ -1,5 +1,6 @@
 import clsx from "clsx";
 import { ActionManager } from "../actions/manager";
+import { t } from "../i18n";
 import { AppState, ExcalidrawProps } from "../types";
 import {
   ExitZenModeAction,
@@ -8,20 +9,23 @@ import {
   ZoomActions,
 } from "./Actions";
 import { useDevice } from "./App";
-import { Island } from "./Island";
+import { WelcomeScreenHelpArrow } from "./icons";
 import { Section } from "./Section";
 import Stack from "./Stack";
+import WelcomeScreenDecor from "./WelcomeScreenDecor";
 
 const Footer = ({
   appState,
   actionManager,
   renderCustomFooter,
   showExitZenModeBtn,
+  renderWelcomeScreen,
 }: {
   appState: AppState;
   actionManager: ActionManager;
   renderCustomFooter?: ExcalidrawProps["renderFooter"];
   showExitZenModeBtn: boolean;
+  renderWelcomeScreen: boolean;
 }) => {
   const device = useDevice();
   const showFinalize =
@@ -39,31 +43,19 @@ const Footer = ({
       >
         <Stack.Col gap={2}>
           <Section heading="canvasActions">
-            <Island padding={1}>
-              <ZoomActions
+            <ZoomActions
+              renderAction={actionManager.renderAction}
+              zoom={appState.zoom}
+            />
+
+            {!appState.viewModeEnabled && (
+              <UndoRedoActions
                 renderAction={actionManager.renderAction}
-                zoom={appState.zoom}
+                className={clsx("zen-mode-transition", {
+                  "layer-ui__wrapper__footer-left--transition-bottom":
+                    appState.zenModeEnabled,
+                })}
               />
-            </Island>
-            {!appState.viewModeEnabled && (
-              <>
-                <UndoRedoActions
-                  renderAction={actionManager.renderAction}
-                  className={clsx("zen-mode-transition", {
-                    "layer-ui__wrapper__footer-left--transition-bottom":
-                      appState.zenModeEnabled,
-                  })}
-                />
-
-                <div
-                  className={clsx("eraser-buttons zen-mode-transition", {
-                    "layer-ui__wrapper__footer-left--transition-left":
-                      appState.zenModeEnabled,
-                  })}
-                >
-                  {actionManager.renderAction("eraser", { size: "small" })}
-                </div>
-              </>
             )}
             {showFinalize && (
               <FinalizeAction
@@ -93,7 +85,18 @@ const Footer = ({
           "transition-right disable-pointerEvents": appState.zenModeEnabled,
         })}
       >
-        {actionManager.renderAction("toggleShortcuts")}
+        <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")}
+        </div>
       </div>
       <ExitZenModeAction
         actionManager={actionManager}

+ 4 - 4
src/components/HelpIcon.tsx → src/components/HelpButton.tsx

@@ -1,13 +1,13 @@
-import { questionCircle } from "../components/icons";
+import { HelpIcon } from "./icons";
 
-type HelpIconProps = {
+type HelpButtonProps = {
   title?: string;
   name?: string;
   id?: string;
   onClick?(): void;
 };
 
-export const HelpIcon = (props: HelpIconProps) => (
+export const HelpButton = (props: HelpButtonProps) => (
   <button
     className="help-icon"
     onClick={props.onClick}
@@ -15,6 +15,6 @@ export const HelpIcon = (props: HelpIconProps) => (
     title={`${props.title} — ?`}
     aria-label={props.title}
   >
-    {questionCircle}
+    {HelpIcon}
   </button>
 );

+ 104 - 45
src/components/HelpDialog.scss

@@ -1,56 +1,115 @@
 @import "../css/variables.module";
 
 .excalidraw {
-  .HelpDialog h3 {
-    border-bottom: 1px solid var(--button-gray-2);
-    padding-bottom: 4px;
-  }
+  .HelpDialog {
+    .Modal__content {
+      max-width: 960px;
+    }
 
-  .HelpDialog--island {
-    border: 1px solid var(--button-gray-2);
-    margin-bottom: 16px;
-  }
+    h3 {
+      margin: 1.5rem 0;
+      font-weight: bold;
+      font-size: 1.125rem;
+    }
 
-  .HelpDialog--island-title {
-    margin: 0;
-    padding: 4px;
-    background-color: var(--button-gray-1);
-    text-align: center;
-  }
+    &__header {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 0.75rem;
+    }
 
-  .HelpDialog--shortcut {
-    border-top: 1px solid var(--button-gray-2);
-  }
+    &__btn {
+      display: flex;
+      column-gap: 0.5rem;
+      align-items: center;
+      border: 1px solid var(--default-border-color);
+      padding: 0.625rem 1rem;
+      border-radius: var(--border-radius-lg);
+      color: var(--text-primary-color);
+      font-weight: 600;
+      font-size: 0.75rem;
+      letter-spacing: 0.4px;
 
-  .HelpDialog--key {
-    word-break: keep-all;
-    border: 1px solid var(--button-gray-2);
-    padding: 2px 8px;
-    margin: auto 4px;
-    background-color: var(--button-gray-1);
-    border-radius: 2px;
-    font-size: 0.8em;
-    min-height: 26px;
-    box-sizing: border-box;
-    display: flex;
-    align-items: center;
-    font-family: inherit;
-  }
+      &:hover {
+        text-decoration: none;
+      }
+    }
 
-  .HelpDialog--header {
-    display: flex;
-    flex-direction: row;
-    justify-content: space-evenly;
-    margin-bottom: 32px;
-    padding-bottom: 16px;
-  }
+    &__link-icon {
+      line-height: 0;
+      svg {
+        width: 1rem;
+        height: 1rem;
+      }
+    }
 
-  .HelpDialog--btn {
-    border: 1px solid var(--link-color);
-    padding: 8px 32px;
-    border-radius: 4px;
-  }
-  .HelpDialog--btn:hover {
-    text-decoration: none;
+    &__islands-container {
+      display: grid;
+      @media screen and (min-width: 1024px) {
+        grid-template-columns: 1fr 1fr;
+      }
+      grid-column-gap: 1.5rem;
+      grid-row-gap: 2rem;
+    }
+
+    @media screen and (min-width: 1024px) {
+      &__island--tools {
+        grid-area: 1 / 1 / 2 / 2;
+      }
+      &__island--view {
+        grid-area: 2 / 1 / 3 / 2;
+      }
+      &__island--editor {
+        grid-area: 1 / 2 / 3 / 3;
+      }
+    }
+
+    &__island {
+      h4 {
+        font-size: 1rem;
+        font-weight: bold;
+        margin: 0;
+        margin-bottom: 0.625rem;
+      }
+
+      &-content {
+        border: 1px solid var(--dialog-border-color);
+        border-radius: var(--border-radius-lg);
+      }
+    }
+
+    &__shortcut {
+      border-bottom: 1px solid var(--dialog-border-color);
+      padding: 0.375rem 0.75rem;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      font-size: 0.875rem;
+      column-gap: 0.5rem;
+
+      &:last-child {
+        border-bottom: none;
+      }
+    }
+
+    &__key-container {
+      display: flex;
+      align-items: center;
+      column-gap: 0.25rem;
+      flex-shrink: 0;
+    }
+
+    &__key {
+      display: flex;
+      box-sizing: border-box;
+      font-size: 0.625rem;
+      background-color: var(--color-primary-light);
+      border-radius: var(--border-radius-md);
+      padding: 0.5rem;
+      word-break: keep-all;
+      align-items: center;
+      font-family: inherit;
+      line-height: 1;
+    }
   }
 }

+ 313 - 346
src/components/HelpDialog.tsx

@@ -4,32 +4,36 @@ import { isDarwin, isWindows } from "../keys";
 import { Dialog } from "./Dialog";
 import { getShortcutKey } from "../utils";
 import "./HelpDialog.scss";
+import { ExternalLinkIcon } from "./icons";
 
 const Header = () => (
-  <div className="HelpDialog--header">
+  <div className="HelpDialog__header">
     <a
-      className="HelpDialog--btn"
+      className="HelpDialog__btn"
       href="https://github.com/excalidraw/excalidraw#documentation"
       target="_blank"
       rel="noopener noreferrer"
     >
       {t("helpDialog.documentation")}
+      <div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
     </a>
     <a
-      className="HelpDialog--btn"
+      className="HelpDialog__btn"
       href="https://blog.excalidraw.com"
       target="_blank"
       rel="noopener noreferrer"
     >
       {t("helpDialog.blog")}
+      <div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
     </a>
     <a
-      className="HelpDialog--btn"
+      className="HelpDialog__btn"
       href="https://github.com/excalidraw/excalidraw/issues"
       target="_blank"
       rel="noopener noreferrer"
     >
       {t("helpDialog.github")}
+      <div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
     </a>
   </div>
 );
@@ -37,88 +41,61 @@ const Header = () => (
 const Section = (props: { title: string; children: React.ReactNode }) => (
   <>
     <h3>{props.title}</h3>
-    {props.children}
+    <div className="HelpDialog__islands-container">{props.children}</div>
   </>
 );
 
-const Columns = (props: { children: React.ReactNode }) => (
-  <div
-    style={{
-      display: "flex",
-      flexDirection: "row",
-      flexWrap: "wrap",
-      justifyContent: "space-between",
-    }}
-  >
-    {props.children}
-  </div>
-);
-
-const Column = (props: { children: React.ReactNode }) => (
-  <div style={{ width: "49%" }}>{props.children}</div>
-);
-
 const ShortcutIsland = (props: {
   caption: string;
   children: React.ReactNode;
+  className?: string;
 }) => (
-  <div className="HelpDialog--island">
-    <h3 className="HelpDialog--island-title">{props.caption}</h3>
-    {props.children}
+  <div className={`HelpDialog__island ${props.className}`}>
+    <h4 className="HelpDialog__island-title">{props.caption}</h4>
+    <div className="HelpDialog__island-content">{props.children}</div>
   </div>
 );
 
-const Shortcut = (props: {
+function* intersperse(as: JSX.Element[][], delim: string | null) {
+  let first = true;
+  for (const x of as) {
+    if (!first) {
+      yield delim;
+    }
+    first = false;
+    yield x;
+  }
+}
+
+const Shortcut = ({
+  label,
+  shortcuts,
+  isOr = true,
+}: {
   label: string;
   shortcuts: string[];
-  isOr: boolean;
+  isOr?: boolean;
 }) => {
+  const splitShortcutKeys = shortcuts.map((shortcut) => {
+    const keys = shortcut.endsWith("++")
+      ? [...shortcut.slice(0, -2).split("+"), "+"]
+      : shortcut.split("+");
+
+    return keys.map((key) => <ShortcutKey key={key}>{key}</ShortcutKey>);
+  });
+
   return (
-    <div className="HelpDialog--shortcut">
-      <div
-        style={{
-          display: "flex",
-          margin: "0",
-          padding: "4px 8px",
-          alignItems: "center",
-        }}
-      >
-        <div
-          style={{
-            lineHeight: 1.4,
-          }}
-        >
-          {props.label}
-        </div>
-        <div
-          style={{
-            display: "flex",
-            flex: "0 0 auto",
-            justifyContent: "flex-end",
-            marginInlineStart: "auto",
-            minWidth: "30%",
-          }}
-        >
-          {props.shortcuts.map((shortcut, index) => (
-            <React.Fragment key={index}>
-              <ShortcutKey>{shortcut}</ShortcutKey>
-              {props.isOr &&
-                index !== props.shortcuts.length - 1 &&
-                t("helpDialog.or")}
-            </React.Fragment>
-          ))}
-        </div>
+    <div className="HelpDialog__shortcut">
+      <div>{label}</div>
+      <div className="HelpDialog__key-container">
+        {[...intersperse(splitShortcutKeys, isOr ? t("helpDialog.or") : null)]}
       </div>
     </div>
   );
 };
 
-Shortcut.defaultProps = {
-  isOr: true,
-};
-
 const ShortcutKey = (props: { children: React.ReactNode }) => (
-  <kbd className="HelpDialog--key" {...props} />
+  <kbd className="HelpDialog__key" {...props} />
 );
 
 export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
@@ -137,286 +114,276 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
       >
         <Header />
         <Section title={t("helpDialog.shortcuts")}>
-          <Columns>
-            <Column>
-              <ShortcutIsland caption={t("helpDialog.tools")}>
-                <Shortcut
-                  label={t("toolBar.selection")}
-                  shortcuts={["V", "1"]}
-                />
-                <Shortcut
-                  label={t("toolBar.rectangle")}
-                  shortcuts={["R", "2"]}
-                />
-                <Shortcut label={t("toolBar.diamond")} shortcuts={["D", "3"]} />
-                <Shortcut label={t("toolBar.ellipse")} shortcuts={["O", "4"]} />
-                <Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
-                <Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
-                <Shortcut
-                  label={t("toolBar.freedraw")}
-                  shortcuts={["Shift + P", "X", "7"]}
-                />
-                <Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
-                <Shortcut label={t("toolBar.image")} shortcuts={["9"]} />
-                <Shortcut label={t("toolBar.library")} shortcuts={["0"]} />
-                <Shortcut
-                  label={t("toolBar.eraser")}
-                  shortcuts={[getShortcutKey("E")]}
-                />
-                <Shortcut
-                  label={t("helpDialog.editSelectedShape")}
-                  shortcuts={[
-                    getShortcutKey("Enter"),
-                    t("helpDialog.doubleClick"),
-                  ]}
-                />
-                <Shortcut
-                  label={t("helpDialog.textNewLine")}
-                  shortcuts={[
-                    getShortcutKey("Enter"),
-                    getShortcutKey("Shift+Enter"),
-                  ]}
-                />
-                <Shortcut
-                  label={t("helpDialog.textFinish")}
-                  shortcuts={[
-                    getShortcutKey("Esc"),
-                    getShortcutKey("CtrlOrCmd+Enter"),
-                  ]}
-                />
-                <Shortcut
-                  label={t("helpDialog.curvedArrow")}
-                  shortcuts={[
-                    "A",
-                    t("helpDialog.click"),
-                    t("helpDialog.click"),
-                    t("helpDialog.click"),
-                  ]}
-                  isOr={false}
-                />
-                <Shortcut
-                  label={t("helpDialog.curvedLine")}
-                  shortcuts={[
-                    "L",
-                    t("helpDialog.click"),
-                    t("helpDialog.click"),
-                    t("helpDialog.click"),
-                  ]}
-                  isOr={false}
-                />
-                <Shortcut label={t("toolBar.lock")} shortcuts={["Q"]} />
-                <Shortcut
-                  label={t("helpDialog.preventBinding")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd")]}
-                />
-                <Shortcut
-                  label={t("toolBar.link")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+K")]}
-                />
-              </ShortcutIsland>
-              <ShortcutIsland caption={t("helpDialog.view")}>
-                <Shortcut
-                  label={t("buttons.zoomIn")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd++")]}
-                />
-                <Shortcut
-                  label={t("buttons.zoomOut")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+-")]}
-                />
-                <Shortcut
-                  label={t("buttons.resetZoom")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+0")]}
-                />
-                <Shortcut
-                  label={t("helpDialog.zoomToFit")}
-                  shortcuts={["Shift+1"]}
-                />
-                <Shortcut
-                  label={t("helpDialog.zoomToSelection")}
-                  shortcuts={["Shift+2"]}
-                />
-                <Shortcut label={t("buttons.fullScreen")} shortcuts={["F"]} />
-                <Shortcut
-                  label={t("buttons.zenMode")}
-                  shortcuts={[getShortcutKey("Alt+Z")]}
-                />
-                <Shortcut
-                  label={t("labels.showGrid")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
-                />
-                <Shortcut
-                  label={t("labels.viewMode")}
-                  shortcuts={[getShortcutKey("Alt+R")]}
-                />
-                <Shortcut
-                  label={t("labels.toggleTheme")}
-                  shortcuts={[getShortcutKey("Alt+Shift+D")]}
-                />
-                <Shortcut
-                  label={t("stats.title")}
-                  shortcuts={[getShortcutKey("Alt+/")]}
-                />
-              </ShortcutIsland>
-            </Column>
-            <Column>
-              <ShortcutIsland caption={t("helpDialog.editor")}>
-                <Shortcut
-                  label={t("labels.selectAll")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+A")]}
-                />
-                <Shortcut
-                  label={t("labels.multiSelect")}
-                  shortcuts={[getShortcutKey(`Shift+${t("helpDialog.click")}`)]}
-                />
-                <Shortcut
-                  label={t("helpDialog.deepSelect")}
-                  shortcuts={[
-                    getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`),
-                  ]}
-                />
-                <Shortcut
-                  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")]}
-                />
-                <Shortcut
-                  label={t("labels.copyStyles")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]}
-                />
-                <Shortcut
-                  label={t("labels.pasteStyles")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+Alt+V")]}
-                />
-                <Shortcut
-                  label={t("labels.delete")}
-                  shortcuts={[getShortcutKey("Del")]}
-                />
-                <Shortcut
-                  label={t("labels.sendToBack")}
-                  shortcuts={[
-                    isDarwin
-                      ? getShortcutKey("CtrlOrCmd+Alt+[")
-                      : getShortcutKey("CtrlOrCmd+Shift+["),
-                  ]}
-                />
-                <Shortcut
-                  label={t("labels.bringToFront")}
-                  shortcuts={[
-                    isDarwin
-                      ? getShortcutKey("CtrlOrCmd+Alt+]")
-                      : getShortcutKey("CtrlOrCmd+Shift+]"),
-                  ]}
-                />
-                <Shortcut
-                  label={t("labels.sendBackward")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+[")]}
-                />
-                <Shortcut
-                  label={t("labels.bringForward")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+]")]}
-                />
-                <Shortcut
-                  label={t("labels.alignTop")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Up")]}
-                />
-                <Shortcut
-                  label={t("labels.alignBottom")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Down")]}
-                />
-                <Shortcut
-                  label={t("labels.alignLeft")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Left")]}
-                />
-                <Shortcut
-                  label={t("labels.alignRight")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Right")]}
-                />
-                <Shortcut
-                  label={t("labels.duplicateSelection")}
-                  shortcuts={[
-                    getShortcutKey("CtrlOrCmd+D"),
-                    getShortcutKey(`Alt+${t("helpDialog.drag")}`),
-                  ]}
-                />
-                <Shortcut
-                  label={t("helpDialog.toggleElementLock")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+Shift+L")]}
-                />
-                <Shortcut
-                  label={t("buttons.undo")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+Z")]}
-                />
-                <Shortcut
-                  label={t("buttons.redo")}
-                  shortcuts={
-                    isWindows
-                      ? [
-                          getShortcutKey("CtrlOrCmd+Y"),
-                          getShortcutKey("CtrlOrCmd+Shift+Z"),
-                        ]
-                      : [getShortcutKey("CtrlOrCmd+Shift+Z")]
-                  }
-                />
-                <Shortcut
-                  label={t("labels.group")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+G")]}
-                />
-                <Shortcut
-                  label={t("labels.ungroup")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]}
-                />
-                <Shortcut
-                  label={t("labels.flipHorizontal")}
-                  shortcuts={[getShortcutKey("Shift+H")]}
-                />
-                <Shortcut
-                  label={t("labels.flipVertical")}
-                  shortcuts={[getShortcutKey("Shift+V")]}
-                />
-                <Shortcut
-                  label={t("labels.showStroke")}
-                  shortcuts={[getShortcutKey("S")]}
-                />
-                <Shortcut
-                  label={t("labels.showBackground")}
-                  shortcuts={[getShortcutKey("G")]}
-                />
-                <Shortcut
-                  label={t("labels.decreaseFontSize")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+Shift+<")]}
-                />
-                <Shortcut
-                  label={t("labels.increaseFontSize")}
-                  shortcuts={[getShortcutKey("CtrlOrCmd+Shift+>")]}
-                />
-              </ShortcutIsland>
-            </Column>
-          </Columns>
+          <ShortcutIsland
+            className="HelpDialog__island--tools"
+            caption={t("helpDialog.tools")}
+          >
+            <Shortcut label={t("toolBar.selection")} shortcuts={["V", "1"]} />
+            <Shortcut label={t("toolBar.rectangle")} shortcuts={["R", "2"]} />
+            <Shortcut label={t("toolBar.diamond")} shortcuts={["D", "3"]} />
+            <Shortcut label={t("toolBar.ellipse")} shortcuts={["O", "4"]} />
+            <Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
+            <Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
+            <Shortcut
+              label={t("toolBar.freedraw")}
+              shortcuts={["Shift + P", "X", "7"]}
+            />
+            <Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
+            <Shortcut label={t("toolBar.image")} shortcuts={["9"]} />
+            <Shortcut label={t("toolBar.library")} shortcuts={["0"]} />
+            <Shortcut
+              label={t("toolBar.eraser")}
+              shortcuts={[getShortcutKey("E")]}
+            />
+            <Shortcut
+              label={t("helpDialog.editSelectedShape")}
+              shortcuts={[getShortcutKey("Enter"), t("helpDialog.doubleClick")]}
+            />
+            <Shortcut
+              label={t("helpDialog.textNewLine")}
+              shortcuts={[
+                getShortcutKey("Enter"),
+                getShortcutKey("Shift+Enter"),
+              ]}
+            />
+            <Shortcut
+              label={t("helpDialog.textFinish")}
+              shortcuts={[
+                getShortcutKey("Esc"),
+                getShortcutKey("CtrlOrCmd+Enter"),
+              ]}
+            />
+            <Shortcut
+              label={t("helpDialog.curvedArrow")}
+              shortcuts={[
+                "A",
+                t("helpDialog.click"),
+                t("helpDialog.click"),
+                t("helpDialog.click"),
+              ]}
+              isOr={false}
+            />
+            <Shortcut
+              label={t("helpDialog.curvedLine")}
+              shortcuts={[
+                "L",
+                t("helpDialog.click"),
+                t("helpDialog.click"),
+                t("helpDialog.click"),
+              ]}
+              isOr={false}
+            />
+            <Shortcut label={t("toolBar.lock")} shortcuts={["Q"]} />
+            <Shortcut
+              label={t("helpDialog.preventBinding")}
+              shortcuts={[getShortcutKey("CtrlOrCmd")]}
+            />
+            <Shortcut
+              label={t("toolBar.link")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+K")]}
+            />
+          </ShortcutIsland>
+          <ShortcutIsland
+            className="HelpDialog__island--view"
+            caption={t("helpDialog.view")}
+          >
+            <Shortcut
+              label={t("buttons.zoomIn")}
+              shortcuts={[getShortcutKey("CtrlOrCmd++")]}
+            />
+            <Shortcut
+              label={t("buttons.zoomOut")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+-")]}
+            />
+            <Shortcut
+              label={t("buttons.resetZoom")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+0")]}
+            />
+            <Shortcut
+              label={t("helpDialog.zoomToFit")}
+              shortcuts={["Shift+1"]}
+            />
+            <Shortcut
+              label={t("helpDialog.zoomToSelection")}
+              shortcuts={["Shift+2"]}
+            />
+            <Shortcut label={t("buttons.fullScreen")} shortcuts={["F"]} />
+            <Shortcut
+              label={t("buttons.zenMode")}
+              shortcuts={[getShortcutKey("Alt+Z")]}
+            />
+            <Shortcut
+              label={t("labels.showGrid")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
+            />
+            <Shortcut
+              label={t("labels.viewMode")}
+              shortcuts={[getShortcutKey("Alt+R")]}
+            />
+            <Shortcut
+              label={t("labels.toggleTheme")}
+              shortcuts={[getShortcutKey("Alt+Shift+D")]}
+            />
+            <Shortcut
+              label={t("stats.title")}
+              shortcuts={[getShortcutKey("Alt+/")]}
+            />
+          </ShortcutIsland>
+          <ShortcutIsland
+            className="HelpDialog__island--editor"
+            caption={t("helpDialog.editor")}
+          >
+            <Shortcut
+              label={t("labels.selectAll")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+A")]}
+            />
+            <Shortcut
+              label={t("labels.multiSelect")}
+              shortcuts={[getShortcutKey(`Shift+${t("helpDialog.click")}`)]}
+            />
+            <Shortcut
+              label={t("helpDialog.deepSelect")}
+              shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`)]}
+            />
+            <Shortcut
+              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")]}
+            />
+            <Shortcut
+              label={t("labels.copyStyles")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]}
+            />
+            <Shortcut
+              label={t("labels.pasteStyles")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+Alt+V")]}
+            />
+            <Shortcut
+              label={t("labels.delete")}
+              shortcuts={[getShortcutKey("Del")]}
+            />
+            <Shortcut
+              label={t("labels.sendToBack")}
+              shortcuts={[
+                isDarwin
+                  ? getShortcutKey("CtrlOrCmd+Alt+[")
+                  : getShortcutKey("CtrlOrCmd+Shift+["),
+              ]}
+            />
+            <Shortcut
+              label={t("labels.bringToFront")}
+              shortcuts={[
+                isDarwin
+                  ? getShortcutKey("CtrlOrCmd+Alt+]")
+                  : getShortcutKey("CtrlOrCmd+Shift+]"),
+              ]}
+            />
+            <Shortcut
+              label={t("labels.sendBackward")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+[")]}
+            />
+            <Shortcut
+              label={t("labels.bringForward")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+]")]}
+            />
+            <Shortcut
+              label={t("labels.alignTop")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Up")]}
+            />
+            <Shortcut
+              label={t("labels.alignBottom")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Down")]}
+            />
+            <Shortcut
+              label={t("labels.alignLeft")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Left")]}
+            />
+            <Shortcut
+              label={t("labels.alignRight")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Right")]}
+            />
+            <Shortcut
+              label={t("labels.duplicateSelection")}
+              shortcuts={[
+                getShortcutKey("CtrlOrCmd+D"),
+                getShortcutKey(`Alt+${t("helpDialog.drag")}`),
+              ]}
+            />
+            <Shortcut
+              label={t("helpDialog.toggleElementLock")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+Shift+L")]}
+            />
+            <Shortcut
+              label={t("buttons.undo")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+Z")]}
+            />
+            <Shortcut
+              label={t("buttons.redo")}
+              shortcuts={
+                isWindows
+                  ? [
+                      getShortcutKey("CtrlOrCmd+Y"),
+                      getShortcutKey("CtrlOrCmd+Shift+Z"),
+                    ]
+                  : [getShortcutKey("CtrlOrCmd+Shift+Z")]
+              }
+            />
+            <Shortcut
+              label={t("labels.group")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+G")]}
+            />
+            <Shortcut
+              label={t("labels.ungroup")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]}
+            />
+            <Shortcut
+              label={t("labels.flipHorizontal")}
+              shortcuts={[getShortcutKey("Shift+H")]}
+            />
+            <Shortcut
+              label={t("labels.flipVertical")}
+              shortcuts={[getShortcutKey("Shift+V")]}
+            />
+            <Shortcut
+              label={t("labels.showStroke")}
+              shortcuts={[getShortcutKey("S")]}
+            />
+            <Shortcut
+              label={t("labels.showBackground")}
+              shortcuts={[getShortcutKey("G")]}
+            />
+            <Shortcut
+              label={t("labels.decreaseFontSize")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+Shift+<")]}
+            />
+            <Shortcut
+              label={t("labels.increaseFontSize")}
+              shortcuts={[getShortcutKey("CtrlOrCmd+Shift+>")]}
+            />
+          </ShortcutIsland>
         </Section>
       </Dialog>
     </>

+ 11 - 7
src/components/HintViewer.scss

@@ -14,20 +14,24 @@ $wide-viewport-width: 1000px;
     top: 100%;
     max-width: 100%;
     width: 100%;
-    margin-top: 6px;
+    margin-top: 0.5rem;
     text-align: center;
-    color: $oc-gray-6;
-    font-size: 0.8rem;
+    color: var(--color-gray-40);
+    font-size: 0.75rem;
 
     @include isMobile {
       position: static;
-      padding-right: 2em;
+      padding-right: 2rem;
     }
 
     > span {
-      padding: 0.2rem 0.4rem;
-      background-color: var(--overlay-bg-color);
-      border-radius: 4px;
+      padding: 0.25rem;
+    }
+  }
+
+  &.theme--dark {
+    .HintViewer {
+      color: var(--color-gray-60);
     }
   }
 }

+ 2 - 2
src/components/IconPicker.scss

@@ -10,7 +10,8 @@
   .picker {
     background: var(--popup-bg-color);
     border: 0 solid transparentize($oc-white, 0.75);
-    box-shadow: transparentize($oc-black, 0.75) 0 1px 4px;
+    // ˇˇ yeah, i dunno, open to suggestions here :D
+    box-shadow: rgb(0 0 0 / 25%) 2px 2px 4px 2px;
     border-radius: 4px;
     position: absolute;
   }
@@ -46,7 +47,6 @@
       margin: 0;
       width: 36px;
       height: 18px;
-      opacity: 0.6;
       pointer-events: none;
     }
   }

+ 6 - 3
src/components/IconPicker.tsx

@@ -4,6 +4,7 @@ import { Popover } from "./Popover";
 import "./IconPicker.scss";
 import { isArrowKey, KEYS } from "../keys";
 import { getLanguage } from "../i18n";
+import clsx from "clsx";
 
 function Picker<T>({
   options,
@@ -102,7 +103,9 @@ function Picker<T>({
       <div className="picker-content" ref={rGallery}>
         {options.map((option, i) => (
           <button
-            className="picker-option"
+            className={clsx("picker-option", {
+              active: value === option.value,
+            })}
             onClick={(event) => {
               (event.currentTarget as HTMLButtonElement).focus();
               onChange(option.value);
@@ -150,7 +153,7 @@ export function IconPicker<T>({
   const isRTL = getLanguage().rtl;
 
   return (
-    <label className={"picker-container"}>
+    <div>
       <button
         name={group}
         className={isActive ? "active" : ""}
@@ -184,6 +187,6 @@ export function IconPicker<T>({
           </>
         ) : null}
       </React.Suspense>
-    </label>
+    </div>
   );
 }

+ 6 - 19
src/components/ImageExportDialog.tsx

@@ -5,14 +5,12 @@ import { canvasToBlob } from "../data/blob";
 import { NonDeletedExcalidrawElement } from "../element/types";
 import { CanvasError } from "../errors";
 import { t } from "../i18n";
-import { useDevice } from "./App";
 import { getSelectedElements, isSomeElementSelected } from "../scene";
 import { exportToCanvas } from "../scene/export";
 import { AppState, BinaryFiles } from "../types";
 import { Dialog } from "./Dialog";
-import { clipboard, exportImage } from "./icons";
+import { clipboard } from "./icons";
 import Stack from "./Stack";
-import { ToolButton } from "./ToolButton";
 import "./ExportDialog.scss";
 import OpenColor from "open-color";
 import { CheckboxItem } from "./CheckboxItem";
@@ -221,6 +219,7 @@ const ImageExportModal = ({
 export const ImageExportDialog = ({
   elements,
   appState,
+  setAppState,
   files,
   exportPadding = DEFAULT_EXPORT_PADDING,
   actionManager,
@@ -229,6 +228,7 @@ export const ImageExportDialog = ({
   onExportToClipboard,
 }: {
   appState: AppState;
+  setAppState: React.Component<any, AppState>["setState"];
   elements: readonly NonDeletedExcalidrawElement[];
   files: BinaryFiles;
   exportPadding?: number;
@@ -237,26 +237,13 @@ export const ImageExportDialog = ({
   onExportToSvg: ExportCB;
   onExportToClipboard: ExportCB;
 }) => {
-  const [modalIsShown, setModalIsShown] = useState(false);
-
   const handleClose = React.useCallback(() => {
-    setModalIsShown(false);
-  }, []);
+    setAppState({ openDialog: null });
+  }, [setAppState]);
 
   return (
     <>
-      <ToolButton
-        onClick={() => {
-          setModalIsShown(true);
-        }}
-        data-testid="image-export-button"
-        icon={exportImage}
-        type="button"
-        aria-label={t("buttons.exportImage")}
-        showAriaLabel={useDevice().isMobile}
-        title={t("buttons.exportImage")}
-      />
-      {modalIsShown && (
+      {appState.openDialog === "imageExport" && (
         <Dialog onCloseRequest={handleClose} title={t("buttons.exportImage")}>
           <ImageExportModal
             elements={elements}

+ 1 - 0
src/components/Island.scss

@@ -1,6 +1,7 @@
 .excalidraw {
   .Island {
     --padding: 0;
+    box-sizing: border-box;
     background-color: var(--island-bg-color);
     box-shadow: var(--shadow-island);
     border-radius: var(--border-radius-lg);

+ 8 - 10
src/components/JSONExportDialog.tsx

@@ -1,10 +1,10 @@
 import React, { useState } from "react";
 import { NonDeletedExcalidrawElement } from "../element/types";
 import { t } from "../i18n";
-import { useDevice } from "./App";
+
 import { AppState, ExportOpts, BinaryFiles } from "../types";
 import { Dialog } from "./Dialog";
-import { exportFile, exportToFileIcon, link } from "./icons";
+import { ExportIcon, exportToFileIcon, LinkIcon } from "./icons";
 import { ToolButton } from "./ToolButton";
 import { actionSaveFileToDisk } from "../actions/actionExport";
 import { Card } from "./Card";
@@ -14,6 +14,7 @@ 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[],
@@ -63,7 +64,7 @@ const JSONExportModal = ({
         )}
         {onExportToBackend && (
           <Card color="pink">
-            <div className="Card-icon">{link}</div>
+            <div className="Card-icon">{LinkIcon}</div>
             <h2>{t("exportDialog.link_title")}</h2>
             <div className="Card-details">{t("exportDialog.link_details")}</div>
             <ToolButton
@@ -109,16 +110,13 @@ export const JSONExportDialog = ({
 
   return (
     <>
-      <ToolButton
+      <MenuItem
+        icon={ExportIcon}
+        label={t("buttons.export")}
         onClick={() => {
           setModalIsShown(true);
         }}
-        data-testid="json-export-button"
-        icon={exportFile}
-        type="button"
-        aria-label={t("buttons.export")}
-        showAriaLabel={useDevice().isMobile}
-        title={t("buttons.export")}
+        dataTestId="json-export-button"
       />
       {modalIsShown && (
         <Dialog onCloseRequest={handleClose} title={t("buttons.export")}>

+ 2 - 12
src/components/LayerUI.scss

@@ -16,8 +16,10 @@
     height: 100%;
     pointer-events: none;
     z-index: var(--zIndex-layerUI);
+
     &__top-right {
       display: flex;
+      gap: 0.75rem;
     }
 
     &__footer {
@@ -48,13 +50,6 @@
         transform: translate(-999px, 0);
       }
 
-      :root[dir="ltr"] &.layer-ui__wrapper__footer-left--transition-left {
-        transform: translate(-76px, 0);
-      }
-      :root[dir="rtl"] &.layer-ui__wrapper__footer-left--transition-left {
-        transform: translate(76px, 0);
-      }
-
       &.layer-ui__wrapper__footer-left--transition-bottom {
         transform: translate(0, 92px);
       }
@@ -97,14 +92,9 @@
       pointer-events: all;
     }
 
-    .layer-ui__wrapper__footer-left {
-      margin-bottom: 0.2em;
-    }
-
     .layer-ui__wrapper__footer-right {
       margin-top: auto;
       margin-bottom: auto;
-      margin-inline-end: 1em;
     }
   }
 }

+ 183 - 113
src/components/LayerUI.tsx

@@ -11,7 +11,6 @@ import { ExportType } from "../scene/types";
 import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
 import { muteFSAbortError } from "../utils";
 import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
-import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
 import CollabButton from "./CollabButton";
 import { ErrorDialog } from "./ErrorDialog";
 import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
@@ -36,13 +35,26 @@ import "./LayerUI.scss";
 import "./Toolbar.scss";
 import { PenModeButton } from "./PenModeButton";
 import { trackEvent } from "../analytics";
-import { useDevice } from "../components/App";
+import { isMenuOpenAtom, 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 { 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";
 
 interface LayerUIProps {
   actionManager: ActionManager;
@@ -68,6 +80,7 @@ interface LayerUIProps {
   library: Library;
   id: string;
   onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
+  renderWelcomeScreen: boolean;
 }
 const LayerUI = ({
   actionManager,
@@ -92,6 +105,7 @@ const LayerUI = ({
   library,
   id,
   onImageAction,
+  renderWelcomeScreen,
 }: LayerUIProps) => {
   const device = useDevice();
 
@@ -151,6 +165,7 @@ const LayerUI = ({
       <ImageExportDialog
         elements={elements}
         appState={appState}
+        setAppState={setAppState}
         files={files}
         actionManager={actionManager}
         onExportToPng={createExporter("png")}
@@ -160,71 +175,100 @@ const LayerUI = ({
     );
   };
 
-  const Separator = () => {
-    return <div style={{ width: ".625em" }} />;
-  };
+  const [isMenuOpen, setIsMenuOpen] = useAtom(isMenuOpenAtom);
+  const menuRef = useOutsideClickHook(() => setIsMenuOpen(false));
 
-  const renderViewModeCanvasActions = () => {
-    return (
-      <Section
-        heading="canvasActions"
-        className={clsx("zen-mode-transition", {
+  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"
       >
-        {/* the zIndex ensures this menu has higher stacking order,
-         see https://github.com/excalidraw/excalidraw/pull/1445 */}
-        <Island padding={2} style={{ zIndex: 1 }}>
-          <Stack.Col gap={4}>
-            <Stack.Row gap={1} justifyContent="space-between">
-              {renderJSONExportDialog()}
-              {renderImageExportDialog()}
-            </Stack.Row>
-          </Stack.Col>
-        </Island>
-      </Section>
-    );
-  };
+        {HamburgerMenuIcon}
+      </button>
 
-  const renderCanvasActions = () => (
-    <Section
-      heading="canvasActions"
-      className={clsx("zen-mode-transition", {
-        "transition-left": appState.zenModeEnabled,
-      })}
-    >
-      {/* the zIndex ensures this menu has higher stacking order,
+      {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 padding={2} style={{ zIndex: 1 }}>
-        <Stack.Col gap={4}>
-          <Stack.Row gap={1} justifyContent="space-between">
-            {actionManager.renderAction("clearCanvas")}
-            <Separator />
-            {actionManager.renderAction("loadScene")}
-            {renderJSONExportDialog()}
-            {renderImageExportDialog()}
-            <Separator />
-            {onCollabButtonClick && (
-              <CollabButton
-                isCollaborating={isCollaborating}
-                collaboratorCount={appState.collaborators.size}
-                onClick={onCollabButtonClick}
+            <Island
+              className="menu-container"
+              padding={2}
+              style={{ zIndex: 1 }}
+            >
+              {actionManager.renderAction("loadScene")}
+              {/* // TODO barnabasmolnar/editor-redesign  */}
+              {/* is this fine here? */}
+              {appState.fileHandle &&
+                actionManager.renderAction("saveToActiveFile")}
+              {renderJSONExportDialog()}
+              <MenuItem
+                label={t("buttons.exportImage")}
+                icon={ExportImageIcon}
+                dataTestId="image-export-button"
+                onClick={() => setAppState({ openDialog: "imageExport" })}
+                shortcut={getShortcutFromShortcutName("imageExport")}
               />
-            )}
-          </Stack.Row>
-          <BackgroundPickerAndDarkModeToggle actionManager={actionManager} />
-          {appState.fileHandle && (
-            <>{actionManager.renderAction("saveToActiveFile")}</>
-          )}
-        </Stack.Col>
-      </Island>
-    </Section>
+              {onCollabButtonClick && (
+                <CollabButton
+                  isCollaborating={isCollaborating}
+                  collaboratorCount={appState.collaborators.size}
+                  onClick={onCollabButtonClick}
+                />
+              )}
+              {actionManager.renderAction("toggleShortcuts", undefined, true)}
+              {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>
+                <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>
+      )}
+    </div>
   );
 
   const renderSelectedShapeActions = () => (
     <Section
       heading="selectedShapeActions"
-      className={clsx("zen-mode-transition", {
+      className={clsx("selected-shape-actions zen-mode-transition", {
         "transition-left": appState.zenModeEnabled,
       })}
     >
@@ -232,10 +276,9 @@ const LayerUI = ({
         className={CLASSES.SHAPE_ACTIONS_MENU}
         padding={2}
         style={{
-          // we want to make sure this doesn't overflow so subtracting 200
-          // which is approximately height of zoom footer and top left menu items with some buffer
-          // if active file name is displayed, subtracting 248 to account for its height
-          maxHeight: `${appState.height - (appState.fileHandle ? 248 : 200)}px`,
+          // we want to make sure this doesn't overflow so subtracting the
+          // approximate height of hamburgerMenu + footer
+          maxHeight: `${appState.height - 166}px`,
         }}
       >
         <SelectedShapeActions
@@ -255,74 +298,89 @@ const LayerUI = ({
 
     return (
       <FixedSideContainer side="top">
+        {renderWelcomeScreen && !appState.isLoading && (
+          <WelcomeScreen actionManager={actionManager} />
+        )}
         <div className="App-menu App-menu_top">
           <Stack.Col
-            gap={4}
+            gap={6}
             className={clsx({
               "disable-pointerEvents": appState.zenModeEnabled,
             })}
           >
-            {appState.viewModeEnabled
-              ? renderViewModeCanvasActions()
-              : renderCanvasActions()}
+            {renderCanvasActions()}
             {shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
           </Stack.Col>
           {!appState.viewModeEnabled && (
-            <Section heading="shapes">
+            <Section heading="shapes" className="shapes-section">
               {(heading: React.ReactNode) => (
-                <Stack.Col gap={4} align="start">
-                  <Stack.Row
-                    gap={1}
-                    className={clsx("App-toolbar-container", {
-                      "zen-mode": appState.zenModeEnabled,
-                    })}
+                <div style={{ position: "relative" }}>
+                  <WelcomeScreenDecor
+                    shouldRender={renderWelcomeScreen && !appState.isLoading}
                   >
-                    <PenModeButton
-                      zenModeEnabled={appState.zenModeEnabled}
-                      checked={appState.penMode}
-                      onChange={onPenModeToggle}
-                      title={t("toolBar.penMode")}
-                      penDetected={appState.penDetected}
-                    />
-                    <LockButton
-                      zenModeEnabled={appState.zenModeEnabled}
-                      checked={appState.activeTool.locked}
-                      onChange={() => onLockToggle()}
-                      title={t("toolBar.lock")}
-                    />
-                    <Island
-                      padding={1}
-                      className={clsx("App-toolbar", {
+                    <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>
+
+                  <Stack.Col gap={4} align="start">
+                    <Stack.Row
+                      gap={1}
+                      className={clsx("App-toolbar-container", {
                         "zen-mode": appState.zenModeEnabled,
                       })}
                     >
-                      <HintViewer
-                        appState={appState}
-                        elements={elements}
-                        isMobile={device.isMobile}
-                        device={device}
-                      />
-                      {heading}
-                      <Stack.Row gap={1}>
-                        <ShapesSwitcher
+                      <Island
+                        padding={1}
+                        className={clsx("App-toolbar", {
+                          "zen-mode": appState.zenModeEnabled,
+                        })}
+                      >
+                        <HintViewer
                           appState={appState}
-                          canvas={canvas}
-                          activeTool={appState.activeTool}
-                          setAppState={setAppState}
-                          onImageAction={({ pointerType }) => {
-                            onImageAction({
-                              insertOnCanvasDirectly: pointerType !== "mouse",
-                            });
-                          }}
+                          elements={elements}
+                          isMobile={device.isMobile}
+                          device={device}
                         />
-                      </Stack.Row>
-                    </Island>
-                    <LibraryButton
-                      appState={appState}
-                      setAppState={setAppState}
-                    />
-                  </Stack.Row>
-                </Stack.Col>
+                        {heading}
+                        <Stack.Row gap={1}>
+                          <PenModeButton
+                            zenModeEnabled={appState.zenModeEnabled}
+                            checked={appState.penMode}
+                            onChange={onPenModeToggle}
+                            title={t("toolBar.penMode")}
+                            penDetected={appState.penDetected}
+                          />
+                          <LockButton
+                            zenModeEnabled={appState.zenModeEnabled}
+                            checked={appState.activeTool.locked}
+                            onChange={() => onLockToggle()}
+                            title={t("toolBar.lock")}
+                          />
+                          <div className="App-toolbar__divider"></div>
+
+                          <ShapesSwitcher
+                            appState={appState}
+                            canvas={canvas}
+                            activeTool={appState.activeTool}
+                            setAppState={setAppState}
+                            onImageAction={({ pointerType }) => {
+                              onImageAction({
+                                insertOnCanvasDirectly: pointerType !== "mouse",
+                              });
+                            }}
+                          />
+                          {/* {actionManager.renderAction("eraser", {
+                          // size: "small",
+                        })} */}
+                        </Stack.Row>
+                      </Island>
+                    </Stack.Row>
+                  </Stack.Col>
+                </div>
               )}
             </Section>
           )}
@@ -338,7 +396,16 @@ const LayerUI = ({
               collaborators={appState.collaborators}
               actionManager={actionManager}
             />
+            {onCollabButtonClick && (
+              <CollabButton
+                isInHamburgerMenu={false}
+                isCollaborating={isCollaborating}
+                collaboratorCount={appState.collaborators.size}
+                onClick={onCollabButtonClick}
+              />
+            )}
             {renderTopRightUI?.(device.isMobile, appState)}
+            <LibraryButton appState={appState} setAppState={setAppState} />
           </div>
         </div>
       </FixedSideContainer>
@@ -371,13 +438,14 @@ const LayerUI = ({
           onClose={() => setAppState({ errorMessage: null })}
         />
       )}
-      {appState.showHelpDialog && (
+      {appState.openDialog === "help" && (
         <HelpDialog
           onClose={() => {
-            setAppState({ showHelpDialog: false });
+            setAppState({ openDialog: null });
           }}
         />
       )}
+      {renderImageExportDialog()}
       {appState.pasteDialog.shown && (
         <PasteChartDialog
           setAppState={setAppState}
@@ -392,6 +460,7 @@ const LayerUI = ({
       )}
       {device.isMobile && (
         <MobileMenu
+          renderWelcomeScreen={renderWelcomeScreen}
           appState={appState}
           elements={elements}
           actionManager={actionManager}
@@ -433,6 +502,7 @@ const LayerUI = ({
           >
             {renderFixedSideContainer()}
             <Footer
+              renderWelcomeScreen={renderWelcomeScreen}
               appState={appState}
               actionManager={actionManager}
               renderCustomFooter={renderCustomFooter}

+ 32 - 0
src/components/LibraryButton.scss

@@ -0,0 +1,32 @@
+@import "../css/variables.module";
+
+.library-button {
+  @include outlineButtonStyles;
+
+  background-color: var(--island-bg-color);
+
+  width: auto;
+  height: var(--lg-button-size);
+
+  display: flex;
+  align-items: center;
+  gap: 0.5rem;
+
+  line-height: 0;
+
+  font-size: 0.75rem;
+  letter-spacing: 0.4px;
+
+  svg {
+    width: var(--lg-icon-size);
+    height: var(--lg-icon-size);
+  }
+
+  &__label {
+    display: none;
+
+    @media screen and (min-width: 1024px) {
+      display: block;
+    }
+  }
+}

+ 17 - 21
src/components/LibraryButton.tsx

@@ -1,19 +1,11 @@
 import React from "react";
-import clsx from "clsx";
 import { t } from "../i18n";
 import { AppState } from "../types";
 import { capitalizeString } from "../utils";
 import { trackEvent } from "../analytics";
 import { useDevice } from "./App";
-
-const LIBRARY_ICON = (
-  <svg viewBox="0 0 576 512">
-    <path
-      fill="currentColor"
-      d="M542.22 32.05c-54.8 3.11-163.72 14.43-230.96 55.59-4.64 2.84-7.27 7.89-7.27 13.17v363.87c0 11.55 12.63 18.85 23.28 13.49 69.18-34.82 169.23-44.32 218.7-46.92 16.89-.89 30.02-14.43 30.02-30.66V62.75c.01-17.71-15.35-31.74-33.77-30.7zM264.73 87.64C197.5 46.48 88.58 35.17 33.78 32.05 15.36 31.01 0 45.04 0 62.75V400.6c0 16.24 13.13 29.78 30.02 30.66 49.49 2.6 149.59 12.11 218.77 46.95 10.62 5.35 23.21-1.94 23.21-13.46V100.63c0-5.29-2.62-10.14-7.27-12.99z"
-    ></path>
-  </svg>
-);
+import "./LibraryButton.scss";
+import { LibraryIcon } from "./icons";
 
 export const LibraryButton: React.FC<{
   appState: AppState;
@@ -21,17 +13,16 @@ export const LibraryButton: React.FC<{
   isMobile?: boolean;
 }> = ({ appState, setAppState, isMobile }) => {
   const device = useDevice();
+  const showLabel = !isMobile;
+
+  // TODO barnabasmolnar/redesign
+  // not great, toolbar jumps in a jarring manner
+  if (appState.isSidebarDocked && appState.openSidebar === "library") {
+    return null;
+  }
+
   return (
-    <label
-      className={clsx(
-        "ToolIcon ToolIcon_type_floating ToolIcon__library",
-        `ToolIcon_size_medium`,
-        {
-          "is-mobile": isMobile,
-        },
-      )}
-      title={`${capitalizeString(t("toolBar.library"))} — 0`}
-    >
+    <label title={`${capitalizeString(t("toolBar.library"))} — 0`}>
       <input
         className="ToolIcon_type_checkbox"
         type="checkbox"
@@ -55,7 +46,12 @@ export const LibraryButton: React.FC<{
         aria-label={capitalizeString(t("toolBar.library"))}
         aria-keyshortcuts="0"
       />
-      <div className="ToolIcon__icon">{LIBRARY_ICON}</div>
+      <div className="library-button">
+        <div>{LibraryIcon}</div>
+        {showLabel && (
+          <div className="library-button__label">{t("toolBar.library")}</div>
+        )}
+      </div>
     </label>
   );
 };

+ 31 - 93
src/components/LibraryMenu.scss

@@ -35,103 +35,32 @@
     }
   }
 
-  .library-actions {
-    width: 100%;
+  .library-actions-counter {
+    background-color: var(--color-primary);
+    color: var(--color-primary-light);
+    font-weight: bold;
     display: flex;
-    margin-right: auto;
     align-items: center;
-
-    button .library-actions-counter {
-      position: absolute;
-      right: 2px;
-      bottom: 2px;
-      border-radius: 50%;
-      width: 1em;
-      height: 1em;
-      padding: 1px;
-      font-size: 0.7rem;
-      background: #fff;
-    }
-
-    &--remove {
-      background-color: $oc-red-7;
-      &:hover {
-        background-color: $oc-red-8;
-      }
-      &:active {
-        background-color: $oc-red-9;
-      }
-      svg {
-        color: $oc-white;
-      }
-      .library-actions-counter {
-        color: $oc-red-7;
-      }
-    }
-
-    &--export {
-      background-color: $oc-lime-5;
-
-      &:hover {
-        background-color: $oc-lime-7;
-      }
-
-      &:active {
-        background-color: $oc-lime-8;
-      }
-      svg {
-        color: $oc-white;
-      }
-      .library-actions-counter {
-        color: $oc-lime-5;
-      }
-    }
-
-    &--publish {
-      background-color: $oc-cyan-6;
-      &:hover {
-        background-color: $oc-cyan-7;
-      }
-      &:active {
-        background-color: $oc-cyan-9;
-      }
-      svg {
-        color: $oc-white;
-      }
-      label {
-        margin-left: -0.2em;
-        margin-right: 1.1em;
-        color: $oc-white;
-        font-size: 0.86em;
-      }
-      .library-actions-counter {
-        color: $oc-cyan-6;
-      }
-    }
-
-    &--load {
-      background-color: $oc-blue-6;
-      &:hover {
-        background-color: $oc-blue-7;
-      }
-      &:active {
-        background-color: $oc-blue-9;
-      }
-      svg {
-        color: $oc-white;
-      }
-    }
+    justify-content: center;
+    border-radius: 50%;
+    width: 1rem;
+    height: 1rem;
+    position: absolute;
+    bottom: -0.25rem;
+    right: -0.25rem;
+    font-size: 0.625rem;
+    pointer-events: none;
   }
 
   .layer-ui__library-message {
-    padding: 2em 4em;
+    padding: 2rem;
     min-width: 200px;
     display: flex;
     flex-direction: column;
     align-items: center;
-    .Spinner {
-      margin-bottom: 1em;
-    }
+    flex-grow: 1;
+    justify-content: center;
+
     span {
       font-size: 0.8em;
     }
@@ -159,11 +88,10 @@
   }
 
   .library-menu-browse-button {
-    width: 80%;
-    min-height: 22px;
-    margin: 0 auto;
-    margin-top: 1rem;
-    padding: 10px;
+    margin: 1rem auto;
+
+    padding: 0.875rem 1rem;
+
     display: flex;
     align-items: center;
     justify-content: center;
@@ -176,6 +104,10 @@
     text-align: center;
     white-space: nowrap;
     text-decoration: none !important;
+
+    font-weight: 600;
+    font-size: 0.75rem;
+
     &:hover {
       background-color: var(--color-primary-darker);
     }
@@ -184,6 +116,12 @@
     }
   }
 
+  &.theme--dark {
+    .library-menu-browse-button {
+      color: var(--color-gray-100);
+    }
+  }
+
   .library-menu-browse-button--mobile {
     min-height: 22px;
     margin-left: auto;

+ 20 - 17
src/components/LibraryMenu.tsx

@@ -16,7 +16,7 @@ import { LibraryItems, LibraryItem, AppState, ExcalidrawProps } from "../types";
 
 import "./LibraryMenu.scss";
 import LibraryMenuItems from "./LibraryMenuItems";
-import { EVENT, VERSIONS } from "../constants";
+import { EVENT } from "../constants";
 import { KEYS } from "../keys";
 import { trackEvent } from "../analytics";
 import { useAtom } from "jotai";
@@ -31,6 +31,7 @@ import { Sidebar } from "./Sidebar/Sidebar";
 import { getSelectedElements } from "../scene";
 import { NonDeletedExcalidrawElement } from "../element/types";
 import { LibraryMenuHeader } from "./LibraryMenuHeaderContent";
+import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
 
 const useOnClickOutside = (
   ref: RefObject<HTMLElement>,
@@ -94,9 +95,6 @@ export const LibraryMenuContent = ({
   selectedItems: LibraryItem["id"][];
   onSelectItems: (id: LibraryItem["id"][]) => void;
 }) => {
-  const referrer =
-    libraryReturnUrl || window.location.origin + window.location.pathname;
-
   const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
 
   const addToLibrary = useCallback(
@@ -131,13 +129,18 @@ export const LibraryMenuContent = ({
     return (
       <LibraryMenuWrapper>
         <div className="layer-ui__library-message">
-          <Spinner size="2em" />
-          <span>{t("labels.libraryLoadingMessage")}</span>
+          <div>
+            <Spinner size="2em" />
+            <span>{t("labels.libraryLoadingMessage")}</span>
+          </div>
         </div>
       </LibraryMenuWrapper>
     );
   }
 
+  const showBtn =
+    libraryItemsData.libraryItems.length > 0 || pendingElements.length > 0;
+
   return (
     <LibraryMenuWrapper>
       <LibraryMenuItems
@@ -150,18 +153,17 @@ export const LibraryMenuContent = ({
         pendingElements={pendingElements}
         selectedItems={selectedItems}
         onSelectItems={onSelectItems}
+        id={id}
+        libraryReturnUrl={libraryReturnUrl}
+        theme={appState.theme}
       />
-      <a
-        className="library-menu-browse-button"
-        href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
-          window.name || "_blank"
-        }&referrer=${referrer}&useHash=true&token=${id}&theme=${
-          appState.theme
-        }&version=${VERSIONS.excalidrawLibrary}`}
-        target="_excalidraw_libraries"
-      >
-        {t("labels.libraries")}
-      </a>
+      {showBtn && (
+        <LibraryMenuBrowseButton
+          id={id}
+          libraryReturnUrl={libraryReturnUrl}
+          theme={appState.theme}
+        />
+      )}
     </LibraryMenuWrapper>
   );
 };
@@ -265,6 +267,7 @@ export const LibraryMenu: React.FC<{
       // is colled correctly
       key="library"
       className="layer-ui__library-sidebar"
+      initialDockedState={appState.isSidebarDocked}
       onDock={(docked) => {
         trackEvent(
           "library",

+ 31 - 0
src/components/LibraryMenuBrowseButton.tsx

@@ -0,0 +1,31 @@
+import { VERSIONS } from "../constants";
+import { t } from "../i18n";
+import { AppState, ExcalidrawProps } from "../types";
+
+const LibraryMenuBrowseButton = ({
+  theme,
+  id,
+  libraryReturnUrl,
+}: {
+  libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
+  theme: AppState["theme"];
+  id: string;
+}) => {
+  const referrer =
+    libraryReturnUrl || window.location.origin + window.location.pathname;
+  return (
+    <a
+      className="library-menu-browse-button"
+      href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
+        window.name || "_blank"
+      }&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
+        VERSIONS.excalidrawLibrary
+      }`}
+      target="_excalidraw_libraries"
+    >
+      {t("labels.libraries")}
+    </a>
+  );
+};
+
+export default LibraryMenuBrowseButton;

+ 86 - 87
src/components/LibraryMenuHeaderContent.tsx

@@ -3,9 +3,14 @@ import { saveLibraryAsJSON } from "../data/json";
 import Library, { libraryItemsAtom } from "../data/library";
 import { t } from "../i18n";
 import { AppState, LibraryItem, LibraryItems } from "../types";
-import { exportToFileIcon, load, publishIcon, trash } from "./icons";
+import {
+  DotsIcon,
+  ExportIcon,
+  LoadIcon,
+  publishIcon,
+  TrashIcon,
+} from "./icons";
 import { ToolButton } from "./ToolButton";
-import { Tooltip } from "./Tooltip";
 import { fileOpen } from "../data/filesystem";
 import { muteFSAbortError } from "../utils";
 import { useAtom } from "jotai";
@@ -13,6 +18,9 @@ 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";
 
 const getSelectedItems = (
   libraryItems: LibraryItems,
@@ -165,93 +173,84 @@ export const LibraryMenuHeader: React.FC<{
       });
   };
 
+  const [isDropdownOpen, setIsDropdownOpen] = useAtom(isDropdownOpenAtom);
+  const dropdownRef = useOutsideClickHook(() => setIsDropdownOpen(false));
+
   return (
-    <div className="library-actions">
-      {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()}
-      {!itemsSelected && (
-        <ToolButton
-          key="import"
-          type="button"
-          title={t("buttons.load")}
-          aria-label={t("buttons.load")}
-          icon={load}
-          onClick={onLibraryImport}
-          className="library-actions--load"
-        />
-      )}
-      {!!items.length && (
-        <>
-          <ToolButton
-            key="export"
-            type="button"
-            title={t("buttons.export")}
-            aria-label={t("buttons.export")}
-            icon={exportToFileIcon}
-            onClick={onLibraryExport}
-            className="library-actions--export"
-          >
-            {selectedItems.length > 0 && (
-              <span className="library-actions-counter">
-                {selectedItems.length}
-              </span>
-            )}
-          </ToolButton>
-          <ToolButton
-            key="reset"
-            type="button"
-            title={resetLabel}
-            aria-label={resetLabel}
-            icon={trash}
-            onClick={() => setShowRemoveLibAlert(true)}
-            className="library-actions--remove"
-          >
-            {selectedItems.length > 0 && (
-              <span className="library-actions-counter">
-                {selectedItems.length}
-              </span>
-            )}
-          </ToolButton>
-        </>
+    <div style={{ position: "relative" }}>
+      <button
+        type="button"
+        className="Sidebar__dropdown-btn"
+        data-prevent-outside-click
+        onClick={() => setIsDropdownOpen(!isDropdownOpen)}
+      >
+        {DotsIcon}
+      </button>
+
+      {selectedItems.length > 0 && (
+        <div className="library-actions-counter">{selectedItems.length}</div>
       )}
-      {itemsSelected && (
-        <Tooltip label={t("hints.publishLibrary")}>
-          <ToolButton
-            type="button"
-            aria-label={t("buttons.publishLibrary")}
-            label={t("buttons.publishLibrary")}
-            icon={publishIcon}
-            className="library-actions--publish"
-            onClick={() => setShowPublishLibraryDialog(true)}
-          >
-            <label>{t("buttons.publishLibrary")}</label>
-            {selectedItems.length > 0 && (
-              <span className="library-actions-counter">
-                {selectedItems.length}
-              </span>
-            )}
-          </ToolButton>
-        </Tooltip>
+
+      {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,
+              )}
+              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>
       )}
     </div>
   );

+ 52 - 0
src/components/LibraryMenuItems.scss

@@ -1,18 +1,70 @@
 @import "open-color/open-color";
 
 .excalidraw {
+  --container-padding-y: 1.5rem;
+  --container-padding-x: 0.75rem;
+
+  .library-menu-items__no-items {
+    text-align: center;
+    color: var(--color-gray-70);
+    line-height: 1.5;
+    font-size: 0.875rem;
+    width: 100%;
+
+    &__label {
+      color: var(--color-primary);
+      font-weight: bold;
+      font-size: 1.125rem;
+      margin-bottom: 0.75rem;
+    }
+  }
+
+  &.theme--dark {
+    .library-menu-items__no-items {
+      color: var(--color-gray-40);
+    }
+  }
+
   .library-menu-items-container {
     display: flex;
+    flex-grow: 1;
+    flex-shrink: 1;
+    flex-basis: 0;
+    overflow-y: auto;
     flex-direction: column;
     height: 100%;
+    justify-content: center;
+    margin: 0;
+    border-bottom: 1px solid var(--sidebar-border-color);
+
+    position: relative;
+
+    &__row {
+      display: grid;
+      grid-template-columns: repeat(4, 1fr);
+      gap: 1rem;
+    }
 
     &__items {
+      row-gap: 0.5rem;
+      padding: var(--container-padding-y) var(--container-padding-x);
       flex: 1;
       overflow-y: auto;
       overflow-x: hidden;
       margin-bottom: 1rem;
     }
 
+    &__header {
+      color: var(--color-primary);
+      font-size: 1.125rem;
+      font-weight: bold;
+      margin-bottom: 0.75rem;
+
+      &--excal {
+        margin-top: 2.5rem;
+      }
+    }
+
     .separator {
       width: 100%;
       display: flex;

+ 52 - 40
src/components/LibraryMenuItems.tsx

@@ -2,7 +2,7 @@ import React, { useState } from "react";
 import { serializeLibraryAsJSON } from "../data/json";
 import { ExcalidrawElement, NonDeleted } from "../element/types";
 import { t } from "../i18n";
-import { LibraryItem, LibraryItems } from "../types";
+import { AppState, ExcalidrawProps, LibraryItem, LibraryItems } from "../types";
 import { arrayToMap, chunk } from "../utils";
 import { LibraryUnit } from "./LibraryUnit";
 import Stack from "./Stack";
@@ -10,6 +10,8 @@ import Stack from "./Stack";
 import "./LibraryMenuItems.scss";
 import { MIME_TYPES } from "../constants";
 import Spinner from "./Spinner";
+import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
+import clsx from "clsx";
 
 const CELLS_PER_ROW = 4;
 
@@ -21,6 +23,9 @@ const LibraryMenuItems = ({
   pendingElements,
   selectedItems,
   onSelectItems,
+  theme,
+  id,
+  libraryReturnUrl,
 }: {
   isLoading: boolean;
   libraryItems: LibraryItems;
@@ -29,6 +34,9 @@ const LibraryMenuItems = ({
   onAddToLibrary: (elements: LibraryItem["elements"]) => void;
   selectedItems: LibraryItem["id"][];
   onSelectItems: (id: LibraryItem["id"][]) => void;
+  libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
+  theme: AppState["theme"];
+  id: string;
 }) => {
   const [lastSelectedItem, setLastSelectedItem] = useState<
     LibraryItem["id"] | null
@@ -167,7 +175,11 @@ const LibraryMenuItems = ({
         );
       }
       return (
-        <Stack.Row align="center" gap={1} key={index}>
+        <Stack.Row
+          align="center"
+          key={index}
+          className="library-menu-items-container__row"
+        >
           {rowItems}
         </Stack.Row>
       );
@@ -181,19 +193,21 @@ const LibraryMenuItems = ({
     (item) => item.status === "published",
   );
 
+  const showBtn =
+    !libraryItems.length &&
+    !unpublishedItems.length &&
+    !publishedItems.length &&
+    !pendingElements.length;
+
   return (
     <div
       className="library-menu-items-container"
       style={
-        publishedItems.length || unpublishedItems.length
-          ? {
-              flex: "1 1 0",
-              overflowY: "auto",
-            }
-          : {
-              marginBottom: "2rem",
-              flex: 0,
-            }
+        pendingElements.length ||
+        unpublishedItems.length ||
+        publishedItems.length
+          ? { justifyContent: "flex-start" }
+          : {}
       }
     >
       <Stack.Col
@@ -206,49 +220,37 @@ const LibraryMenuItems = ({
         }}
       >
         <>
-          <div className="separator">
+          <div>
             {(pendingElements.length > 0 ||
               unpublishedItems.length > 0 ||
               publishedItems.length > 0) && (
-              <div>{t("labels.personalLib")}</div>
+              <div className="library-menu-items-container__header">
+                {t("labels.personalLib")}
+              </div>
             )}
             {isLoading && (
               <div
                 style={{
-                  marginLeft: "auto",
-                  marginRight: "1rem",
-                  display: "flex",
-                  alignItems: "center",
-                  fontWeight: "normal",
+                  position: "absolute",
+                  top: "var(--container-padding-y)",
+                  right: "var(--container-padding-x)",
+                  transform: "translateY(50%)",
                 }}
               >
-                <div style={{ transform: "translateY(2px)" }}>
-                  <Spinner />
-                </div>
+                <Spinner />
               </div>
             )}
           </div>
           {!pendingElements.length && !unpublishedItems.length ? (
-            <div
-              style={{
-                height: 65,
-                display: "flex",
-                flexDirection: "column",
-                alignItems: "center",
-                justifyContent: "center",
-                width: "100%",
-                fontSize: ".9rem",
-              }}
-            >
-              {t("library.noItems")}
+            <div className="library-menu-items__no-items">
               <div
-                style={{
-                  margin: ".6rem 0",
-                  fontSize: ".8em",
-                  width: "70%",
-                  textAlign: "center",
-                }}
+                className={clsx({
+                  "library-menu-items__no-items__label": showBtn,
+                })}
               >
+                {t("library.noItems")}
+              </div>
+              <div className="library-menu-items__no-items__hint">
                 {publishedItems.length > 0
                   ? t("library.hint_emptyPrivateLibrary")
                   : t("library.hint_emptyLibrary")}
@@ -269,7 +271,9 @@ const LibraryMenuItems = ({
           {(publishedItems.length > 0 ||
             pendingElements.length > 0 ||
             unpublishedItems.length > 0) && (
-            <div className="separator">{t("labels.excalidrawLib")}</div>
+            <div className="library-menu-items-container__header library-menu-items-container__header--excal">
+              {t("labels.excalidrawLib")}
+            </div>
           )}
           {publishedItems.length > 0 ? (
             renderLibrarySection(publishedItems)
@@ -289,6 +293,14 @@ const LibraryMenuItems = ({
             </div>
           ) : null}
         </>
+
+        {showBtn && (
+          <LibraryMenuBrowseButton
+            id={id}
+            libraryReturnUrl={libraryReturnUrl}
+            theme={theme}
+          />
+        )}
       </Stack.Col>
     </div>
   );

+ 45 - 26
src/components/LibraryUnit.scss

@@ -7,17 +7,18 @@
     display: flex;
     justify-content: center;
     position: relative;
-    width: 63px;
-    height: 63px; // match width
+    width: 55px;
+    height: 55px;
+    box-sizing: border-box;
+    border-radius: var(--border-radius-lg);
 
     &--hover {
-      box-shadow: inset 0px 0px 0px 2px $oc-blue-5;
-      border-color: $oc-blue-5;
+      border-color: var(--color-primary);
     }
 
     &--selected {
-      box-shadow: inset 0px 0px 0px 2px $oc-blue-8;
-      border-color: $oc-blue-8;
+      border-color: var(--color-primary);
+      border-width: 1px;
     }
   }
 
@@ -59,20 +60,34 @@
 
   .library-unit__checkbox {
     position: absolute;
-    left: 2.3rem;
-    bottom: 2.3rem;
+    top: 0.125rem;
+    right: 0.125rem;
+    margin: 0;
 
     .Checkbox-box {
-      width: 13px;
-      height: 13px;
-      border-radius: 2px;
-      margin: 0.5em 0.5em 0.2em 0.2em;
-      background-color: $oc-blue-1;
+      margin: 0;
+      width: 1rem;
+      height: 1rem;
+      border-radius: 4px;
+      background-color: var(--color-primary-light);
+      border: 1px solid var(--color-primary);
+      box-shadow: none !important;
+      padding: 2px;
     }
 
     &.Checkbox:hover {
       .Checkbox-box {
-        background-color: $oc-blue-2;
+        background-color: var(--color-primary-light);
+      }
+    }
+
+    &.is-checked {
+      .Checkbox-box {
+        background-color: var(--color-primary) !important;
+
+        svg {
+          color: var(--color-primary-light);
+        }
       }
     }
   }
@@ -85,25 +100,29 @@
   .library-unit__adder {
     transform: scale(1);
     animation: library-unit__adder-animation 1s ease-in infinite;
-  }
 
-  .library-unit__adder {
     position: absolute;
-    left: 40%;
-    top: 40%;
-    width: 2rem;
-    height: 2rem;
-    margin-left: -10px;
-    margin-top: -10px;
+    width: 1.5rem;
+    height: 1.5rem;
+    background-color: var(--color-primary);
+    border-radius: var(--border-radius-md);
+
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
     pointer-events: none;
+
+    svg {
+      color: var(--color-primary-light);
+      width: 1rem;
+      height: 1rem;
+    }
   }
-  .library-unit:hover .library-unit__adder {
-    fill: $oc-blue-7;
-  }
+
   .library-unit:active .library-unit__adder {
     animation: none;
     transform: scale(0.8);
-    fill: $oc-black;
   }
 
   .library-unit__active {

+ 2 - 14
src/components/LibraryUnit.tsx

@@ -6,19 +6,7 @@ import { exportToSvg } from "../scene/export";
 import { LibraryItem } from "../types";
 import "./LibraryUnit.scss";
 import { CheckboxItem } from "./CheckboxItem";
-
-const PLUS_ICON = (
-  <svg viewBox="0 0 1792 1792">
-    <path
-      d="M1600 736v192c0 26.667-9.33 49.333-28 68-18.67 18.67-41.33 28-68 28h-416v416c0 26.67-9.33 49.33-28 68s-41.33 28-68 28H800c-26.667 0-49.333-9.33-68-28s-28-41.33-28-68v-416H288c-26.667 0-49.333-9.33-68-28-18.667-18.667-28-41.333-28-68V736c0-26.667 9.333-49.333 28-68s41.333-28 68-28h416V224c0-26.667 9.333-49.333 28-68s41.333-28 68-28h192c26.67 0 49.33 9.333 68 28s28 41.333 28 68v416h416c26.67 0 49.33 9.333 68 28s28 41.333 28 68Z"
-      style={{
-        stroke: "#fff",
-        strokeWidth: 140,
-      }}
-      transform="translate(0 64)"
-    />
-  </svg>
-);
+import { PlusIcon } from "./icons";
 
 export const LibraryUnit = ({
   id,
@@ -67,7 +55,7 @@ export const LibraryUnit = ({
   const [isHovered, setIsHovered] = useState(false);
   const isMobile = useDevice().isMobile;
   const adder = isPending && (
-    <div className="library-unit__adder">{PLUS_ICON}</div>
+    <div className="library-unit__adder">{PlusIcon}</div>
   );
 
   return (

+ 4 - 23
src/components/LockButton.tsx

@@ -1,8 +1,8 @@
 import "./ToolIcon.scss";
 
-import React from "react";
 import clsx from "clsx";
 import { ToolButtonSize } from "./ToolButton";
+import { LockedIcon, UnlockedIcon } from "./icons";
 
 type LockIconProps = {
   title?: string;
@@ -16,34 +16,15 @@ type LockIconProps = {
 const DEFAULT_SIZE: ToolButtonSize = "medium";
 
 const ICONS = {
-  CHECKED: (
-    <svg
-      width="1792"
-      height="1792"
-      viewBox="0 0 1792 1792"
-      xmlns="http://www.w3.org/2000/svg"
-    >
-      <path d="M640 768h512v-192q0-106-75-181t-181-75-181 75-75 181v192zm832 96v576q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-576q0-40 28-68t68-28h32v-192q0-184 132-316t316-132 316 132 132 316v192h32q40 0 68 28t28 68z" />
-    </svg>
-  ),
-  UNCHECKED: (
-    <svg
-      width="1792"
-      height="1792"
-      viewBox="0 0 1792 1792"
-      xmlns="http://www.w3.org/2000/svg"
-      className="unlocked-icon rtl-mirror"
-    >
-      <path d="M1728 576v256q0 26-19 45t-45 19h-64q-26 0-45-19t-19-45v-256q0-106-75-181t-181-75-181 75-75 181v192h96q40 0 68 28t28 68v576q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-576q0-40 28-68t68-28h672v-192q0-185 131.5-316.5t316.5-131.5 316.5 131.5 131.5 316.5z" />
-    </svg>
-  ),
+  CHECKED: LockedIcon,
+  UNCHECKED: UnlockedIcon,
 };
 
 export const LockButton = (props: LockIconProps) => {
   return (
     <label
       className={clsx(
-        "ToolIcon ToolIcon__lock ToolIcon_type_floating",
+        "ToolIcon ToolIcon__lock",
         `ToolIcon_size_${DEFAULT_SIZE}`,
         {
           "is-mobile": props.isMobile,

+ 85 - 0
src/components/Menu.scss

@@ -0,0 +1,85 @@
+@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;
+    }
+  }
+}

+ 37 - 0
src/components/MenuItem.tsx

@@ -0,0 +1,37 @@
+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;

+ 53 - 0
src/components/MenuUtils.tsx

@@ -0,0 +1,53 @@
+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",
+    }}
+  />
+);

+ 76 - 35
src/components/MobileMenu.tsx

@@ -8,18 +8,21 @@ import { NonDeletedExcalidrawElement } from "../element/types";
 import { FixedSideContainer } from "./FixedSideContainer";
 import { Island } from "./Island";
 import { HintViewer } from "./HintViewer";
-import { calculateScrollCenter, getSelectedElements } from "../scene";
+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 { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
 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";
 
 type MobileMenuProps = {
   appState: AppState;
@@ -45,6 +48,7 @@ type MobileMenuProps = {
   renderCustomStats?: ExcalidrawProps["renderCustomStats"];
   renderSidebars: () => JSX.Element | null;
   device: Device;
+  renderWelcomeScreen?: boolean;
 };
 
 export const MobileMenu = ({
@@ -65,17 +69,35 @@ export const MobileMenu = ({
   renderCustomStats,
   renderSidebars,
   device,
+  renderWelcomeScreen,
 }: MobileMenuProps) => {
   const renderToolbar = () => {
     return (
       <FixedSideContainer side="top" className="App-top-bar">
+        {renderWelcomeScreen && !appState.isLoading && (
+          <WelcomeScreen actionManager={actionManager} />
+        )}
         <Section heading="shapes">
           {(heading: React.ReactNode) => (
             <Stack.Col gap={4} align="center">
               <Stack.Row gap={1} className="App-toolbar-container">
-                <Island padding={1} className="App-toolbar">
+                <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}
@@ -90,24 +112,27 @@ export const MobileMenu = ({
                   </Stack.Row>
                 </Island>
                 {renderTopRightUI && renderTopRightUI(true, appState)}
-                <LockButton
-                  checked={appState.activeTool.locked}
-                  onChange={onLockToggle}
-                  title={t("toolBar.lock")}
-                  isMobile
-                />
-                <LibraryButton
-                  appState={appState}
-                  setAppState={setAppState}
-                  isMobile
-                />
-                <PenModeButton
-                  checked={appState.penMode}
-                  onChange={onPenModeToggle}
-                  title={t("toolBar.penMode")}
-                  isMobile
-                  penDetected={appState.penDetected}
-                />
+                <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
+                  />
+                  <LibraryButton
+                    appState={appState}
+                    setAppState={setAppState}
+                    isMobile
+                  />
+                </div>
               </Stack.Row>
             </Stack.Col>
           )}
@@ -123,11 +148,6 @@ export const MobileMenu = ({
   };
 
   const renderAppToolbar = () => {
-    // Render eraser conditionally in mobile
-    const showEraser =
-      !appState.editingElement &&
-      getSelectedElements(elements, appState).length === 0;
-
     if (appState.viewModeEnabled) {
       return (
         <div className="App-toolbar-content">
@@ -140,14 +160,11 @@ export const MobileMenu = ({
       <div className="App-toolbar-content">
         {actionManager.renderAction("toggleCanvasMenu")}
         {actionManager.renderAction("toggleEditMenu")}
-
         {actionManager.renderAction("undo")}
         {actionManager.renderAction("redo")}
-        {showEraser
-          ? actionManager.renderAction("eraser")
-          : actionManager.renderAction(
-              appState.multiElement ? "finalize" : "duplicateSelection",
-            )}
+        {actionManager.renderAction(
+          appState.multiElement ? "finalize" : "duplicateSelection",
+        )}
         {actionManager.renderAction("deleteSelectedElements")}
       </div>
     );
@@ -158,16 +175,27 @@ export const MobileMenu = ({
       return (
         <>
           {renderJSONExportDialog()}
+          <MenuItem
+            label={t("buttons.exportImage")}
+            icon={ExportImageIcon}
+            dataTestId="image-export-button"
+            onClick={() => setAppState({ openDialog: "imageExport" })}
+          />
           {renderImageExportDialog()}
         </>
       );
     }
     return (
       <>
-        {actionManager.renderAction("clearCanvas")}
         {actionManager.renderAction("loadScene")}
         {renderJSONExportDialog()}
         {renderImageExportDialog()}
+        <MenuItem
+          label={t("buttons.exportImage")}
+          icon={ExportImageIcon}
+          dataTestId="image-export-button"
+          onClick={() => setAppState({ openDialog: "imageExport" })}
+        />
         {onCollabButtonClick && (
           <CollabButton
             isCollaborating={isCollaborating}
@@ -175,7 +203,20 @@ export const MobileMenu = ({
             onClick={onCollabButtonClick}
           />
         )}
-        {<BackgroundPickerAndDarkModeToggle actionManager={actionManager} />}
+        {actionManager.renderAction("toggleShortcuts", undefined, true)}
+        {actionManager.renderAction("clearCanvas")}
+        <Separator />
+        <MenuLinks />
+        <Separator />
+        <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")}
       </>
     );
   };
@@ -206,7 +247,7 @@ export const MobileMenu = ({
           {appState.openMenu === "canvas" ? (
             <Section className="App-mobile-menu" heading="canvasActions">
               <div className="panelColumn">
-                <Stack.Col gap={4}>
+                <Stack.Col gap={2}>
                   {renderCanvasActions()}
                   {renderCustomFooter?.(true, appState)}
                   {appState.collaborators.size > 0 && (

+ 18 - 8
src/components/Modal.scss

@@ -17,6 +17,10 @@
     justify-content: center;
     overflow: auto;
     padding: calc(var(--space-factor) * 10);
+
+    .Island {
+      padding: 2.5rem !important;
+    }
   }
 
   .Modal__background {
@@ -26,7 +30,7 @@
     right: 0;
     bottom: 0;
     z-index: 1;
-    background-color: transparentize($oc-black, 0.3);
+    background-color: rgba(#121212, 0.2);
   }
 
   .Modal__content {
@@ -46,7 +50,7 @@
     background: var(--island-bg-color);
 
     border: 1px solid var(--dialog-border-color);
-    box-shadow: 0 2px 10px transparentize($oc-black, 0.75);
+    box-shadow: var(--modal-shadow);
     border-radius: 6px;
     box-sizing: border-box;
 
@@ -73,14 +77,20 @@
   }
 
   .Modal__close {
-    width: calc(var(--space-factor) * 7);
-    height: calc(var(--space-factor) * 7);
-    display: flex;
-    align-items: center;
-    justify-content: center;
+    color: var(--icon-fill-color);
+    margin: 0;
+    padding: 0.375rem;
+    position: absolute;
+    top: 1rem;
+    right: 1rem;
+    border: 0;
+    background-color: transparent;
+    line-height: 0;
+    cursor: pointer;
 
     svg {
-      height: calc(var(--space-factor) * 5);
+      width: 1.5rem;
+      height: 1.5rem;
     }
   }
 

+ 1 - 0
src/components/Modal.tsx

@@ -39,6 +39,7 @@ export const Modal: React.FC<{
       aria-modal="true"
       onKeyDown={handleKeydown}
       aria-labelledby={props.labelledBy}
+      data-prevent-outside-click
     >
       <div
         className="Modal__background"

+ 5 - 50
src/components/PenModeButton.tsx

@@ -2,6 +2,7 @@ import "./ToolIcon.scss";
 
 import clsx from "clsx";
 import { ToolButtonSize } from "./ToolButton";
+import { PenModeIcon } from "./icons";
 
 type PenModeIconProps = {
   title?: string;
@@ -15,59 +16,15 @@ type PenModeIconProps = {
 
 const DEFAULT_SIZE: ToolButtonSize = "medium";
 
-const ICONS = {
-  CHECKED: (
-    <svg
-      width="205"
-      height="205"
-      viewBox="0 0 205 205"
-      xmlns="http://www.w3.org/2000/svg"
-    >
-      <path d="m35 195-25-29.17V50h50v115l-25 30" />
-      <path d="M10 40V10h50v30H10" />
-      <path d="M125 145h70v50h-70" />
-      <path d="M190 145v-30l-10-20h-40l-10 20v30h15v-30l5-5h20l5 5v30h15" />
-    </svg>
-  ),
-  UNCHECKED: (
-    <svg
-      width="205"
-      height="205"
-      viewBox="0 0 205 205"
-      xmlns="http://www.w3.org/2000/svg"
-      className="unlocked-icon rtl-mirror"
-    >
-      <path d="m35 195-25-29.17V50h50v115l-25 30" />
-      <path d="M10 40V10h50v30H10" />
-      <path d="M125 145h70v50h-70" />
-      <path d="M145 145v-30l-10-20H95l-10 20v30h15v-30l5-5h20l5 5v30h15" />
-    </svg>
-  ),
-};
-
 export const PenModeButton = (props: PenModeIconProps) => {
   if (!props.penDetected) {
-    if (props.isMobile) {
-      return null;
-    }
-    return (
-      <label
-        className={clsx(
-          "ToolIcon ToolIcon__penMode ToolIcon_type_floating",
-          `ToolIcon_size_${DEFAULT_SIZE}`,
-          {
-            "is-mobile": props.isMobile,
-          },
-        )}
-      >
-        <div className="ToolIcon__icon ToolIcon__hidden" />
-      </label>
-    );
+    return null;
   }
+
   return (
     <label
       className={clsx(
-        "ToolIcon ToolIcon__penMode ToolIcon_type_floating",
+        "ToolIcon ToolIcon__penMode",
         `ToolIcon_size_${DEFAULT_SIZE}`,
         {
           "is-mobile": props.isMobile,
@@ -83,9 +40,7 @@ export const PenModeButton = (props: PenModeIconProps) => {
         checked={props.checked}
         aria-label={props.title}
       />
-      <div className="ToolIcon__icon">
-        {props.checked ? ICONS.CHECKED : ICONS.UNCHECKED}
-      </div>
+      <div className="ToolIcon__icon">{PenModeIcon}</div>
     </label>
   );
 };

+ 3 - 3
src/components/PublishLibrary.scss

@@ -7,7 +7,7 @@
       flex-direction: column;
 
       label {
-        padding: 1em;
+        padding: 1em 0;
         display: flex;
         justify-content: space-between;
         align-items: center;
@@ -34,6 +34,7 @@
       display: flex;
       padding: 0.2rem 0;
       justify-content: flex-end;
+      gap: 0.5rem;
 
       .ToolIcon__icon {
         min-width: 2.5rem;
@@ -74,7 +75,6 @@
 
     .selected-library-items {
       display: flex;
-      padding: 0 0.8rem;
       flex-wrap: wrap;
 
       .single-library-item-wrapper {
@@ -87,7 +87,7 @@
     }
 
     &-note {
-      padding: 1em;
+      padding: 1em 0;
       font-style: italic;
       font-size: 14px;
       display: block;

+ 4 - 11
src/components/PublishLibrary.tsx

@@ -4,8 +4,6 @@ import OpenColor from "open-color";
 import { Dialog } from "./Dialog";
 import { t } from "../i18n";
 
-import { ToolButton } from "./ToolButton";
-
 import { AppState, LibraryItems, LibraryItem } from "../types";
 import { exportToCanvas } from "../packages/utils";
 import {
@@ -20,6 +18,7 @@ import "./PublishLibrary.scss";
 import SingleLibraryItem from "./SingleLibraryItem";
 import { canvasToBlob, resizeImageFile } from "../data/blob";
 import { chunk } from "../utils";
+import DialogActionButton from "./DialogActionButton";
 
 interface PublishLibraryDataParams {
   authorName: string;
@@ -434,21 +433,15 @@ const PublishLibrary = ({
             </span>
           </div>
           <div className="publish-library__buttons">
-            <ToolButton
-              type="button"
-              title={t("buttons.cancel")}
-              aria-label={t("buttons.cancel")}
+            <DialogActionButton
               label={t("buttons.cancel")}
               onClick={onDialogClose}
               data-testid="cancel-clear-canvas-button"
-              className="publish-library__buttons--cancel"
             />
-            <ToolButton
+            <DialogActionButton
               type="submit"
-              title={t("buttons.submit")}
-              aria-label={t("buttons.submit")}
               label={t("buttons.submit")}
-              className="publish-library__buttons--confirm"
+              actionType="primary"
               isLoading={isSubmitting}
             />
           </div>

+ 93 - 36
src/components/Sidebar/Sidebar.scss

@@ -2,20 +2,101 @@
 @import "../../css/variables.module";
 
 .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 {
+      @include outlineButtonStyles;
+      width: var(--lg-button-size);
+      height: var(--lg-button-size);
+      padding: 0;
+
+      svg {
+        width: var(--lg-icon-size);
+        height: var(--lg-icon-size);
+      }
+    }
+
+    &__pin-btn {
+      &--pinned {
+        background-color: var(--color-primary);
+        border-color: var(--color-primary);
+
+        svg {
+          color: #fff;
+        }
+
+        &:hover,
+        &:active {
+          background-color: var(--color-primary-darker);
+        }
+      }
+    }
+  }
+
+  &.theme--dark {
+    .Sidebar {
+      &__pin-btn {
+        &--pinned {
+          svg {
+            color: var(--color-gray-90);
+          }
+        }
+      }
+    }
+  }
+
   .layer-ui__sidebar {
     position: absolute;
-    top: var(--sat);
-    bottom: var(--sab);
-    right: var(--sar);
+    top: 0;
+    bottom: 0;
+    right: 0;
     z-index: 5;
+    margin: 0;
+
+    :root[dir="rtl"] & {
+      left: 0;
+      right: auto;
+    }
+
+    background-color: var(--sidebar-bg-color);
+
+    box-shadow: var(--sidebar-shadow);
+
+    &--docked {
+      box-shadow: none;
+    }
 
-    box-shadow: var(--shadow-island);
     overflow: hidden;
-    border-radius: var(--border-radius-lg);
-    margin: var(--space-factor);
+    border-radius: 0;
     width: calc(#{$right-sidebar-width} - var(--space-factor) * 2);
 
-    padding: 0.5rem;
+    border-left: 1px solid var(--sidebar-border-color);
+
+    :root[dir="rtl"] & {
+      border-right: 1px solid var(--sidebar-border-color);
+      border-left: 0;
+    }
+
+    padding: 0;
     box-sizing: border-box;
 
     .Island {
@@ -48,42 +129,18 @@
   }
 
   .layer-ui__sidebar__header {
+    box-sizing: border-box;
     display: flex;
+    justify-content: space-between;
     align-items: center;
     width: 100%;
-    margin: 2px 0 15px 0;
-    &:empty {
-      margin: 0;
-    }
-    button {
-      // 2px from the left to account for focus border of left-most button
-      margin: 0 2px;
-    }
+    padding: 1rem;
+    border-bottom: 1px solid var(--sidebar-border-color);
   }
 
   .layer-ui__sidebar__header__buttons {
     display: flex;
     align-items: center;
-    margin-left: auto;
-  }
-
-  .layer-ui__sidebar-dock-button {
-    @include toolbarButtonColorStates;
-    margin-right: 0.2rem;
-
-    .ToolIcon_type_floating .ToolIcon__icon {
-      width: calc(var(--space-factor) * 7);
-      height: calc(var(--space-factor) * 7);
-      svg {
-        // mirror
-        transform: scale(-1, 1);
-      }
-    }
-
-    .ToolIcon_type_checkbox {
-      &:not(.ToolIcon_toggle_opaque):checked + .ToolIcon__icon {
-        background-color: var(--color-primary);
-      }
-    }
+    gap: 0.625rem;
   }
 }

+ 15 - 3
src/components/Sidebar/Sidebar.tsx

@@ -33,6 +33,13 @@ export const Sidebar = Object.assign(
         onClose,
         onDock,
         docked,
+        /** Undocumented, may be removed later. Generally should either be
+         * `props.docked` or `appState.isSidebarDocked`. Currently serves to
+         *  prevent unwanted animation of the shadow if initially docked. */
+        //
+        // NOTE we'll want to remove this after we sort out how to subscribe to
+        // individual appState properties
+        initialDockedState = docked,
         dockable = true,
         className,
         __isInternal,
@@ -52,7 +59,9 @@ export const Sidebar = Object.assign(
 
       const setAppState = useExcalidrawSetAppState();
 
-      const [isDockedFallback, setIsDockedFallback] = useState(docked ?? false);
+      const [isDockedFallback, setIsDockedFallback] = useState(
+        docked ?? initialDockedState ?? false,
+      );
 
       useLayoutEffect(() => {
         if (docked === undefined) {
@@ -119,8 +128,11 @@ export const Sidebar = Object.assign(
 
       return (
         <Island
-          padding={2}
-          className={clsx("layer-ui__sidebar", className)}
+          className={clsx(
+            "layer-ui__sidebar",
+            { "layer-ui__sidebar--docked": isDockedFallback },
+            className,
+          )}
           ref={ref}
         >
           <SidebarPropsContext.Provider value={headerPropsRef.current}>

+ 16 - 22
src/components/Sidebar/SidebarHeader.tsx

@@ -3,16 +3,10 @@ import { useContext } from "react";
 import { t } from "../../i18n";
 import { useDevice } from "../App";
 import { SidebarPropsContext } from "./common";
-import { close } from "../icons";
+import { CloseIcon, PinIcon } from "../icons";
 import { withUpstreamOverride } from "../hoc/withUpstreamOverride";
 import { Tooltip } from "../Tooltip";
 
-const SIDE_LIBRARY_TOGGLE_ICON = (
-  <svg viewBox="0 0 24 24" fill="#ffffff">
-    <path d="M19 22H5a3 3 0 01-3-3V5a3 3 0 013-3h14a3 3 0 013 3v14a3 3 0 01-3 3zm0-18h-9v16h9a1.01 1.01 0 001-1V5a1.01 1.01 0 00-1-1z"></path>
-  </svg>
-);
-
 export const SidebarDockButton = (props: {
   checked: boolean;
   onChange?(): void;
@@ -33,8 +27,13 @@ export const SidebarDockButton = (props: {
             checked={props.checked}
             aria-label={t("labels.sidebarLock")}
           />{" "}
-          <div className="ToolIcon__icon" tabIndex={0}>
-            {SIDE_LIBRARY_TOGGLE_ICON}
+          <div
+            className={clsx("Sidebar__pin-btn", {
+              "Sidebar__pin-btn--pinned": props.checked,
+            })}
+            tabIndex={0}
+          >
+            {PinIcon}
           </div>{" "}
         </label>{" "}
       </Tooltip>
@@ -64,24 +63,19 @@ const _SidebarHeader: React.FC<{
             <SidebarDockButton
               checked={!!props.docked}
               onChange={() => {
-                document
-                  .querySelector(".layer-ui__wrapper")
-                  ?.classList.add("animate");
-
                 props.onDock?.(!props.docked);
               }}
             />
           )}
           {renderCloseButton && (
-            <div className="ToolIcon__icon__close" data-testid="sidebar-close">
-              <button
-                className="Modal__close"
-                onClick={props.onClose}
-                aria-label={t("buttons.close")}
-              >
-                {close}
-              </button>
-            </div>
+            <button
+              data-testid="sidebar-close"
+              className="Sidebar__close-btn"
+              onClick={props.onClose}
+              aria-label={t("buttons.close")}
+            >
+              {CloseIcon}
+            </button>
           )}
         </div>
       )}

+ 1 - 0
src/components/Sidebar/common.ts

@@ -9,6 +9,7 @@ export type SidebarProps<P = {}> = {
   /** if not supplied, sidebar won't be dockable */
   onDock?: (docked: boolean) => void;
   docked?: boolean;
+  initialDockedState?: boolean;
   dockable?: boolean;
   className?: string;
 } & P;

+ 3 - 3
src/components/SingleLibraryItem.tsx

@@ -3,7 +3,7 @@ import { useEffect, useRef } from "react";
 import { t } from "../i18n";
 import { exportToSvg } from "../packages/utils";
 import { AppState, LibraryItem } from "../types";
-import { close } from "./icons";
+import { CloseIcon } from "./icons";
 
 import "./SingleLibraryItem.scss";
 import { ToolButton } from "./ToolButton";
@@ -54,7 +54,7 @@ const SingleLibraryItem = ({
       <ToolButton
         aria-label={t("buttons.remove")}
         type="button"
-        icon={close}
+        icon={CloseIcon}
         className="single-library-item--remove"
         onClick={onRemove.bind(null, libItem.id)}
         title={t("buttons.remove")}
@@ -62,7 +62,7 @@ const SingleLibraryItem = ({
       <div
         style={{
           display: "flex",
-          margin: "0.8rem 0.3rem",
+          margin: "0.8rem 0",
           width: "100%",
           fontSize: "14px",
           fontWeight: 500,

+ 2 - 2
src/components/Stats.tsx

@@ -4,7 +4,7 @@ import { NonDeletedExcalidrawElement } from "../element/types";
 import { t } from "../i18n";
 import { getTargetElements } from "../scene";
 import { AppState, ExcalidrawProps } from "../types";
-import { close } from "./icons";
+import { CloseIcon } from "./icons";
 import { Island } from "./Island";
 import "./Stats.scss";
 
@@ -23,7 +23,7 @@ export const Stats = (props: {
     <div className="Stats">
       <Island padding={2}>
         <div className="close" onClick={props.onClose}>
-          {close}
+          {CloseIcon}
         </div>
         <h3>{t("stats.title")}</h3>
         <table>

+ 2 - 2
src/components/Toast.tsx

@@ -1,5 +1,5 @@
 import { useCallback, useEffect, useRef } from "react";
-import { close } from "./icons";
+import { CloseIcon } from "./icons";
 import "./Toast.scss";
 import { ToolButton } from "./ToolButton";
 
@@ -47,7 +47,7 @@ export const Toast = ({
       <p className="Toast__message">{message}</p>
       {closable && (
         <ToolButton
-          icon={close}
+          icon={CloseIcon}
           aria-label="close"
           type="icon"
           onClick={onClose}

+ 35 - 96
src/components/ToolIcon.scss

@@ -3,12 +3,19 @@
 
 .excalidraw {
   .ToolIcon {
+    border-radius: var(--border-radius-lg);
     display: inline-flex;
     align-items: center;
     position: relative;
     cursor: pointer;
     -webkit-tap-highlight-color: transparent;
     user-select: none;
+
+    &__hidden {
+      display: none !important;
+    }
+
+    @include toolbarButtonColorStates;
   }
 
   .ToolIcon--plain {
@@ -21,21 +28,15 @@
 
   .ToolIcon_type_radio,
   .ToolIcon_type_checkbox {
-    & + .ToolIcon__icon {
-      background-color: var(--button-gray-1);
-
-      &:hover {
-        background-color: var(--button-gray-2);
-      }
-      &:active {
-        background-color: var(--button-gray-3);
-      }
-    }
+    position: absolute;
+    opacity: 0;
+    pointer-events: none;
   }
 
   .ToolIcon__icon {
-    width: 2.5rem;
-    height: 2.5rem;
+    box-sizing: border-box;
+    width: var(--default-button-size);
+    height: var(--default-button-size);
     color: var(--icon-fill-color);
 
     display: flex;
@@ -50,8 +51,8 @@
 
     svg {
       position: relative;
-      height: 1em;
-      fill: var(--icon-fill-color);
+      width: var(--default-icon-size);
+      height: var(--default-icon-size);
       color: var(--icon-fill-color);
     }
   }
@@ -75,13 +76,14 @@
     font-size: 0.8em;
   }
 
-  .excalidraw .ToolIcon_type_button,
+  .ToolIcon_type_button,
   .Modal .ToolIcon_type_button,
   .ToolIcon_type_button {
     padding: 0;
     border: none;
     margin: 0;
     font-size: inherit;
+    background-color: initial;
 
     &:focus-visible {
       box-shadow: 0 0 0 2px var(--focus-highlight-color);
@@ -95,9 +97,9 @@
       }
     }
 
-    &:hover {
-      background-color: var(--button-gray-2);
-    }
+    // &:hover {
+    //   background-color: var(--button-gray-2);
+    // }
 
     &:active {
       background-color: var(--button-gray-3);
@@ -108,29 +110,8 @@
     }
 
     &--hide {
-      visibility: hidden;
-    }
-  }
-
-  .ToolIcon_type_radio,
-  .ToolIcon_type_checkbox {
-    position: absolute;
-    opacity: 0;
-    pointer-events: none;
-
-    &:not(.ToolIcon_toggle_opaque):checked + .ToolIcon__icon {
-      background-color: var(--button-gray-2);
-      &:active {
-        background-color: var(--button-gray-3);
-      }
-    }
-
-    &:focus-visible + .ToolIcon__icon {
-      box-shadow: 0 0 0 2px var(--focus-highlight-color);
-    }
-
-    &:active + .ToolIcon__icon {
-      background-color: var(--button-gray-3);
+      // visibility: hidden;
+      display: none !important;
     }
   }
 
@@ -163,66 +144,12 @@
     position: absolute;
     bottom: 2px;
     right: 3px;
-    font-size: 0.5em;
+    font-size: 0.625rem;
     color: var(--keybinding-color);
     font-family: var(--ui-font);
     user-select: none;
   }
 
-  // shrink shape icons on small viewports to make them fit
-  @media (max-width: 425px) {
-    .Shape .ToolIcon__icon {
-      width: 2rem;
-      height: 2rem;
-
-      svg {
-        height: 0.8em;
-      }
-    }
-  }
-
-  // move the lock button out of the way on small viewports
-  // it begins to collide with the GitHub icon before we switch to mobile mode
-  @media (max-width: 760px) {
-    .ToolIcon.ToolIcon_type_floating {
-      display: inline-block;
-      position: absolute;
-      right: -8px;
-
-      margin-left: 0;
-      border-radius: 20px 0 0 20px;
-      z-index: 1;
-
-      background-color: var(--button-gray-1);
-
-      &:hover {
-        background-color: var(--button-gray-1);
-      }
-
-      &:active {
-        background-color: var(--button-gray-2);
-      }
-
-      .ToolIcon__icon {
-        border-radius: inherit;
-      }
-
-      svg {
-        position: static;
-      }
-    }
-    .ToolIcon.ToolIcon__library {
-      top: calc(var(--sat) + 100px);
-    }
-
-    .ToolIcon.ToolIcon__lock {
-      top: calc(var(--sat) + 60px);
-    }
-    .ToolIcon.ToolIcon__penMode {
-      top: calc(var(--sat) + 140px);
-    }
-  }
-
   .unlocked-icon {
     :root[dir="ltr"] & {
       left: 2px;
@@ -232,4 +159,16 @@
       right: 2px;
     }
   }
+
+  .App-toolbar-container {
+    .ToolIcon__icon {
+      width: var(--lg-button-size);
+      height: var(--lg-button-size);
+
+      svg {
+        width: var(--lg-icon-size);
+        height: var(--lg-icon-size);
+      }
+    }
+  }
 }

+ 7 - 88
src/components/Toolbar.scss

@@ -2,101 +2,20 @@
 @import "../css/variables.module";
 
 .excalidraw {
-  .App-toolbar-container {
-    .ToolIcon_type_floating {
-      @include toolbarButtonColorStates;
-
-      &:not(.is-mobile) {
-        .ToolIcon__icon {
-          padding: 1px;
-          background-color: var(--island-bg-color);
-          box-shadow: 1px 3px 4px 0px rgb(0 0 0 / 15%);
-          border-radius: 50%;
-          transition: box-shadow 0.5s ease, transform 0.5s ease;
-        }
-      }
-
-      .ToolIcon_type_radio,
-      .ToolIcon_type_checkbox {
-        &:focus-within + .ToolIcon__icon {
-          // override for custom floating button shadow
-          box-shadow: 0 0 0 2px var(--focus-highlight-color);
-        }
-      }
-    }
-
-    .ToolIcon__hidden {
-      box-shadow: none !important;
-      background-color: transparent !important;
-      pointer-events: none !important;
-    }
-
-    .ToolIcon.ToolIcon__lock {
-      &.ToolIcon_type_floating {
-        margin-left: 0.1rem;
-      }
-    }
-
-    .ToolIcon__library {
-      margin-inline-start: var(--space-factor);
-    }
-
-    &.zen-mode {
-      .ToolIcon_type_floating {
-        .ToolIcon__icon {
-          box-shadow: none;
-          transform: scale(0.9);
-        }
-        .ToolIcon_type_checkbox:not(:checked):not(:hover):not(:active) {
-          & + .ToolIcon__icon {
-            svg {
-              fill: $oc-gray-5;
-              color: $oc-gray-5;
-            }
-          }
-        }
-      }
-    }
-  }
-
   .App-toolbar {
-    border-radius: var(--border-radius-lg);
-    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.01), 1px 1px 5px rgb(0 0 0 / 15%);
-
-    .ToolIcon {
-      &:hover {
-        --icon-fill-color: var(
-          --color-primary-contrast-offset,
-          var(--color-primary)
-        );
-        --keybinding-color: var(
-          --color-primary-contrast-offset,
-          var(--color-primary)
-        );
-      }
-      &:active {
-        --icon-fill-color: #{$oc-gray-9};
-        --keybinding-color: #{$oc-gray-9};
-      }
-
-      .ToolIcon__icon {
-        background: transparent;
-        border-radius: var(--border-radius-lg);
-      }
-
-      @include toolbarButtonColorStates;
-    }
-
     &.zen-mode {
       .ToolIcon__keybinding,
       .HintViewer {
         display: none;
       }
     }
-  }
 
-  &.theme--dark .App-toolbar .ToolIcon:active {
-    --icon-fill-color: #{$oc-gray-3};
-    --keybinding-color: #{$oc-gray-3};
+    &__divider {
+      width: 1px;
+      height: 1.5rem;
+      align-self: center;
+      background-color: var(--default-border-color);
+      margin: 0 0.5rem;
+    }
   }
 }

+ 12 - 5
src/components/UserList.scss

@@ -7,23 +7,30 @@
     display: flex;
     flex-wrap: wrap;
     justify-content: flex-end;
+    gap: 0.625rem;
 
     &:empty {
       display: none;
     }
+
+    // can fit max 5 avatars in a column
+    max-height: 140px;
+
+    // can fit max 10 avatars in a row when there's enough space
+    max-width: 290px;
+
+    // Tweak in 30px increments to fit more/fewer avatars in a row/column ^^
+
+    overflow: hidden;
   }
 
   .UserList > * {
     pointer-events: all;
-    margin: 0 0 var(--space-factor) var(--space-factor);
   }
 
   .UserList_mobile {
     padding: 0;
     justify-content: normal;
-  }
-
-  .UserList_mobile > * {
-    margin: 0 var(--space-factor) var(--space-factor) 0;
+    margin: 0.5rem 0;
   }
 }

+ 20 - 0
src/components/UserList.tsx

@@ -44,6 +44,26 @@ 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}

+ 273 - 0
src/components/WelcomeScreen.scss

@@ -0,0 +1,273 @@
+.excalidraw {
+  .virgil {
+    font-family: "Virgil";
+  }
+
+  .WelcomeScreen-logo {
+    display: flex;
+    align-items: center;
+    column-gap: 0.75rem;
+    font-size: 2.25rem;
+
+    svg {
+      width: 1.625rem;
+      height: auto;
+    }
+  }
+
+  .WelcomeScreen-decor {
+    pointer-events: none;
+
+    color: var(--color-gray-40);
+
+    &--subheading {
+      font-size: 1.125rem;
+      text-align: center;
+    }
+
+    &--help-pointer {
+      display: flex;
+      position: absolute;
+      right: 0;
+      bottom: 100%;
+
+      :root[dir="rtl"] & {
+        left: 0;
+        right: auto;
+      }
+
+      svg {
+        margin-top: 0.5rem;
+        width: 85px;
+        height: 71px;
+
+        transform: scaleX(-1) rotate(80deg);
+
+        :root[dir="rtl"] & {
+          transform: rotate(80deg);
+        }
+      }
+    }
+
+    &--top-toolbar-pointer {
+      position: absolute;
+      top: 100%;
+      left: 50%;
+      transform: translateX(-50%);
+      margin-top: 2.5rem;
+      display: flex;
+      align-items: baseline;
+
+      &__label {
+        width: 120px;
+        position: relative;
+        top: -0.5rem;
+      }
+
+      svg {
+        width: 38px;
+        height: 78px;
+
+        :root[dir="rtl"] & {
+          transform: scaleX(-1);
+        }
+      }
+    }
+
+    &--menu-pointer {
+      position: absolute;
+      width: 320px;
+      font-size: 1rem;
+
+      top: 100%;
+      margin-top: 0.25rem;
+      margin-inline-start: 0.6rem;
+
+      display: flex;
+      align-items: flex-end;
+      gap: 0.5rem;
+
+      svg {
+        width: 41px;
+        height: 94px;
+
+        :root[dir="rtl"] & {
+          transform: scaleX(-1);
+        }
+      }
+    }
+  }
+
+  .WelcomeScreen-container {
+    display: flex;
+    flex-direction: column;
+    gap: 2rem;
+    justify-content: center;
+    align-items: center;
+    position: absolute;
+    pointer-events: none;
+    left: 1rem;
+    top: 1rem;
+    right: 1rem;
+    bottom: 1rem;
+  }
+
+  .WelcomeScreen-items {
+    display: flex;
+    flex-direction: column;
+    gap: 2px;
+    justify-content: center;
+    align-items: center;
+  }
+
+  .WelcomeScreen-item {
+    box-sizing: border-box;
+
+    pointer-events: all;
+
+    color: var(--color-gray-50);
+    font-size: 0.875rem;
+
+    min-width: 300px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+    background: none;
+    border: none;
+
+    padding: 0.75rem;
+
+    border-radius: var(--border-radius-md);
+
+    &__label {
+      display: flex;
+      align-items: center;
+      column-gap: 0.5rem;
+
+      svg {
+        width: var(--default-icon-size);
+        height: var(--default-icon-size);
+      }
+    }
+
+    &__shortcut {
+      color: var(--color-gray-40);
+      font-size: 0.75rem;
+    }
+  }
+
+  &:not(:active) .WelcomeScreen-item:hover {
+    text-decoration: none;
+    background: var(--color-gray-10);
+
+    .WelcomeScreen-item__shortcut {
+      color: var(--color-gray-50);
+    }
+
+    .WelcomeScreen-item__label {
+      color: var(--color-gray-100);
+    }
+  }
+
+  .WelcomeScreen-item:active {
+    background: var(--color-gray-20);
+
+    .WelcomeScreen-item__shortcut {
+      color: var(--color-gray-50);
+    }
+
+    .WelcomeScreen-item__label {
+      color: var(--color-gray-100);
+    }
+
+    &--promo {
+      color: var(--color-promo) !important;
+
+      &:hover {
+        .WelcomeScreen-item__label {
+          color: var(--color-promo) !important;
+        }
+      }
+    }
+  }
+
+  &.theme--dark {
+    .WelcomeScreen-decor {
+      color: var(--color-gray-60);
+    }
+
+    .WelcomeScreen-item {
+      color: var(--color-gray-60);
+
+      &__shortcut {
+        color: var(--color-gray-60);
+      }
+    }
+
+    &:not(:active) .WelcomeScreen-item:hover {
+      background: var(--color-gray-85);
+
+      .WelcomeScreen-item__shortcut {
+        color: var(--color-gray-50);
+      }
+
+      .WelcomeScreen-item__label {
+        color: var(--color-gray-10);
+      }
+    }
+
+    .WelcomeScreen-item:active {
+      background-color: var(--color-gray-90);
+      .WelcomeScreen-item__label {
+        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 {
+      margin-top: 4rem;
+    }
+  }
+  @media (min-height: 600px) and (max-height: 900px) {
+    .WelcomeScreen-container {
+      margin-top: 8rem;
+    }
+  }
+  @media (max-height: 630px) {
+    .WelcomeScreen-decor--top-toolbar-pointer {
+      display: none;
+    }
+  }
+  @media (max-height: 500px) {
+    .WelcomeScreen-container {
+      display: none;
+    }
+  }
+
+  // @media (max-height: 740px) {
+  //   .WelcomeScreen-decor {
+  //     &--help-pointer,
+  //     &--top-toolbar-pointer,
+  //     &--menu-pointer {
+  //       display: none;
+  //     }
+  //   }
+  // }
+}

+ 131 - 0
src/components/WelcomeScreen.tsx

@@ -0,0 +1,131 @@
+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 {
+  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 = ({ actionManager }: { actionManager: ActionManager }) => {
+  const [, setCollabDialogShown] = useAtom(collabDialogShownAtom);
+
+  let subheadingJSX;
+
+  if (isExcalidrawPlusSignedUser) {
+    subheadingJSX = t("welcomeScreen.switchToPlusApp")
+      .split(/(Excalidraw\+)/)
+      .map((bit) => {
+        if (bit === "Excalidraw+") {
+          return (
+            <a
+              style={{ pointerEvents: "all" }}
+              href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
+            >
+              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">
+        <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;

+ 11 - 0
src/components/WelcomeScreenDecor.tsx

@@ -0,0 +1,11 @@
+import { ReactNode } from "react";
+
+const WelcomeScreenDecor = ({
+  children,
+  shouldRender,
+}: {
+  children: ReactNode;
+  shouldRender: boolean;
+}) => (shouldRender ? <>{children}</> : null);
+
+export default WelcomeScreenDecor;

Fichier diff supprimé car celui-ci est trop grand
+ 207 - 3
src/components/icons.tsx


+ 124 - 139
src/css/styles.scss

@@ -19,6 +19,10 @@
   height: 100%;
   width: 100%;
 
+  button {
+    cursor: pointer;
+  }
+
   &:focus {
     outline: none;
   }
@@ -85,15 +89,16 @@
   .panelColumn {
     display: flex;
     flex-direction: column;
+    row-gap: 0.75rem;
 
     h3,
     legend,
     .control-label {
-      margin-top: 0.333rem;
-      margin-bottom: 0.333rem;
+      margin: 0;
+      margin-bottom: 0.25rem;
       font-size: 0.75rem;
       color: var(--text-primary-color);
-      font-weight: bold;
+      font-weight: normal;
       display: block;
     }
 
@@ -102,12 +107,6 @@
       width: 100%;
     }
 
-    h3:first-child,
-    legend:first-child,
-    .control-label:first-child {
-      margin-top: 0;
-    }
-
     legend {
       padding: 0;
     }
@@ -119,11 +118,12 @@
 
     .buttonList {
       flex-wrap: wrap;
+      display: flex;
+      column-gap: 0.5rem;
+      row-gap: 0.5rem;
 
       label {
-        margin-right: 0.25rem;
         font-size: 0.75rem;
-        display: inline-block;
       }
 
       input[type="radio"],
@@ -136,38 +136,10 @@
       .iconRow {
         margin-top: 8px;
       }
-
-      .ToolIcon {
-        margin: 0;
-        margin-inline-end: 8px;
-
-        &:focus {
-          outline: transparent;
-          box-shadow: 0 0 0 2px var(--focus-highlight-color);
-        }
-
-        &:hover {
-          background-color: var(--button-gray-2);
-        }
-
-        &:active {
-          background-color: var(--button-gray-3);
-        }
-
-        &:disabled {
-          cursor: not-allowed;
-        }
-      }
-
-      .ToolIcon__icon {
-        width: 28px;
-        height: 28px;
-      }
     }
 
     fieldset {
       margin: 0;
-      margin-top: 0.333rem;
       padding: 0;
       border: none;
     }
@@ -185,64 +157,26 @@
     box-shadow: 0 0 0 2px var(--focus-highlight-color);
   }
 
-  button,
-  .buttonList label {
-    user-select: none;
-    background-color: var(--button-gray-1);
-    border: 0;
-    border-radius: var(--border-radius-md);
-    margin: 0.125rem 0;
-    padding: 0.25rem;
-    white-space: nowrap;
-
-    cursor: pointer;
-
-    &:focus-visible {
-      outline: transparent;
-      box-shadow: 0 0 0 2px var(--focus-highlight-color);
+  .buttonList {
+    .ToolIcon__icon {
+      all: unset !important;
+      display: flex !important;
     }
 
-    &:hover {
-      background-color: var(--button-gray-2);
+    button {
+      background-color: transparent;
     }
 
-    &:active {
-      background-color: var(--button-gray-3);
-    }
+    label,
+    button,
+    .zIndexButton {
+      @include outlineButtonStyles;
 
-    &:disabled {
-      cursor: not-allowed;
-    }
-  }
-
-  .active,
-  .buttonList label.active {
-    background-color: var(--color-primary);
-
-    --icon-fill-color: #{$oc-white};
-
-    &:hover {
-      background-color: var(--color-primary-darker);
-    }
-
-    &:active {
-      background-color: var(--color-primary-darkest);
-    }
-  }
+      padding: 0;
 
-  .buttonList.buttonListIcon {
-    label {
-      display: inline-flex;
-      justify-content: center;
-      align-items: center;
       svg {
-        width: 35px;
-        height: 14px;
-        padding: 2px;
-        opacity: 0.6;
-      }
-      &.active svg {
-        opacity: 1;
+        width: var(--default-icon-size);
+        height: var(--default-icon-size);
       }
     }
   }
@@ -289,8 +223,6 @@
   .App-toolbar {
     width: 100%;
 
-    box-sizing: border-box;
-
     .eraser {
       &.ToolIcon:hover {
         --icon-fill-color: #fff;
@@ -322,12 +254,27 @@
     color: var(--icon-fill-color);
   }
 
+  .shapes-section {
+    display: flex;
+    justify-content: center;
+    pointer-events: none !important;
+
+    & > * {
+      pointer-events: all;
+    }
+  }
+
   .App-menu_top {
-    grid-template-columns: auto max-content auto;
-    grid-gap: 4px;
+    grid-template-columns: 1fr 2fr 1fr;
+    grid-gap: 2rem;
     align-items: flex-start;
     cursor: default;
     pointer-events: none !important;
+
+    @media (min-width: 1536px) {
+      grid-template-columns: 1fr 1fr 1fr;
+      grid-gap: 3rem;
+    }
   }
 
   .layer-ui__wrapper:not(.disable-pointerEvents) .App-menu_top > * {
@@ -344,20 +291,14 @@
 
   .App-menu_bottom {
     position: absolute;
-    bottom: 0;
-    grid-template-columns: min-content auto min-content;
-    grid-gap: 15px;
+    bottom: 1rem;
+    display: flex;
+    justify-content: space-between;
     align-items: flex-start;
     cursor: default;
     pointer-events: none !important;
-
-    :root[dir="ltr"] & {
-      left: 0.25rem;
-    }
-
-    :root[dir="rtl"] & {
-      right: 0.25rem;
-    }
+    box-sizing: border-box;
+    padding: 0 1rem;
 
     &--transition-left {
       section {
@@ -390,7 +331,10 @@
 
   .App-menu__left {
     overflow-y: auto;
-    box-shadow: var(--shadow-island);
+    padding: 0.75rem;
+    width: 202px;
+    box-sizing: border-box;
+    position: absolute;
   }
 
   .dropdown-select {
@@ -426,55 +370,65 @@
     &:active {
       background-color: var(--button-gray-2);
     }
+
+    &__language {
+      height: 2rem;
+      background-color: var(--island-bg-color);
+      border-color: var(--default-border-color) !important;
+      cursor: pointer;
+
+      &:hover {
+        background-color: var(--island-bg-color);
+      }
+    }
   }
 
-  .zIndexButton {
-    margin: 0;
-    margin-inline-end: 8px;
-    padding: 5px;
-    display: inline-flex;
-    align-items: center;
-    justify-content: center;
+  .disable-zen-mode {
+    border-radius: var(--border-radius-lg);
+    background-color: var(--color-gray-20);
+    border: 1px solid var(--color-gray-30);
+    padding: 10px 20px;
 
-    svg {
-      width: 18px;
-      height: 18px;
+    &:hover {
+      background-color: var(--color-gray-30);
     }
   }
 
   .scroll-back-to-content {
-    color: var(--popup-text-color);
+    border-radius: var(--border-radius-lg);
+    background-color: var(--island-bg-color);
+    color: var(--icon-fill-color);
+
+    border: 1px solid var(--default-border-color);
+    padding: 10px 20px;
     position: absolute;
     left: 50%;
     bottom: 30px;
     transform: translateX(-50%);
-    padding: 10px 20px;
     pointer-events: all;
+
+    &:hover {
+      background-color: var(--button-hover);
+    }
+
+    &:active {
+      border: 1px solid var(--color-primary-darkest);
+    }
   }
 
   .help-icon {
-    display: flex;
-    cursor: pointer;
-    fill: $oc-gray-6;
-    padding: 0;
-    margin: 0;
-    background: none;
-    color: var(--icon-fill-color);
+    @include outlineButtonStyles;
+    background-color: var(--island-bg-color);
+    width: var(--lg-button-size);
+    height: var(--lg-button-size);
 
     svg {
-      width: 1.5rem;
-      height: 1.5rem;
-    }
-
-    &:hover {
-      background: none;
+      width: var(--lg-icon-size);
+      height: var(--lg-icon-size);
     }
   }
 
   .reset-zoom-button {
-    padding: 0.2em;
-    background: transparent;
-    color: var(--text-primary-color);
     font-family: var(--ui-font);
   }
 
@@ -491,7 +445,6 @@
   .eraser-buttons {
     display: grid;
     grid-auto-flow: column;
-    gap: 0.4em;
     margin-top: auto;
     margin-bottom: auto;
     margin-inline-start: 0.6em;
@@ -572,17 +525,49 @@
   // use custom, minimalistic scrollbar
   // (doesn't work in Firefox)
   ::-webkit-scrollbar {
-    width: 5px;
+    width: 3px;
   }
+
   ::-webkit-scrollbar-thumb {
-    background: var(--button-gray-2);
+    background: var(--scrollbar-thumb);
     border-radius: 10px;
   }
   ::-webkit-scrollbar-thumb:hover {
-    background: var(--button-gray-3);
+    background: var(--scrollbar-thumb-hover);
   }
   ::-webkit-scrollbar-thumb:active {
-    background: var(--button-gray-2);
+    background: var(--scrollbar-thumb);
+  }
+
+  .mobile-misc-tools-container {
+    position: fixed;
+    top: 5rem;
+    right: 0;
+    display: flex;
+    flex-direction: column;
+    border: 1px solid var(--sidebar-border-color);
+    border-top-left-radius: var(--border-radius-lg);
+    border-bottom-left-radius: var(--border-radius-lg);
+    border-right: 0;
+
+    background-color: var(--island-bg-color);
+
+    .ToolIcon__icon {
+      border-radius: 0;
+    }
+
+    .library-button {
+      border: 0;
+    }
+  }
+
+  .App-toolbar--mobile {
+    overflow-x: hidden;
+    max-width: 100vw;
+
+    .ToolIcon__keybinding {
+      display: none;
+    }
   }
 }
 

+ 95 - 18
src/css/theme.scss

@@ -9,10 +9,10 @@
   --button-gray-2: #{$oc-gray-4};
   --button-gray-3: #{$oc-gray-5};
   --button-special-active-bg-color: #{$oc-green-0};
-  --dialog-border-color: #{$oc-gray-6};
+  --dialog-border-color: var(--color-gray-20);
   --dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
   --focus-highlight-color: #{$oc-blue-2};
-  --icon-fill-color: #{$oc-gray-9};
+  --icon-fill-color: var(--color-gray-80);
   --icon-green-fill-color: #{$oc-green-9};
   --default-bg-color: #{$oc-white};
   --input-bg-color: #{$oc-white};
@@ -20,7 +20,7 @@
   --input-hover-bg-color: #{$oc-gray-1};
   --input-label-color: #{$oc-gray-7};
   --island-bg-color: rgba(255, 255, 255, 0.96);
-  --keybinding-color: #{$oc-gray-5};
+  --keybinding-color: var(--color-gray-40);
   --link-color: #{$oc-blue-7};
   --overlay-bg-color: #{transparentize($oc-white, 0.12)};
   --popup-bg-color: #{$oc-white};
@@ -32,22 +32,75 @@
   --sar: env(safe-area-inset-right);
   --sat: env(safe-area-inset-top);
   --select-highlight-color: #{$oc-blue-5};
-  --shadow-island: 0 0 0 1px rgba(0, 0, 0, 0.01), 1px 1px 5px rgb(0 0 0 / 12%);
+  --shadow-island: 0px 7px 14px rgba(0, 0, 0, 0.05),
+    0px 0px 3.12708px rgba(0, 0, 0, 0.0798),
+    0px 0px 0.931014px rgba(0, 0, 0, 0.1702);
+  --button-hover: var(--color-gray-10);
+  --default-border-color: var(--color-gray-30);
+
+  --default-button-size: 2rem;
+  --default-icon-size: 1rem;
+  --lg-button-size: 2.25rem;
+  --lg-icon-size: 1rem;
+
+  @media screen and (min-device-width: 1921px) {
+    --lg-button-size: 2.5rem;
+    --lg-icon-size: 1.25rem;
+    --default-button-size: 2.25rem;
+    --default-icon-size: 1.25rem;
+  }
+
+  --scrollbar-thumb: var(--button-gray-2);
+  --scrollbar-thumb-hover: var(--button-gray-3);
+
+  --modal-shadow: 0px 100px 80px rgba(0, 0, 0, 0.07),
+    0px 41.7776px 33.4221px rgba(0, 0, 0, 0.0503198),
+    0px 22.3363px 17.869px rgba(0, 0, 0, 0.0417275),
+    0px 12.5216px 10.0172px rgba(0, 0, 0, 0.035),
+    0px 6.6501px 5.32008px rgba(0, 0, 0, 0.0282725),
+    0px 2.76726px 2.21381px rgba(0, 0, 0, 0.0196802);
+  --avatar-border-color: var(--color-gray-20);
+  --sidebar-shadow: 0px 100px 80px rgba(0, 0, 0, 0.07),
+    0px 41.7776px 33.4221px rgba(0, 0, 0, 0.0503198),
+    0px 22.3363px 17.869px rgba(0, 0, 0, 0.0417275),
+    0px 12.5216px 10.0172px rgba(0, 0, 0, 0.035),
+    0px 6.6501px 5.32008px rgba(0, 0, 0, 0.0282725),
+    0px 2.76726px 2.21381px rgba(0, 0, 0, 0.0196802);
+  --sidebar-border-color: var(--color-gray-20);
+  --sidebar-bg-color: #fff;
+  --library-dropdown-shadow: 0px 15px 6px rgba(0, 0, 0, 0.01),
+    0px 8px 5px rgba(0, 0, 0, 0.05), 0px 4px 4px rgba(0, 0, 0, 0.09),
+    0px 1px 2px rgba(0, 0, 0, 0.1), 0px 0px 0px rgba(0, 0, 0, 0.1);
 
   --space-factor: 0.25rem;
-  --text-primary-color: #{$oc-gray-8};
+  --text-primary-color: var(--color-gray-80);
+
+  --color-selection: #6965db;
 
   --color-primary: #6965db;
   --color-primary-darker: #5b57d1;
   --color-primary-darkest: #4a47b1;
-  --color-primary-light: #e2e1fc;
+  --color-primary-light: #e3e2fe;
+
+  --color-gray-10: #f5f5f5;
+  --color-gray-20: #ebebeb;
+  --color-gray-30: #d6d6d6;
+  --color-gray-40: #b8b8b8;
+  --color-gray-50: #999999;
+  --color-gray-60: #7a7a7a;
+  --color-gray-70: #5c5c5c;
+  --color-gray-80: #3d3d3d;
+  --color-gray-85: #242424;
+  --color-gray-90: #1e1e1e;
+  --color-gray-100: #121212;
+
+  --color-danger: #db6965;
+  --color-promo: #e70078;
 
   --border-radius-md: 0.375rem;
   --border-radius-lg: 0.5rem;
 
   &.theme--dark {
-    background: $oc-black;
-
     &.theme--dark-background-none {
       background: none;
     }
@@ -57,22 +110,23 @@
     --theme-filter: #{$theme-filter};
     --button-destructive-bg-color: #5a0000;
     --button-destructive-color: #{$oc-red-3};
+
     --button-gray-1: #363636;
     --button-gray-2: #272727;
     --button-gray-3: #222;
     --button-special-active-bg-color: #204624;
-    --dialog-border-color: #{$oc-gray-9};
+    --dialog-border-color: var(--color-gray-80);
     --dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path fill="%23ced4da" d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
     --focus-highlight-color: #{$oc-blue-6};
-    --icon-fill-color: #{$oc-gray-4};
+    --icon-fill-color: var(--color-gray-40);
     --icon-green-fill-color: #{$oc-green-4};
     --default-bg-color: #121212;
     --input-bg-color: #121212;
     --input-border-color: #2e2e2e;
     --input-hover-bg-color: #181818;
     --input-label-color: #{$oc-gray-2};
-    --island-bg-color: rgba(30, 30, 30, 0.98);
-    --keybinding-color: #{$oc-gray-6};
+    --island-bg-color: #262627;
+    --keybinding-color: var(--color-gray-60);
     --link-color: #{$oc-blue-4};
     --overlay-bg-color: #{transparentize($oc-gray-8, 0.88)};
     --popup-bg-color: #2c2c2c;
@@ -80,12 +134,35 @@
     --popup-text-color: #{$oc-gray-4};
     --popup-text-inverted-color: #2c2c2c;
     --select-highlight-color: #{$oc-blue-4};
-    --shadow-island: 1px 1px 5px #{transparentize($oc-black, 0.7)};
-    --text-primary-color: #{$oc-gray-4};
+    --text-primary-color: var(--color-gray-40);
+    --button-hover: var(--color-gray-80);
+    --default-border-color: var(--color-gray-80);
+    --shadow-island: 0px 13px 33px rgba(0, 0, 0, 0.07),
+      0px 4.13px 9.94853px rgba(0, 0, 0, 0.0456112),
+      0px 1.13px 4.13211px rgba(0, 0, 0, 0.035),
+      0px 0.769896px 1.4945px rgba(0, 0, 0, 0.0243888);
+    --modal-shadow: 0px 100px 80px rgba(0, 0, 0, 0.07),
+      0px 41.7776px 33.4221px rgba(0, 0, 0, 0.0503198),
+      0px 22.3363px 17.869px rgba(0, 0, 0, 0.0417275),
+      0px 12.5216px 10.0172px rgba(0, 0, 0, 0.035),
+      0px 6.6501px 5.32008px rgba(0, 0, 0, 0.0282725),
+      0px 2.76726px 2.21381px rgba(0, 0, 0, 0.0196802);
+    --avatar-border-color: var(--color-gray-85);
+    --sidebar-border-color: var(--color-gray-85);
+    --sidebar-bg-color: #191919;
+
+    --scrollbar-thumb: #{$oc-gray-8};
+    --scrollbar-thumb-hover: #{$oc-gray-7};
+
+    // will be inverted to a lighter color.
+    --color-selection: #3530c4;
+
+    --color-primary: #a8a5ff;
+    --color-primary-darker: #b2aeff;
+    --color-primary-darkest: #beb9ff;
+    --color-primary-light: #4f4d6f;
 
-    --color-primary: #5650f0;
-    --color-primary-darker: #4b46d8;
-    --color-primary-darkest: #3e39be;
-    --color-primary-light: #3f3d64;
+    --color-danger: #ffa8a5;
+    --color-promo: #d297ff;
   }
 }

+ 69 - 9
src/css/variables.module.scss

@@ -7,18 +7,28 @@
 }
 
 @mixin toolbarButtonColorStates {
+  &.fillable {
+    .ToolIcon_type_radio,
+    .ToolIcon_type_checkbox {
+      &:checked + .ToolIcon__icon {
+        --icon-fill-color: var(--color-primary-darker);
+
+        svg {
+          fill: var(--icon-fill-color);
+        }
+      }
+    }
+  }
+
   .ToolIcon_type_radio,
   .ToolIcon_type_checkbox {
-    & + .ToolIcon__icon:active {
-      background: var(--color-primary-light);
-    }
     &:checked + .ToolIcon__icon {
-      background: var(--color-primary);
-      --icon-fill-color: #{$oc-white};
-      --keybinding-color: #{$oc-white};
-    }
-    &:checked + .ToolIcon__icon:active {
-      background: var(--color-primary-darker);
+      background: var(--color-primary-light);
+      --keybinding-color: var(--color-gray-60);
+
+      svg {
+        color: var(--color-primary-darker);
+      }
     }
   }
 
@@ -26,6 +36,56 @@
     bottom: 4px;
     right: 4px;
   }
+
+  .ToolIcon__icon {
+    &:hover {
+      background: var(--button-hover);
+    }
+
+    &:active {
+      background: var(--button-hover);
+      border: 1px solid var(--color-primary-darkest);
+    }
+  }
+}
+
+@mixin outlineButtonStyles {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 0.625rem;
+  width: var(--default-button-size);
+  height: var(--default-button-size);
+  box-sizing: border-box;
+  border-width: 1px;
+  border-style: solid;
+  border-color: var(--default-border-color);
+  border-radius: var(--border-radius-lg);
+  cursor: pointer;
+  background-color: transparent;
+  color: var(--text-primary-color);
+
+  &:hover {
+    background-color: var(--button-hover);
+  }
+
+  &:active {
+    background-color: var(--button-hover);
+    border-color: var(--color-primary-darkest);
+  }
+
+  &.active {
+    background-color: var(--color-primary-light);
+    border-color: var(--color-primary-light);
+
+    &:hover {
+      background-color: var(--color-primary-light);
+    }
+
+    svg {
+      color: var(--color-primary-darker);
+    }
+  }
 }
 
 $theme-filter: "invert(93%) hue-rotate(180deg)";

+ 4 - 4
src/element/Hyperlink.tsx

@@ -10,7 +10,7 @@ import { NonDeletedExcalidrawElement } from "./types";
 
 import { register } from "../actions/register";
 import { ToolButton } from "../components/ToolButton";
-import { editIcon, link, trash } from "../components/icons";
+import { FreedrawIcon, LinkIcon, TrashIcon } from "../components/icons";
 import { t } from "../i18n";
 import {
   useCallback,
@@ -197,7 +197,7 @@ export const Hyperlink = ({
             label={t("buttons.edit")}
             onClick={onEdit}
             className="excalidraw-hyperlinkContainer--edit"
-            icon={editIcon}
+            icon={FreedrawIcon}
           />
         )}
 
@@ -209,7 +209,7 @@ export const Hyperlink = ({
             label={t("buttons.remove")}
             onClick={handleRemove}
             className="excalidraw-hyperlinkContainer--remove"
-            icon={trash}
+            icon={TrashIcon}
           />
         )}
       </div>
@@ -277,7 +277,7 @@ export const actionLink = register({
     return (
       <ToolButton
         type="button"
-        icon={link}
+        icon={LinkIcon}
         aria-label={t(getContextMenuLabel(elements, appState))}
         title={`${t("labels.link.label")} - ${getShortcutKey("CtrlOrCmd+K")}`}
         onClick={() => updateData(null)}

+ 2 - 2
src/element/transformHandles.ts

@@ -100,7 +100,7 @@ export const getTransformHandlesFromCoords = (
   const cx = (x1 + x2) / 2;
   const cy = (y1 + y2) / 2;
   const dashedLineMargin = margin / zoom.value;
-  const centeringOffset = (size - 8) / (2 * zoom.value);
+  const centeringOffset = (size - DEFAULT_SPACING * 2) / (2 * zoom.value);
 
   const transformHandles: TransformHandles = {
     nw: omitSides.nw
@@ -253,7 +253,7 @@ export const getTransformHandles = (
     omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
   }
   const dashedLineMargin = isLinearElement(element)
-    ? DEFAULT_SPACING * 3
+    ? DEFAULT_SPACING + 8
     : DEFAULT_SPACING;
   return getTransformHandlesFromCoords(
     getElementAbsoluteCoords(element),

+ 4 - 0
src/excalidraw-app/collab/RoomDialog.scss

@@ -1,6 +1,10 @@
 @import "../../css/variables.module";
 
 .excalidraw {
+  .RoomDialog__button {
+    border: 1px solid var(--default-border-color) !important;
+  }
+
   .RoomDialog-linkContainer {
     display: flex;
     margin: 1.5em 0;

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff