diff options
| author | kj_sh604 | 2026-03-15 16:19:35 -0400 |
|---|---|---|
| committer | kj_sh604 | 2026-03-15 16:19:35 -0400 |
| commit | 6ec259a0e71174651bae95d4628138bf6fd68742 (patch) | |
| tree | 5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/components/MobileMenu.tsx | |
| parent | 16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff) | |
refactor: packages/
Diffstat (limited to 'packages/excalidraw/components/MobileMenu.tsx')
| -rw-r--r-- | packages/excalidraw/components/MobileMenu.tsx | 211 |
1 files changed, 211 insertions, 0 deletions
diff --git a/packages/excalidraw/components/MobileMenu.tsx b/packages/excalidraw/components/MobileMenu.tsx new file mode 100644 index 0000000..bef99a9 --- /dev/null +++ b/packages/excalidraw/components/MobileMenu.tsx @@ -0,0 +1,211 @@ +import type { JSX } from "react"; +import React from "react"; +import type { + AppClassProperties, + AppProps, + AppState, + Device, + ExcalidrawProps, + UIAppState, +} from "../types"; +import type { ActionManager } from "../actions/manager"; +import { t } from "../i18n"; +import Stack from "./Stack"; +import { showSelectedShapeActions } from "../element"; +import type { NonDeletedExcalidrawElement } from "../element/types"; +import { FixedSideContainer } from "./FixedSideContainer"; +import { Island } from "./Island"; +import { HintViewer } from "./HintViewer"; +import { calculateScrollCenter } from "../scene"; +import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; +import { Section } from "./Section"; +import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars"; +import { LockButton } from "./LockButton"; +import { PenModeButton } from "./PenModeButton"; +import { HandButton } from "./HandButton"; +import { isHandToolActive } from "../appState"; +import { useTunnels } from "../context/tunnels"; + +type MobileMenuProps = { + appState: UIAppState; + actionManager: ActionManager; + renderJSONExportDialog: () => React.ReactNode; + renderImageExportDialog: () => React.ReactNode; + setAppState: React.Component<any, AppState>["setState"]; + elements: readonly NonDeletedExcalidrawElement[]; + onLockToggle: () => void; + onHandToolToggle: () => void; + onPenModeToggle: AppClassProperties["togglePenMode"]; + + renderTopRightUI?: ( + isMobile: boolean, + appState: UIAppState, + ) => JSX.Element | null; + renderCustomStats?: ExcalidrawProps["renderCustomStats"]; + renderSidebars: () => JSX.Element | null; + device: Device; + renderWelcomeScreen: boolean; + UIOptions: AppProps["UIOptions"]; + app: AppClassProperties; +}; + +export const MobileMenu = ({ + appState, + elements, + actionManager, + setAppState, + onLockToggle, + onHandToolToggle, + onPenModeToggle, + + renderTopRightUI, + renderCustomStats, + renderSidebars, + device, + renderWelcomeScreen, + UIOptions, + app, +}: MobileMenuProps) => { + const { + WelcomeScreenCenterTunnel, + MainMenuTunnel, + DefaultSidebarTriggerTunnel, + } = useTunnels(); + const renderToolbar = () => { + return ( + <FixedSideContainer side="top" className="App-top-bar"> + {renderWelcomeScreen && <WelcomeScreenCenterTunnel.Out />} + <Section heading="shapes"> + {(heading: React.ReactNode) => ( + <Stack.Col gap={4} align="center"> + <Stack.Row gap={1} className="App-toolbar-container"> + <Island padding={1} className="App-toolbar App-toolbar--mobile"> + {heading} + <Stack.Row gap={1}> + <ShapesSwitcher + appState={appState} + activeTool={appState.activeTool} + UIOptions={UIOptions} + app={app} + /> + </Stack.Row> + </Island> + {renderTopRightUI && renderTopRightUI(true, appState)} + <div className="mobile-misc-tools-container"> + {!appState.viewModeEnabled && + appState.openDialog?.name !== "elementLinkSelector" && ( + <DefaultSidebarTriggerTunnel.Out /> + )} + <PenModeButton + checked={appState.penMode} + onChange={() => onPenModeToggle(null)} + title={t("toolBar.penMode")} + isMobile + penDetected={appState.penDetected} + /> + <LockButton + checked={appState.activeTool.locked} + onChange={onLockToggle} + title={t("toolBar.lock")} + isMobile + /> + <HandButton + checked={isHandToolActive(appState)} + onChange={() => onHandToolToggle()} + title={t("toolBar.hand")} + isMobile + /> + </div> + </Stack.Row> + </Stack.Col> + )} + </Section> + <HintViewer + appState={appState} + isMobile={true} + device={device} + app={app} + /> + </FixedSideContainer> + ); + }; + + const renderAppToolbar = () => { + if ( + appState.viewModeEnabled || + appState.openDialog?.name === "elementLinkSelector" + ) { + return ( + <div className="App-toolbar-content"> + <MainMenuTunnel.Out /> + </div> + ); + } + + return ( + <div className="App-toolbar-content"> + <MainMenuTunnel.Out /> + {actionManager.renderAction("toggleEditMenu")} + {actionManager.renderAction( + appState.multiElement ? "finalize" : "duplicateSelection", + )} + {actionManager.renderAction("deleteSelectedElements")} + <div> + {actionManager.renderAction("undo")} + {actionManager.renderAction("redo")} + </div> + </div> + ); + }; + + return ( + <> + {renderSidebars()} + {!appState.viewModeEnabled && + appState.openDialog?.name !== "elementLinkSelector" && + renderToolbar()} + <div + className="App-bottom-bar" + style={{ + marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2, + marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2, + marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2, + }} + > + <Island padding={0}> + {appState.openMenu === "shape" && + !appState.viewModeEnabled && + appState.openDialog?.name !== "elementLinkSelector" && + showSelectedShapeActions(appState, elements) ? ( + <Section className="App-mobile-menu" heading="selectedShapeActions"> + <SelectedShapeActions + appState={appState} + elementsMap={app.scene.getNonDeletedElementsMap()} + renderAction={actionManager.renderAction} + app={app} + /> + </Section> + ) : null} + <footer className="App-toolbar"> + {renderAppToolbar()} + {appState.scrolledOutside && + !appState.openMenu && + !appState.openSidebar && ( + <button + type="button" + className="scroll-back-to-content" + onClick={() => { + setAppState((appState) => ({ + ...calculateScrollCenter(elements, appState), + })); + }} + > + {t("buttons.scrollBackToContent")} + </button> + )} + </footer> + </Island> + </div> + </> + ); +}; |
