Sfoglia il codice sorgente

feat: new mobile layout (#9996)

* compact bottom toolbar

* put menu trigger to top left

* add popup to switch between grouped tool types

* add a dedicated mobile toolbar

* update position for mobile

* fix active tool type

* add mobile mode as well

* mobile actions

* remove refactored popups

* excali logo mobile

* include mobile

* update mobile menu layout

* move selection and deletion back to right

* do not fill eraser

* fix styling

* fix active styling

* bigger buttons, smaller gaps

* fix other tools not opened

* fix: Style panel persistence and restore

Signed-off-by: Mark Tolmacs <[email protected]>

* move hidden action btns to extra popover

* fix dropdown overlapping with welcome screen

* replace custom popup with popover

* improve button styles

* swapping redo and delete

* always show undo & redo and improve styling

* change background

* toolbar styles

* no any

* persist perferred selection tool and align tablet as well

* add a renderTopLeftUI to props

* tweak border and bg

* show combined properties only when using suitable tools

* fix preferred tool

* new stroke icon

* hide color picker hot keys

* init preferred tool based on device

* fix main menu sizing

* fix welcome screen offset

* put text before image

* disable call highlight on buttons

* fix renderTopLeftUI

---------

Signed-off-by: Mark Tolmacs <[email protected]>
Co-authored-by: Mark Tolmacs <[email protected]>
Co-authored-by: dwelle <[email protected]>
Ryan Di 2 settimane fa
parent
commit
416e8b3e42
47 ha cambiato i file con 2362 aggiunte e 667 eliminazioni
  1. 5 0
      packages/common/src/constants.ts
  2. 7 1
      packages/element/src/comparisons.ts
  3. 6 3
      packages/excalidraw/actions/actionCanvas.tsx
  4. 15 3
      packages/excalidraw/actions/actionDeleteSelected.tsx
  5. 10 1
      packages/excalidraw/actions/actionDuplicateSelection.tsx
  6. 2 2
      packages/excalidraw/actions/actionFinalize.tsx
  7. 19 3
      packages/excalidraw/actions/actionHistory.tsx
  8. 26 21
      packages/excalidraw/actions/actionProperties.tsx
  9. 6 1
      packages/excalidraw/appState.ts
  10. 34 36
      packages/excalidraw/components/Actions.scss
  11. 661 350
      packages/excalidraw/components/Actions.tsx
  12. 38 19
      packages/excalidraw/components/App.tsx
  13. 15 0
      packages/excalidraw/components/ColorPicker/ColorPicker.scss
  14. 22 22
      packages/excalidraw/components/ColorPicker/ColorPicker.tsx
  15. 21 1
      packages/excalidraw/components/ColorPicker/Picker.tsx
  16. 3 1
      packages/excalidraw/components/ColorPicker/PickerColorList.tsx
  17. 10 2
      packages/excalidraw/components/ColorPicker/ShadeList.tsx
  18. 14 0
      packages/excalidraw/components/ExcalidrawLogo.scss
  19. 1 1
      packages/excalidraw/components/ExcalidrawLogo.tsx
  20. 1 0
      packages/excalidraw/components/FontPicker/FontPicker.tsx
  21. 7 5
      packages/excalidraw/components/FontPicker/FontPickerList.tsx
  22. 13 0
      packages/excalidraw/components/FontPicker/FontPickerTrigger.tsx
  23. 1 1
      packages/excalidraw/components/HandButton.tsx
  24. 4 6
      packages/excalidraw/components/IconPicker.tsx
  25. 4 4
      packages/excalidraw/components/LayerUI.tsx
  26. 77 132
      packages/excalidraw/components/MobileMenu.tsx
  27. 78 0
      packages/excalidraw/components/MobileToolBar.scss
  28. 471 0
      packages/excalidraw/components/MobileToolBar.tsx
  29. 18 0
      packages/excalidraw/components/ToolPopover.scss
  30. 120 0
      packages/excalidraw/components/ToolPopover.tsx
  31. 4 0
      packages/excalidraw/components/Toolbar.scss
  32. 26 4
      packages/excalidraw/components/dropdownMenu/DropdownMenu.scss
  33. 12 1
      packages/excalidraw/components/dropdownMenu/DropdownMenu.tsx
  34. 3 0
      packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx
  35. 1 13
      packages/excalidraw/components/icons.tsx
  36. 2 0
      packages/excalidraw/components/main-menu/MainMenu.tsx
  37. 1 1
      packages/excalidraw/components/shapes.tsx
  38. 3 7
      packages/excalidraw/components/welcome-screen/WelcomeScreen.scss
  39. 28 18
      packages/excalidraw/css/styles.scss
  40. 9 0
      packages/excalidraw/css/theme.scss
  41. 16 0
      packages/excalidraw/css/variables.module.scss
  42. 2 0
      packages/excalidraw/index.tsx
  43. 68 0
      packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap
  44. 255 3
      packages/excalidraw/tests/__snapshots__/history.test.tsx.snap
  45. 209 1
      packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap
  46. 10 4
      packages/excalidraw/types.ts
  47. 4 0
      packages/utils/tests/__snapshots__/export.test.ts.snap

+ 5 - 0
packages/common/src/constants.ts

@@ -543,3 +543,8 @@ export enum UserIdleState {
 export const LINE_POLYGON_POINT_MERGE_DISTANCE = 20;
 
 export const DOUBLE_TAP_POSITION_THRESHOLD = 35;
+
+// glass background for mobile action buttons
+export const MOBILE_ACTION_BUTTON_BG = {
+  background: "var(--mobile-action-button-bg)",
+} as const;

+ 7 - 1
packages/element/src/comparisons.ts

@@ -10,7 +10,13 @@ export const hasBackground = (type: ElementOrToolType) =>
   type === "freedraw";
 
 export const hasStrokeColor = (type: ElementOrToolType) =>
-  type !== "image" && type !== "frame" && type !== "magicframe";
+  type === "rectangle" ||
+  type === "ellipse" ||
+  type === "diamond" ||
+  type === "freedraw" ||
+  type === "arrow" ||
+  type === "line" ||
+  type === "text";
 
 export const hasStrokeWidth = (type: ElementOrToolType) =>
   type === "rectangle" ||

+ 6 - 3
packages/excalidraw/actions/actionCanvas.tsx

@@ -122,7 +122,10 @@ export const actionClearCanvas = register({
         pasteDialog: appState.pasteDialog,
         activeTool:
           appState.activeTool.type === "image"
-            ? { ...appState.activeTool, type: app.defaultSelectionTool }
+            ? {
+                ...appState.activeTool,
+                type: app.state.preferredSelectionTool.type,
+              }
             : appState.activeTool,
       },
       captureUpdate: CaptureUpdateAction.IMMEDIATELY,
@@ -501,7 +504,7 @@ export const actionToggleEraserTool = register({
     if (isEraserActive(appState)) {
       activeTool = updateActiveTool(appState, {
         ...(appState.activeTool.lastActiveTool || {
-          type: app.defaultSelectionTool,
+          type: app.state.preferredSelectionTool.type,
         }),
         lastActiveToolBeforeEraser: null,
       });
@@ -532,7 +535,7 @@ export const actionToggleLassoTool = register({
   icon: LassoIcon,
   trackEvent: { category: "toolbar" },
   predicate: (elements, appState, props, app) => {
-    return app.defaultSelectionTool !== "lasso";
+    return app.state.preferredSelectionTool.type !== "lasso";
   },
   perform: (elements, appState, _, app) => {
     let activeTool: AppState["activeTool"];

+ 15 - 3
packages/excalidraw/actions/actionDeleteSelected.tsx

@@ -1,4 +1,8 @@
-import { KEYS, updateActiveTool } from "@excalidraw/common";
+import {
+  KEYS,
+  MOBILE_ACTION_BUTTON_BG,
+  updateActiveTool,
+} from "@excalidraw/common";
 
 import { getNonDeletedElements } from "@excalidraw/element";
 import { fixBindingsAfterDeletion } from "@excalidraw/element";
@@ -299,7 +303,7 @@ export const actionDeleteSelected = register({
       appState: {
         ...nextAppState,
         activeTool: updateActiveTool(appState, {
-          type: app.defaultSelectionTool,
+          type: app.state.preferredSelectionTool.type,
         }),
         multiElement: null,
         activeEmbeddable: null,
@@ -323,7 +327,15 @@ export const actionDeleteSelected = register({
       title={t("labels.delete")}
       aria-label={t("labels.delete")}
       onClick={() => updateData(null)}
-      visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
+      disabled={
+        !isSomeElementSelected(getNonDeletedElements(elements), appState)
+      }
+      style={{
+        ...(appState.stylesPanelMode === "mobile" &&
+        appState.openPopup !== "compactOtherProperties"
+          ? MOBILE_ACTION_BUTTON_BG
+          : {}),
+      }}
     />
   ),
 });

+ 10 - 1
packages/excalidraw/actions/actionDuplicateSelection.tsx

@@ -1,6 +1,7 @@
 import {
   DEFAULT_GRID_SIZE,
   KEYS,
+  MOBILE_ACTION_BUTTON_BG,
   arrayToMap,
   getShortcutKey,
 } from "@excalidraw/common";
@@ -115,7 +116,15 @@ export const actionDuplicateSelection = register({
       )}`}
       aria-label={t("labels.duplicateSelection")}
       onClick={() => updateData(null)}
-      visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
+      disabled={
+        !isSomeElementSelected(getNonDeletedElements(elements), appState)
+      }
+      style={{
+        ...(appState.stylesPanelMode === "mobile" &&
+        appState.openPopup !== "compactOtherProperties"
+          ? MOBILE_ACTION_BUTTON_BG
+          : {}),
+      }}
     />
   ),
 });

+ 2 - 2
packages/excalidraw/actions/actionFinalize.tsx

@@ -261,13 +261,13 @@ export const actionFinalize = register({
     if (appState.activeTool.type === "eraser") {
       activeTool = updateActiveTool(appState, {
         ...(appState.activeTool.lastActiveTool || {
-          type: app.defaultSelectionTool,
+          type: app.state.preferredSelectionTool.type,
         }),
         lastActiveToolBeforeEraser: null,
       });
     } else {
       activeTool = updateActiveTool(appState, {
-        type: app.defaultSelectionTool,
+        type: app.state.preferredSelectionTool.type,
       });
     }
 

+ 19 - 3
packages/excalidraw/actions/actionHistory.tsx

@@ -1,4 +1,10 @@
-import { isWindows, KEYS, matchKey, arrayToMap } from "@excalidraw/common";
+import {
+  isWindows,
+  KEYS,
+  matchKey,
+  arrayToMap,
+  MOBILE_ACTION_BUTTON_BG,
+} from "@excalidraw/common";
 
 import { CaptureUpdateAction } from "@excalidraw/element";
 
@@ -67,7 +73,7 @@ export const createUndoAction: ActionCreator = (history) => ({
     ),
   keyTest: (event) =>
     event[KEYS.CTRL_OR_CMD] && matchKey(event, KEYS.Z) && !event.shiftKey,
-  PanelComponent: ({ updateData, data }) => {
+  PanelComponent: ({ appState, updateData, data }) => {
     const { isUndoStackEmpty } = useEmitter<HistoryChangedEvent>(
       history.onHistoryChangedEmitter,
       new HistoryChangedEvent(
@@ -85,6 +91,11 @@ export const createUndoAction: ActionCreator = (history) => ({
         size={data?.size || "medium"}
         disabled={isUndoStackEmpty}
         data-testid="button-undo"
+        style={{
+          ...(appState.stylesPanelMode === "mobile"
+            ? MOBILE_ACTION_BUTTON_BG
+            : {}),
+        }}
       />
     );
   },
@@ -103,7 +114,7 @@ export const createRedoAction: ActionCreator = (history) => ({
   keyTest: (event) =>
     (event[KEYS.CTRL_OR_CMD] && event.shiftKey && matchKey(event, KEYS.Z)) ||
     (isWindows && event.ctrlKey && !event.shiftKey && matchKey(event, KEYS.Y)),
-  PanelComponent: ({ updateData, data }) => {
+  PanelComponent: ({ appState, updateData, data }) => {
     const { isRedoStackEmpty } = useEmitter(
       history.onHistoryChangedEmitter,
       new HistoryChangedEvent(
@@ -121,6 +132,11 @@ export const createRedoAction: ActionCreator = (history) => ({
         size={data?.size || "medium"}
         disabled={isRedoStackEmpty}
         data-testid="button-redo"
+        style={{
+          ...(appState.stylesPanelMode === "mobile"
+            ? MOBILE_ACTION_BUTTON_BG
+            : {}),
+        }}
       />
     );
   },

+ 26 - 21
packages/excalidraw/actions/actionProperties.tsx

@@ -348,7 +348,10 @@ export const actionChangeStrokeColor = register({
         elements={elements}
         appState={appState}
         updateData={updateData}
-        compactMode={appState.stylesPanelMode === "compact"}
+        compactMode={
+          appState.stylesPanelMode === "compact" ||
+          appState.stylesPanelMode === "mobile"
+        }
       />
     </>
   ),
@@ -428,7 +431,10 @@ export const actionChangeBackgroundColor = register({
         elements={elements}
         appState={appState}
         updateData={updateData}
-        compactMode={appState.stylesPanelMode === "compact"}
+        compactMode={
+          appState.stylesPanelMode === "compact" ||
+          appState.stylesPanelMode === "mobile"
+        }
       />
     </>
   ),
@@ -531,9 +537,7 @@ export const actionChangeStrokeWidth = register({
   },
   PanelComponent: ({ elements, appState, updateData, app, data }) => (
     <fieldset>
-      {appState.stylesPanelMode === "full" && (
-        <legend>{t("labels.strokeWidth")}</legend>
-      )}
+      <legend>{t("labels.strokeWidth")}</legend>
       <div className="buttonList">
         <RadioSelection
           group="stroke-width"
@@ -590,9 +594,7 @@ export const actionChangeSloppiness = register({
   },
   PanelComponent: ({ elements, appState, updateData, app, data }) => (
     <fieldset>
-      {appState.stylesPanelMode === "full" && (
-        <legend>{t("labels.sloppiness")}</legend>
-      )}
+      <legend>{t("labels.sloppiness")}</legend>
       <div className="buttonList">
         <RadioSelection
           group="sloppiness"
@@ -645,9 +647,7 @@ export const actionChangeStrokeStyle = register({
   },
   PanelComponent: ({ elements, appState, updateData, app, data }) => (
     <fieldset>
-      {appState.stylesPanelMode === "full" && (
-        <legend>{t("labels.strokeStyle")}</legend>
-      )}
+      <legend>{t("labels.strokeStyle")}</legend>
       <div className="buttonList">
         <RadioSelection
           group="strokeStyle"
@@ -776,7 +776,8 @@ export const actionChangeFontSize = register({
           onChange={(value) => {
             withCaretPositionPreservation(
               () => updateData(value),
-              appState.stylesPanelMode === "compact",
+              appState.stylesPanelMode === "compact" ||
+                appState.stylesPanelMode === "mobile",
               !!appState.editingTextElement,
               data?.onPreventClose,
             );
@@ -1040,7 +1041,7 @@ export const actionChangeFontFamily = register({
 
     return result;
   },
-  PanelComponent: ({ elements, appState, app, updateData, data }) => {
+  PanelComponent: ({ elements, appState, app, updateData }) => {
     const cachedElementsRef = useRef<ElementsMap>(new Map());
     const prevSelectedFontFamilyRef = useRef<number | null>(null);
     // relying on state batching as multiple `FontPicker` handlers could be called in rapid succession and we want to combine them
@@ -1117,7 +1118,7 @@ export const actionChangeFontFamily = register({
     }, []);
 
     return (
-      <fieldset>
+      <>
         {appState.stylesPanelMode === "full" && (
           <legend>{t("labels.fontFamily")}</legend>
         )}
@@ -1125,7 +1126,7 @@ export const actionChangeFontFamily = register({
           isOpened={appState.openPopup === "fontFamily"}
           selectedFontFamily={selectedFontFamily}
           hoveredFontFamily={appState.currentHoveredFontFamily}
-          compactMode={appState.stylesPanelMode === "compact"}
+          compactMode={appState.stylesPanelMode !== "full"}
           onSelect={(fontFamily) => {
             withCaretPositionPreservation(
               () => {
@@ -1137,7 +1138,8 @@ export const actionChangeFontFamily = register({
                 // defensive clear so immediate close won't abuse the cached elements
                 cachedElementsRef.current.clear();
               },
-              appState.stylesPanelMode === "compact",
+              appState.stylesPanelMode === "compact" ||
+                appState.stylesPanelMode === "mobile",
               !!appState.editingTextElement,
             );
           }}
@@ -1213,7 +1215,8 @@ export const actionChangeFontFamily = register({
 
               // Refocus text editor when font picker closes if we were editing text
               if (
-                appState.stylesPanelMode === "compact" &&
+                (appState.stylesPanelMode === "compact" ||
+                  appState.stylesPanelMode === "mobile") &&
                 appState.editingTextElement
               ) {
                 restoreCaretPosition(null); // Just refocus without saved position
@@ -1221,7 +1224,7 @@ export const actionChangeFontFamily = register({
             }
           }}
         />
-      </fieldset>
+      </>
     );
   },
 });
@@ -1314,7 +1317,8 @@ export const actionChangeTextAlign = register({
             onChange={(value) => {
               withCaretPositionPreservation(
                 () => updateData(value),
-                appState.stylesPanelMode === "compact",
+                appState.stylesPanelMode === "compact" ||
+                  appState.stylesPanelMode === "mobile",
                 !!appState.editingTextElement,
                 data?.onPreventClose,
               );
@@ -1413,7 +1417,8 @@ export const actionChangeVerticalAlign = register({
             onChange={(value) => {
               withCaretPositionPreservation(
                 () => updateData(value),
-                appState.stylesPanelMode === "compact",
+                appState.stylesPanelMode === "compact" ||
+                  appState.stylesPanelMode === "mobile",
                 !!appState.editingTextElement,
                 data?.onPreventClose,
               );
@@ -1678,8 +1683,8 @@ export const actionChangeArrowProperties = register({
   PanelComponent: ({ elements, appState, updateData, app, renderAction }) => {
     return (
       <div className="selected-shape-actions">
-        {renderAction("changeArrowType")}
         {renderAction("changeArrowhead")}
+        {renderAction("changeArrowType")}
       </div>
     );
   },

+ 6 - 1
packages/excalidraw/appState.ts

@@ -55,6 +55,10 @@ export const getDefaultAppState = (): Omit<
       fromSelection: false,
       lastActiveTool: null,
     },
+    preferredSelectionTool: {
+      type: "selection",
+      initialized: false,
+    },
     penMode: false,
     penDetected: false,
     errorMessage: null,
@@ -176,6 +180,7 @@ const APP_STATE_STORAGE_CONF = (<
   editingTextElement: { browser: false, export: false, server: false },
   editingGroupId: { browser: true, export: false, server: false },
   activeTool: { browser: true, export: false, server: false },
+  preferredSelectionTool: { browser: true, export: false, server: false },
   penMode: { browser: true, export: false, server: false },
   penDetected: { browser: true, export: false, server: false },
   errorMessage: { browser: false, export: false, server: false },
@@ -248,7 +253,7 @@ const APP_STATE_STORAGE_CONF = (<
   searchMatches: { browser: false, export: false, server: false },
   lockedMultiSelections: { browser: true, export: true, server: true },
   activeLockedId: { browser: false, export: false, server: false },
-  stylesPanelMode: { browser: true, export: false, server: false },
+  stylesPanelMode: { browser: false, export: false, server: false },
 });
 
 const _clearAppStateForStorage = <

+ 34 - 36
packages/excalidraw/components/Actions.scss

@@ -106,15 +106,15 @@
     justify-content: center;
     align-items: center;
     min-height: 2.5rem;
+    pointer-events: auto;
 
     --default-button-size: 2rem;
 
     .compact-action-button {
-      width: 2rem;
-      height: 2rem;
+      width: var(--mobile-action-button-size);
+      height: var(--mobile-action-button-size);
       border: none;
       border-radius: var(--border-radius-lg);
-      background: transparent;
       color: var(--color-on-surface);
       cursor: pointer;
       display: flex;
@@ -122,24 +122,20 @@
       justify-content: center;
       transition: all 0.2s ease;
 
+      background: var(--mobile-action-button-bg);
+
       svg {
         width: 1rem;
         height: 1rem;
         flex: 0 0 auto;
       }
 
-      &:hover {
-        background: var(--button-hover-bg, var(--island-bg-color));
-        border-color: var(
-          --button-hover-border,
-          var(--button-border, var(--default-border-color))
+      &.active {
+        background: var(
+          --color-surface-primary-container,
+          var(--mobile-action-button-bg)
         );
       }
-
-      &:active {
-        background: var(--button-active-bg, var(--island-bg-color));
-        border-color: var(--button-active-border, var(--color-primary-darkest));
-      }
     }
 
     .compact-popover-content {
@@ -167,6 +163,19 @@
       }
     }
   }
+
+  .ToolIcon {
+    .ToolIcon__icon {
+      width: var(--mobile-action-button-size);
+      height: var(--mobile-action-button-size);
+
+      background: var(--mobile-action-button-bg);
+
+      &:hover {
+        background-color: transparent;
+      }
+    }
+  }
 }
 
 .compact-shape-actions-island {
@@ -174,29 +183,18 @@
   overflow-x: hidden;
 }
 
-.compact-popover-content {
-  .popover-section {
-    margin-bottom: 1rem;
-
-    &:last-child {
-      margin-bottom: 0;
-    }
-
-    .popover-section-title {
-      font-size: 0.75rem;
-      font-weight: 600;
-      color: var(--color-text-secondary);
-      margin-bottom: 0.5rem;
-      text-transform: uppercase;
-      letter-spacing: 0.5px;
-    }
-
-    .buttonList {
-      display: flex;
-      flex-wrap: wrap;
-      gap: 0.25rem;
-    }
-  }
+.mobile-shape-actions {
+  z-index: 999;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  width: 100%;
+  background: transparent;
+  border-radius: var(--border-radius-lg);
+  box-shadow: none;
+  overflow: none;
+  scrollbar-width: none;
+  -ms-overflow-style: none;
 }
 
 .shape-actions-theme-scope {

File diff suppressed because it is too large
+ 661 - 350
packages/excalidraw/components/Actions.tsx


+ 38 - 19
packages/excalidraw/components/App.tsx

@@ -666,14 +666,9 @@ class App extends React.Component<AppProps, AppState> {
   >();
   onRemoveEventListenersEmitter = new Emitter<[]>();
 
-  defaultSelectionTool: "selection" | "lasso" = "selection";
-
   constructor(props: AppProps) {
     super(props);
     const defaultAppState = getDefaultAppState();
-    this.defaultSelectionTool = isMobileOrTablet()
-      ? ("lasso" as const)
-      : ("selection" as const);
     const {
       excalidrawAPI,
       viewModeEnabled = false,
@@ -1527,7 +1522,7 @@ class App extends React.Component<AppProps, AppState> {
 
   public render() {
     const selectedElements = this.scene.getSelectedElements(this.state);
-    const { renderTopRightUI, renderCustomStats } = this.props;
+    const { renderTopRightUI, renderTopLeftUI, renderCustomStats } = this.props;
 
     const sceneNonce = this.scene.getSceneNonce();
     const { elementsMap, visibleElements } =
@@ -1613,6 +1608,7 @@ class App extends React.Component<AppProps, AppState> {
                           onPenModeToggle={this.togglePenMode}
                           onHandToolToggle={this.onHandToolToggle}
                           langCode={getLanguage().code}
+                          renderTopLeftUI={renderTopLeftUI}
                           renderTopRightUI={renderTopRightUI}
                           renderCustomStats={renderCustomStats}
                           showExitZenModeBtn={
@@ -1625,7 +1621,7 @@ class App extends React.Component<AppProps, AppState> {
                             !this.state.isLoading &&
                             this.state.showWelcomeScreen &&
                             this.state.activeTool.type ===
-                              this.defaultSelectionTool &&
+                              this.state.preferredSelectionTool.type &&
                             !this.state.zenModeEnabled &&
                             !this.scene.getElementsIncludingDeleted().length
                           }
@@ -2370,6 +2366,14 @@ class App extends React.Component<AppProps, AppState> {
       deleteInvisibleElements: true,
     });
     const activeTool = scene.appState.activeTool;
+
+    if (!scene.appState.preferredSelectionTool.initialized) {
+      scene.appState.preferredSelectionTool = {
+        type: this.device.editor.isMobile ? "lasso" : "selection",
+        initialized: true,
+      };
+    }
+
     scene.appState = {
       ...scene.appState,
       theme: this.props.theme || scene.appState.theme,
@@ -2384,12 +2388,13 @@ class App extends React.Component<AppProps, AppState> {
         activeTool.type === "selection"
           ? {
               ...activeTool,
-              type: this.defaultSelectionTool,
+              type: scene.appState.preferredSelectionTool.type,
             }
           : scene.appState.activeTool,
       isLoading: false,
       toast: this.state.toast,
     };
+
     if (initialData?.scrollToContent) {
       scene.appState = {
         ...scene.appState,
@@ -2490,6 +2495,8 @@ class App extends React.Component<AppProps, AppState> {
         // but not too narrow (> MQ_MAX_WIDTH_MOBILE)
         this.isTabletBreakpoint(editorWidth, editorHeight) && isMobileOrTablet()
           ? "compact"
+          : this.isMobileBreakpoint(editorWidth, editorHeight)
+          ? "mobile"
           : "full",
     });
 
@@ -3289,7 +3296,10 @@ class App extends React.Component<AppProps, AppState> {
 
       await this.insertClipboardContent(data, filesList, isPlainPaste);
 
-      this.setActiveTool({ type: this.defaultSelectionTool }, true);
+      this.setActiveTool(
+        { type: this.state.preferredSelectionTool.type },
+        true,
+      );
       event?.preventDefault();
     },
   );
@@ -3435,7 +3445,7 @@ class App extends React.Component<AppProps, AppState> {
         }
       },
     );
-    this.setActiveTool({ type: this.defaultSelectionTool }, true);
+    this.setActiveTool({ type: this.state.preferredSelectionTool.type }, true);
 
     if (opts.fitToContent) {
       this.scrollToContent(duplicatedElements, {
@@ -3647,7 +3657,7 @@ class App extends React.Component<AppProps, AppState> {
           ...updateActiveTool(
             this.state,
             prevState.activeTool.locked
-              ? { type: this.defaultSelectionTool }
+              ? { type: this.state.preferredSelectionTool.type }
               : prevState.activeTool,
           ),
           locked: !prevState.activeTool.locked,
@@ -3989,7 +3999,12 @@ class App extends React.Component<AppProps, AppState> {
       }
 
       if (appState) {
-        this.setState(appState);
+        this.setState({
+          ...appState,
+          // keep existing stylesPanelMode as it needs to be preserved
+          // or set at startup
+          stylesPanelMode: this.state.stylesPanelMode,
+        } as Pick<AppState, K> | null);
       }
 
       if (elements) {
@@ -4653,7 +4668,7 @@ class App extends React.Component<AppProps, AppState> {
 
       if (event.key === KEYS.K && !event.altKey && !event[KEYS.CTRL_OR_CMD]) {
         if (this.state.activeTool.type === "laser") {
-          this.setActiveTool({ type: this.defaultSelectionTool });
+          this.setActiveTool({ type: this.state.preferredSelectionTool.type });
         } else {
           this.setActiveTool({ type: "laser" });
         }
@@ -5498,7 +5513,7 @@ class App extends React.Component<AppProps, AppState> {
       return;
     }
     // we should only be able to double click when mode is selection
-    if (this.state.activeTool.type !== this.defaultSelectionTool) {
+    if (this.state.activeTool.type !== this.state.preferredSelectionTool.type) {
       return;
     }
 
@@ -6491,6 +6506,10 @@ class App extends React.Component<AppProps, AppState> {
       this.setAppState({ snapLines: [] });
     }
 
+    if (this.state.openPopup) {
+      this.setState({ openPopup: null });
+    }
+
     this.updateGestureOnPointerDown(event);
 
     // if dragging element is freedraw and another pointerdown event occurs
@@ -7695,7 +7714,7 @@ class App extends React.Component<AppProps, AppState> {
     if (!this.state.activeTool.locked) {
       this.setState({
         activeTool: updateActiveTool(this.state, {
-          type: this.defaultSelectionTool,
+          type: this.state.preferredSelectionTool.type,
         }),
       });
     }
@@ -9409,7 +9428,7 @@ class App extends React.Component<AppProps, AppState> {
             this.setState((prevState) => ({
               newElement: null,
               activeTool: updateActiveTool(this.state, {
-                type: this.defaultSelectionTool,
+                type: this.state.preferredSelectionTool.type,
               }),
               selectedElementIds: makeNextSelectedElementIds(
                 {
@@ -10026,7 +10045,7 @@ class App extends React.Component<AppProps, AppState> {
           newElement: null,
           suggestedBindings: [],
           activeTool: updateActiveTool(this.state, {
-            type: this.defaultSelectionTool,
+            type: this.state.preferredSelectionTool.type,
           }),
         });
       } else {
@@ -10256,7 +10275,7 @@ class App extends React.Component<AppProps, AppState> {
         {
           newElement: null,
           activeTool: updateActiveTool(this.state, {
-            type: this.defaultSelectionTool,
+            type: this.state.preferredSelectionTool.type,
           }),
         },
         () => {
@@ -10720,7 +10739,7 @@ class App extends React.Component<AppProps, AppState> {
           event.nativeEvent.pointerType === "pen" &&
           // always allow if user uses a pen secondary button
           event.button !== POINTER_BUTTON.SECONDARY)) &&
-      this.state.activeTool.type !== this.defaultSelectionTool
+      this.state.activeTool.type !== this.state.preferredSelectionTool.type
     ) {
       return;
     }

+ 15 - 0
packages/excalidraw/components/ColorPicker/ColorPicker.scss

@@ -7,6 +7,12 @@
     }
   }
 
+  .color-picker__title {
+    padding: 0 0.5rem;
+    font-size: 0.875rem;
+    text-align: left;
+  }
+
   .color-picker__heading {
     padding: 0 0.5rem;
     font-size: 0.75rem;
@@ -157,6 +163,15 @@
       width: 1.625rem;
       height: 1.625rem;
     }
+
+    &.compact-sizing {
+      width: var(--mobile-action-button-size);
+      height: var(--mobile-action-button-size);
+    }
+
+    &.mobile-border {
+      border: 1px solid var(--mobile-color-border);
+    }
   }
 
   .color-picker__button__hotkey-label {

+ 22 - 22
packages/excalidraw/components/ColorPicker/ColorPicker.tsx

@@ -19,7 +19,7 @@ import { useExcalidrawContainer } from "../App";
 import { ButtonSeparator } from "../ButtonSeparator";
 import { activeEyeDropperAtom } from "../EyeDropper";
 import { PropertiesPopover } from "../PropertiesPopover";
-import { backgroundIcon, slashIcon, strokeIcon } from "../icons";
+import { slashIcon, strokeIcon } from "../icons";
 import {
   saveCaretPosition,
   restoreCaretPosition,
@@ -216,6 +216,11 @@ const ColorPickerPopupContent = ({
           type={type}
           elements={elements}
           updateData={updateData}
+          showTitle={
+            appState.stylesPanelMode === "compact" ||
+            appState.stylesPanelMode === "mobile"
+          }
+          showHotKey={appState.stylesPanelMode !== "mobile"}
         >
           {colorInputJSX}
         </Picker>
@@ -230,7 +235,7 @@ const ColorPickerTrigger = ({
   label,
   color,
   type,
-  compactMode = false,
+  stylesPanelMode,
   mode = "background",
   onToggle,
   editingTextElement,
@@ -238,7 +243,7 @@ const ColorPickerTrigger = ({
   color: string | null;
   label: string;
   type: ColorPickerType;
-  compactMode?: boolean;
+  stylesPanelMode?: AppState["stylesPanelMode"];
   mode?: "background" | "stroke";
   onToggle: () => void;
   editingTextElement?: boolean;
@@ -263,6 +268,9 @@ const ColorPickerTrigger = ({
         "is-transparent": !color || color === "transparent",
         "has-outline":
           !color || !isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD),
+        "compact-sizing":
+          stylesPanelMode === "compact" || stylesPanelMode === "mobile",
+        "mobile-border": stylesPanelMode === "mobile",
       })}
       aria-label={label}
       style={color ? { "--swatch-color": color } : undefined}
@@ -275,20 +283,10 @@ const ColorPickerTrigger = ({
       onClick={handleClick}
     >
       <div className="color-picker__button-outline">{!color && slashIcon}</div>
-      {compactMode && color && (
-        <div className="color-picker__button-background">
-          {mode === "background" ? (
-            <span
-              style={{
-                color:
-                  color && isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD)
-                    ? "#fff"
-                    : "#111",
-              }}
-            >
-              {backgroundIcon}
-            </span>
-          ) : (
+      {(stylesPanelMode === "compact" || stylesPanelMode === "mobile") &&
+        color &&
+        mode === "stroke" && (
+          <div className="color-picker__button-background">
             <span
               style={{
                 color:
@@ -299,9 +297,8 @@ const ColorPickerTrigger = ({
             >
               {strokeIcon}
             </span>
-          )}
-        </div>
-      )}
+          </div>
+        )}
     </Popover.Trigger>
   );
 };
@@ -316,12 +313,15 @@ export const ColorPicker = ({
   topPicks,
   updateData,
   appState,
-  compactMode = false,
 }: ColorPickerProps) => {
   const openRef = useRef(appState.openPopup);
   useEffect(() => {
     openRef.current = appState.openPopup;
   }, [appState.openPopup]);
+  const compactMode =
+    appState.stylesPanelMode === "compact" ||
+    appState.stylesPanelMode === "mobile";
+
   return (
     <div>
       <div
@@ -353,7 +353,7 @@ export const ColorPicker = ({
             color={color}
             label={label}
             type={type}
-            compactMode={compactMode}
+            stylesPanelMode={appState.stylesPanelMode}
             mode={type === "elementStroke" ? "stroke" : "background"}
             editingTextElement={!!appState.editingTextElement}
             onToggle={() => {

+ 21 - 1
packages/excalidraw/components/ColorPicker/Picker.tsx

@@ -37,8 +37,10 @@ interface PickerProps {
   palette: ColorPaletteCustom;
   updateData: (formData?: any) => void;
   children?: React.ReactNode;
+  showTitle?: boolean;
   onEyeDropperToggle: (force?: boolean) => void;
   onEscape: (event: React.KeyboardEvent | KeyboardEvent) => void;
+  showHotKey?: boolean;
 }
 
 export const Picker = React.forwardRef(
@@ -51,11 +53,21 @@ export const Picker = React.forwardRef(
       palette,
       updateData,
       children,
+      showTitle,
       onEyeDropperToggle,
       onEscape,
+      showHotKey = true,
     }: PickerProps,
     ref,
   ) => {
+    const title = showTitle
+      ? type === "elementStroke"
+        ? t("labels.stroke")
+        : type === "elementBackground"
+        ? t("labels.background")
+        : null
+      : null;
+
     const [customColors] = React.useState(() => {
       if (type === "canvasBackground") {
         return [];
@@ -154,6 +166,8 @@ export const Picker = React.forwardRef(
           // to allow focusing by clicking but not by tabbing
           tabIndex={-1}
         >
+          {title && <div className="color-picker__title">{title}</div>}
+
           {!!customColors.length && (
             <div>
               <PickerHeading>
@@ -175,12 +189,18 @@ export const Picker = React.forwardRef(
               palette={palette}
               onChange={onChange}
               activeShade={activeShade}
+              showHotKey={showHotKey}
             />
           </div>
 
           <div>
             <PickerHeading>{t("colorPicker.shades")}</PickerHeading>
-            <ShadeList color={color} onChange={onChange} palette={palette} />
+            <ShadeList
+              color={color}
+              onChange={onChange}
+              palette={palette}
+              showHotKey={showHotKey}
+            />
           </div>
           {children}
         </div>

+ 3 - 1
packages/excalidraw/components/ColorPicker/PickerColorList.tsx

@@ -20,6 +20,7 @@ interface PickerColorListProps {
   color: string | null;
   onChange: (color: string) => void;
   activeShade: number;
+  showHotKey?: boolean;
 }
 
 const PickerColorList = ({
@@ -27,6 +28,7 @@ const PickerColorList = ({
   color,
   onChange,
   activeShade,
+  showHotKey = true,
 }: PickerColorListProps) => {
   const colorObj = getColorNameAndShadeFromColor({
     color,
@@ -82,7 +84,7 @@ const PickerColorList = ({
             key={key}
           >
             <div className="color-picker__button-outline" />
-            <HotkeyLabel color={color} keyLabel={keybinding} />
+            {showHotKey && <HotkeyLabel color={color} keyLabel={keybinding} />}
           </button>
         );
       })}

+ 10 - 2
packages/excalidraw/components/ColorPicker/ShadeList.tsx

@@ -16,9 +16,15 @@ interface ShadeListProps {
   color: string | null;
   onChange: (color: string) => void;
   palette: ColorPaletteCustom;
+  showHotKey?: boolean;
 }
 
-export const ShadeList = ({ color, onChange, palette }: ShadeListProps) => {
+export const ShadeList = ({
+  color,
+  onChange,
+  palette,
+  showHotKey,
+}: ShadeListProps) => {
   const colorObj = getColorNameAndShadeFromColor({
     color: color || "transparent",
     palette,
@@ -67,7 +73,9 @@ export const ShadeList = ({ color, onChange, palette }: ShadeListProps) => {
               }}
             >
               <div className="color-picker__button-outline" />
-              <HotkeyLabel color={color} keyLabel={i + 1} isShade />
+              {showHotKey && (
+                <HotkeyLabel color={color} keyLabel={i + 1} isShade />
+              )}
             </button>
           ))}
         </div>

+ 14 - 0
packages/excalidraw/components/ExcalidrawLogo.scss

@@ -1,5 +1,8 @@
 .excalidraw {
   .ExcalidrawLogo {
+    --logo-icon--mobile: 1rem;
+    --logo-text--mobile: 0.75rem;
+
     --logo-icon--xs: 2rem;
     --logo-text--xs: 1.5rem;
 
@@ -30,6 +33,17 @@
       color: var(--color-logo-text);
     }
 
+    &.is-mobile {
+      .ExcalidrawLogo-icon {
+        height: var(--logo-icon--mobile);
+      }
+
+      .ExcalidrawLogo-text {
+        height: var(--logo-text--mobile);
+        margin-left: 0.5rem;
+      }
+    }
+
     &.is-xs {
       .ExcalidrawLogo-icon {
         height: var(--logo-icon--xs);

+ 1 - 1
packages/excalidraw/components/ExcalidrawLogo.tsx

@@ -41,7 +41,7 @@ const LogoText = () => (
   </svg>
 );
 
-type LogoSize = "xs" | "small" | "normal" | "large" | "custom";
+type LogoSize = "xs" | "small" | "normal" | "large" | "custom" | "mobile";
 
 interface LogoProps {
   size?: LogoSize;

+ 1 - 0
packages/excalidraw/components/FontPicker/FontPicker.tsx

@@ -106,6 +106,7 @@ export const FontPicker = React.memo(
           <FontPickerTrigger
             selectedFontFamily={selectedFontFamily}
             isOpened={isOpened}
+            compactMode={compactMode}
           />
           {isOpened && (
             <FontPickerList

+ 7 - 5
packages/excalidraw/components/FontPicker/FontPickerList.tsx

@@ -338,11 +338,13 @@ export const FontPickerList = React.memo(
         onKeyDown={onKeyDown}
         preventAutoFocusOnTouch={!!app.state.editingTextElement}
       >
-        <QuickSearch
-          ref={inputRef}
-          placeholder={t("quickSearch.placeholder")}
-          onChange={debounce(setSearchTerm, 20)}
-        />
+        {app.state.stylesPanelMode === "full" && (
+          <QuickSearch
+            ref={inputRef}
+            placeholder={t("quickSearch.placeholder")}
+            onChange={debounce(setSearchTerm, 20)}
+          />
+        )}
         <ScrollableList
           className="dropdown-menu fonts manual-hover"
           placeholder={t("fontList.empty")}

+ 13 - 0
packages/excalidraw/components/FontPicker/FontPickerTrigger.tsx

@@ -1,5 +1,7 @@
 import * as Popover from "@radix-ui/react-popover";
 
+import { MOBILE_ACTION_BUTTON_BG } from "@excalidraw/common";
+
 import type { FontFamilyValues } from "@excalidraw/element/types";
 
 import { t } from "../../i18n";
@@ -11,14 +13,24 @@ import { useExcalidrawSetAppState } from "../App";
 interface FontPickerTriggerProps {
   selectedFontFamily: FontFamilyValues | null;
   isOpened?: boolean;
+  compactMode?: boolean;
 }
 
 export const FontPickerTrigger = ({
   selectedFontFamily,
   isOpened = false,
+  compactMode = false,
 }: FontPickerTriggerProps) => {
   const setAppState = useExcalidrawSetAppState();
 
+  const compactStyle = compactMode
+    ? {
+        ...MOBILE_ACTION_BUTTON_BG,
+        width: "2rem",
+        height: "2rem",
+      }
+    : {};
+
   return (
     <Popover.Trigger asChild>
       <div data-openpopup="fontFamily" className="properties-trigger">
@@ -37,6 +49,7 @@ export const FontPickerTrigger = ({
           }}
           style={{
             border: "none",
+            ...compactStyle,
           }}
         />
       </div>

+ 1 - 1
packages/excalidraw/components/HandButton.tsx

@@ -18,7 +18,7 @@ type LockIconProps = {
 export const HandButton = (props: LockIconProps) => {
   return (
     <ToolButton
-      className={clsx("Shape", { fillable: false })}
+      className={clsx("Shape", { fillable: false, active: props.checked })}
       type="radio"
       icon={handIcon}
       name="editor-current-shape"

+ 4 - 6
packages/excalidraw/components/IconPicker.tsx

@@ -152,15 +152,13 @@ function Picker<T>({
     );
   };
 
+  const isMobile = device.editor.isMobile;
+
   return (
     <Popover.Content
-      side={
-        device.editor.isMobile && !device.viewport.isLandscape
-          ? "top"
-          : "bottom"
-      }
+      side={isMobile ? "right" : "bottom"}
       align="start"
-      sideOffset={12}
+      sideOffset={isMobile ? 8 : 12}
       style={{ zIndex: "var(--zIndex-popup)" }}
       onKeyDown={handleKeyDown}
     >

+ 4 - 4
packages/excalidraw/components/LayerUI.tsx

@@ -91,6 +91,7 @@ interface LayerUIProps {
   onPenModeToggle: AppClassProperties["togglePenMode"];
   showExitZenModeBtn: boolean;
   langCode: Language["code"];
+  renderTopLeftUI?: ExcalidrawProps["renderTopLeftUI"];
   renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
   renderCustomStats?: ExcalidrawProps["renderCustomStats"];
   UIOptions: AppProps["UIOptions"];
@@ -149,6 +150,7 @@ const LayerUI = ({
   onHandToolToggle,
   onPenModeToggle,
   showExitZenModeBtn,
+  renderTopLeftUI,
   renderTopRightUI,
   renderCustomStats,
   UIOptions,
@@ -366,7 +368,7 @@ const LayerUI = ({
                             />
 
                             <ShapesSwitcher
-                              appState={appState}
+                              setAppState={setAppState}
                               activeTool={appState.activeTool}
                               UIOptions={UIOptions}
                               app={app}
@@ -582,13 +584,11 @@ const LayerUI = ({
           renderJSONExportDialog={renderJSONExportDialog}
           renderImageExportDialog={renderImageExportDialog}
           setAppState={setAppState}
-          onLockToggle={onLockToggle}
           onHandToolToggle={onHandToolToggle}
           onPenModeToggle={onPenModeToggle}
+          renderTopLeftUI={renderTopLeftUI}
           renderTopRightUI={renderTopRightUI}
-          renderCustomStats={renderCustomStats}
           renderSidebars={renderSidebars}
-          device={device}
           renderWelcomeScreen={renderWelcomeScreen}
           UIOptions={UIOptions}
         />

+ 77 - 132
packages/excalidraw/components/MobileMenu.tsx

@@ -1,32 +1,23 @@
 import React from "react";
 
-import { showSelectedShapeActions } from "@excalidraw/element";
-
 import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
 
-import { isHandToolActive } from "../appState";
 import { useTunnels } from "../context/tunnels";
 import { t } from "../i18n";
 import { calculateScrollCenter } from "../scene";
 import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
 
-import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
+import { MobileShapeActions } from "./Actions";
+import { MobileToolBar } from "./MobileToolBar";
 import { FixedSideContainer } from "./FixedSideContainer";
-import { HandButton } from "./HandButton";
-import { HintViewer } from "./HintViewer";
+
 import { Island } from "./Island";
-import { LockButton } from "./LockButton";
-import { PenModeButton } from "./PenModeButton";
-import { Section } from "./Section";
-import Stack from "./Stack";
 
 import type { ActionManager } from "../actions/manager";
 import type {
   AppClassProperties,
   AppProps,
   AppState,
-  Device,
-  ExcalidrawProps,
   UIAppState,
 } from "../types";
 import type { JSX } from "react";
@@ -38,7 +29,6 @@ type MobileMenuProps = {
   renderImageExportDialog: () => React.ReactNode;
   setAppState: React.Component<any, AppState>["setState"];
   elements: readonly NonDeletedExcalidrawElement[];
-  onLockToggle: () => void;
   onHandToolToggle: () => void;
   onPenModeToggle: AppClassProperties["togglePenMode"];
 
@@ -46,9 +36,11 @@ type MobileMenuProps = {
     isMobile: boolean,
     appState: UIAppState,
   ) => JSX.Element | null;
-  renderCustomStats?: ExcalidrawProps["renderCustomStats"];
+  renderTopLeftUI?: (
+    isMobile: boolean,
+    appState: UIAppState,
+  ) => JSX.Element | null;
   renderSidebars: () => JSX.Element | null;
-  device: Device;
   renderWelcomeScreen: boolean;
   UIOptions: AppProps["UIOptions"];
   app: AppClassProperties;
@@ -59,14 +51,10 @@ export const MobileMenu = ({
   elements,
   actionManager,
   setAppState,
-  onLockToggle,
   onHandToolToggle,
-  onPenModeToggle,
-
+  renderTopLeftUI,
   renderTopRightUI,
-  renderCustomStats,
   renderSidebars,
-  device,
   renderWelcomeScreen,
   UIOptions,
   app,
@@ -76,141 +64,98 @@ export const MobileMenu = ({
     MainMenuTunnel,
     DefaultSidebarTriggerTunnel,
   } = useTunnels();
-  const renderToolbar = () => {
-    return (
-      <FixedSideContainer side="top" className="App-top-bar">
-        {renderWelcomeScreen && <WelcomeScreenCenterTunnel.Out />}
-        <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 App-toolbar--mobile">
-                  {heading}
-                  <Stack.Row gap={1}>
-                    <ShapesSwitcher
-                      appState={appState}
-                      activeTool={appState.activeTool}
-                      UIOptions={UIOptions}
-                      app={app}
-                    />
-                  </Stack.Row>
-                </Island>
-                {renderTopRightUI && renderTopRightUI(true, appState)}
-                <div className="mobile-misc-tools-container">
-                  {!appState.viewModeEnabled &&
-                    appState.openDialog?.name !== "elementLinkSelector" && (
-                      <DefaultSidebarTriggerTunnel.Out />
-                    )}
-                  <PenModeButton
-                    checked={appState.penMode}
-                    onChange={() => onPenModeToggle(null)}
-                    title={t("toolBar.penMode")}
-                    isMobile
-                    penDetected={appState.penDetected}
-                  />
-                  <LockButton
-                    checked={appState.activeTool.locked}
-                    onChange={onLockToggle}
-                    title={t("toolBar.lock")}
-                    isMobile
-                  />
-                  <HandButton
-                    checked={isHandToolActive(appState)}
-                    onChange={() => onHandToolToggle()}
-                    title={t("toolBar.hand")}
-                    isMobile
-                  />
-                </div>
-              </Stack.Row>
-            </Stack.Col>
-          )}
-        </Section>
-        <HintViewer
-          appState={appState}
-          isMobile={true}
-          device={device}
-          app={app}
-        />
-      </FixedSideContainer>
+  const renderAppTopBar = () => {
+    const topRightUI = renderTopRightUI?.(true, appState) ?? (
+      <DefaultSidebarTriggerTunnel.Out />
+    );
+
+    const topLeftUI = (
+      <div className="excalidraw-ui-top-left">
+        {renderTopLeftUI?.(true, appState)}
+        <MainMenuTunnel.Out />
+      </div>
     );
-  };
 
-  const renderAppToolbar = () => {
     if (
       appState.viewModeEnabled ||
       appState.openDialog?.name === "elementLinkSelector"
     ) {
-      return (
-        <div className="App-toolbar-content">
-          <MainMenuTunnel.Out />
-        </div>
-      );
+      return <div className="App-toolbar-content">{topLeftUI}</div>;
     }
 
     return (
-      <div className="App-toolbar-content">
-        <MainMenuTunnel.Out />
-        {actionManager.renderAction("toggleEditMenu")}
-        {actionManager.renderAction(
-          appState.multiElement ? "finalize" : "duplicateSelection",
-        )}
-        {actionManager.renderAction("deleteSelectedElements")}
-        <div>
-          {actionManager.renderAction("undo")}
-          {actionManager.renderAction("redo")}
-        </div>
+      <div
+        className="App-toolbar-content"
+        style={{
+          display: "flex",
+          flexDirection: "row",
+          justifyContent: "space-between",
+        }}
+      >
+        {topLeftUI}
+        {topRightUI}
       </div>
     );
   };
 
+  const renderToolbar = () => {
+    return (
+      <MobileToolBar
+        app={app}
+        onHandToolToggle={onHandToolToggle}
+        setAppState={setAppState}
+      />
+    );
+  };
+
   return (
     <>
       {renderSidebars()}
-      {!appState.viewModeEnabled &&
-        appState.openDialog?.name !== "elementLinkSelector" &&
-        renderToolbar()}
+      {/* welcome screen, bottom bar, and top bar all have the same z-index */}
+      {/* ordered in this reverse order so that top bar is on top */}
+      <div className="App-welcome-screen">
+        {renderWelcomeScreen && <WelcomeScreenCenterTunnel.Out />}
+      </div>
+
       <div
         className="App-bottom-bar"
         style={{
-          marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
-          marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
-          marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
+          marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN,
         }}
       >
-        <Island padding={0}>
-          {appState.openMenu === "shape" &&
-          !appState.viewModeEnabled &&
-          appState.openDialog?.name !== "elementLinkSelector" &&
-          showSelectedShapeActions(appState, elements) ? (
-            <Section className="App-mobile-menu" heading="selectedShapeActions">
-              <SelectedShapeActions
-                appState={appState}
-                elementsMap={app.scene.getNonDeletedElementsMap()}
-                renderAction={actionManager.renderAction}
-                app={app}
-              />
-            </Section>
-          ) : null}
-          <footer className="App-toolbar">
-            {renderAppToolbar()}
-            {appState.scrolledOutside &&
-              !appState.openMenu &&
-              !appState.openSidebar && (
-                <button
-                  type="button"
-                  className="scroll-back-to-content"
-                  onClick={() => {
-                    setAppState((appState) => ({
-                      ...calculateScrollCenter(elements, appState),
-                    }));
-                  }}
-                >
-                  {t("buttons.scrollBackToContent")}
-                </button>
-              )}
-          </footer>
+        <MobileShapeActions
+          appState={appState}
+          elementsMap={app.scene.getNonDeletedElementsMap()}
+          renderAction={actionManager.renderAction}
+          app={app}
+          setAppState={setAppState}
+        />
+
+        <Island className="App-toolbar">
+          {!appState.viewModeEnabled &&
+            appState.openDialog?.name !== "elementLinkSelector" &&
+            renderToolbar()}
+          {appState.scrolledOutside &&
+            !appState.openMenu &&
+            !appState.openSidebar && (
+              <button
+                type="button"
+                className="scroll-back-to-content"
+                onClick={() => {
+                  setAppState((appState) => ({
+                    ...calculateScrollCenter(elements, appState),
+                  }));
+                }}
+              >
+                {t("buttons.scrollBackToContent")}
+              </button>
+            )}
         </Island>
       </div>
+
+      <FixedSideContainer side="top" className="App-top-bar">
+        {renderAppTopBar()}
+      </FixedSideContainer>
     </>
   );
 };

+ 78 - 0
packages/excalidraw/components/MobileToolBar.scss

@@ -0,0 +1,78 @@
+@import "open-color/open-color.scss";
+@import "../css/variables.module.scss";
+
+.excalidraw {
+  .mobile-toolbar {
+    display: flex;
+    flex: 1;
+    align-items: center;
+    padding: 0px;
+    gap: 4px;
+    border-radius: var(--space-factor);
+    overflow-x: auto;
+    scrollbar-width: none;
+    -ms-overflow-style: none;
+    justify-content: space-between;
+  }
+
+  .mobile-toolbar::-webkit-scrollbar {
+    display: none;
+  }
+
+  .mobile-toolbar .ToolIcon {
+    min-width: 2rem;
+    min-height: 2rem;
+    border-radius: 4px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+
+    .ToolIcon__icon {
+      width: 2.25rem;
+      height: 2.25rem;
+
+      &:hover {
+        background-color: transparent;
+      }
+    }
+
+    &.active {
+      background: var(
+        --color-surface-primary-container,
+        var(--island-bg-color)
+      );
+      border-color: var(--button-active-border, var(--color-primary-darkest));
+    }
+
+    svg {
+      width: 1rem;
+      height: 1rem;
+    }
+  }
+
+  .mobile-toolbar .App-toolbar__extra-tools-dropdown {
+    min-width: 160px;
+    z-index: var(--zIndex-layerUI);
+  }
+
+  .mobile-toolbar-separator {
+    width: 1px;
+    height: 24px;
+    background: var(--default-border-color);
+    margin: 0 2px;
+    flex-shrink: 0;
+  }
+
+  .mobile-toolbar-undo {
+    display: flex;
+    align-items: center;
+  }
+
+  .mobile-toolbar-undo .ToolIcon {
+    min-width: 32px;
+    min-height: 32px;
+    width: 32px;
+    height: 32px;
+  }
+}

+ 471 - 0
packages/excalidraw/components/MobileToolBar.tsx

@@ -0,0 +1,471 @@
+import { useState, useEffect, useRef } from "react";
+import clsx from "clsx";
+
+import { KEYS, capitalizeString } from "@excalidraw/common";
+
+import { trackEvent } from "../analytics";
+
+import { t } from "../i18n";
+
+import { isHandToolActive } from "../appState";
+
+import { useTunnels } from "../context/tunnels";
+
+import { HandButton } from "./HandButton";
+import { ToolButton } from "./ToolButton";
+import DropdownMenu from "./dropdownMenu/DropdownMenu";
+import { ToolPopover } from "./ToolPopover";
+
+import {
+  SelectionIcon,
+  FreedrawIcon,
+  EraserIcon,
+  RectangleIcon,
+  ArrowIcon,
+  extraToolsIcon,
+  DiamondIcon,
+  EllipseIcon,
+  LineIcon,
+  TextIcon,
+  ImageIcon,
+  frameToolIcon,
+  EmbedIcon,
+  laserPointerToolIcon,
+  LassoIcon,
+  mermaidLogoIcon,
+  MagicIcon,
+} from "./icons";
+
+import "./ToolIcon.scss";
+import "./MobileToolBar.scss";
+
+import type { AppClassProperties, ToolType, UIAppState } from "../types";
+
+const SHAPE_TOOLS = [
+  {
+    type: "rectangle",
+    icon: RectangleIcon,
+    title: capitalizeString(t("toolBar.rectangle")),
+  },
+  {
+    type: "diamond",
+    icon: DiamondIcon,
+    title: capitalizeString(t("toolBar.diamond")),
+  },
+  {
+    type: "ellipse",
+    icon: EllipseIcon,
+    title: capitalizeString(t("toolBar.ellipse")),
+  },
+] as const;
+
+const SELECTION_TOOLS = [
+  {
+    type: "selection",
+    icon: SelectionIcon,
+    title: capitalizeString(t("toolBar.selection")),
+  },
+  {
+    type: "lasso",
+    icon: LassoIcon,
+    title: capitalizeString(t("toolBar.lasso")),
+  },
+] as const;
+
+const LINEAR_ELEMENT_TOOLS = [
+  {
+    type: "arrow",
+    icon: ArrowIcon,
+    title: capitalizeString(t("toolBar.arrow")),
+  },
+  { type: "line", icon: LineIcon, title: capitalizeString(t("toolBar.line")) },
+] as const;
+
+type MobileToolBarProps = {
+  app: AppClassProperties;
+  onHandToolToggle: () => void;
+  setAppState: React.Component<any, UIAppState>["setState"];
+};
+
+export const MobileToolBar = ({
+  app,
+  onHandToolToggle,
+  setAppState,
+}: MobileToolBarProps) => {
+  const activeTool = app.state.activeTool;
+  const [isOtherShapesMenuOpen, setIsOtherShapesMenuOpen] = useState(false);
+  const [lastActiveGenericShape, setLastActiveGenericShape] = useState<
+    "rectangle" | "diamond" | "ellipse"
+  >("rectangle");
+  const [lastActiveLinearElement, setLastActiveLinearElement] = useState<
+    "arrow" | "line"
+  >("arrow");
+
+  const toolbarRef = useRef<HTMLDivElement>(null);
+
+  // keep lastActiveGenericShape in sync with active tool if user switches via other UI
+  useEffect(() => {
+    if (
+      activeTool.type === "rectangle" ||
+      activeTool.type === "diamond" ||
+      activeTool.type === "ellipse"
+    ) {
+      setLastActiveGenericShape(activeTool.type);
+    }
+  }, [activeTool.type]);
+
+  // keep lastActiveLinearElement in sync with active tool if user switches via other UI
+  useEffect(() => {
+    if (activeTool.type === "arrow" || activeTool.type === "line") {
+      setLastActiveLinearElement(activeTool.type);
+    }
+  }, [activeTool.type]);
+
+  const frameToolSelected = activeTool.type === "frame";
+  const laserToolSelected = activeTool.type === "laser";
+  const embeddableToolSelected = activeTool.type === "embeddable";
+
+  const { TTDDialogTriggerTunnel } = useTunnels();
+
+  const handleToolChange = (toolType: string, pointerType?: string) => {
+    if (app.state.activeTool.type !== toolType) {
+      trackEvent("toolbar", toolType, "ui");
+    }
+
+    if (toolType === "selection") {
+      if (app.state.activeTool.type === "selection") {
+        // Toggle selection tool behavior if needed
+      } else {
+        app.setActiveTool({ type: "selection" });
+      }
+    } else {
+      app.setActiveTool({ type: toolType as ToolType });
+    }
+  };
+
+  const toolbarWidth =
+    toolbarRef.current?.getBoundingClientRect()?.width ?? 0 - 8;
+  const WIDTH = 36;
+  const GAP = 4;
+
+  // hand, selection, freedraw, eraser, rectangle, arrow, others
+  const MIN_TOOLS = 7;
+  const MIN_WIDTH = MIN_TOOLS * WIDTH + (MIN_TOOLS - 1) * GAP;
+  const ADDITIONAL_WIDTH = WIDTH + GAP;
+
+  const showTextToolOutside = toolbarWidth >= MIN_WIDTH + 1 * ADDITIONAL_WIDTH;
+  const showImageToolOutside = toolbarWidth >= MIN_WIDTH + 2 * ADDITIONAL_WIDTH;
+  const showFrameToolOutside = toolbarWidth >= MIN_WIDTH + 3 * ADDITIONAL_WIDTH;
+
+  const extraTools = [
+    "text",
+    "frame",
+    "embeddable",
+    "laser",
+    "magicframe",
+  ].filter((tool) => {
+    if (showImageToolOutside && tool === "image") {
+      return false;
+    }
+    if (showFrameToolOutside && tool === "frame") {
+      return false;
+    }
+    return true;
+  });
+  const extraToolSelected = extraTools.includes(activeTool.type);
+  const extraIcon = extraToolSelected
+    ? activeTool.type === "frame"
+      ? frameToolIcon
+      : activeTool.type === "embeddable"
+      ? EmbedIcon
+      : activeTool.type === "laser"
+      ? laserPointerToolIcon
+      : activeTool.type === "text"
+      ? TextIcon
+      : activeTool.type === "magicframe"
+      ? MagicIcon
+      : extraToolsIcon
+    : extraToolsIcon;
+
+  return (
+    <div className="mobile-toolbar" ref={toolbarRef}>
+      {/* Hand Tool */}
+      <HandButton
+        checked={isHandToolActive(app.state)}
+        onChange={onHandToolToggle}
+        title={t("toolBar.hand")}
+        isMobile
+      />
+
+      {/* Selection Tool */}
+      <ToolPopover
+        app={app}
+        options={SELECTION_TOOLS}
+        activeTool={activeTool}
+        defaultOption={app.state.preferredSelectionTool.type}
+        namePrefix="selectionType"
+        title={capitalizeString(t("toolBar.selection"))}
+        data-testid="toolbar-selection"
+        onToolChange={(type: string) => {
+          if (type === "selection" || type === "lasso") {
+            app.setActiveTool({ type });
+            setAppState({
+              preferredSelectionTool: { type, initialized: true },
+            });
+          }
+        }}
+        displayedOption={
+          SELECTION_TOOLS.find(
+            (tool) => tool.type === app.state.preferredSelectionTool.type,
+          ) || SELECTION_TOOLS[0]
+        }
+      />
+
+      {/* Free Draw */}
+      <ToolButton
+        className={clsx({
+          active: activeTool.type === "freedraw",
+        })}
+        type="radio"
+        icon={FreedrawIcon}
+        checked={activeTool.type === "freedraw"}
+        name="editor-current-shape"
+        title={`${capitalizeString(t("toolBar.freedraw"))}`}
+        aria-label={capitalizeString(t("toolBar.freedraw"))}
+        data-testid="toolbar-freedraw"
+        onChange={() => handleToolChange("freedraw")}
+      />
+
+      {/* Eraser */}
+      <ToolButton
+        className={clsx({
+          active: activeTool.type === "eraser",
+        })}
+        type="radio"
+        icon={EraserIcon}
+        checked={activeTool.type === "eraser"}
+        name="editor-current-shape"
+        title={`${capitalizeString(t("toolBar.eraser"))}`}
+        aria-label={capitalizeString(t("toolBar.eraser"))}
+        data-testid="toolbar-eraser"
+        onChange={() => handleToolChange("eraser")}
+      />
+
+      {/* Rectangle */}
+      <ToolPopover
+        app={app}
+        options={SHAPE_TOOLS}
+        activeTool={activeTool}
+        defaultOption={lastActiveGenericShape}
+        namePrefix="shapeType"
+        title={capitalizeString(
+          t(
+            lastActiveGenericShape === "rectangle"
+              ? "toolBar.rectangle"
+              : lastActiveGenericShape === "diamond"
+              ? "toolBar.diamond"
+              : lastActiveGenericShape === "ellipse"
+              ? "toolBar.ellipse"
+              : "toolBar.rectangle",
+          ),
+        )}
+        data-testid="toolbar-rectangle"
+        onToolChange={(type: string) => {
+          if (
+            type === "rectangle" ||
+            type === "diamond" ||
+            type === "ellipse"
+          ) {
+            setLastActiveGenericShape(type);
+            app.setActiveTool({ type });
+          }
+        }}
+        displayedOption={
+          SHAPE_TOOLS.find((tool) => tool.type === lastActiveGenericShape) ||
+          SHAPE_TOOLS[0]
+        }
+      />
+
+      {/* Arrow/Line */}
+      <ToolPopover
+        app={app}
+        options={LINEAR_ELEMENT_TOOLS}
+        activeTool={activeTool}
+        defaultOption={lastActiveLinearElement}
+        namePrefix="linearElementType"
+        title={capitalizeString(
+          t(
+            lastActiveLinearElement === "arrow"
+              ? "toolBar.arrow"
+              : "toolBar.line",
+          ),
+        )}
+        data-testid="toolbar-arrow"
+        fillable={true}
+        onToolChange={(type: string) => {
+          if (type === "arrow" || type === "line") {
+            setLastActiveLinearElement(type);
+            app.setActiveTool({ type });
+          }
+        }}
+        displayedOption={
+          LINEAR_ELEMENT_TOOLS.find(
+            (tool) => tool.type === lastActiveLinearElement,
+          ) || LINEAR_ELEMENT_TOOLS[0]
+        }
+      />
+
+      {/* Text Tool */}
+      {showTextToolOutside && (
+        <ToolButton
+          className={clsx({
+            active: activeTool.type === "text",
+          })}
+          type="radio"
+          icon={TextIcon}
+          checked={activeTool.type === "text"}
+          name="editor-current-shape"
+          title={`${capitalizeString(t("toolBar.text"))}`}
+          aria-label={capitalizeString(t("toolBar.text"))}
+          data-testid="toolbar-text"
+          onChange={() => handleToolChange("text")}
+        />
+      )}
+
+      {/* Image */}
+      {showImageToolOutside && (
+        <ToolButton
+          className={clsx({
+            active: activeTool.type === "image",
+          })}
+          type="radio"
+          icon={ImageIcon}
+          checked={activeTool.type === "image"}
+          name="editor-current-shape"
+          title={`${capitalizeString(t("toolBar.image"))}`}
+          aria-label={capitalizeString(t("toolBar.image"))}
+          data-testid="toolbar-image"
+          onChange={() => handleToolChange("image")}
+        />
+      )}
+
+      {/* Frame Tool */}
+      {showFrameToolOutside && (
+        <ToolButton
+          className={clsx({ active: frameToolSelected })}
+          type="radio"
+          icon={frameToolIcon}
+          checked={frameToolSelected}
+          name="editor-current-shape"
+          title={`${capitalizeString(t("toolBar.frame"))}`}
+          aria-label={capitalizeString(t("toolBar.frame"))}
+          data-testid="toolbar-frame"
+          onChange={() => handleToolChange("frame")}
+        />
+      )}
+
+      {/* Other Shapes */}
+      <DropdownMenu open={isOtherShapesMenuOpen} placement="top">
+        <DropdownMenu.Trigger
+          className={clsx(
+            "App-toolbar__extra-tools-trigger App-toolbar__extra-tools-trigger--mobile",
+            {
+              "App-toolbar__extra-tools-trigger--selected":
+                extraToolSelected || isOtherShapesMenuOpen,
+            },
+          )}
+          onToggle={() => setIsOtherShapesMenuOpen(!isOtherShapesMenuOpen)}
+          title={t("toolBar.extraTools")}
+          style={{
+            width: WIDTH,
+            height: WIDTH,
+            display: "flex",
+            alignItems: "center",
+            justifyContent: "center",
+          }}
+        >
+          {extraIcon}
+        </DropdownMenu.Trigger>
+        <DropdownMenu.Content
+          onClickOutside={() => setIsOtherShapesMenuOpen(false)}
+          onSelect={() => setIsOtherShapesMenuOpen(false)}
+          className="App-toolbar__extra-tools-dropdown"
+        >
+          {!showTextToolOutside && (
+            <DropdownMenu.Item
+              onSelect={() => app.setActiveTool({ type: "text" })}
+              icon={TextIcon}
+              shortcut={KEYS.T.toLocaleUpperCase()}
+              data-testid="toolbar-text"
+              selected={activeTool.type === "text"}
+            >
+              {t("toolBar.text")}
+            </DropdownMenu.Item>
+          )}
+
+          {!showImageToolOutside && (
+            <DropdownMenu.Item
+              onSelect={() => app.setActiveTool({ type: "image" })}
+              icon={ImageIcon}
+              data-testid="toolbar-image"
+              selected={activeTool.type === "image"}
+            >
+              {t("toolBar.image")}
+            </DropdownMenu.Item>
+          )}
+          {!showFrameToolOutside && (
+            <DropdownMenu.Item
+              onSelect={() => app.setActiveTool({ type: "frame" })}
+              icon={frameToolIcon}
+              shortcut={KEYS.F.toLocaleUpperCase()}
+              data-testid="toolbar-frame"
+              selected={frameToolSelected}
+            >
+              {t("toolBar.frame")}
+            </DropdownMenu.Item>
+          )}
+          <DropdownMenu.Item
+            onSelect={() => app.setActiveTool({ type: "embeddable" })}
+            icon={EmbedIcon}
+            data-testid="toolbar-embeddable"
+            selected={embeddableToolSelected}
+          >
+            {t("toolBar.embeddable")}
+          </DropdownMenu.Item>
+          <DropdownMenu.Item
+            onSelect={() => app.setActiveTool({ type: "laser" })}
+            icon={laserPointerToolIcon}
+            data-testid="toolbar-laser"
+            selected={laserToolSelected}
+            shortcut={KEYS.K.toLocaleUpperCase()}
+          >
+            {t("toolBar.laser")}
+          </DropdownMenu.Item>
+          <div style={{ margin: "6px 0", fontSize: 14, fontWeight: 600 }}>
+            Generate
+          </div>
+          {app.props.aiEnabled !== false && <TTDDialogTriggerTunnel.Out />}
+          <DropdownMenu.Item
+            onSelect={() => app.setOpenDialog({ name: "ttd", tab: "mermaid" })}
+            icon={mermaidLogoIcon}
+            data-testid="toolbar-embeddable"
+          >
+            {t("toolBar.mermaidToExcalidraw")}
+          </DropdownMenu.Item>
+          {app.props.aiEnabled !== false && app.plugins.diagramToCode && (
+            <>
+              <DropdownMenu.Item
+                onSelect={() => app.onMagicframeToolSelect()}
+                icon={MagicIcon}
+                data-testid="toolbar-magicframe"
+              >
+                {t("toolBar.magicframe")}
+                <DropdownMenu.Item.Badge>AI</DropdownMenu.Item.Badge>
+              </DropdownMenu.Item>
+            </>
+          )}
+        </DropdownMenu.Content>
+      </DropdownMenu>
+    </div>
+  );
+};

+ 18 - 0
packages/excalidraw/components/ToolPopover.scss

@@ -0,0 +1,18 @@
+@import "../css/variables.module.scss";
+
+.excalidraw {
+  .tool-popover-content {
+    display: flex;
+    flex-direction: row;
+    gap: 0.25rem;
+    border-radius: 0.5rem;
+    background: var(--island-bg-color);
+    box-shadow: var(--shadow-island);
+    padding: 0.5rem;
+    z-index: var(--zIndex-layerUI);
+  }
+
+  &:focus {
+    outline: none;
+  }
+}

+ 120 - 0
packages/excalidraw/components/ToolPopover.tsx

@@ -0,0 +1,120 @@
+import React, { useEffect, useState } from "react";
+import clsx from "clsx";
+
+import { capitalizeString } from "@excalidraw/common";
+
+import * as Popover from "@radix-ui/react-popover";
+
+import { trackEvent } from "../analytics";
+
+import { ToolButton } from "./ToolButton";
+
+import "./ToolPopover.scss";
+
+import type { AppClassProperties } from "../types";
+
+type ToolOption = {
+  type: string;
+  icon: React.ReactNode;
+  title?: string;
+};
+
+type ToolPopoverProps = {
+  app: AppClassProperties;
+  options: readonly ToolOption[];
+  activeTool: { type: string };
+  defaultOption: string;
+  className?: string;
+  namePrefix: string;
+  title: string;
+  "data-testid": string;
+  onToolChange: (type: string) => void;
+  displayedOption: ToolOption;
+  fillable?: boolean;
+};
+
+export const ToolPopover = ({
+  app,
+  options,
+  activeTool,
+  defaultOption,
+  className = "Shape",
+  namePrefix,
+  title,
+  "data-testid": dataTestId,
+  onToolChange,
+  displayedOption,
+  fillable = false,
+}: ToolPopoverProps) => {
+  const [isPopupOpen, setIsPopupOpen] = useState(false);
+  const currentType = activeTool.type;
+  const isActive = displayedOption.type === currentType;
+  const SIDE_OFFSET = 32 / 2 + 10;
+
+  // if currentType is not in options, close popup
+  if (!options.some((o) => o.type === currentType) && isPopupOpen) {
+    setIsPopupOpen(false);
+  }
+
+  // Close popover when user starts interacting with the canvas (pointer down)
+  useEffect(() => {
+    // app.onPointerDownEmitter emits when pointer down happens on canvas area
+    const unsubscribe = app.onPointerDownEmitter.on(() => {
+      setIsPopupOpen(false);
+    });
+    return () => unsubscribe?.();
+  }, [app]);
+
+  return (
+    <Popover.Root open={isPopupOpen}>
+      <Popover.Trigger asChild>
+        <ToolButton
+          className={clsx(className, {
+            fillable,
+            active: options.some((o) => o.type === activeTool.type),
+          })}
+          type="radio"
+          icon={displayedOption.icon}
+          checked={isActive}
+          name="editor-current-shape"
+          title={title}
+          aria-label={title}
+          data-testid={dataTestId}
+          onPointerDown={() => {
+            setIsPopupOpen((v) => !v);
+            onToolChange(defaultOption);
+          }}
+        />
+      </Popover.Trigger>
+
+      <Popover.Content
+        className="tool-popover-content"
+        sideOffset={SIDE_OFFSET}
+      >
+        {options.map(({ type, icon, title }) => (
+          <ToolButton
+            className={clsx(className, {
+              active: currentType === type,
+            })}
+            key={type}
+            type="radio"
+            icon={icon}
+            checked={currentType === type}
+            name={`${namePrefix}-option`}
+            title={title || capitalizeString(type)}
+            keyBindingLabel=""
+            aria-label={title || capitalizeString(type)}
+            data-testid={`toolbar-${type}`}
+            onChange={() => {
+              if (app.state.activeTool.type !== type) {
+                trackEvent("toolbar", type, "ui");
+              }
+              app.setActiveTool({ type: type as any });
+              onToolChange?.(type);
+            }}
+          />
+        ))}
+      </Popover.Content>
+    </Popover.Root>
+  );
+};

+ 4 - 0
packages/excalidraw/components/Toolbar.scss

@@ -44,6 +44,10 @@
         var(--button-active-border, var(--color-primary-darkest)) inset;
     }
 
+    &:hover {
+      background-color: transparent;
+    }
+
     &--selected,
     &--selected:hover {
       background: var(--color-primary-light);

+ 26 - 4
packages/excalidraw/components/dropdownMenu/DropdownMenu.scss

@@ -3,24 +3,46 @@
 .excalidraw {
   .dropdown-menu {
     position: absolute;
-    top: 100%;
+    top: 2.5rem;
     margin-top: 0.5rem;
 
+    &--placement-top {
+      top: auto;
+      bottom: 100%;
+      margin-top: 0;
+      margin-bottom: 0.5rem;
+    }
+
     &--mobile {
-      left: 0;
       width: 100%;
       row-gap: 0.75rem;
 
+      // When main menu is in the top toolbar, position relative to trigger
+      &.main-menu-dropdown {
+        min-width: 232px;
+        max-width: calc(100vw - var(--editor-container-padding) * 2);
+        margin-top: 0;
+        margin-bottom: 0;
+        z-index: var(--zIndex-layerUI);
+
+        @media screen and (orientation: landscape) {
+          max-width: 232px;
+        }
+      }
+
       .dropdown-menu-container {
         padding: 8px 8px;
         box-sizing: border-box;
-        // background-color: var(--island-bg-color);
+        max-height: calc(
+          100svh - var(--editor-container-padding) * 2 - 2.25rem
+        );
         box-shadow: var(--shadow-island);
         border-radius: var(--border-radius-lg);
         position: relative;
         transition: box-shadow 0.5s ease-in-out;
         display: flex;
         flex-direction: column;
+        overflow-y: auto;
 
         &.zen-mode {
           box-shadow: none;
@@ -30,7 +52,7 @@
 
     .dropdown-menu-container {
       background-color: var(--island-bg-color);
-      max-height: calc(100vh - 150px);
+
       overflow-y: auto;
       --gap: 2;
     }

+ 12 - 1
packages/excalidraw/components/dropdownMenu/DropdownMenu.tsx

@@ -17,16 +17,27 @@ import "./DropdownMenu.scss";
 const DropdownMenu = ({
   children,
   open,
+  placement,
 }: {
   children?: React.ReactNode;
   open: boolean;
+  placement?: "top" | "bottom";
 }) => {
   const MenuTriggerComp = getMenuTriggerComponent(children);
   const MenuContentComp = getMenuContentComponent(children);
+
+  // clone the MenuContentComp to pass the placement prop
+  const MenuContentCompWithPlacement =
+    MenuContentComp && React.isValidElement(MenuContentComp)
+      ? React.cloneElement(MenuContentComp as React.ReactElement<any>, {
+          placement,
+        })
+      : MenuContentComp;
+
   return (
     <>
       {MenuTriggerComp}
-      {open && MenuContentComp}
+      {open && MenuContentCompWithPlacement}
     </>
   );
 };

+ 3 - 0
packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx

@@ -17,6 +17,7 @@ const MenuContent = ({
   className = "",
   onSelect,
   style,
+  placement = "bottom",
 }: {
   children?: React.ReactNode;
   onClickOutside?: () => void;
@@ -26,6 +27,7 @@ const MenuContent = ({
    */
   onSelect?: (event: Event) => void;
   style?: React.CSSProperties;
+  placement?: "top" | "bottom";
 }) => {
   const device = useDevice();
   const menuRef = useRef<HTMLDivElement>(null);
@@ -58,6 +60,7 @@ const MenuContent = ({
 
   const classNames = clsx(`dropdown-menu ${className}`, {
     "dropdown-menu--mobile": device.editor.isMobile,
+    "dropdown-menu--placement-top": placement === "top",
   }).trim();
 
   return (

+ 1 - 13
packages/excalidraw/components/icons.tsx

@@ -2319,22 +2319,10 @@ export const adjustmentsIcon = createIcon(
   tablerIconProps,
 );
 
-export const backgroundIcon = createIcon(
-  <g strokeWidth={1}>
-    <path stroke="none" d="M0 0h24v24H0z" fill="none" />
-    <path d="M6 10l4 -4" />
-    <path d="M6 14l8 -8" />
-    <path d="M6 18l12 -12" />
-    <path d="M10 18l8 -8" />
-    <path d="M14 18l4 -4" />
-  </g>,
-  tablerIconProps,
-);
-
 export const strokeIcon = createIcon(
   <g strokeWidth={1}>
     <path stroke="none" d="M0 0h24v24H0z" fill="none" />
-    <rect x="6" y="6" width="12" height="12" fill="none" />
+    <path d="M6 10l4 -4 L6 14l8 -8 L6 18l12 -12 L10 18l8 -8 L14 18l4 -4" />
   </g>,
   tablerIconProps,
 );

+ 2 - 0
packages/excalidraw/components/main-menu/MainMenu.tsx

@@ -53,6 +53,8 @@ const MainMenu = Object.assign(
               onSelect={composeEventHandlers(onSelect, () => {
                 setAppState({ openMenu: null });
               })}
+              placement="bottom"
+              className={device.editor.isMobile ? "main-menu-dropdown" : ""}
             >
               {children}
               {device.editor.isMobile && appState.collaborators.size > 0 && (

+ 1 - 1
packages/excalidraw/components/shapes.tsx

@@ -89,7 +89,7 @@ export const SHAPES = [
 ] as const;
 
 export const getToolbarTools = (app: AppClassProperties) => {
-  return app.defaultSelectionTool === "lasso"
+  return app.state.preferredSelectionTool.type === "lasso"
     ? ([
         {
           value: "lasso",

+ 3 - 7
packages/excalidraw/components/welcome-screen/WelcomeScreen.scss

@@ -252,16 +252,12 @@
     }
   }
 
-  @media (max-height: 599px) {
+  &.excalidraw--mobile {
     .welcome-screen-center {
-      margin-top: 4rem;
-    }
-  }
-  @media (min-height: 600px) and (max-height: 900px) {
-    .welcome-screen-center {
-      margin-top: 8rem;
+      margin-bottom: 2rem;
     }
   }
+
   @media (max-height: 500px), (max-width: 320px) {
     .welcome-screen-center {
       display: none;

+ 28 - 18
packages/excalidraw/css/styles.scss

@@ -44,6 +44,11 @@ body.excalidraw-cursor-resize * {
   height: 100%;
   width: 100%;
 
+  button,
+  label {
+    @include buttonNoHighlight;
+  }
+
   button {
     cursor: pointer;
     user-select: none;
@@ -235,27 +240,32 @@ body.excalidraw-cursor-resize * {
     z-index: var(--zIndex-layerUI);
     display: flex;
     flex-direction: column;
-    align-items: center;
+  }
+
+  .App-welcome-screen {
+    z-index: var(--zIndex-layerUI);
   }
 
   .App-bottom-bar {
     position: absolute;
-    top: 0;
+    // account for margins
+    width: calc(100% - 28px);
+    max-width: 450px;
     bottom: 0;
-    left: 0;
-    right: 0;
+    left: 50%;
+    transform: translateX(-50%);
     --bar-padding: calc(4 * var(--space-factor));
-    z-index: 4;
+    z-index: var(--zIndex-layerUI);
     display: flex;
-    align-items: flex-end;
+    flex-direction: column;
+
     pointer-events: none;
+    justify-content: center;
 
     > .Island {
-      width: 100%;
-      max-width: 100%;
-      min-width: 100%;
       box-sizing: border-box;
       max-height: 100%;
+      padding: 4px;
       display: flex;
       flex-direction: column;
       pointer-events: var(--ui-pointerEvents);
@@ -263,7 +273,8 @@ body.excalidraw-cursor-resize * {
   }
 
   .App-toolbar {
-    width: 100%;
+    display: flex;
+    justify-content: center;
 
     .eraser {
       &.ToolIcon:hover {
@@ -276,16 +287,15 @@ body.excalidraw-cursor-resize * {
     }
   }
 
-  .App-toolbar-content {
+  .excalidraw-ui-top-left {
     display: flex;
     align-items: center;
-    justify-content: space-between;
-    padding: 8px;
+    gap: 0.5rem;
+  }
 
-    .dropdown-menu--mobile {
-      bottom: 55px;
-      top: auto;
-    }
+  .App-toolbar-content {
+    display: flex;
+    flex-direction: column;
   }
 
   .App-mobile-menu {
@@ -506,7 +516,7 @@ body.excalidraw-cursor-resize * {
       display: none;
     }
     .scroll-back-to-content {
-      bottom: calc(80px + var(--sab, 0));
+      bottom: calc(100px + var(--sab, 0));
       z-index: -1;
     }
   }

+ 9 - 0
packages/excalidraw/css/theme.scss

@@ -8,6 +8,8 @@
   --button-gray-1: #{$oc-gray-2};
   --button-gray-2: #{$oc-gray-4};
   --button-gray-3: #{$oc-gray-5};
+  --mobile-action-button-bg: rgba(255, 255, 255, 0.35);
+  --mobile-color-border: var(--default-border-color);
   --button-special-active-bg-color: #{$oc-green-0};
   --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>');
@@ -42,6 +44,11 @@
   --lg-button-size: 2.25rem;
   --lg-icon-size: 1rem;
   --editor-container-padding: 1rem;
+  --mobile-action-button-size: 2rem;
+
+  @include isMobile {
+    --editor-container-padding: 0.75rem;
+  }
 
   @media screen and (min-device-width: 1921px) {
     --lg-button-size: 2.5rem;
@@ -177,6 +184,8 @@
     --button-gray-1: #363636;
     --button-gray-2: #272727;
     --button-gray-3: #222;
+    --mobile-action-button-bg: var(--island-bg-color);
+    --mobile-color-border: rgba(255, 255, 255, 0.85);
     --button-special-active-bg-color: #204624;
     --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>');

+ 16 - 0
packages/excalidraw/css/variables.module.scss

@@ -122,6 +122,17 @@
       color: var(--button-color, var(--color-on-primary-container));
     }
   }
+
+  @include isMobile() {
+    width: var(--mobile-action-button-size, var(--default-button-size));
+    height: var(--mobile-action-button-size, var(--default-button-size));
+  }
+}
+
+@mixin buttonNoHighlight {
+  -webkit-tap-highlight-color: transparent;
+  -webkit-touch-callout: none;
+  user-select: none;
 }
 
 @mixin outlineButtonIconStyles {
@@ -187,4 +198,9 @@
   &:active {
     box-shadow: 0 0 0 1px var(--color-brand-active);
   }
+
+  @include isMobile() {
+    width: var(--mobile-action-button-size, 2rem);
+    height: var(--mobile-action-button-size, 2rem);
+  }
 }

+ 2 - 0
packages/excalidraw/index.tsx

@@ -28,6 +28,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
     excalidrawAPI,
     isCollaborating = false,
     onPointerUpdate,
+    renderTopLeftUI,
     renderTopRightUI,
     langCode = defaultLang.code,
     viewModeEnabled,
@@ -120,6 +121,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
           excalidrawAPI={excalidrawAPI}
           isCollaborating={isCollaborating}
           onPointerUpdate={onPointerUpdate}
+          renderTopLeftUI={renderTopLeftUI}
           renderTopRightUI={renderTopRightUI}
           langCode={langCode}
           viewModeEnabled={viewModeEnabled}

+ 68 - 0
packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap

@@ -956,6 +956,10 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -1151,6 +1155,10 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -1364,6 +1372,10 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -1694,6 +1706,10 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -2024,6 +2040,10 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -2237,6 +2257,10 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -2477,6 +2501,10 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -2774,6 +2802,10 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id3": true,
   },
@@ -3145,6 +3177,10 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -3637,6 +3673,10 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -3959,6 +3999,10 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -4281,6 +4325,10 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id3": true,
   },
@@ -5565,6 +5613,10 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -6781,6 +6833,10 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -7718,6 +7774,10 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -8714,6 +8774,10 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -9707,6 +9771,10 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,

+ 255 - 3
packages/excalidraw/tests/__snapshots__/history.test.tsx.snap

@@ -78,6 +78,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id4": true,
   },
@@ -693,6 +697,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id4": true,
   },
@@ -1181,6 +1189,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -1544,6 +1556,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -1910,6 +1926,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -2169,6 +2189,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -2613,6 +2637,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -2915,6 +2943,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -3233,6 +3265,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -3526,6 +3562,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -3811,6 +3851,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -4045,6 +4089,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -4301,6 +4349,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -4571,6 +4623,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -4799,6 +4855,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -5027,6 +5087,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -5273,6 +5337,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -5528,6 +5596,10 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -5782,6 +5854,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id1": true,
   },
@@ -6101,7 +6177,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
   "offsetTop": 0,
   "openDialog": null,
   "openMenu": null,
-  "openPopup": "elementBackground",
+  "openPopup": null,
   "openSidebar": null,
   "originSnapOffset": null,
   "pasteDialog": {
@@ -6110,6 +6186,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id8": true,
   },
@@ -6536,6 +6616,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id1": true,
   },
@@ -6912,6 +6996,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -7220,6 +7308,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -7535,6 +7627,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -7764,6 +7860,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -8115,6 +8215,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -8466,6 +8570,10 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
     "id3": true,
@@ -8871,6 +8979,10 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -9157,6 +9269,10 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -9420,6 +9536,10 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -9684,6 +9804,10 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -9918,6 +10042,10 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -10211,6 +10339,10 @@ exports[`history > multiplayer undo/redo > should override remotely added points
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -10559,6 +10691,10 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -10797,6 +10933,10 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -11241,6 +11381,10 @@ exports[`history > multiplayer undo/redo > should update history entries after r
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -11500,6 +11644,10 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -11734,6 +11882,10 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -11961,7 +12113,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
   "offsetTop": 0,
   "openDialog": null,
   "openMenu": null,
-  "openPopup": "elementStroke",
+  "openPopup": null,
   "openSidebar": null,
   "originSnapOffset": null,
   "pasteDialog": {
@@ -11970,6 +12122,10 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -12375,6 +12531,10 @@ exports[`history > singleplayer undo/redo > should create new history entry on e
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -12581,6 +12741,10 @@ exports[`history > singleplayer undo/redo > should create new history entry on e
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -12790,6 +12954,10 @@ exports[`history > singleplayer undo/redo > should create new history entry on i
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -13087,6 +13255,10 @@ exports[`history > singleplayer undo/redo > should create new history entry on i
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -13387,6 +13559,10 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": -50,
@@ -13628,6 +13804,10 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -13864,6 +14044,10 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -14100,6 +14284,10 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -14346,6 +14534,10 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -14679,6 +14871,10 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -14845,6 +15041,10 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -15131,6 +15331,10 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -15393,6 +15597,10 @@ exports[`history > singleplayer undo/redo > should not modify anything on unrela
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -15533,7 +15741,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
   "offsetTop": 0,
   "openDialog": null,
   "openMenu": null,
-  "openPopup": "elementBackground",
+  "openPopup": null,
   "openSidebar": null,
   "originSnapOffset": null,
   "pasteDialog": {
@@ -15542,6 +15750,10 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -15826,6 +16038,10 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -15984,6 +16200,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -16688,6 +16908,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -17322,6 +17546,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -17956,6 +18184,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -18674,6 +18906,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -19424,6 +19660,10 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -19903,6 +20143,10 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id1": true,
   },
@@ -20413,6 +20657,10 @@ exports[`history > singleplayer undo/redo > should support element creation, del
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id3": true,
   },
@@ -20871,6 +21119,10 @@ exports[`history > singleplayer undo/redo > should support linear element creati
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },

+ 209 - 1
packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap

@@ -79,6 +79,10 @@ exports[`given element A and group of elements B and given both are selected whe
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
     "id3": true,
@@ -504,6 +508,10 @@ exports[`given element A and group of elements B and given both are selected whe
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
     "id3": true,
@@ -919,6 +927,10 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -1484,6 +1496,10 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -1690,6 +1706,10 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -2073,6 +2093,10 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -2317,6 +2341,10 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -2496,6 +2524,10 @@ exports[`regression tests > can drag element that covers another element, while
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id6": true,
   },
@@ -2820,6 +2852,10 @@ exports[`regression tests > change the properties of a shape > [end of test] app
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -3074,6 +3110,10 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -3314,6 +3354,10 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -3549,6 +3593,10 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id3": true,
   },
@@ -3806,6 +3854,10 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id6": true,
   },
@@ -4119,6 +4171,10 @@ exports[`regression tests > deleting last but one element in editing group shoul
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -4554,6 +4610,10 @@ exports[`regression tests > deselects group of selected elements on pointer down
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
     "id3": true,
@@ -4836,6 +4896,10 @@ exports[`regression tests > deselects group of selected elements on pointer up w
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
     "id3": true,
@@ -5111,6 +5175,10 @@ exports[`regression tests > deselects selected element on pointer down when poin
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -5318,6 +5386,10 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -5517,6 +5589,10 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -5909,6 +5985,10 @@ exports[`regression tests > drags selected elements from point inside common bou
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
     "id3": true,
@@ -6205,6 +6285,10 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -7060,6 +7144,10 @@ exports[`regression tests > given a group of selected elements with an element t
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
     "id6": true,
@@ -7384,7 +7472,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
   "offsetTop": 0,
   "openDialog": null,
   "openMenu": null,
-  "openPopup": "elementBackground",
+  "openPopup": null,
   "openSidebar": null,
   "originSnapOffset": null,
   "pasteDialog": {
@@ -7393,6 +7481,10 @@ exports[`regression tests > given a selected element A and a not selected elemen
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -7671,6 +7763,10 @@ exports[`regression tests > given selected element A with lower z-index than uns
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -7905,6 +8001,10 @@ exports[`regression tests > given selected element A with lower z-index than uns
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -8144,6 +8244,10 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -8323,6 +8427,10 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -8502,6 +8610,10 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -8681,6 +8793,10 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -8910,6 +9026,10 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -9137,6 +9257,10 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -9332,6 +9456,10 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -9561,6 +9689,10 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -9740,6 +9872,10 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -9967,6 +10103,10 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -10146,6 +10286,10 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -10341,6 +10485,10 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -10520,6 +10668,10 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
     "id3": true,
@@ -11050,6 +11202,10 @@ exports[`regression tests > noop interaction after undo shouldn't create history
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -11329,6 +11485,10 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": "-6.25000",
@@ -11451,6 +11611,10 @@ exports[`regression tests > shift click on selected element should deselect it o
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -11650,6 +11814,10 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
     "id3": true,
@@ -11968,6 +12136,10 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
     "id3": true,
@@ -12396,6 +12568,10 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
     "id15": true,
@@ -13038,6 +13214,10 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 60,
@@ -13160,6 +13340,10 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id0": true,
   },
@@ -13790,6 +13974,10 @@ exports[`regression tests > switches from group of selected elements to another
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id3": true,
     "id6": true,
@@ -14128,6 +14316,10 @@ exports[`regression tests > switches selected element on pointer down > [end of
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {
     "id3": true,
   },
@@ -14391,6 +14583,10 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 20,
@@ -14513,6 +14709,10 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -14904,6 +15104,10 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
@@ -15029,6 +15233,10 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": true,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,

+ 10 - 4
packages/excalidraw/types.ts

@@ -316,6 +316,10 @@ export interface AppState {
     // indicates if the current tool is temporarily switched on from the selection tool
     fromSelection: boolean;
   } & ActiveTool;
+  preferredSelectionTool: {
+    type: "selection" | "lasso";
+    initialized: boolean;
+  };
   penMode: boolean;
   penDetected: boolean;
   exportBackground: boolean;
@@ -364,7 +368,6 @@ export interface AppState {
     | { name: "ttd"; tab: "text-to-diagram" | "mermaid" }
     | { name: "commandPalette" }
     | { name: "elementLinkSelector"; sourceElementId: ExcalidrawElement["id"] };
-
   /**
    * Reflects user preference for whether the default sidebar should be docked.
    *
@@ -448,7 +451,7 @@ export interface AppState {
   lockedMultiSelections: { [groupId: string]: true };
 
   /** properties sidebar mode - determines whether to show compact or complete sidebar */
-  stylesPanelMode: "compact" | "full";
+  stylesPanelMode: "compact" | "full" | "mobile";
 }
 
 export type SearchMatch = {
@@ -571,6 +574,10 @@ export interface ExcalidrawProps {
     /** excludes the duplicated elements */
     prevElements: readonly ExcalidrawElement[],
   ) => ExcalidrawElement[] | void;
+  renderTopLeftUI?: (
+    isMobile: boolean,
+    appState: UIAppState,
+  ) => JSX.Element | null;
   renderTopRightUI?: (
     isMobile: boolean,
     appState: UIAppState,
@@ -738,8 +745,7 @@ export type AppClassProperties = {
 
   onPointerUpEmitter: App["onPointerUpEmitter"];
   updateEditorAtom: App["updateEditorAtom"];
-
-  defaultSelectionTool: "selection" | "lasso";
+  onPointerDownEmitter: App["onPointerDownEmitter"];
 };
 
 export type PointerDownState = Readonly<{

+ 4 - 0
packages/utils/tests/__snapshots__/export.test.ts.snap

@@ -80,6 +80,10 @@ exports[`exportToSvg > with default arguments 1`] = `
   },
   "penDetected": false,
   "penMode": false,
+  "preferredSelectionTool": {
+    "initialized": false,
+    "type": "selection",
+  },
   "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,

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