mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-20 16:00:38 +02:00
feat: introduce layout (incomplete)
This commit is contained in:
@@ -69,7 +69,7 @@ export const actionChangeViewBackgroundColor = register({
|
|||||||
: CaptureUpdateAction.EVENTUALLY,
|
: CaptureUpdateAction.EVENTUALLY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, appProps }) => {
|
PanelComponent: ({ elements, appState, updateData, appProps, data }) => {
|
||||||
// FIXME move me to src/components/mainMenu/DefaultItems.tsx
|
// FIXME move me to src/components/mainMenu/DefaultItems.tsx
|
||||||
return (
|
return (
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
@@ -83,6 +83,7 @@ export const actionChangeViewBackgroundColor = register({
|
|||||||
elements={elements}
|
elements={elements}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
|
compactMode={data?.compactMode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@@ -78,6 +78,10 @@ export const actionToggleLinearEditor = register({
|
|||||||
selectedElementIds: appState.selectedElementIds,
|
selectedElementIds: appState.selectedElementIds,
|
||||||
})[0] as ExcalidrawLinearElement;
|
})[0] as ExcalidrawLinearElement;
|
||||||
|
|
||||||
|
if (!selectedElement) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const label = t(
|
const label = t(
|
||||||
selectedElement.type === "arrow"
|
selectedElement.type === "arrow"
|
||||||
? "labels.lineEditor.editArrow"
|
? "labels.lineEditor.editArrow"
|
||||||
|
@@ -321,9 +321,9 @@ export const actionChangeStrokeColor = register({
|
|||||||
: CaptureUpdateAction.EVENTUALLY,
|
: 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
|
<ColorPicker
|
||||||
topPicks={DEFAULT_ELEMENT_STROKE_PICKS}
|
topPicks={DEFAULT_ELEMENT_STROKE_PICKS}
|
||||||
palette={DEFAULT_ELEMENT_STROKE_COLOR_PALETTE}
|
palette={DEFAULT_ELEMENT_STROKE_COLOR_PALETTE}
|
||||||
@@ -341,6 +341,7 @@ export const actionChangeStrokeColor = register({
|
|||||||
elements={elements}
|
elements={elements}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
|
compactMode={data?.compactMode}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -398,9 +399,11 @@ export const actionChangeBackgroundColor = register({
|
|||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app, data }) => (
|
||||||
<>
|
<>
|
||||||
|
{!data?.compactMode && (
|
||||||
<h3 aria-hidden="true">{t("labels.background")}</h3>
|
<h3 aria-hidden="true">{t("labels.background")}</h3>
|
||||||
|
)}
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
topPicks={DEFAULT_ELEMENT_BACKGROUND_PICKS}
|
topPicks={DEFAULT_ELEMENT_BACKGROUND_PICKS}
|
||||||
palette={DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE}
|
palette={DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE}
|
||||||
@@ -418,6 +421,7 @@ export const actionChangeBackgroundColor = register({
|
|||||||
elements={elements}
|
elements={elements}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
|
compactMode={data?.compactMode}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -518,9 +522,9 @@ export const actionChangeStrokeWidth = register({
|
|||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app, data }) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.strokeWidth")}</legend>
|
{!data?.compactMode && <legend>{t("labels.strokeWidth")}</legend>}
|
||||||
<div className="buttonList">
|
<div className="buttonList">
|
||||||
<RadioSelection
|
<RadioSelection
|
||||||
group="stroke-width"
|
group="stroke-width"
|
||||||
@@ -575,9 +579,9 @@ export const actionChangeSloppiness = register({
|
|||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app, data }) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.sloppiness")}</legend>
|
{!data?.compactMode && <legend>{t("labels.sloppiness")}</legend>}
|
||||||
<div className="buttonList">
|
<div className="buttonList">
|
||||||
<RadioSelection
|
<RadioSelection
|
||||||
group="sloppiness"
|
group="sloppiness"
|
||||||
@@ -628,9 +632,9 @@ export const actionChangeStrokeStyle = register({
|
|||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app, data }) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.strokeStyle")}</legend>
|
{!data?.compactMode && <legend>{t("labels.strokeStyle")}</legend>}
|
||||||
<div className="buttonList">
|
<div className="buttonList">
|
||||||
<RadioSelection
|
<RadioSelection
|
||||||
group="strokeStyle"
|
group="strokeStyle"
|
||||||
@@ -1016,7 +1020,7 @@ export const actionChangeFontFamily = register({
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, app, updateData }) => {
|
PanelComponent: ({ elements, appState, app, updateData, data }) => {
|
||||||
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
|
||||||
@@ -1094,11 +1098,12 @@ export const actionChangeFontFamily = register({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.fontFamily")}</legend>
|
{!data?.compactMode && <legend>{t("labels.fontFamily")}</legend>}
|
||||||
<FontPicker
|
<FontPicker
|
||||||
isOpened={appState.openPopup === "fontFamily"}
|
isOpened={appState.openPopup === "fontFamily"}
|
||||||
selectedFontFamily={selectedFontFamily}
|
selectedFontFamily={selectedFontFamily}
|
||||||
hoveredFontFamily={appState.currentHoveredFontFamily}
|
hoveredFontFamily={appState.currentHoveredFontFamily}
|
||||||
|
compactMode={data?.compactMode}
|
||||||
onSelect={(fontFamily) => {
|
onSelect={(fontFamily) => {
|
||||||
setBatchedData({
|
setBatchedData({
|
||||||
openPopup: null,
|
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({
|
export const actionChangeArrowType = register({
|
||||||
name: "changeArrowType",
|
name: "changeArrowType",
|
||||||
label: "Change arrow types",
|
label: "Change arrow types",
|
||||||
|
@@ -18,6 +18,7 @@ export {
|
|||||||
actionChangeFontFamily,
|
actionChangeFontFamily,
|
||||||
actionChangeTextAlign,
|
actionChangeTextAlign,
|
||||||
actionChangeVerticalAlign,
|
actionChangeVerticalAlign,
|
||||||
|
actionChangeArrowProperties,
|
||||||
} from "./actionProperties";
|
} from "./actionProperties";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@@ -180,6 +180,7 @@ export class ActionManager {
|
|||||||
app={this.app}
|
app={this.app}
|
||||||
data={data}
|
data={data}
|
||||||
renderAction={this.renderAction}
|
renderAction={this.renderAction}
|
||||||
|
compactMode={Boolean(data?.compactMode)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -69,6 +69,7 @@ export type ActionName =
|
|||||||
| "changeStrokeStyle"
|
| "changeStrokeStyle"
|
||||||
| "changeArrowhead"
|
| "changeArrowhead"
|
||||||
| "changeArrowType"
|
| "changeArrowType"
|
||||||
|
| "changeArrowProperties"
|
||||||
| "changeOpacity"
|
| "changeOpacity"
|
||||||
| "changeFontSize"
|
| "changeFontSize"
|
||||||
| "toggleCanvasMenu"
|
| "toggleCanvasMenu"
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CLASSES,
|
CLASSES,
|
||||||
@@ -19,6 +20,8 @@ import {
|
|||||||
isImageElement,
|
isImageElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
|
getBoundTextElement,
|
||||||
|
isArrowElement,
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import { hasStrokeColor, toolIsArrow } from "@excalidraw/element";
|
import { hasStrokeColor, toolIsArrow } from "@excalidraw/element";
|
||||||
@@ -46,15 +49,19 @@ import {
|
|||||||
hasStrokeWidth,
|
hasStrokeWidth,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
|
|
||||||
|
import { getFormValue } from "../actions/actionProperties";
|
||||||
|
|
||||||
import { SHAPES } from "./shapes";
|
import { SHAPES } from "./shapes";
|
||||||
|
|
||||||
import "./Actions.scss";
|
import "./Actions.scss";
|
||||||
|
|
||||||
import { useDevice } from "./App";
|
import { useDevice, useExcalidrawContainer } from "./App";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { Tooltip } from "./Tooltip";
|
import { Tooltip } from "./Tooltip";
|
||||||
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
||||||
|
import { PropertiesPopover } from "./PropertiesPopover";
|
||||||
|
import { RadioSelection } from "./RadioSelection";
|
||||||
import {
|
import {
|
||||||
EmbedIcon,
|
EmbedIcon,
|
||||||
extraToolsIcon,
|
extraToolsIcon,
|
||||||
@@ -63,9 +70,21 @@ import {
|
|||||||
laserPointerToolIcon,
|
laserPointerToolIcon,
|
||||||
MagicIcon,
|
MagicIcon,
|
||||||
LassoIcon,
|
LassoIcon,
|
||||||
|
sharpArrowIcon,
|
||||||
|
roundArrowIcon,
|
||||||
|
elbowArrowIcon,
|
||||||
|
TextSizeIcon,
|
||||||
|
resizeIcon,
|
||||||
|
settingsPlusIcon,
|
||||||
} from "./icons";
|
} 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";
|
import type { ActionManager } from "../actions/manager";
|
||||||
|
|
||||||
export const canChangeStrokeColor = (
|
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 = ({
|
export const ShapesSwitcher = ({
|
||||||
activeTool,
|
activeTool,
|
||||||
appState,
|
appState,
|
||||||
|
@@ -86,6 +86,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.color-picker__button-background {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
.color-picker__button-outline {
|
.color-picker__button-outline {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@@ -18,7 +18,7 @@ import { useExcalidrawContainer } from "../App";
|
|||||||
import { ButtonSeparator } from "../ButtonSeparator";
|
import { ButtonSeparator } from "../ButtonSeparator";
|
||||||
import { activeEyeDropperAtom } from "../EyeDropper";
|
import { activeEyeDropperAtom } from "../EyeDropper";
|
||||||
import { PropertiesPopover } from "../PropertiesPopover";
|
import { PropertiesPopover } from "../PropertiesPopover";
|
||||||
import { slashIcon } from "../icons";
|
import { backgroundIcon, slashIcon, strokeIcon } from "../icons";
|
||||||
|
|
||||||
import { ColorInput } from "./ColorInput";
|
import { ColorInput } from "./ColorInput";
|
||||||
import { Picker } from "./Picker";
|
import { Picker } from "./Picker";
|
||||||
@@ -189,10 +189,14 @@ const ColorPickerTrigger = ({
|
|||||||
label,
|
label,
|
||||||
color,
|
color,
|
||||||
type,
|
type,
|
||||||
|
compactMode = false,
|
||||||
|
mode = "background",
|
||||||
}: {
|
}: {
|
||||||
color: string | null;
|
color: string | null;
|
||||||
label: string;
|
label: string;
|
||||||
type: ColorPickerType;
|
type: ColorPickerType;
|
||||||
|
compactMode?: boolean;
|
||||||
|
mode?: "background" | "stroke";
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Popover.Trigger
|
<Popover.Trigger
|
||||||
@@ -211,6 +215,33 @@ const ColorPickerTrigger = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="color-picker__button-outline">{!color && slashIcon}</div>
|
<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>
|
</Popover.Trigger>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -252,7 +283,13 @@ export const ColorPicker = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* serves as an active color indicator as well */}
|
{/* 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 */}
|
{/* popup content */}
|
||||||
{appState.openPopup === type && (
|
{appState.openPopup === type && (
|
||||||
<ColorPickerPopupContent
|
<ColorPickerPopupContent
|
||||||
|
@@ -11,5 +11,10 @@
|
|||||||
2rem + 4 * var(--default-button-size)
|
2rem + 4 * var(--default-button-size)
|
||||||
); // 4 gaps + 4 buttons
|
); // 4 gaps + 4 buttons
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--compact {
|
||||||
|
display: block;
|
||||||
|
grid-template-columns: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import * as Popover from "@radix-ui/react-popover";
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
|
import clsx from "clsx";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
|
|
||||||
import { FONT_FAMILY } from "@excalidraw/common";
|
import { FONT_FAMILY } from "@excalidraw/common";
|
||||||
@@ -83,7 +84,13 @@ export const FontPicker = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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 && (
|
{!compactMode && (
|
||||||
<div className="buttonList">
|
<div className="buttonList">
|
||||||
<RadioSelection<FontFamilyValues | false>
|
<RadioSelection<FontFamilyValues | false>
|
||||||
|
@@ -28,7 +28,11 @@ import { useAtom, useAtomValue } from "../editor-jotai";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { calculateScrollCenter } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
|
|
||||||
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
import {
|
||||||
|
SelectedShapeActions,
|
||||||
|
ShapesSwitcher,
|
||||||
|
CompactShapeActions,
|
||||||
|
} from "./Actions";
|
||||||
import { LoadingMessage } from "./LoadingMessage";
|
import { LoadingMessage } from "./LoadingMessage";
|
||||||
import { LockButton } from "./LockButton";
|
import { LockButton } from "./LockButton";
|
||||||
import { MobileMenu } from "./MobileMenu";
|
import { MobileMenu } from "./MobileMenu";
|
||||||
@@ -209,13 +213,38 @@ const LayerUI = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderSelectedShapeActions = () => (
|
const renderSelectedShapeActions = (isTablet: boolean) => {
|
||||||
|
// Use compact layout for tablets (when device is mobile but not too small)
|
||||||
|
|
||||||
|
return (
|
||||||
<Section
|
<Section
|
||||||
heading="selectedShapeActions"
|
heading="selectedShapeActions"
|
||||||
className={clsx("selected-shape-actions zen-mode-transition", {
|
className={clsx("selected-shape-actions zen-mode-transition", {
|
||||||
"transition-left": appState.zenModeEnabled,
|
"transition-left": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
{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
|
<Island
|
||||||
className={CLASSES.SHAPE_ACTIONS_MENU}
|
className={CLASSES.SHAPE_ACTIONS_MENU}
|
||||||
padding={2}
|
padding={2}
|
||||||
@@ -232,8 +261,10 @@ const LayerUI = ({
|
|||||||
app={app}
|
app={app}
|
||||||
/>
|
/>
|
||||||
</Island>
|
</Island>
|
||||||
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderFixedSideContainer = () => {
|
const renderFixedSideContainer = () => {
|
||||||
const shouldRenderSelectedShapeActions = showSelectedShapeActions(
|
const shouldRenderSelectedShapeActions = showSelectedShapeActions(
|
||||||
@@ -252,7 +283,8 @@ const LayerUI = ({
|
|||||||
<div className="App-menu App-menu_top">
|
<div className="App-menu App-menu_top">
|
||||||
<Stack.Col gap={6} className={clsx("App-menu_top__left")}>
|
<Stack.Col gap={6} className={clsx("App-menu_top__left")}>
|
||||||
{renderCanvasActions()}
|
{renderCanvasActions()}
|
||||||
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
{shouldRenderSelectedShapeActions &&
|
||||||
|
renderSelectedShapeActions(device.isTouchScreen)}
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
{!appState.viewModeEnabled &&
|
{!appState.viewModeEnabled &&
|
||||||
appState.openDialog?.name !== "elementLinkSelector" && (
|
appState.openDialog?.name !== "elementLinkSelector" && (
|
||||||
|
@@ -396,6 +396,19 @@ export const TextIcon = createIcon(
|
|||||||
tablerIconProps,
|
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
|
// modified tabler-icons: photo
|
||||||
export const ImageIcon = createIcon(
|
export const ImageIcon = createIcon(
|
||||||
<g strokeWidth="1.25">
|
<g strokeWidth="1.25">
|
||||||
@@ -2269,3 +2282,46 @@ export const elementLinkIcon = createIcon(
|
|||||||
</g>,
|
</g>,
|
||||||
tablerIconProps,
|
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,
|
||||||
|
);
|
||||||
|
@@ -354,6 +354,9 @@ export interface AppState {
|
|||||||
| "elementBackground"
|
| "elementBackground"
|
||||||
| "elementStroke"
|
| "elementStroke"
|
||||||
| "fontFamily"
|
| "fontFamily"
|
||||||
|
| "fontSize"
|
||||||
|
| "textAlign"
|
||||||
|
| "arrowProperties"
|
||||||
| null;
|
| null;
|
||||||
openSidebar: { name: SidebarName; tab?: SidebarTabName } | null;
|
openSidebar: { name: SidebarName; tab?: SidebarTabName } | null;
|
||||||
openDialog:
|
openDialog:
|
||||||
|
Reference in New Issue
Block a user