More mobile tweaks (#790)
* Disable text selection * Set content-editable=plaintext-only to disable Touch Bar formatting buttons * Enlarge resize handle tap targets for pen/touch * Make the lock button a button in mobile mode * Use icons instead of Unicode characters; add an alternate toolbar for creating multipoint lines * Allow buttons to hide themselves * Fix heuristic for showing shape actions * Refactor icons * Fix label for edit button * Switch edit button icon * Remove lock button on mobile * Add language selector on mobile * Fix showing edit button on mobile * Fix showing edit button on mobile, part 2 * Fix handle touch regions * Fix scroll-back button position * Allow using the text tool on a text object to start editing it * Fix deletion of last point in line
This commit is contained in:
parent
949c3841ea
commit
0fd3fb4b5b
@ -18,13 +18,14 @@ export const actionDeleteSelected: Action = {
|
||||
contextMenuOrder: 3,
|
||||
commitToHistory: (_, elements) => isSomeElementSelected(elements),
|
||||
keyTest: event => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
|
||||
PanelComponent: ({ updateData }) => (
|
||||
PanelComponent: ({ elements, updateData }) => (
|
||||
<ToolButton
|
||||
type="button"
|
||||
icon={trash}
|
||||
title={t("labels.delete")}
|
||||
aria-label={t("labels.delete")}
|
||||
onClick={() => updateData(null)}
|
||||
visible={isSomeElementSelected(elements)}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ import { isInvisiblySmallElement } from "../element";
|
||||
import { resetCursor } from "../utils";
|
||||
import React from "react";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { save } from "../components/icons";
|
||||
import { done } from "../components/icons";
|
||||
import { t } from "../i18n";
|
||||
|
||||
export const actionFinalize: Action = {
|
||||
@ -16,10 +16,13 @@ export const actionFinalize: Action = {
|
||||
window.document.activeElement.blur();
|
||||
}
|
||||
if (appState.multiElement) {
|
||||
// pen and mouse have hover
|
||||
if (appState.lastPointerDownWith !== "touch") {
|
||||
appState.multiElement.points = appState.multiElement.points.slice(
|
||||
0,
|
||||
appState.multiElement.points.length - 1,
|
||||
);
|
||||
}
|
||||
if (isInvisiblySmallElement(appState.multiElement)) {
|
||||
newElements = newElements.slice(0, -1);
|
||||
}
|
||||
@ -50,12 +53,12 @@ export const actionFinalize: Action = {
|
||||
PanelComponent: ({ appState, updateData }) => (
|
||||
<div
|
||||
style={{
|
||||
visibility: appState.multiElement !== null ? "visible" : "hidden",
|
||||
visibility: appState.multiElement != null ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
<ToolButton
|
||||
type="button"
|
||||
icon={save}
|
||||
icon={done}
|
||||
title={t("buttons.done")}
|
||||
aria-label={t("buttons.done")}
|
||||
onClick={() => updateData(null)}
|
||||
|
@ -30,6 +30,7 @@ export function getDefaultAppState(): AppState {
|
||||
selectionElement: null,
|
||||
zoom: 1,
|
||||
openedMenu: null,
|
||||
lastPointerDownWith: "mouse",
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -5,15 +5,19 @@ export function LanguageList<T>({
|
||||
onChange,
|
||||
languages,
|
||||
currentLanguage,
|
||||
floating,
|
||||
}: {
|
||||
languages: { lng: string; label: string }[];
|
||||
onChange: (value: string) => void;
|
||||
currentLanguage: string;
|
||||
floating?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<select
|
||||
className="language-select"
|
||||
className={`dropdown-select dropdown-select__language${
|
||||
floating ? " dropdown-select--floating" : ""
|
||||
}`}
|
||||
onChange={({ target }) => onChange(target.value)}
|
||||
value={currentLanguage}
|
||||
aria-label={t("buttons.selectLanguage")}
|
||||
|
@ -11,6 +11,7 @@ type LockIconProps = {
|
||||
checked: boolean;
|
||||
onChange?(): void;
|
||||
size?: LockIconSize;
|
||||
isButton?: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_SIZE: LockIconSize = "m";
|
||||
@ -43,7 +44,12 @@ export function LockIcon(props: LockIconProps) {
|
||||
const sizeCn = `ToolIcon_size_${props.size || DEFAULT_SIZE}`;
|
||||
|
||||
return (
|
||||
<label className={`ToolIcon ToolIcon__lock ${sizeCn}`} title={props.title}>
|
||||
<label
|
||||
className={`ToolIcon ToolIcon__lock ${
|
||||
props.isButton ? "ToolIcon_type_button" : "ToolIcon_type_floating"
|
||||
} ${sizeCn}`}
|
||||
title={props.title}
|
||||
>
|
||||
<input
|
||||
className="ToolIcon_type_checkbox"
|
||||
type="checkbox"
|
||||
|
@ -36,7 +36,7 @@ export class ProjectName extends Component<Props> {
|
||||
return (
|
||||
<span
|
||||
suppressContentEditableWarning
|
||||
contentEditable="true"
|
||||
contentEditable={"plaintext-only" as any}
|
||||
data-type="wysiwyg"
|
||||
className="ProjectName"
|
||||
role="textbox"
|
||||
|
@ -15,6 +15,7 @@ type ToolButtonBaseProps = {
|
||||
size?: ToolIconSize;
|
||||
keyBindingLabel?: string;
|
||||
showAriaLabel?: boolean;
|
||||
visible?: boolean;
|
||||
};
|
||||
|
||||
type ToolButtonProps =
|
||||
@ -45,6 +46,10 @@ export const ToolButton = React.forwardRef(function(
|
||||
type="button"
|
||||
onClick={props.onClick}
|
||||
ref={innerRef}
|
||||
style={{
|
||||
visibility:
|
||||
props.visible || props.visible == null ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
<div className="ToolIcon__icon" aria-hidden="true">
|
||||
{props.icon || props.label}
|
||||
|
@ -5,6 +5,7 @@
|
||||
font-family: Cascadia;
|
||||
cursor: pointer;
|
||||
background-color: #e9ecef;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.ToolIcon__icon {
|
||||
@ -69,17 +70,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ToolIcon.ToolIcon__lock {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 0.1rem;
|
||||
.ToolIcon_type_floating {
|
||||
background-color: transparent;
|
||||
|
||||
.ToolIcon__icon {
|
||||
width: 2rem;
|
||||
height: 2em;
|
||||
}
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
@ -89,6 +81,22 @@
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
.ToolIcon__icon {
|
||||
width: 2rem;
|
||||
height: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.ToolIcon.ToolIcon__lock {
|
||||
&.ToolIcon_type_button {
|
||||
border-radius: 4px;
|
||||
svg {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
&.ToolIcon_type_floating {
|
||||
margin-left: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ToolIcon__keybinding {
|
||||
|
@ -5,92 +5,77 @@
|
||||
|
||||
import React from "react";
|
||||
|
||||
export const link = (
|
||||
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 512 512">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z"
|
||||
/>
|
||||
const createIcon = (d: string, width = 512) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox={`0 0 ${width} 512`}
|
||||
>
|
||||
<path fill="currentColor" d={d} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const save = (
|
||||
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 448 512">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z"
|
||||
/>
|
||||
</svg>
|
||||
export const link = createIcon(
|
||||
"M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z",
|
||||
);
|
||||
|
||||
export const load = (
|
||||
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 576 512">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z"
|
||||
/>
|
||||
</svg>
|
||||
export const save = createIcon(
|
||||
"M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z",
|
||||
448,
|
||||
);
|
||||
|
||||
export const image = (
|
||||
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 384 512">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M384 121.941V128H256V0h6.059a24 24 0 0 1 16.97 7.029l97.941 97.941a24.002 24.002 0 0 1 7.03 16.971zM248 160c-13.2 0-24-10.8-24-24V0H24C10.745 0 0 10.745 0 24v464c0 13.255 10.745 24 24 24h336c13.255 0 24-10.745 24-24V160H248zm-135.455 16c26.51 0 48 21.49 48 48s-21.49 48-48 48-48-21.49-48-48 21.491-48 48-48zm208 240h-256l.485-48.485L104.545 328c4.686-4.686 11.799-4.201 16.485.485L160.545 368 264.06 264.485c4.686-4.686 12.284-4.686 16.971 0L320.545 304v112z"
|
||||
/>
|
||||
</svg>
|
||||
export const load = createIcon(
|
||||
"M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z",
|
||||
576,
|
||||
);
|
||||
|
||||
export const clipboard = (
|
||||
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 384 512">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M384 112v352c0 26.51-21.49 48-48 48H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h80c0-35.29 28.71-64 64-64s64 28.71 64 64h80c26.51 0 48 21.49 48 48zM192 40c-13.255 0-24 10.745-24 24s10.745 24 24 24 24-10.745 24-24-10.745-24-24-24m96 114v-20a6 6 0 0 0-6-6H102a6 6 0 0 0-6 6v20a6 6 0 0 0 6 6h180a6 6 0 0 0 6-6z"
|
||||
/>
|
||||
</svg>
|
||||
export const image = createIcon(
|
||||
"M384 121.941V128H256V0h6.059a24 24 0 0 1 16.97 7.029l97.941 97.941a24.002 24.002 0 0 1 7.03 16.971zM248 160c-13.2 0-24-10.8-24-24V0H24C10.745 0 0 10.745 0 24v464c0 13.255 10.745 24 24 24h336c13.255 0 24-10.745 24-24V160H248zm-135.455 16c26.51 0 48 21.49 48 48s-21.49 48-48 48-48-21.49-48-48 21.491-48 48-48zm208 240h-256l.485-48.485L104.545 328c4.686-4.686 11.799-4.201 16.485.485L160.545 368 264.06 264.485c4.686-4.686 12.284-4.686 16.971 0L320.545 304v112z",
|
||||
384,
|
||||
);
|
||||
|
||||
export const trash = (
|
||||
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 448 512">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"
|
||||
/>
|
||||
</svg>
|
||||
export const clipboard = createIcon(
|
||||
"M384 112v352c0 26.51-21.49 48-48 48H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h80c0-35.29 28.71-64 64-64s64 28.71 64 64h80c26.51 0 48 21.49 48 48zM192 40c-13.255 0-24 10.745-24 24s10.745 24 24 24 24-10.745 24-24-10.745-24-24-24m96 114v-20a6 6 0 0 0-6-6H102a6 6 0 0 0-6 6v20a6 6 0 0 0 6 6h180a6 6 0 0 0 6-6z",
|
||||
384,
|
||||
);
|
||||
|
||||
export const palete = (
|
||||
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 512 512">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368.1-26.9 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z"
|
||||
/>
|
||||
</svg>
|
||||
export const trash = createIcon(
|
||||
"M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z",
|
||||
448,
|
||||
);
|
||||
|
||||
export const exportFile = (
|
||||
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 576 512">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M384 121.9c0-6.3-2.5-12.4-7-16.9L279.1 7c-4.5-4.5-10.6-7-17-7H256v128h128zM571 308l-95.7-96.4c-10.1-10.1-27.4-3-27.4 11.3V288h-64v64h64v65.2c0 14.3 17.3 21.4 27.4 11.3L571 332c6.6-6.6 6.6-17.4 0-24zm-379 28v-32c0-8.8 7.2-16 16-16h176V160H248c-13.2 0-24-10.8-24-24V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24V352H208c-8.8 0-16-7.2-16-16z"
|
||||
/>
|
||||
</svg>
|
||||
export const palette = createIcon(
|
||||
"M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368.1-26.9 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z",
|
||||
);
|
||||
|
||||
export const zoomIn = (
|
||||
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 448 512">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"
|
||||
/>
|
||||
</svg>
|
||||
export const exportFile = createIcon(
|
||||
"M384 121.9c0-6.3-2.5-12.4-7-16.9L279.1 7c-4.5-4.5-10.6-7-17-7H256v128h128zM571 308l-95.7-96.4c-10.1-10.1-27.4-3-27.4 11.3V288h-64v64h64v65.2c0 14.3 17.3 21.4 27.4 11.3L571 332c6.6-6.6 6.6-17.4 0-24zm-379 28v-32c0-8.8 7.2-16 16-16h176V160H248c-13.2 0-24-10.8-24-24V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24V352H208c-8.8 0-16-7.2-16-16z",
|
||||
576,
|
||||
);
|
||||
|
||||
export const zoomOut = (
|
||||
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 448 512">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M416 208H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h384c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"
|
||||
/>
|
||||
</svg>
|
||||
export const zoomIn = createIcon(
|
||||
"M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z",
|
||||
448,
|
||||
);
|
||||
|
||||
export const zoomOut = createIcon(
|
||||
"M416 208H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h384c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z",
|
||||
448,
|
||||
);
|
||||
|
||||
export const done = createIcon(
|
||||
"M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z",
|
||||
);
|
||||
|
||||
export const menu = createIcon(
|
||||
"M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z",
|
||||
);
|
||||
|
||||
export const undo = createIcon(
|
||||
"M255.545 8c-66.269.119-126.438 26.233-170.86 68.685L48.971 40.971C33.851 25.851 8 36.559 8 57.941V192c0 13.255 10.745 24 24 24h134.059c21.382 0 32.09-25.851 16.971-40.971l-41.75-41.75c30.864-28.899 70.801-44.907 113.23-45.273 92.398-.798 170.283 73.977 169.484 169.442C423.236 348.009 349.816 424 256 424c-41.127 0-79.997-14.678-110.63-41.556-4.743-4.161-11.906-3.908-16.368.553L89.34 422.659c-4.872 4.872-4.631 12.815.482 17.433C133.798 479.813 192.074 504 256 504c136.966 0 247.999-111.033 248-247.998C504.001 119.193 392.354 7.755 255.545 8z",
|
||||
);
|
||||
|
||||
export const redo = createIcon(
|
||||
"M256.455 8c66.269.119 126.437 26.233 170.859 68.685l35.715-35.715C478.149 25.851 504 36.559 504 57.941V192c0 13.255-10.745 24-24 24H345.941c-21.382 0-32.09-25.851-16.971-40.971l41.75-41.75c-30.864-28.899-70.801-44.907-113.23-45.273-92.398-.798-170.283 73.977-169.484 169.442C88.764 348.009 162.184 424 256 424c41.127 0 79.997-14.678 110.629-41.556 4.743-4.161 11.906-3.908 16.368.553l39.662 39.662c4.872 4.872 4.631 12.815-.482 17.433C378.202 479.813 319.926 504 256 504 119.034 504 8.001 392.967 8 256.002 7.999 119.193 119.646 7.755 256.455 8z",
|
||||
);
|
||||
|
@ -1,15 +1,26 @@
|
||||
import { ExcalidrawElement } from "./types";
|
||||
import { ExcalidrawElement, PointerType } from "./types";
|
||||
|
||||
import { getElementAbsoluteCoords } from "./bounds";
|
||||
|
||||
type Sides = "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se";
|
||||
|
||||
export function handlerRectangles(element: ExcalidrawElement, zoom: number) {
|
||||
const handlerWidth = 8 / zoom;
|
||||
const handlerHeight = 8 / zoom;
|
||||
const handleSizes: { [k in PointerType]: number } = {
|
||||
mouse: 8,
|
||||
pen: 16,
|
||||
touch: 28,
|
||||
};
|
||||
|
||||
const handlerMarginX = 8 / zoom;
|
||||
const handlerMarginY = 8 / zoom;
|
||||
export function handlerRectangles(
|
||||
element: ExcalidrawElement,
|
||||
zoom: number,
|
||||
pointerType: PointerType = "mouse",
|
||||
) {
|
||||
const size = handleSizes[pointerType];
|
||||
const handlerWidth = size / zoom;
|
||||
const handlerHeight = size / zoom;
|
||||
|
||||
const handlerMarginX = size / zoom;
|
||||
const handlerMarginY = size / zoom;
|
||||
|
||||
const [elementX1, elementY1, elementX2, elementY2] = getElementAbsoluteCoords(
|
||||
element,
|
||||
@ -20,59 +31,61 @@ export function handlerRectangles(element: ExcalidrawElement, zoom: number) {
|
||||
|
||||
const dashedLineMargin = 4 / zoom;
|
||||
|
||||
const centeringOffset = (size - 8) / (2 * zoom);
|
||||
|
||||
const handlers = {
|
||||
nw: [
|
||||
elementX1 - dashedLineMargin - handlerMarginX,
|
||||
elementY1 - dashedLineMargin - handlerMarginY,
|
||||
elementX1 - dashedLineMargin - handlerMarginX + centeringOffset,
|
||||
elementY1 - dashedLineMargin - handlerMarginY + centeringOffset,
|
||||
handlerWidth,
|
||||
handlerHeight,
|
||||
],
|
||||
ne: [
|
||||
elementX2 + dashedLineMargin,
|
||||
elementY1 - dashedLineMargin - handlerMarginY,
|
||||
elementX2 + dashedLineMargin - centeringOffset,
|
||||
elementY1 - dashedLineMargin - handlerMarginY + centeringOffset,
|
||||
handlerWidth,
|
||||
handlerHeight,
|
||||
],
|
||||
sw: [
|
||||
elementX1 - dashedLineMargin - handlerMarginX,
|
||||
elementY2 + dashedLineMargin,
|
||||
elementX1 - dashedLineMargin - handlerMarginX + centeringOffset,
|
||||
elementY2 + dashedLineMargin - centeringOffset,
|
||||
handlerWidth,
|
||||
handlerHeight,
|
||||
],
|
||||
se: [
|
||||
elementX2 + dashedLineMargin,
|
||||
elementY2 + dashedLineMargin,
|
||||
elementX2 + dashedLineMargin - centeringOffset,
|
||||
elementY2 + dashedLineMargin - centeringOffset,
|
||||
handlerWidth,
|
||||
handlerHeight,
|
||||
],
|
||||
} as { [T in Sides]: number[] };
|
||||
|
||||
// We only want to show height handlers (all cardinal directions) above a certain size
|
||||
const minimumSizeForEightHandlers = 40 / zoom;
|
||||
const minimumSizeForEightHandlers = (5 * size) / zoom;
|
||||
if (Math.abs(elementWidth) > minimumSizeForEightHandlers) {
|
||||
handlers["n"] = [
|
||||
elementX1 + elementWidth / 2,
|
||||
elementY1 - dashedLineMargin - handlerMarginY,
|
||||
elementX1 + elementWidth / 2 - handlerWidth / 2,
|
||||
elementY1 - dashedLineMargin - handlerMarginY + centeringOffset,
|
||||
handlerWidth,
|
||||
handlerHeight,
|
||||
];
|
||||
handlers["s"] = [
|
||||
elementX1 + elementWidth / 2,
|
||||
elementY2 + dashedLineMargin,
|
||||
elementX1 + elementWidth / 2 - handlerWidth / 2,
|
||||
elementY2 + dashedLineMargin - centeringOffset,
|
||||
handlerWidth,
|
||||
handlerHeight,
|
||||
];
|
||||
}
|
||||
if (Math.abs(elementHeight) > minimumSizeForEightHandlers) {
|
||||
handlers["w"] = [
|
||||
elementX1 - dashedLineMargin - handlerMarginX,
|
||||
elementY1 + elementHeight / 2,
|
||||
elementX1 - dashedLineMargin - handlerMarginX + centeringOffset,
|
||||
elementY1 + elementHeight / 2 - handlerHeight / 2,
|
||||
handlerWidth,
|
||||
handlerHeight,
|
||||
];
|
||||
handlers["e"] = [
|
||||
elementX2 + dashedLineMargin,
|
||||
elementY1 + elementHeight / 2,
|
||||
elementX2 + dashedLineMargin - centeringOffset,
|
||||
elementY1 + elementHeight / 2 - handlerHeight / 2,
|
||||
handlerWidth,
|
||||
handlerHeight,
|
||||
];
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ExcalidrawElement } from "./types";
|
||||
import { ExcalidrawElement, PointerType } from "./types";
|
||||
|
||||
import { handlerRectangles } from "./handlerRectangles";
|
||||
|
||||
@ -9,12 +9,13 @@ export function resizeTest(
|
||||
x: number,
|
||||
y: number,
|
||||
zoom: number,
|
||||
pointerType: PointerType,
|
||||
): HandlerRectanglesRet | false {
|
||||
if (!element.isSelected || element.type === "text") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const handlers = handlerRectangles(element, zoom);
|
||||
const handlers = handlerRectangles(element, zoom, pointerType);
|
||||
|
||||
const filter = Object.keys(handlers).filter(key => {
|
||||
const handler = handlers[key as HandlerRectanglesRet]!;
|
||||
@ -41,12 +42,13 @@ export function getElementWithResizeHandler(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
{ x, y }: { x: number; y: number },
|
||||
zoom: number,
|
||||
pointerType: PointerType,
|
||||
) {
|
||||
return elements.reduce((result, element) => {
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
const resizeHandle = resizeTest(element, x, y, zoom);
|
||||
const resizeHandle = resizeTest(element, x, y, zoom, pointerType);
|
||||
return resizeHandle ? { element, resizeHandle } : null;
|
||||
}, null as { element: ExcalidrawElement; resizeHandle: ReturnType<typeof resizeTest> } | null);
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ export function textWysiwyg({
|
||||
// But this solution has an issue — it allows to paste
|
||||
// multiline text, which is not currently supported
|
||||
const editable = document.createElement("div");
|
||||
editable.contentEditable = "true";
|
||||
editable.contentEditable = "plaintext-only";
|
||||
editable.tabIndex = 0;
|
||||
editable.innerText = initText;
|
||||
editable.dataset.type = "wysiwyg";
|
||||
|
@ -9,3 +9,5 @@ export type ExcalidrawTextElement = ExcalidrawElement & {
|
||||
actualBoundingBoxAscent?: number;
|
||||
baseline: number;
|
||||
};
|
||||
|
||||
export type PointerType = "mouse" | "pen" | "touch";
|
||||
|
117
src/index.tsx
117
src/index.tsx
@ -109,6 +109,7 @@ import useIsMobile, { IsMobileProvider } from "./is-mobile";
|
||||
import { copyToAppClipboard, getClipboardContent } from "./clipboard";
|
||||
import { normalizeScroll } from "./scene/data";
|
||||
import { getCenter, getDistance } from "./gesture";
|
||||
import { menu, palette } from "./components/icons";
|
||||
|
||||
let { elements } = createScene();
|
||||
const { history } = createHistory();
|
||||
@ -286,9 +287,11 @@ const LayerUI = React.memo(
|
||||
);
|
||||
}
|
||||
|
||||
const showSelectedShapeActions =
|
||||
(appState.editingElement || getSelectedElements(elements).length) &&
|
||||
appState.elementType === "selection";
|
||||
const showSelectedShapeActions = Boolean(
|
||||
appState.editingElement ||
|
||||
getSelectedElements(elements).length ||
|
||||
appState.elementType !== "selection",
|
||||
);
|
||||
|
||||
function renderSelectedShapeActions() {
|
||||
const { elementType, editingElement } = appState;
|
||||
@ -386,21 +389,6 @@ const LayerUI = React.memo(
|
||||
);
|
||||
}
|
||||
|
||||
const lockButton = (
|
||||
<LockIcon
|
||||
checked={appState.elementLocked}
|
||||
onChange={() => {
|
||||
setAppState({
|
||||
elementLocked: !appState.elementLocked,
|
||||
elementType: appState.elementLocked
|
||||
? "selection"
|
||||
: appState.elementType,
|
||||
});
|
||||
}}
|
||||
title={t("toolBar.lock")}
|
||||
/>
|
||||
);
|
||||
|
||||
return isMobile ? (
|
||||
<>
|
||||
{appState.openedMenu === "canvas" ? (
|
||||
@ -411,13 +399,24 @@ const LayerUI = React.memo(
|
||||
<h2 className="visually-hidden" id="canvas-actions-title">
|
||||
{t("headings.canvasActions")}
|
||||
</h2>
|
||||
<div className="App-mobile-menu-scroller">
|
||||
<div className="App-mobile-menu-scroller panelColumn">
|
||||
<Stack.Col gap={4}>
|
||||
{actionManager.renderAction("loadScene")}
|
||||
{actionManager.renderAction("saveScene")}
|
||||
{renderExportDialog()}
|
||||
{actionManager.renderAction("clearCanvas")}
|
||||
{actionManager.renderAction("changeViewBackgroundColor")}
|
||||
<fieldset>
|
||||
<legend>{t("labels.language")}</legend>
|
||||
<LanguageList
|
||||
onChange={lng => {
|
||||
setLanguage(lng);
|
||||
setAppState({});
|
||||
}}
|
||||
languages={languages}
|
||||
currentLanguage={language}
|
||||
/>
|
||||
</fieldset>
|
||||
</Stack.Col>
|
||||
</div>
|
||||
</section>
|
||||
@ -456,11 +455,27 @@ const LayerUI = React.memo(
|
||||
</FixedSideContainer>
|
||||
<footer className="App-toolbar">
|
||||
<div className="App-toolbar-content">
|
||||
{appState.multiElement ? (
|
||||
<>
|
||||
{actionManager.renderAction("deleteSelectedElements")}
|
||||
<ToolButton
|
||||
visible={showSelectedShapeActions}
|
||||
type="button"
|
||||
icon={palette}
|
||||
aria-label={t("buttons.edit")}
|
||||
onClick={() =>
|
||||
setAppState(({ openedMenu }: any) => ({
|
||||
openedMenu: openedMenu === "shape" ? null : "shape",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
{actionManager.renderAction("finalize")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ToolButton
|
||||
type="button"
|
||||
icon={
|
||||
<span style={{ fontSize: "2em", marginTop: "-0.15em" }}>☰</span>
|
||||
}
|
||||
icon={menu}
|
||||
aria-label={t("buttons.menu")}
|
||||
onClick={() =>
|
||||
setAppState(({ openedMenu }: any) => ({
|
||||
@ -468,40 +483,18 @@ const LayerUI = React.memo(
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
visibility: isSomeElementSelected(elements)
|
||||
? "visible"
|
||||
: "hidden",
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
{actionManager.renderAction("deleteSelectedElements")}
|
||||
</div>
|
||||
{lockButton}
|
||||
{actionManager.renderAction("finalize")}
|
||||
<div
|
||||
style={{
|
||||
visibility: isSomeElementSelected(elements)
|
||||
? "visible"
|
||||
: "hidden",
|
||||
}}
|
||||
>
|
||||
<ToolButton
|
||||
visible={showSelectedShapeActions}
|
||||
type="button"
|
||||
icon={
|
||||
<span style={{ fontSize: "2em", marginTop: "-0.15em" }}>
|
||||
✎
|
||||
</span>
|
||||
}
|
||||
aria-label={t("buttons.menu")}
|
||||
icon={palette}
|
||||
aria-label={t("buttons.edit")}
|
||||
onClick={() =>
|
||||
setAppState(({ openedMenu }: any) => ({
|
||||
openedMenu: openedMenu === "shape" ? null : "shape",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{actionManager.renderAction("deleteSelectedElements")}
|
||||
{appState.scrolledOutside && (
|
||||
<button
|
||||
className="scroll-back-to-content"
|
||||
@ -512,6 +505,8 @@ const LayerUI = React.memo(
|
||||
{t("buttons.scrollBackToContent")}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
@ -545,7 +540,7 @@ const LayerUI = React.memo(
|
||||
</Stack.Col>
|
||||
</Island>
|
||||
</section>
|
||||
{showSelectedShapeActions ? (
|
||||
{showSelectedShapeActions && (
|
||||
<section
|
||||
className="App-right-menu"
|
||||
aria-labelledby="selected-shape-title"
|
||||
@ -555,7 +550,7 @@ const LayerUI = React.memo(
|
||||
</h2>
|
||||
<Island padding={4}>{renderSelectedShapeActions()}</Island>
|
||||
</section>
|
||||
) : null}
|
||||
)}
|
||||
</Stack.Col>
|
||||
<section aria-labelledby="shapes-title">
|
||||
<Stack.Col gap={4} align="start">
|
||||
@ -566,7 +561,19 @@ const LayerUI = React.memo(
|
||||
</h2>
|
||||
<Stack.Row gap={1}>{renderShapesSwitcher()}</Stack.Row>
|
||||
</Island>
|
||||
{lockButton}
|
||||
<LockIcon
|
||||
checked={appState.elementLocked}
|
||||
onChange={() => {
|
||||
setAppState({
|
||||
elementLocked: !appState.elementLocked,
|
||||
elementType: appState.elementLocked
|
||||
? "selection"
|
||||
: appState.elementType,
|
||||
});
|
||||
}}
|
||||
title={t("toolBar.lock")}
|
||||
isButton={isMobile}
|
||||
/>
|
||||
</Stack.Row>
|
||||
</Stack.Col>
|
||||
</section>
|
||||
@ -591,6 +598,7 @@ const LayerUI = React.memo(
|
||||
}}
|
||||
languages={languages}
|
||||
currentLanguage={language}
|
||||
floating
|
||||
/>
|
||||
{appState.scrolledOutside && (
|
||||
<button
|
||||
@ -1085,6 +1093,8 @@ export class App extends React.Component<any, AppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ lastPointerDownWith: e.pointerType });
|
||||
|
||||
// pan canvas on wheel button drag or space+drag
|
||||
if (
|
||||
gesture.pointers.length === 0 &&
|
||||
@ -1213,6 +1223,7 @@ export class App extends React.Component<any, AppState> {
|
||||
elements,
|
||||
{ x, y },
|
||||
this.state.zoom,
|
||||
e.pointerType,
|
||||
);
|
||||
|
||||
const selectedElements = getSelectedElements(elements);
|
||||
@ -1279,6 +1290,9 @@ export class App extends React.Component<any, AppState> {
|
||||
if (this.state.editingElement?.type === "text") {
|
||||
return;
|
||||
}
|
||||
if (elementIsAddedToSelection) {
|
||||
element = hitElement!;
|
||||
}
|
||||
let textX = e.clientX;
|
||||
let textY = e.clientY;
|
||||
if (!e.altKey) {
|
||||
@ -2152,6 +2166,7 @@ export class App extends React.Component<any, AppState> {
|
||||
elements,
|
||||
{ x, y },
|
||||
this.state.zoom,
|
||||
e.pointerType,
|
||||
);
|
||||
if (resizeElement && resizeElement.resizeHandle) {
|
||||
document.documentElement.style.cursor = getCursorForResizingElement(
|
||||
|
@ -40,7 +40,8 @@
|
||||
"colorPicker": "Color picker",
|
||||
"canvasBackground": "Canvas background",
|
||||
"drawingCanvas": "Drawing Canvas",
|
||||
"layers": "Layers"
|
||||
"layers": "Layers",
|
||||
"language": "Language"
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Reset the canvas",
|
||||
@ -57,7 +58,8 @@
|
||||
"zoomIn": "Zoom in",
|
||||
"zoomOut": "Zoom out",
|
||||
"menu": "Menu",
|
||||
"done": "Done"
|
||||
"done": "Done",
|
||||
"edit": "Edit"
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "This will clear the whole canvas. Are you sure?",
|
||||
|
@ -6,6 +6,11 @@ body {
|
||||
font-family: var(--ui-font);
|
||||
color: var(--text-color-primary);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
[contenteditable] {
|
||||
user-select: auto;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
canvas {
|
||||
@ -139,11 +144,19 @@ button,
|
||||
}
|
||||
}
|
||||
|
||||
.App-toolbar,
|
||||
.App-mobile-menu {
|
||||
--spacing: 0.5rem;
|
||||
--padding: calc(4 * var(--space-factor));
|
||||
padding: var(--padding);
|
||||
padding-left: #{"max(var(--padding), env(safe-area-inset-left))"};
|
||||
padding-right: #{"max(var(--padding), env(safe-area-inset-right))"};
|
||||
background: #fcfcfc;
|
||||
border-top: 1px solid #ccc;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.App-toolbar {
|
||||
padding: var(--spacing);
|
||||
padding-bottom: #{"max(var(--spacing), env(safe-area-inset-bottom))"};
|
||||
padding-left: #{"max(var(--spacing), env(safe-area-inset-left))"};
|
||||
padding-right: #{"max(var(--spacing), env(safe-area-inset-right))"};
|
||||
padding-bottom: #{"max(var(--padding), env(safe-area-inset-bottom))"};
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
@ -155,14 +168,8 @@ button,
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.App-toolbar,
|
||||
.App-mobile-menu {
|
||||
--spacing: 0.5rem;
|
||||
background: #fcfcfc;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
.App-mobile-menu {
|
||||
--bottom: calc(3rem - 1px + max(var(--spacing), env(safe-area-inset-bottom)));
|
||||
--bottom: calc(3rem - 1px + max(var(--padding), env(safe-area-inset-bottom)));
|
||||
display: grid;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
@ -174,10 +181,6 @@ button,
|
||||
.App-mobile-menu .App-mobile-menu-scroller {
|
||||
background: #fcfcfc;
|
||||
box-shadow: none;
|
||||
--padding: calc(4 * var(--space-factor));
|
||||
padding: var(--padding);
|
||||
padding-left: #{"max(var(--padding), env(safe-area-inset-left))"};
|
||||
padding-right: #{"max(var(--padding), env(safe-area-inset-right))"};
|
||||
}
|
||||
|
||||
.App-menu {
|
||||
@ -288,12 +291,7 @@ button,
|
||||
}
|
||||
|
||||
.dropdown-select {
|
||||
position: absolute;
|
||||
margin-bottom: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
height: 1.5rem;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 0 1.5rem 0 0.5rem;
|
||||
background-color: #e9ecef;
|
||||
border-radius: var(--space-factor);
|
||||
@ -317,10 +315,14 @@ button,
|
||||
&:active {
|
||||
background-color: #ced4da;
|
||||
}
|
||||
&.dropdown-select--floating {
|
||||
position: absolute;
|
||||
margin-bottom: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.language-select {
|
||||
@extend .dropdown-select;
|
||||
.dropdown-select__language.dropdown-select--floating {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
@ -351,7 +353,7 @@ button,
|
||||
.scroll-back-to-content {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 20px;
|
||||
bottom: 30px;
|
||||
transform: translateX(-50%);
|
||||
padding: 10px 20px;
|
||||
}
|
||||
@ -361,7 +363,7 @@ button,
|
||||
display: none;
|
||||
}
|
||||
.scroll-back-to-content {
|
||||
bottom: 70px;
|
||||
bottom: calc(70px + env(safe-area-inset-bottom));
|
||||
bottom: 80px;
|
||||
bottom: calc(80px + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ExcalidrawElement } from "./element/types";
|
||||
import { ExcalidrawElement, PointerType } from "./element/types";
|
||||
import { SHAPES } from "./shapes";
|
||||
|
||||
export type FlooredNumber = number & { _brand: "FlooredNumber" };
|
||||
@ -32,6 +32,7 @@ export type AppState = {
|
||||
isResizing: boolean;
|
||||
zoom: number;
|
||||
openedMenu: "canvas" | "shape" | null;
|
||||
lastPointerDownWith: PointerType;
|
||||
};
|
||||
|
||||
export type Pointer = Readonly<{
|
||||
|
Loading…
x
Reference in New Issue
Block a user