feat: new mobile layout (#9996)

* compact bottom toolbar

* put menu trigger to top left

* add popup to switch between grouped tool types

* add a dedicated mobile toolbar

* update position for mobile

* fix active tool type

* add mobile mode as well

* mobile actions

* remove refactored popups

* excali logo mobile

* include mobile

* update mobile menu layout

* move selection and deletion back to right

* do not fill eraser

* fix styling

* fix active styling

* bigger buttons, smaller gaps

* fix other tools not opened

* fix: Style panel persistence and restore

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* move hidden action btns to extra popover

* fix dropdown overlapping with welcome screen

* replace custom popup with popover

* improve button styles

* swapping redo and delete

* always show undo & redo and improve styling

* change background

* toolbar styles

* no any

* persist perferred selection tool and align tablet as well

* add a renderTopLeftUI to props

* tweak border and bg

* show combined properties only when using suitable tools

* fix preferred tool

* new stroke icon

* hide color picker hot keys

* init preferred tool based on device

* fix main menu sizing

* fix welcome screen offset

* put text before image

* disable call highlight on buttons

* fix renderTopLeftUI

---------

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