From 12644eb347620868aaca5b67f8763cf130621216 Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Thu, 11 Sep 2025 11:30:33 +1000 Subject: [PATCH] compact bottom toolbar --- .../components/LinearElementTypePopup.tsx | 122 +++++ packages/excalidraw/components/MobileMenu.tsx | 74 +-- .../excalidraw/components/MobileToolBar.scss | 78 ++++ .../excalidraw/components/MobileToolBar.tsx | 429 ++++++++++++++++++ .../components/SelectionTypePopup.tsx | 122 +++++ .../excalidraw/components/ShapeTypePopup.tsx | 123 +++++ .../components/dropdownMenu/DropdownMenu.scss | 7 + .../components/dropdownMenu/DropdownMenu.tsx | 13 +- .../dropdownMenu/DropdownMenuContent.tsx | 3 + packages/excalidraw/css/styles.scss | 8 +- 10 files changed, 920 insertions(+), 59 deletions(-) create mode 100644 packages/excalidraw/components/LinearElementTypePopup.tsx create mode 100644 packages/excalidraw/components/MobileToolBar.scss create mode 100644 packages/excalidraw/components/MobileToolBar.tsx create mode 100644 packages/excalidraw/components/SelectionTypePopup.tsx create mode 100644 packages/excalidraw/components/ShapeTypePopup.tsx diff --git a/packages/excalidraw/components/LinearElementTypePopup.tsx b/packages/excalidraw/components/LinearElementTypePopup.tsx new file mode 100644 index 000000000..ccb4c978e --- /dev/null +++ b/packages/excalidraw/components/LinearElementTypePopup.tsx @@ -0,0 +1,122 @@ +import React, { useEffect, useRef, useState } from "react"; + +import { CLASSES, capitalizeString } from "@excalidraw/common"; +import { trackEvent } from "../analytics"; +import { t } from "../i18n"; +import { ToolButton } from "./ToolButton"; +import { ArrowIcon, LineIcon } from "./icons"; + +import type { AppClassProperties } from "../types"; + +import "./ConvertElementTypePopup.scss"; + +const LINEAR_ELEMENT_TYPES = [ + { type: "arrow", icon: ArrowIcon }, + { type: "line", icon: LineIcon }, +] as const; + +type LinearElementType = "arrow" | "line"; + +type LinearElementTypePopupProps = { + app: AppClassProperties; + triggerElement: HTMLElement | null; + isOpen: boolean; + onClose: () => void; + currentType: LinearElementType; + onChange?: (type: LinearElementType) => void; +}; + +export const LinearElementTypePopup = ({ + app, + triggerElement, + isOpen, + onClose, + onChange, + currentType, +}: LinearElementTypePopupProps) => { + const [panelPosition, setPanelPosition] = useState({ x: 0, y: 0 }); + const panelRef = useRef(null); + + useEffect(() => { + if (!isOpen || !triggerElement) { + return; + } + + const updatePosition = () => { + const triggerRect = triggerElement.getBoundingClientRect(); + const panelRect = panelRef.current?.getBoundingClientRect(); + const panelWidth = panelRect?.width ?? 0; + const panelHeight = panelRect?.height ?? 0; + setPanelPosition({ + x: triggerRect.x + triggerRect.width / 2 - panelWidth / 2, + y: triggerRect.top - panelHeight - 16, + }); + }; + + updatePosition(); + + const handleClick = (event: MouseEvent) => { + const target = event.target as Node | null; + const panelEl = panelRef.current; + const triggerEl = triggerElement; + if (!target) { + onClose(); + return; + } + const clickedInsidePanel = !!panelEl && panelEl.contains(target); + const clickedTrigger = !!triggerEl && triggerEl.contains(target); + if (!clickedInsidePanel && !clickedTrigger) { + onClose(); + } + }; + + // use capture to ensure we run before potential re-renders hide elements + document.addEventListener("pointerdown", handleClick, true); + document.addEventListener("pointerup", handleClick, true); + + return () => { + document.removeEventListener("pointerdown", handleClick, true); + document.removeEventListener("pointerup", handleClick, true); + }; + }, [isOpen, triggerElement, onClose]); + + if (!isOpen) return null; + + return ( +
+ {LINEAR_ELEMENT_TYPES.map(({ type, icon }) => ( + { + if (app.state.activeTool.type !== type) { + trackEvent("toolbar", type, "ui"); + } + app.setActiveTool({ type: type as any }); + onChange?.(type); + // Intentionally NOT calling onClose here; popup should stay open + // until user clicks outside trigger or popup per requirements. + }} + /> + ))} +
+ ); +}; diff --git a/packages/excalidraw/components/MobileMenu.tsx b/packages/excalidraw/components/MobileMenu.tsx index 454c0f64e..a03c5e38d 100644 --- a/packages/excalidraw/components/MobileMenu.tsx +++ b/packages/excalidraw/components/MobileMenu.tsx @@ -11,6 +11,7 @@ import { calculateScrollCenter } from "../scene"; import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars"; import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; +import { MobileToolBar } from "./MobileToolBar"; import { FixedSideContainer } from "./FixedSideContainer"; import { HandButton } from "./HandButton"; import { HintViewer } from "./HintViewer"; @@ -78,60 +79,16 @@ export const MobileMenu = ({ } = useTunnels(); const renderToolbar = () => { return ( - - {renderWelcomeScreen && } -
- {(heading: React.ReactNode) => ( - - - - {heading} - - - - - {renderTopRightUI && renderTopRightUI(true, appState)} -
- {!appState.viewModeEnabled && - appState.openDialog?.name !== "elementLinkSelector" && ( - - )} - onPenModeToggle(null)} - title={t("toolBar.penMode")} - isMobile - penDetected={appState.penDetected} - /> - - onHandToolToggle()} - title={t("toolBar.hand")} - isMobile - /> -
-
-
- )} -
- + {/* {renderWelcomeScreen && } */} + -
+ ); }; @@ -166,9 +123,14 @@ export const MobileMenu = ({ return ( <> {renderSidebars()} - {!appState.viewModeEnabled && - appState.openDialog?.name !== "elementLinkSelector" && - renderToolbar()} + + +
) : null}