Переглянути джерело

refactor: single source of truths with editor interface (#10178)

* refactor device to editor interface and derive styles panel

* allow host app to control form factor and ui mode

* add editor interface event listener

* put new props inside UIOptions

* refactor: move related apis into one file

* expose getFormFactor

* privatize the setting of desktop mode and fix snapshots

* refactor and fix test

* remove unimplemented code

* export getFormFactor()

* replace `getFormFactor` with `getEditorInterface`

* remove dead & useless

* comment

* fix ts

---------

Co-authored-by: dwelle <[email protected]>
Ryan Di 1 місяць тому
батько
коміт
47cbb5b6fb
56 змінених файлів з 912 додано та 906 видалено
  1. 5 5
      dev-docs/docs/@excalidraw/excalidraw/api/children-components/footer.mdx
  2. 17 9
      dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md
  3. 3 3
      examples/with-script-in-browser/components/MobileFooter.tsx
  4. 4 0
      excalidraw-app/App.tsx
  5. 3 18
      excalidraw-app/tests/MobileMenu.test.tsx
  6. 0 46
      packages/common/src/constants.ts
  7. 223 0
      packages/common/src/editorInterface.ts
  8. 1 0
      packages/common/src/index.ts
  9. 1 1
      packages/common/src/keys.ts
  10. 0 58
      packages/common/src/utils.ts
  11. 14 11
      packages/element/src/resizeTest.ts
  12. 12 13
      packages/element/src/transformHandles.ts
  13. 0 1
      packages/excalidraw/actions/actionCanvas.tsx
  14. 23 18
      packages/excalidraw/actions/actionDeleteSelected.tsx
  15. 25 20
      packages/excalidraw/actions/actionDuplicateSelection.tsx
  16. 2 2
      packages/excalidraw/actions/actionExport.tsx
  17. 8 8
      packages/excalidraw/actions/actionHistory.tsx
  18. 155 144
      packages/excalidraw/actions/actionProperties.tsx
  19. 3 1
      packages/excalidraw/actions/manager.tsx
  20. 0 2
      packages/excalidraw/appState.ts
  21. 16 7
      packages/excalidraw/components/Actions.tsx
  22. 127 147
      packages/excalidraw/components/App.tsx
  23. 3 3
      packages/excalidraw/components/ColorPicker/ColorInput.tsx
  24. 30 36
      packages/excalidraw/components/ColorPicker/ColorPicker.tsx
  25. 3 3
      packages/excalidraw/components/CommandPalette/CommandPalette.tsx
  26. 2 2
      packages/excalidraw/components/Dialog.tsx
  27. 8 2
      packages/excalidraw/components/FontPicker/FontPickerList.tsx
  28. 8 6
      packages/excalidraw/components/HintViewer.tsx
  29. 3 3
      packages/excalidraw/components/IconPicker.tsx
  30. 42 36
      packages/excalidraw/components/LayerUI.tsx
  31. 3 3
      packages/excalidraw/components/LibraryMenuItems.tsx
  32. 2 2
      packages/excalidraw/components/LibraryUnit.tsx
  33. 6 5
      packages/excalidraw/components/PropertiesPopover.tsx
  34. 6 6
      packages/excalidraw/components/Sidebar/Sidebar.tsx
  35. 3 3
      packages/excalidraw/components/Sidebar/SidebarHeader.tsx
  36. 4 3
      packages/excalidraw/components/canvases/InteractiveCanvas.tsx
  37. 5 10
      packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx
  38. 3 3
      packages/excalidraw/components/dropdownMenu/DropdownMenuItemContent.tsx
  39. 3 3
      packages/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.tsx
  40. 3 3
      packages/excalidraw/components/dropdownMenu/DropdownMenuTrigger.tsx
  41. 4 4
      packages/excalidraw/components/hyperlink/Hyperlink.tsx
  42. 5 2
      packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.tsx
  43. 18 13
      packages/excalidraw/components/main-menu/MainMenu.tsx
  44. 3 3
      packages/excalidraw/components/welcome-screen/WelcomeScreen.Center.tsx
  45. 7 4
      packages/excalidraw/hooks/useCreatePortalContainer.ts
  46. 4 6
      packages/excalidraw/index.tsx
  47. 13 6
      packages/excalidraw/renderer/interactiveScene.ts
  48. 2 3
      packages/excalidraw/scene/types.ts
  49. 0 17
      packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap
  50. 0 63
      packages/excalidraw/tests/__snapshots__/history.test.tsx.snap
  51. 52 104
      packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap
  52. 8 2
      packages/excalidraw/tests/regressionTests.test.tsx
  53. 6 10
      packages/excalidraw/tests/test-utils.ts
  54. 9 16
      packages/excalidraw/types.ts
  55. 2 6
      packages/excalidraw/wysiwyg/textWysiwyg.test.tsx
  56. 0 1
      packages/utils/tests/__snapshots__/export.test.ts.snap

+ 5 - 5
dev-docs/docs/@excalidraw/excalidraw/api/children-components/footer.mdx

@@ -9,7 +9,7 @@ You will need to import the `Footer` component from the package and wrap your co
 ```jsx live
 function App() {
   return (
-    <div style={{ height: "500px"}}>
+    <div style={{ height: "500px" }}>
       <Excalidraw>
         <Footer>
           <button
@@ -27,19 +27,19 @@ function App() {
 
 This will only work for `Desktop` devices.
 
-For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
+For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useEditorInterface`](#useEditorInterface) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
 
 Open the `Menu` in the below playground and you will see the `custom footer` rendered.
 
 ```jsx live noInline
 const MobileFooter = ({}) => {
-  const device = useDevice();
-  if (device.editor.isMobile) {
+  const editorInterface = useEditorInterface();
+  if (editorInterface.formFactor === "phone") {
     return (
       <Footer>
         <button
           className="custom-footer"
-          style= {{ marginLeft: '20px', height: '2rem'}}
+          style={{ marginLeft: "20px", height: "2rem" }}
           onClick={() => alert("This is custom footer in mobile menu")}
         >
           custom footer

+ 17 - 9
dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md

@@ -292,7 +292,7 @@ viewportCoordsToSceneCoords(&#123; clientX: number, clientY: number },<br/>&nbsp
   appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a><br/>): &#123;x: number, y: number}
 </pre>
 
-### useDevice
+### useEditorInterface
 
 This hook can be used to check the type of device which is being used. It can only be used inside the `children` of `Excalidraw` component.
 
@@ -300,8 +300,8 @@ Open the `main menu` in the below example to view the footer.
 
 ```jsx live noInline
 const MobileFooter = ({}) => {
-  const device = useDevice();
-  if (device.editor.isMobile) {
+  const editorInterface = useEditorInterface();
+  if (editorInterface.formFactor === "phone") {
     return (
       <Footer>
         <button
@@ -336,12 +336,20 @@ render(<App />);
 The `device` has the following `attributes`, some grouped into `viewport` and `editor` objects, per context.
 
 | Name | Type | Description |
-| --- | --- | --- |
-| `viewport.isMobile` | `boolean` | Set to `true` when viewport is in `mobile` breakpoint |
-| `viewport.isLandscape` | `boolean` | Set to `true` when the viewport is in `landscape` mode |
-| `editor.canFitSidebar` | `boolean` | Set to `true` if there's enough space to fit the `sidebar` |
-| `editor.isMobile` | `boolean` | Set to `true` when editor container is in `mobile` breakpoint |
-| `isTouchScreen` | `boolean` | Set to `true` for `touch` when touch event detected |
+| ---- | ---- | ----------- |
+
+The `EditorInterface` object has the following properties:
+
+| Name | Type | Description |
+| --- | --- | --- | --- | --- | --- |
+| `formFactor` | `'phone' | 'tablet' | 'desktop'` | Indicates the device type based on screen size |
+| `desktopUIMode` | `'compact' | 'full'` | UI mode for desktop form factor |
+| `userAgent.raw` | `string` | Raw user agent string |
+| `userAgent.isMobileDevice` | `boolean` | True if device is mobile |
+| `userAgent.platform` | `'ios' | 'android' | 'other' | 'unknown'` | Device platform |
+| `isTouchScreen` | `boolean` | True if touch events are detected |
+| `canFitSidebar` | `boolean` | True if sidebar can fit in the viewport |
+| `isLandscape` | `boolean` | True if viewport is in landscape mode |
 
 ### i18n
 

+ 3 - 3
examples/with-script-in-browser/components/MobileFooter.tsx

@@ -12,10 +12,10 @@ const MobileFooter = ({
   excalidrawAPI: ExcalidrawImperativeAPI;
   excalidrawLib: typeof TExcalidraw;
 }) => {
-  const { useDevice, Footer } = excalidrawLib;
+  const { useEditorInterface, Footer } = excalidrawLib;
 
-  const device = useDevice();
-  if (device.editor.isMobile) {
+  const editorInterface = useEditorInterface();
+  if (editorInterface.formFactor === "phone") {
     return (
       <Footer>
         <CustomFooter

+ 4 - 0
excalidraw-app/App.tsx

@@ -4,6 +4,7 @@ import {
   TTDDialogTrigger,
   CaptureUpdateAction,
   reconcileElements,
+  useEditorInterface,
 } from "@excalidraw/excalidraw";
 import { trackEvent } from "@excalidraw/excalidraw/analytics";
 import { getDefaultAppState } from "@excalidraw/excalidraw/appState";
@@ -342,6 +343,8 @@ const ExcalidrawWrapper = () => {
 
   const [langCode, setLangCode] = useAppLangCode();
 
+  const editorInterface = useEditorInterface();
+
   // initial state
   // ---------------------------------------------------------------------------
 
@@ -856,6 +859,7 @@ const ExcalidrawWrapper = () => {
                 onSelect={() =>
                   setShareDialogState({ isOpen: true, type: "share" })
                 }
+                editorInterface={editorInterface}
               />
             </div>
           );

+ 3 - 18
excalidraw-app/tests/MobileMenu.test.tsx

@@ -17,30 +17,15 @@ describe("Test MobileMenu", () => {
 
   beforeEach(async () => {
     await render(<ExcalidrawApp />);
-    // @ts-ignore
-    h.app.refreshViewportBreakpoints();
-    // @ts-ignore
-    h.app.refreshEditorBreakpoints();
+    h.app.refreshEditorInterface();
   });
 
   afterAll(() => {
     restoreOriginalGetBoundingClientRect();
   });
 
-  it("should set device correctly", () => {
-    expect(h.app.device).toMatchInlineSnapshot(`
-      {
-        "editor": {
-          "canFitSidebar": false,
-          "isMobile": true,
-        },
-        "isTouchScreen": false,
-        "viewport": {
-          "isLandscape": true,
-          "isMobile": true,
-        },
-      }
-    `);
+  it("should set editor interface correctly", () => {
+    expect(h.app.editorInterface.formFactor).toBe("phone");
   });
 
   it("should initialize with welcome screen and hide once user interacts", async () => {

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

@@ -6,32 +6,6 @@ import type { AppProps, AppState } from "@excalidraw/excalidraw/types";
 
 import { COLOR_PALETTE } from "./colors";
 
-export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
-export const isWindows = /^Win/.test(navigator.platform);
-export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
-export const isFirefox =
-  typeof window !== "undefined" &&
-  "netscape" in window &&
-  navigator.userAgent.indexOf("rv:") > 1 &&
-  navigator.userAgent.indexOf("Gecko") > 1;
-export const isChrome = navigator.userAgent.indexOf("Chrome") !== -1;
-export const isSafari =
-  !isChrome && navigator.userAgent.indexOf("Safari") !== -1;
-export const isIOS =
-  /iPad|iPhone/i.test(navigator.platform) ||
-  // iPadOS 13+
-  (navigator.userAgent.includes("Mac") && "ontouchend" in document);
-// keeping function so it can be mocked in test
-export const isBrave = () =>
-  (navigator as any).brave?.isBrave?.name === "isBrave";
-
-export const isMobile =
-  isIOS ||
-  /android|webos|ipod|blackberry|iemobile|opera mini/i.test(
-    navigator.userAgent,
-  ) ||
-  /android|ios|ipod|blackberry|windows phone/i.test(navigator.platform);
-
 export const supportsResizeObserver =
   typeof window !== "undefined" && "ResizeObserver" in window;
 
@@ -349,26 +323,6 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
   },
 };
 
-// breakpoints
-// -----------------------------------------------------------------------------
-
-// mobile: up to 699px
-export const MQ_MAX_MOBILE = 599;
-
-export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
-export const MQ_MAX_HEIGHT_LANDSCAPE = 500;
-
-// tablets
-export const MQ_MIN_TABLET = MQ_MAX_MOBILE + 1; // lower bound (excludes phones)
-export const MQ_MAX_TABLET = 1400; // upper bound (excludes laptops/desktops)
-
-// desktop/laptop
-export const MQ_MIN_WIDTH_DESKTOP = 1440;
-
-// sidebar
-export const MQ_RIGHT_SIDEBAR_MIN_WIDTH = 1229;
-// -----------------------------------------------------------------------------
-
 export const MAX_DECIMALS_FOR_SVG_EXPORT = 2;
 
 export const EXPORT_SCALES = [1, 2, 3];

+ 223 - 0
packages/common/src/editorInterface.ts

@@ -0,0 +1,223 @@
+export type StylesPanelMode = "compact" | "full" | "mobile";
+
+export type EditorInterface = Readonly<{
+  formFactor: "phone" | "tablet" | "desktop";
+  desktopUIMode: "compact" | "full";
+  userAgent: Readonly<{
+    isMobileDevice: boolean;
+    platform: "ios" | "android" | "other" | "unknown";
+  }>;
+  isTouchScreen: boolean;
+  canFitSidebar: boolean;
+  isLandscape: boolean;
+}>;
+
+// storage key
+const DESKTOP_UI_MODE_STORAGE_KEY = "excalidraw.desktopUIMode";
+
+// breakpoints
+// mobile: up to 699px
+export const MQ_MAX_MOBILE = 599;
+
+export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
+export const MQ_MAX_HEIGHT_LANDSCAPE = 500;
+
+// tablets
+export const MQ_MIN_TABLET = MQ_MAX_MOBILE + 1; // lower bound (excludes phones)
+export const MQ_MAX_TABLET = 1400; // upper bound (excludes laptops/desktops)
+
+// desktop/laptop
+export const MQ_MIN_WIDTH_DESKTOP = 1440;
+
+// sidebar
+export const MQ_RIGHT_SIDEBAR_MIN_WIDTH = 1229;
+
+// -----------------------------------------------------------------------------
+
+// user agent detections
+export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
+export const isWindows = /^Win/.test(navigator.platform);
+export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
+export const isFirefox =
+  typeof window !== "undefined" &&
+  "netscape" in window &&
+  navigator.userAgent.indexOf("rv:") > 1 &&
+  navigator.userAgent.indexOf("Gecko") > 1;
+export const isChrome = navigator.userAgent.indexOf("Chrome") !== -1;
+export const isSafari =
+  !isChrome && navigator.userAgent.indexOf("Safari") !== -1;
+export const isIOS =
+  /iPad|iPhone/i.test(navigator.platform) ||
+  // iPadOS 13+
+  (navigator.userAgent.includes("Mac") && "ontouchend" in document);
+// keeping function so it can be mocked in test
+export const isBrave = () =>
+  (navigator as any).brave?.isBrave?.name === "isBrave";
+
+// export const isMobile =
+//   isIOS ||
+//   /android|webos|ipod|blackberry|iemobile|opera mini/i.test(
+//     navigator.userAgent,
+//   ) ||
+//   /android|ios|ipod|blackberry|windows phone/i.test(navigator.platform);
+
+// utilities
+export const isMobileBreakpoint = (width: number, height: number) => {
+  return (
+    width <= MQ_MAX_MOBILE ||
+    (height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE)
+  );
+};
+
+export const isTabletBreakpoint = (
+  editorWidth: number,
+  editorHeight: number,
+) => {
+  const minSide = Math.min(editorWidth, editorHeight);
+  const maxSide = Math.max(editorWidth, editorHeight);
+
+  return minSide >= MQ_MIN_TABLET && maxSide <= MQ_MAX_TABLET;
+};
+
+const isMobileOrTablet = (): boolean => {
+  const ua = navigator.userAgent || "";
+  const platform = navigator.platform || "";
+  const uaData = (navigator as any).userAgentData as
+    | { mobile?: boolean; platform?: string }
+    | undefined;
+
+  // --- 1) chromium: prefer ua client hints -------------------------------
+  if (uaData) {
+    const plat = (uaData.platform || "").toLowerCase();
+    const isDesktopOS =
+      plat === "windows" ||
+      plat === "macos" ||
+      plat === "linux" ||
+      plat === "chrome os";
+    if (uaData.mobile === true) {
+      return true;
+    }
+    if (uaData.mobile === false && plat === "android") {
+      const looksTouchTablet =
+        matchMedia?.("(hover: none)").matches &&
+        matchMedia?.("(pointer: coarse)").matches;
+      return looksTouchTablet;
+    }
+    if (isDesktopOS) {
+      return false;
+    }
+  }
+
+  // --- 2) ios (includes ipad) --------------------------------------------
+  if (isIOS) {
+    return true;
+  }
+
+  // --- 3) android legacy ua fallback -------------------------------------
+  if (isAndroid) {
+    const isAndroidPhone = /Mobile/i.test(ua);
+    const isAndroidTablet = !isAndroidPhone;
+    if (isAndroidPhone || isAndroidTablet) {
+      const looksTouchTablet =
+        matchMedia?.("(hover: none)").matches &&
+        matchMedia?.("(pointer: coarse)").matches;
+      return looksTouchTablet;
+    }
+  }
+
+  // --- 4) last resort desktop exclusion ----------------------------------
+  const looksDesktopPlatform =
+    /Win|Linux|CrOS|Mac/.test(platform) ||
+    /Windows NT|X11|CrOS|Macintosh/.test(ua);
+  if (looksDesktopPlatform) {
+    return false;
+  }
+  return false;
+};
+
+export const getFormFactor = (
+  editorWidth: number,
+  editorHeight: number,
+): EditorInterface["formFactor"] => {
+  if (isMobileBreakpoint(editorWidth, editorHeight)) {
+    return "phone";
+  }
+
+  if (isTabletBreakpoint(editorWidth, editorHeight)) {
+    return "tablet";
+  }
+
+  return "desktop";
+};
+
+export const deriveStylesPanelMode = (
+  editorInterface: EditorInterface,
+): StylesPanelMode => {
+  if (editorInterface.formFactor === "phone") {
+    return "mobile";
+  }
+
+  if (editorInterface.formFactor === "tablet") {
+    return "compact";
+  }
+
+  return editorInterface.desktopUIMode;
+};
+
+export const createUserAgentDescriptor = (
+  userAgentString: string,
+): EditorInterface["userAgent"] => {
+  const normalizedUA = userAgentString ?? "";
+  let platform: EditorInterface["userAgent"]["platform"] = "unknown";
+
+  if (isIOS) {
+    platform = "ios";
+  } else if (isAndroid) {
+    platform = "android";
+  } else if (normalizedUA) {
+    platform = "other";
+  }
+
+  return {
+    isMobileDevice: isMobileOrTablet(),
+    platform,
+  } as const;
+};
+
+export const loadDesktopUIModePreference = () => {
+  if (typeof window === "undefined") {
+    return null;
+  }
+
+  try {
+    const stored = window.localStorage.getItem(DESKTOP_UI_MODE_STORAGE_KEY);
+    if (stored === "compact" || stored === "full") {
+      return stored as EditorInterface["desktopUIMode"];
+    }
+  } catch (error) {
+    // ignore storage access issues (e.g., Safari private mode)
+  }
+
+  return null;
+};
+
+const persistDesktopUIMode = (mode: EditorInterface["desktopUIMode"]) => {
+  if (typeof window === "undefined") {
+    return;
+  }
+  try {
+    window.localStorage.setItem(DESKTOP_UI_MODE_STORAGE_KEY, mode);
+  } catch (error) {
+    // ignore storage access issues (e.g., Safari private mode)
+  }
+};
+
+export const setDesktopUIMode = (mode: EditorInterface["desktopUIMode"]) => {
+  if (mode !== "compact" && mode !== "full") {
+    return;
+  }
+
+  persistDesktopUIMode(mode);
+
+  return mode;
+};

+ 1 - 0
packages/common/src/index.ts

@@ -10,3 +10,4 @@ export * from "./random";
 export * from "./url";
 export * from "./utils";
 export * from "./emitter";
+export * from "./editorInterface";

+ 1 - 1
packages/common/src/keys.ts

@@ -1,4 +1,4 @@
-import { isDarwin } from "./constants";
+import { isDarwin } from "./editorInterface";
 
 import type { ValueOf } from "./utility-types";
 

+ 0 - 58
packages/common/src/utils.ts

@@ -20,8 +20,6 @@ import {
   ENV,
   FONT_FAMILY,
   getFontFamilyFallbacks,
-  isAndroid,
-  isIOS,
   WINDOWS_EMOJI_FALLBACK_FONT,
 } from "./constants";
 
@@ -1272,59 +1270,3 @@ export const reduceToCommonValue = <T, R = T>(
 
   return commonValue;
 };
-
-export const isMobileOrTablet = (): boolean => {
-  const ua = navigator.userAgent || "";
-  const platform = navigator.platform || "";
-  const uaData = (navigator as any).userAgentData as
-    | { mobile?: boolean; platform?: string }
-    | undefined;
-
-  // --- 1) chromium: prefer ua client hints -------------------------------
-  if (uaData) {
-    const plat = (uaData.platform || "").toLowerCase();
-    const isDesktopOS =
-      plat === "windows" ||
-      plat === "macos" ||
-      plat === "linux" ||
-      plat === "chrome os";
-    if (uaData.mobile === true) {
-      return true;
-    }
-    if (uaData.mobile === false && plat === "android") {
-      const looksTouchTablet =
-        matchMedia?.("(hover: none)").matches &&
-        matchMedia?.("(pointer: coarse)").matches;
-      return looksTouchTablet;
-    }
-    if (isDesktopOS) {
-      return false;
-    }
-  }
-
-  // --- 2) ios (includes ipad) --------------------------------------------
-  if (isIOS) {
-    return true;
-  }
-
-  // --- 3) android legacy ua fallback -------------------------------------
-  if (isAndroid) {
-    const isAndroidPhone = /Mobile/i.test(ua);
-    const isAndroidTablet = !isAndroidPhone;
-    if (isAndroidPhone || isAndroidTablet) {
-      const looksTouchTablet =
-        matchMedia?.("(hover: none)").matches &&
-        matchMedia?.("(pointer: coarse)").matches;
-      return looksTouchTablet;
-    }
-  }
-
-  // --- 4) last resort desktop exclusion ----------------------------------
-  const looksDesktopPlatform =
-    /Win|Linux|CrOS|Mac/.test(platform) ||
-    /Windows NT|X11|CrOS|Macintosh/.test(ua);
-  if (looksDesktopPlatform) {
-    return false;
-  }
-  return false;
-};

+ 14 - 11
packages/element/src/resizeTest.ts

@@ -5,17 +5,20 @@ import {
   type Radians,
 } from "@excalidraw/math";
 
-import { SIDE_RESIZING_THRESHOLD } from "@excalidraw/common";
+import {
+  SIDE_RESIZING_THRESHOLD,
+  type EditorInterface,
+} from "@excalidraw/common";
 
 import type { GlobalPoint, LineSegment, LocalPoint } from "@excalidraw/math";
 
-import type { AppState, Device, Zoom } from "@excalidraw/excalidraw/types";
+import type { AppState, Zoom } from "@excalidraw/excalidraw/types";
 
 import { getElementAbsoluteCoords } from "./bounds";
 import {
   getTransformHandlesFromCoords,
   getTransformHandles,
-  getOmitSidesForDevice,
+  getOmitSidesForEditorInterface,
   canResizeFromSides,
 } from "./transformHandles";
 import { isImageElement, isLinearElement } from "./typeChecks";
@@ -51,7 +54,7 @@ export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
   y: number,
   zoom: Zoom,
   pointerType: PointerType,
-  device: Device,
+  editorInterface: EditorInterface,
 ): MaybeTransformHandleType => {
   if (!appState.selectedElementIds[element.id]) {
     return false;
@@ -63,7 +66,7 @@ export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
       zoom,
       elementsMap,
       pointerType,
-      getOmitSidesForDevice(device),
+      getOmitSidesForEditorInterface(editorInterface),
     );
 
   if (
@@ -86,7 +89,7 @@ export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
     return filter[0] as TransformHandleType;
   }
 
-  if (canResizeFromSides(device)) {
+  if (canResizeFromSides(editorInterface)) {
     const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
       element,
       elementsMap,
@@ -132,7 +135,7 @@ export const getElementWithTransformHandleType = (
   zoom: Zoom,
   pointerType: PointerType,
   elementsMap: ElementsMap,
-  device: Device,
+  editorInterface: EditorInterface,
 ) => {
   return elements.reduce((result, element) => {
     if (result) {
@@ -146,7 +149,7 @@ export const getElementWithTransformHandleType = (
       scenePointerY,
       zoom,
       pointerType,
-      device,
+      editorInterface,
     );
     return transformHandleType ? { element, transformHandleType } : null;
   }, null as { element: NonDeletedExcalidrawElement; transformHandleType: MaybeTransformHandleType } | null);
@@ -160,14 +163,14 @@ export const getTransformHandleTypeFromCoords = <
   scenePointerY: number,
   zoom: Zoom,
   pointerType: PointerType,
-  device: Device,
+  editorInterface: EditorInterface,
 ): MaybeTransformHandleType => {
   const transformHandles = getTransformHandlesFromCoords(
     [x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2],
     0 as Radians,
     zoom,
     pointerType,
-    getOmitSidesForDevice(device),
+    getOmitSidesForEditorInterface(editorInterface),
   );
 
   const found = Object.keys(transformHandles).find((key) => {
@@ -183,7 +186,7 @@ export const getTransformHandleTypeFromCoords = <
     return found as MaybeTransformHandleType;
   }
 
-  if (canResizeFromSides(device)) {
+  if (canResizeFromSides(editorInterface)) {
     const cx = (x1 + x2) / 2;
     const cy = (y1 + y2) / 2;
 

+ 12 - 13
packages/element/src/transformHandles.ts

@@ -1,8 +1,6 @@
 import {
   DEFAULT_TRANSFORM_HANDLE_SPACING,
-  isAndroid,
-  isIOS,
-  isMobileOrTablet,
+  type EditorInterface,
 } from "@excalidraw/common";
 
 import { pointFrom, pointRotateRads } from "@excalidraw/math";
@@ -10,7 +8,6 @@ import { pointFrom, pointRotateRads } from "@excalidraw/math";
 import type { Radians } from "@excalidraw/math";
 
 import type {
-  Device,
   InteractiveCanvasAppState,
   Zoom,
 } from "@excalidraw/excalidraw/types";
@@ -112,20 +109,21 @@ const generateTransformHandle = (
   return [xx - width / 2, yy - height / 2, width, height];
 };
 
-export const canResizeFromSides = (device: Device) => {
-  if (device.viewport.isMobile) {
-    return false;
-  }
-
-  if (device.isTouchScreen && (isAndroid || isIOS)) {
+export const canResizeFromSides = (editorInterface: EditorInterface) => {
+  if (
+    editorInterface.formFactor === "phone" &&
+    editorInterface.userAgent.isMobileDevice
+  ) {
     return false;
   }
 
   return true;
 };
 
-export const getOmitSidesForDevice = (device: Device) => {
-  if (canResizeFromSides(device)) {
+export const getOmitSidesForEditorInterface = (
+  editorInterface: EditorInterface,
+) => {
+  if (canResizeFromSides(editorInterface)) {
     return DEFAULT_OMIT_SIDES;
   }
 
@@ -330,6 +328,7 @@ export const getTransformHandles = (
 export const hasBoundingBox = (
   elements: readonly NonDeletedExcalidrawElement[],
   appState: InteractiveCanvasAppState,
+  editorInterface: EditorInterface,
 ) => {
   if (appState.selectedLinearElement?.isEditing) {
     return false;
@@ -348,5 +347,5 @@ export const hasBoundingBox = (
 
   // on mobile/tablet we currently don't show bbox because of resize issues
   // (also prob best for simplicity's sake)
-  return element.points.length > 2 && !isMobileOrTablet();
+  return element.points.length > 2 && !editorInterface.userAgent.isMobileDevice;
 };

+ 0 - 1
packages/excalidraw/actions/actionCanvas.tsx

@@ -83,7 +83,6 @@ export const actionChangeViewBackgroundColor = register({
         elements={elements}
         appState={appState}
         updateData={updateData}
-        compactMode={appState.stylesPanelMode === "compact"}
       />
     );
   },

+ 23 - 18
packages/excalidraw/actions/actionDeleteSelected.tsx

@@ -30,6 +30,8 @@ import { getSelectedElements, isSomeElementSelected } from "../scene";
 import { TrashIcon } from "../components/icons";
 import { ToolButton } from "../components/ToolButton";
 
+import { useStylesPanelMode } from "..";
+
 import { register } from "./register";
 
 import type { AppClassProperties, AppState } from "../types";
@@ -320,22 +322,25 @@ export const actionDeleteSelected = register({
   keyTest: (event, appState, elements) =>
     (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) &&
     !event[KEYS.CTRL_OR_CMD],
-  PanelComponent: ({ elements, appState, updateData }) => (
-    <ToolButton
-      type="button"
-      icon={TrashIcon}
-      title={t("labels.delete")}
-      aria-label={t("labels.delete")}
-      onClick={() => updateData(null)}
-      disabled={
-        !isSomeElementSelected(getNonDeletedElements(elements), appState)
-      }
-      style={{
-        ...(appState.stylesPanelMode === "mobile" &&
-        appState.openPopup !== "compactOtherProperties"
-          ? MOBILE_ACTION_BUTTON_BG
-          : {}),
-      }}
-    />
-  ),
+  PanelComponent: ({ elements, appState, updateData, app }) => {
+    const isMobile = useStylesPanelMode() === "mobile";
+
+    return (
+      <ToolButton
+        type="button"
+        icon={TrashIcon}
+        title={t("labels.delete")}
+        aria-label={t("labels.delete")}
+        onClick={() => updateData(null)}
+        disabled={
+          !isSomeElementSelected(getNonDeletedElements(elements), appState)
+        }
+        style={{
+          ...(isMobile && appState.openPopup !== "compactOtherProperties"
+            ? MOBILE_ACTION_BUTTON_BG
+            : {}),
+        }}
+      />
+    );
+  },
 });

+ 25 - 20
packages/excalidraw/actions/actionDuplicateSelection.tsx

@@ -27,6 +27,8 @@ import { t } from "../i18n";
 import { isSomeElementSelected } from "../scene";
 import { getShortcutKey } from "../shortcut";
 
+import { useStylesPanelMode } from "..";
+
 import { register } from "./register";
 
 export const actionDuplicateSelection = register({
@@ -107,24 +109,27 @@ export const actionDuplicateSelection = register({
     };
   },
   keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.D,
-  PanelComponent: ({ elements, appState, updateData }) => (
-    <ToolButton
-      type="button"
-      icon={DuplicateIcon}
-      title={`${t("labels.duplicateSelection")} — ${getShortcutKey(
-        "CtrlOrCmd+D",
-      )}`}
-      aria-label={t("labels.duplicateSelection")}
-      onClick={() => updateData(null)}
-      disabled={
-        !isSomeElementSelected(getNonDeletedElements(elements), appState)
-      }
-      style={{
-        ...(appState.stylesPanelMode === "mobile" &&
-        appState.openPopup !== "compactOtherProperties"
-          ? MOBILE_ACTION_BUTTON_BG
-          : {}),
-      }}
-    />
-  ),
+  PanelComponent: ({ elements, appState, updateData, app }) => {
+    const isMobile = useStylesPanelMode() === "mobile";
+
+    return (
+      <ToolButton
+        type="button"
+        icon={DuplicateIcon}
+        title={`${t("labels.duplicateSelection")} — ${getShortcutKey(
+          "CtrlOrCmd+D",
+        )}`}
+        aria-label={t("labels.duplicateSelection")}
+        onClick={() => updateData(null)}
+        disabled={
+          !isSomeElementSelected(getNonDeletedElements(elements), appState)
+        }
+        style={{
+          ...(isMobile && appState.openPopup !== "compactOtherProperties"
+            ? MOBILE_ACTION_BUTTON_BG
+            : {}),
+        }}
+      />
+    );
+  },
 });

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

@@ -11,7 +11,7 @@ import { CaptureUpdateAction } from "@excalidraw/element";
 
 import type { Theme } from "@excalidraw/element/types";
 
-import { useDevice } from "../components/App";
+import { useEditorInterface } from "../components/App";
 import { CheckboxItem } from "../components/CheckboxItem";
 import { DarkModeToggle } from "../components/DarkModeToggle";
 import { ProjectName } from "../components/ProjectName";
@@ -242,7 +242,7 @@ export const actionSaveFileToDisk = register({
       icon={saveAs}
       title={t("buttons.saveAs")}
       aria-label={t("buttons.saveAs")}
-      showAriaLabel={useDevice().editor.isMobile}
+      showAriaLabel={useEditorInterface().formFactor === "phone"}
       hidden={!nativeFileSystemSupported}
       onClick={() => updateData(null)}
       data-testid="save-as-button"

+ 8 - 8
packages/excalidraw/actions/actionHistory.tsx

@@ -18,6 +18,8 @@ import { HistoryChangedEvent } from "../history";
 import { useEmitter } from "../hooks/useEmitter";
 import { t } from "../i18n";
 
+import { useStylesPanelMode } from "..";
+
 import type { History } from "../history";
 import type { AppClassProperties, AppState } from "../types";
 import type { Action, ActionResult } from "./types";
@@ -73,7 +75,7 @@ export const createUndoAction: ActionCreator = (history) => ({
     ),
   keyTest: (event) =>
     event[KEYS.CTRL_OR_CMD] && matchKey(event, KEYS.Z) && !event.shiftKey,
-  PanelComponent: ({ appState, updateData, data }) => {
+  PanelComponent: ({ appState, updateData, data, app }) => {
     const { isUndoStackEmpty } = useEmitter<HistoryChangedEvent>(
       history.onHistoryChangedEmitter,
       new HistoryChangedEvent(
@@ -81,6 +83,7 @@ export const createUndoAction: ActionCreator = (history) => ({
         history.isRedoStackEmpty,
       ),
     );
+    const isMobile = useStylesPanelMode() === "mobile";
 
     return (
       <ToolButton
@@ -92,9 +95,7 @@ export const createUndoAction: ActionCreator = (history) => ({
         disabled={isUndoStackEmpty}
         data-testid="button-undo"
         style={{
-          ...(appState.stylesPanelMode === "mobile"
-            ? MOBILE_ACTION_BUTTON_BG
-            : {}),
+          ...(isMobile ? MOBILE_ACTION_BUTTON_BG : {}),
         }}
       />
     );
@@ -114,7 +115,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: ({ appState, updateData, data }) => {
+  PanelComponent: ({ appState, updateData, data, app }) => {
     const { isRedoStackEmpty } = useEmitter(
       history.onHistoryChangedEmitter,
       new HistoryChangedEvent(
@@ -122,6 +123,7 @@ export const createRedoAction: ActionCreator = (history) => ({
         history.isRedoStackEmpty,
       ),
     );
+    const isMobile = useStylesPanelMode() === "mobile";
 
     return (
       <ToolButton
@@ -133,9 +135,7 @@ export const createRedoAction: ActionCreator = (history) => ({
         disabled={isRedoStackEmpty}
         data-testid="button-redo"
         style={{
-          ...(appState.stylesPanelMode === "mobile"
-            ? MOBILE_ACTION_BUTTON_BG
-            : {}),
+          ...(isMobile ? MOBILE_ACTION_BUTTON_BG : {}),
         }}
       />
     );

+ 155 - 144
packages/excalidraw/actions/actionProperties.tsx

@@ -57,6 +57,8 @@ import {
   toggleLinePolygonState,
 } from "@excalidraw/element";
 
+import { deriveStylesPanelMode } from "@excalidraw/common";
+
 import type { LocalPoint } from "@excalidraw/math";
 
 import type {
@@ -80,9 +82,6 @@ import { RadioSelection } from "../components/RadioSelection";
 import { ColorPicker } from "../components/ColorPicker/ColorPicker";
 import { FontPicker } from "../components/FontPicker/FontPicker";
 import { IconPicker } from "../components/IconPicker";
-// TODO barnabasmolnar/editor-redesign
-// TextAlignTopIcon, TextAlignBottomIcon,TextAlignMiddleIcon,
-// ArrowHead icons
 import { Range } from "../components/Range";
 import {
   ArrowheadArrowIcon,
@@ -149,6 +148,15 @@ import type { AppClassProperties, AppState, Primitive } from "../types";
 
 const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
 
+const getStylesPanelInfo = (app: AppClassProperties) => {
+  const stylesPanelMode = deriveStylesPanelMode(app.editorInterface);
+  return {
+    stylesPanelMode,
+    isCompact: stylesPanelMode !== "full",
+    isMobile: stylesPanelMode === "mobile",
+  } as const;
+};
+
 export const changeProperty = (
   elements: readonly ExcalidrawElement[],
   appState: AppState,
@@ -327,35 +335,35 @@ export const actionChangeStrokeColor = register({
         : CaptureUpdateAction.EVENTUALLY,
     };
   },
-  PanelComponent: ({ elements, appState, updateData, app, data }) => (
-    <>
-      {appState.stylesPanelMode === "full" && (
-        <h3 aria-hidden="true">{t("labels.stroke")}</h3>
-      )}
-      <ColorPicker
-        topPicks={DEFAULT_ELEMENT_STROKE_PICKS}
-        palette={DEFAULT_ELEMENT_STROKE_COLOR_PALETTE}
-        type="elementStroke"
-        label={t("labels.stroke")}
-        color={getFormValue(
-          elements,
-          app,
-          (element) => element.strokeColor,
-          true,
-          (hasSelection) =>
-            !hasSelection ? appState.currentItemStrokeColor : null,
+  PanelComponent: ({ elements, appState, updateData, app, data }) => {
+    const { stylesPanelMode } = getStylesPanelInfo(app);
+
+    return (
+      <>
+        {stylesPanelMode === "full" && (
+          <h3 aria-hidden="true">{t("labels.stroke")}</h3>
         )}
-        onChange={(color) => updateData({ currentItemStrokeColor: color })}
-        elements={elements}
-        appState={appState}
-        updateData={updateData}
-        compactMode={
-          appState.stylesPanelMode === "compact" ||
-          appState.stylesPanelMode === "mobile"
-        }
-      />
-    </>
-  ),
+        <ColorPicker
+          topPicks={DEFAULT_ELEMENT_STROKE_PICKS}
+          palette={DEFAULT_ELEMENT_STROKE_COLOR_PALETTE}
+          type="elementStroke"
+          label={t("labels.stroke")}
+          color={getFormValue(
+            elements,
+            app,
+            (element) => element.strokeColor,
+            true,
+            (hasSelection) =>
+              !hasSelection ? appState.currentItemStrokeColor : null,
+          )}
+          onChange={(color) => updateData({ currentItemStrokeColor: color })}
+          elements={elements}
+          appState={appState}
+          updateData={updateData}
+        />
+      </>
+    );
+  },
 });
 
 export const actionChangeBackgroundColor = register({
@@ -410,35 +418,37 @@ export const actionChangeBackgroundColor = register({
       captureUpdate: CaptureUpdateAction.IMMEDIATELY,
     };
   },
-  PanelComponent: ({ elements, appState, updateData, app, data }) => (
-    <>
-      {appState.stylesPanelMode === "full" && (
-        <h3 aria-hidden="true">{t("labels.background")}</h3>
-      )}
-      <ColorPicker
-        topPicks={DEFAULT_ELEMENT_BACKGROUND_PICKS}
-        palette={DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE}
-        type="elementBackground"
-        label={t("labels.background")}
-        color={getFormValue(
-          elements,
-          app,
-          (element) => element.backgroundColor,
-          true,
-          (hasSelection) =>
-            !hasSelection ? appState.currentItemBackgroundColor : null,
+  PanelComponent: ({ elements, appState, updateData, app, data }) => {
+    const { stylesPanelMode } = getStylesPanelInfo(app);
+
+    return (
+      <>
+        {stylesPanelMode === "full" && (
+          <h3 aria-hidden="true">{t("labels.background")}</h3>
         )}
-        onChange={(color) => updateData({ currentItemBackgroundColor: color })}
-        elements={elements}
-        appState={appState}
-        updateData={updateData}
-        compactMode={
-          appState.stylesPanelMode === "compact" ||
-          appState.stylesPanelMode === "mobile"
-        }
-      />
-    </>
-  ),
+        <ColorPicker
+          topPicks={DEFAULT_ELEMENT_BACKGROUND_PICKS}
+          palette={DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE}
+          type="elementBackground"
+          label={t("labels.background")}
+          color={getFormValue(
+            elements,
+            app,
+            (element) => element.backgroundColor,
+            true,
+            (hasSelection) =>
+              !hasSelection ? appState.currentItemBackgroundColor : null,
+          )}
+          onChange={(color) =>
+            updateData({ currentItemBackgroundColor: color })
+          }
+          elements={elements}
+          appState={appState}
+          updateData={updateData}
+        />
+      </>
+    );
+  },
 });
 
 export const actionChangeFillStyle = register({
@@ -449,7 +459,9 @@ export const actionChangeFillStyle = register({
     trackEvent(
       "element",
       "changeFillStyle",
-      `${value} (${app.device.editor.isMobile ? "mobile" : "desktop"})`,
+      `${value} (${
+        app.editorInterface.formFactor === "phone" ? "mobile" : "desktop"
+      })`,
     );
     return {
       elements: changeProperty(elements, appState, (el) =>
@@ -715,78 +727,81 @@ export const actionChangeFontSize = register({
   perform: (elements, appState, value, app) => {
     return changeFontSize(elements, appState, app, () => value, value);
   },
-  PanelComponent: ({ elements, appState, updateData, app, data }) => (
-    <fieldset>
-      <legend>{t("labels.fontSize")}</legend>
-      <div className="buttonList">
-        <RadioSelection
-          group="font-size"
-          options={[
-            {
-              value: 16,
-              text: t("labels.small"),
-              icon: FontSizeSmallIcon,
-              testId: "fontSize-small",
-            },
-            {
-              value: 20,
-              text: t("labels.medium"),
-              icon: FontSizeMediumIcon,
-              testId: "fontSize-medium",
-            },
-            {
-              value: 28,
-              text: t("labels.large"),
-              icon: FontSizeLargeIcon,
-              testId: "fontSize-large",
-            },
-            {
-              value: 36,
-              text: t("labels.veryLarge"),
-              icon: FontSizeExtraLargeIcon,
-              testId: "fontSize-veryLarge",
-            },
-          ]}
-          value={getFormValue(
-            elements,
-            app,
-            (element) => {
-              if (isTextElement(element)) {
-                return element.fontSize;
-              }
-              const boundTextElement = getBoundTextElement(
-                element,
-                app.scene.getNonDeletedElementsMap(),
+  PanelComponent: ({ elements, appState, updateData, app, data }) => {
+    const { isCompact } = getStylesPanelInfo(app);
+
+    return (
+      <fieldset>
+        <legend>{t("labels.fontSize")}</legend>
+        <div className="buttonList">
+          <RadioSelection
+            group="font-size"
+            options={[
+              {
+                value: 16,
+                text: t("labels.small"),
+                icon: FontSizeSmallIcon,
+                testId: "fontSize-small",
+              },
+              {
+                value: 20,
+                text: t("labels.medium"),
+                icon: FontSizeMediumIcon,
+                testId: "fontSize-medium",
+              },
+              {
+                value: 28,
+                text: t("labels.large"),
+                icon: FontSizeLargeIcon,
+                testId: "fontSize-large",
+              },
+              {
+                value: 36,
+                text: t("labels.veryLarge"),
+                icon: FontSizeExtraLargeIcon,
+                testId: "fontSize-veryLarge",
+              },
+            ]}
+            value={getFormValue(
+              elements,
+              app,
+              (element) => {
+                if (isTextElement(element)) {
+                  return element.fontSize;
+                }
+                const boundTextElement = getBoundTextElement(
+                  element,
+                  app.scene.getNonDeletedElementsMap(),
+                );
+                if (boundTextElement) {
+                  return boundTextElement.fontSize;
+                }
+                return null;
+              },
+              (element) =>
+                isTextElement(element) ||
+                getBoundTextElement(
+                  element,
+                  app.scene.getNonDeletedElementsMap(),
+                ) !== null,
+              (hasSelection) =>
+                hasSelection
+                  ? null
+                  : appState.currentItemFontSize || DEFAULT_FONT_SIZE,
+            )}
+            onChange={(value) => {
+              withCaretPositionPreservation(
+                () => updateData(value),
+                isCompact,
+                !!appState.editingTextElement,
+                data?.onPreventClose,
               );
-              if (boundTextElement) {
-                return boundTextElement.fontSize;
-              }
-              return null;
-            },
-            (element) =>
-              isTextElement(element) ||
-              getBoundTextElement(
-                element,
-                app.scene.getNonDeletedElementsMap(),
-              ) !== null,
-            (hasSelection) =>
-              hasSelection
-                ? null
-                : appState.currentItemFontSize || DEFAULT_FONT_SIZE,
-          )}
-          onChange={(value) => {
-            withCaretPositionPreservation(
-              () => updateData(value),
-              appState.stylesPanelMode === "compact" ||
-                appState.stylesPanelMode === "mobile",
-              !!appState.editingTextElement,
-              data?.onPreventClose,
-            );
-          }}
-        />
-      </div>
-    </fieldset>
-  ),
+            }}
+          />
+        </div>
+      </fieldset>
+    );
+  },
 });
 
 export const actionDecreaseFontSize = register({
@@ -1048,6 +1063,7 @@ export const actionChangeFontFamily = register({
     // relying on state batching as multiple `FontPicker` handlers could be called in rapid succession and we want to combine them
     const [batchedData, setBatchedData] = useState<ChangeFontFamilyData>({});
     const isUnmounted = useRef(true);
+    const { stylesPanelMode, isCompact } = getStylesPanelInfo(app);
 
     const selectedFontFamily = useMemo(() => {
       const getFontFamily = (
@@ -1120,14 +1136,14 @@ export const actionChangeFontFamily = register({
 
     return (
       <>
-        {appState.stylesPanelMode === "full" && (
+        {stylesPanelMode === "full" && (
           <legend>{t("labels.fontFamily")}</legend>
         )}
         <FontPicker
           isOpened={appState.openPopup === "fontFamily"}
           selectedFontFamily={selectedFontFamily}
           hoveredFontFamily={appState.currentHoveredFontFamily}
-          compactMode={appState.stylesPanelMode !== "full"}
+          compactMode={stylesPanelMode !== "full"}
           onSelect={(fontFamily) => {
             withCaretPositionPreservation(
               () => {
@@ -1139,8 +1155,7 @@ export const actionChangeFontFamily = register({
                 // defensive clear so immediate close won't abuse the cached elements
                 cachedElementsRef.current.clear();
               },
-              appState.stylesPanelMode === "compact" ||
-                appState.stylesPanelMode === "mobile",
+              isCompact,
               !!appState.editingTextElement,
             );
           }}
@@ -1215,11 +1230,7 @@ export const actionChangeFontFamily = register({
               cachedElementsRef.current.clear();
 
               // Refocus text editor when font picker closes if we were editing text
-              if (
-                (appState.stylesPanelMode === "compact" ||
-                  appState.stylesPanelMode === "mobile") &&
-                appState.editingTextElement
-              ) {
+              if (isCompact && appState.editingTextElement) {
                 restoreCaretPosition(null); // Just refocus without saved position
               }
             }
@@ -1266,6 +1277,7 @@ export const actionChangeTextAlign = register({
   },
   PanelComponent: ({ elements, appState, updateData, app, data }) => {
     const elementsMap = app.scene.getNonDeletedElementsMap();
+    const { isCompact } = getStylesPanelInfo(app);
 
     return (
       <fieldset>
@@ -1318,8 +1330,7 @@ export const actionChangeTextAlign = register({
             onChange={(value) => {
               withCaretPositionPreservation(
                 () => updateData(value),
-                appState.stylesPanelMode === "compact" ||
-                  appState.stylesPanelMode === "mobile",
+                isCompact,
                 !!appState.editingTextElement,
                 data?.onPreventClose,
               );
@@ -1366,6 +1377,7 @@ export const actionChangeVerticalAlign = register({
     };
   },
   PanelComponent: ({ elements, appState, updateData, app, data }) => {
+    const { isCompact } = getStylesPanelInfo(app);
     return (
       <fieldset>
         <div className="buttonList">
@@ -1418,8 +1430,7 @@ export const actionChangeVerticalAlign = register({
             onChange={(value) => {
               withCaretPositionPreservation(
                 () => updateData(value),
-                appState.stylesPanelMode === "compact" ||
-                  appState.stylesPanelMode === "mobile",
+                isCompact,
                 !!appState.editingTextElement,
                 data?.onPreventClose,
               );

+ 3 - 1
packages/excalidraw/actions/manager.tsx

@@ -37,7 +37,9 @@ const trackAction = (
           trackEvent(
             action.trackEvent.category,
             action.trackEvent.action || action.name,
-            `${source} (${app.device.editor.isMobile ? "mobile" : "desktop"})`,
+            `${source} (${
+              app.editorInterface.formFactor === "phone" ? "mobile" : "desktop"
+            })`,
           );
         }
       }

+ 0 - 2
packages/excalidraw/appState.ts

@@ -127,7 +127,6 @@ export const getDefaultAppState = (): Omit<
     searchMatches: null,
     lockedMultiSelections: {},
     activeLockedId: null,
-    stylesPanelMode: "full",
   };
 };
 
@@ -253,7 +252,6 @@ 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: false, export: false, server: false },
 });
 
 const _clearAppStateForStorage = <

+ 16 - 7
packages/excalidraw/components/Actions.tsx

@@ -53,7 +53,11 @@ import { getToolbarTools } from "./shapes";
 
 import "./Actions.scss";
 
-import { useDevice, useExcalidrawContainer } from "./App";
+import {
+  useEditorInterface,
+  useStylesPanelMode,
+  useExcalidrawContainer,
+} from "./App";
 import Stack from "./Stack";
 import { ToolButton } from "./ToolButton";
 import { ToolPopover } from "./ToolPopover";
@@ -151,7 +155,7 @@ export const SelectedShapeActions = ({
   const isEditingTextOrNewElement = Boolean(
     appState.editingTextElement || appState.newElement,
   );
-  const device = useDevice();
+  const editorInterface = useEditorInterface();
   const isRTL = document.documentElement.getAttribute("dir") === "rtl";
 
   const showFillIcons =
@@ -292,8 +296,10 @@ export const SelectedShapeActions = ({
         <fieldset>
           <legend>{t("labels.actions")}</legend>
           <div className="buttonList">
-            {!device.editor.isMobile && renderAction("duplicateSelection")}
-            {!device.editor.isMobile && renderAction("deleteSelectedElements")}
+            {editorInterface.formFactor !== "phone" &&
+              renderAction("duplicateSelection")}
+            {editorInterface.formFactor !== "phone" &&
+              renderAction("deleteSelectedElements")}
             {renderAction("group")}
             {renderAction("ungroup")}
             {showLinkIcon && renderAction("hyperlink")}
@@ -1041,6 +1047,9 @@ export const ShapesSwitcher = ({
   UIOptions: AppProps["UIOptions"];
 }) => {
   const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
+  const stylesPanelMode = useStylesPanelMode();
+  const isFullStylesPanel = stylesPanelMode === "full";
+  const isCompactStylesPanel = stylesPanelMode === "compact";
 
   const SELECTION_TOOLS = [
     {
@@ -1058,7 +1067,7 @@ export const ShapesSwitcher = ({
   const frameToolSelected = activeTool.type === "frame";
   const laserToolSelected = activeTool.type === "laser";
   const lassoToolSelected =
-    app.state.stylesPanelMode === "full" &&
+    isFullStylesPanel &&
     activeTool.type === "lasso" &&
     app.state.preferredSelectionTool.type !== "lasso";
 
@@ -1091,7 +1100,7 @@ export const ShapesSwitcher = ({
           // use a ToolPopover for selection/lasso toggle as well
           if (
             (value === "selection" || value === "lasso") &&
-            app.state.stylesPanelMode === "compact"
+            isCompactStylesPanel
           ) {
             return (
               <ToolPopover
@@ -1225,7 +1234,7 @@ export const ShapesSwitcher = ({
           >
             {t("toolBar.laser")}
           </DropdownMenu.Item>
-          {app.state.stylesPanelMode === "full" && (
+          {isFullStylesPanel && (
             <DropdownMenu.Item
               onSelect={() => app.setActiveTool({ type: "lasso" })}
               icon={LassoIcon}

+ 127 - 147
packages/excalidraw/components/App.tsx

@@ -37,7 +37,6 @@ import {
   FRAME_STYLE,
   IMAGE_MIME_TYPES,
   IMAGE_RENDER_TIMEOUT,
-  isBrave,
   LINE_CONFIRM_THRESHOLD,
   MAX_ALLOWED_FILE_BYTES,
   MIME_TYPES,
@@ -55,13 +54,11 @@ import {
   ZOOM_STEP,
   POINTER_EVENTS,
   TOOL_TYPE,
-  isIOS,
   supportsResizeObserver,
   DEFAULT_COLLISION_THRESHOLD,
   DEFAULT_TEXT_ALIGN,
   ARROW_TYPE,
   DEFAULT_REDUCED_GLOBAL_ALPHA,
-  isSafari,
   isLocalLink,
   normalizeLink,
   toValidURL,
@@ -98,12 +95,16 @@ import {
   Emitter,
   MINIMUM_ARROW_SIZE,
   DOUBLE_TAP_POSITION_THRESHOLD,
-  isMobileOrTablet,
-  MQ_MAX_MOBILE,
-  MQ_MIN_TABLET,
-  MQ_MAX_TABLET,
-  MQ_MAX_HEIGHT_LANDSCAPE,
-  MQ_MAX_WIDTH_LANDSCAPE,
+  createUserAgentDescriptor,
+  getFormFactor,
+  deriveStylesPanelMode,
+  isIOS,
+  isBrave,
+  isSafari,
+  type EditorInterface,
+  type StylesPanelMode,
+  loadDesktopUIModePreference,
+  setDesktopUIMode,
 } from "@excalidraw/common";
 
 import {
@@ -460,7 +461,6 @@ import type {
   LibraryItems,
   PointerDownState,
   SceneData,
-  Device,
   FrameNameBoundsCache,
   SidebarName,
   SidebarTabName,
@@ -481,19 +481,20 @@ import type { Action, ActionResult } from "../actions/types";
 const AppContext = React.createContext<AppClassProperties>(null!);
 const AppPropsContext = React.createContext<AppProps>(null!);
 
-const deviceContextInitialValue = {
-  viewport: {
-    isMobile: false,
-    isLandscape: false,
-  },
-  editor: {
-    isMobile: false,
-    canFitSidebar: false,
-  },
+const editorInterfaceContextInitialValue: EditorInterface = {
+  formFactor: "desktop",
+  desktopUIMode: "full",
+  userAgent: createUserAgentDescriptor(
+    typeof navigator !== "undefined" ? navigator.userAgent : "",
+  ),
   isTouchScreen: false,
+  canFitSidebar: false,
+  isLandscape: true,
 };
-const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
-DeviceContext.displayName = "DeviceContext";
+const EditorInterfaceContext = React.createContext<EditorInterface>(
+  editorInterfaceContextInitialValue,
+);
+EditorInterfaceContext.displayName = "EditorInterfaceContext";
 
 export const ExcalidrawContainerContext = React.createContext<{
   container: HTMLDivElement | null;
@@ -529,7 +530,10 @@ ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext";
 
 export const useApp = () => useContext(AppContext);
 export const useAppProps = () => useContext(AppPropsContext);
-export const useDevice = () => useContext<Device>(DeviceContext);
+export const useEditorInterface = () =>
+  useContext<EditorInterface>(EditorInterfaceContext);
+export const useStylesPanelMode = () =>
+  deriveStylesPanelMode(useEditorInterface());
 export const useExcalidrawContainer = () =>
   useContext(ExcalidrawContainerContext);
 export const useExcalidrawElements = () =>
@@ -577,7 +581,10 @@ class App extends React.Component<AppProps, AppState> {
   rc: RoughCanvas;
   unmounted: boolean = false;
   actionManager: ActionManager;
-  device: Device = deviceContextInitialValue;
+  editorInterface: EditorInterface = editorInterfaceContextInitialValue;
+  private stylesPanelMode: StylesPanelMode = deriveStylesPanelMode(
+    editorInterfaceContextInitialValue,
+  );
 
   private excalidrawContainerRef = React.createRef<HTMLDivElement>();
 
@@ -693,6 +700,9 @@ class App extends React.Component<AppProps, AppState> {
       height: window.innerHeight,
     };
 
+    this.refreshEditorInterface();
+    this.stylesPanelMode = deriveStylesPanelMode(this.editorInterface);
+
     this.id = nanoid();
     this.library = new Library(this);
     this.actionManager = new ActionManager(
@@ -739,6 +749,7 @@ class App extends React.Component<AppProps, AppState> {
         setActiveTool: this.setActiveTool,
         setCursor: this.setCursor,
         resetCursor: this.resetCursor,
+        getEditorInterface: () => this.editorInterface,
         updateFrameRendering: this.updateFrameRendering,
         toggleSidebar: this.toggleSidebar,
         onChange: (cb) => this.onChangeEmitter.on(cb),
@@ -1567,7 +1578,7 @@ class App extends React.Component<AppProps, AppState> {
           "excalidraw--view-mode":
             this.state.viewModeEnabled ||
             this.state.openDialog?.name === "elementLinkSelector",
-          "excalidraw--mobile": this.device.editor.isMobile,
+          "excalidraw--mobile": this.editorInterface.formFactor === "phone",
         })}
         style={{
           ["--ui-pointerEvents" as any]: shouldBlockPointerEvents
@@ -1589,7 +1600,7 @@ class App extends React.Component<AppProps, AppState> {
             <ExcalidrawContainerContext.Provider
               value={this.excalidrawContainerValue}
             >
-              <DeviceContext.Provider value={this.device}>
+              <EditorInterfaceContext.Provider value={this.editorInterface}>
                 <ExcalidrawSetAppStateContext.Provider value={this.setAppState}>
                   <ExcalidrawAppStateContext.Provider value={this.state}>
                     <ExcalidrawElementsContext.Provider
@@ -1817,7 +1828,7 @@ class App extends React.Component<AppProps, AppState> {
                           renderScrollbars={
                             this.props.renderScrollbars === true
                           }
-                          device={this.device}
+                          editorInterface={this.editorInterface}
                           renderInteractiveSceneCallback={
                             this.renderInteractiveSceneCallback
                           }
@@ -1853,7 +1864,7 @@ class App extends React.Component<AppProps, AppState> {
                     </ExcalidrawElementsContext.Provider>
                   </ExcalidrawAppStateContext.Provider>
                 </ExcalidrawSetAppStateContext.Provider>
-              </DeviceContext.Provider>
+              </EditorInterfaceContext.Provider>
             </ExcalidrawContainerContext.Provider>
           </AppPropsContext.Provider>
         </AppContext.Provider>
@@ -2370,7 +2381,8 @@ class App extends React.Component<AppProps, AppState> {
 
     if (!scene.appState.preferredSelectionTool.initialized) {
       scene.appState.preferredSelectionTool = {
-        type: this.device.editor.isMobile ? "lasso" : "selection",
+        type:
+          this.editorInterface.formFactor === "phone" ? "lasso" : "selection",
         initialized: true,
       };
     }
@@ -2430,21 +2442,14 @@ class App extends React.Component<AppProps, AppState> {
     }
   };
 
-  private isMobileBreakpoint = (width: number, height: number) => {
+  private getFormFactor = (editorWidth: number, editorHeight: number) => {
     return (
-      width <= MQ_MAX_MOBILE ||
-      (height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE)
+      this.props.UIOptions.formFactor ??
+      getFormFactor(editorWidth, editorHeight)
     );
   };
 
-  private isTabletBreakpoint = (editorWidth: number, editorHeight: number) => {
-    const minSide = Math.min(editorWidth, editorHeight);
-    const maxSide = Math.max(editorWidth, editorHeight);
-
-    return minSide >= MQ_MIN_TABLET && maxSide <= MQ_MAX_TABLET;
-  };
-
-  private refreshViewportBreakpoints = () => {
+  public refreshEditorInterface = () => {
     const container = this.excalidrawContainerRef.current;
     if (!container) {
       return;
@@ -2453,70 +2458,56 @@ class App extends React.Component<AppProps, AppState> {
     const { width: editorWidth, height: editorHeight } =
       container.getBoundingClientRect();
 
-    const prevViewportState = this.device.viewport;
-
-    const nextViewportState = updateObject(prevViewportState, {
+    const storedDesktopUIMode = loadDesktopUIModePreference();
+    const userAgentDescriptor = createUserAgentDescriptor(
+      typeof navigator !== "undefined" ? navigator.userAgent : "",
+    );
+    // allow host app to control formFactor and desktopUIMode via props
+    const sidebarBreakpoint =
+      this.props.UIOptions.dockedSidebarBreakpoint != null
+        ? this.props.UIOptions.dockedSidebarBreakpoint
+        : MQ_RIGHT_SIDEBAR_MIN_WIDTH;
+    const nextEditorInterface = updateObject(this.editorInterface, {
+      desktopUIMode:
+        this.props.UIOptions.desktopUIMode ??
+        storedDesktopUIMode ??
+        this.editorInterface.desktopUIMode,
+      formFactor: this.getFormFactor(editorWidth, editorHeight),
+      userAgent: userAgentDescriptor,
+      canFitSidebar: editorWidth > sidebarBreakpoint,
       isLandscape: editorWidth > editorHeight,
-      isMobile: this.isMobileBreakpoint(editorWidth, editorHeight),
     });
 
-    if (prevViewportState !== nextViewportState) {
-      this.device = { ...this.device, viewport: nextViewportState };
-      return true;
-    }
-    return false;
+    this.editorInterface = nextEditorInterface;
+    this.reconcileStylesPanelMode(nextEditorInterface);
   };
 
-  private refreshEditorBreakpoints = () => {
-    const container = this.excalidrawContainerRef.current;
-    if (!container) {
+  private reconcileStylesPanelMode = (nextEditorInterface: EditorInterface) => {
+    const nextStylesPanelMode = deriveStylesPanelMode(nextEditorInterface);
+    if (nextStylesPanelMode === this.stylesPanelMode) {
       return;
     }
 
-    const { width: editorWidth, height: editorHeight } =
-      container.getBoundingClientRect();
+    const prevStylesPanelMode = this.stylesPanelMode;
+    this.stylesPanelMode = nextStylesPanelMode;
 
-    const sidebarBreakpoint =
-      this.props.UIOptions.dockedSidebarBreakpoint != null
-        ? this.props.UIOptions.dockedSidebarBreakpoint
-        : MQ_RIGHT_SIDEBAR_MIN_WIDTH;
-
-    const prevEditorState = this.device.editor;
+    if (prevStylesPanelMode !== "full" && nextStylesPanelMode === "full") {
+      this.setState((prevState) => ({
+        preferredSelectionTool: {
+          type: "selection",
+          initialized: true,
+        },
+      }));
+    }
+  };
 
-    const nextEditorState = updateObject(prevEditorState, {
-      isMobile: this.isMobileBreakpoint(editorWidth, editorHeight),
-      canFitSidebar: editorWidth > sidebarBreakpoint,
+  /** TO BE USED LATER */
+  private setDesktopUIMode = (mode: EditorInterface["desktopUIMode"]) => {
+    const nextMode = setDesktopUIMode(mode);
+    this.editorInterface = updateObject(this.editorInterface, {
+      desktopUIMode: nextMode,
     });
-
-    const stylesPanelMode =
-      // NOTE: we could also remove the isMobileOrTablet check here and
-      // always switch to compact mode when the editor is narrow (e.g. < MQ_MIN_WIDTH_DESKTOP)
-      // but not too narrow (> MQ_MAX_WIDTH_MOBILE)
-      this.isTabletBreakpoint(editorWidth, editorHeight) && isMobileOrTablet()
-        ? "compact"
-        : this.isMobileBreakpoint(editorWidth, editorHeight)
-        ? "mobile"
-        : "full";
-
-    // also check if we need to update the app state
-    this.setState((prevState) => ({
-      stylesPanelMode,
-      // reset to box selection mode if the UI changes to full
-      // where you'd not be able to change the mode yourself currently
-      preferredSelectionTool:
-        stylesPanelMode === "full"
-          ? {
-              type: "selection",
-              initialized: true,
-            }
-          : prevState.preferredSelectionTool,
-    }));
-
-    if (prevEditorState !== nextEditorState) {
-      this.device = { ...this.device, editor: nextEditorState };
-      return true;
-    }
-    return false;
+    this.reconcileStylesPanelMode(this.editorInterface);
   };
 
   private clearImageShapeCache(filesMap?: BinaryFiles) {
@@ -2588,19 +2579,9 @@ class App extends React.Component<AppProps, AppState> {
       this.focusContainer();
     }
 
-    if (
-      // bounding rects don't work in tests so updating
-      // the state on init would result in making the test enviro run
-      // in mobile breakpoint (0 width/height), making everything fail
-      !isTestEnv()
-    ) {
-      this.refreshViewportBreakpoints();
-      this.refreshEditorBreakpoints();
-    }
-
     if (supportsResizeObserver && this.excalidrawContainerRef.current) {
       this.resizeObserver = new ResizeObserver(() => {
-        this.refreshEditorBreakpoints();
+        this.refreshEditorInterface();
         this.updateDOMRect();
       });
       this.resizeObserver?.observe(this.excalidrawContainerRef.current);
@@ -2654,11 +2635,8 @@ class App extends React.Component<AppProps, AppState> {
     this.scene
       .getElementsIncludingDeleted()
       .forEach((element) => ShapeCache.delete(element));
-    this.refreshViewportBreakpoints();
+    this.refreshEditorInterface();
     this.updateDOMRect();
-    if (!supportsResizeObserver) {
-      this.refreshEditorBreakpoints();
-    }
     this.setState({});
   });
 
@@ -2817,13 +2795,6 @@ class App extends React.Component<AppProps, AppState> {
       this.setState({ showWelcomeScreen: true });
     }
 
-    if (
-      prevProps.UIOptions.dockedSidebarBreakpoint !==
-      this.props.UIOptions.dockedSidebarBreakpoint
-    ) {
-      this.refreshEditorBreakpoints();
-    }
-
     const hasFollowedPersonLeft =
       prevState.userToFollow &&
       !this.state.collaborators.has(prevState.userToFollow.socketId);
@@ -3178,7 +3149,8 @@ class App extends React.Component<AppProps, AppState> {
       this.addElementsFromPasteOrLibrary({
         elements,
         files: data.files || null,
-        position: isMobileOrTablet() ? "center" : "cursor",
+        position:
+          this.editorInterface.formFactor === "desktop" ? "cursor" : "center",
         retainSeed: isPlainPaste,
       });
       return;
@@ -3203,7 +3175,8 @@ class App extends React.Component<AppProps, AppState> {
         this.addElementsFromPasteOrLibrary({
           elements,
           files,
-          position: isMobileOrTablet() ? "center" : "cursor",
+          position:
+            this.editorInterface.formFactor === "desktop" ? "cursor" : "center",
         });
 
         return;
@@ -3429,7 +3402,7 @@ class App extends React.Component<AppProps, AppState> {
         // from library, not when pasting from clipboard. Alas.
         openSidebar:
           this.state.openSidebar &&
-          this.device.editor.canFitSidebar &&
+          this.editorInterface.canFitSidebar &&
           editorJotaiStore.get(isSidebarDockedAtom)
             ? this.state.openSidebar
             : null,
@@ -3627,7 +3600,7 @@ class App extends React.Component<AppProps, AppState> {
       !isPlainPaste &&
       textElements.length > 1 &&
       PLAIN_PASTE_TOAST_SHOWN === false &&
-      !this.device.editor.isMobile
+      this.editorInterface.formFactor !== "phone"
     ) {
       this.setToast({
         message: t("toast.pasteAsSingleElement", {
@@ -3659,7 +3632,9 @@ class App extends React.Component<AppProps, AppState> {
       trackEvent(
         "toolbar",
         "toggleLock",
-        `${source} (${this.device.editor.isMobile ? "mobile" : "desktop"})`,
+        `${source} (${
+          this.editorInterface.formFactor === "phone" ? "mobile" : "desktop"
+        })`,
       );
     }
     this.setState((prevState) => {
@@ -4011,12 +3986,7 @@ class App extends React.Component<AppProps, AppState> {
       }
 
       if (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);
+        this.setState(appState as Pick<AppState, K> | null);
       }
 
       if (elements) {
@@ -4594,7 +4564,9 @@ class App extends React.Component<AppProps, AppState> {
               "toolbar",
               shape,
               `keyboard (${
-                this.device.editor.isMobile ? "mobile" : "desktop"
+                this.editorInterface.formFactor === "phone"
+                  ? "mobile"
+                  : "desktop"
               })`,
             );
           }
@@ -5100,7 +5072,7 @@ class App extends React.Component<AppProps, AppState> {
       // caret (i.e. deselect). There's not much use for always selecting
       // the text on edit anyway (and users can select-all from contextmenu
       // if needed)
-      autoSelect: !this.device.isTouchScreen,
+      autoSelect: !this.editorInterface.isTouchScreen,
     });
     // deselect all other elements when inserting text
     this.deselectElements();
@@ -5263,7 +5235,7 @@ class App extends React.Component<AppProps, AppState> {
     if (
       considerBoundingBox &&
       this.state.selectedElementIds[element.id] &&
-      hasBoundingBox([element], this.state)
+      hasBoundingBox([element], this.state, this.editorInterface)
     ) {
       // if hitting the bounding box, return early
       // but if not, we should check for other cases as well (e.g. frame name)
@@ -5733,7 +5705,7 @@ class App extends React.Component<AppProps, AppState> {
           this.scene.getNonDeletedElementsMap(),
           this.state,
           pointFrom(scenePointer.x, scenePointer.y),
-          this.device.editor.isMobile,
+          this.editorInterface.formFactor === "phone",
         )
       ) {
         return element;
@@ -5768,7 +5740,7 @@ class App extends React.Component<AppProps, AppState> {
       elementsMap,
       this.state,
       pointFrom(lastPointerDownCoords.x, lastPointerDownCoords.y),
-      this.device.editor.isMobile,
+      this.editorInterface.formFactor === "phone",
     );
     const lastPointerUpCoords = viewportCoordsToSceneCoords(
       this.lastPointerUpEvent!,
@@ -5779,7 +5751,7 @@ class App extends React.Component<AppProps, AppState> {
       elementsMap,
       this.state,
       pointFrom(lastPointerUpCoords.x, lastPointerUpCoords.y),
-      this.device.editor.isMobile,
+      this.editorInterface.formFactor === "phone",
     );
     if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
       hideHyperlinkToolip();
@@ -6171,7 +6143,8 @@ class App extends React.Component<AppProps, AppState> {
         // better way of showing them is found
         !(
           isLinearElement(selectedElements[0]) &&
-          (isMobileOrTablet() || selectedElements[0].points.length === 2)
+          (this.editorInterface.userAgent.isMobileDevice ||
+            selectedElements[0].points.length === 2)
         )
       ) {
         const elementWithTransformHandleType =
@@ -6183,7 +6156,7 @@ class App extends React.Component<AppProps, AppState> {
             this.state.zoom,
             event.pointerType,
             this.scene.getNonDeletedElementsMap(),
-            this.device,
+            this.editorInterface,
           );
         if (
           elementWithTransformHandleType &&
@@ -6207,7 +6180,7 @@ class App extends React.Component<AppProps, AppState> {
         scenePointerY,
         this.state.zoom,
         event.pointerType,
-        this.device,
+        this.editorInterface,
       );
       if (transformHandleType) {
         setCursor(
@@ -6593,10 +6566,12 @@ class App extends React.Component<AppProps, AppState> {
     }
 
     if (
-      !this.device.isTouchScreen &&
+      !this.editorInterface.isTouchScreen &&
       ["pen", "touch"].includes(event.pointerType)
     ) {
-      this.device = updateObject(this.device, { isTouchScreen: true });
+      this.editorInterface = updateObject(this.editorInterface, {
+        isTouchScreen: true,
+      });
     }
 
     if (isPanning) {
@@ -6730,12 +6705,13 @@ class App extends React.Component<AppProps, AppState> {
 
         // block dragging after lasso selection on PCs until the next pointer down
         // (on mobile or tablet, we want to allow user to drag immediately)
-        pointerDownState.drag.blockDragging = !isMobileOrTablet();
+        pointerDownState.drag.blockDragging =
+          this.editorInterface.formFactor === "desktop";
       }
 
       // only for mobile or tablet, if we hit an element, select it immediately like normal selection
       if (
-        isMobileOrTablet() &&
+        this.editorInterface.formFactor !== "desktop" &&
         pointerDownState.hit.element &&
         !hitSelectedElement
       ) {
@@ -6919,7 +6895,7 @@ class App extends React.Component<AppProps, AppState> {
     const clicklength =
       event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0);
 
-    if (this.device.editor.isMobile && clicklength < 300) {
+    if (this.editorInterface.formFactor === "phone" && clicklength < 300) {
       const hitElement = this.getElementAtPosition(
         scenePointer.x,
         scenePointer.y,
@@ -6938,7 +6914,7 @@ class App extends React.Component<AppProps, AppState> {
       }
     }
 
-    if (this.device.isTouchScreen) {
+    if (this.editorInterface.isTouchScreen) {
       const hitElement = this.getElementAtPosition(
         scenePointer.x,
         scenePointer.y,
@@ -6968,7 +6944,7 @@ class App extends React.Component<AppProps, AppState> {
       ) {
         this.handleEmbeddableCenterClick(this.hitLinkElement);
       } else {
-        this.redirectToLink(event, this.device.isTouchScreen);
+        this.redirectToLink(event, this.editorInterface.isTouchScreen);
       }
     } else if (this.state.viewModeEnabled) {
       this.setState({
@@ -7293,7 +7269,8 @@ class App extends React.Component<AppProps, AppState> {
         !isElbowArrow(selectedElements[0]) &&
         !(
           isLinearElement(selectedElements[0]) &&
-          (isMobileOrTablet() || selectedElements[0].points.length === 2)
+          (this.editorInterface.userAgent.isMobileDevice ||
+            selectedElements[0].points.length === 2)
         ) &&
         !(
           this.state.selectedLinearElement &&
@@ -7309,7 +7286,7 @@ class App extends React.Component<AppProps, AppState> {
             this.state.zoom,
             event.pointerType,
             this.scene.getNonDeletedElementsMap(),
-            this.device,
+            this.editorInterface,
           );
         if (elementWithTransformHandleType != null) {
           if (
@@ -7338,7 +7315,7 @@ class App extends React.Component<AppProps, AppState> {
           pointerDownState.origin.y,
           this.state.zoom,
           event.pointerType,
-          this.device,
+          this.editorInterface,
         );
       }
       if (pointerDownState.resize.handleType) {
@@ -8540,7 +8517,10 @@ class App extends React.Component<AppProps, AppState> {
         if (
           this.state.activeTool.type === "lasso" &&
           this.lassoTrail.hasCurrentTrail &&
-          !(isMobileOrTablet() && pointerDownState.hit.element) &&
+          !(
+            this.editorInterface.formFactor !== "desktop" &&
+            pointerDownState.hit.element
+          ) &&
           !this.state.activeTool.fromSelection
         ) {
           return;
@@ -9388,7 +9368,7 @@ class App extends React.Component<AppProps, AppState> {
           newElement &&
           !multiElement
         ) {
-          if (this.device.isTouchScreen) {
+          if (this.editorInterface.isTouchScreen) {
             const FIXED_DELTA_X = Math.min(
               (this.state.width * 0.7) / this.state.zoom.value,
               100,
@@ -11206,7 +11186,7 @@ class App extends React.Component<AppProps, AppState> {
     }
 
     const zIndexActions: ContextMenuItems =
-      this.state.stylesPanelMode === "full"
+      this.editorInterface.formFactor === "desktop"
         ? [
             CONTEXT_MENU_SEPARATOR,
             actionSendBackward,

+ 3 - 3
packages/excalidraw/components/ColorPicker/ColorInput.tsx

@@ -6,7 +6,7 @@ import { KEYS } from "@excalidraw/common";
 import { getShortcutKey } from "../..//shortcut";
 import { useAtom } from "../../editor-jotai";
 import { t } from "../../i18n";
-import { useDevice } from "../App";
+import { useEditorInterface } from "../App";
 import { activeEyeDropperAtom } from "../EyeDropper";
 import { eyeDropperIcon } from "../icons";
 
@@ -30,7 +30,7 @@ export const ColorInput = ({
   colorPickerType,
   placeholder,
 }: ColorInputProps) => {
-  const device = useDevice();
+  const editorInterface = useEditorInterface();
   const [innerValue, setInnerValue] = useState(color);
   const [activeSection, setActiveColorPickerSection] = useAtom(
     activeColorPickerSectionAtom,
@@ -99,7 +99,7 @@ export const ColorInput = ({
         placeholder={placeholder}
       />
       {/* TODO reenable on mobile with a better UX */}
-      {!device.editor.isMobile && (
+      {editorInterface.formFactor !== "phone" && (
         <>
           <div
             style={{

+ 30 - 36
packages/excalidraw/components/ColorPicker/ColorPicker.tsx

@@ -15,7 +15,7 @@ import type { ExcalidrawElement } from "@excalidraw/element/types";
 
 import { useAtom } from "../../editor-jotai";
 import { t } from "../../i18n";
-import { useExcalidrawContainer } from "../App";
+import { useExcalidrawContainer, useStylesPanelMode } from "../App";
 import { ButtonSeparator } from "../ButtonSeparator";
 import { activeEyeDropperAtom } from "../EyeDropper";
 import { PropertiesPopover } from "../PropertiesPopover";
@@ -73,7 +73,6 @@ interface ColorPickerProps {
   palette?: ColorPaletteCustom | null;
   topPicks?: ColorTuple;
   updateData: (formData?: any) => void;
-  compactMode?: boolean;
 }
 
 const ColorPickerPopupContent = ({
@@ -100,6 +99,9 @@ const ColorPickerPopupContent = ({
   getOpenPopup: () => AppState["openPopup"];
 }) => {
   const { container } = useExcalidrawContainer();
+  const stylesPanelMode = useStylesPanelMode();
+  const isCompactMode = stylesPanelMode !== "full";
+  const isMobileMode = stylesPanelMode === "mobile";
   const [, setActiveColorPickerSection] = useAtom(activeColorPickerSectionAtom);
 
   const [eyeDropperState, setEyeDropperState] = useAtom(activeEyeDropperAtom);
@@ -216,11 +218,8 @@ const ColorPickerPopupContent = ({
           type={type}
           elements={elements}
           updateData={updateData}
-          showTitle={
-            appState.stylesPanelMode === "compact" ||
-            appState.stylesPanelMode === "mobile"
-          }
-          showHotKey={appState.stylesPanelMode !== "mobile"}
+          showTitle={isCompactMode}
+          showHotKey={!isMobileMode}
         >
           {colorInputJSX}
         </Picker>
@@ -235,7 +234,6 @@ const ColorPickerTrigger = ({
   label,
   color,
   type,
-  stylesPanelMode,
   mode = "background",
   onToggle,
   editingTextElement,
@@ -243,11 +241,13 @@ const ColorPickerTrigger = ({
   color: string | null;
   label: string;
   type: ColorPickerType;
-  stylesPanelMode?: AppState["stylesPanelMode"];
   mode?: "background" | "stroke";
   onToggle: () => void;
   editingTextElement?: boolean;
 }) => {
+  const stylesPanelMode = useStylesPanelMode();
+  const isCompactMode = stylesPanelMode !== "full";
+  const isMobileMode = stylesPanelMode === "mobile";
   const handleClick = (e: React.MouseEvent) => {
     // use pointerdown so we run before outside-close logic
     e.preventDefault();
@@ -268,9 +268,8 @@ 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",
+        "compact-sizing": isCompactMode,
+        "mobile-border": isMobileMode,
       })}
       aria-label={label}
       style={color ? { "--swatch-color": color } : undefined}
@@ -283,22 +282,20 @@ const ColorPickerTrigger = ({
       onClick={handleClick}
     >
       <div className="color-picker__button-outline">{!color && slashIcon}</div>
-      {(stylesPanelMode === "compact" || stylesPanelMode === "mobile") &&
-        color &&
-        mode === "stroke" && (
-          <div className="color-picker__button-background">
-            <span
-              style={{
-                color:
-                  color && isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD)
-                    ? "#fff"
-                    : "#111",
-              }}
-            >
-              {strokeIcon}
-            </span>
-          </div>
-        )}
+      {isCompactMode && color && mode === "stroke" && (
+        <div className="color-picker__button-background">
+          <span
+            style={{
+              color:
+                color && isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD)
+                  ? "#fff"
+                  : "#111",
+            }}
+          >
+            {strokeIcon}
+          </span>
+        </div>
+      )}
     </Popover.Trigger>
   );
 };
@@ -318,10 +315,8 @@ export const ColorPicker = ({
   useEffect(() => {
     openRef.current = appState.openPopup;
   }, [appState.openPopup]);
-  const compactMode =
-    type !== "canvasBackground" &&
-    (appState.stylesPanelMode === "compact" ||
-      appState.stylesPanelMode === "mobile");
+  const stylesPanelMode = useStylesPanelMode();
+  const isCompactMode = stylesPanelMode !== "full";
 
   return (
     <div>
@@ -329,10 +324,10 @@ export const ColorPicker = ({
         role="dialog"
         aria-modal="true"
         className={clsx("color-picker-container", {
-          "color-picker-container--no-top-picks": compactMode,
+          "color-picker-container--no-top-picks": isCompactMode,
         })}
       >
-        {!compactMode && (
+        {!isCompactMode && (
           <TopPicks
             activeColor={color}
             onChange={onChange}
@@ -340,7 +335,7 @@ export const ColorPicker = ({
             topPicks={topPicks}
           />
         )}
-        {!compactMode && <ButtonSeparator />}
+        {!isCompactMode && <ButtonSeparator />}
         <Popover.Root
           open={appState.openPopup === type}
           onOpenChange={(open) => {
@@ -354,7 +349,6 @@ export const ColorPicker = ({
             color={color}
             label={label}
             type={type}
-            stylesPanelMode={appState.stylesPanelMode}
             mode={type === "elementStroke" ? "stroke" : "background"}
             editingTextElement={!!appState.editingTextElement}
             onToggle={() => {

+ 3 - 3
packages/excalidraw/components/CommandPalette/CommandPalette.tsx

@@ -903,7 +903,7 @@ function CommandPaletteInner({
         ref={inputRef}
       />
 
-      {!app.device.viewport.isMobile && (
+      {app.editorInterface.formFactor !== "phone" && (
         <div className="shortcuts-wrapper">
           <CommandShortcutHint shortcut="↑↓">
             {t("commandPalette.shortcuts.select")}
@@ -937,7 +937,7 @@ function CommandPaletteInner({
               onClick={(event) => executeCommand(lastUsed, event)}
               disabled={!isCommandAvailable(lastUsed)}
               onMouseMove={() => setCurrentCommand(lastUsed)}
-              showShortcut={!app.device.viewport.isMobile}
+              showShortcut={app.editorInterface.formFactor !== "phone"}
               appState={uiAppState}
             />
           </div>
@@ -955,7 +955,7 @@ function CommandPaletteInner({
                     isSelected={command.label === currentCommand?.label}
                     onClick={(event) => executeCommand(command, event)}
                     onMouseMove={() => setCurrentCommand(command)}
-                    showShortcut={!app.device.viewport.isMobile}
+                    showShortcut={app.editorInterface.formFactor !== "phone"}
                     appState={uiAppState}
                     size={category === "Library" ? "large" : "small"}
                   />

+ 2 - 2
packages/excalidraw/components/Dialog.tsx

@@ -9,7 +9,7 @@ import { t } from "../i18n";
 
 import {
   useExcalidrawContainer,
-  useDevice,
+  useEditorInterface,
   useExcalidrawSetAppState,
 } from "./App";
 import { Island } from "./Island";
@@ -51,7 +51,7 @@ export const Dialog = (props: DialogProps) => {
   const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
   const [lastActiveElement] = useState(document.activeElement);
   const { id } = useExcalidrawContainer();
-  const isFullscreen = useDevice().viewport.isMobile;
+  const isFullscreen = useEditorInterface().formFactor === "phone";
 
   useEffect(() => {
     if (!islandNode) {

+ 8 - 2
packages/excalidraw/components/FontPicker/FontPickerList.tsx

@@ -20,7 +20,12 @@ import type { ValueOf } from "@excalidraw/common/utility-types";
 
 import { Fonts } from "../../fonts";
 import { t } from "../../i18n";
-import { useApp, useAppProps, useExcalidrawContainer } from "../App";
+import {
+  useApp,
+  useAppProps,
+  useExcalidrawContainer,
+  useStylesPanelMode,
+} from "../App";
 import { PropertiesPopover } from "../PropertiesPopover";
 import { QuickSearch } from "../QuickSearch";
 import { ScrollableList } from "../ScrollableList";
@@ -93,6 +98,7 @@ export const FontPickerList = React.memo(
     const app = useApp();
     const { fonts } = app;
     const { showDeprecatedFonts } = useAppProps();
+    const stylesPanelMode = useStylesPanelMode();
 
     const [searchTerm, setSearchTerm] = useState("");
     const inputRef = useRef<HTMLInputElement>(null);
@@ -338,7 +344,7 @@ export const FontPickerList = React.memo(
         onKeyDown={onKeyDown}
         preventAutoFocusOnTouch={!!app.state.editingTextElement}
       >
-        {app.state.stylesPanelMode === "full" && (
+        {stylesPanelMode === "full" && (
           <QuickSearch
             ref={inputRef}
             placeholder={t("quickSearch.placeholder")}

+ 8 - 6
packages/excalidraw/components/HintViewer.tsx

@@ -11,6 +11,8 @@ import {
 
 import { isNodeInFlowchart } from "@excalidraw/element";
 
+import type { EditorInterface } from "@excalidraw/common";
+
 import { t } from "../i18n";
 import { getShortcutKey } from "../shortcut";
 import { isEraserActive } from "../appState";
@@ -18,12 +20,12 @@ import { isGridModeEnabled } from "../snapping";
 
 import "./HintViewer.scss";
 
-import type { AppClassProperties, Device, UIAppState } from "../types";
+import type { AppClassProperties, UIAppState } from "../types";
 
 interface HintViewerProps {
   appState: UIAppState;
   isMobile: boolean;
-  device: Device;
+  editorInterface: EditorInterface;
   app: AppClassProperties;
 }
 
@@ -35,7 +37,7 @@ const getTaggedShortcutKey = (key: string | string[]) =>
 const getHints = ({
   appState,
   isMobile,
-  device,
+  editorInterface,
   app,
 }: HintViewerProps): null | string | string[] => {
   const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
@@ -51,7 +53,7 @@ const getHints = ({
     });
   }
 
-  if (appState.openSidebar && !device.editor.canFitSidebar) {
+  if (appState.openSidebar && !editorInterface.canFitSidebar) {
     return null;
   }
 
@@ -225,13 +227,13 @@ const getHints = ({
 export const HintViewer = ({
   appState,
   isMobile,
-  device,
+  editorInterface,
   app,
 }: HintViewerProps) => {
   const hints = getHints({
     appState,
     isMobile,
-    device,
+    editorInterface,
     app,
   });
 

+ 3 - 3
packages/excalidraw/components/IconPicker.tsx

@@ -8,7 +8,7 @@ import { atom, useAtom } from "../editor-jotai";
 import { getLanguage, t } from "../i18n";
 
 import Collapsible from "./Stats/Collapsible";
-import { useDevice, useExcalidrawContainer } from "./App";
+import { useEditorInterface, useExcalidrawContainer } from "./App";
 
 import "./IconPicker.scss";
 
@@ -38,7 +38,7 @@ function Picker<T>({
   onClose: () => void;
   numberOfOptionsToAlwaysShow?: number;
 }) {
-  const device = useDevice();
+  const editorInterface = useEditorInterface();
   const { container } = useExcalidrawContainer();
 
   const handleKeyDown = (event: React.KeyboardEvent) => {
@@ -153,7 +153,7 @@ function Picker<T>({
     );
   };
 
-  const isMobile = device.editor.isMobile;
+  const isMobile = editorInterface.formFactor === "phone";
 
   return (
     <Popover.Content

+ 42 - 36
packages/excalidraw/components/LayerUI.tsx

@@ -46,7 +46,7 @@ import Footer from "./footer/Footer";
 import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
 import MainMenu from "./main-menu/MainMenu";
 import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
-import { useDevice } from "./App";
+import { useEditorInterface, useStylesPanelMode } from "./App";
 import { OverwriteConfirmDialog } from "./OverwriteConfirm/OverwriteConfirm";
 import { LibraryIcon } from "./icons";
 import { DefaultSidebar } from "./DefaultSidebar";
@@ -161,27 +161,28 @@ const LayerUI = ({
   isCollaborating,
   generateLinkForSelection,
 }: LayerUIProps) => {
-  const device = useDevice();
+  const editorInterface = useEditorInterface();
+  const stylesPanelMode = useStylesPanelMode();
+  const isCompactStylesPanel = stylesPanelMode === "compact";
   const tunnels = useInitializeTunnels();
 
-  const spacing =
-    appState.stylesPanelMode === "compact"
-      ? {
-          menuTopGap: 4,
-          toolbarColGap: 4,
-          toolbarRowGap: 1,
-          toolbarInnerRowGap: 0.5,
-          islandPadding: 1,
-          collabMarginLeft: 8,
-        }
-      : {
-          menuTopGap: 6,
-          toolbarColGap: 4,
-          toolbarRowGap: 1,
-          toolbarInnerRowGap: 1,
-          islandPadding: 1,
-          collabMarginLeft: 8,
-        };
+  const spacing = isCompactStylesPanel
+    ? {
+        menuTopGap: 4,
+        toolbarColGap: 4,
+        toolbarRowGap: 1,
+        toolbarInnerRowGap: 0.5,
+        islandPadding: 1,
+        collabMarginLeft: 8,
+      }
+    : {
+        menuTopGap: 6,
+        toolbarColGap: 4,
+        toolbarRowGap: 1,
+        toolbarInnerRowGap: 1,
+        islandPadding: 1,
+        collabMarginLeft: 8,
+      };
 
   const TunnelsJotaiProvider = tunnels.tunnelsJotai.Provider;
 
@@ -236,7 +237,7 @@ const LayerUI = ({
   );
 
   const renderSelectedShapeActions = () => {
-    const isCompactMode = appState.stylesPanelMode === "compact";
+    const isCompactMode = isCompactStylesPanel;
 
     return (
       <Section
@@ -308,7 +309,7 @@ const LayerUI = ({
             <div
               className={clsx("selected-shape-actions-container", {
                 "selected-shape-actions-container--compact":
-                  appState.stylesPanelMode === "compact",
+                  isCompactStylesPanel,
               })}
             >
               {shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
@@ -333,14 +334,13 @@ const LayerUI = ({
                           padding={spacing.islandPadding}
                           className={clsx("App-toolbar", {
                             "zen-mode": appState.zenModeEnabled,
-                            "App-toolbar--compact":
-                              appState.stylesPanelMode === "compact",
+                            "App-toolbar--compact": isCompactStylesPanel,
                           })}
                         >
                           <HintViewer
                             appState={appState}
-                            isMobile={device.editor.isMobile}
-                            device={device}
+                            isMobile={editorInterface.formFactor === "phone"}
+                            editorInterface={editorInterface}
                             app={app}
                           />
                           {heading}
@@ -406,8 +406,7 @@ const LayerUI = ({
               "layer-ui__wrapper__top-right zen-mode-transition",
               {
                 "transition-right": appState.zenModeEnabled,
-                "layer-ui__wrapper__top-right--compact":
-                  appState.stylesPanelMode === "compact",
+                "layer-ui__wrapper__top-right--compact": isCompactStylesPanel,
               },
             )}
           >
@@ -417,7 +416,10 @@ const LayerUI = ({
                 userToFollow={appState.userToFollow?.socketId || null}
               />
             )}
-            {renderTopRightUI?.(device.editor.isMobile, appState)}
+            {renderTopRightUI?.(
+              editorInterface.formFactor === "phone",
+              appState,
+            )}
             {!appState.viewModeEnabled &&
               appState.openDialog?.name !== "elementLinkSelector" &&
               // hide button when sidebar docked
@@ -448,7 +450,9 @@ const LayerUI = ({
           trackEvent(
             "sidebar",
             `toggleDock (${docked ? "dock" : "undock"})`,
-            `(${device.editor.isMobile ? "mobile" : "desktop"})`,
+            `(${
+              editorInterface.formFactor === "phone" ? "mobile" : "desktop"
+            })`,
           );
         }}
       />
@@ -476,13 +480,15 @@ const LayerUI = ({
             trackEvent(
               "sidebar",
               `${DEFAULT_SIDEBAR.name} (open)`,
-              `button (${device.editor.isMobile ? "mobile" : "desktop"})`,
+              `button (${
+                editorInterface.formFactor === "phone" ? "mobile" : "desktop"
+              })`,
             );
           }
         }}
         tab={DEFAULT_SIDEBAR.defaultTab}
       >
-        {appState.stylesPanelMode === "full" &&
+        {stylesPanelMode === "full" &&
           appState.width >= MQ_MIN_WIDTH_DESKTOP &&
           t("toolBar.library")}
       </DefaultSidebar.Trigger>
@@ -496,7 +502,7 @@ const LayerUI = ({
           {appState.errorMessage}
         </ErrorDialog>
       )}
-      {eyeDropperState && !device.editor.isMobile && (
+      {eyeDropperState && editorInterface.formFactor !== "phone" && (
         <EyeDropper
           colorPickerType={eyeDropperState.colorPickerType}
           onCancel={() => {
@@ -575,7 +581,7 @@ const LayerUI = ({
           }
         />
       )}
-      {device.editor.isMobile && (
+      {editorInterface.formFactor === "phone" && (
         <MobileMenu
           app={app}
           appState={appState}
@@ -593,14 +599,14 @@ const LayerUI = ({
           UIOptions={UIOptions}
         />
       )}
-      {!device.editor.isMobile && (
+      {editorInterface.formFactor !== "phone" && (
         <>
           <div
             className="layer-ui__wrapper"
             style={
               appState.openSidebar &&
               isSidebarDocked &&
-              device.editor.canFitSidebar
+              editorInterface.canFitSidebar
                 ? { width: `calc(100% - var(--right-sidebar-width))` }
                 : {}
             }

+ 3 - 3
packages/excalidraw/components/LibraryMenuItems.tsx

@@ -32,7 +32,7 @@ import "./LibraryMenuItems.scss";
 
 import { TextField } from "./TextField";
 
-import { useDevice } from "./App";
+import { useEditorInterface } from "./App";
 
 import { Button } from "./Button";
 
@@ -75,7 +75,7 @@ export default function LibraryMenuItems({
   selectedItems: LibraryItem["id"][];
   onSelectItems: (id: LibraryItem["id"][]) => void;
 }) {
-  const device = useDevice();
+  const editorInterface = useEditorInterface();
   const libraryContainerRef = useRef<HTMLDivElement>(null);
   const scrollPosition = useScrollPosition<HTMLDivElement>(libraryContainerRef);
 
@@ -392,7 +392,7 @@ export default function LibraryMenuItems({
             ref={searchInputRef}
             type="search"
             className={clsx("library-menu-items-container__search", {
-              hideCancelButton: !device.editor.isMobile,
+              hideCancelButton: editorInterface.formFactor !== "phone",
             })}
             placeholder={t("library.search.inputPlaceholder")}
             value={searchInputValue}

+ 2 - 2
packages/excalidraw/components/LibraryUnit.tsx

@@ -3,7 +3,7 @@ import { memo, useRef, useState } from "react";
 
 import { useLibraryItemSvg } from "../hooks/useLibraryItemSvg";
 
-import { useDevice } from "./App";
+import { useEditorInterface } from "./App";
 import { CheckboxItem } from "./CheckboxItem";
 import { PlusIcon } from "./icons";
 
@@ -36,7 +36,7 @@ export const LibraryUnit = memo(
     const svg = useLibraryItemSvg(id, elements, svgCache, ref);
 
     const [isHovered, setIsHovered] = useState(false);
-    const isMobile = useDevice().editor.isMobile;
+    const isMobile = useEditorInterface().formFactor === "phone";
     const adder = isPending && (
       <div className="library-unit__adder">{PlusIcon}</div>
     );

+ 6 - 5
packages/excalidraw/components/PropertiesPopover.tsx

@@ -4,7 +4,7 @@ import React, { type ReactNode } from "react";
 
 import { isInteractive } from "@excalidraw/common";
 
-import { useDevice } from "./App";
+import { useEditorInterface } from "./App";
 import { Island } from "./Island";
 
 interface PropertiesPopoverProps {
@@ -39,9 +39,9 @@ export const PropertiesPopover = React.forwardRef<
     },
     ref,
   ) => {
-    const device = useDevice();
+    const editorInterface = useEditorInterface();
     const isMobilePortrait =
-      device.editor.isMobile && !device.viewport.isLandscape;
+      editorInterface.formFactor === "phone" && !editorInterface.isLandscape;
 
     return (
       <Popover.Portal container={container}>
@@ -56,7 +56,8 @@ export const PropertiesPopover = React.forwardRef<
           collisionBoundary={container ?? undefined}
           style={{
             zIndex: "var(--zIndex-ui-styles-popup)",
-            marginLeft: device.editor.isMobile ? "0.5rem" : undefined,
+            marginLeft:
+              editorInterface.formFactor === "phone" ? "0.5rem" : undefined,
           }}
           onPointerLeave={onPointerLeave}
           onKeyDown={onKeyDown}
@@ -64,7 +65,7 @@ export const PropertiesPopover = React.forwardRef<
           onPointerDownOutside={onPointerDownOutside}
           onOpenAutoFocus={(e) => {
             // prevent auto-focus on touch devices to avoid keyboard popup
-            if (preventAutoFocusOnTouch && device.isTouchScreen) {
+            if (preventAutoFocusOnTouch && editorInterface.isTouchScreen) {
               e.preventDefault();
             }
           }}

+ 6 - 6
packages/excalidraw/components/Sidebar/Sidebar.tsx

@@ -20,7 +20,7 @@ import {
 import { useUIAppState } from "../../context/ui-appState";
 import { atom, useSetAtom } from "../../editor-jotai";
 import { useOutsideClick } from "../../hooks/useOutsideClick";
-import { useDevice, useExcalidrawSetAppState } from "../App";
+import { useEditorInterface, useExcalidrawSetAppState } from "../App";
 import { Island } from "../Island";
 
 import { SidebarHeader } from "./SidebarHeader";
@@ -96,7 +96,7 @@ export const SidebarInner = forwardRef(
       return islandRef.current!;
     });
 
-    const device = useDevice();
+    const editorInterface = useEditorInterface();
 
     const closeLibrary = useCallback(() => {
       const isDialogOpen = !!document.querySelector(".Dialog");
@@ -117,11 +117,11 @@ export const SidebarInner = forwardRef(
           if ((event.target as Element).closest(".sidebar-trigger")) {
             return;
           }
-          if (!docked || !device.editor.canFitSidebar) {
+          if (!docked || !editorInterface.canFitSidebar) {
             closeLibrary();
           }
         },
-        [closeLibrary, docked, device.editor.canFitSidebar],
+        [closeLibrary, docked, editorInterface.canFitSidebar],
       ),
     );
 
@@ -129,7 +129,7 @@ export const SidebarInner = forwardRef(
       const handleKeyDown = (event: KeyboardEvent) => {
         if (
           event.key === KEYS.ESCAPE &&
-          (!docked || !device.editor.canFitSidebar)
+          (!docked || !editorInterface.canFitSidebar)
         ) {
           closeLibrary();
         }
@@ -138,7 +138,7 @@ export const SidebarInner = forwardRef(
       return () => {
         document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
       };
-    }, [closeLibrary, docked, device.editor.canFitSidebar]);
+    }, [closeLibrary, docked, editorInterface.canFitSidebar]);
 
     return (
       <Island

+ 3 - 3
packages/excalidraw/components/Sidebar/SidebarHeader.tsx

@@ -2,7 +2,7 @@ import clsx from "clsx";
 import { useContext } from "react";
 
 import { t } from "../../i18n";
-import { useDevice } from "../App";
+import { useEditorInterface } from "../App";
 import { Button } from "../Button";
 import { Tooltip } from "../Tooltip";
 import { CloseIcon, PinIcon } from "../icons";
@@ -16,11 +16,11 @@ export const SidebarHeader = ({
   children?: React.ReactNode;
   className?: string;
 }) => {
-  const device = useDevice();
+  const editorInterface = useEditorInterface();
   const props = useContext(SidebarPropsContext);
 
   const renderDockButton = !!(
-    device.editor.canFitSidebar && props.shouldRenderDockButton
+    editorInterface.canFitSidebar && props.shouldRenderDockButton
   );
 
   return (

+ 4 - 3
packages/excalidraw/components/canvases/InteractiveCanvas.tsx

@@ -4,6 +4,7 @@ import {
   CURSOR_TYPE,
   isShallowEqual,
   sceneCoordsToViewportCoords,
+  type EditorInterface,
 } from "@excalidraw/common";
 
 import type {
@@ -20,7 +21,7 @@ import type {
   RenderableElementsMap,
   RenderInteractiveSceneCallback,
 } from "../../scene/types";
-import type { AppState, Device, InteractiveCanvasAppState } from "../../types";
+import type { AppState, InteractiveCanvasAppState } from "../../types";
 import type { DOMAttributes } from "react";
 
 type InteractiveCanvasProps = {
@@ -35,7 +36,7 @@ type InteractiveCanvasProps = {
   scale: number;
   appState: InteractiveCanvasAppState;
   renderScrollbars: boolean;
-  device: Device;
+  editorInterface: EditorInterface;
   renderInteractiveSceneCallback: (
     data: RenderInteractiveSceneCallback,
   ) => void;
@@ -146,7 +147,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
           selectionColor,
           renderScrollbars: props.renderScrollbars,
         },
-        device: props.device,
+        editorInterface: props.editorInterface,
         callback: props.renderInteractiveSceneCallback,
       },
       isRenderThrottlingEnabled(),

+ 5 - 10
packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx

@@ -5,7 +5,7 @@ import { EVENT, KEYS } from "@excalidraw/common";
 
 import { useOutsideClick } from "../../hooks/useOutsideClick";
 import { useStable } from "../../hooks/useStable";
-import { useDevice } from "../App";
+import { useEditorInterface } from "../App";
 import { Island } from "../Island";
 import Stack from "../Stack";
 
@@ -29,7 +29,7 @@ const MenuContent = ({
   style?: React.CSSProperties;
   placement?: "top" | "bottom";
 }) => {
-  const device = useDevice();
+  const editorInterface = useEditorInterface();
   const menuRef = useRef<HTMLDivElement>(null);
 
   const callbacksRef = useStable({ onClickOutside });
@@ -59,7 +59,7 @@ const MenuContent = ({
   }, [callbacksRef]);
 
   const classNames = clsx(`dropdown-menu ${className}`, {
-    "dropdown-menu--mobile": device.editor.isMobile,
+    "dropdown-menu--mobile": editorInterface.formFactor === "phone",
     "dropdown-menu--placement-top": placement === "top",
   }).trim();
 
@@ -73,13 +73,8 @@ const MenuContent = ({
       >
         {/* the zIndex ensures this menu has higher stacking order,
     see https://github.com/excalidraw/excalidraw/pull/1445 */}
-        {device.editor.isMobile ? (
-          <Stack.Col
-            className="dropdown-menu-container"
-            style={{ ["--gap" as any]: 1.25 }}
-          >
-            {children}
-          </Stack.Col>
+        {editorInterface.formFactor === "phone" ? (
+          <Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
         ) : (
           <Island
             className="dropdown-menu-container"

+ 3 - 3
packages/excalidraw/components/dropdownMenu/DropdownMenuItemContent.tsx

@@ -1,4 +1,4 @@
-import { useDevice } from "../App";
+import { useEditorInterface } from "../App";
 
 import { Ellipsify } from "../Ellipsify";
 
@@ -15,14 +15,14 @@ const MenuItemContent = ({
   textStyle?: React.CSSProperties;
   children: React.ReactNode;
 }) => {
-  const device = useDevice();
+  const editorInterface = useEditorInterface();
   return (
     <>
       {icon && <div className="dropdown-menu-item__icon">{icon}</div>}
       <div style={textStyle} className="dropdown-menu-item__text">
         <Ellipsify>{children}</Ellipsify>
       </div>
-      {shortcut && !device.editor.isMobile && (
+      {shortcut && editorInterface.formFactor !== "phone" && (
         <div className="dropdown-menu-item__shortcut">{shortcut}</div>
       )}
     </>

+ 3 - 3
packages/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.tsx

@@ -1,4 +1,4 @@
-import { useDevice } from "../App";
+import { useEditorInterface } from "../App";
 import { RadioGroup } from "../RadioGroup";
 
 type Props<T> = {
@@ -22,7 +22,7 @@ const DropdownMenuItemContentRadio = <T,>({
   children,
   name,
 }: Props<T>) => {
-  const device = useDevice();
+  const editorInterface = useEditorInterface();
 
   return (
     <>
@@ -37,7 +37,7 @@ const DropdownMenuItemContentRadio = <T,>({
           choices={choices}
         />
       </div>
-      {shortcut && !device.editor.isMobile && (
+      {shortcut && editorInterface.formFactor !== "phone" && (
         <div className="dropdown-menu-item__shortcut dropdown-menu-item__shortcut--orphaned">
           {shortcut}
         </div>

+ 3 - 3
packages/excalidraw/components/dropdownMenu/DropdownMenuTrigger.tsx

@@ -1,6 +1,6 @@
 import clsx from "clsx";
 
-import { useDevice } from "../App";
+import { useEditorInterface } from "../App";
 
 const MenuTrigger = ({
   className = "",
@@ -14,12 +14,12 @@ const MenuTrigger = ({
   onToggle: () => void;
   title?: string;
 } & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
-  const device = useDevice();
+  const editorInterface = useEditorInterface();
   const classNames = clsx(
     `dropdown-menu-button ${className}`,
     "zen-mode-transition",
     {
-      "dropdown-menu-button--mobile": device.editor.isMobile,
+      "dropdown-menu-button--mobile": editorInterface.formFactor === "phone",
     },
   ).trim();
   return (

+ 4 - 4
packages/excalidraw/components/hyperlink/Hyperlink.tsx

@@ -41,7 +41,7 @@ import { getTooltipDiv, updateTooltipPosition } from "../../components/Tooltip";
 
 import { t } from "../../i18n";
 
-import { useAppProps, useDevice, useExcalidrawAppState } from "../App";
+import { useAppProps, useEditorInterface, useExcalidrawAppState } from "../App";
 import { ToolButton } from "../ToolButton";
 import { FreedrawIcon, TrashIcon, elementLinkIcon } from "../icons";
 import { getSelectedElements } from "../../scene";
@@ -88,7 +88,7 @@ export const Hyperlink = ({
   const elementsMap = scene.getNonDeletedElementsMap();
   const appState = useExcalidrawAppState();
   const appProps = useAppProps();
-  const device = useDevice();
+  const editorInterface = useEditorInterface();
 
   const linkVal = element.link || "";
 
@@ -189,11 +189,11 @@ export const Hyperlink = ({
     if (
       isEditing &&
       inputRef?.current &&
-      !(device.viewport.isMobile || device.isTouchScreen)
+      !(editorInterface.formFactor === "phone" || editorInterface.isTouchScreen)
     ) {
       inputRef.current.select();
     }
-  }, [isEditing, device.viewport.isMobile, device.isTouchScreen]);
+  }, [isEditing, editorInterface.formFactor, editorInterface.isTouchScreen]);
 
   useEffect(() => {
     let timeoutId: number | null = null;

+ 5 - 2
packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.tsx

@@ -1,6 +1,6 @@
 import clsx from "clsx";
 
-import { isMobileOrTablet, MQ_MIN_WIDTH_DESKTOP } from "@excalidraw/common";
+import { MQ_MIN_WIDTH_DESKTOP, type EditorInterface } from "@excalidraw/common";
 
 import { t } from "../../i18n";
 import { Button } from "../Button";
@@ -12,15 +12,18 @@ import "./LiveCollaborationTrigger.scss";
 const LiveCollaborationTrigger = ({
   isCollaborating,
   onSelect,
+  editorInterface,
   ...rest
 }: {
   isCollaborating: boolean;
   onSelect: () => void;
+  editorInterface?: EditorInterface;
 } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
   const appState = useUIAppState();
 
   const showIconOnly =
-    isMobileOrTablet() || appState.width < MQ_MIN_WIDTH_DESKTOP;
+    editorInterface?.formFactor !== "desktop" ||
+    appState.width < MQ_MIN_WIDTH_DESKTOP;
 
   return (
     <Button

+ 18 - 13
packages/excalidraw/components/main-menu/MainMenu.tsx

@@ -5,7 +5,7 @@ import { composeEventHandlers } from "@excalidraw/common";
 import { useTunnels } from "../../context/tunnels";
 import { useUIAppState } from "../../context/ui-appState";
 import { t } from "../../i18n";
-import { useDevice, useExcalidrawSetAppState } from "../App";
+import { useEditorInterface, useExcalidrawSetAppState } from "../App";
 import { UserList } from "../UserList";
 import DropdownMenu from "../dropdownMenu/DropdownMenu";
 import { withInternalFallback } from "../hoc/withInternalFallback";
@@ -27,7 +27,7 @@ const MainMenu = Object.assign(
       onSelect?: (event: Event) => void;
     }) => {
       const { MainMenuTunnel } = useTunnels();
-      const device = useDevice();
+      const editorInterface = useEditorInterface();
       const appState = useUIAppState();
       const setAppState = useExcalidrawSetAppState();
 
@@ -53,19 +53,24 @@ const MainMenu = Object.assign(
                 setAppState({ openMenu: null });
               })}
               placement="bottom"
-              className={device.editor.isMobile ? "main-menu-dropdown" : ""}
+              className={
+                editorInterface.formFactor === "phone"
+                  ? "main-menu-dropdown"
+                  : ""
+              }
             >
               {children}
-              {device.editor.isMobile && appState.collaborators.size > 0 && (
-                <fieldset className="UserList-Wrapper">
-                  <legend>{t("labels.collaborators")}</legend>
-                  <UserList
-                    mobile={true}
-                    collaborators={appState.collaborators}
-                    userToFollow={appState.userToFollow?.socketId || null}
-                  />
-                </fieldset>
-              )}
+              {editorInterface.formFactor === "phone" &&
+                appState.collaborators.size > 0 && (
+                  <fieldset className="UserList-Wrapper">
+                    <legend>{t("labels.collaborators")}</legend>
+                    <UserList
+                      mobile={true}
+                      collaborators={appState.collaborators}
+                      userToFollow={appState.userToFollow?.socketId || null}
+                    />
+                  </fieldset>
+                )}
             </DropdownMenu.Content>
           </DropdownMenu>
         </MainMenuTunnel.In>

+ 3 - 3
packages/excalidraw/components/welcome-screen/WelcomeScreen.Center.tsx

@@ -3,7 +3,7 @@ import { getShortcutFromShortcutName } from "../../actions/shortcuts";
 import { useTunnels } from "../../context/tunnels";
 import { useUIAppState } from "../../context/ui-appState";
 import { t, useI18n } from "../../i18n";
-import { useDevice, useExcalidrawActionManager } from "../App";
+import { useEditorInterface, useExcalidrawActionManager } from "../App";
 import { ExcalidrawLogo } from "../ExcalidrawLogo";
 import { HelpIcon, LoadIcon, usersIcon } from "../icons";
 
@@ -18,12 +18,12 @@ const WelcomeScreenMenuItemContent = ({
   shortcut?: string | null;
   children: React.ReactNode;
 }) => {
-  const device = useDevice();
+  const editorInterface = useEditorInterface();
   return (
     <>
       <div className="welcome-screen-menu-item__icon">{icon}</div>
       <div className="welcome-screen-menu-item__text">{children}</div>
-      {shortcut && !device.editor.isMobile && (
+      {shortcut && editorInterface.formFactor !== "phone" && (
         <div className="welcome-screen-menu-item__shortcut">{shortcut}</div>
       )}
     </>

+ 7 - 4
packages/excalidraw/hooks/useCreatePortalContainer.ts

@@ -2,7 +2,7 @@ import { useState, useLayoutEffect } from "react";
 
 import { THEME } from "@excalidraw/common";
 
-import { useDevice, useExcalidrawContainer } from "../components/App";
+import { useEditorInterface, useExcalidrawContainer } from "../components/App";
 import { useUIAppState } from "../context/ui-appState";
 
 export const useCreatePortalContainer = (opts?: {
@@ -11,7 +11,7 @@ export const useCreatePortalContainer = (opts?: {
 }) => {
   const [div, setDiv] = useState<HTMLDivElement | null>(null);
 
-  const device = useDevice();
+  const editorInterface = useEditorInterface();
   const { theme } = useUIAppState();
 
   const { container: excalidrawContainer } = useExcalidrawContainer();
@@ -20,10 +20,13 @@ export const useCreatePortalContainer = (opts?: {
     if (div) {
       div.className = "";
       div.classList.add("excalidraw", ...(opts?.className?.split(/\s+/) || []));
-      div.classList.toggle("excalidraw--mobile", device.editor.isMobile);
+      div.classList.toggle(
+        "excalidraw--mobile",
+        editorInterface.formFactor === "phone",
+      );
       div.classList.toggle("theme--dark", theme === THEME.DARK);
     }
-  }, [div, theme, device.editor.isMobile, opts?.className]);
+  }, [div, theme, editorInterface.formFactor, opts?.className]);
 
   useLayoutEffect(() => {
     const container = opts?.parentSelector

+ 4 - 6
packages/excalidraw/index.tsx

@@ -263,6 +263,9 @@ export {
   DEFAULT_LASER_COLOR,
   UserIdleState,
   normalizeLink,
+  sceneCoordsToViewportCoords,
+  viewportCoordsToSceneCoords,
+  getFormFactor,
 } from "@excalidraw/common";
 
 export {
@@ -275,17 +278,12 @@ export { CaptureUpdateAction } from "@excalidraw/element";
 
 export { parseLibraryTokensFromUrl, useHandleLibrary } from "./data/library";
 
-export {
-  sceneCoordsToViewportCoords,
-  viewportCoordsToSceneCoords,
-} from "@excalidraw/common";
-
 export { Sidebar } from "./components/Sidebar/Sidebar";
 export { Button } from "./components/Button";
 export { Footer };
 export { MainMenu };
 export { Ellipsify } from "./components/Ellipsify";
-export { useDevice } from "./components/App";
+export { useEditorInterface, useStylesPanelMode } from "./components/App";
 export { WelcomeScreen };
 export { LiveCollaborationTrigger };
 export { Stats } from "./components/Stats";

+ 13 - 6
packages/excalidraw/renderer/interactiveScene.ts

@@ -19,7 +19,7 @@ import {
 import { FIXED_BINDING_DISTANCE, maxBindingGap } from "@excalidraw/element";
 import { LinearElementEditor } from "@excalidraw/element";
 import {
-  getOmitSidesForDevice,
+  getOmitSidesForEditorInterface,
   getTransformHandles,
   getTransformHandlesFromCoords,
   hasBoundingBox,
@@ -734,7 +734,7 @@ const _renderInteractiveScene = ({
   scale,
   appState,
   renderConfig,
-  device,
+  editorInterface,
 }: InteractiveSceneRenderConfig) => {
   if (canvas === null) {
     return { atLeastOneVisibleElement: false, elementsMap };
@@ -892,7 +892,11 @@ const _renderInteractiveScene = ({
 
   // Paint selected elements
   if (!appState.multiElement && !appState.selectedLinearElement?.isEditing) {
-    const showBoundingBox = hasBoundingBox(selectedElements, appState);
+    const showBoundingBox = hasBoundingBox(
+      selectedElements,
+      appState,
+      editorInterface,
+    );
 
     const isSingleLinearElementSelected =
       selectedElements.length === 1 && isLinearElement(selectedElements[0]);
@@ -1024,7 +1028,7 @@ const _renderInteractiveScene = ({
         appState.zoom,
         elementsMap,
         "mouse", // when we render we don't know which pointer type so use mouse,
-        getOmitSidesForDevice(device),
+        getOmitSidesForEditorInterface(editorInterface),
       );
       if (
         !appState.viewModeEnabled &&
@@ -1088,8 +1092,11 @@ const _renderInteractiveScene = ({
         appState.zoom,
         "mouse",
         isFrameSelected
-          ? { ...getOmitSidesForDevice(device), rotation: true }
-          : getOmitSidesForDevice(device),
+          ? {
+              ...getOmitSidesForEditorInterface(editorInterface),
+              rotation: true,
+            }
+          : getOmitSidesForEditorInterface(editorInterface),
       );
       if (selectedElements.some((element) => !element.locked)) {
         renderTransformHandles(

+ 2 - 3
packages/excalidraw/scene/types.ts

@@ -1,4 +1,4 @@
-import type { UserIdleState } from "@excalidraw/common";
+import type { UserIdleState, EditorInterface } from "@excalidraw/common";
 import type {
   ExcalidrawElement,
   NonDeletedElementsMap,
@@ -16,7 +16,6 @@ import type {
   InteractiveCanvasAppState,
   StaticCanvasAppState,
   SocketId,
-  Device,
   PendingExcalidrawElements,
 } from "../types";
 import type { RoughCanvas } from "roughjs/bin/canvas";
@@ -97,7 +96,7 @@ export type InteractiveSceneRenderConfig = {
   scale: number;
   appState: InteractiveCanvasAppState;
   renderConfig: InteractiveCanvasRenderConfig;
-  device: Device;
+  editorInterface: EditorInterface;
   callback: (data: RenderInteractiveSceneCallback) => void;
 };
 

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

@@ -985,7 +985,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -1181,7 +1180,6 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": {
@@ -1398,7 +1396,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -1732,7 +1729,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -2066,7 +2062,6 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": {
@@ -2281,7 +2276,6 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -2527,7 +2521,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -2833,7 +2826,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -3203,7 +3195,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": {
@@ -3699,7 +3690,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -4025,7 +4015,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -4354,7 +4343,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -5642,7 +5630,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -6864,7 +6851,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -7798,7 +7784,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -8800,7 +8785,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -9797,7 +9781,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,

+ 0 - 63
packages/excalidraw/tests/__snapshots__/history.test.tsx.snap

@@ -104,7 +104,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -723,7 +722,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -1211,7 +1209,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -1578,7 +1575,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -1948,7 +1944,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -2213,7 +2208,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -2659,7 +2653,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -2965,7 +2958,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -3287,7 +3279,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -3584,7 +3575,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -3873,7 +3863,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -4111,7 +4100,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -4371,7 +4359,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -4645,7 +4632,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -4877,7 +4863,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -5109,7 +5094,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -5359,7 +5343,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -5618,7 +5601,6 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -5878,7 +5860,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -6210,7 +6191,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -6643,7 +6623,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -7026,7 +7005,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -7330,7 +7308,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -7649,7 +7626,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -7882,7 +7858,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -8237,7 +8212,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -8598,7 +8572,6 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -9001,7 +8974,6 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -9293,7 +9265,6 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -9560,7 +9531,6 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -9828,7 +9798,6 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -10064,7 +10033,6 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -10363,7 +10331,6 @@ exports[`history > multiplayer undo/redo > should override remotely added points
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -10713,7 +10680,6 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -10955,7 +10921,6 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -11405,7 +11370,6 @@ exports[`history > multiplayer undo/redo > should update history entries after r
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -11666,7 +11630,6 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -11906,7 +11869,6 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -12144,7 +12106,6 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -12555,7 +12516,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on e
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -12765,7 +12725,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on e
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -12979,7 +12938,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on i
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -13280,7 +13238,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on i
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -13581,7 +13538,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -13828,7 +13784,6 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -14068,7 +14023,6 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -14308,7 +14262,6 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -14558,7 +14511,6 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -14893,7 +14845,6 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -15068,7 +15019,6 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -15353,7 +15303,6 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -15619,7 +15568,6 @@ exports[`history > singleplayer undo/redo > should not modify anything on unrela
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -15776,7 +15724,6 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -16060,7 +16007,6 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -16226,7 +16172,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -16934,7 +16879,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -17572,7 +17516,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -18208,7 +18151,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -18933,7 +18875,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -19687,7 +19628,6 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -20172,7 +20112,6 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -20681,7 +20620,6 @@ exports[`history > singleplayer undo/redo > should support element creation, del
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
@@ -21145,7 +21083,6 @@ exports[`history > singleplayer undo/redo > should support linear element creati
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,

+ 52 - 104
packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap

@@ -112,14 +112,13 @@ exports[`given element A and group of elements B and given both are selected whe
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -543,14 +542,13 @@ exports[`given element A and group of elements B and given both are selected whe
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -953,14 +951,13 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -1522,14 +1519,13 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -1737,14 +1733,13 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -2121,14 +2116,13 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -2367,14 +2361,13 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -2552,14 +2545,13 @@ exports[`regression tests > can drag element that covers another element, while
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -2878,14 +2870,13 @@ exports[`regression tests > change the properties of a shape > [end of test] app
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -3138,14 +3129,13 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -3382,14 +3372,13 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -3621,14 +3610,13 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -3883,14 +3871,13 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -4199,14 +4186,13 @@ exports[`regression tests > deleting last but one element in editing group shoul
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -4665,14 +4651,13 @@ exports[`regression tests > deselects group of selected elements on pointer down
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -4923,14 +4908,13 @@ exports[`regression tests > deselects group of selected elements on pointer up w
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -5229,14 +5213,13 @@ exports[`regression tests > deselects selected element on pointer down when poin
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -5412,14 +5395,13 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -5615,14 +5597,13 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -6015,14 +5996,13 @@ exports[`regression tests > drags selected elements from point inside common bou
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -6309,14 +6289,13 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -7173,14 +7152,13 @@ exports[`regression tests > given a group of selected elements with an element t
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -7510,14 +7488,13 @@ exports[`regression tests > given a selected element A and a not selected elemen
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -7791,14 +7768,13 @@ exports[`regression tests > given selected element A with lower z-index than uns
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -8029,14 +8005,13 @@ exports[`regression tests > given selected element A with lower z-index than uns
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -8270,14 +8245,13 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -8453,14 +8427,13 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -8636,14 +8609,13 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -8846,14 +8818,13 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -9079,14 +9050,13 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -9281,14 +9251,13 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -9509,14 +9478,13 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -9715,14 +9683,13 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -9925,14 +9892,13 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -10129,14 +10095,13 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -10310,14 +10275,13 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -10511,14 +10475,13 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -10702,14 +10665,13 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -11230,14 +11192,13 @@ exports[`regression tests > noop interaction after undo shouldn't create history
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -11509,14 +11470,13 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -11637,14 +11597,13 @@ exports[`regression tests > shift click on selected element should deselect it o
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -11844,14 +11803,13 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -12168,14 +12126,13 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -12604,14 +12561,13 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -13238,14 +13194,13 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -13368,14 +13323,13 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -14031,14 +13985,13 @@ exports[`regression tests > switches from group of selected elements to another
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -14372,14 +14325,13 @@ exports[`regression tests > switches selected element on pointer down > [end of
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -14607,14 +14559,13 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -14735,14 +14686,13 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -15128,14 +15078,13 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,
@@ -15257,14 +15206,13 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "userToFollow": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
-  "width": 1024,
+  "width": 1440,
   "zenModeEnabled": false,
   "zoom": {
     "value": 1,

+ 8 - 2
packages/excalidraw/tests/regressionTests.test.tsx

@@ -1,7 +1,13 @@
 import React from "react";
 import { vi } from "vitest";
 
-import { FONT_FAMILY, CODES, KEYS, reseed } from "@excalidraw/common";
+import {
+  FONT_FAMILY,
+  CODES,
+  KEYS,
+  reseed,
+  MQ_MIN_WIDTH_DESKTOP,
+} from "@excalidraw/common";
 
 import { setDateTimeForTests } from "@excalidraw/common";
 
@@ -60,7 +66,7 @@ beforeEach(async () => {
   finger2.reset();
 
   await render(<Excalidraw handleKeyboardGlobally={true} />);
-  API.setAppState({ height: 768, width: 1024 });
+  API.setAppState({ height: 768, width: MQ_MIN_WIDTH_DESKTOP });
 });
 
 afterEach(() => {

+ 6 - 10
packages/excalidraw/tests/test-utils.ts

@@ -189,24 +189,20 @@ export const withExcalidrawDimensions = async (
   dimensions: { width: number; height: number },
   cb: () => void,
 ) => {
+  const { h } = window;
+
   mockBoundingClientRect(dimensions);
   act(() => {
-    // @ts-ignore
-    h.app.refreshViewportBreakpoints();
-    // @ts-ignore
-    h.app.refreshEditorBreakpoints();
-    window.h.app.refresh();
+    h.app.refreshEditorInterface();
+    h.app.refresh();
   });
 
   await cb();
 
   restoreOriginalGetBoundingClientRect();
   act(() => {
-    // @ts-ignore
-    h.app.refreshViewportBreakpoints();
-    // @ts-ignore
-    h.app.refreshEditorBreakpoints();
-    window.h.app.refresh();
+    h.app.refreshEditorInterface();
+    h.app.refresh();
   });
 };
 

+ 9 - 16
packages/excalidraw/types.ts

@@ -3,6 +3,7 @@ import type {
   UserIdleState,
   throttleRAF,
   MIME_TYPES,
+  EditorInterface,
 } from "@excalidraw/common";
 
 import type { SuggestedBinding } from "@excalidraw/element";
@@ -449,9 +450,6 @@ export interface AppState {
   // as elements are unlocked, we remove the groupId from the elements
   // and also remove groupId from this map
   lockedMultiSelections: { [groupId: string]: true };
-
-  /** properties sidebar mode - determines whether to show compact or complete sidebar */
-  stylesPanelMode: "compact" | "full" | "mobile";
 }
 
 export type SearchMatch = {
@@ -676,6 +674,12 @@ export type UIOptions = Partial<{
   tools: {
     image: boolean;
   };
+  /**
+   * Optionally control the editor form factor and desktop UI mode from the host app.
+   * If not provided, we will take care of it internally.
+   */
+  formFactor?: EditorInterface["formFactor"];
+  desktopUIMode?: EditorInterface["desktopUIMode"];
   /** @deprecated does nothing. Will be removed in 0.15 */
   welcomeScreen?: boolean;
 }>;
@@ -715,7 +719,7 @@ export type AppClassProperties = {
     }
   >;
   files: BinaryFiles;
-  device: App["device"];
+  editorInterface: App["editorInterface"];
   scene: App["scene"];
   syncActionResult: App["syncActionResult"];
   fonts: App["fonts"];
@@ -847,6 +851,7 @@ export interface ExcalidrawImperativeAPI {
   setCursor: InstanceType<typeof App>["setCursor"];
   resetCursor: InstanceType<typeof App>["resetCursor"];
   toggleSidebar: InstanceType<typeof App>["toggleSidebar"];
+  getEditorInterface: () => EditorInterface;
   /**
    * Disables rendering of frames (including element clipping), but currently
    * the frames are still interactive in edit mode. As such, this API should be
@@ -885,18 +890,6 @@ export interface ExcalidrawImperativeAPI {
   ) => UnsubscribeCallback;
 }
 
-export type Device = Readonly<{
-  viewport: {
-    isMobile: boolean;
-    isLandscape: boolean;
-  };
-  editor: {
-    isMobile: boolean;
-    canFitSidebar: boolean;
-  };
-  isTouchScreen: boolean;
-}>;
-
 export type FrameNameBounds = {
   x: number;
   y: number;

+ 2 - 6
packages/excalidraw/wysiwyg/textWysiwyg.test.tsx

@@ -254,9 +254,7 @@ describe("textWysiwyg", () => {
     beforeEach(async () => {
       await render(<Excalidraw handleKeyboardGlobally={true} />);
       // @ts-ignore
-      h.app.refreshViewportBreakpoints();
-      // @ts-ignore
-      h.app.refreshEditorBreakpoints();
+      h.app.refreshEditorInterface();
 
       API.setElements([]);
     });
@@ -363,9 +361,7 @@ describe("textWysiwyg", () => {
     beforeEach(async () => {
       await render(<Excalidraw handleKeyboardGlobally={true} />);
       // @ts-ignore
-      h.app.refreshViewportBreakpoints();
-      // @ts-ignore
-      h.app.refreshEditorBreakpoints();
+      h.app.refreshEditorInterface();
 
       textElement = UI.createElement("text");
 

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

@@ -104,7 +104,6 @@ exports[`exportToSvg > with default arguments 1`] = `
     "open": false,
     "panels": 3,
   },
-  "stylesPanelMode": "full",
   "suggestedBindings": [],
   "theme": "light",
   "toast": null,