refactor device to editor interface and derive styles panel

This commit is contained in:
Ryan Di
2025-10-15 01:58:09 +11:00
parent 8608d7b2e0
commit 1225fcc339
49 changed files with 704 additions and 680 deletions

View File

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

View File

@@ -292,7 +292,7 @@ viewportCoordsToSceneCoords(&#123; clientX: number, clientY: number },<br/>&nbsp
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a><br/>): &#123;x: number, y: number} appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a><br/>): &#123;x: number, y: number}
</pre> </pre>
### useDevice ### useEditorInterface
This hook can be used to check the type of device which is being used. It can only be used inside the `children` of `Excalidraw` component. 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 ```jsx live noInline
const MobileFooter = ({}) => { const MobileFooter = ({}) => {
const device = useDevice(); const editorInterface = useEditorInterface();
if (device.editor.isMobile) { if (editorInterface.formFactor === "phone") {
return ( return (
<Footer> <Footer>
<button <button
@@ -336,12 +336,20 @@ render(<App />);
The `device` has the following `attributes`, some grouped into `viewport` and `editor` objects, per context. The `device` has the following `attributes`, some grouped into `viewport` and `editor` objects, per context.
| Name | Type | Description | | 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 | The `EditorInterface` object has the following properties:
| `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 | | Name | Type | Description |
| `isTouchScreen` | `boolean` | Set to `true` for `touch` when touch event detected | | --- | --- | --- | --- | --- | --- |
| `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 ### i18n

View File

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

View File

@@ -18,26 +18,25 @@ describe("Test MobileMenu", () => {
beforeEach(async () => { beforeEach(async () => {
await render(<ExcalidrawApp />); await render(<ExcalidrawApp />);
// @ts-ignore // @ts-ignore
h.app.refreshViewportBreakpoints(); h.app.refreshEditorInterface();
// @ts-ignore
h.app.refreshEditorBreakpoints();
}); });
afterAll(() => { afterAll(() => {
restoreOriginalGetBoundingClientRect(); restoreOriginalGetBoundingClientRect();
}); });
it("should set device correctly", () => { it("should set editor interface correctly", () => {
expect(h.app.device).toMatchInlineSnapshot(` expect(h.app.editorInterface).toMatchInlineSnapshot(`
{ {
"editor": { "canFitSidebar": false,
"canFitSidebar": false, "desktopUIMode": "full",
"isMobile": true, "formFactor": "phone",
}, "isLandscape": true,
"isTouchScreen": false, "isTouchScreen": false,
"viewport": { "userAgent": {
"isLandscape": true, "isMobileDevice": false,
"isMobile": true, "platform": "other",
"raw": "Mozilla/5.0 (darwin) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/22.1.0",
}, },
} }
`); `);

View File

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

View File

@@ -1,15 +1,11 @@
import { import { DEFAULT_TRANSFORM_HANDLE_SPACING } from "@excalidraw/common";
DEFAULT_TRANSFORM_HANDLE_SPACING,
isAndroid,
isIOS,
} from "@excalidraw/common";
import { pointFrom, pointRotateRads } from "@excalidraw/math"; import { pointFrom, pointRotateRads } from "@excalidraw/math";
import type { Radians } from "@excalidraw/math"; import type { Radians } from "@excalidraw/math";
import type { import type {
Device, EditorInterface,
InteractiveCanvasAppState, InteractiveCanvasAppState,
Zoom, Zoom,
} from "@excalidraw/excalidraw/types"; } from "@excalidraw/excalidraw/types";
@@ -111,20 +107,21 @@ const generateTransformHandle = (
return [xx - width / 2, yy - height / 2, width, height]; return [xx - width / 2, yy - height / 2, width, height];
}; };
export const canResizeFromSides = (device: Device) => { export const canResizeFromSides = (editorInterface: EditorInterface) => {
if (device.viewport.isMobile) { if (
return false; editorInterface.formFactor === "phone" &&
} editorInterface.userAgent.isMobileDevice
) {
if (device.isTouchScreen && (isAndroid || isIOS)) {
return false; return false;
} }
return true; return true;
}; };
export const getOmitSidesForDevice = (device: Device) => { export const getOmitSidesForEditorInterface = (
if (canResizeFromSides(device)) { editorInterface: EditorInterface,
) => {
if (canResizeFromSides(editorInterface)) {
return DEFAULT_OMIT_SIDES; return DEFAULT_OMIT_SIDES;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -81,9 +81,6 @@ import { RadioSelection } from "../components/RadioSelection";
import { ColorPicker } from "../components/ColorPicker/ColorPicker"; import { ColorPicker } from "../components/ColorPicker/ColorPicker";
import { FontPicker } from "../components/FontPicker/FontPicker"; import { FontPicker } from "../components/FontPicker/FontPicker";
import { IconPicker } from "../components/IconPicker"; import { IconPicker } from "../components/IconPicker";
// TODO barnabasmolnar/editor-redesign
// TextAlignTopIcon, TextAlignBottomIcon,TextAlignMiddleIcon,
// ArrowHead icons
import { Range } from "../components/Range"; import { Range } from "../components/Range";
import { import {
ArrowheadArrowIcon, ArrowheadArrowIcon,
@@ -142,12 +139,23 @@ import {
restoreCaretPosition, restoreCaretPosition,
} from "../hooks/useTextEditorFocus"; } from "../hooks/useTextEditorFocus";
import { deriveStylesPanelMode } from "../editorInterface";
import { register } from "./register"; import { register } from "./register";
import type { AppClassProperties, AppState, Primitive } from "../types"; import type { AppClassProperties, AppState, Primitive } from "../types";
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1; 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 = ( export const changeProperty = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
@@ -326,35 +334,35 @@ export const actionChangeStrokeColor = register({
: CaptureUpdateAction.EVENTUALLY, : CaptureUpdateAction.EVENTUALLY,
}; };
}, },
PanelComponent: ({ elements, appState, updateData, app, data }) => ( PanelComponent: ({ elements, appState, updateData, app, data }) => {
<> const { stylesPanelMode } = getStylesPanelInfo(app);
{appState.stylesPanelMode === "full" && (
<h3 aria-hidden="true">{t("labels.stroke")}</h3> return (
)} <>
<ColorPicker {stylesPanelMode === "full" && (
topPicks={DEFAULT_ELEMENT_STROKE_PICKS} <h3 aria-hidden="true">{t("labels.stroke")}</h3>
palette={DEFAULT_ELEMENT_STROKE_COLOR_PALETTE}
type="elementStroke"
label={t("labels.stroke")}
color={getFormValue(
elements,
app,
(element) => element.strokeColor,
true,
(hasSelection) =>
!hasSelection ? appState.currentItemStrokeColor : null,
)} )}
onChange={(color) => updateData({ currentItemStrokeColor: color })} <ColorPicker
elements={elements} topPicks={DEFAULT_ELEMENT_STROKE_PICKS}
appState={appState} palette={DEFAULT_ELEMENT_STROKE_COLOR_PALETTE}
updateData={updateData} type="elementStroke"
compactMode={ label={t("labels.stroke")}
appState.stylesPanelMode === "compact" || color={getFormValue(
appState.stylesPanelMode === "mobile" elements,
} app,
/> (element) => element.strokeColor,
</> true,
), (hasSelection) =>
!hasSelection ? appState.currentItemStrokeColor : null,
)}
onChange={(color) => updateData({ currentItemStrokeColor: color })}
elements={elements}
appState={appState}
updateData={updateData}
/>
</>
);
},
}); });
export const actionChangeBackgroundColor = register({ export const actionChangeBackgroundColor = register({
@@ -409,35 +417,37 @@ export const actionChangeBackgroundColor = register({
captureUpdate: CaptureUpdateAction.IMMEDIATELY, captureUpdate: CaptureUpdateAction.IMMEDIATELY,
}; };
}, },
PanelComponent: ({ elements, appState, updateData, app, data }) => ( PanelComponent: ({ elements, appState, updateData, app, data }) => {
<> const { stylesPanelMode } = getStylesPanelInfo(app);
{appState.stylesPanelMode === "full" && (
<h3 aria-hidden="true">{t("labels.background")}</h3> return (
)} <>
<ColorPicker {stylesPanelMode === "full" && (
topPicks={DEFAULT_ELEMENT_BACKGROUND_PICKS} <h3 aria-hidden="true">{t("labels.background")}</h3>
palette={DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE}
type="elementBackground"
label={t("labels.background")}
color={getFormValue(
elements,
app,
(element) => element.backgroundColor,
true,
(hasSelection) =>
!hasSelection ? appState.currentItemBackgroundColor : null,
)} )}
onChange={(color) => updateData({ currentItemBackgroundColor: color })} <ColorPicker
elements={elements} topPicks={DEFAULT_ELEMENT_BACKGROUND_PICKS}
appState={appState} palette={DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE}
updateData={updateData} type="elementBackground"
compactMode={ label={t("labels.background")}
appState.stylesPanelMode === "compact" || color={getFormValue(
appState.stylesPanelMode === "mobile" elements,
} app,
/> (element) => element.backgroundColor,
</> true,
), (hasSelection) =>
!hasSelection ? appState.currentItemBackgroundColor : null,
)}
onChange={(color) =>
updateData({ currentItemBackgroundColor: color })
}
elements={elements}
appState={appState}
updateData={updateData}
/>
</>
);
},
}); });
export const actionChangeFillStyle = register({ export const actionChangeFillStyle = register({
@@ -448,7 +458,9 @@ export const actionChangeFillStyle = register({
trackEvent( trackEvent(
"element", "element",
"changeFillStyle", "changeFillStyle",
`${value} (${app.device.editor.isMobile ? "mobile" : "desktop"})`, `${value} (${
app.editorInterface.formFactor === "phone" ? "mobile" : "desktop"
})`,
); );
return { return {
elements: changeProperty(elements, appState, (el) => elements: changeProperty(elements, appState, (el) =>
@@ -714,78 +726,81 @@ export const actionChangeFontSize = register({
perform: (elements, appState, value, app) => { perform: (elements, appState, value, app) => {
return changeFontSize(elements, appState, app, () => value, value); return changeFontSize(elements, appState, app, () => value, value);
}, },
PanelComponent: ({ elements, appState, updateData, app, data }) => ( PanelComponent: ({ elements, appState, updateData, app, data }) => {
<fieldset> const { isCompact } = getStylesPanelInfo(app);
<legend>{t("labels.fontSize")}</legend>
<div className="buttonList"> return (
<RadioSelection <fieldset>
group="font-size" <legend>{t("labels.fontSize")}</legend>
options={[ <div className="buttonList">
{ <RadioSelection
value: 16, group="font-size"
text: t("labels.small"), options={[
icon: FontSizeSmallIcon, {
testId: "fontSize-small", value: 16,
}, text: t("labels.small"),
{ icon: FontSizeSmallIcon,
value: 20, testId: "fontSize-small",
text: t("labels.medium"), },
icon: FontSizeMediumIcon, {
testId: "fontSize-medium", value: 20,
}, text: t("labels.medium"),
{ icon: FontSizeMediumIcon,
value: 28, testId: "fontSize-medium",
text: t("labels.large"), },
icon: FontSizeLargeIcon, {
testId: "fontSize-large", value: 28,
}, text: t("labels.large"),
{ icon: FontSizeLargeIcon,
value: 36, testId: "fontSize-large",
text: t("labels.veryLarge"), },
icon: FontSizeExtraLargeIcon, {
testId: "fontSize-veryLarge", value: 36,
}, text: t("labels.veryLarge"),
]} icon: FontSizeExtraLargeIcon,
value={getFormValue( testId: "fontSize-veryLarge",
elements, },
app, ]}
(element) => { value={getFormValue(
if (isTextElement(element)) { elements,
return element.fontSize; app,
} (element) => {
const boundTextElement = getBoundTextElement( if (isTextElement(element)) {
element, return element.fontSize;
app.scene.getNonDeletedElementsMap(), }
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; />
} </div>
return null; </fieldset>
}, );
(element) => },
isTextElement(element) ||
getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap(),
) !== null,
(hasSelection) =>
hasSelection
? null
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
)}
onChange={(value) => {
withCaretPositionPreservation(
() => updateData(value),
appState.stylesPanelMode === "compact" ||
appState.stylesPanelMode === "mobile",
!!appState.editingTextElement,
data?.onPreventClose,
);
}}
/>
</div>
</fieldset>
),
}); });
export const actionDecreaseFontSize = register({ export const actionDecreaseFontSize = register({
@@ -1047,6 +1062,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 // relying on state batching as multiple `FontPicker` handlers could be called in rapid succession and we want to combine them
const [batchedData, setBatchedData] = useState<ChangeFontFamilyData>({}); const [batchedData, setBatchedData] = useState<ChangeFontFamilyData>({});
const isUnmounted = useRef(true); const isUnmounted = useRef(true);
const { stylesPanelMode, isCompact } = getStylesPanelInfo(app);
const selectedFontFamily = useMemo(() => { const selectedFontFamily = useMemo(() => {
const getFontFamily = ( const getFontFamily = (
@@ -1119,14 +1135,14 @@ export const actionChangeFontFamily = register({
return ( return (
<> <>
{appState.stylesPanelMode === "full" && ( {stylesPanelMode === "full" && (
<legend>{t("labels.fontFamily")}</legend> <legend>{t("labels.fontFamily")}</legend>
)} )}
<FontPicker <FontPicker
isOpened={appState.openPopup === "fontFamily"} isOpened={appState.openPopup === "fontFamily"}
selectedFontFamily={selectedFontFamily} selectedFontFamily={selectedFontFamily}
hoveredFontFamily={appState.currentHoveredFontFamily} hoveredFontFamily={appState.currentHoveredFontFamily}
compactMode={appState.stylesPanelMode !== "full"} compactMode={stylesPanelMode !== "full"}
onSelect={(fontFamily) => { onSelect={(fontFamily) => {
withCaretPositionPreservation( withCaretPositionPreservation(
() => { () => {
@@ -1138,8 +1154,7 @@ export const actionChangeFontFamily = register({
// defensive clear so immediate close won't abuse the cached elements // defensive clear so immediate close won't abuse the cached elements
cachedElementsRef.current.clear(); cachedElementsRef.current.clear();
}, },
appState.stylesPanelMode === "compact" || isCompact,
appState.stylesPanelMode === "mobile",
!!appState.editingTextElement, !!appState.editingTextElement,
); );
}} }}
@@ -1214,11 +1229,7 @@ export const actionChangeFontFamily = register({
cachedElementsRef.current.clear(); cachedElementsRef.current.clear();
// Refocus text editor when font picker closes if we were editing text // Refocus text editor when font picker closes if we were editing text
if ( if (isCompact && appState.editingTextElement) {
(appState.stylesPanelMode === "compact" ||
appState.stylesPanelMode === "mobile") &&
appState.editingTextElement
) {
restoreCaretPosition(null); // Just refocus without saved position restoreCaretPosition(null); // Just refocus without saved position
} }
} }
@@ -1265,6 +1276,7 @@ export const actionChangeTextAlign = register({
}, },
PanelComponent: ({ elements, appState, updateData, app, data }) => { PanelComponent: ({ elements, appState, updateData, app, data }) => {
const elementsMap = app.scene.getNonDeletedElementsMap(); const elementsMap = app.scene.getNonDeletedElementsMap();
const { isCompact } = getStylesPanelInfo(app);
return ( return (
<fieldset> <fieldset>
@@ -1317,8 +1329,7 @@ export const actionChangeTextAlign = register({
onChange={(value) => { onChange={(value) => {
withCaretPositionPreservation( withCaretPositionPreservation(
() => updateData(value), () => updateData(value),
appState.stylesPanelMode === "compact" || isCompact,
appState.stylesPanelMode === "mobile",
!!appState.editingTextElement, !!appState.editingTextElement,
data?.onPreventClose, data?.onPreventClose,
); );
@@ -1365,6 +1376,7 @@ export const actionChangeVerticalAlign = register({
}; };
}, },
PanelComponent: ({ elements, appState, updateData, app, data }) => { PanelComponent: ({ elements, appState, updateData, app, data }) => {
const { isCompact } = getStylesPanelInfo(app);
return ( return (
<fieldset> <fieldset>
<div className="buttonList"> <div className="buttonList">
@@ -1417,8 +1429,7 @@ export const actionChangeVerticalAlign = register({
onChange={(value) => { onChange={(value) => {
withCaretPositionPreservation( withCaretPositionPreservation(
() => updateData(value), () => updateData(value),
appState.stylesPanelMode === "compact" || isCompact,
appState.stylesPanelMode === "mobile",
!!appState.editingTextElement, !!appState.editingTextElement,
data?.onPreventClose, data?.onPreventClose,
); );

View File

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

View File

@@ -127,7 +127,6 @@ export const getDefaultAppState = (): Omit<
searchMatches: null, searchMatches: null,
lockedMultiSelections: {}, lockedMultiSelections: {},
activeLockedId: null, activeLockedId: null,
stylesPanelMode: "full",
}; };
}; };
@@ -253,7 +252,6 @@ const APP_STATE_STORAGE_CONF = (<
searchMatches: { browser: false, export: false, server: false }, searchMatches: { browser: false, export: false, server: false },
lockedMultiSelections: { browser: true, export: true, server: true }, lockedMultiSelections: { browser: true, export: true, server: true },
activeLockedId: { browser: false, export: false, server: false }, activeLockedId: { browser: false, export: false, server: false },
stylesPanelMode: { browser: false, export: false, server: false },
}); });
const _clearAppStateForStorage = < const _clearAppStateForStorage = <

View File

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

View File

@@ -406,6 +406,13 @@ import { LassoTrail } from "../lasso";
import { EraserTrail } from "../eraser"; import { EraserTrail } from "../eraser";
import {
DESKTOP_UI_MODE_STORAGE_KEY,
createUserAgentDescriptor,
deriveFormFactor,
deriveStylesPanelMode,
} from "../editorInterface";
import ConvertElementTypePopup, { import ConvertElementTypePopup, {
getConversionTypeFromElements, getConversionTypeFromElements,
convertElementTypePopupAtom, convertElementTypePopupAtom,
@@ -459,7 +466,8 @@ import type {
LibraryItems, LibraryItems,
PointerDownState, PointerDownState,
SceneData, SceneData,
Device, EditorInterface,
StylesPanelMode,
FrameNameBoundsCache, FrameNameBoundsCache,
SidebarName, SidebarName,
SidebarTabName, SidebarTabName,
@@ -480,19 +488,20 @@ import type { Action, ActionResult } from "../actions/types";
const AppContext = React.createContext<AppClassProperties>(null!); const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!); const AppPropsContext = React.createContext<AppProps>(null!);
const deviceContextInitialValue = { const editorInterfaceContextInitialValue: EditorInterface = {
viewport: { formFactor: "desktop",
isMobile: false, desktopUIMode: "full",
isLandscape: false, userAgent: createUserAgentDescriptor(
}, typeof navigator !== "undefined" ? navigator.userAgent : "",
editor: { ),
isMobile: false,
canFitSidebar: false,
},
isTouchScreen: false, isTouchScreen: false,
canFitSidebar: false,
isLandscape: true,
}; };
const DeviceContext = React.createContext<Device>(deviceContextInitialValue); const EditorInterfaceContext = React.createContext<EditorInterface>(
DeviceContext.displayName = "DeviceContext"; editorInterfaceContextInitialValue,
);
EditorInterfaceContext.displayName = "EditorInterfaceContext";
export const ExcalidrawContainerContext = React.createContext<{ export const ExcalidrawContainerContext = React.createContext<{
container: HTMLDivElement | null; container: HTMLDivElement | null;
@@ -528,7 +537,10 @@ ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext";
export const useApp = () => useContext(AppContext); export const useApp = () => useContext(AppContext);
export const useAppProps = () => useContext(AppPropsContext); export const useAppProps = () => useContext(AppPropsContext);
export const useDevice = () => useContext<Device>(DeviceContext); export const useEditorInterface = () =>
useContext<EditorInterface>(EditorInterfaceContext);
export const useStylesPanelMode = () =>
deriveStylesPanelMode(useEditorInterface());
export const useExcalidrawContainer = () => export const useExcalidrawContainer = () =>
useContext(ExcalidrawContainerContext); useContext(ExcalidrawContainerContext);
export const useExcalidrawElements = () => export const useExcalidrawElements = () =>
@@ -576,7 +588,52 @@ class App extends React.Component<AppProps, AppState> {
rc: RoughCanvas; rc: RoughCanvas;
unmounted: boolean = false; unmounted: boolean = false;
actionManager: ActionManager; actionManager: ActionManager;
device: Device = deviceContextInitialValue; editorInterface: EditorInterface = editorInterfaceContextInitialValue;
private stylesPanelMode: StylesPanelMode = deriveStylesPanelMode(
editorInterfaceContextInitialValue,
);
private 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;
};
private 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)
}
};
public setDesktopUIMode = (mode: EditorInterface["desktopUIMode"]) => {
if (mode !== "compact" && mode !== "full") {
return;
}
if (mode === this.editorInterface.desktopUIMode) {
return;
}
this.editorInterface = updateObject(this.editorInterface, {
desktopUIMode: mode,
});
this.persistDesktopUIMode(mode);
this.reconcileStylesPanelMode(this.editorInterface);
};
private excalidrawContainerRef = React.createRef<HTMLDivElement>(); private excalidrawContainerRef = React.createRef<HTMLDivElement>();
@@ -692,6 +749,16 @@ class App extends React.Component<AppProps, AppState> {
height: window.innerHeight, height: window.innerHeight,
}; };
const storedDesktopUIMode = this.loadDesktopUIModePreference();
const userAgentDescriptor = createUserAgentDescriptor(
typeof navigator !== "undefined" ? navigator.userAgent : "",
);
this.editorInterface = updateObject(this.editorInterface, {
desktopUIMode: storedDesktopUIMode ?? this.editorInterface.desktopUIMode,
userAgent: userAgentDescriptor,
});
this.stylesPanelMode = deriveStylesPanelMode(this.editorInterface);
this.id = nanoid(); this.id = nanoid();
this.library = new Library(this); this.library = new Library(this);
this.actionManager = new ActionManager( this.actionManager = new ActionManager(
@@ -1566,7 +1633,7 @@ class App extends React.Component<AppProps, AppState> {
"excalidraw--view-mode": "excalidraw--view-mode":
this.state.viewModeEnabled || this.state.viewModeEnabled ||
this.state.openDialog?.name === "elementLinkSelector", this.state.openDialog?.name === "elementLinkSelector",
"excalidraw--mobile": this.device.editor.isMobile, "excalidraw--mobile": this.editorInterface.formFactor === "phone",
})} })}
style={{ style={{
["--ui-pointerEvents" as any]: shouldBlockPointerEvents ["--ui-pointerEvents" as any]: shouldBlockPointerEvents
@@ -1588,7 +1655,7 @@ class App extends React.Component<AppProps, AppState> {
<ExcalidrawContainerContext.Provider <ExcalidrawContainerContext.Provider
value={this.excalidrawContainerValue} value={this.excalidrawContainerValue}
> >
<DeviceContext.Provider value={this.device}> <EditorInterfaceContext.Provider value={this.editorInterface}>
<ExcalidrawSetAppStateContext.Provider value={this.setAppState}> <ExcalidrawSetAppStateContext.Provider value={this.setAppState}>
<ExcalidrawAppStateContext.Provider value={this.state}> <ExcalidrawAppStateContext.Provider value={this.state}>
<ExcalidrawElementsContext.Provider <ExcalidrawElementsContext.Provider
@@ -1816,7 +1883,7 @@ class App extends React.Component<AppProps, AppState> {
renderScrollbars={ renderScrollbars={
this.props.renderScrollbars === true this.props.renderScrollbars === true
} }
device={this.device} editorInterface={this.editorInterface}
renderInteractiveSceneCallback={ renderInteractiveSceneCallback={
this.renderInteractiveSceneCallback this.renderInteractiveSceneCallback
} }
@@ -1852,7 +1919,7 @@ class App extends React.Component<AppProps, AppState> {
</ExcalidrawElementsContext.Provider> </ExcalidrawElementsContext.Provider>
</ExcalidrawAppStateContext.Provider> </ExcalidrawAppStateContext.Provider>
</ExcalidrawSetAppStateContext.Provider> </ExcalidrawSetAppStateContext.Provider>
</DeviceContext.Provider> </EditorInterfaceContext.Provider>
</ExcalidrawContainerContext.Provider> </ExcalidrawContainerContext.Provider>
</AppPropsContext.Provider> </AppPropsContext.Provider>
</AppContext.Provider> </AppContext.Provider>
@@ -2369,7 +2436,8 @@ class App extends React.Component<AppProps, AppState> {
if (!scene.appState.preferredSelectionTool.initialized) { if (!scene.appState.preferredSelectionTool.initialized) {
scene.appState.preferredSelectionTool = { scene.appState.preferredSelectionTool = {
type: this.device.editor.isMobile ? "lasso" : "selection", type:
this.editorInterface.formFactor === "phone" ? "lasso" : "selection",
initialized: true, initialized: true,
}; };
} }
@@ -2443,30 +2511,7 @@ class App extends React.Component<AppProps, AppState> {
return minSide >= MQ_MIN_TABLET && maxSide <= MQ_MAX_TABLET; return minSide >= MQ_MIN_TABLET && maxSide <= MQ_MAX_TABLET;
}; };
private refreshViewportBreakpoints = () => { private refreshEditorInterface = () => {
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 = () => {
const container = this.excalidrawContainerRef.current; const container = this.excalidrawContainerRef.current;
if (!container) { if (!container) {
return; return;
@@ -2480,42 +2525,43 @@ class App extends React.Component<AppProps, AppState> {
? this.props.UIOptions.dockedSidebarBreakpoint ? this.props.UIOptions.dockedSidebarBreakpoint
: MQ_RIGHT_SIDEBAR_MIN_WIDTH; : MQ_RIGHT_SIDEBAR_MIN_WIDTH;
const prevEditorState = this.device.editor; const nextEditorInterface = updateObject(this.editorInterface, {
formFactor: deriveFormFactor(editorWidth, editorHeight, {
const nextEditorState = updateObject(prevEditorState, { isMobile: (width, height) => this.isMobileBreakpoint(width, height),
isMobile: this.isMobileBreakpoint(editorWidth, editorHeight), isTablet: (width, height) => this.isTabletBreakpoint(width, height),
}),
canFitSidebar: editorWidth > sidebarBreakpoint, canFitSidebar: editorWidth > sidebarBreakpoint,
isLandscape: editorWidth > editorHeight,
}); });
const stylesPanelMode = const didChange = nextEditorInterface !== this.editorInterface;
// NOTE: we could also remove the isMobileOrTablet check here and
// always switch to compact mode when the editor is narrow (e.g. < MQ_MIN_WIDTH_DESKTOP)
// but not too narrow (> MQ_MAX_WIDTH_MOBILE)
this.isTabletBreakpoint(editorWidth, editorHeight) && isMobileOrTablet()
? "compact"
: this.isMobileBreakpoint(editorWidth, editorHeight)
? "mobile"
: "full";
// also check if we need to update the app state if (didChange) {
this.setState((prevState) => ({ this.editorInterface = nextEditorInterface;
stylesPanelMode, }
// reset to box selection mode if the UI changes to full
// where you'd not be able to change the mode yourself currently this.reconcileStylesPanelMode(nextEditorInterface);
preferredSelectionTool:
stylesPanelMode === "full" return didChange;
? { };
type: "selection",
initialized: true, private reconcileStylesPanelMode = (nextEditorInterface: EditorInterface) => {
} const nextStylesPanelMode = deriveStylesPanelMode(nextEditorInterface);
: prevState.preferredSelectionTool, if (nextStylesPanelMode === this.stylesPanelMode) {
})); return;
}
if (prevEditorState !== nextEditorState) {
this.device = { ...this.device, editor: nextEditorState }; const prevStylesPanelMode = this.stylesPanelMode;
return true; this.stylesPanelMode = nextStylesPanelMode;
if (prevStylesPanelMode !== "full" && nextStylesPanelMode === "full") {
this.setState((prevState) => ({
preferredSelectionTool: {
type: "selection",
initialized: true,
},
}));
} }
return false;
}; };
private clearImageShapeCache(filesMap?: BinaryFiles) { private clearImageShapeCache(filesMap?: BinaryFiles) {
@@ -2593,13 +2639,12 @@ class App extends React.Component<AppProps, AppState> {
// in mobile breakpoint (0 width/height), making everything fail // in mobile breakpoint (0 width/height), making everything fail
!isTestEnv() !isTestEnv()
) { ) {
this.refreshViewportBreakpoints(); this.refreshEditorInterface();
this.refreshEditorBreakpoints();
} }
if (supportsResizeObserver && this.excalidrawContainerRef.current) { if (supportsResizeObserver && this.excalidrawContainerRef.current) {
this.resizeObserver = new ResizeObserver(() => { this.resizeObserver = new ResizeObserver(() => {
this.refreshEditorBreakpoints(); this.refreshEditorInterface();
this.updateDOMRect(); this.updateDOMRect();
}); });
this.resizeObserver?.observe(this.excalidrawContainerRef.current); this.resizeObserver?.observe(this.excalidrawContainerRef.current);
@@ -2653,11 +2698,8 @@ class App extends React.Component<AppProps, AppState> {
this.scene this.scene
.getElementsIncludingDeleted() .getElementsIncludingDeleted()
.forEach((element) => ShapeCache.delete(element)); .forEach((element) => ShapeCache.delete(element));
this.refreshViewportBreakpoints(); this.refreshEditorInterface();
this.updateDOMRect(); this.updateDOMRect();
if (!supportsResizeObserver) {
this.refreshEditorBreakpoints();
}
this.setState({}); this.setState({});
}); });
@@ -2820,7 +2862,7 @@ class App extends React.Component<AppProps, AppState> {
prevProps.UIOptions.dockedSidebarBreakpoint !== prevProps.UIOptions.dockedSidebarBreakpoint !==
this.props.UIOptions.dockedSidebarBreakpoint this.props.UIOptions.dockedSidebarBreakpoint
) { ) {
this.refreshEditorBreakpoints(); this.refreshEditorInterface();
} }
const hasFollowedPersonLeft = const hasFollowedPersonLeft =
@@ -3428,7 +3470,7 @@ class App extends React.Component<AppProps, AppState> {
// from library, not when pasting from clipboard. Alas. // from library, not when pasting from clipboard. Alas.
openSidebar: openSidebar:
this.state.openSidebar && this.state.openSidebar &&
this.device.editor.canFitSidebar && this.editorInterface.canFitSidebar &&
editorJotaiStore.get(isSidebarDockedAtom) editorJotaiStore.get(isSidebarDockedAtom)
? this.state.openSidebar ? this.state.openSidebar
: null, : null,
@@ -3626,7 +3668,7 @@ class App extends React.Component<AppProps, AppState> {
!isPlainPaste && !isPlainPaste &&
textElements.length > 1 && textElements.length > 1 &&
PLAIN_PASTE_TOAST_SHOWN === false && PLAIN_PASTE_TOAST_SHOWN === false &&
!this.device.editor.isMobile this.editorInterface.formFactor !== "phone"
) { ) {
this.setToast({ this.setToast({
message: t("toast.pasteAsSingleElement", { message: t("toast.pasteAsSingleElement", {
@@ -3658,7 +3700,9 @@ class App extends React.Component<AppProps, AppState> {
trackEvent( trackEvent(
"toolbar", "toolbar",
"toggleLock", "toggleLock",
`${source} (${this.device.editor.isMobile ? "mobile" : "desktop"})`, `${source} (${
this.editorInterface.formFactor === "phone" ? "mobile" : "desktop"
})`,
); );
} }
this.setState((prevState) => { this.setState((prevState) => {
@@ -4010,12 +4054,7 @@ class App extends React.Component<AppProps, AppState> {
} }
if (appState) { if (appState) {
this.setState({ this.setState(appState as Pick<AppState, K> | null);
...appState,
// keep existing stylesPanelMode as it needs to be preserved
// or set at startup
stylesPanelMode: this.state.stylesPanelMode,
} as Pick<AppState, K> | null);
} }
if (elements) { if (elements) {
@@ -4593,7 +4632,9 @@ class App extends React.Component<AppProps, AppState> {
"toolbar", "toolbar",
shape, shape,
`keyboard (${ `keyboard (${
this.device.editor.isMobile ? "mobile" : "desktop" this.editorInterface.formFactor === "phone"
? "mobile"
: "desktop"
})`, })`,
); );
} }
@@ -5099,7 +5140,7 @@ class App extends React.Component<AppProps, AppState> {
// caret (i.e. deselect). There's not much use for always selecting // caret (i.e. deselect). There's not much use for always selecting
// the text on edit anyway (and users can select-all from contextmenu // the text on edit anyway (and users can select-all from contextmenu
// if needed) // if needed)
autoSelect: !this.device.isTouchScreen, autoSelect: !this.editorInterface.isTouchScreen,
}); });
// deselect all other elements when inserting text // deselect all other elements when inserting text
this.deselectElements(); this.deselectElements();
@@ -5732,7 +5773,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.state, this.state,
pointFrom(scenePointer.x, scenePointer.y), pointFrom(scenePointer.x, scenePointer.y),
this.device.editor.isMobile, this.editorInterface.formFactor === "phone",
) )
) { ) {
return element; return element;
@@ -5767,7 +5808,7 @@ class App extends React.Component<AppProps, AppState> {
elementsMap, elementsMap,
this.state, this.state,
pointFrom(lastPointerDownCoords.x, lastPointerDownCoords.y), pointFrom(lastPointerDownCoords.x, lastPointerDownCoords.y),
this.device.editor.isMobile, this.editorInterface.formFactor === "phone",
); );
const lastPointerUpCoords = viewportCoordsToSceneCoords( const lastPointerUpCoords = viewportCoordsToSceneCoords(
this.lastPointerUpEvent!, this.lastPointerUpEvent!,
@@ -5778,7 +5819,7 @@ class App extends React.Component<AppProps, AppState> {
elementsMap, elementsMap,
this.state, this.state,
pointFrom(lastPointerUpCoords.x, lastPointerUpCoords.y), pointFrom(lastPointerUpCoords.x, lastPointerUpCoords.y),
this.device.editor.isMobile, this.editorInterface.formFactor === "phone",
); );
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) { if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
hideHyperlinkToolip(); hideHyperlinkToolip();
@@ -6176,7 +6217,7 @@ class App extends React.Component<AppProps, AppState> {
this.state.zoom, this.state.zoom,
event.pointerType, event.pointerType,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.device, this.editorInterface,
); );
if ( if (
elementWithTransformHandleType && elementWithTransformHandleType &&
@@ -6200,7 +6241,7 @@ class App extends React.Component<AppProps, AppState> {
scenePointerY, scenePointerY,
this.state.zoom, this.state.zoom,
event.pointerType, event.pointerType,
this.device, this.editorInterface,
); );
if (transformHandleType) { if (transformHandleType) {
setCursor( setCursor(
@@ -6586,10 +6627,12 @@ class App extends React.Component<AppProps, AppState> {
} }
if ( if (
!this.device.isTouchScreen && !this.editorInterface.isTouchScreen &&
["pen", "touch"].includes(event.pointerType) ["pen", "touch"].includes(event.pointerType)
) { ) {
this.device = updateObject(this.device, { isTouchScreen: true }); this.editorInterface = updateObject(this.editorInterface, {
isTouchScreen: true,
});
} }
if (isPanning) { if (isPanning) {
@@ -6912,7 +6955,7 @@ class App extends React.Component<AppProps, AppState> {
const clicklength = const clicklength =
event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0); event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0);
if (this.device.editor.isMobile && clicklength < 300) { if (this.editorInterface.formFactor === "phone" && clicklength < 300) {
const hitElement = this.getElementAtPosition( const hitElement = this.getElementAtPosition(
scenePointer.x, scenePointer.x,
scenePointer.y, scenePointer.y,
@@ -6931,7 +6974,7 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
if (this.device.isTouchScreen) { if (this.editorInterface.isTouchScreen) {
const hitElement = this.getElementAtPosition( const hitElement = this.getElementAtPosition(
scenePointer.x, scenePointer.x,
scenePointer.y, scenePointer.y,
@@ -6961,7 +7004,7 @@ class App extends React.Component<AppProps, AppState> {
) { ) {
this.handleEmbeddableCenterClick(this.hitLinkElement); this.handleEmbeddableCenterClick(this.hitLinkElement);
} else { } else {
this.redirectToLink(event, this.device.isTouchScreen); this.redirectToLink(event, this.editorInterface.isTouchScreen);
} }
} else if (this.state.viewModeEnabled) { } else if (this.state.viewModeEnabled) {
this.setState({ this.setState({
@@ -7308,7 +7351,7 @@ class App extends React.Component<AppProps, AppState> {
this.state.zoom, this.state.zoom,
event.pointerType, event.pointerType,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.device, this.editorInterface,
); );
if (elementWithTransformHandleType != null) { if (elementWithTransformHandleType != null) {
if ( if (
@@ -7337,7 +7380,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.origin.y, pointerDownState.origin.y,
this.state.zoom, this.state.zoom,
event.pointerType, event.pointerType,
this.device, this.editorInterface,
); );
} }
if (pointerDownState.resize.handleType) { if (pointerDownState.resize.handleType) {
@@ -9387,7 +9430,7 @@ class App extends React.Component<AppProps, AppState> {
newElement && newElement &&
!multiElement !multiElement
) { ) {
if (this.device.isTouchScreen) { if (this.editorInterface.isTouchScreen) {
const FIXED_DELTA_X = Math.min( const FIXED_DELTA_X = Math.min(
(this.state.width * 0.7) / this.state.zoom.value, (this.state.width * 0.7) / this.state.zoom.value,
100, 100,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,19 +19,19 @@ import { isGridModeEnabled } from "../snapping";
import "./HintViewer.scss"; import "./HintViewer.scss";
import type { AppClassProperties, Device, UIAppState } from "../types"; import type { AppClassProperties, EditorInterface, UIAppState } from "../types";
interface HintViewerProps { interface HintViewerProps {
appState: UIAppState; appState: UIAppState;
isMobile: boolean; isMobile: boolean;
device: Device; editorInterface: EditorInterface;
app: AppClassProperties; app: AppClassProperties;
} }
const getHints = ({ const getHints = ({
appState, appState,
isMobile, isMobile,
device, editorInterface,
app, app,
}: HintViewerProps): null | string | string[] => { }: HintViewerProps): null | string | string[] => {
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState; const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
@@ -45,7 +45,7 @@ const getHints = ({
return t("hints.dismissSearch"); return t("hints.dismissSearch");
} }
if (appState.openSidebar && !device.editor.canFitSidebar) { if (appState.openSidebar && !editorInterface.canFitSidebar) {
return null; return null;
} }
@@ -168,13 +168,13 @@ const getHints = ({
export const HintViewer = ({ export const HintViewer = ({
appState, appState,
isMobile, isMobile,
device, editorInterface,
app, app,
}: HintViewerProps) => { }: HintViewerProps) => {
const hints = getHints({ const hints = getHints({
appState, appState,
isMobile, isMobile,
device, editorInterface,
app, app,
}); });

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import React, { type ReactNode } from "react";
import { isInteractive } from "@excalidraw/common"; import { isInteractive } from "@excalidraw/common";
import { useDevice } from "./App"; import { useEditorInterface } from "./App";
import { Island } from "./Island"; import { Island } from "./Island";
interface PropertiesPopoverProps { interface PropertiesPopoverProps {
@@ -39,7 +39,7 @@ export const PropertiesPopover = React.forwardRef<
}, },
ref, ref,
) => { ) => {
const device = useDevice(); const editorInterface = useEditorInterface();
return ( return (
<Popover.Portal container={container}> <Popover.Portal container={container}>
@@ -48,12 +48,14 @@ export const PropertiesPopover = React.forwardRef<
className={clsx("focus-visible-none", className)} className={clsx("focus-visible-none", className)}
data-prevent-outside-click data-prevent-outside-click
side={ side={
device.editor.isMobile && !device.viewport.isLandscape editorInterface.formFactor === "phone" &&
!editorInterface.isLandscape
? "bottom" ? "bottom"
: "right" : "right"
} }
align={ align={
device.editor.isMobile && !device.viewport.isLandscape editorInterface.formFactor === "phone" &&
!editorInterface.isLandscape
? "center" ? "center"
: "start" : "start"
} }
@@ -68,7 +70,7 @@ export const PropertiesPopover = React.forwardRef<
onPointerDownOutside={onPointerDownOutside} onPointerDownOutside={onPointerDownOutside}
onOpenAutoFocus={(e) => { onOpenAutoFocus={(e) => {
// prevent auto-focus on touch devices to avoid keyboard popup // prevent auto-focus on touch devices to avoid keyboard popup
if (preventAutoFocusOnTouch && device.isTouchScreen) { if (preventAutoFocusOnTouch && editorInterface.isTouchScreen) {
e.preventDefault(); e.preventDefault();
} }
}} }}

View File

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

View File

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

View File

@@ -20,7 +20,11 @@ import type {
RenderableElementsMap, RenderableElementsMap,
RenderInteractiveSceneCallback, RenderInteractiveSceneCallback,
} from "../../scene/types"; } from "../../scene/types";
import type { AppState, Device, InteractiveCanvasAppState } from "../../types"; import type {
AppState,
EditorInterface,
InteractiveCanvasAppState,
} from "../../types";
import type { DOMAttributes } from "react"; import type { DOMAttributes } from "react";
type InteractiveCanvasProps = { type InteractiveCanvasProps = {
@@ -35,7 +39,7 @@ type InteractiveCanvasProps = {
scale: number; scale: number;
appState: InteractiveCanvasAppState; appState: InteractiveCanvasAppState;
renderScrollbars: boolean; renderScrollbars: boolean;
device: Device; editorInterface: EditorInterface;
renderInteractiveSceneCallback: ( renderInteractiveSceneCallback: (
data: RenderInteractiveSceneCallback, data: RenderInteractiveSceneCallback,
) => void; ) => void;
@@ -146,7 +150,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
selectionColor, selectionColor,
renderScrollbars: props.renderScrollbars, renderScrollbars: props.renderScrollbars,
}, },
device: props.device, editorInterface: props.editorInterface,
callback: props.renderInteractiveSceneCallback, callback: props.renderInteractiveSceneCallback,
}, },
isRenderThrottlingEnabled(), isRenderThrottlingEnabled(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,59 @@
import { isAndroid, isIOS, isMobileOrTablet } from "@excalidraw/common";
import type { EditorInterface, StylesPanelMode } from "./types";
export const DESKTOP_UI_MODE_STORAGE_KEY = "excalidraw.desktopUIMode";
export const deriveFormFactor = (
editorWidth: number,
editorHeight: number,
breakpoints: {
isMobile: (width: number, height: number) => boolean;
isTablet: (width: number, height: number) => boolean;
},
): EditorInterface["formFactor"] => {
if (breakpoints.isMobile(editorWidth, editorHeight)) {
return "phone";
}
if (breakpoints.isTablet(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 {
raw: normalizedUA,
isMobileDevice: isMobileOrTablet(),
platform,
} as const;
};

View File

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

View File

@@ -285,7 +285,7 @@ export { Button } from "./components/Button";
export { Footer }; export { Footer };
export { MainMenu }; export { MainMenu };
export { Ellipsify } from "./components/Ellipsify"; export { Ellipsify } from "./components/Ellipsify";
export { useDevice } from "./components/App"; export { useEditorInterface, useStylesPanelMode } from "./components/App";
export { WelcomeScreen }; export { WelcomeScreen };
export { LiveCollaborationTrigger }; export { LiveCollaborationTrigger };
export { Stats } from "./components/Stats"; export { Stats } from "./components/Stats";

View File

@@ -19,7 +19,7 @@ import {
import { FIXED_BINDING_DISTANCE, maxBindingGap } from "@excalidraw/element"; import { FIXED_BINDING_DISTANCE, maxBindingGap } from "@excalidraw/element";
import { LinearElementEditor } from "@excalidraw/element"; import { LinearElementEditor } from "@excalidraw/element";
import { import {
getOmitSidesForDevice, getOmitSidesForEditorInterface,
getTransformHandles, getTransformHandles,
getTransformHandlesFromCoords, getTransformHandlesFromCoords,
shouldShowBoundingBox, shouldShowBoundingBox,
@@ -734,7 +734,7 @@ const _renderInteractiveScene = ({
scale, scale,
appState, appState,
renderConfig, renderConfig,
device, editorInterface,
}: InteractiveSceneRenderConfig) => { }: InteractiveSceneRenderConfig) => {
if (canvas === null) { if (canvas === null) {
return { atLeastOneVisibleElement: false, elementsMap }; return { atLeastOneVisibleElement: false, elementsMap };
@@ -1024,7 +1024,7 @@ const _renderInteractiveScene = ({
appState.zoom, appState.zoom,
elementsMap, elementsMap,
"mouse", // when we render we don't know which pointer type so use mouse, "mouse", // when we render we don't know which pointer type so use mouse,
getOmitSidesForDevice(device), getOmitSidesForEditorInterface(editorInterface),
); );
if ( if (
!appState.viewModeEnabled && !appState.viewModeEnabled &&
@@ -1088,8 +1088,11 @@ const _renderInteractiveScene = ({
appState.zoom, appState.zoom,
"mouse", "mouse",
isFrameSelected isFrameSelected
? { ...getOmitSidesForDevice(device), rotation: true } ? {
: getOmitSidesForDevice(device), ...getOmitSidesForEditorInterface(editorInterface),
rotation: true,
}
: getOmitSidesForEditorInterface(editorInterface),
); );
if (selectedElements.some((element) => !element.locked)) { if (selectedElements.some((element) => !element.locked)) {
renderTransformHandles( renderTransformHandles(

View File

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

View File

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

View File

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

View File

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

View File

@@ -192,9 +192,7 @@ export const withExcalidrawDimensions = async (
mockBoundingClientRect(dimensions); mockBoundingClientRect(dimensions);
act(() => { act(() => {
// @ts-ignore // @ts-ignore
h.app.refreshViewportBreakpoints(); h.app.refreshEditorInterface();
// @ts-ignore
h.app.refreshEditorBreakpoints();
window.h.app.refresh(); window.h.app.refresh();
}); });
@@ -203,9 +201,7 @@ export const withExcalidrawDimensions = async (
restoreOriginalGetBoundingClientRect(); restoreOriginalGetBoundingClientRect();
act(() => { act(() => {
// @ts-ignore // @ts-ignore
h.app.refreshViewportBreakpoints(); h.app.refreshEditorInterface();
// @ts-ignore
h.app.refreshEditorBreakpoints();
window.h.app.refresh(); window.h.app.refresh();
}); });
}; };

View File

@@ -449,9 +449,6 @@ export interface AppState {
// as elements are unlocked, we remove the groupId from the elements // as elements are unlocked, we remove the groupId from the elements
// and also remove groupId from this map // and also remove groupId from this map
lockedMultiSelections: { [groupId: string]: true }; lockedMultiSelections: { [groupId: string]: true };
/** properties sidebar mode - determines whether to show compact or complete sidebar */
stylesPanelMode: "compact" | "full" | "mobile";
} }
export type SearchMatch = { export type SearchMatch = {
@@ -715,7 +712,7 @@ export type AppClassProperties = {
} }
>; >;
files: BinaryFiles; files: BinaryFiles;
device: App["device"]; editorInterface: App["editorInterface"];
scene: App["scene"]; scene: App["scene"];
syncActionResult: App["syncActionResult"]; syncActionResult: App["syncActionResult"];
fonts: App["fonts"]; fonts: App["fonts"];
@@ -732,6 +729,7 @@ export type AppClassProperties = {
setActiveTool: App["setActiveTool"]; setActiveTool: App["setActiveTool"];
setOpenDialog: App["setOpenDialog"]; setOpenDialog: App["setOpenDialog"];
insertEmbeddableElement: App["insertEmbeddableElement"]; insertEmbeddableElement: App["insertEmbeddableElement"];
setDesktopUIMode: App["setDesktopUIMode"];
onMagicframeToolSelect: App["onMagicframeToolSelect"]; onMagicframeToolSelect: App["onMagicframeToolSelect"];
getName: App["getName"]; getName: App["getName"];
dismissLinearEditor: App["dismissLinearEditor"]; dismissLinearEditor: App["dismissLinearEditor"];
@@ -885,16 +883,19 @@ export interface ExcalidrawImperativeAPI {
) => UnsubscribeCallback; ) => UnsubscribeCallback;
} }
export type Device = Readonly<{ export type StylesPanelMode = "compact" | "full" | "mobile";
viewport: {
isMobile: boolean; export type EditorInterface = Readonly<{
isLandscape: boolean; formFactor: "phone" | "tablet" | "desktop";
}; desktopUIMode: "compact" | "full";
editor: { userAgent: Readonly<{
isMobile: boolean; raw: string;
canFitSidebar: boolean; isMobileDevice: boolean;
}; platform: "ios" | "android" | "other" | "unknown";
}>;
isTouchScreen: boolean; isTouchScreen: boolean;
canFitSidebar: boolean;
isLandscape: boolean;
}>; }>;
export type FrameNameBounds = { export type FrameNameBounds = {

View File

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

View File

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