feat: introduce layout (incomplete)

This commit is contained in:
Ryan Di
2025-07-21 16:07:35 +10:00
parent 89246d96a5
commit b7eb1afe2f
15 changed files with 708 additions and 45 deletions

View File

@@ -69,7 +69,7 @@ export const actionChangeViewBackgroundColor = register({
: CaptureUpdateAction.EVENTUALLY,
};
},
PanelComponent: ({ elements, appState, updateData, appProps }) => {
PanelComponent: ({ elements, appState, updateData, appProps, data }) => {
// FIXME move me to src/components/mainMenu/DefaultItems.tsx
return (
<ColorPicker
@@ -83,6 +83,7 @@ export const actionChangeViewBackgroundColor = register({
elements={elements}
appState={appState}
updateData={updateData}
compactMode={data?.compactMode}
/>
);
},

View File

@@ -78,6 +78,10 @@ export const actionToggleLinearEditor = register({
selectedElementIds: appState.selectedElementIds,
})[0] as ExcalidrawLinearElement;
if (!selectedElement) {
return null;
}
const label = t(
selectedElement.type === "arrow"
? "labels.lineEditor.editArrow"

View File

@@ -321,9 +321,9 @@ export const actionChangeStrokeColor = register({
: CaptureUpdateAction.EVENTUALLY,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData, app, data }) => (
<>
<h3 aria-hidden="true">{t("labels.stroke")}</h3>
{!data?.compactMode && <h3 aria-hidden="true">{t("labels.stroke")}</h3>}
<ColorPicker
topPicks={DEFAULT_ELEMENT_STROKE_PICKS}
palette={DEFAULT_ELEMENT_STROKE_COLOR_PALETTE}
@@ -341,6 +341,7 @@ export const actionChangeStrokeColor = register({
elements={elements}
appState={appState}
updateData={updateData}
compactMode={data?.compactMode}
/>
</>
),
@@ -398,9 +399,11 @@ export const actionChangeBackgroundColor = register({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData, app, data }) => (
<>
<h3 aria-hidden="true">{t("labels.background")}</h3>
{!data?.compactMode && (
<h3 aria-hidden="true">{t("labels.background")}</h3>
)}
<ColorPicker
topPicks={DEFAULT_ELEMENT_BACKGROUND_PICKS}
palette={DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE}
@@ -418,6 +421,7 @@ export const actionChangeBackgroundColor = register({
elements={elements}
appState={appState}
updateData={updateData}
compactMode={data?.compactMode}
/>
</>
),
@@ -518,9 +522,9 @@ export const actionChangeStrokeWidth = register({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData, app, data }) => (
<fieldset>
<legend>{t("labels.strokeWidth")}</legend>
{!data?.compactMode && <legend>{t("labels.strokeWidth")}</legend>}
<div className="buttonList">
<RadioSelection
group="stroke-width"
@@ -575,9 +579,9 @@ export const actionChangeSloppiness = register({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData, app, data }) => (
<fieldset>
<legend>{t("labels.sloppiness")}</legend>
{!data?.compactMode && <legend>{t("labels.sloppiness")}</legend>}
<div className="buttonList">
<RadioSelection
group="sloppiness"
@@ -628,9 +632,9 @@ export const actionChangeStrokeStyle = register({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData, app, data }) => (
<fieldset>
<legend>{t("labels.strokeStyle")}</legend>
{!data?.compactMode && <legend>{t("labels.strokeStyle")}</legend>}
<div className="buttonList">
<RadioSelection
group="strokeStyle"
@@ -1016,7 +1020,7 @@ export const actionChangeFontFamily = register({
return result;
},
PanelComponent: ({ elements, appState, app, updateData }) => {
PanelComponent: ({ elements, appState, app, updateData, data }) => {
const cachedElementsRef = useRef<ElementsMap>(new Map());
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
@@ -1094,11 +1098,12 @@ export const actionChangeFontFamily = register({
return (
<fieldset>
<legend>{t("labels.fontFamily")}</legend>
{!data?.compactMode && <legend>{t("labels.fontFamily")}</legend>}
<FontPicker
isOpened={appState.openPopup === "fontFamily"}
selectedFontFamily={selectedFontFamily}
hoveredFontFamily={appState.currentHoveredFontFamily}
compactMode={data?.compactMode}
onSelect={(fontFamily) => {
setBatchedData({
openPopup: null,
@@ -1616,6 +1621,25 @@ export const actionChangeArrowhead = register({
},
});
export const actionChangeArrowProperties = register({
name: "changeArrowProperties",
label: "Change arrow properties",
trackEvent: false,
perform: (elements, appState, value, app) => {
// This action doesn't perform any changes directly
// It's just a container for the arrow type and arrowhead actions
return false;
},
PanelComponent: ({ elements, appState, updateData, app, renderAction }) => {
return (
<div className="selected-shape-actions">
{renderAction("changeArrowType")}
{renderAction("changeArrowhead")}
</div>
);
},
});
export const actionChangeArrowType = register({
name: "changeArrowType",
label: "Change arrow types",

View File

@@ -18,6 +18,7 @@ export {
actionChangeFontFamily,
actionChangeTextAlign,
actionChangeVerticalAlign,
actionChangeArrowProperties,
} from "./actionProperties";
export {

View File

@@ -180,6 +180,7 @@ export class ActionManager {
app={this.app}
data={data}
renderAction={this.renderAction}
compactMode={Boolean(data?.compactMode)}
/>
);
}

View File

@@ -69,6 +69,7 @@ export type ActionName =
| "changeStrokeStyle"
| "changeArrowhead"
| "changeArrowType"
| "changeArrowProperties"
| "changeOpacity"
| "changeFontSize"
| "toggleCanvasMenu"

View File

@@ -91,3 +91,106 @@
}
}
}
.compact-shape-actions {
display: flex;
flex-direction: column;
gap: 0.5rem;
max-height: calc(100vh - 200px);
overflow-y: auto;
padding: 0.5rem;
.compact-action-item {
position: relative;
display: flex;
justify-content: center;
align-items: center;
min-height: 2.5rem;
.compact-action-button {
width: 2rem;
height: 2rem;
border: none;
border-radius: var(--border-radius-lg);
background: var(--button-bg, var(--island-bg-color));
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
svg {
width: 16px;
height: 16px;
}
&:hover {
background: var(--button-hover-bg, var(--island-bg-color));
border-color: var(
--button-hover-border,
var(--button-border, var(--default-border-color))
);
}
&:active {
background: var(--button-active-bg, var(--island-bg-color));
border-color: var(--button-active-border, var(--color-primary-darkest));
}
}
.compact-popover-content {
.popover-section {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
.popover-section-title {
font-size: 0.75rem;
font-weight: 600;
color: var(--color-text-secondary);
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.buttonList {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
}
}
}
}
.compact-shape-actions-island {
width: fit-content;
overflow-x: hidden;
}
.compact-popover-content {
.popover-section {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
.popover-section-title {
font-size: 0.75rem;
font-weight: 600;
color: var(--color-text-secondary);
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.buttonList {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
}
}

View File

@@ -1,5 +1,6 @@
import clsx from "clsx";
import { useState } from "react";
import { useCallback, useState } from "react";
import * as Popover from "@radix-ui/react-popover";
import {
CLASSES,
@@ -19,6 +20,8 @@ import {
isImageElement,
isLinearElement,
isTextElement,
getBoundTextElement,
isArrowElement,
} from "@excalidraw/element";
import { hasStrokeColor, toolIsArrow } from "@excalidraw/element";
@@ -46,15 +49,19 @@ import {
hasStrokeWidth,
} from "../scene";
import { getFormValue } from "../actions/actionProperties";
import { SHAPES } from "./shapes";
import "./Actions.scss";
import { useDevice } from "./App";
import { useDevice, useExcalidrawContainer } from "./App";
import Stack from "./Stack";
import { ToolButton } from "./ToolButton";
import { Tooltip } from "./Tooltip";
import DropdownMenu from "./dropdownMenu/DropdownMenu";
import { PropertiesPopover } from "./PropertiesPopover";
import { RadioSelection } from "./RadioSelection";
import {
EmbedIcon,
extraToolsIcon,
@@ -63,9 +70,21 @@ import {
laserPointerToolIcon,
MagicIcon,
LassoIcon,
sharpArrowIcon,
roundArrowIcon,
elbowArrowIcon,
TextSizeIcon,
resizeIcon,
settingsPlusIcon,
} from "./icons";
import type { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
import type {
AppClassProperties,
AppProps,
UIAppState,
Zoom,
AppState,
} from "../types";
import type { ActionManager } from "../actions/manager";
export const canChangeStrokeColor = (
@@ -280,6 +299,365 @@ export const SelectedShapeActions = ({
);
};
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 [strokePopoverOpen, setStrokePopoverOpen] = useState(false);
const [otherActionsPopoverOpen, setOtherActionsPopoverOpen] = useState(false);
const fontSizePopoverOpen = appState.openPopup === "fontSize";
const setFontSizePopoverOpen = useCallback(
(open: boolean) => {
setAppState({ openPopup: open ? "fontSize" : null });
},
[setAppState],
);
const { container } = useExcalidrawContainer();
const isEditingTextOrNewElement = Boolean(
appState.editingTextElement || appState.newElement,
);
const showFillIcons =
(hasBackground(appState.activeTool.type) &&
!isTransparent(appState.currentItemBackgroundColor)) ||
targetElements.some(
(element) =>
hasBackground(element.type) && !isTransparent(element.backgroundColor),
);
const showLinkIcon = targetElements.length === 1;
const showLineEditorAction =
!appState.editingLinearElement &&
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", { compactMode: true })}
</div>
)}
{/* Background Color */}
{canChangeBackgroundColor(appState, targetElements) && (
<div className="compact-action-item">
{renderAction("changeBackgroundColor", { compactMode: true })}
</div>
)}
{/* Combined Properties (Fill, Stroke, Opacity) */}
{(showFillIcons ||
hasStrokeWidth(appState.activeTool.type) ||
targetElements.some((element) => hasStrokeWidth(element.type)) ||
hasStrokeStyle(appState.activeTool.type) ||
targetElements.some((element) => hasStrokeStyle(element.type)) ||
canChangeRoundness(appState.activeTool.type) ||
targetElements.some((element) => canChangeRoundness(element.type))) && (
<div className="compact-action-item">
<Popover.Root
open={strokePopoverOpen}
onOpenChange={setStrokePopoverOpen}
>
<Popover.Trigger asChild>
<button
type="button"
className="compact-action-button"
title={t("labels.stroke")}
>
{resizeIcon}
</button>
</Popover.Trigger>
{strokePopoverOpen && (
<PropertiesPopover
container={container}
style={{ maxWidth: "13rem" }}
onClose={() => setStrokePopoverOpen(false)}
>
<div className="selected-shape-actions">
{showFillIcons && renderAction("changeFillStyle")}
{(hasStrokeWidth(appState.activeTool.type) ||
targetElements.some((element) =>
hasStrokeWidth(element.type),
)) &&
renderAction("changeStrokeWidth")}
{(hasStrokeStyle(appState.activeTool.type) ||
targetElements.some((element) =>
hasStrokeStyle(element.type),
)) && (
<>
{renderAction("changeStrokeStyle")}
{renderAction("changeSloppiness")}
</>
)}
{(canChangeRoundness(appState.activeTool.type) ||
targetElements.some((element) =>
canChangeRoundness(element.type),
)) &&
renderAction("changeRoundness")}
{renderAction("changeOpacity")}
</div>
</PropertiesPopover>
)}
</Popover.Root>
</div>
)}
{/* Combined Arrow Properties */}
{(toolIsArrow(appState.activeTool.type) ||
targetElements.some((element) => toolIsArrow(element.type))) && (
<div className="compact-action-item">
<Popover.Root
open={appState.openPopup === "arrowProperties"}
onOpenChange={(open) => {
setAppState({ openPopup: open ? "arrowProperties" : null });
}}
>
<Popover.Trigger asChild>
<button
type="button"
className="compact-action-button"
title={t("labels.arrowtypes")}
>
{(() => {
// Show an icon based on the current arrow type
const arrowType = getFormValue(
targetElements,
app,
(element) => {
if (isArrowElement(element)) {
return element.elbowed
? "elbow"
: element.roundness
? "round"
: "sharp";
}
return null;
},
(element) => isArrowElement(element),
(hasSelection) =>
hasSelection ? null : appState.currentItemArrowType,
);
if (arrowType === "elbow") {
return elbowArrowIcon;
}
if (arrowType === "round") {
return roundArrowIcon;
}
return sharpArrowIcon; // default
})()}
</button>
</Popover.Trigger>
{appState.openPopup === "arrowProperties" && (
<PropertiesPopover
container={container}
style={{ maxWidth: "13rem" }}
onClose={() => setAppState({ openPopup: null })}
>
{renderAction("changeArrowProperties")}
</PropertiesPopover>
)}
</Popover.Root>
</div>
)}
{/* Linear Editor */}
{showLineEditorAction && (
<div className="compact-action-item">
{renderAction("toggleLinearEditor", { compactMode: true })}
</div>
)}
{/* Text Properties */}
{(appState.activeTool.type === "text" ||
targetElements.some(isTextElement)) && (
<>
<div className="compact-action-item">
{renderAction("changeFontFamily", {
compactMode: true,
})}
</div>
<div className="compact-action-item">
<Popover.Root
open={appState.openPopup === "textAlign"}
onOpenChange={(open) => {
setAppState({ openPopup: open ? "textAlign" : null });
}}
>
<Popover.Trigger asChild>
<button
type="button"
className="compact-action-button"
title={t("labels.textAlign")}
>
{TextSizeIcon}
</button>
</Popover.Trigger>
{appState.openPopup === "textAlign" && (
<PropertiesPopover
container={container}
style={{ maxWidth: "13rem" }}
onClose={() => setAppState({ openPopup: null })}
>
<div className="selected-shape-actions">
{(appState.activeTool.type === "text" ||
suppportsHorizontalAlign(targetElements, elementsMap)) &&
renderAction("changeTextAlign")}
{shouldAllowVerticalAlign(targetElements, elementsMap) &&
renderAction("changeVerticalAlign")}
{(appState.activeTool.type === "text" ||
targetElements.some(isTextElement)) &&
renderAction("changeFontSize")}
</div>
</PropertiesPopover>
)}
</Popover.Root>
</div>
</>
)}
{/* Dedicated Copy Button */}
{!isEditingTextOrNewElement && targetElements.length > 0 && (
<div className="compact-action-item">
{renderAction("duplicateSelection", { compactMode: true })}
</div>
)}
{/* Dedicated Delete Button */}
{!isEditingTextOrNewElement && targetElements.length > 0 && (
<div className="compact-action-item">
{renderAction("deleteSelectedElements", { compactMode: true })}
</div>
)}
{/* Combined Other Actions */}
{!isEditingTextOrNewElement && targetElements.length > 0 && (
<div className="compact-action-item">
<Popover.Root
open={otherActionsPopoverOpen}
onOpenChange={setOtherActionsPopoverOpen}
>
<Popover.Trigger asChild>
<button
type="button"
className="compact-action-button"
title={t("labels.actions")}
>
{settingsPlusIcon}
</button>
</Popover.Trigger>
{otherActionsPopoverOpen && (
<PropertiesPopover
container={container}
style={{
maxWidth: "12rem",
// center the popover content
justifyContent: "center",
alignItems: "center",
}}
onClose={() => setOtherActionsPopoverOpen(false)}
>
<div className="selected-shape-actions">
<fieldset>
<legend>{t("labels.layers")}</legend>
<div className="buttonList">
{renderAction("sendToBack")}
{renderAction("sendBackward")}
{renderAction("bringForward")}
{renderAction("bringToFront")}
</div>
</fieldset>
{showAlignActions && !isSingleElementBoundContainer && (
<fieldset>
<legend>{t("labels.align")}</legend>
<div className="buttonList">
{isRTL ? (
<>
{renderAction("alignRight")}
{renderAction("alignHorizontallyCentered")}
{renderAction("alignLeft")}
</>
) : (
<>
{renderAction("alignLeft")}
{renderAction("alignHorizontallyCentered")}
{renderAction("alignRight")}
</>
)}
{targetElements.length > 2 &&
renderAction("distributeHorizontally")}
{/* breaks the row ˇˇ */}
<div style={{ flexBasis: "100%", height: 0 }} />
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: ".5rem",
marginTop: "-0.5rem",
}}
>
{renderAction("alignTop")}
{renderAction("alignVerticallyCentered")}
{renderAction("alignBottom")}
{targetElements.length > 2 &&
renderAction("distributeVertically")}
</div>
</div>
</fieldset>
)}
<fieldset>
<legend>{t("labels.actions")}</legend>
<div className="buttonList">
{renderAction("group")}
{renderAction("ungroup")}
{showLinkIcon && renderAction("hyperlink")}
{showCropEditorAction && renderAction("cropEditor")}
</div>
</fieldset>
</div>
</PropertiesPopover>
)}
</Popover.Root>
</div>
)}
</div>
);
};
export const ShapesSwitcher = ({
activeTool,
appState,

View File

@@ -86,6 +86,16 @@
}
}
.color-picker__button-background {
display: flex;
align-items: center;
justify-content: center;
svg {
width: 100%;
height: 100%;
}
}
&.active {
.color-picker__button-outline {
position: absolute;

View File

@@ -18,7 +18,7 @@ import { useExcalidrawContainer } from "../App";
import { ButtonSeparator } from "../ButtonSeparator";
import { activeEyeDropperAtom } from "../EyeDropper";
import { PropertiesPopover } from "../PropertiesPopover";
import { slashIcon } from "../icons";
import { backgroundIcon, slashIcon, strokeIcon } from "../icons";
import { ColorInput } from "./ColorInput";
import { Picker } from "./Picker";
@@ -189,10 +189,14 @@ const ColorPickerTrigger = ({
label,
color,
type,
compactMode = false,
mode = "background",
}: {
color: string | null;
label: string;
type: ColorPickerType;
compactMode?: boolean;
mode?: "background" | "stroke";
}) => {
return (
<Popover.Trigger
@@ -211,6 +215,33 @@ const ColorPickerTrigger = ({
}
>
<div className="color-picker__button-outline">{!color && slashIcon}</div>
{compactMode && color && (
<div className="color-picker__button-background">
{mode === "background" ? (
<span
style={{
color:
color && isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD)
? "#fff"
: "#111",
}}
>
{backgroundIcon}
</span>
) : (
<span
style={{
color:
color && isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD)
? "#fff"
: "#111",
}}
>
{strokeIcon}
</span>
)}
</div>
)}
</Popover.Trigger>
);
};
@@ -252,7 +283,13 @@ export const ColorPicker = ({
}}
>
{/* serves as an active color indicator as well */}
<ColorPickerTrigger color={color} label={label} type={type} />
<ColorPickerTrigger
color={color}
label={label}
type={type}
compactMode={compactMode}
mode={type === "elementStroke" ? "stroke" : "background"}
/>
{/* popup content */}
{appState.openPopup === type && (
<ColorPickerPopupContent

View File

@@ -11,5 +11,10 @@
2rem + 4 * var(--default-button-size)
); // 4 gaps + 4 buttons
}
&--compact {
display: block;
grid-template-columns: none;
}
}
}

View File

@@ -1,4 +1,5 @@
import * as Popover from "@radix-ui/react-popover";
import clsx from "clsx";
import React, { useCallback, useMemo } from "react";
import { FONT_FAMILY } from "@excalidraw/common";
@@ -83,7 +84,13 @@ export const FontPicker = React.memo(
);
return (
<div role="dialog" aria-modal="true" className="FontPicker__container">
<div
role="dialog"
aria-modal="true"
className={clsx("FontPicker__container", {
"FontPicker__container--compact": compactMode,
})}
>
{!compactMode && (
<div className="buttonList">
<RadioSelection<FontFamilyValues | false>

View File

@@ -28,7 +28,11 @@ import { useAtom, useAtomValue } from "../editor-jotai";
import { t } from "../i18n";
import { calculateScrollCenter } from "../scene";
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
import {
SelectedShapeActions,
ShapesSwitcher,
CompactShapeActions,
} from "./Actions";
import { LoadingMessage } from "./LoadingMessage";
import { LockButton } from "./LockButton";
import { MobileMenu } from "./MobileMenu";
@@ -209,31 +213,58 @@ const LayerUI = ({
</div>
);
const renderSelectedShapeActions = () => (
<Section
heading="selectedShapeActions"
className={clsx("selected-shape-actions zen-mode-transition", {
"transition-left": appState.zenModeEnabled,
})}
>
<Island
className={CLASSES.SHAPE_ACTIONS_MENU}
padding={2}
style={{
// we want to make sure this doesn't overflow so subtracting the
// approximate height of hamburgerMenu + footer
maxHeight: `${appState.height - 166}px`,
}}
const renderSelectedShapeActions = (isTablet: boolean) => {
// Use compact layout for tablets (when device is mobile but not too small)
return (
<Section
heading="selectedShapeActions"
className={clsx("selected-shape-actions zen-mode-transition", {
"transition-left": appState.zenModeEnabled,
})}
>
<SelectedShapeActions
appState={appState}
elementsMap={app.scene.getNonDeletedElementsMap()}
renderAction={actionManager.renderAction}
app={app}
/>
</Island>
</Section>
);
{isTablet ? (
<Island
className={clsx(
// CLASSES.SHAPE_ACTIONS_MENU,
"compact-shape-actions-island",
)}
padding={0}
style={{
// we want to make sure this doesn't overflow so subtracting the
// approximate height of hamburgerMenu + footer
maxHeight: `${appState.height - 166}px`,
}}
>
<CompactShapeActions
appState={appState}
elementsMap={app.scene.getNonDeletedElementsMap()}
renderAction={actionManager.renderAction}
app={app}
setAppState={setAppState}
/>
</Island>
) : (
<Island
className={CLASSES.SHAPE_ACTIONS_MENU}
padding={2}
style={{
// we want to make sure this doesn't overflow so subtracting the
// approximate height of hamburgerMenu + footer
maxHeight: `${appState.height - 166}px`,
}}
>
<SelectedShapeActions
appState={appState}
elementsMap={app.scene.getNonDeletedElementsMap()}
renderAction={actionManager.renderAction}
app={app}
/>
</Island>
)}
</Section>
);
};
const renderFixedSideContainer = () => {
const shouldRenderSelectedShapeActions = showSelectedShapeActions(
@@ -252,7 +283,8 @@ const LayerUI = ({
<div className="App-menu App-menu_top">
<Stack.Col gap={6} className={clsx("App-menu_top__left")}>
{renderCanvasActions()}
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
{shouldRenderSelectedShapeActions &&
renderSelectedShapeActions(device.isTouchScreen)}
</Stack.Col>
{!appState.viewModeEnabled &&
appState.openDialog?.name !== "elementLinkSelector" && (

View File

@@ -396,6 +396,19 @@ export const TextIcon = createIcon(
tablerIconProps,
);
export const TextSizeIcon = createIcon(
<g stroke="currentColor" strokeWidth="1.5">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 7v-2h13v2" />
<path d="M10 5v14" />
<path d="M12 19h-4" />
<path d="M15 13v-1h6v1" />
<path d="M18 12v7" />
<path d="M17 19h2" />
</g>,
tablerIconProps,
);
// modified tabler-icons: photo
export const ImageIcon = createIcon(
<g strokeWidth="1.25">
@@ -2269,3 +2282,46 @@ export const elementLinkIcon = createIcon(
</g>,
tablerIconProps,
);
export const resizeIcon = createIcon(
<g strokeWidth={1.5}>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M4 11v8a1 1 0 0 0 1 1h8m-9 -14v-1a1 1 0 0 1 1 -1h1m5 0h2m5 0h1a1 1 0 0 1 1 1v1m0 5v2m0 5v1a1 1 0 0 1 -1 1h-1" />
<path d="M4 12h7a1 1 0 0 1 1 1v7" />
</g>,
tablerIconProps,
);
export const settingsPlusIcon = createIcon(
<g strokeWidth={1.5}>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12.483 20.935c-.862 .239 -1.898 -.178 -2.158 -1.252a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.08 .262 1.496 1.308 1.247 2.173" />
<path d="M16 19h6" />
<path d="M19 16v6" />
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
</g>,
tablerIconProps,
);
export const backgroundIcon = createIcon(
<g strokeWidth={1.5}>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M4 8l4 -4" />
<path d="M14 4l-10 10" />
<path d="M4 20l16 -16" />
<path d="M20 10l-10 10" />
<path d="M20 16l-4 4" />
</g>,
tablerIconProps,
);
export const strokeIcon = createIcon(
<g strokeWidth={1.5}>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M4 8v8" />
<path d="M20 16v-8" />
<path d="M8 4h8" />
<path d="M8 20h8" />
</g>,
tablerIconProps,
);

View File

@@ -354,6 +354,9 @@ export interface AppState {
| "elementBackground"
| "elementStroke"
| "fontFamily"
| "fontSize"
| "textAlign"
| "arrowProperties"
| null;
openSidebar: { name: SidebarName; tab?: SidebarTabName } | null;
openDialog: