diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/children-components/footer.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/children-components/footer.mdx
index e7852cee94..39e8e18425 100644
--- a/dev-docs/docs/@excalidraw/excalidraw/api/children-components/footer.mdx
+++ b/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 (
-
+
{
- const device = useDevice();
- if (device.editor.isMobile) {
+ const editorInterface = useEditorInterface();
+ if (editorInterface.formFactor === "phone") {
return (
alert("This is custom footer in mobile menu")}
>
custom footer
diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md b/dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md
index 69bd88a838..ddbc7194d5 100644
--- a/dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md
+++ b/dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md
@@ -292,7 +292,7 @@ viewportCoordsToSceneCoords({ clientX: number, clientY: number },  
appState: AppState ): {x: number, y: number}
-### 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 (
);
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
diff --git a/examples/with-script-in-browser/components/MobileFooter.tsx b/examples/with-script-in-browser/components/MobileFooter.tsx
index a6e1fa1b25..94b54c3daa 100644
--- a/examples/with-script-in-browser/components/MobileFooter.tsx
+++ b/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 (
{
const [langCode, setLangCode] = useAppLangCode();
+ const editorInterface = useEditorInterface();
+
// initial state
// ---------------------------------------------------------------------------
@@ -856,6 +859,7 @@ const ExcalidrawWrapper = () => {
onSelect={() =>
setShareDialogState({ isOpen: true, type: "share" })
}
+ editorInterface={editorInterface}
/>
);
diff --git a/excalidraw-app/tests/MobileMenu.test.tsx b/excalidraw-app/tests/MobileMenu.test.tsx
index 400b625ec2..70f2162f9b 100644
--- a/excalidraw-app/tests/MobileMenu.test.tsx
+++ b/excalidraw-app/tests/MobileMenu.test.tsx
@@ -17,30 +17,15 @@ describe("Test MobileMenu", () => {
beforeEach(async () => {
await render(
);
- // @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 () => {
diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts
index dfbb69aa97..b6a451d988 100644
--- a/packages/common/src/constants.ts
+++ b/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];
diff --git a/packages/common/src/editorInterface.ts b/packages/common/src/editorInterface.ts
new file mode 100644
index 0000000000..36b55d9182
--- /dev/null
+++ b/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;
+};
diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts
index 79f243f4f0..b4213df455 100644
--- a/packages/common/src/index.ts
+++ b/packages/common/src/index.ts
@@ -10,3 +10,4 @@ export * from "./random";
export * from "./url";
export * from "./utils";
export * from "./emitter";
+export * from "./editorInterface";
diff --git a/packages/common/src/keys.ts b/packages/common/src/keys.ts
index 948e7f568f..1db356b7ec 100644
--- a/packages/common/src/keys.ts
+++ b/packages/common/src/keys.ts
@@ -1,4 +1,4 @@
-import { isDarwin } from "./constants";
+import { isDarwin } from "./editorInterface";
import type { ValueOf } from "./utility-types";
diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts
index 7bf73c6581..69e854b0b0 100644
--- a/packages/common/src/utils.ts
+++ b/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 =
(
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;
-};
diff --git a/packages/element/src/resizeTest.ts b/packages/element/src/resizeTest.ts
index 411dcf9a7b..4257d4a7e8 100644
--- a/packages/element/src/resizeTest.ts
+++ b/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 = (
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 = (
zoom,
elementsMap,
pointerType,
- getOmitSidesForDevice(device),
+ getOmitSidesForEditorInterface(editorInterface),
);
if (
@@ -86,7 +89,7 @@ export const resizeTest = (
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;
diff --git a/packages/element/src/transformHandles.ts b/packages/element/src/transformHandles.ts
index b311e3af83..4a9e5f167c 100644
--- a/packages/element/src/transformHandles.ts
+++ b/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;
};
diff --git a/packages/excalidraw/actions/actionCanvas.tsx b/packages/excalidraw/actions/actionCanvas.tsx
index b0760fd8ba..b8f837b402 100644
--- a/packages/excalidraw/actions/actionCanvas.tsx
+++ b/packages/excalidraw/actions/actionCanvas.tsx
@@ -83,7 +83,6 @@ export const actionChangeViewBackgroundColor = register({
elements={elements}
appState={appState}
updateData={updateData}
- compactMode={appState.stylesPanelMode === "compact"}
/>
);
},
diff --git a/packages/excalidraw/actions/actionDeleteSelected.tsx b/packages/excalidraw/actions/actionDeleteSelected.tsx
index 694f02b90c..cfc5e69e21 100644
--- a/packages/excalidraw/actions/actionDeleteSelected.tsx
+++ b/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 }) => (
- 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 (
+ updateData(null)}
+ disabled={
+ !isSomeElementSelected(getNonDeletedElements(elements), appState)
+ }
+ style={{
+ ...(isMobile && appState.openPopup !== "compactOtherProperties"
+ ? MOBILE_ACTION_BUTTON_BG
+ : {}),
+ }}
+ />
+ );
+ },
});
diff --git a/packages/excalidraw/actions/actionDuplicateSelection.tsx b/packages/excalidraw/actions/actionDuplicateSelection.tsx
index 69508a0228..462803d205 100644
--- a/packages/excalidraw/actions/actionDuplicateSelection.tsx
+++ b/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 }) => (
- 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 (
+ updateData(null)}
+ disabled={
+ !isSomeElementSelected(getNonDeletedElements(elements), appState)
+ }
+ style={{
+ ...(isMobile && appState.openPopup !== "compactOtherProperties"
+ ? MOBILE_ACTION_BUTTON_BG
+ : {}),
+ }}
+ />
+ );
+ },
});
diff --git a/packages/excalidraw/actions/actionExport.tsx b/packages/excalidraw/actions/actionExport.tsx
index 908e2463e9..cf7a58a98a 100644
--- a/packages/excalidraw/actions/actionExport.tsx
+++ b/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"
diff --git a/packages/excalidraw/actions/actionHistory.tsx b/packages/excalidraw/actions/actionHistory.tsx
index a1971f527c..0232bb33e3 100644
--- a/packages/excalidraw/actions/actionHistory.tsx
+++ b/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(
history.onHistoryChangedEmitter,
new HistoryChangedEvent(
@@ -81,6 +83,7 @@ export const createUndoAction: ActionCreator = (history) => ({
history.isRedoStackEmpty,
),
);
+ const isMobile = useStylesPanelMode() === "mobile";
return (
({
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 (
({
disabled={isRedoStackEmpty}
data-testid="button-redo"
style={{
- ...(appState.stylesPanelMode === "mobile"
- ? MOBILE_ACTION_BUTTON_BG
- : {}),
+ ...(isMobile ? MOBILE_ACTION_BUTTON_BG : {}),
}}
/>
);
diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx
index 360220b1e2..c356456ac1 100644
--- a/packages/excalidraw/actions/actionProperties.tsx
+++ b/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" && (
- {t("labels.stroke")}
- )}
- element.strokeColor,
- true,
- (hasSelection) =>
- !hasSelection ? appState.currentItemStrokeColor : null,
+ PanelComponent: ({ elements, appState, updateData, app, data }) => {
+ const { stylesPanelMode } = getStylesPanelInfo(app);
+
+ return (
+ <>
+ {stylesPanelMode === "full" && (
+ {t("labels.stroke")}
)}
- onChange={(color) => updateData({ currentItemStrokeColor: color })}
- elements={elements}
- appState={appState}
- updateData={updateData}
- compactMode={
- appState.stylesPanelMode === "compact" ||
- appState.stylesPanelMode === "mobile"
- }
- />
- >
- ),
+ 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" && (
- {t("labels.background")}
- )}
- element.backgroundColor,
- true,
- (hasSelection) =>
- !hasSelection ? appState.currentItemBackgroundColor : null,
+ PanelComponent: ({ elements, appState, updateData, app, data }) => {
+ const { stylesPanelMode } = getStylesPanelInfo(app);
+
+ return (
+ <>
+ {stylesPanelMode === "full" && (
+ {t("labels.background")}
)}
- onChange={(color) => updateData({ currentItemBackgroundColor: color })}
- elements={elements}
- appState={appState}
- updateData={updateData}
- compactMode={
- appState.stylesPanelMode === "compact" ||
- appState.stylesPanelMode === "mobile"
- }
- />
- >
- ),
+ 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 }) => (
-
- {t("labels.fontSize")}
-
-
{
- if (isTextElement(element)) {
- return element.fontSize;
- }
- const boundTextElement = getBoundTextElement(
- element,
- app.scene.getNonDeletedElementsMap(),
+ PanelComponent: ({ elements, appState, updateData, app, data }) => {
+ const { isCompact } = getStylesPanelInfo(app);
+
+ return (
+
+ {t("labels.fontSize")}
+
+ {
+ 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,
- );
- }}
- />
-
-
- ),
+ }}
+ />
+
+
+ );
+ },
});
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({});
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" && (
{t("labels.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 (
@@ -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 (
@@ -1418,8 +1430,7 @@ export const actionChangeVerticalAlign = register({
onChange={(value) => {
withCaretPositionPreservation(
() => updateData(value),
- appState.stylesPanelMode === "compact" ||
- appState.stylesPanelMode === "mobile",
+ isCompact,
!!appState.editingTextElement,
data?.onPreventClose,
);
diff --git a/packages/excalidraw/actions/manager.tsx b/packages/excalidraw/actions/manager.tsx
index f3314bf35e..adac253e22 100644
--- a/packages/excalidraw/actions/manager.tsx
+++ b/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"
+ })`,
);
}
}
diff --git a/packages/excalidraw/appState.ts b/packages/excalidraw/appState.ts
index 96876e5854..7ec58fec12 100644
--- a/packages/excalidraw/appState.ts
+++ b/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 = <
diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx
index 48ec4dc9a2..c5b8d6ae5f 100644
--- a/packages/excalidraw/components/Actions.tsx
+++ b/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 = ({
{t("labels.actions")}
- {!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 (
{t("toolBar.laser")}
- {app.state.stylesPanelMode === "full" && (
+ {isFullStylesPanel && (
app.setActiveTool({ type: "lasso" })}
icon={LassoIcon}
diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx
index 470c72081e..db90022d2b 100644
--- a/packages/excalidraw/components/App.tsx
+++ b/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(null!);
const AppPropsContext = React.createContext(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(deviceContextInitialValue);
-DeviceContext.displayName = "DeviceContext";
+const EditorInterfaceContext = React.createContext(
+ 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(DeviceContext);
+export const useEditorInterface = () =>
+ useContext(EditorInterfaceContext);
+export const useStylesPanelMode = () =>
+ deriveStylesPanelMode(useEditorInterface());
export const useExcalidrawContainer = () =>
useContext(ExcalidrawContainerContext);
export const useExcalidrawElements = () =>
@@ -577,7 +581,10 @@ class App extends React.Component {
rc: RoughCanvas;
unmounted: boolean = false;
actionManager: ActionManager;
- device: Device = deviceContextInitialValue;
+ editorInterface: EditorInterface = editorInterfaceContextInitialValue;
+ private stylesPanelMode: StylesPanelMode = deriveStylesPanelMode(
+ editorInterfaceContextInitialValue,
+ );
private excalidrawContainerRef = React.createRef();
@@ -693,6 +700,9 @@ class App extends React.Component {
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 {
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 {
"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 {
-
+
{
renderScrollbars={
this.props.renderScrollbars === true
}
- device={this.device}
+ editorInterface={this.editorInterface}
renderInteractiveSceneCallback={
this.renderInteractiveSceneCallback
}
@@ -1853,7 +1864,7 @@ class App extends React.Component {
-
+
@@ -2370,7 +2381,8 @@ class App extends React.Component {
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,44 +2442,14 @@ class App extends React.Component {
}
};
- 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 = () => {
- const container = this.excalidrawContainerRef.current;
- if (!container) {
- return;
- }
-
- const { width: editorWidth, height: editorHeight } =
- container.getBoundingClientRect();
-
- const prevViewportState = this.device.viewport;
-
- const nextViewportState = updateObject(prevViewportState, {
- isLandscape: editorWidth > editorHeight,
- isMobile: this.isMobileBreakpoint(editorWidth, editorHeight),
- });
-
- if (prevViewportState !== nextViewportState) {
- this.device = { ...this.device, viewport: nextViewportState };
- return true;
- }
- return false;
- };
-
- private refreshEditorBreakpoints = () => {
+ public refreshEditorInterface = () => {
const container = this.excalidrawContainerRef.current;
if (!container) {
return;
@@ -2476,47 +2458,56 @@ class App extends React.Component {
const { width: editorWidth, height: editorHeight } =
container.getBoundingClientRect();
+ 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 prevEditorState = this.device.editor;
-
- const nextEditorState = updateObject(prevEditorState, {
- isMobile: this.isMobileBreakpoint(editorWidth, editorHeight),
+ 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,
});
- 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";
+ this.editorInterface = nextEditorInterface;
+ this.reconcileStylesPanelMode(nextEditorInterface);
+ };
- // 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;
+ private reconcileStylesPanelMode = (nextEditorInterface: EditorInterface) => {
+ const nextStylesPanelMode = deriveStylesPanelMode(nextEditorInterface);
+ if (nextStylesPanelMode === this.stylesPanelMode) {
+ return;
}
- return false;
+
+ const prevStylesPanelMode = this.stylesPanelMode;
+ this.stylesPanelMode = nextStylesPanelMode;
+
+ if (prevStylesPanelMode !== "full" && nextStylesPanelMode === "full") {
+ this.setState((prevState) => ({
+ preferredSelectionTool: {
+ type: "selection",
+ initialized: true,
+ },
+ }));
+ }
+ };
+
+ /** TO BE USED LATER */
+ private setDesktopUIMode = (mode: EditorInterface["desktopUIMode"]) => {
+ const nextMode = setDesktopUIMode(mode);
+ this.editorInterface = updateObject(this.editorInterface, {
+ desktopUIMode: nextMode,
+ });
+ this.reconcileStylesPanelMode(this.editorInterface);
};
private clearImageShapeCache(filesMap?: BinaryFiles) {
@@ -2588,19 +2579,9 @@ class App extends React.Component {
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 {
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 {
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 {
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 {
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 {
// 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 {
!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 {
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 {
}
if (appState) {
- this.setState({
- ...appState,
- // keep existing stylesPanelMode as it needs to be preserved
- // or set at startup
- stylesPanelMode: this.state.stylesPanelMode,
- } as Pick | null);
+ this.setState(appState as Pick | null);
}
if (elements) {
@@ -4594,7 +4564,9 @@ class App extends React.Component {
"toolbar",
shape,
`keyboard (${
- this.device.editor.isMobile ? "mobile" : "desktop"
+ this.editorInterface.formFactor === "phone"
+ ? "mobile"
+ : "desktop"
})`,
);
}
@@ -5100,7 +5072,7 @@ class App extends React.Component {
// 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 {
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 {
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 {
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 {
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 {
// 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 {
this.state.zoom,
event.pointerType,
this.scene.getNonDeletedElementsMap(),
- this.device,
+ this.editorInterface,
);
if (
elementWithTransformHandleType &&
@@ -6207,7 +6180,7 @@ class App extends React.Component {
scenePointerY,
this.state.zoom,
event.pointerType,
- this.device,
+ this.editorInterface,
);
if (transformHandleType) {
setCursor(
@@ -6593,10 +6566,12 @@ class App extends React.Component {
}
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 {
// 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 {
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 {
}
}
- 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 {
) {
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 {
!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 {
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 {
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 {
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 {
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 {
}
const zIndexActions: ContextMenuItems =
- this.state.stylesPanelMode === "full"
+ this.editorInterface.formFactor === "desktop"
? [
CONTEXT_MENU_SEPARATOR,
actionSendBackward,
diff --git a/packages/excalidraw/components/ColorPicker/ColorInput.tsx b/packages/excalidraw/components/ColorPicker/ColorInput.tsx
index 557f9c1c00..7de0af41e4 100644
--- a/packages/excalidraw/components/ColorPicker/ColorInput.tsx
+++ b/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" && (
<>
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}
@@ -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}
>
{!color && slashIcon}
- {(stylesPanelMode === "compact" || stylesPanelMode === "mobile") &&
- color &&
- mode === "stroke" && (
-
-
- {strokeIcon}
-
-
- )}
+ {isCompactMode && color && mode === "stroke" && (
+
+
+ {strokeIcon}
+
+
+ )}
);
};
@@ -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 (
@@ -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 && (
)}
- {!compactMode &&
}
+ {!isCompactMode &&
}
{
@@ -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={() => {
diff --git a/packages/excalidraw/components/CommandPalette/CommandPalette.tsx b/packages/excalidraw/components/CommandPalette/CommandPalette.tsx
index 2ef757d5c5..3e922328b8 100644
--- a/packages/excalidraw/components/CommandPalette/CommandPalette.tsx
+++ b/packages/excalidraw/components/CommandPalette/CommandPalette.tsx
@@ -903,7 +903,7 @@ function CommandPaletteInner({
ref={inputRef}
/>
- {!app.device.viewport.isMobile && (
+ {app.editorInterface.formFactor !== "phone" && (
{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}
/>
@@ -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"}
/>
diff --git a/packages/excalidraw/components/Dialog.tsx b/packages/excalidraw/components/Dialog.tsx
index 00ae2be0cb..55109d07f3 100644
--- a/packages/excalidraw/components/Dialog.tsx
+++ b/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();
const [lastActiveElement] = useState(document.activeElement);
const { id } = useExcalidrawContainer();
- const isFullscreen = useDevice().viewport.isMobile;
+ const isFullscreen = useEditorInterface().formFactor === "phone";
useEffect(() => {
if (!islandNode) {
diff --git a/packages/excalidraw/components/FontPicker/FontPickerList.tsx b/packages/excalidraw/components/FontPicker/FontPickerList.tsx
index ff59f05ece..a6202f0227 100644
--- a/packages/excalidraw/components/FontPicker/FontPickerList.tsx
+++ b/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(null);
@@ -338,7 +344,7 @@ export const FontPickerList = React.memo(
onKeyDown={onKeyDown}
preventAutoFocusOnTouch={!!app.state.editingTextElement}
>
- {app.state.stylesPanelMode === "full" && (
+ {stylesPanelMode === "full" && (
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,
});
diff --git a/packages/excalidraw/components/IconPicker.tsx b/packages/excalidraw/components/IconPicker.tsx
index 13c69cf8fd..fab4f109b8 100644
--- a/packages/excalidraw/components/IconPicker.tsx
+++ b/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({
onClose: () => void;
numberOfOptionsToAlwaysShow?: number;
}) {
- const device = useDevice();
+ const editorInterface = useEditorInterface();
const { container } = useExcalidrawContainer();
const handleKeyDown = (event: React.KeyboardEvent) => {
@@ -153,7 +153,7 @@ function Picker({
);
};
- const isMobile = device.editor.isMobile;
+ const isMobile = editorInterface.formFactor === "phone";
return (
{
- 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 (
{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,
})}
>
{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")}
@@ -496,7 +502,7 @@ const LayerUI = ({
{appState.errorMessage}
)}
- {eyeDropperState && !device.editor.isMobile && (
+ {eyeDropperState && editorInterface.formFactor !== "phone" && (
{
@@ -575,7 +581,7 @@ const LayerUI = ({
}
/>
)}
- {device.editor.isMobile && (
+ {editorInterface.formFactor === "phone" && (
)}
- {!device.editor.isMobile && (
+ {editorInterface.formFactor !== "phone" && (
<>
void;
}) {
- const device = useDevice();
+ const editorInterface = useEditorInterface();
const libraryContainerRef = useRef
(null);
const scrollPosition = useScrollPosition(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}
diff --git a/packages/excalidraw/components/LibraryUnit.tsx b/packages/excalidraw/components/LibraryUnit.tsx
index 36607910e5..7d6a599526 100644
--- a/packages/excalidraw/components/LibraryUnit.tsx
+++ b/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 && (
{PlusIcon}
);
diff --git a/packages/excalidraw/components/PropertiesPopover.tsx b/packages/excalidraw/components/PropertiesPopover.tsx
index ccedd87a02..151d8eff16 100644
--- a/packages/excalidraw/components/PropertiesPopover.tsx
+++ b/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 (
@@ -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();
}
}}
diff --git a/packages/excalidraw/components/Sidebar/Sidebar.tsx b/packages/excalidraw/components/Sidebar/Sidebar.tsx
index 5f0ca487f2..8226eacef5 100644
--- a/packages/excalidraw/components/Sidebar/Sidebar.tsx
+++ b/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 (
{
- const device = useDevice();
+ const editorInterface = useEditorInterface();
const props = useContext(SidebarPropsContext);
const renderDockButton = !!(
- device.editor.canFitSidebar && props.shouldRenderDockButton
+ editorInterface.canFitSidebar && props.shouldRenderDockButton
);
return (
diff --git a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx
index c375a2b168..1bbf3789c6 100644
--- a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx
+++ b/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(),
diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx
index 5bbb41763b..28f7c78fc0 100644
--- a/packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx
+++ b/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(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 ? (
-
- {children}
-
+ {editorInterface.formFactor === "phone" ? (
+ {children}
) : (
{
- const device = useDevice();
+ const editorInterface = useEditorInterface();
return (
<>
{icon && {icon}
}
{children}
- {shortcut && !device.editor.isMobile && (
+ {shortcut && editorInterface.formFactor !== "phone" && (
{shortcut}
)}
>
diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.tsx
index 14bfe1a904..d8177c50e0 100644
--- a/packages/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.tsx
+++ b/packages/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.tsx
@@ -1,4 +1,4 @@
-import { useDevice } from "../App";
+import { useEditorInterface } from "../App";
import { RadioGroup } from "../RadioGroup";
type Props = {
@@ -22,7 +22,7 @@ const DropdownMenuItemContentRadio = ({
children,
name,
}: Props) => {
- const device = useDevice();
+ const editorInterface = useEditorInterface();
return (
<>
@@ -37,7 +37,7 @@ const DropdownMenuItemContentRadio = ({
choices={choices}
/>
- {shortcut && !device.editor.isMobile && (
+ {shortcut && editorInterface.formFactor !== "phone" && (
{shortcut}
diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenuTrigger.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuTrigger.tsx
index a7301fb447..f43e4493b1 100644
--- a/packages/excalidraw/components/dropdownMenu/DropdownMenuTrigger.tsx
+++ b/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, "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 (
diff --git a/packages/excalidraw/components/hyperlink/Hyperlink.tsx b/packages/excalidraw/components/hyperlink/Hyperlink.tsx
index 5e380e4e6e..8e15bb0d54 100644
--- a/packages/excalidraw/components/hyperlink/Hyperlink.tsx
+++ b/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;
diff --git a/packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.tsx b/packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.tsx
index 0efadcfc62..3a611be04b 100644
--- a/packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.tsx
+++ b/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) => {
const appState = useUIAppState();
const showIconOnly =
- isMobileOrTablet() || appState.width < MQ_MIN_WIDTH_DESKTOP;
+ editorInterface?.formFactor !== "desktop" ||
+ appState.width < MQ_MIN_WIDTH_DESKTOP;
return (
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 && (
-
- {t("labels.collaborators")}
-
-
- )}
+ {editorInterface.formFactor === "phone" &&
+ appState.collaborators.size > 0 && (
+
+ {t("labels.collaborators")}
+
+
+ )}
diff --git a/packages/excalidraw/components/welcome-screen/WelcomeScreen.Center.tsx b/packages/excalidraw/components/welcome-screen/WelcomeScreen.Center.tsx
index 8f548823e9..823384abbc 100644
--- a/packages/excalidraw/components/welcome-screen/WelcomeScreen.Center.tsx
+++ b/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 (
<>
{icon}
{children}
- {shortcut && !device.editor.isMobile && (
+ {shortcut && editorInterface.formFactor !== "phone" && (
{shortcut}
)}
>
diff --git a/packages/excalidraw/hooks/useCreatePortalContainer.ts b/packages/excalidraw/hooks/useCreatePortalContainer.ts
index fb0d24bc3d..c1fe2c9e8f 100644
--- a/packages/excalidraw/hooks/useCreatePortalContainer.ts
+++ b/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(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
diff --git a/packages/excalidraw/index.tsx b/packages/excalidraw/index.tsx
index 1d599a98ec..a215787381 100644
--- a/packages/excalidraw/index.tsx
+++ b/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";
diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts
index 9b836aecf8..b948aa8c38 100644
--- a/packages/excalidraw/renderer/interactiveScene.ts
+++ b/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(
diff --git a/packages/excalidraw/scene/types.ts b/packages/excalidraw/scene/types.ts
index 12a5e27a8e..01fae229c8 100644
--- a/packages/excalidraw/scene/types.ts
+++ b/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;
};
diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap
index 6f4f6fd559..e4ef367a8a 100644
--- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap
+++ b/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,
diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap
index d436af1375..37037e7822 100644
--- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap
+++ b/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,
diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap
index a33ca9c963..43ca509d84 100644
--- a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap
+++ b/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,
diff --git a/packages/excalidraw/tests/regressionTests.test.tsx b/packages/excalidraw/tests/regressionTests.test.tsx
index d4b5185bac..2ee2167914 100644
--- a/packages/excalidraw/tests/regressionTests.test.tsx
+++ b/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( );
- API.setAppState({ height: 768, width: 1024 });
+ API.setAppState({ height: 768, width: MQ_MIN_WIDTH_DESKTOP });
});
afterEach(() => {
diff --git a/packages/excalidraw/tests/test-utils.ts b/packages/excalidraw/tests/test-utils.ts
index 78e11d1821..7289a59820 100644
--- a/packages/excalidraw/tests/test-utils.ts
+++ b/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();
});
};
diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts
index 2c81de4ef7..c7857382cb 100644
--- a/packages/excalidraw/types.ts
+++ b/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["setCursor"];
resetCursor: InstanceType["resetCursor"];
toggleSidebar: InstanceType["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;
diff --git a/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx b/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx
index b03fab7391..e4d290a92f 100644
--- a/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx
+++ b/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx
@@ -254,9 +254,7 @@ describe("textWysiwyg", () => {
beforeEach(async () => {
await render( );
// @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( );
// @ts-ignore
- h.app.refreshViewportBreakpoints();
- // @ts-ignore
- h.app.refreshEditorBreakpoints();
+ h.app.refreshEditorInterface();
textElement = UI.createElement("text");
diff --git a/packages/utils/tests/__snapshots__/export.test.ts.snap b/packages/utils/tests/__snapshots__/export.test.ts.snap
index 1f799501c9..d3bbff7af7 100644
--- a/packages/utils/tests/__snapshots__/export.test.ts.snap
+++ b/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,