Compare commits
13 Commits
master
...
ryan-di/mo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ca55e9a45 | ||
|
|
e6da5b213d | ||
|
|
62be53845e | ||
|
|
eebe491f4e | ||
|
|
ffcc2e13d8 | ||
|
|
ddb5dc313c | ||
|
|
e064bc236f | ||
|
|
4492c81fa8 | ||
|
|
d28b63fcbb | ||
|
|
fd34f7437b | ||
|
|
6935934417 | ||
|
|
fb2975b0f2 | ||
|
|
12644eb347 |
@ -10,7 +10,13 @@ export const hasBackground = (type: ElementOrToolType) =>
|
|||||||
type === "freedraw";
|
type === "freedraw";
|
||||||
|
|
||||||
export const hasStrokeColor = (type: ElementOrToolType) =>
|
export const hasStrokeColor = (type: ElementOrToolType) =>
|
||||||
type !== "image" && type !== "frame" && type !== "magicframe";
|
type === "rectangle" ||
|
||||||
|
type === "ellipse" ||
|
||||||
|
type === "diamond" ||
|
||||||
|
type === "freedraw" ||
|
||||||
|
type === "arrow" ||
|
||||||
|
type === "line" ||
|
||||||
|
type === "text";
|
||||||
|
|
||||||
export const hasStrokeWidth = (type: ElementOrToolType) =>
|
export const hasStrokeWidth = (type: ElementOrToolType) =>
|
||||||
type === "rectangle" ||
|
type === "rectangle" ||
|
||||||
|
|||||||
@ -348,7 +348,10 @@ export const actionChangeStrokeColor = register({
|
|||||||
elements={elements}
|
elements={elements}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
compactMode={appState.stylesPanelMode === "compact"}
|
compactMode={
|
||||||
|
appState.stylesPanelMode === "compact" ||
|
||||||
|
appState.stylesPanelMode === "mobile"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@ -428,7 +431,10 @@ export const actionChangeBackgroundColor = register({
|
|||||||
elements={elements}
|
elements={elements}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
compactMode={appState.stylesPanelMode === "compact"}
|
compactMode={
|
||||||
|
appState.stylesPanelMode === "compact" ||
|
||||||
|
appState.stylesPanelMode === "mobile"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@ -531,9 +537,7 @@ export const actionChangeStrokeWidth = register({
|
|||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, app, data }) => (
|
PanelComponent: ({ elements, appState, updateData, app, data }) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{appState.stylesPanelMode === "full" && (
|
|
||||||
<legend>{t("labels.strokeWidth")}</legend>
|
<legend>{t("labels.strokeWidth")}</legend>
|
||||||
)}
|
|
||||||
<div className="buttonList">
|
<div className="buttonList">
|
||||||
<RadioSelection
|
<RadioSelection
|
||||||
group="stroke-width"
|
group="stroke-width"
|
||||||
@ -590,9 +594,7 @@ export const actionChangeSloppiness = register({
|
|||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, app, data }) => (
|
PanelComponent: ({ elements, appState, updateData, app, data }) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{appState.stylesPanelMode === "full" && (
|
|
||||||
<legend>{t("labels.sloppiness")}</legend>
|
<legend>{t("labels.sloppiness")}</legend>
|
||||||
)}
|
|
||||||
<div className="buttonList">
|
<div className="buttonList">
|
||||||
<RadioSelection
|
<RadioSelection
|
||||||
group="sloppiness"
|
group="sloppiness"
|
||||||
@ -645,9 +647,7 @@ export const actionChangeStrokeStyle = register({
|
|||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, app, data }) => (
|
PanelComponent: ({ elements, appState, updateData, app, data }) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{appState.stylesPanelMode === "full" && (
|
|
||||||
<legend>{t("labels.strokeStyle")}</legend>
|
<legend>{t("labels.strokeStyle")}</legend>
|
||||||
)}
|
|
||||||
<div className="buttonList">
|
<div className="buttonList">
|
||||||
<RadioSelection
|
<RadioSelection
|
||||||
group="strokeStyle"
|
group="strokeStyle"
|
||||||
@ -776,7 +776,8 @@ export const actionChangeFontSize = register({
|
|||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
withCaretPositionPreservation(
|
withCaretPositionPreservation(
|
||||||
() => updateData(value),
|
() => updateData(value),
|
||||||
appState.stylesPanelMode === "compact",
|
appState.stylesPanelMode === "compact" ||
|
||||||
|
appState.stylesPanelMode === "mobile",
|
||||||
!!appState.editingTextElement,
|
!!appState.editingTextElement,
|
||||||
data?.onPreventClose,
|
data?.onPreventClose,
|
||||||
);
|
);
|
||||||
@ -1040,7 +1041,7 @@ export const actionChangeFontFamily = register({
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, app, updateData, data }) => {
|
PanelComponent: ({ elements, appState, app, updateData }) => {
|
||||||
const cachedElementsRef = useRef<ElementsMap>(new Map());
|
const cachedElementsRef = useRef<ElementsMap>(new Map());
|
||||||
const prevSelectedFontFamilyRef = useRef<number | null>(null);
|
const prevSelectedFontFamilyRef = useRef<number | null>(null);
|
||||||
// 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
|
||||||
@ -1117,7 +1118,7 @@ export const actionChangeFontFamily = register({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<fieldset>
|
<>
|
||||||
{appState.stylesPanelMode === "full" && (
|
{appState.stylesPanelMode === "full" && (
|
||||||
<legend>{t("labels.fontFamily")}</legend>
|
<legend>{t("labels.fontFamily")}</legend>
|
||||||
)}
|
)}
|
||||||
@ -1125,7 +1126,7 @@ export const actionChangeFontFamily = register({
|
|||||||
isOpened={appState.openPopup === "fontFamily"}
|
isOpened={appState.openPopup === "fontFamily"}
|
||||||
selectedFontFamily={selectedFontFamily}
|
selectedFontFamily={selectedFontFamily}
|
||||||
hoveredFontFamily={appState.currentHoveredFontFamily}
|
hoveredFontFamily={appState.currentHoveredFontFamily}
|
||||||
compactMode={appState.stylesPanelMode === "compact"}
|
compactMode={appState.stylesPanelMode !== "full"}
|
||||||
onSelect={(fontFamily) => {
|
onSelect={(fontFamily) => {
|
||||||
withCaretPositionPreservation(
|
withCaretPositionPreservation(
|
||||||
() => {
|
() => {
|
||||||
@ -1137,7 +1138,8 @@ 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",
|
appState.stylesPanelMode === "compact" ||
|
||||||
|
appState.stylesPanelMode === "mobile",
|
||||||
!!appState.editingTextElement,
|
!!appState.editingTextElement,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@ -1213,7 +1215,8 @@ export const actionChangeFontFamily = register({
|
|||||||
|
|
||||||
// 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 (
|
||||||
appState.stylesPanelMode === "compact" &&
|
(appState.stylesPanelMode === "compact" ||
|
||||||
|
appState.stylesPanelMode === "mobile") &&
|
||||||
appState.editingTextElement
|
appState.editingTextElement
|
||||||
) {
|
) {
|
||||||
restoreCaretPosition(null); // Just refocus without saved position
|
restoreCaretPosition(null); // Just refocus without saved position
|
||||||
@ -1221,7 +1224,7 @@ export const actionChangeFontFamily = register({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -1314,7 +1317,8 @@ export const actionChangeTextAlign = register({
|
|||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
withCaretPositionPreservation(
|
withCaretPositionPreservation(
|
||||||
() => updateData(value),
|
() => updateData(value),
|
||||||
appState.stylesPanelMode === "compact",
|
appState.stylesPanelMode === "compact" ||
|
||||||
|
appState.stylesPanelMode === "mobile",
|
||||||
!!appState.editingTextElement,
|
!!appState.editingTextElement,
|
||||||
data?.onPreventClose,
|
data?.onPreventClose,
|
||||||
);
|
);
|
||||||
@ -1413,7 +1417,8 @@ export const actionChangeVerticalAlign = register({
|
|||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
withCaretPositionPreservation(
|
withCaretPositionPreservation(
|
||||||
() => updateData(value),
|
() => updateData(value),
|
||||||
appState.stylesPanelMode === "compact",
|
appState.stylesPanelMode === "compact" ||
|
||||||
|
appState.stylesPanelMode === "mobile",
|
||||||
!!appState.editingTextElement,
|
!!appState.editingTextElement,
|
||||||
data?.onPreventClose,
|
data?.onPreventClose,
|
||||||
);
|
);
|
||||||
@ -1678,8 +1683,8 @@ export const actionChangeArrowProperties = register({
|
|||||||
PanelComponent: ({ elements, appState, updateData, app, renderAction }) => {
|
PanelComponent: ({ elements, appState, updateData, app, renderAction }) => {
|
||||||
return (
|
return (
|
||||||
<div className="selected-shape-actions">
|
<div className="selected-shape-actions">
|
||||||
{renderAction("changeArrowType")}
|
|
||||||
{renderAction("changeArrowhead")}
|
{renderAction("changeArrowhead")}
|
||||||
|
{renderAction("changeArrowType")}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -110,8 +110,8 @@
|
|||||||
--default-button-size: 2rem;
|
--default-button-size: 2rem;
|
||||||
|
|
||||||
.compact-action-button {
|
.compact-action-button {
|
||||||
width: 2rem;
|
width: 1.625rem;
|
||||||
height: 2rem;
|
height: 1.625rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: var(--border-radius-lg);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@ -167,6 +167,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ToolIcon__icon {
|
||||||
|
width: 1.625rem;
|
||||||
|
height: 1.625rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-shape-actions-island {
|
.compact-shape-actions-island {
|
||||||
@ -199,6 +204,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-shape-actions {
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
.shape-actions-theme-scope {
|
.shape-actions-theme-scope {
|
||||||
--button-border: transparent;
|
--button-border: transparent;
|
||||||
--button-bg: var(--color-surface-mid);
|
--button-bg: var(--color-surface-mid);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -78,6 +78,8 @@ import {
|
|||||||
DotsHorizontalIcon,
|
DotsHorizontalIcon,
|
||||||
} from "./icons";
|
} from "./icons";
|
||||||
|
|
||||||
|
import { Island } from "./Island";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppClassProperties,
|
AppClassProperties,
|
||||||
AppProps,
|
AppProps,
|
||||||
@ -305,27 +307,19 @@ export const SelectedShapeActions = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CompactShapeActions = ({
|
const CombinedShapeProperties = ({
|
||||||
appState,
|
appState,
|
||||||
elementsMap,
|
|
||||||
renderAction,
|
renderAction,
|
||||||
app,
|
|
||||||
setAppState,
|
setAppState,
|
||||||
|
targetElements,
|
||||||
|
container,
|
||||||
}: {
|
}: {
|
||||||
|
targetElements: ExcalidrawElement[];
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap;
|
|
||||||
renderAction: ActionManager["renderAction"];
|
renderAction: ActionManager["renderAction"];
|
||||||
app: AppClassProperties;
|
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
container: HTMLDivElement | null;
|
||||||
}) => {
|
}) => {
|
||||||
const targetElements = getTargetElements(elementsMap, appState);
|
|
||||||
const { saveCaretPosition, restoreCaretPosition } = useTextEditorFocus();
|
|
||||||
const { container } = useExcalidrawContainer();
|
|
||||||
|
|
||||||
const isEditingTextOrNewElement = Boolean(
|
|
||||||
appState.editingTextElement || appState.newElement,
|
|
||||||
);
|
|
||||||
|
|
||||||
const showFillIcons =
|
const showFillIcons =
|
||||||
(hasBackground(appState.activeTool.type) &&
|
(hasBackground(appState.activeTool.type) &&
|
||||||
!isTransparent(appState.currentItemBackgroundColor)) ||
|
!isTransparent(appState.currentItemBackgroundColor)) ||
|
||||||
@ -334,56 +328,20 @@ export const CompactShapeActions = ({
|
|||||||
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
||||||
);
|
);
|
||||||
|
|
||||||
const showLinkIcon = targetElements.length === 1;
|
const showShowCombinedProperties =
|
||||||
|
showFillIcons ||
|
||||||
const showLineEditorAction =
|
|
||||||
!appState.selectedLinearElement?.isEditing &&
|
|
||||||
targetElements.length === 1 &&
|
|
||||||
isLinearElement(targetElements[0]) &&
|
|
||||||
!isElbowArrow(targetElements[0]);
|
|
||||||
|
|
||||||
const showCropEditorAction =
|
|
||||||
!appState.croppingElementId &&
|
|
||||||
targetElements.length === 1 &&
|
|
||||||
isImageElement(targetElements[0]);
|
|
||||||
|
|
||||||
const showAlignActions = alignActionsPredicate(appState, app);
|
|
||||||
|
|
||||||
let isSingleElementBoundContainer = false;
|
|
||||||
if (
|
|
||||||
targetElements.length === 2 &&
|
|
||||||
(hasBoundTextElement(targetElements[0]) ||
|
|
||||||
hasBoundTextElement(targetElements[1]))
|
|
||||||
) {
|
|
||||||
isSingleElementBoundContainer = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="compact-shape-actions">
|
|
||||||
{/* Stroke Color */}
|
|
||||||
{canChangeStrokeColor(appState, targetElements) && (
|
|
||||||
<div className={clsx("compact-action-item")}>
|
|
||||||
{renderAction("changeStrokeColor")}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Background Color */}
|
|
||||||
{canChangeBackgroundColor(appState, targetElements) && (
|
|
||||||
<div className="compact-action-item">
|
|
||||||
{renderAction("changeBackgroundColor")}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Combined Properties (Fill, Stroke, Opacity) */}
|
|
||||||
{(showFillIcons ||
|
|
||||||
hasStrokeWidth(appState.activeTool.type) ||
|
hasStrokeWidth(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => hasStrokeWidth(element.type)) ||
|
targetElements.some((element) => hasStrokeWidth(element.type)) ||
|
||||||
hasStrokeStyle(appState.activeTool.type) ||
|
hasStrokeStyle(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => hasStrokeStyle(element.type)) ||
|
targetElements.some((element) => hasStrokeStyle(element.type)) ||
|
||||||
canChangeRoundness(appState.activeTool.type) ||
|
canChangeRoundness(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => canChangeRoundness(element.type))) && (
|
targetElements.some((element) => canChangeRoundness(element.type));
|
||||||
|
|
||||||
|
if (!showShowCombinedProperties) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="compact-action-item">
|
<div className="compact-action-item">
|
||||||
<Popover.Root
|
<Popover.Root
|
||||||
open={appState.openPopup === "compactStrokeStyles"}
|
open={appState.openPopup === "compactStrokeStyles"}
|
||||||
@ -449,11 +407,33 @@ export const CompactShapeActions = ({
|
|||||||
)}
|
)}
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
{/* Combined Arrow Properties */}
|
const CombinedArrowProperties = ({
|
||||||
{(toolIsArrow(appState.activeTool.type) ||
|
appState,
|
||||||
targetElements.some((element) => toolIsArrow(element.type))) && (
|
renderAction,
|
||||||
|
setAppState,
|
||||||
|
targetElements,
|
||||||
|
container,
|
||||||
|
app,
|
||||||
|
}: {
|
||||||
|
targetElements: ExcalidrawElement[];
|
||||||
|
appState: UIAppState;
|
||||||
|
renderAction: ActionManager["renderAction"];
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
container: HTMLDivElement | null;
|
||||||
|
app: AppClassProperties;
|
||||||
|
}) => {
|
||||||
|
const showShowArrowProperties =
|
||||||
|
toolIsArrow(appState.activeTool.type) ||
|
||||||
|
targetElements.some((element) => toolIsArrow(element.type));
|
||||||
|
|
||||||
|
if (!showShowArrowProperties) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="compact-action-item">
|
<div className="compact-action-item">
|
||||||
<Popover.Root
|
<Popover.Root
|
||||||
open={appState.openPopup === "compactArrowProperties"}
|
open={appState.openPopup === "compactArrowProperties"}
|
||||||
@ -524,22 +504,27 @@ export const CompactShapeActions = ({
|
|||||||
)}
|
)}
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
{/* Linear Editor */}
|
const CombinedTextProperties = ({
|
||||||
{showLineEditorAction && (
|
appState,
|
||||||
<div className="compact-action-item">
|
renderAction,
|
||||||
{renderAction("toggleLinearEditor")}
|
setAppState,
|
||||||
</div>
|
targetElements,
|
||||||
)}
|
container,
|
||||||
|
elementsMap,
|
||||||
|
}: {
|
||||||
|
appState: UIAppState;
|
||||||
|
renderAction: ActionManager["renderAction"];
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
targetElements: ExcalidrawElement[];
|
||||||
|
container: HTMLDivElement | null;
|
||||||
|
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap;
|
||||||
|
}) => {
|
||||||
|
const { saveCaretPosition, restoreCaretPosition } = useTextEditorFocus();
|
||||||
|
|
||||||
{/* Text Properties */}
|
return (
|
||||||
{(appState.activeTool.type === "text" ||
|
|
||||||
targetElements.some(isTextElement)) && (
|
|
||||||
<>
|
|
||||||
<div className="compact-action-item">
|
|
||||||
{renderAction("changeFontFamily")}
|
|
||||||
</div>
|
|
||||||
<div className="compact-action-item">
|
<div className="compact-action-item">
|
||||||
<Popover.Root
|
<Popover.Root
|
||||||
open={appState.openPopup === "compactTextProperties"}
|
open={appState.openPopup === "compactTextProperties"}
|
||||||
@ -607,25 +592,49 @@ export const CompactShapeActions = ({
|
|||||||
)}
|
)}
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
</div>
|
</div>
|
||||||
</>
|
);
|
||||||
)}
|
};
|
||||||
|
|
||||||
{/* Dedicated Copy Button */}
|
const CombinedExtraActions = ({
|
||||||
{!isEditingTextOrNewElement && targetElements.length > 0 && (
|
appState,
|
||||||
<div className="compact-action-item">
|
renderAction,
|
||||||
{renderAction("duplicateSelection")}
|
targetElements,
|
||||||
</div>
|
setAppState,
|
||||||
)}
|
container,
|
||||||
|
app,
|
||||||
|
}: {
|
||||||
|
appState: UIAppState;
|
||||||
|
targetElements: ExcalidrawElement[];
|
||||||
|
renderAction: ActionManager["renderAction"];
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
container: HTMLDivElement | null;
|
||||||
|
app: AppClassProperties;
|
||||||
|
}) => {
|
||||||
|
const isEditingTextOrNewElement = Boolean(
|
||||||
|
appState.editingTextElement || appState.newElement,
|
||||||
|
);
|
||||||
|
const showCropEditorAction =
|
||||||
|
!appState.croppingElementId &&
|
||||||
|
targetElements.length === 1 &&
|
||||||
|
isImageElement(targetElements[0]);
|
||||||
|
const showLinkIcon = targetElements.length === 1;
|
||||||
|
const showAlignActions = alignActionsPredicate(appState, app);
|
||||||
|
let isSingleElementBoundContainer = false;
|
||||||
|
if (
|
||||||
|
targetElements.length === 2 &&
|
||||||
|
(hasBoundTextElement(targetElements[0]) ||
|
||||||
|
hasBoundTextElement(targetElements[1]))
|
||||||
|
) {
|
||||||
|
isSingleElementBoundContainer = true;
|
||||||
|
}
|
||||||
|
|
||||||
{/* Dedicated Delete Button */}
|
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||||
{!isEditingTextOrNewElement && targetElements.length > 0 && (
|
|
||||||
<div className="compact-action-item">
|
|
||||||
{renderAction("deleteSelectedElements")}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Combined Other Actions */}
|
if (isEditingTextOrNewElement || targetElements.length === 0) {
|
||||||
{!isEditingTextOrNewElement && targetElements.length > 0 && (
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="compact-action-item">
|
<div className="compact-action-item">
|
||||||
<Popover.Root
|
<Popover.Root
|
||||||
open={appState.openPopup === "compactOtherProperties"}
|
open={appState.openPopup === "compactOtherProperties"}
|
||||||
@ -662,7 +671,6 @@ export const CompactShapeActions = ({
|
|||||||
container={container}
|
container={container}
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "12rem",
|
maxWidth: "12rem",
|
||||||
// center the popover content
|
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
@ -731,11 +739,281 @@ export const CompactShapeActions = ({
|
|||||||
)}
|
)}
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LinearEditorAction = ({
|
||||||
|
appState,
|
||||||
|
renderAction,
|
||||||
|
targetElements,
|
||||||
|
}: {
|
||||||
|
appState: UIAppState;
|
||||||
|
targetElements: ExcalidrawElement[];
|
||||||
|
renderAction: ActionManager["renderAction"];
|
||||||
|
}) => {
|
||||||
|
const showLineEditorAction =
|
||||||
|
!appState.selectedLinearElement?.isEditing &&
|
||||||
|
targetElements.length === 1 &&
|
||||||
|
isLinearElement(targetElements[0]) &&
|
||||||
|
!isElbowArrow(targetElements[0]);
|
||||||
|
|
||||||
|
if (!showLineEditorAction) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="compact-action-item">
|
||||||
|
{renderAction("toggleLinearEditor")}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CompactShapeActions = ({
|
||||||
|
appState,
|
||||||
|
elementsMap,
|
||||||
|
renderAction,
|
||||||
|
app,
|
||||||
|
setAppState,
|
||||||
|
}: {
|
||||||
|
appState: UIAppState;
|
||||||
|
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap;
|
||||||
|
renderAction: ActionManager["renderAction"];
|
||||||
|
app: AppClassProperties;
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
}) => {
|
||||||
|
const targetElements = getTargetElements(elementsMap, appState);
|
||||||
|
const { container } = useExcalidrawContainer();
|
||||||
|
|
||||||
|
const isEditingTextOrNewElement = Boolean(
|
||||||
|
appState.editingTextElement || appState.newElement,
|
||||||
|
);
|
||||||
|
|
||||||
|
const showLineEditorAction =
|
||||||
|
!appState.selectedLinearElement?.isEditing &&
|
||||||
|
targetElements.length === 1 &&
|
||||||
|
isLinearElement(targetElements[0]) &&
|
||||||
|
!isElbowArrow(targetElements[0]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="compact-shape-actions">
|
||||||
|
{/* Stroke Color */}
|
||||||
|
{canChangeStrokeColor(appState, targetElements) && (
|
||||||
|
<div
|
||||||
|
className={clsx("compact-action-item")}
|
||||||
|
style={{
|
||||||
|
marginRight: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderAction("changeStrokeColor")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Background Color */}
|
||||||
|
{canChangeBackgroundColor(appState, targetElements) && (
|
||||||
|
<div className="compact-action-item">
|
||||||
|
{renderAction("changeBackgroundColor")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CombinedShapeProperties
|
||||||
|
appState={appState}
|
||||||
|
renderAction={renderAction}
|
||||||
|
setAppState={setAppState}
|
||||||
|
targetElements={targetElements}
|
||||||
|
container={container}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CombinedArrowProperties
|
||||||
|
appState={appState}
|
||||||
|
renderAction={renderAction}
|
||||||
|
setAppState={setAppState}
|
||||||
|
targetElements={targetElements}
|
||||||
|
container={container}
|
||||||
|
app={app}
|
||||||
|
/>
|
||||||
|
{/* Linear Editor */}
|
||||||
|
{showLineEditorAction && (
|
||||||
|
<div className="compact-action-item">
|
||||||
|
{renderAction("toggleLinearEditor")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Text Properties */}
|
||||||
|
{(appState.activeTool.type === "text" ||
|
||||||
|
targetElements.some(isTextElement)) && (
|
||||||
|
<>
|
||||||
|
<div className="compact-action-item">
|
||||||
|
{renderAction("changeFontFamily")}
|
||||||
|
</div>
|
||||||
|
<CombinedTextProperties
|
||||||
|
appState={appState}
|
||||||
|
renderAction={renderAction}
|
||||||
|
setAppState={setAppState}
|
||||||
|
targetElements={targetElements}
|
||||||
|
container={container}
|
||||||
|
elementsMap={elementsMap}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dedicated Copy Button */}
|
||||||
|
{!isEditingTextOrNewElement && targetElements.length > 0 && (
|
||||||
|
<div className="compact-action-item">
|
||||||
|
{renderAction("duplicateSelection")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dedicated Delete Button */}
|
||||||
|
{!isEditingTextOrNewElement && targetElements.length > 0 && (
|
||||||
|
<div className="compact-action-item">
|
||||||
|
{renderAction("deleteSelectedElements")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CombinedExtraActions
|
||||||
|
appState={appState}
|
||||||
|
renderAction={renderAction}
|
||||||
|
targetElements={targetElements}
|
||||||
|
setAppState={setAppState}
|
||||||
|
container={container}
|
||||||
|
app={app}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MobileShapeActions = ({
|
||||||
|
appState,
|
||||||
|
elementsMap,
|
||||||
|
renderAction,
|
||||||
|
app,
|
||||||
|
setAppState,
|
||||||
|
}: {
|
||||||
|
appState: UIAppState;
|
||||||
|
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap;
|
||||||
|
renderAction: ActionManager["renderAction"];
|
||||||
|
app: AppClassProperties;
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
}) => {
|
||||||
|
const targetElements = getTargetElements(elementsMap, appState);
|
||||||
|
const { container } = useExcalidrawContainer();
|
||||||
|
const mobileActionsRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const width = mobileActionsRef.current?.getBoundingClientRect()?.width ?? 0;
|
||||||
|
|
||||||
|
const WIDTH = 26;
|
||||||
|
const GAP = 8;
|
||||||
|
|
||||||
|
// max 6 actions + undo
|
||||||
|
const MIN_WIDTH = 7 * WIDTH + 6 * GAP;
|
||||||
|
|
||||||
|
const ADDITIONAL_WIDTH = WIDTH + GAP;
|
||||||
|
|
||||||
|
const showDelete = width >= MIN_WIDTH;
|
||||||
|
const showDuplicate = width >= MIN_WIDTH + 2 * ADDITIONAL_WIDTH;
|
||||||
|
const showRedo = width >= MIN_WIDTH + 3 * ADDITIONAL_WIDTH;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Island
|
||||||
|
className="compact-shape-actions mobile-shape-actions"
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
boxShadow: "none",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
padding: 0,
|
||||||
|
margin: "0 0.25rem",
|
||||||
|
zIndex: 2,
|
||||||
|
height: WIDTH * 1.75,
|
||||||
|
alignItems: "center",
|
||||||
|
gap: GAP,
|
||||||
|
}}
|
||||||
|
ref={mobileActionsRef}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: GAP,
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{canChangeStrokeColor(appState, targetElements) && (
|
||||||
|
<div className={clsx("compact-action-item")}>
|
||||||
|
{renderAction("changeStrokeColor")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{canChangeBackgroundColor(appState, targetElements) && (
|
||||||
|
<div className="compact-action-item">
|
||||||
|
{renderAction("changeBackgroundColor")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<CombinedShapeProperties
|
||||||
|
appState={appState}
|
||||||
|
renderAction={renderAction}
|
||||||
|
setAppState={setAppState}
|
||||||
|
targetElements={targetElements}
|
||||||
|
container={container}
|
||||||
|
/>
|
||||||
|
{/* Combined Arrow Properties */}
|
||||||
|
<CombinedArrowProperties
|
||||||
|
appState={appState}
|
||||||
|
renderAction={renderAction}
|
||||||
|
setAppState={setAppState}
|
||||||
|
targetElements={targetElements}
|
||||||
|
container={container}
|
||||||
|
app={app}
|
||||||
|
/>
|
||||||
|
{/* Linear Editor */}
|
||||||
|
<LinearEditorAction
|
||||||
|
appState={appState}
|
||||||
|
renderAction={renderAction}
|
||||||
|
targetElements={targetElements}
|
||||||
|
/>
|
||||||
|
{/* Text Properties */}
|
||||||
|
{(appState.activeTool.type === "text" ||
|
||||||
|
targetElements.some(isTextElement)) && (
|
||||||
|
<>
|
||||||
|
<div className="compact-action-item">
|
||||||
|
{renderAction("changeFontFamily")}
|
||||||
|
</div>
|
||||||
|
<CombinedTextProperties
|
||||||
|
appState={appState}
|
||||||
|
renderAction={renderAction}
|
||||||
|
setAppState={setAppState}
|
||||||
|
targetElements={targetElements}
|
||||||
|
container={container}
|
||||||
|
elementsMap={elementsMap}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDuplicate && renderAction("duplicateSelection")}
|
||||||
|
{showDelete && renderAction("deleteSelectedElements")}
|
||||||
|
|
||||||
|
{/* Combined Other Actions */}
|
||||||
|
<CombinedExtraActions
|
||||||
|
appState={appState}
|
||||||
|
renderAction={renderAction}
|
||||||
|
targetElements={targetElements}
|
||||||
|
setAppState={setAppState}
|
||||||
|
container={container}
|
||||||
|
app={app}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: GAP,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderAction("undo")}
|
||||||
|
{showRedo && renderAction("redo")}
|
||||||
|
</div>
|
||||||
|
</Island>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ShapesSwitcher = ({
|
export const ShapesSwitcher = ({
|
||||||
activeTool,
|
activeTool,
|
||||||
appState,
|
appState,
|
||||||
|
|||||||
@ -2484,6 +2484,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// but not too narrow (> MQ_MAX_WIDTH_MOBILE)
|
// but not too narrow (> MQ_MAX_WIDTH_MOBILE)
|
||||||
this.isTabletBreakpoint(editorWidth, editorHeight) && isMobileOrTablet()
|
this.isTabletBreakpoint(editorWidth, editorHeight) && isMobileOrTablet()
|
||||||
? "compact"
|
? "compact"
|
||||||
|
: this.isMobileBreakpoint(editorWidth, editorHeight)
|
||||||
|
? "mobile"
|
||||||
: "full",
|
: "full",
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -6485,6 +6487,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setAppState({ snapLines: [] });
|
this.setAppState({ snapLines: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.openPopup) {
|
||||||
|
this.setState({ openPopup: null });
|
||||||
|
}
|
||||||
|
|
||||||
this.updateGestureOnPointerDown(event);
|
this.updateGestureOnPointerDown(event);
|
||||||
|
|
||||||
// if dragging element is freedraw and another pointerdown event occurs
|
// if dragging element is freedraw and another pointerdown event occurs
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
.excalidraw {
|
.excalidraw {
|
||||||
.ExcalidrawLogo {
|
.ExcalidrawLogo {
|
||||||
|
--logo-icon--mobile: 1rem;
|
||||||
|
--logo-text--mobile: 0.75rem;
|
||||||
|
|
||||||
--logo-icon--xs: 2rem;
|
--logo-icon--xs: 2rem;
|
||||||
--logo-text--xs: 1.5rem;
|
--logo-text--xs: 1.5rem;
|
||||||
|
|
||||||
@ -30,6 +33,17 @@
|
|||||||
color: var(--color-logo-text);
|
color: var(--color-logo-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-mobile {
|
||||||
|
.ExcalidrawLogo-icon {
|
||||||
|
height: var(--logo-icon--mobile);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExcalidrawLogo-text {
|
||||||
|
height: var(--logo-text--mobile);
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.is-xs {
|
&.is-xs {
|
||||||
.ExcalidrawLogo-icon {
|
.ExcalidrawLogo-icon {
|
||||||
height: var(--logo-icon--xs);
|
height: var(--logo-icon--xs);
|
||||||
|
|||||||
@ -41,7 +41,7 @@ const LogoText = () => (
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
type LogoSize = "xs" | "small" | "normal" | "large" | "custom";
|
type LogoSize = "xs" | "small" | "normal" | "large" | "custom" | "mobile";
|
||||||
|
|
||||||
interface LogoProps {
|
interface LogoProps {
|
||||||
size?: LogoSize;
|
size?: LogoSize;
|
||||||
|
|||||||
@ -106,6 +106,7 @@ export const FontPicker = React.memo(
|
|||||||
<FontPickerTrigger
|
<FontPickerTrigger
|
||||||
selectedFontFamily={selectedFontFamily}
|
selectedFontFamily={selectedFontFamily}
|
||||||
isOpened={isOpened}
|
isOpened={isOpened}
|
||||||
|
compactMode={compactMode}
|
||||||
/>
|
/>
|
||||||
{isOpened && (
|
{isOpened && (
|
||||||
<FontPickerList
|
<FontPickerList
|
||||||
|
|||||||
@ -338,11 +338,13 @@ export const FontPickerList = React.memo(
|
|||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
preventAutoFocusOnTouch={!!app.state.editingTextElement}
|
preventAutoFocusOnTouch={!!app.state.editingTextElement}
|
||||||
>
|
>
|
||||||
|
{app.state.stylesPanelMode === "full" && (
|
||||||
<QuickSearch
|
<QuickSearch
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
placeholder={t("quickSearch.placeholder")}
|
placeholder={t("quickSearch.placeholder")}
|
||||||
onChange={debounce(setSearchTerm, 20)}
|
onChange={debounce(setSearchTerm, 20)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
className="dropdown-menu fonts manual-hover"
|
className="dropdown-menu fonts manual-hover"
|
||||||
placeholder={t("fontList.empty")}
|
placeholder={t("fontList.empty")}
|
||||||
|
|||||||
@ -11,11 +11,13 @@ import { useExcalidrawSetAppState } from "../App";
|
|||||||
interface FontPickerTriggerProps {
|
interface FontPickerTriggerProps {
|
||||||
selectedFontFamily: FontFamilyValues | null;
|
selectedFontFamily: FontFamilyValues | null;
|
||||||
isOpened?: boolean;
|
isOpened?: boolean;
|
||||||
|
compactMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FontPickerTrigger = ({
|
export const FontPickerTrigger = ({
|
||||||
selectedFontFamily,
|
selectedFontFamily,
|
||||||
isOpened = false,
|
isOpened = false,
|
||||||
|
compactMode = false,
|
||||||
}: FontPickerTriggerProps) => {
|
}: FontPickerTriggerProps) => {
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
|
|
||||||
@ -37,6 +39,8 @@ export const FontPickerTrigger = ({
|
|||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
border: "none",
|
border: "none",
|
||||||
|
width: compactMode ? "1.625rem" : undefined,
|
||||||
|
height: compactMode ? "1.625rem" : undefined,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -152,15 +152,13 @@ function Picker<T>({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isMobile = device.editor.isMobile;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Content
|
<Popover.Content
|
||||||
side={
|
side={isMobile ? "right" : "bottom"}
|
||||||
device.editor.isMobile && !device.viewport.isLandscape
|
|
||||||
? "top"
|
|
||||||
: "bottom"
|
|
||||||
}
|
|
||||||
align="start"
|
align="start"
|
||||||
sideOffset={12}
|
sideOffset={isMobile ? 8 : 12}
|
||||||
style={{ zIndex: "var(--zIndex-popup)" }}
|
style={{ zIndex: "var(--zIndex-popup)" }}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -582,13 +582,10 @@ const LayerUI = ({
|
|||||||
renderJSONExportDialog={renderJSONExportDialog}
|
renderJSONExportDialog={renderJSONExportDialog}
|
||||||
renderImageExportDialog={renderImageExportDialog}
|
renderImageExportDialog={renderImageExportDialog}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
onLockToggle={onLockToggle}
|
|
||||||
onHandToolToggle={onHandToolToggle}
|
onHandToolToggle={onHandToolToggle}
|
||||||
onPenModeToggle={onPenModeToggle}
|
onPenModeToggle={onPenModeToggle}
|
||||||
renderTopRightUI={renderTopRightUI}
|
renderTopRightUI={renderTopRightUI}
|
||||||
renderCustomStats={renderCustomStats}
|
|
||||||
renderSidebars={renderSidebars}
|
renderSidebars={renderSidebars}
|
||||||
device={device}
|
|
||||||
renderWelcomeScreen={renderWelcomeScreen}
|
renderWelcomeScreen={renderWelcomeScreen}
|
||||||
UIOptions={UIOptions}
|
UIOptions={UIOptions}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,32 +1,23 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { showSelectedShapeActions } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { isHandToolActive } from "../appState";
|
|
||||||
import { useTunnels } from "../context/tunnels";
|
import { useTunnels } from "../context/tunnels";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { calculateScrollCenter } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
||||||
|
|
||||||
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
import { MobileShapeActions } from "./Actions";
|
||||||
|
import { MobileToolBar } from "./MobileToolBar";
|
||||||
import { FixedSideContainer } from "./FixedSideContainer";
|
import { FixedSideContainer } from "./FixedSideContainer";
|
||||||
import { HandButton } from "./HandButton";
|
|
||||||
import { HintViewer } from "./HintViewer";
|
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import { LockButton } from "./LockButton";
|
|
||||||
import { PenModeButton } from "./PenModeButton";
|
|
||||||
import { Section } from "./Section";
|
|
||||||
import Stack from "./Stack";
|
|
||||||
|
|
||||||
import type { ActionManager } from "../actions/manager";
|
import type { ActionManager } from "../actions/manager";
|
||||||
import type {
|
import type {
|
||||||
AppClassProperties,
|
AppClassProperties,
|
||||||
AppProps,
|
AppProps,
|
||||||
AppState,
|
AppState,
|
||||||
Device,
|
|
||||||
ExcalidrawProps,
|
|
||||||
UIAppState,
|
UIAppState,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import type { JSX } from "react";
|
import type { JSX } from "react";
|
||||||
@ -38,7 +29,6 @@ type MobileMenuProps = {
|
|||||||
renderImageExportDialog: () => React.ReactNode;
|
renderImageExportDialog: () => React.ReactNode;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
onLockToggle: () => void;
|
|
||||||
onHandToolToggle: () => void;
|
onHandToolToggle: () => void;
|
||||||
onPenModeToggle: AppClassProperties["togglePenMode"];
|
onPenModeToggle: AppClassProperties["togglePenMode"];
|
||||||
|
|
||||||
@ -46,9 +36,7 @@ type MobileMenuProps = {
|
|||||||
isMobile: boolean,
|
isMobile: boolean,
|
||||||
appState: UIAppState,
|
appState: UIAppState,
|
||||||
) => JSX.Element | null;
|
) => JSX.Element | null;
|
||||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
|
||||||
renderSidebars: () => JSX.Element | null;
|
renderSidebars: () => JSX.Element | null;
|
||||||
device: Device;
|
|
||||||
renderWelcomeScreen: boolean;
|
renderWelcomeScreen: boolean;
|
||||||
UIOptions: AppProps["UIOptions"];
|
UIOptions: AppProps["UIOptions"];
|
||||||
app: AppClassProperties;
|
app: AppClassProperties;
|
||||||
@ -59,14 +47,10 @@ export const MobileMenu = ({
|
|||||||
elements,
|
elements,
|
||||||
actionManager,
|
actionManager,
|
||||||
setAppState,
|
setAppState,
|
||||||
onLockToggle,
|
|
||||||
onHandToolToggle,
|
onHandToolToggle,
|
||||||
onPenModeToggle,
|
|
||||||
|
|
||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
renderCustomStats,
|
|
||||||
renderSidebars,
|
renderSidebars,
|
||||||
device,
|
|
||||||
renderWelcomeScreen,
|
renderWelcomeScreen,
|
||||||
UIOptions,
|
UIOptions,
|
||||||
app,
|
app,
|
||||||
@ -78,64 +62,15 @@ export const MobileMenu = ({
|
|||||||
} = useTunnels();
|
} = useTunnels();
|
||||||
const renderToolbar = () => {
|
const renderToolbar = () => {
|
||||||
return (
|
return (
|
||||||
<FixedSideContainer side="top" className="App-top-bar">
|
<MobileToolBar
|
||||||
{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}
|
appState={appState}
|
||||||
activeTool={appState.activeTool}
|
|
||||||
UIOptions={UIOptions}
|
|
||||||
app={app}
|
app={app}
|
||||||
|
onHandToolToggle={onHandToolToggle}
|
||||||
/>
|
/>
|
||||||
</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 = () => {
|
const renderAppTopBar = () => {
|
||||||
if (
|
if (
|
||||||
appState.viewModeEnabled ||
|
appState.viewModeEnabled ||
|
||||||
appState.openDialog?.name === "elementLinkSelector"
|
appState.openDialog?.name === "elementLinkSelector"
|
||||||
@ -147,18 +82,28 @@ export const MobileMenu = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const topRightUI = renderTopRightUI?.(true, appState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App-toolbar-content">
|
<div
|
||||||
|
className="App-toolbar-content"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<MainMenuTunnel.Out />
|
<MainMenuTunnel.Out />
|
||||||
{actionManager.renderAction("toggleEditMenu")}
|
|
||||||
{actionManager.renderAction(
|
|
||||||
appState.multiElement ? "finalize" : "duplicateSelection",
|
|
||||||
)}
|
|
||||||
{actionManager.renderAction("deleteSelectedElements")}
|
|
||||||
<div>
|
|
||||||
{actionManager.renderAction("undo")}
|
|
||||||
{actionManager.renderAction("redo")}
|
|
||||||
</div>
|
</div>
|
||||||
|
{topRightUI ? topRightUI : <DefaultSidebarTriggerTunnel.Out />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -166,33 +111,28 @@ export const MobileMenu = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderSidebars()}
|
{renderSidebars()}
|
||||||
{!appState.viewModeEnabled &&
|
<FixedSideContainer side="top" className="App-top-bar">
|
||||||
appState.openDialog?.name !== "elementLinkSelector" &&
|
{renderAppTopBar()}
|
||||||
renderToolbar()}
|
{renderWelcomeScreen && <WelcomeScreenCenterTunnel.Out />}
|
||||||
|
</FixedSideContainer>
|
||||||
<div
|
<div
|
||||||
className="App-bottom-bar"
|
className="App-bottom-bar"
|
||||||
style={{
|
style={{
|
||||||
marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN,
|
||||||
marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
|
||||||
marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Island padding={0}>
|
<MobileShapeActions
|
||||||
{appState.openMenu === "shape" &&
|
|
||||||
!appState.viewModeEnabled &&
|
|
||||||
appState.openDialog?.name !== "elementLinkSelector" &&
|
|
||||||
showSelectedShapeActions(appState, elements) ? (
|
|
||||||
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
|
||||||
<SelectedShapeActions
|
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elementsMap={app.scene.getNonDeletedElementsMap()}
|
elementsMap={app.scene.getNonDeletedElementsMap()}
|
||||||
renderAction={actionManager.renderAction}
|
renderAction={actionManager.renderAction}
|
||||||
app={app}
|
app={app}
|
||||||
|
setAppState={setAppState}
|
||||||
/>
|
/>
|
||||||
</Section>
|
|
||||||
) : null}
|
<Island className="App-toolbar">
|
||||||
<footer className="App-toolbar">
|
{!appState.viewModeEnabled &&
|
||||||
{renderAppToolbar()}
|
appState.openDialog?.name !== "elementLinkSelector" &&
|
||||||
|
renderToolbar()}
|
||||||
{appState.scrolledOutside &&
|
{appState.scrolledOutside &&
|
||||||
!appState.openMenu &&
|
!appState.openMenu &&
|
||||||
!appState.openSidebar && (
|
!appState.openSidebar && (
|
||||||
@ -208,7 +148,6 @@ export const MobileMenu = ({
|
|||||||
{t("buttons.scrollBackToContent")}
|
{t("buttons.scrollBackToContent")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</footer>
|
|
||||||
</Island>
|
</Island>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
74
packages/excalidraw/components/MobileToolBar.scss
Normal file
74
packages/excalidraw/components/MobileToolBar.scss
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
@import "open-color/open-color.scss";
|
||||||
|
@import "../css/variables.module.scss";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.mobile-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px;
|
||||||
|
gap: 4px;
|
||||||
|
border-radius: var(--space-factor);
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
@media screen and (min-width: 340px) {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 380px) {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-toolbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-toolbar .ToolIcon {
|
||||||
|
min-width: 2rem;
|
||||||
|
min-height: 2rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.ToolIcon__icon {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-toolbar .App-toolbar__extra-tools-dropdown {
|
||||||
|
min-width: 160px;
|
||||||
|
z-index: var(--zIndex-layerUI);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-toolbar-separator {
|
||||||
|
width: 1px;
|
||||||
|
height: 24px;
|
||||||
|
background: var(--default-border-color);
|
||||||
|
margin: 0 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-toolbar-undo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-toolbar-undo .ToolIcon {
|
||||||
|
min-width: 32px;
|
||||||
|
min-height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
422
packages/excalidraw/components/MobileToolBar.tsx
Normal file
422
packages/excalidraw/components/MobileToolBar.tsx
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import { KEYS, capitalizeString } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { trackEvent } from "../analytics";
|
||||||
|
|
||||||
|
import { t } from "../i18n";
|
||||||
|
|
||||||
|
import { isHandToolActive } from "../appState";
|
||||||
|
|
||||||
|
import { useTunnels } from "../context/tunnels";
|
||||||
|
|
||||||
|
import { HandButton } from "./HandButton";
|
||||||
|
import { ToolButton } from "./ToolButton";
|
||||||
|
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
||||||
|
import { ToolWithPopup } from "./ToolWithPopup";
|
||||||
|
|
||||||
|
import {
|
||||||
|
SelectionIcon,
|
||||||
|
FreedrawIcon,
|
||||||
|
EraserIcon,
|
||||||
|
RectangleIcon,
|
||||||
|
ArrowIcon,
|
||||||
|
extraToolsIcon,
|
||||||
|
DiamondIcon,
|
||||||
|
EllipseIcon,
|
||||||
|
LineIcon,
|
||||||
|
TextIcon,
|
||||||
|
ImageIcon,
|
||||||
|
frameToolIcon,
|
||||||
|
EmbedIcon,
|
||||||
|
laserPointerToolIcon,
|
||||||
|
LassoIcon,
|
||||||
|
mermaidLogoIcon,
|
||||||
|
MagicIcon,
|
||||||
|
} from "./icons";
|
||||||
|
|
||||||
|
import "./ToolIcon.scss";
|
||||||
|
import "./MobileToolBar.scss";
|
||||||
|
|
||||||
|
import type { AppClassProperties, UIAppState } from "../types";
|
||||||
|
|
||||||
|
const SHAPE_TOOLS = [
|
||||||
|
{
|
||||||
|
type: "rectangle",
|
||||||
|
icon: RectangleIcon,
|
||||||
|
title: capitalizeString(t("toolBar.rectangle")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "diamond",
|
||||||
|
icon: DiamondIcon,
|
||||||
|
title: capitalizeString(t("toolBar.diamond")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "ellipse",
|
||||||
|
icon: EllipseIcon,
|
||||||
|
title: capitalizeString(t("toolBar.ellipse")),
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const SELECTION_TOOLS = [
|
||||||
|
{
|
||||||
|
type: "selection",
|
||||||
|
icon: SelectionIcon,
|
||||||
|
title: capitalizeString(t("toolBar.selection")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "lasso",
|
||||||
|
icon: LassoIcon,
|
||||||
|
title: capitalizeString(t("toolBar.lasso")),
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const LINEAR_ELEMENT_TOOLS = [
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
icon: ArrowIcon,
|
||||||
|
title: capitalizeString(t("toolBar.arrow")),
|
||||||
|
},
|
||||||
|
{ type: "line", icon: LineIcon, title: capitalizeString(t("toolBar.line")) },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type MobileToolBarProps = {
|
||||||
|
appState: UIAppState;
|
||||||
|
app: AppClassProperties;
|
||||||
|
onHandToolToggle: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MobileToolBar = ({
|
||||||
|
appState,
|
||||||
|
app,
|
||||||
|
onHandToolToggle,
|
||||||
|
}: MobileToolBarProps) => {
|
||||||
|
const activeTool = appState.activeTool;
|
||||||
|
const [isOtherShapesMenuOpen, setIsOtherShapesMenuOpen] = useState(false);
|
||||||
|
const [lastActiveGenericShape, setLastActiveGenericShape] = useState<
|
||||||
|
"rectangle" | "diamond" | "ellipse"
|
||||||
|
>("rectangle");
|
||||||
|
const [lastActiveLinearElement, setLastActiveLinearElement] = useState<
|
||||||
|
"arrow" | "line"
|
||||||
|
>("arrow");
|
||||||
|
|
||||||
|
// keep lastActiveGenericShape in sync with active tool if user switches via other UI
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
activeTool.type === "rectangle" ||
|
||||||
|
activeTool.type === "diamond" ||
|
||||||
|
activeTool.type === "ellipse"
|
||||||
|
) {
|
||||||
|
setLastActiveGenericShape(activeTool.type);
|
||||||
|
}
|
||||||
|
}, [activeTool.type]);
|
||||||
|
|
||||||
|
// keep lastActiveLinearElement in sync with active tool if user switches via other UI
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTool.type === "arrow" || activeTool.type === "line") {
|
||||||
|
setLastActiveLinearElement(activeTool.type);
|
||||||
|
}
|
||||||
|
}, [activeTool.type]);
|
||||||
|
|
||||||
|
const frameToolSelected = activeTool.type === "frame";
|
||||||
|
const laserToolSelected = activeTool.type === "laser";
|
||||||
|
const embeddableToolSelected = activeTool.type === "embeddable";
|
||||||
|
|
||||||
|
const { TTDDialogTriggerTunnel } = useTunnels();
|
||||||
|
|
||||||
|
const handleToolChange = (toolType: string, pointerType?: string) => {
|
||||||
|
if (appState.activeTool.type !== toolType) {
|
||||||
|
trackEvent("toolbar", toolType, "ui");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolType === "selection") {
|
||||||
|
if (appState.activeTool.type === "selection") {
|
||||||
|
// Toggle selection tool behavior if needed
|
||||||
|
} else {
|
||||||
|
app.setActiveTool({ type: "selection" });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
app.setActiveTool({ type: toolType as any });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showTextToolOutside = appState.width >= 400;
|
||||||
|
const showFrameToolOutside = appState.width >= 440;
|
||||||
|
|
||||||
|
const extraTools = [
|
||||||
|
"text",
|
||||||
|
"frame",
|
||||||
|
"embeddable",
|
||||||
|
"laser",
|
||||||
|
"magicframe",
|
||||||
|
].filter((tool) => {
|
||||||
|
if (showTextToolOutside && tool === "text") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (showFrameToolOutside && tool === "frame") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
const extraToolSelected = extraTools.includes(appState.activeTool.type);
|
||||||
|
const extraIcon = extraToolSelected
|
||||||
|
? appState.activeTool.type === "frame"
|
||||||
|
? frameToolIcon
|
||||||
|
: appState.activeTool.type === "embeddable"
|
||||||
|
? EmbedIcon
|
||||||
|
: appState.activeTool.type === "laser"
|
||||||
|
? laserPointerToolIcon
|
||||||
|
: appState.activeTool.type === "text"
|
||||||
|
? TextIcon
|
||||||
|
: appState.activeTool.type === "magicframe"
|
||||||
|
? MagicIcon
|
||||||
|
: extraToolsIcon
|
||||||
|
: extraToolsIcon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mobile-toolbar">
|
||||||
|
{/* Hand Tool */}
|
||||||
|
<HandButton
|
||||||
|
checked={isHandToolActive(appState)}
|
||||||
|
onChange={onHandToolToggle}
|
||||||
|
title={t("toolBar.hand")}
|
||||||
|
isMobile
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Selection Tool */}
|
||||||
|
<ToolWithPopup
|
||||||
|
app={app}
|
||||||
|
options={SELECTION_TOOLS}
|
||||||
|
activeTool={activeTool}
|
||||||
|
defaultOption={app.defaultSelectionTool}
|
||||||
|
className="Selection"
|
||||||
|
namePrefix="selectionType"
|
||||||
|
title={capitalizeString(t("toolBar.selection"))}
|
||||||
|
data-testid="toolbar-selection"
|
||||||
|
onToolChange={(type: string) => {
|
||||||
|
app.setActiveTool({ type: type as any });
|
||||||
|
app.defaultSelectionTool = type as any;
|
||||||
|
}}
|
||||||
|
getDisplayedOption={() =>
|
||||||
|
SELECTION_TOOLS.find(
|
||||||
|
(tool) => tool.type === app.defaultSelectionTool,
|
||||||
|
) || SELECTION_TOOLS[0]
|
||||||
|
}
|
||||||
|
isActive={
|
||||||
|
activeTool.type === "selection" || activeTool.type === "lasso"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Free Draw */}
|
||||||
|
<ToolButton
|
||||||
|
className={clsx("Shape", { fillable: false })}
|
||||||
|
type="radio"
|
||||||
|
icon={FreedrawIcon}
|
||||||
|
checked={activeTool.type === "freedraw"}
|
||||||
|
name="editor-current-shape"
|
||||||
|
title={`${capitalizeString(t("toolBar.freedraw"))}`}
|
||||||
|
aria-label={capitalizeString(t("toolBar.freedraw"))}
|
||||||
|
data-testid="toolbar-freedraw"
|
||||||
|
onChange={() => handleToolChange("freedraw")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Eraser */}
|
||||||
|
<ToolButton
|
||||||
|
className={clsx("Shape", { fillable: true })}
|
||||||
|
type="radio"
|
||||||
|
icon={EraserIcon}
|
||||||
|
checked={activeTool.type === "eraser"}
|
||||||
|
name="editor-current-shape"
|
||||||
|
title={`${capitalizeString(t("toolBar.eraser"))}`}
|
||||||
|
aria-label={capitalizeString(t("toolBar.eraser"))}
|
||||||
|
data-testid="toolbar-eraser"
|
||||||
|
onChange={() => handleToolChange("eraser")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Rectangle */}
|
||||||
|
<ToolWithPopup
|
||||||
|
app={app}
|
||||||
|
options={SHAPE_TOOLS}
|
||||||
|
activeTool={activeTool}
|
||||||
|
defaultOption={lastActiveGenericShape}
|
||||||
|
className="Shape"
|
||||||
|
namePrefix="shapeType"
|
||||||
|
title={capitalizeString(
|
||||||
|
t(
|
||||||
|
lastActiveGenericShape === "rectangle"
|
||||||
|
? "toolBar.rectangle"
|
||||||
|
: lastActiveGenericShape === "diamond"
|
||||||
|
? "toolBar.diamond"
|
||||||
|
: lastActiveGenericShape === "ellipse"
|
||||||
|
? "toolBar.ellipse"
|
||||||
|
: "toolBar.rectangle",
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
data-testid="toolbar-rectangle"
|
||||||
|
onToolChange={(type: string) => {
|
||||||
|
setLastActiveGenericShape(type as any);
|
||||||
|
app.setActiveTool({ type: type as any });
|
||||||
|
}}
|
||||||
|
getDisplayedOption={() =>
|
||||||
|
SHAPE_TOOLS.find((tool) => tool.type === lastActiveGenericShape) ||
|
||||||
|
SHAPE_TOOLS[0]
|
||||||
|
}
|
||||||
|
isActive={["rectangle", "diamond", "ellipse"].includes(activeTool.type)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Arrow/Line */}
|
||||||
|
<ToolWithPopup
|
||||||
|
app={app}
|
||||||
|
options={LINEAR_ELEMENT_TOOLS}
|
||||||
|
activeTool={activeTool}
|
||||||
|
defaultOption={lastActiveLinearElement}
|
||||||
|
className="LinearElement"
|
||||||
|
namePrefix="linearElementType"
|
||||||
|
title={capitalizeString(
|
||||||
|
t(
|
||||||
|
lastActiveLinearElement === "arrow"
|
||||||
|
? "toolBar.arrow"
|
||||||
|
: "toolBar.line",
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
data-testid="toolbar-arrow"
|
||||||
|
fillable={true}
|
||||||
|
onToolChange={(type: string) => {
|
||||||
|
setLastActiveLinearElement(type as any);
|
||||||
|
app.setActiveTool({ type: type as any });
|
||||||
|
}}
|
||||||
|
getDisplayedOption={() =>
|
||||||
|
LINEAR_ELEMENT_TOOLS.find(
|
||||||
|
(tool) => tool.type === lastActiveLinearElement,
|
||||||
|
) || LINEAR_ELEMENT_TOOLS[0]
|
||||||
|
}
|
||||||
|
isActive={["arrow", "line"].includes(activeTool.type)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Image */}
|
||||||
|
<ToolButton
|
||||||
|
className={clsx("Shape", { fillable: false })}
|
||||||
|
type="radio"
|
||||||
|
icon={ImageIcon}
|
||||||
|
checked={activeTool.type === "image"}
|
||||||
|
name="editor-current-shape"
|
||||||
|
title={`${capitalizeString(t("toolBar.image"))}`}
|
||||||
|
aria-label={capitalizeString(t("toolBar.image"))}
|
||||||
|
data-testid="toolbar-image"
|
||||||
|
onChange={() => handleToolChange("image")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Text Tool */}
|
||||||
|
{showTextToolOutside && (
|
||||||
|
<ToolButton
|
||||||
|
className={clsx("Shape", { fillable: false })}
|
||||||
|
type="radio"
|
||||||
|
icon={TextIcon}
|
||||||
|
checked={activeTool.type === "text"}
|
||||||
|
name="editor-current-shape"
|
||||||
|
title={`${capitalizeString(t("toolBar.text"))}`}
|
||||||
|
aria-label={capitalizeString(t("toolBar.text"))}
|
||||||
|
data-testid="toolbar-text"
|
||||||
|
onChange={() => handleToolChange("text")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Frame Tool */}
|
||||||
|
{showFrameToolOutside && (
|
||||||
|
<ToolButton
|
||||||
|
className={clsx("Shape", { fillable: false })}
|
||||||
|
type="radio"
|
||||||
|
icon={frameToolIcon}
|
||||||
|
checked={frameToolSelected}
|
||||||
|
name="editor-current-shape"
|
||||||
|
title={`${capitalizeString(t("toolBar.frame"))}`}
|
||||||
|
aria-label={capitalizeString(t("toolBar.frame"))}
|
||||||
|
data-testid="toolbar-frame"
|
||||||
|
onChange={() => handleToolChange("frame")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Other Shapes */}
|
||||||
|
<DropdownMenu open={isOtherShapesMenuOpen} placement="top">
|
||||||
|
<DropdownMenu.Trigger
|
||||||
|
className={clsx("App-toolbar__extra-tools-trigger", {
|
||||||
|
"App-toolbar__extra-tools-trigger--selected": extraToolSelected,
|
||||||
|
})}
|
||||||
|
onToggle={() => setIsOtherShapesMenuOpen(!isOtherShapesMenuOpen)}
|
||||||
|
title={t("toolBar.extraTools")}
|
||||||
|
>
|
||||||
|
{extraIcon}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
onClickOutside={() => setIsOtherShapesMenuOpen(false)}
|
||||||
|
onSelect={() => setIsOtherShapesMenuOpen(false)}
|
||||||
|
className="App-toolbar__extra-tools-dropdown"
|
||||||
|
>
|
||||||
|
{!showTextToolOutside && (
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => app.setActiveTool({ type: "text" })}
|
||||||
|
icon={TextIcon}
|
||||||
|
shortcut={KEYS.T.toLocaleUpperCase()}
|
||||||
|
data-testid="toolbar-text"
|
||||||
|
selected={activeTool.type === "text"}
|
||||||
|
>
|
||||||
|
{t("toolBar.text")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
)}
|
||||||
|
{!showFrameToolOutside && (
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => app.setActiveTool({ type: "frame" })}
|
||||||
|
icon={frameToolIcon}
|
||||||
|
shortcut={KEYS.F.toLocaleUpperCase()}
|
||||||
|
data-testid="toolbar-frame"
|
||||||
|
selected={frameToolSelected}
|
||||||
|
>
|
||||||
|
{t("toolBar.frame")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
)}
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => app.setActiveTool({ type: "embeddable" })}
|
||||||
|
icon={EmbedIcon}
|
||||||
|
data-testid="toolbar-embeddable"
|
||||||
|
selected={embeddableToolSelected}
|
||||||
|
>
|
||||||
|
{t("toolBar.embeddable")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => app.setActiveTool({ type: "laser" })}
|
||||||
|
icon={laserPointerToolIcon}
|
||||||
|
data-testid="toolbar-laser"
|
||||||
|
selected={laserToolSelected}
|
||||||
|
shortcut={KEYS.K.toLocaleUpperCase()}
|
||||||
|
>
|
||||||
|
{t("toolBar.laser")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<div style={{ margin: "6px 0", fontSize: 14, fontWeight: 600 }}>
|
||||||
|
Generate
|
||||||
|
</div>
|
||||||
|
{app.props.aiEnabled !== false && <TTDDialogTriggerTunnel.Out />}
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => app.setOpenDialog({ name: "ttd", tab: "mermaid" })}
|
||||||
|
icon={mermaidLogoIcon}
|
||||||
|
data-testid="toolbar-embeddable"
|
||||||
|
>
|
||||||
|
{t("toolBar.mermaidToExcalidraw")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{app.props.aiEnabled !== false && app.plugins.diagramToCode && (
|
||||||
|
<>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => app.onMagicframeToolSelect()}
|
||||||
|
icon={MagicIcon}
|
||||||
|
data-testid="toolbar-magicframe"
|
||||||
|
>
|
||||||
|
{t("toolBar.magicframe")}
|
||||||
|
<DropdownMenu.Item.Badge>AI</DropdownMenu.Item.Badge>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
194
packages/excalidraw/components/ToolWithPopup.tsx
Normal file
194
packages/excalidraw/components/ToolWithPopup.tsx
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import { capitalizeString, CLASSES } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { trackEvent } from "../analytics";
|
||||||
|
|
||||||
|
import { ToolButton } from "./ToolButton";
|
||||||
|
|
||||||
|
import type { AppClassProperties } from "../types";
|
||||||
|
|
||||||
|
type ToolOption = {
|
||||||
|
type: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ToolTypePopupProps = {
|
||||||
|
app: AppClassProperties;
|
||||||
|
triggerElement: HTMLElement | null;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
currentType: string;
|
||||||
|
onChange?: (type: string) => void;
|
||||||
|
options: readonly ToolOption[];
|
||||||
|
className?: string;
|
||||||
|
namePrefix: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToolTypePopup = ({
|
||||||
|
app,
|
||||||
|
triggerElement,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onChange,
|
||||||
|
currentType,
|
||||||
|
options,
|
||||||
|
className = "Shape",
|
||||||
|
namePrefix,
|
||||||
|
}: ToolTypePopupProps) => {
|
||||||
|
const [panelPosition, setPanelPosition] = useState({ x: 0, y: 0 });
|
||||||
|
const panelRef = useRef<HTMLDivElement>(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 - panelWidth / 2,
|
||||||
|
y: panelHeight + 8,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
updatePosition();
|
||||||
|
|
||||||
|
// Outside click handling (capture pointer events for reliability on mobile)
|
||||||
|
const handlePointer = (event: PointerEvent) => {
|
||||||
|
const target = event.target as Node | null;
|
||||||
|
const panelEl = panelRef.current;
|
||||||
|
const triggerEl = triggerElement;
|
||||||
|
if (!target) {
|
||||||
|
onClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const insidePanel = !!panelEl && panelEl.contains(target);
|
||||||
|
const onTrigger = !!triggerEl && triggerEl.contains(target);
|
||||||
|
if (!insidePanel && !onTrigger) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("pointerdown", handlePointer, true);
|
||||||
|
document.addEventListener("pointerup", handlePointer, true);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("pointerdown", handlePointer, true);
|
||||||
|
document.removeEventListener("pointerup", handlePointer, true);
|
||||||
|
};
|
||||||
|
}, [isOpen, triggerElement, onClose]);
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={panelRef}
|
||||||
|
tabIndex={-1}
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
bottom: `${panelPosition.y}px`,
|
||||||
|
left: `${panelPosition.x}px`,
|
||||||
|
zIndex: 2,
|
||||||
|
}}
|
||||||
|
className={CLASSES.CONVERT_ELEMENT_TYPE_POPUP}
|
||||||
|
>
|
||||||
|
{options.map(({ type, icon, title }) => (
|
||||||
|
<ToolButton
|
||||||
|
className={className}
|
||||||
|
key={type}
|
||||||
|
type="radio"
|
||||||
|
icon={icon}
|
||||||
|
checked={currentType === type}
|
||||||
|
name={`${namePrefix}-option`}
|
||||||
|
title={title || capitalizeString(type)}
|
||||||
|
keyBindingLabel=""
|
||||||
|
aria-label={title || capitalizeString(type)}
|
||||||
|
data-testid={`toolbar-${type}`}
|
||||||
|
onChange={() => {
|
||||||
|
if (app.state.activeTool.type !== type) {
|
||||||
|
trackEvent("toolbar", type, "ui");
|
||||||
|
}
|
||||||
|
app.setActiveTool({ type: type as any });
|
||||||
|
onChange?.(type);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ToolWithPopupProps = {
|
||||||
|
app: AppClassProperties;
|
||||||
|
options: readonly ToolOption[];
|
||||||
|
activeTool: { type: string };
|
||||||
|
defaultOption: string;
|
||||||
|
className?: string;
|
||||||
|
namePrefix: string;
|
||||||
|
title: string;
|
||||||
|
"data-testid": string;
|
||||||
|
onToolChange: (type: string) => void;
|
||||||
|
getDisplayedOption: () => ToolOption;
|
||||||
|
isActive: boolean;
|
||||||
|
fillable?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToolWithPopup = ({
|
||||||
|
app,
|
||||||
|
options,
|
||||||
|
activeTool,
|
||||||
|
defaultOption,
|
||||||
|
className = "Shape",
|
||||||
|
namePrefix,
|
||||||
|
title,
|
||||||
|
"data-testid": dataTestId,
|
||||||
|
onToolChange,
|
||||||
|
getDisplayedOption,
|
||||||
|
isActive,
|
||||||
|
fillable = false,
|
||||||
|
}: ToolWithPopupProps) => {
|
||||||
|
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
||||||
|
const [triggerRef, setTriggerRef] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const displayedOption = getDisplayedOption();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative" }}>
|
||||||
|
<div ref={setTriggerRef}>
|
||||||
|
<ToolButton
|
||||||
|
className={clsx(className, { fillable })}
|
||||||
|
type="radio"
|
||||||
|
icon={displayedOption.icon}
|
||||||
|
checked={isActive}
|
||||||
|
name="editor-current-shape"
|
||||||
|
title={title}
|
||||||
|
aria-label={title}
|
||||||
|
data-testid={dataTestId}
|
||||||
|
onPointerDown={() => {
|
||||||
|
setIsPopupOpen((val) => !val);
|
||||||
|
onToolChange(defaultOption);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ToolTypePopup
|
||||||
|
app={app}
|
||||||
|
triggerElement={triggerRef}
|
||||||
|
isOpen={isPopupOpen}
|
||||||
|
onClose={() => setIsPopupOpen(false)}
|
||||||
|
options={options}
|
||||||
|
className={className}
|
||||||
|
namePrefix={namePrefix}
|
||||||
|
currentType={activeTool.type}
|
||||||
|
onChange={(type: string) => {
|
||||||
|
onToolChange(type);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -6,11 +6,30 @@
|
|||||||
top: 100%;
|
top: 100%;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
|
|
||||||
|
&--placement-top {
|
||||||
|
top: auto;
|
||||||
|
bottom: 100%;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
&--mobile {
|
&--mobile {
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
row-gap: 0.75rem;
|
row-gap: 0.75rem;
|
||||||
|
|
||||||
|
// When main menu is in the top toolbar, position relative to trigger
|
||||||
|
&.main-menu-dropdown {
|
||||||
|
position: fixed;
|
||||||
|
left: 1rem;
|
||||||
|
top: 3.5rem;
|
||||||
|
min-width: 280px;
|
||||||
|
max-width: calc(100vw - 2rem);
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
z-index: var(--zIndex-layerUI);
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-menu-container {
|
.dropdown-menu-container {
|
||||||
padding: 8px 8px;
|
padding: 8px 8px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
@ -17,16 +17,27 @@ import "./DropdownMenu.scss";
|
|||||||
const DropdownMenu = ({
|
const DropdownMenu = ({
|
||||||
children,
|
children,
|
||||||
open,
|
open,
|
||||||
|
placement,
|
||||||
}: {
|
}: {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
placement?: "top" | "bottom";
|
||||||
}) => {
|
}) => {
|
||||||
const MenuTriggerComp = getMenuTriggerComponent(children);
|
const MenuTriggerComp = getMenuTriggerComponent(children);
|
||||||
const MenuContentComp = getMenuContentComponent(children);
|
const MenuContentComp = getMenuContentComponent(children);
|
||||||
|
|
||||||
|
// clone the MenuContentComp to pass the placement prop
|
||||||
|
const MenuContentCompWithPlacement =
|
||||||
|
MenuContentComp && React.isValidElement(MenuContentComp)
|
||||||
|
? React.cloneElement(MenuContentComp as React.ReactElement<any>, {
|
||||||
|
placement,
|
||||||
|
})
|
||||||
|
: MenuContentComp;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{MenuTriggerComp}
|
{MenuTriggerComp}
|
||||||
{open && MenuContentComp}
|
{open && MenuContentCompWithPlacement}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,6 +17,7 @@ const MenuContent = ({
|
|||||||
className = "",
|
className = "",
|
||||||
onSelect,
|
onSelect,
|
||||||
style,
|
style,
|
||||||
|
placement = "bottom",
|
||||||
}: {
|
}: {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
onClickOutside?: () => void;
|
onClickOutside?: () => void;
|
||||||
@ -26,6 +27,7 @@ const MenuContent = ({
|
|||||||
*/
|
*/
|
||||||
onSelect?: (event: Event) => void;
|
onSelect?: (event: Event) => void;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
placement?: "top" | "bottom";
|
||||||
}) => {
|
}) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
@ -58,6 +60,7 @@ const MenuContent = ({
|
|||||||
|
|
||||||
const classNames = clsx(`dropdown-menu ${className}`, {
|
const classNames = clsx(`dropdown-menu ${className}`, {
|
||||||
"dropdown-menu--mobile": device.editor.isMobile,
|
"dropdown-menu--mobile": device.editor.isMobile,
|
||||||
|
"dropdown-menu--placement-top": placement === "top",
|
||||||
}).trim();
|
}).trim();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -53,6 +53,8 @@ const MainMenu = Object.assign(
|
|||||||
onSelect={composeEventHandlers(onSelect, () => {
|
onSelect={composeEventHandlers(onSelect, () => {
|
||||||
setAppState({ openMenu: null });
|
setAppState({ openMenu: null });
|
||||||
})}
|
})}
|
||||||
|
placement="bottom"
|
||||||
|
className={device.editor.isMobile ? "main-menu-dropdown" : ""}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{device.editor.isMobile && appState.collaborators.size > 0 && (
|
{device.editor.isMobile && appState.collaborators.size > 0 && (
|
||||||
|
|||||||
@ -235,27 +235,28 @@ body.excalidraw-cursor-resize * {
|
|||||||
z-index: var(--zIndex-layerUI);
|
z-index: var(--zIndex-layerUI);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-bottom-bar {
|
.App-bottom-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
// account for margins
|
||||||
|
width: calc(100% - 28px);
|
||||||
|
max-width: 400px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 50%;
|
||||||
right: 0;
|
transform: translateX(-50%);
|
||||||
--bar-padding: calc(4 * var(--space-factor));
|
--bar-padding: calc(4 * var(--space-factor));
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
flex-direction: column;
|
||||||
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
> .Island {
|
> .Island {
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
min-width: 100%;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
|
padding: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
pointer-events: var(--ui-pointerEvents);
|
pointer-events: var(--ui-pointerEvents);
|
||||||
@ -263,7 +264,8 @@ body.excalidraw-cursor-resize * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.App-toolbar {
|
.App-toolbar {
|
||||||
width: 100%;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
.eraser {
|
.eraser {
|
||||||
&.ToolIcon:hover {
|
&.ToolIcon:hover {
|
||||||
@ -278,14 +280,7 @@ body.excalidraw-cursor-resize * {
|
|||||||
|
|
||||||
.App-toolbar-content {
|
.App-toolbar-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
|
||||||
padding: 8px;
|
|
||||||
|
|
||||||
.dropdown-menu--mobile {
|
|
||||||
bottom: 55px;
|
|
||||||
top: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-mobile-menu {
|
.App-mobile-menu {
|
||||||
|
|||||||
@ -43,6 +43,10 @@
|
|||||||
--lg-icon-size: 1rem;
|
--lg-icon-size: 1rem;
|
||||||
--editor-container-padding: 1rem;
|
--editor-container-padding: 1rem;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
--editor-container-padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-device-width: 1921px) {
|
@media screen and (min-device-width: 1921px) {
|
||||||
--lg-button-size: 2.5rem;
|
--lg-button-size: 2.5rem;
|
||||||
--lg-icon-size: 1.25rem;
|
--lg-icon-size: 1.25rem;
|
||||||
|
|||||||
@ -6101,7 +6101,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"offsetTop": 0,
|
"offsetTop": 0,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": "elementBackground",
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
@ -11961,7 +11961,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
|||||||
"offsetTop": 0,
|
"offsetTop": 0,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": "elementStroke",
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
@ -15533,7 +15533,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
|
|||||||
"offsetTop": 0,
|
"offsetTop": 0,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": "elementBackground",
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
|
|||||||
@ -7384,7 +7384,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
|
|||||||
"offsetTop": 0,
|
"offsetTop": 0,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": "elementBackground",
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
|
|||||||
@ -448,7 +448,7 @@ export interface AppState {
|
|||||||
lockedMultiSelections: { [groupId: string]: true };
|
lockedMultiSelections: { [groupId: string]: true };
|
||||||
|
|
||||||
/** properties sidebar mode - determines whether to show compact or complete sidebar */
|
/** properties sidebar mode - determines whether to show compact or complete sidebar */
|
||||||
stylesPanelMode: "compact" | "full";
|
stylesPanelMode: "compact" | "full" | "mobile";
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SearchMatch = {
|
export type SearchMatch = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user