Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f68f4cb9e0 | |||
| 71fb573750 | |||
| 7562d9b533 | |||
| e6de1fe4a4 | |||
| 3a141ca77a | |||
| 5ae39c9292 | |||
| e41ea9562b | |||
| b52c8943e4 | |||
| cf38c0f933 | |||
| 1db078a3dc | |||
| d4afd66268 | |||
| 849e6a0c86 | |||
| f03f5c948d | |||
| d2b698093c | |||
| 0f1720be61 | |||
| d0b33d35db | |||
| d6a5ef1936 | |||
| c7a11f5cd2 | |||
| 893c487add | |||
| 99fdffdab7 | |||
| faad8a65f1 | |||
| 9d04479f98 | |||
| 599a8f3c6f | |||
| 0982da38fe | |||
| 699897f71b | |||
| 328ff6c32d | |||
| 618442299f | |||
| 06b45e0cfc | |||
| 809d5ba17f | |||
| 40d53d9231 | |||
| 9803a85381 | |||
| 72784f9d29 | |||
| e3249f930c | |||
| cbe0d34f1a | |||
| bed8093e47 | |||
| 1255ca2e84 | |||
| 14d02dcaea | |||
| 9747223705 |
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
yarn lint-staged
|
# yarn lint-staged
|
||||||
|
|||||||
+3
-3
@@ -4692,9 +4692,9 @@ json-schema-traverse@^1.0.0:
|
|||||||
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
|
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
|
||||||
|
|
||||||
json5@^2.1.2, json5@^2.2.1:
|
json5@^2.1.2, json5@^2.2.1:
|
||||||
version "2.2.1"
|
version "2.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
|
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
|
||||||
integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
|
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
|
||||||
|
|
||||||
jsonfile@^6.0.1:
|
jsonfile@^6.0.1:
|
||||||
version "6.1.0"
|
version "6.1.0"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dwelle/tunnel-rat": "0.1.1",
|
||||||
"@sentry/browser": "6.2.5",
|
"@sentry/browser": "6.2.5",
|
||||||
"@sentry/integrations": "6.2.5",
|
"@sentry/integrations": "6.2.5",
|
||||||
"@testing-library/jest-dom": "5.16.2",
|
"@testing-library/jest-dom": "5.16.2",
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
"roughjs": "4.5.2",
|
"roughjs": "4.5.2",
|
||||||
"sass": "1.51.0",
|
"sass": "1.51.0",
|
||||||
"socket.io-client": "2.3.1",
|
"socket.io-client": "2.3.1",
|
||||||
|
"tunnel-rat": "0.1.0",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.4",
|
||||||
"workbox-background-sync": "^6.5.4",
|
"workbox-background-sync": "^6.5.4",
|
||||||
"workbox-broadcast-update": "^6.5.4",
|
"workbox-broadcast-update": "^6.5.4",
|
||||||
|
|||||||
@@ -167,9 +167,6 @@
|
|||||||
body,
|
body,
|
||||||
html {
|
html {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
--ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system,
|
|
||||||
Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
|
||||||
font-family: var(--ui-font);
|
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const actionUnbindText = register({
|
|||||||
name: "unbindText",
|
name: "unbindText",
|
||||||
contextItemLabel: "labels.unbindText",
|
contextItemLabel: "labels.unbindText",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
contextItemPredicate: (elements, appState) => {
|
predicate: (elements, appState) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
return selectedElements.some((element) => hasBoundTextElement(element));
|
return selectedElements.some((element) => hasBoundTextElement(element));
|
||||||
},
|
},
|
||||||
@@ -76,7 +76,7 @@ export const actionBindText = register({
|
|||||||
name: "bindText",
|
name: "bindText",
|
||||||
contextItemLabel: "labels.bindText",
|
contextItemLabel: "labels.bindText",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
contextItemPredicate: (elements, appState) => {
|
predicate: (elements, appState) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
|
||||||
if (selectedElements.length === 2) {
|
if (selectedElements.length === 2) {
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { ColorPicker } from "../components/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker";
|
||||||
import {
|
import { ZoomInIcon, ZoomOutIcon } from "../components/icons";
|
||||||
eraser,
|
|
||||||
MoonIcon,
|
|
||||||
SunIcon,
|
|
||||||
ZoomInIcon,
|
|
||||||
ZoomOutIcon,
|
|
||||||
} from "../components/icons";
|
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { MIN_ZOOM, THEME, ZOOM_STEP } from "../constants";
|
import { CURSOR_TYPE, MIN_ZOOM, THEME, ZOOM_STEP } from "../constants";
|
||||||
import { getCommonBounds, getNonDeletedElements } from "../element";
|
import { getCommonBounds, getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@@ -16,19 +10,25 @@ import { getNormalizedZoom, getSelectedElements } from "../scene";
|
|||||||
import { centerScrollOn } from "../scene/scroll";
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
import { getStateForZoom } from "../scene/zoom";
|
import { getStateForZoom } from "../scene/zoom";
|
||||||
import { AppState, NormalizedZoomValue } from "../types";
|
import { AppState, NormalizedZoomValue } from "../types";
|
||||||
import { getShortcutKey, updateActiveTool } from "../utils";
|
import { getShortcutKey, setCursor, updateActiveTool } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { Tooltip } from "../components/Tooltip";
|
import { Tooltip } from "../components/Tooltip";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { getDefaultAppState, isEraserActive } from "../appState";
|
import {
|
||||||
import ClearCanvas from "../components/ClearCanvas";
|
getDefaultAppState,
|
||||||
import clsx from "clsx";
|
isEraserActive,
|
||||||
import DropdownMenuItem from "../components/dropdownMenu/DropdownMenuItem";
|
isHandToolActive,
|
||||||
import { getShortcutFromShortcutName } from "./shortcuts";
|
} from "../appState";
|
||||||
|
|
||||||
export const actionChangeViewBackgroundColor = register({
|
export const actionChangeViewBackgroundColor = register({
|
||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
|
predicate: (elements, appState, props, app) => {
|
||||||
|
return (
|
||||||
|
!!app.props.UIOptions.canvasActions.changeViewBackgroundColor &&
|
||||||
|
!appState.viewModeEnabled
|
||||||
|
);
|
||||||
|
},
|
||||||
perform: (_, appState, value) => {
|
perform: (_, appState, value) => {
|
||||||
return {
|
return {
|
||||||
appState: { ...appState, ...value },
|
appState: { ...appState, ...value },
|
||||||
@@ -36,6 +36,7 @@ export const actionChangeViewBackgroundColor = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => {
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
|
// FIXME move me to src/components/mainMenu/DefaultItems.tsx
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "relative" }}>
|
<div style={{ position: "relative" }}>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
@@ -59,6 +60,12 @@ export const actionChangeViewBackgroundColor = register({
|
|||||||
export const actionClearCanvas = register({
|
export const actionClearCanvas = register({
|
||||||
name: "clearCanvas",
|
name: "clearCanvas",
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
|
predicate: (elements, appState, props, app) => {
|
||||||
|
return (
|
||||||
|
!!app.props.UIOptions.canvasActions.clearCanvas &&
|
||||||
|
!appState.viewModeEnabled
|
||||||
|
);
|
||||||
|
},
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
app.imageCache.clear();
|
app.imageCache.clear();
|
||||||
return {
|
return {
|
||||||
@@ -84,8 +91,6 @@ export const actionClearCanvas = register({
|
|||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
PanelComponent: ({ updateData }) => <ClearCanvas onConfirm={updateData} />,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionZoomIn = register({
|
export const actionZoomIn = register({
|
||||||
@@ -298,37 +303,21 @@ export const actionToggleTheme = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData }) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={() => {
|
|
||||||
updateData(appState.theme === THEME.LIGHT ? THEME.DARK : THEME.LIGHT);
|
|
||||||
}}
|
|
||||||
icon={appState.theme === "dark" ? SunIcon : MoonIcon}
|
|
||||||
dataTestId="toggle-dark-mode"
|
|
||||||
shortcut={getShortcutFromShortcutName("toggleTheme")}
|
|
||||||
ariaLabel={
|
|
||||||
appState.theme === "dark"
|
|
||||||
? t("buttons.lightMode")
|
|
||||||
: t("buttons.darkMode")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{appState.theme === "dark"
|
|
||||||
? t("buttons.lightMode")
|
|
||||||
: t("buttons.darkMode")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
),
|
|
||||||
keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D,
|
keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D,
|
||||||
|
predicate: (elements, appState, props, app) => {
|
||||||
|
return !!app.props.UIOptions.canvasActions.toggleTheme;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionErase = register({
|
export const actionToggleEraserTool = register({
|
||||||
name: "eraser",
|
name: "toggleEraserTool",
|
||||||
trackEvent: { category: "toolbar" },
|
trackEvent: { category: "toolbar" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
let activeTool: AppState["activeTool"];
|
let activeTool: AppState["activeTool"];
|
||||||
|
|
||||||
if (isEraserActive(appState)) {
|
if (isEraserActive(appState)) {
|
||||||
activeTool = updateActiveTool(appState, {
|
activeTool = updateActiveTool(appState, {
|
||||||
...(appState.activeTool.lastActiveToolBeforeEraser || {
|
...(appState.activeTool.lastActiveTool || {
|
||||||
type: "selection",
|
type: "selection",
|
||||||
}),
|
}),
|
||||||
lastActiveToolBeforeEraser: null,
|
lastActiveToolBeforeEraser: null,
|
||||||
@@ -351,17 +340,38 @@ export const actionErase = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
keyTest: (event) => event.key === KEYS.E,
|
keyTest: (event) => event.key === KEYS.E,
|
||||||
PanelComponent: ({ elements, appState, updateData, data }) => (
|
});
|
||||||
<ToolButton
|
|
||||||
type="button"
|
export const actionToggleHandTool = register({
|
||||||
icon={eraser}
|
name: "toggleHandTool",
|
||||||
className={clsx("eraser", { active: isEraserActive(appState) })}
|
trackEvent: { category: "toolbar" },
|
||||||
title={`${t("toolBar.eraser")}-${getShortcutKey("E")}`}
|
perform: (elements, appState, _, app) => {
|
||||||
aria-label={t("toolBar.eraser")}
|
let activeTool: AppState["activeTool"];
|
||||||
onClick={() => {
|
|
||||||
updateData(null);
|
if (isHandToolActive(appState)) {
|
||||||
}}
|
activeTool = updateActiveTool(appState, {
|
||||||
size={data?.size || "medium"}
|
...(appState.activeTool.lastActiveTool || {
|
||||||
></ToolButton>
|
type: "selection",
|
||||||
),
|
}),
|
||||||
|
lastActiveToolBeforeEraser: null,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
activeTool = updateActiveTool(appState, {
|
||||||
|
type: "hand",
|
||||||
|
lastActiveToolBeforeEraser: appState.activeTool,
|
||||||
|
});
|
||||||
|
setCursor(app.canvas, CURSOR_TYPE.GRAB);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
selectedElementIds: {},
|
||||||
|
selectedGroupIds: {},
|
||||||
|
activeTool,
|
||||||
|
},
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
keyTest: (event) => event.key === KEYS.H,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const actionCopy = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemPredicate: (elements, appState, appProps, app) => {
|
predicate: (elements, appState, appProps, app) => {
|
||||||
return app.device.isMobile && !!navigator.clipboard;
|
return app.device.isMobile && !!navigator.clipboard;
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.copy",
|
contextItemLabel: "labels.copy",
|
||||||
@@ -41,7 +41,7 @@ export const actionPaste = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemPredicate: (elements, appState, appProps, app) => {
|
predicate: (elements, appState, appProps, app) => {
|
||||||
return app.device.isMobile && !!navigator.clipboard;
|
return app.device.isMobile && !!navigator.clipboard;
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.paste",
|
contextItemLabel: "labels.paste",
|
||||||
@@ -56,7 +56,7 @@ export const actionCut = register({
|
|||||||
actionCopy.perform(elements, appState, data, app);
|
actionCopy.perform(elements, appState, data, app);
|
||||||
return actionDeleteSelected.perform(elements, appState);
|
return actionDeleteSelected.perform(elements, appState);
|
||||||
},
|
},
|
||||||
contextItemPredicate: (elements, appState, appProps, app) => {
|
predicate: (elements, appState, appProps, app) => {
|
||||||
return app.device.isMobile && !!navigator.clipboard;
|
return app.device.isMobile && !!navigator.clipboard;
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.cut",
|
contextItemLabel: "labels.cut",
|
||||||
@@ -101,7 +101,7 @@ export const actionCopyAsSvg = register({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
contextItemPredicate: (elements) => {
|
predicate: (elements) => {
|
||||||
return probablySupportsClipboardWriteText && elements.length > 0;
|
return probablySupportsClipboardWriteText && elements.length > 0;
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.copyAsSvg",
|
contextItemLabel: "labels.copyAsSvg",
|
||||||
@@ -158,7 +158,7 @@ export const actionCopyAsPng = register({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
contextItemPredicate: (elements) => {
|
predicate: (elements) => {
|
||||||
return probablySupportsClipboardBlob && elements.length > 0;
|
return probablySupportsClipboardBlob && elements.length > 0;
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.copyAsPng",
|
contextItemLabel: "labels.copyAsPng",
|
||||||
@@ -188,7 +188,7 @@ export const copyText = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemPredicate: (elements, appState) => {
|
predicate: (elements, appState) => {
|
||||||
return (
|
return (
|
||||||
probablySupportsClipboardWriteText &&
|
probablySupportsClipboardWriteText &&
|
||||||
getSelectedElements(elements, appState, true).some(isTextElement)
|
getSelectedElements(elements, appState, true).some(isTextElement)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { LoadIcon, questionCircle, saveAs } from "../components/icons";
|
import { questionCircle, saveAs } from "../components/icons";
|
||||||
import { ProjectName } from "../components/ProjectName";
|
import { ProjectName } from "../components/ProjectName";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import "../components/ToolIcon.scss";
|
|
||||||
import { Tooltip } from "../components/Tooltip";
|
import { Tooltip } from "../components/Tooltip";
|
||||||
import { DarkModeToggle } from "../components/DarkModeToggle";
|
import { DarkModeToggle } from "../components/DarkModeToggle";
|
||||||
import { loadFromJSON, saveAsJSON } from "../data";
|
import { loadFromJSON, saveAsJSON } from "../data";
|
||||||
@@ -15,12 +14,11 @@ import { getExportSize } from "../scene/export";
|
|||||||
import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES, THEME } from "../constants";
|
import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES, THEME } from "../constants";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ActiveFile } from "../components/ActiveFile";
|
|
||||||
import { isImageFileHandle } from "../data/blob";
|
import { isImageFileHandle } from "../data/blob";
|
||||||
import { nativeFileSystemSupported } from "../data/filesystem";
|
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||||
import { Theme } from "../element/types";
|
import { Theme } from "../element/types";
|
||||||
import DropdownMenuItem from "../components/dropdownMenu/DropdownMenuItem";
|
|
||||||
import { getShortcutFromShortcutName } from "./shortcuts";
|
import "../components/ToolIcon.scss";
|
||||||
|
|
||||||
export const actionChangeProjectName = register({
|
export const actionChangeProjectName = register({
|
||||||
name: "changeProjectName",
|
name: "changeProjectName",
|
||||||
@@ -133,6 +131,13 @@ export const actionChangeExportEmbedScene = register({
|
|||||||
export const actionSaveToActiveFile = register({
|
export const actionSaveToActiveFile = register({
|
||||||
name: "saveToActiveFile",
|
name: "saveToActiveFile",
|
||||||
trackEvent: { category: "export" },
|
trackEvent: { category: "export" },
|
||||||
|
predicate: (elements, appState, props, app) => {
|
||||||
|
return (
|
||||||
|
!!app.props.UIOptions.canvasActions.saveToActiveFile &&
|
||||||
|
!!appState.fileHandle &&
|
||||||
|
!appState.viewModeEnabled
|
||||||
|
);
|
||||||
|
},
|
||||||
perform: async (elements, appState, value, app) => {
|
perform: async (elements, appState, value, app) => {
|
||||||
const fileHandleExists = !!appState.fileHandle;
|
const fileHandleExists = !!appState.fileHandle;
|
||||||
|
|
||||||
@@ -169,12 +174,6 @@ export const actionSaveToActiveFile = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event.key === KEYS.S && event[KEYS.CTRL_OR_CMD] && !event.shiftKey,
|
event.key === KEYS.S && event[KEYS.CTRL_OR_CMD] && !event.shiftKey,
|
||||||
PanelComponent: ({ updateData, appState }) => (
|
|
||||||
<ActiveFile
|
|
||||||
onSave={() => updateData(null)}
|
|
||||||
fileName={appState.fileHandle?.name}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionSaveFileToDisk = register({
|
export const actionSaveFileToDisk = register({
|
||||||
@@ -220,6 +219,11 @@ export const actionSaveFileToDisk = register({
|
|||||||
export const actionLoadScene = register({
|
export const actionLoadScene = register({
|
||||||
name: "loadScene",
|
name: "loadScene",
|
||||||
trackEvent: { category: "export" },
|
trackEvent: { category: "export" },
|
||||||
|
predicate: (elements, appState, props, app) => {
|
||||||
|
return (
|
||||||
|
!!app.props.UIOptions.canvasActions.loadScene && !appState.viewModeEnabled
|
||||||
|
);
|
||||||
|
},
|
||||||
perform: async (elements, appState, _, app) => {
|
perform: async (elements, appState, _, app) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
@@ -247,19 +251,6 @@ export const actionLoadScene = register({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
|
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
|
||||||
PanelComponent: ({ updateData }) => {
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem
|
|
||||||
icon={LoadIcon}
|
|
||||||
onSelect={updateData}
|
|
||||||
dataTestId="load-button"
|
|
||||||
shortcut={getShortcutFromShortcutName("loadScene")}
|
|
||||||
ariaLabel={t("buttons.load")}
|
|
||||||
>
|
|
||||||
{t("buttons.load")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionExportWithDarkMode = register({
|
export const actionExportWithDarkMode = register({
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export const actionFinalize = register({
|
|||||||
let activeTool: AppState["activeTool"];
|
let activeTool: AppState["activeTool"];
|
||||||
if (appState.activeTool.type === "eraser") {
|
if (appState.activeTool.type === "eraser") {
|
||||||
activeTool = updateActiveTool(appState, {
|
activeTool = updateActiveTool(appState, {
|
||||||
...(appState.activeTool.lastActiveToolBeforeEraser || {
|
...(appState.activeTool.lastActiveTool || {
|
||||||
type: "selection",
|
type: "selection",
|
||||||
}),
|
}),
|
||||||
lastActiveToolBeforeEraser: null,
|
lastActiveToolBeforeEraser: null,
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const actionFlipHorizontal = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) => event.shiftKey && event.code === "KeyH",
|
keyTest: (event) => event.shiftKey && event.code === "KeyH",
|
||||||
contextItemLabel: "labels.flipHorizontal",
|
contextItemLabel: "labels.flipHorizontal",
|
||||||
contextItemPredicate: (elements, appState) =>
|
predicate: (elements, appState) =>
|
||||||
enableActionFlipHorizontal(elements, appState),
|
enableActionFlipHorizontal(elements, appState),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ export const actionFlipVertical = register({
|
|||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event.shiftKey && event.code === "KeyV" && !event[KEYS.CTRL_OR_CMD],
|
event.shiftKey && event.code === "KeyV" && !event[KEYS.CTRL_OR_CMD],
|
||||||
contextItemLabel: "labels.flipVertical",
|
contextItemLabel: "labels.flipVertical",
|
||||||
contextItemPredicate: (elements, appState) =>
|
predicate: (elements, appState) =>
|
||||||
enableActionFlipVertical(elements, appState),
|
enableActionFlipVertical(elements, appState),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -129,8 +129,7 @@ export const actionGroup = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.group",
|
contextItemLabel: "labels.group",
|
||||||
contextItemPredicate: (elements, appState) =>
|
predicate: (elements, appState) => enableActionGroup(elements, appState),
|
||||||
enableActionGroup(elements, appState),
|
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
|
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
@@ -193,8 +192,7 @@ export const actionUngroup = register({
|
|||||||
event[KEYS.CTRL_OR_CMD] &&
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
event.key === KEYS.G.toUpperCase(),
|
event.key === KEYS.G.toUpperCase(),
|
||||||
contextItemLabel: "labels.ungroup",
|
contextItemLabel: "labels.ungroup",
|
||||||
contextItemPredicate: (elements, appState) =>
|
predicate: (elements, appState) => getSelectedGroupIds(appState).length > 0,
|
||||||
getSelectedGroupIds(appState).length > 0,
|
|
||||||
|
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import { t } from "../i18n";
|
|||||||
import History, { HistoryEntry } from "../history";
|
import History, { HistoryEntry } from "../history";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { isWindows, KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { fixBindingsAfterDeletion } from "../element/binding";
|
import { fixBindingsAfterDeletion } from "../element/binding";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
|
import { isWindows } from "../constants";
|
||||||
|
|
||||||
const writeData = (
|
const writeData = (
|
||||||
prevElements: readonly ExcalidrawElement[],
|
prevElements: readonly ExcalidrawElement[],
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const actionToggleLinearEditor = register({
|
|||||||
trackEvent: {
|
trackEvent: {
|
||||||
category: "element",
|
category: "element",
|
||||||
},
|
},
|
||||||
contextItemPredicate: (elements, appState) => {
|
predicate: (elements, appState) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { HamburgerMenuIcon, HelpIcon, palette } from "../components/icons";
|
import { HamburgerMenuIcon, palette } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { showSelectedShapeActions, getNonDeletedElements } from "../element";
|
import { showSelectedShapeActions, getNonDeletedElements } from "../element";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
|
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { HelpButton } from "../components/HelpButton";
|
|
||||||
import DropdownMenuItem from "../components/dropdownMenu/DropdownMenuItem";
|
|
||||||
|
|
||||||
export const actionToggleCanvasMenu = register({
|
export const actionToggleCanvasMenu = register({
|
||||||
name: "toggleCanvasMenu",
|
name: "toggleCanvasMenu",
|
||||||
@@ -88,19 +86,5 @@ export const actionShortcuts = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ updateData, isInHamburgerMenu }) =>
|
|
||||||
isInHamburgerMenu ? (
|
|
||||||
<DropdownMenuItem
|
|
||||||
dataTestId="help-menu-item"
|
|
||||||
icon={HelpIcon}
|
|
||||||
onSelect={updateData}
|
|
||||||
shortcut="?"
|
|
||||||
ariaLabel={t("helpDialog.title")}
|
|
||||||
>
|
|
||||||
{t("helpDialog.title")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
) : (
|
|
||||||
<HelpButton title={t("helpDialog.title")} onClick={updateData} />
|
|
||||||
),
|
|
||||||
keyTest: (event) => event.key === KEYS.QUESTION_MARK,
|
keyTest: (event) => event.key === KEYS.QUESTION_MARK,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const actionToggleGridMode = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
checked: (appState: AppState) => appState.gridSize !== null,
|
checked: (appState: AppState) => appState.gridSize !== null,
|
||||||
contextItemPredicate: (element, appState, props) => {
|
predicate: (element, appState, props) => {
|
||||||
return typeof props.gridModeEnabled === "undefined";
|
return typeof props.gridModeEnabled === "undefined";
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.showGrid",
|
contextItemLabel: "labels.showGrid",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const actionToggleViewMode = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
checked: (appState) => appState.viewModeEnabled,
|
checked: (appState) => appState.viewModeEnabled,
|
||||||
contextItemPredicate: (elements, appState, appProps) => {
|
predicate: (elements, appState, appProps) => {
|
||||||
return typeof appProps.viewModeEnabled === "undefined";
|
return typeof appProps.viewModeEnabled === "undefined";
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.viewMode",
|
contextItemLabel: "labels.viewMode",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const actionToggleZenMode = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
checked: (appState) => appState.zenModeEnabled,
|
checked: (appState) => appState.zenModeEnabled,
|
||||||
contextItemPredicate: (elements, appState, appProps) => {
|
predicate: (elements, appState, appProps) => {
|
||||||
return typeof appProps.zenModeEnabled === "undefined";
|
return typeof appProps.zenModeEnabled === "undefined";
|
||||||
},
|
},
|
||||||
contextItemLabel: "buttons.zenMode",
|
contextItemLabel: "buttons.zenMode",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
moveAllLeft,
|
moveAllLeft,
|
||||||
moveAllRight,
|
moveAllRight,
|
||||||
} from "../zindex";
|
} from "../zindex";
|
||||||
import { KEYS, isDarwin, CODES } from "../keys";
|
import { KEYS, CODES } from "../keys";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
SendBackwardIcon,
|
SendBackwardIcon,
|
||||||
SendToBackIcon,
|
SendToBackIcon,
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
|
import { isDarwin } from "../constants";
|
||||||
|
|
||||||
export const actionSendBackward = register({
|
export const actionSendBackward = register({
|
||||||
name: "sendBackward",
|
name: "sendBackward",
|
||||||
|
|||||||
+11
-6
@@ -131,11 +131,7 @@ export class ActionManager {
|
|||||||
/**
|
/**
|
||||||
* @param data additional data sent to the PanelComponent
|
* @param data additional data sent to the PanelComponent
|
||||||
*/
|
*/
|
||||||
renderAction = (
|
renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => {
|
||||||
name: ActionName,
|
|
||||||
data?: PanelComponentProps["data"],
|
|
||||||
isInHamburgerMenu = false,
|
|
||||||
) => {
|
|
||||||
const canvasActions = this.app.props.UIOptions.canvasActions;
|
const canvasActions = this.app.props.UIOptions.canvasActions;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -170,11 +166,20 @@ export class ActionManager {
|
|||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
appProps={this.app.props}
|
appProps={this.app.props}
|
||||||
data={data}
|
data={data}
|
||||||
isInHamburgerMenu={isInHamburgerMenu}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
isActionEnabled = (action: Action) => {
|
||||||
|
const elements = this.getElementsIncludingDeleted();
|
||||||
|
const appState = this.getAppState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
!action.predicate ||
|
||||||
|
action.predicate(elements, appState, this.app.props, this.app)
|
||||||
|
);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { isDarwin } from "../constants";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { isDarwin } from "../keys";
|
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import { ActionName } from "./types";
|
import { ActionName } from "./types";
|
||||||
|
|
||||||
|
|||||||
@@ -109,10 +109,11 @@ export type ActionName =
|
|||||||
| "decreaseFontSize"
|
| "decreaseFontSize"
|
||||||
| "unbindText"
|
| "unbindText"
|
||||||
| "hyperlink"
|
| "hyperlink"
|
||||||
| "eraser"
|
|
||||||
| "bindText"
|
| "bindText"
|
||||||
| "toggleLock"
|
| "toggleLock"
|
||||||
| "toggleLinearEditor";
|
| "toggleLinearEditor"
|
||||||
|
| "toggleEraserTool"
|
||||||
|
| "toggleHandTool";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
@@ -124,9 +125,7 @@ export type PanelComponentProps = {
|
|||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
name: ActionName;
|
name: ActionName;
|
||||||
PanelComponent?: React.FC<
|
PanelComponent?: React.FC<PanelComponentProps>;
|
||||||
PanelComponentProps & { isInHamburgerMenu: boolean }
|
|
||||||
>;
|
|
||||||
perform: ActionFn;
|
perform: ActionFn;
|
||||||
keyPriority?: number;
|
keyPriority?: number;
|
||||||
keyTest?: (
|
keyTest?: (
|
||||||
@@ -140,7 +139,7 @@ export interface Action {
|
|||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
) => string);
|
) => string);
|
||||||
contextItemPredicate?: (
|
predicate?: (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
appProps: ExcalidrawProps,
|
appProps: ExcalidrawProps,
|
||||||
|
|||||||
+9
-1
@@ -45,7 +45,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
type: "selection",
|
type: "selection",
|
||||||
customType: null,
|
customType: null,
|
||||||
locked: false,
|
locked: false,
|
||||||
lastActiveToolBeforeEraser: null,
|
lastActiveTool: null,
|
||||||
},
|
},
|
||||||
penMode: false,
|
penMode: false,
|
||||||
penDetected: false,
|
penDetected: false,
|
||||||
@@ -228,3 +228,11 @@ export const isEraserActive = ({
|
|||||||
}: {
|
}: {
|
||||||
activeTool: AppState["activeTool"];
|
activeTool: AppState["activeTool"];
|
||||||
}) => activeTool.type === "eraser";
|
}) => activeTool.type === "eraser";
|
||||||
|
|
||||||
|
export const isHandToolActive = ({
|
||||||
|
activeTool,
|
||||||
|
}: {
|
||||||
|
activeTool: AppState["activeTool"];
|
||||||
|
}) => {
|
||||||
|
return activeTool.type === "hand";
|
||||||
|
};
|
||||||
|
|||||||
+4
-5
@@ -180,16 +180,16 @@ export const parseClipboard = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
||||||
let promise;
|
|
||||||
try {
|
try {
|
||||||
// in Safari so far we need to construct the ClipboardItem synchronously
|
// in Safari so far we need to construct the ClipboardItem synchronously
|
||||||
// (i.e. in the same tick) otherwise browser will complain for lack of
|
// (i.e. in the same tick) otherwise browser will complain for lack of
|
||||||
// user intent. Using a Promise ClipboardItem constructor solves this.
|
// user intent. Using a Promise ClipboardItem constructor solves this.
|
||||||
// https://bugs.webkit.org/show_bug.cgi?id=222262
|
// https://bugs.webkit.org/show_bug.cgi?id=222262
|
||||||
//
|
//
|
||||||
// not await so that we can detect whether the thrown error likely relates
|
// Note that Firefox (and potentially others) seems to support Promise
|
||||||
// to a lack of support for the Promise ClipboardItem constructor
|
// ClipboardItem constructor, but throws on an unrelated MIME type error.
|
||||||
promise = navigator.clipboard.write([
|
// So we need to await this and fallback to awaiting the blob if applicable.
|
||||||
|
await navigator.clipboard.write([
|
||||||
new window.ClipboardItem({
|
new window.ClipboardItem({
|
||||||
[MIME_TYPES.png]: blob,
|
[MIME_TYPES.png]: blob,
|
||||||
}),
|
}),
|
||||||
@@ -207,7 +207,6 @@ export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await promise;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyTextToSystemClipboard = async (text: string | null) => {
|
export const copyTextToSystemClipboard = async (text: string | null) => {
|
||||||
|
|||||||
@@ -219,9 +219,10 @@ export const ShapesSwitcher = ({
|
|||||||
<>
|
<>
|
||||||
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
||||||
const label = t(`toolBar.${value}`);
|
const label = t(`toolBar.${value}`);
|
||||||
const letter = key && (typeof key === "string" ? key : key[0]);
|
const letter =
|
||||||
|
key && capitalizeString(typeof key === "string" ? key : key[0]);
|
||||||
const shortcut = letter
|
const shortcut = letter
|
||||||
? `${capitalizeString(letter)} ${t("helpDialog.or")} ${numericKey}`
|
? `${letter} ${t("helpDialog.or")} ${numericKey}`
|
||||||
: `${numericKey}`;
|
: `${numericKey}`;
|
||||||
return (
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
@@ -232,7 +233,7 @@ export const ShapesSwitcher = ({
|
|||||||
checked={activeTool.type === value}
|
checked={activeTool.type === value}
|
||||||
name="editor-current-shape"
|
name="editor-current-shape"
|
||||||
title={`${capitalizeString(label)} — ${shortcut}`}
|
title={`${capitalizeString(label)} — ${shortcut}`}
|
||||||
keyBindingLabel={numericKey}
|
keyBindingLabel={numericKey || letter}
|
||||||
aria-label={capitalizeString(label)}
|
aria-label={capitalizeString(label)}
|
||||||
aria-keyshortcuts={shortcut}
|
aria-keyshortcuts={shortcut}
|
||||||
data-testid={`toolbar-${value}`}
|
data-testid={`toolbar-${value}`}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { atom, useAtom } from "jotai";
|
||||||
|
import { actionClearCanvas } from "../actions";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { useExcalidrawActionManager } from "./App";
|
||||||
|
import ConfirmDialog from "./ConfirmDialog";
|
||||||
|
|
||||||
|
export const activeConfirmDialogAtom = atom<"clearCanvas" | null>(null);
|
||||||
|
|
||||||
|
export const ActiveConfirmDialog = () => {
|
||||||
|
const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
|
||||||
|
activeConfirmDialogAtom,
|
||||||
|
);
|
||||||
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
|
if (!activeConfirmDialog) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeConfirmDialog === "clearCanvas") {
|
||||||
|
return (
|
||||||
|
<ConfirmDialog
|
||||||
|
onConfirm={() => {
|
||||||
|
actionManager.executeAction(actionClearCanvas);
|
||||||
|
setActiveConfirmDialog(null);
|
||||||
|
}}
|
||||||
|
onCancel={() => setActiveConfirmDialog(null)}
|
||||||
|
title={t("clearCanvasDialog.title")}
|
||||||
|
>
|
||||||
|
<p className="clear-canvas__content"> {t("alerts.clearReset")}</p>
|
||||||
|
</ConfirmDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
// TODO barnabasmolnar/editor-redesign
|
|
||||||
// this icon is not great
|
|
||||||
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
|
||||||
import { save } from "../components/icons";
|
|
||||||
import { t } from "../i18n";
|
|
||||||
|
|
||||||
import "./ActiveFile.scss";
|
|
||||||
import DropdownMenuItem from "./dropdownMenu/DropdownMenuItem";
|
|
||||||
|
|
||||||
type ActiveFileProps = {
|
|
||||||
fileName?: string;
|
|
||||||
onSave: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ActiveFile = ({ fileName, onSave }: ActiveFileProps) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
shortcut={getShortcutFromShortcutName("saveScene")}
|
|
||||||
dataTestId="save-button"
|
|
||||||
onSelect={onSave}
|
|
||||||
icon={save}
|
|
||||||
ariaLabel={`${t("buttons.save")}`}
|
|
||||||
>{`${t("buttons.save")}`}</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
+38
-17
@@ -41,7 +41,11 @@ import { ActionManager } from "../actions/manager";
|
|||||||
import { actions } from "../actions/register";
|
import { actions } from "../actions/register";
|
||||||
import { ActionResult } from "../actions/types";
|
import { ActionResult } from "../actions/types";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { getDefaultAppState, isEraserActive } from "../appState";
|
import {
|
||||||
|
getDefaultAppState,
|
||||||
|
isEraserActive,
|
||||||
|
isHandToolActive,
|
||||||
|
} from "../appState";
|
||||||
import { parseClipboard } from "../clipboard";
|
import { parseClipboard } from "../clipboard";
|
||||||
import {
|
import {
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
@@ -57,6 +61,7 @@ import {
|
|||||||
EVENT,
|
EVENT,
|
||||||
GRID_SIZE,
|
GRID_SIZE,
|
||||||
IMAGE_RENDER_TIMEOUT,
|
IMAGE_RENDER_TIMEOUT,
|
||||||
|
isAndroid,
|
||||||
LINE_CONFIRM_THRESHOLD,
|
LINE_CONFIRM_THRESHOLD,
|
||||||
MAX_ALLOWED_FILE_BYTES,
|
MAX_ALLOWED_FILE_BYTES,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
@@ -166,7 +171,6 @@ import {
|
|||||||
shouldRotateWithDiscreteAngle,
|
shouldRotateWithDiscreteAngle,
|
||||||
isArrowKey,
|
isArrowKey,
|
||||||
KEYS,
|
KEYS,
|
||||||
isAndroid,
|
|
||||||
} from "../keys";
|
} from "../keys";
|
||||||
import { distance2d, getGridPoint, isPathALoop } from "../math";
|
import { distance2d, getGridPoint, isPathALoop } from "../math";
|
||||||
import { renderScene } from "../renderer/renderScene";
|
import { renderScene } from "../renderer/renderScene";
|
||||||
@@ -274,6 +278,7 @@ import {
|
|||||||
import { shouldShowBoundingBox } from "../element/transformHandles";
|
import { shouldShowBoundingBox } from "../element/transformHandles";
|
||||||
import { Fonts } from "../scene/Fonts";
|
import { Fonts } from "../scene/Fonts";
|
||||||
import { actionPaste } from "../actions/actionClipboard";
|
import { actionPaste } from "../actions/actionClipboard";
|
||||||
|
import { actionToggleHandTool } from "../actions/actionCanvas";
|
||||||
|
|
||||||
const deviceContextInitialValue = {
|
const deviceContextInitialValue = {
|
||||||
isSmScreen: false,
|
isSmScreen: false,
|
||||||
@@ -283,15 +288,12 @@ const deviceContextInitialValue = {
|
|||||||
};
|
};
|
||||||
const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
|
const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
|
||||||
DeviceContext.displayName = "DeviceContext";
|
DeviceContext.displayName = "DeviceContext";
|
||||||
export const useDevice = () => useContext<Device>(DeviceContext);
|
|
||||||
|
|
||||||
export const ExcalidrawContainerContext = React.createContext<{
|
export const ExcalidrawContainerContext = React.createContext<{
|
||||||
container: HTMLDivElement | null;
|
container: HTMLDivElement | null;
|
||||||
id: string | null;
|
id: string | null;
|
||||||
}>({ container: null, id: null });
|
}>({ container: null, id: null });
|
||||||
ExcalidrawContainerContext.displayName = "ExcalidrawContainerContext";
|
ExcalidrawContainerContext.displayName = "ExcalidrawContainerContext";
|
||||||
export const useExcalidrawContainer = () =>
|
|
||||||
useContext(ExcalidrawContainerContext);
|
|
||||||
|
|
||||||
const ExcalidrawElementsContext = React.createContext<
|
const ExcalidrawElementsContext = React.createContext<
|
||||||
readonly NonDeletedExcalidrawElement[]
|
readonly NonDeletedExcalidrawElement[]
|
||||||
@@ -309,14 +311,19 @@ ExcalidrawAppStateContext.displayName = "ExcalidrawAppStateContext";
|
|||||||
|
|
||||||
const ExcalidrawSetAppStateContext = React.createContext<
|
const ExcalidrawSetAppStateContext = React.createContext<
|
||||||
React.Component<any, AppState>["setState"]
|
React.Component<any, AppState>["setState"]
|
||||||
>(() => {});
|
>(() => {
|
||||||
|
console.warn("unitialized ExcalidrawSetAppStateContext context!");
|
||||||
|
});
|
||||||
ExcalidrawSetAppStateContext.displayName = "ExcalidrawSetAppStateContext";
|
ExcalidrawSetAppStateContext.displayName = "ExcalidrawSetAppStateContext";
|
||||||
|
|
||||||
const ExcalidrawActionManagerContext = React.createContext<
|
const ExcalidrawActionManagerContext = React.createContext<ActionManager>(
|
||||||
ActionManager | { renderAction: ActionManager["renderAction"] }
|
null!,
|
||||||
>({ renderAction: () => null });
|
);
|
||||||
ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext";
|
ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext";
|
||||||
|
|
||||||
|
export const useDevice = () => useContext<Device>(DeviceContext);
|
||||||
|
export const useExcalidrawContainer = () =>
|
||||||
|
useContext(ExcalidrawContainerContext);
|
||||||
export const useExcalidrawElements = () =>
|
export const useExcalidrawElements = () =>
|
||||||
useContext(ExcalidrawElementsContext);
|
useContext(ExcalidrawElementsContext);
|
||||||
export const useExcalidrawAppState = () =>
|
export const useExcalidrawAppState = () =>
|
||||||
@@ -537,8 +544,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
const { onCollabButtonClick, renderTopRightUI, renderCustomStats } =
|
const { renderTopRightUI, renderCustomStats } = this.props;
|
||||||
this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -572,9 +578,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
setAppState={this.setAppState}
|
setAppState={this.setAppState}
|
||||||
actionManager={this.actionManager}
|
actionManager={this.actionManager}
|
||||||
elements={this.scene.getNonDeletedElements()}
|
elements={this.scene.getNonDeletedElements()}
|
||||||
onCollabButtonClick={onCollabButtonClick}
|
|
||||||
onLockToggle={this.toggleLock}
|
onLockToggle={this.toggleLock}
|
||||||
onPenModeToggle={this.togglePenMode}
|
onPenModeToggle={this.togglePenMode}
|
||||||
|
onHandToolToggle={this.onHandToolToggle}
|
||||||
onInsertElements={(elements) =>
|
onInsertElements={(elements) =>
|
||||||
this.addElementsFromPasteOrLibrary({
|
this.addElementsFromPasteOrLibrary({
|
||||||
elements,
|
elements,
|
||||||
@@ -583,7 +589,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
langCode={getLanguage().code}
|
langCode={getLanguage().code}
|
||||||
isCollaborating={this.props.isCollaborating}
|
|
||||||
renderTopRightUI={renderTopRightUI}
|
renderTopRightUI={renderTopRightUI}
|
||||||
renderCustomStats={renderCustomStats}
|
renderCustomStats={renderCustomStats}
|
||||||
renderCustomSidebar={this.props.renderSidebar}
|
renderCustomSidebar={this.props.renderSidebar}
|
||||||
@@ -598,6 +603,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
id={this.id}
|
id={this.id}
|
||||||
onImageAction={this.onImageAction}
|
onImageAction={this.onImageAction}
|
||||||
renderWelcomeScreen={
|
renderWelcomeScreen={
|
||||||
|
!this.state.isLoading &&
|
||||||
this.state.showWelcomeScreen &&
|
this.state.showWelcomeScreen &&
|
||||||
this.state.activeTool.type === "selection" &&
|
this.state.activeTool.type === "selection" &&
|
||||||
!this.scene.getElementsIncludingDeleted().length
|
!this.scene.getElementsIncludingDeleted().length
|
||||||
@@ -1810,6 +1816,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onHandToolToggle = () => {
|
||||||
|
this.actionManager.executeAction(actionToggleHandTool);
|
||||||
|
};
|
||||||
|
|
||||||
scrollToContent = (
|
scrollToContent = (
|
||||||
target:
|
target:
|
||||||
| ExcalidrawElement
|
| ExcalidrawElement
|
||||||
@@ -2227,11 +2237,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
private setActiveTool = (
|
private setActiveTool = (
|
||||||
tool:
|
tool:
|
||||||
| { type: typeof SHAPES[number]["value"] | "eraser" }
|
| { type: typeof SHAPES[number]["value"] | "eraser" | "hand" }
|
||||||
| { type: "custom"; customType: string },
|
| { type: "custom"; customType: string },
|
||||||
) => {
|
) => {
|
||||||
const nextActiveTool = updateActiveTool(this.state, tool);
|
const nextActiveTool = updateActiveTool(this.state, tool);
|
||||||
if (!isHoldingSpace) {
|
if (nextActiveTool.type === "hand") {
|
||||||
|
setCursor(this.canvas, CURSOR_TYPE.GRAB);
|
||||||
|
} else if (!isHoldingSpace) {
|
||||||
setCursorForShape(this.canvas, this.state);
|
setCursorForShape(this.canvas, this.state);
|
||||||
}
|
}
|
||||||
if (isToolIcon(document.activeElement)) {
|
if (isToolIcon(document.activeElement)) {
|
||||||
@@ -2902,7 +2914,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
null;
|
null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isHoldingSpace || isPanning || isDraggingScrollBar) {
|
if (
|
||||||
|
isHoldingSpace ||
|
||||||
|
isPanning ||
|
||||||
|
isDraggingScrollBar ||
|
||||||
|
isHandToolActive(this.state)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3494,7 +3511,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
} else if (this.state.activeTool.type === "custom") {
|
} else if (this.state.activeTool.type === "custom") {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
||||||
} else if (this.state.activeTool.type !== "eraser") {
|
} else if (
|
||||||
|
this.state.activeTool.type !== "eraser" &&
|
||||||
|
this.state.activeTool.type !== "hand"
|
||||||
|
) {
|
||||||
this.createGenericElementOnPointerDown(
|
this.createGenericElementOnPointerDown(
|
||||||
this.state.activeTool.type,
|
this.state.activeTool.type,
|
||||||
pointerDownState,
|
pointerDownState,
|
||||||
@@ -3605,6 +3625,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
gesture.pointers.size <= 1 &&
|
gesture.pointers.size <= 1 &&
|
||||||
(event.button === POINTER_BUTTON.WHEEL ||
|
(event.button === POINTER_BUTTON.WHEEL ||
|
||||||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
|
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
|
||||||
|
isHandToolActive(this.state) ||
|
||||||
this.state.viewModeEnabled)
|
this.state.viewModeEnabled)
|
||||||
) ||
|
) ||
|
||||||
isTextElement(this.state.editingElement)
|
isTextElement(this.state.editingElement)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@import "../css/theme";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.excalidraw-button {
|
||||||
|
@include outlineButtonStyles;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import "./Button.scss";
|
||||||
|
|
||||||
|
interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||||
|
type?: "button" | "submit" | "reset";
|
||||||
|
onSelect: () => any;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic button component that follows Excalidraw's design system.
|
||||||
|
* Style can be customised using `className` or `style` prop.
|
||||||
|
* Accepts all props that a regular `button` element accepts.
|
||||||
|
*/
|
||||||
|
export const Button = ({
|
||||||
|
type = "button",
|
||||||
|
onSelect,
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
...rest
|
||||||
|
}: ButtonProps) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={(event) => {
|
||||||
|
onSelect();
|
||||||
|
rest.onClick?.(event);
|
||||||
|
}}
|
||||||
|
type={type}
|
||||||
|
className={`excalidraw-button ${className}`}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { t } from "../i18n";
|
|
||||||
import { TrashIcon } from "./icons";
|
|
||||||
|
|
||||||
import ConfirmDialog from "./ConfirmDialog";
|
|
||||||
import DropdownMenuItem from "./dropdownMenu/DropdownMenuItem";
|
|
||||||
|
|
||||||
const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => {
|
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
|
||||||
const toggleDialog = () => {
|
|
||||||
setShowDialog(!showDialog);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DropdownMenuItem
|
|
||||||
icon={TrashIcon}
|
|
||||||
onSelect={toggleDialog}
|
|
||||||
dataTestId="clear-canvas-button"
|
|
||||||
ariaLabel={t("buttons.clearReset")}
|
|
||||||
>
|
|
||||||
{t("buttons.clearReset")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
{showDialog && (
|
|
||||||
<ConfirmDialog
|
|
||||||
onConfirm={() => {
|
|
||||||
onConfirm();
|
|
||||||
toggleDialog();
|
|
||||||
}}
|
|
||||||
onCancel={toggleDialog}
|
|
||||||
title={t("clearCanvasDialog.title")}
|
|
||||||
>
|
|
||||||
<p className="clear-canvas__content"> {t("alerts.clearReset")}</p>
|
|
||||||
</ConfirmDialog>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClearCanvas;
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { t } from "../i18n";
|
|
||||||
import { UsersIcon } from "./icons";
|
|
||||||
|
|
||||||
import "./CollabButton.scss";
|
|
||||||
import DropdownMenuItem from "./dropdownMenu/DropdownMenuItem";
|
|
||||||
import clsx from "clsx";
|
|
||||||
|
|
||||||
const CollabButton = ({
|
|
||||||
isCollaborating,
|
|
||||||
collaboratorCount,
|
|
||||||
onClick,
|
|
||||||
isInHamburgerMenu = true,
|
|
||||||
}: {
|
|
||||||
isCollaborating: boolean;
|
|
||||||
collaboratorCount: number;
|
|
||||||
onClick: () => void;
|
|
||||||
isInHamburgerMenu?: boolean;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isInHamburgerMenu ? (
|
|
||||||
<DropdownMenuItem
|
|
||||||
dataTestId="collab-button"
|
|
||||||
icon={UsersIcon}
|
|
||||||
onSelect={onClick}
|
|
||||||
ariaLabel={t("labels.liveCollaboration")}
|
|
||||||
>
|
|
||||||
{t("labels.liveCollaboration")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className={clsx("collab-button", { active: isCollaborating })}
|
|
||||||
type="button"
|
|
||||||
onClick={onClick}
|
|
||||||
style={{ position: "relative" }}
|
|
||||||
title={t("labels.liveCollaboration")}
|
|
||||||
>
|
|
||||||
{UsersIcon}
|
|
||||||
{collaboratorCount > 0 && (
|
|
||||||
<div className="CollabButton-collaborators">
|
|
||||||
{collaboratorCount}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CollabButton;
|
|
||||||
@@ -39,8 +39,8 @@ export const ContextMenu = React.memo(
|
|||||||
if (
|
if (
|
||||||
item &&
|
item &&
|
||||||
(item === CONTEXT_MENU_SEPARATOR ||
|
(item === CONTEXT_MENU_SEPARATOR ||
|
||||||
!item.contextItemPredicate ||
|
!item.predicate ||
|
||||||
item.contextItemPredicate(
|
item.predicate(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
actionManager.app.props,
|
actionManager.app.props,
|
||||||
|
|||||||
@@ -96,6 +96,10 @@
|
|||||||
width: 5rem;
|
width: 5rem;
|
||||||
height: 5rem;
|
height: 5rem;
|
||||||
margin: 0 0.2em;
|
margin: 0 0.2em;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
background-color: var(--button-color);
|
background-color: var(--button-color);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.FixedSideContainer {
|
.FixedSideContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -9,10 +11,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.FixedSideContainer_side_top {
|
.FixedSideContainer_side_top {
|
||||||
left: 1rem;
|
left: var(--editor-container-padding);
|
||||||
top: 1rem;
|
top: var(--editor-container-padding);
|
||||||
right: 1rem;
|
right: var(--editor-container-padding);
|
||||||
bottom: 1rem;
|
bottom: var(--editor-container-padding);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import "./ToolIcon.scss";
|
||||||
|
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { ToolButton } from "./ToolButton";
|
||||||
|
import { handIcon } from "./icons";
|
||||||
|
import { KEYS } from "../keys";
|
||||||
|
|
||||||
|
type LockIconProps = {
|
||||||
|
title?: string;
|
||||||
|
name?: string;
|
||||||
|
checked: boolean;
|
||||||
|
onChange?(): void;
|
||||||
|
isMobile?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HandButton = (props: LockIconProps) => {
|
||||||
|
return (
|
||||||
|
<ToolButton
|
||||||
|
className={clsx("Shape", { fillable: false })}
|
||||||
|
type="radio"
|
||||||
|
icon={handIcon}
|
||||||
|
name="editor-current-shape"
|
||||||
|
checked={props.checked}
|
||||||
|
title={`${props.title} — H`}
|
||||||
|
keyBindingLabel={!props.isMobile ? KEYS.H.toLocaleUpperCase() : undefined}
|
||||||
|
aria-label={`${props.title} — H`}
|
||||||
|
aria-keyshortcuts={KEYS.H}
|
||||||
|
data-testid={`toolbar-hand`}
|
||||||
|
onChange={() => props.onChange?.()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { isDarwin, isWindows, KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import "./HelpDialog.scss";
|
import "./HelpDialog.scss";
|
||||||
import { ExternalLinkIcon } from "./icons";
|
import { ExternalLinkIcon } from "./icons";
|
||||||
|
import { probablySupportsClipboardBlob } from "../clipboard";
|
||||||
|
import { isDarwin, isFirefox, isWindows } from "../constants";
|
||||||
|
|
||||||
const Header = () => (
|
const Header = () => (
|
||||||
<div className="HelpDialog__header">
|
<div className="HelpDialog__header">
|
||||||
@@ -67,6 +69,10 @@ function* intersperse(as: JSX.Element[][], delim: string | null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const upperCaseSingleChars = (str: string) => {
|
||||||
|
return str.replace(/\b[a-z]\b/, (c) => c.toUpperCase());
|
||||||
|
};
|
||||||
|
|
||||||
const Shortcut = ({
|
const Shortcut = ({
|
||||||
label,
|
label,
|
||||||
shortcuts,
|
shortcuts,
|
||||||
@@ -81,7 +87,9 @@ const Shortcut = ({
|
|||||||
? [...shortcut.slice(0, -2).split("+"), "+"]
|
? [...shortcut.slice(0, -2).split("+"), "+"]
|
||||||
: shortcut.split("+");
|
: shortcut.split("+");
|
||||||
|
|
||||||
return keys.map((key) => <ShortcutKey key={key}>{key}</ShortcutKey>);
|
return keys.map((key) => (
|
||||||
|
<ShortcutKey key={key}>{upperCaseSingleChars(key)}</ShortcutKey>
|
||||||
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -118,6 +126,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
className="HelpDialog__island--tools"
|
className="HelpDialog__island--tools"
|
||||||
caption={t("helpDialog.tools")}
|
caption={t("helpDialog.tools")}
|
||||||
>
|
>
|
||||||
|
<Shortcut label={t("toolBar.hand")} shortcuts={[KEYS.H]} />
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("toolBar.selection")}
|
label={t("toolBar.selection")}
|
||||||
shortcuts={[KEYS.V, KEYS["1"]]}
|
shortcuts={[KEYS.V, KEYS["1"]]}
|
||||||
@@ -304,10 +313,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("labels.pasteAsPlaintext")}
|
label={t("labels.pasteAsPlaintext")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+V")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+V")]}
|
||||||
/>
|
/>
|
||||||
<Shortcut
|
{/* firefox supports clipboard API under a flag, so we'll
|
||||||
label={t("labels.copyAsPng")}
|
show users what they can do in the error message */}
|
||||||
shortcuts={[getShortcutKey("Shift+Alt+C")]}
|
{(probablySupportsClipboardBlob || isFirefox) && (
|
||||||
/>
|
<Shortcut
|
||||||
|
label={t("labels.copyAsPng")}
|
||||||
|
shortcuts={[getShortcutKey("Shift+Alt+C")]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("labels.copyStyles")}
|
label={t("labels.copyStyles")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import Stack from "./Stack";
|
|||||||
import "./ExportDialog.scss";
|
import "./ExportDialog.scss";
|
||||||
import OpenColor from "open-color";
|
import OpenColor from "open-color";
|
||||||
import { CheckboxItem } from "./CheckboxItem";
|
import { CheckboxItem } from "./CheckboxItem";
|
||||||
import { DEFAULT_EXPORT_PADDING } from "../constants";
|
import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants";
|
||||||
import { nativeFileSystemSupported } from "../data/filesystem";
|
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
|
|
||||||
@@ -190,7 +190,9 @@ const ImageExportModal = ({
|
|||||||
>
|
>
|
||||||
SVG
|
SVG
|
||||||
</ExportButton>
|
</ExportButton>
|
||||||
{probablySupportsClipboardBlob && (
|
{/* firefox supports clipboard API under a flag,
|
||||||
|
so let's throw and tell people what they can do */}
|
||||||
|
{(probablySupportsClipboardBlob || isFirefox) && (
|
||||||
<ExportButton
|
<ExportButton
|
||||||
title={t("buttons.copyPngToClipboard")}
|
title={t("buttons.copyPngToClipboard")}
|
||||||
onClick={() => onExportToClipboard(exportedElements)}
|
onClick={() => onExportToClipboard(exportedElements)}
|
||||||
|
|||||||
+110
-116
@@ -8,16 +8,9 @@ import { NonDeletedExcalidrawElement } from "../element/types";
|
|||||||
import { Language, t } from "../i18n";
|
import { Language, t } from "../i18n";
|
||||||
import { calculateScrollCenter } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
import { ExportType } from "../scene/types";
|
import { ExportType } from "../scene/types";
|
||||||
import {
|
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
|
||||||
AppProps,
|
import { isShallowEqual, muteFSAbortError } from "../utils";
|
||||||
AppState,
|
|
||||||
ExcalidrawProps,
|
|
||||||
BinaryFiles,
|
|
||||||
UIChildrenComponents,
|
|
||||||
} from "../types";
|
|
||||||
import { muteFSAbortError, ReactChildrenToObject } from "../utils";
|
|
||||||
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
||||||
import CollabButton from "./CollabButton";
|
|
||||||
import { ErrorDialog } from "./ErrorDialog";
|
import { ErrorDialog } from "./ErrorDialog";
|
||||||
import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
|
import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
|
||||||
import { FixedSideContainer } from "./FixedSideContainer";
|
import { FixedSideContainer } from "./FixedSideContainer";
|
||||||
@@ -45,13 +38,14 @@ import { useDevice } from "../components/App";
|
|||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
import { actionToggleStats } from "../actions/actionToggleStats";
|
import { actionToggleStats } from "../actions/actionToggleStats";
|
||||||
import Footer from "./footer/Footer";
|
import Footer from "./footer/Footer";
|
||||||
import { WelcomeScreenMenuArrow, WelcomeScreenTopToolbarArrow } from "./icons";
|
|
||||||
import WelcomeScreen from "./WelcomeScreen";
|
|
||||||
import { hostSidebarCountersAtom } from "./Sidebar/Sidebar";
|
import { hostSidebarCountersAtom } from "./Sidebar/Sidebar";
|
||||||
import { jotaiScope } from "../jotai";
|
import { jotaiScope } from "../jotai";
|
||||||
import { useAtom } from "jotai";
|
import { Provider, useAtom } from "jotai";
|
||||||
import WelcomeScreenDecor from "./WelcomeScreenDecor";
|
import MainMenu from "./main-menu/MainMenu";
|
||||||
import MainMenu from "./mainMenu/MainMenu";
|
import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
|
||||||
|
import { HandButton } from "./HandButton";
|
||||||
|
import { isHandToolActive } from "../appState";
|
||||||
|
import { TunnelsContext, useInitializeTunnels } from "./context/tunnels";
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@@ -60,13 +54,12 @@ interface LayerUIProps {
|
|||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
onCollabButtonClick?: () => void;
|
|
||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
|
onHandToolToggle: () => void;
|
||||||
onPenModeToggle: () => void;
|
onPenModeToggle: () => void;
|
||||||
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||||
showExitZenModeBtn: boolean;
|
showExitZenModeBtn: boolean;
|
||||||
langCode: Language["code"];
|
langCode: Language["code"];
|
||||||
isCollaborating: boolean;
|
|
||||||
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
||||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||||
renderCustomSidebar?: ExcalidrawProps["renderSidebar"];
|
renderCustomSidebar?: ExcalidrawProps["renderSidebar"];
|
||||||
@@ -80,6 +73,32 @@ interface LayerUIProps {
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DefaultMainMenu: React.FC<{
|
||||||
|
UIOptions: AppProps["UIOptions"];
|
||||||
|
}> = ({ UIOptions }) => {
|
||||||
|
return (
|
||||||
|
<MainMenu __fallback>
|
||||||
|
<MainMenu.DefaultItems.LoadScene />
|
||||||
|
<MainMenu.DefaultItems.SaveToActiveFile />
|
||||||
|
{/* FIXME we should to test for this inside the item itself */}
|
||||||
|
{UIOptions.canvasActions.export && <MainMenu.DefaultItems.Export />}
|
||||||
|
{/* FIXME we should to test for this inside the item itself */}
|
||||||
|
{UIOptions.canvasActions.saveAsImage && (
|
||||||
|
<MainMenu.DefaultItems.SaveAsImage />
|
||||||
|
)}
|
||||||
|
<MainMenu.DefaultItems.Help />
|
||||||
|
<MainMenu.DefaultItems.ClearCanvas />
|
||||||
|
<MainMenu.Separator />
|
||||||
|
<MainMenu.Group title="Excalidraw links">
|
||||||
|
<MainMenu.DefaultItems.Socials />
|
||||||
|
</MainMenu.Group>
|
||||||
|
<MainMenu.Separator />
|
||||||
|
<MainMenu.DefaultItems.ToggleTheme />
|
||||||
|
<MainMenu.DefaultItems.ChangeCanvasBackground />
|
||||||
|
</MainMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const LayerUI = ({
|
const LayerUI = ({
|
||||||
actionManager,
|
actionManager,
|
||||||
appState,
|
appState,
|
||||||
@@ -87,12 +106,11 @@ const LayerUI = ({
|
|||||||
setAppState,
|
setAppState,
|
||||||
elements,
|
elements,
|
||||||
canvas,
|
canvas,
|
||||||
onCollabButtonClick,
|
|
||||||
onLockToggle,
|
onLockToggle,
|
||||||
|
onHandToolToggle,
|
||||||
onPenModeToggle,
|
onPenModeToggle,
|
||||||
onInsertElements,
|
onInsertElements,
|
||||||
showExitZenModeBtn,
|
showExitZenModeBtn,
|
||||||
isCollaborating,
|
|
||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
renderCustomStats,
|
renderCustomStats,
|
||||||
renderCustomSidebar,
|
renderCustomSidebar,
|
||||||
@@ -107,8 +125,7 @@ const LayerUI = ({
|
|||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
|
|
||||||
const childrenComponents =
|
const tunnels = useInitializeTunnels();
|
||||||
ReactChildrenToObject<UIChildrenComponents>(children);
|
|
||||||
|
|
||||||
const renderJSONExportDialog = () => {
|
const renderJSONExportDialog = () => {
|
||||||
if (!UIOptions.canvasActions.export) {
|
if (!UIOptions.canvasActions.export) {
|
||||||
@@ -177,46 +194,12 @@ const LayerUI = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderMenu = () => {
|
|
||||||
return (
|
|
||||||
childrenComponents.Menu || (
|
|
||||||
<MainMenu>
|
|
||||||
<MainMenu.DefaultItems.LoadScene />
|
|
||||||
<MainMenu.DefaultItems.SaveToActiveFile />
|
|
||||||
{UIOptions.canvasActions.export && <MainMenu.DefaultItems.Export />}
|
|
||||||
{UIOptions.canvasActions.saveAsImage && (
|
|
||||||
<MainMenu.DefaultItems.SaveAsImage />
|
|
||||||
)}
|
|
||||||
{onCollabButtonClick && (
|
|
||||||
<MainMenu.DefaultItems.LiveCollaboration
|
|
||||||
onSelect={onCollabButtonClick}
|
|
||||||
isCollaborating={isCollaborating}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<MainMenu.DefaultItems.Help />
|
|
||||||
<MainMenu.DefaultItems.ClearCanvas />
|
|
||||||
<MainMenu.Separator />
|
|
||||||
<MainMenu.Group title="Excalidraw links">
|
|
||||||
<MainMenu.DefaultItems.Socials />
|
|
||||||
</MainMenu.Group>
|
|
||||||
<MainMenu.Separator />
|
|
||||||
<MainMenu.DefaultItems.ToggleTheme />
|
|
||||||
<MainMenu.DefaultItems.ChangeCanvasBackground />
|
|
||||||
</MainMenu>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const renderCanvasActions = () => (
|
const renderCanvasActions = () => (
|
||||||
<div style={{ position: "relative" }}>
|
<div style={{ position: "relative" }}>
|
||||||
<WelcomeScreenDecor
|
{/* wrapping to Fragment stops React from occasionally complaining
|
||||||
shouldRender={renderWelcomeScreen && !appState.isLoading}
|
about identical Keys */}
|
||||||
>
|
<tunnels.mainMenuTunnel.Out />
|
||||||
<div className="virgil WelcomeScreen-decor WelcomeScreen-decor--menu-pointer">
|
{renderWelcomeScreen && <tunnels.welcomeScreenMenuHintTunnel.Out />}
|
||||||
{WelcomeScreenMenuArrow}
|
|
||||||
<div>{t("welcomeScreen.menuHints")}</div>
|
|
||||||
</div>
|
|
||||||
</WelcomeScreenDecor>
|
|
||||||
{renderMenu()}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -253,9 +236,6 @@ const LayerUI = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FixedSideContainer side="top">
|
<FixedSideContainer side="top">
|
||||||
{renderWelcomeScreen && !appState.isLoading && (
|
|
||||||
<WelcomeScreen appState={appState} actionManager={actionManager} />
|
|
||||||
)}
|
|
||||||
<div className="App-menu App-menu_top">
|
<div className="App-menu App-menu_top">
|
||||||
<Stack.Col
|
<Stack.Col
|
||||||
gap={6}
|
gap={6}
|
||||||
@@ -270,17 +250,9 @@ const LayerUI = ({
|
|||||||
<Section heading="shapes" className="shapes-section">
|
<Section heading="shapes" className="shapes-section">
|
||||||
{(heading: React.ReactNode) => (
|
{(heading: React.ReactNode) => (
|
||||||
<div style={{ position: "relative" }}>
|
<div style={{ position: "relative" }}>
|
||||||
<WelcomeScreenDecor
|
{renderWelcomeScreen && (
|
||||||
shouldRender={renderWelcomeScreen && !appState.isLoading}
|
<tunnels.welcomeScreenToolbarHintTunnel.Out />
|
||||||
>
|
)}
|
||||||
<div className="virgil WelcomeScreen-decor WelcomeScreen-decor--top-toolbar-pointer">
|
|
||||||
<div className="WelcomeScreen-decor--top-toolbar-pointer__label">
|
|
||||||
{t("welcomeScreen.toolbarHints")}
|
|
||||||
</div>
|
|
||||||
{WelcomeScreenTopToolbarArrow}
|
|
||||||
</div>
|
|
||||||
</WelcomeScreenDecor>
|
|
||||||
|
|
||||||
<Stack.Col gap={4} align="start">
|
<Stack.Col gap={4} align="start">
|
||||||
<Stack.Row
|
<Stack.Row
|
||||||
gap={1}
|
gap={1}
|
||||||
@@ -310,13 +282,20 @@ const LayerUI = ({
|
|||||||
penDetected={appState.penDetected}
|
penDetected={appState.penDetected}
|
||||||
/>
|
/>
|
||||||
<LockButton
|
<LockButton
|
||||||
zenModeEnabled={appState.zenModeEnabled}
|
|
||||||
checked={appState.activeTool.locked}
|
checked={appState.activeTool.locked}
|
||||||
onChange={() => onLockToggle()}
|
onChange={onLockToggle}
|
||||||
title={t("toolBar.lock")}
|
title={t("toolBar.lock")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="App-toolbar__divider"></div>
|
<div className="App-toolbar__divider"></div>
|
||||||
|
|
||||||
|
<HandButton
|
||||||
|
checked={isHandToolActive(appState)}
|
||||||
|
onChange={() => onHandToolToggle()}
|
||||||
|
title={t("toolBar.hand")}
|
||||||
|
isMobile
|
||||||
|
/>
|
||||||
|
|
||||||
<ShapesSwitcher
|
<ShapesSwitcher
|
||||||
appState={appState}
|
appState={appState}
|
||||||
canvas={canvas}
|
canvas={canvas}
|
||||||
@@ -328,9 +307,6 @@ const LayerUI = ({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* {actionManager.renderAction("eraser", {
|
|
||||||
// size: "small",
|
|
||||||
})} */}
|
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
@@ -348,14 +324,6 @@ const LayerUI = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<UserList collaborators={appState.collaborators} />
|
<UserList collaborators={appState.collaborators} />
|
||||||
{onCollabButtonClick && (
|
|
||||||
<CollabButton
|
|
||||||
isInHamburgerMenu={false}
|
|
||||||
isCollaborating={isCollaborating}
|
|
||||||
collaboratorCount={appState.collaborators.size}
|
|
||||||
onClick={onCollabButtonClick}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{renderTopRightUI?.(device.isMobile, appState)}
|
{renderTopRightUI?.(device.isMobile, appState)}
|
||||||
{!appState.viewModeEnabled && (
|
{!appState.viewModeEnabled && (
|
||||||
<LibraryButton appState={appState} setAppState={setAppState} />
|
<LibraryButton appState={appState} setAppState={setAppState} />
|
||||||
@@ -383,8 +351,18 @@ const LayerUI = ({
|
|||||||
|
|
||||||
const [hostSidebarCounters] = useAtom(hostSidebarCountersAtom, jotaiScope);
|
const [hostSidebarCounters] = useAtom(hostSidebarCountersAtom, jotaiScope);
|
||||||
|
|
||||||
return (
|
const layerUIJSX = (
|
||||||
<>
|
<>
|
||||||
|
{/* ------------------------- tunneled UI ---------------------------- */}
|
||||||
|
{/* make sure we render host app components first so that we can detect
|
||||||
|
them first on initial render to optimize layout shift */}
|
||||||
|
{children}
|
||||||
|
{/* render component fallbacks. Can be rendered anywhere as they'll be
|
||||||
|
tunneled away. We only render tunneled components that actually
|
||||||
|
have defaults when host do not render anything. */}
|
||||||
|
<DefaultMainMenu UIOptions={UIOptions} />
|
||||||
|
{/* ------------------------------------------------------------------ */}
|
||||||
|
|
||||||
{appState.isLoading && <LoadingMessage delay={250} />}
|
{appState.isLoading && <LoadingMessage delay={250} />}
|
||||||
{appState.errorMessage && (
|
{appState.errorMessage && (
|
||||||
<ErrorDialog
|
<ErrorDialog
|
||||||
@@ -399,6 +377,7 @@ const LayerUI = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<ActiveConfirmDialog />
|
||||||
{renderImageExportDialog()}
|
{renderImageExportDialog()}
|
||||||
{renderJSONExportDialog()}
|
{renderJSONExportDialog()}
|
||||||
{appState.pasteDialog.shown && (
|
{appState.pasteDialog.shown && (
|
||||||
@@ -415,24 +394,21 @@ const LayerUI = ({
|
|||||||
)}
|
)}
|
||||||
{device.isMobile && (
|
{device.isMobile && (
|
||||||
<MobileMenu
|
<MobileMenu
|
||||||
renderWelcomeScreen={renderWelcomeScreen}
|
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
renderJSONExportDialog={renderJSONExportDialog}
|
renderJSONExportDialog={renderJSONExportDialog}
|
||||||
renderImageExportDialog={renderImageExportDialog}
|
renderImageExportDialog={renderImageExportDialog}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
onCollabButtonClick={onCollabButtonClick}
|
onLockToggle={onLockToggle}
|
||||||
onLockToggle={() => onLockToggle()}
|
onHandToolToggle={onHandToolToggle}
|
||||||
onPenModeToggle={onPenModeToggle}
|
onPenModeToggle={onPenModeToggle}
|
||||||
canvas={canvas}
|
canvas={canvas}
|
||||||
isCollaborating={isCollaborating}
|
|
||||||
onImageAction={onImageAction}
|
onImageAction={onImageAction}
|
||||||
renderTopRightUI={renderTopRightUI}
|
renderTopRightUI={renderTopRightUI}
|
||||||
renderCustomStats={renderCustomStats}
|
renderCustomStats={renderCustomStats}
|
||||||
renderSidebars={renderSidebars}
|
renderSidebars={renderSidebars}
|
||||||
device={device}
|
device={device}
|
||||||
renderMenu={renderMenu}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -455,15 +431,14 @@ const LayerUI = ({
|
|||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{renderWelcomeScreen && <tunnels.welcomeScreenCenterTunnel.Out />}
|
||||||
{renderFixedSideContainer()}
|
{renderFixedSideContainer()}
|
||||||
<Footer
|
<Footer
|
||||||
renderWelcomeScreen={renderWelcomeScreen}
|
|
||||||
appState={appState}
|
appState={appState}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
showExitZenModeBtn={showExitZenModeBtn}
|
showExitZenModeBtn={showExitZenModeBtn}
|
||||||
footerCenter={childrenComponents.FooterCenter}
|
renderWelcomeScreen={renderWelcomeScreen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{appState.showStats && (
|
{appState.showStats && (
|
||||||
<Stats
|
<Stats
|
||||||
appState={appState}
|
appState={appState}
|
||||||
@@ -493,30 +468,49 @@ const LayerUI = ({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const areEqual = (prev: LayerUIProps, next: LayerUIProps) => {
|
|
||||||
const getNecessaryObj = (appState: AppState): Partial<AppState> => {
|
|
||||||
const {
|
|
||||||
suggestedBindings,
|
|
||||||
startBoundElement: boundElement,
|
|
||||||
...ret
|
|
||||||
} = appState;
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
const prevAppState = getNecessaryObj(prev.appState);
|
|
||||||
const nextAppState = getNecessaryObj(next.appState);
|
|
||||||
|
|
||||||
const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
prev.renderTopRightUI === next.renderTopRightUI &&
|
<Provider scope={tunnels.jotaiScope}>
|
||||||
prev.renderCustomStats === next.renderCustomStats &&
|
<TunnelsContext.Provider value={tunnels}>
|
||||||
prev.renderCustomSidebar === next.renderCustomSidebar &&
|
{layerUIJSX}
|
||||||
prev.langCode === next.langCode &&
|
</TunnelsContext.Provider>
|
||||||
prev.elements === next.elements &&
|
</Provider>
|
||||||
prev.files === next.files &&
|
);
|
||||||
keys.every((key) => prevAppState[key] === nextAppState[key])
|
};
|
||||||
|
|
||||||
|
const stripIrrelevantAppStateProps = (
|
||||||
|
appState: AppState,
|
||||||
|
): Partial<AppState> => {
|
||||||
|
const { suggestedBindings, startBoundElement, cursorButton, ...ret } =
|
||||||
|
appState;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => {
|
||||||
|
// short-circuit early
|
||||||
|
if (prevProps.children !== nextProps.children) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
canvas: _prevCanvas,
|
||||||
|
// not stable, but shouldn't matter in our case
|
||||||
|
onInsertElements: _prevOnInsertElements,
|
||||||
|
appState: prevAppState,
|
||||||
|
...prev
|
||||||
|
} = prevProps;
|
||||||
|
const {
|
||||||
|
canvas: _nextCanvas,
|
||||||
|
onInsertElements: _nextOnInsertElements,
|
||||||
|
appState: nextAppState,
|
||||||
|
...next
|
||||||
|
} = nextProps;
|
||||||
|
|
||||||
|
return (
|
||||||
|
isShallowEqual(
|
||||||
|
stripIrrelevantAppStateProps(prevAppState),
|
||||||
|
stripIrrelevantAppStateProps(nextAppState),
|
||||||
|
) && isShallowEqual(prev, next)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -187,13 +187,14 @@ export const LibraryMenuHeader: React.FC<{
|
|||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
onClickOutside={() => setIsLibraryMenuOpen(false)}
|
onClickOutside={() => setIsLibraryMenuOpen(false)}
|
||||||
|
onSelect={() => setIsLibraryMenuOpen(false)}
|
||||||
className="library-menu"
|
className="library-menu"
|
||||||
>
|
>
|
||||||
{!itemsSelected && (
|
{!itemsSelected && (
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={onLibraryImport}
|
onSelect={onLibraryImport}
|
||||||
icon={LoadIcon}
|
icon={LoadIcon}
|
||||||
dataTestId="lib-dropdown--load"
|
data-testid="lib-dropdown--load"
|
||||||
>
|
>
|
||||||
{t("buttons.load")}
|
{t("buttons.load")}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
@@ -202,7 +203,7 @@ export const LibraryMenuHeader: React.FC<{
|
|||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={onLibraryExport}
|
onSelect={onLibraryExport}
|
||||||
icon={ExportIcon}
|
icon={ExportIcon}
|
||||||
dataTestId="lib-dropdown--export"
|
data-testid="lib-dropdown--export"
|
||||||
>
|
>
|
||||||
{t("buttons.export")}
|
{t("buttons.export")}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
@@ -219,7 +220,7 @@ export const LibraryMenuHeader: React.FC<{
|
|||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
icon={publishIcon}
|
icon={publishIcon}
|
||||||
onSelect={() => setShowPublishLibraryDialog(true)}
|
onSelect={() => setShowPublishLibraryDialog(true)}
|
||||||
dataTestId="lib-dropdown--remove"
|
data-testid="lib-dropdown--remove"
|
||||||
>
|
>
|
||||||
{t("buttons.publishLibrary")}
|
{t("buttons.publishLibrary")}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ type LockIconProps = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange?(): void;
|
onChange?(): void;
|
||||||
zenModeEnabled?: boolean;
|
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ import { LibraryButton } from "./LibraryButton";
|
|||||||
import { PenModeButton } from "./PenModeButton";
|
import { PenModeButton } from "./PenModeButton";
|
||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
import { actionToggleStats } from "../actions";
|
import { actionToggleStats } from "../actions";
|
||||||
import WelcomeScreen from "./WelcomeScreen";
|
import { HandButton } from "./HandButton";
|
||||||
|
import { isHandToolActive } from "../appState";
|
||||||
|
import { useTunnels } from "./context/tunnels";
|
||||||
|
|
||||||
type MobileMenuProps = {
|
type MobileMenuProps = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
@@ -26,11 +28,10 @@ type MobileMenuProps = {
|
|||||||
renderImageExportDialog: () => React.ReactNode;
|
renderImageExportDialog: () => React.ReactNode;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
onCollabButtonClick?: () => void;
|
|
||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
|
onHandToolToggle: () => void;
|
||||||
onPenModeToggle: () => void;
|
onPenModeToggle: () => void;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
isCollaborating: boolean;
|
|
||||||
|
|
||||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||||
renderTopRightUI?: (
|
renderTopRightUI?: (
|
||||||
@@ -40,8 +41,6 @@ type MobileMenuProps = {
|
|||||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||||
renderSidebars: () => JSX.Element | null;
|
renderSidebars: () => JSX.Element | null;
|
||||||
device: Device;
|
device: Device;
|
||||||
renderWelcomeScreen?: boolean;
|
|
||||||
renderMenu: () => React.ReactNode;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MobileMenu = ({
|
export const MobileMenu = ({
|
||||||
@@ -50,23 +49,20 @@ export const MobileMenu = ({
|
|||||||
actionManager,
|
actionManager,
|
||||||
setAppState,
|
setAppState,
|
||||||
onLockToggle,
|
onLockToggle,
|
||||||
|
onHandToolToggle,
|
||||||
onPenModeToggle,
|
onPenModeToggle,
|
||||||
canvas,
|
canvas,
|
||||||
isCollaborating,
|
|
||||||
onImageAction,
|
onImageAction,
|
||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
renderCustomStats,
|
renderCustomStats,
|
||||||
renderSidebars,
|
renderSidebars,
|
||||||
device,
|
device,
|
||||||
renderWelcomeScreen,
|
|
||||||
renderMenu,
|
|
||||||
}: MobileMenuProps) => {
|
}: MobileMenuProps) => {
|
||||||
|
const { welcomeScreenCenterTunnel, mainMenuTunnel } = useTunnels();
|
||||||
const renderToolbar = () => {
|
const renderToolbar = () => {
|
||||||
return (
|
return (
|
||||||
<FixedSideContainer side="top" className="App-top-bar">
|
<FixedSideContainer side="top" className="App-top-bar">
|
||||||
{renderWelcomeScreen && !appState.isLoading && (
|
<welcomeScreenCenterTunnel.Out />
|
||||||
<WelcomeScreen appState={appState} actionManager={actionManager} />
|
|
||||||
)}
|
|
||||||
<Section heading="shapes">
|
<Section heading="shapes">
|
||||||
{(heading: React.ReactNode) => (
|
{(heading: React.ReactNode) => (
|
||||||
<Stack.Col gap={4} align="center">
|
<Stack.Col gap={4} align="center">
|
||||||
@@ -74,20 +70,6 @@ export const MobileMenu = ({
|
|||||||
<Island padding={1} className="App-toolbar App-toolbar--mobile">
|
<Island padding={1} className="App-toolbar App-toolbar--mobile">
|
||||||
{heading}
|
{heading}
|
||||||
<Stack.Row gap={1}>
|
<Stack.Row gap={1}>
|
||||||
{/* <PenModeButton
|
|
||||||
checked={appState.penMode}
|
|
||||||
onChange={onPenModeToggle}
|
|
||||||
title={t("toolBar.penMode")}
|
|
||||||
isMobile
|
|
||||||
penDetected={appState.penDetected}
|
|
||||||
/>
|
|
||||||
<LockButton
|
|
||||||
checked={appState.activeTool.locked}
|
|
||||||
onChange={onLockToggle}
|
|
||||||
title={t("toolBar.lock")}
|
|
||||||
isMobile
|
|
||||||
/>
|
|
||||||
<div className="App-toolbar__divider"></div> */}
|
|
||||||
<ShapesSwitcher
|
<ShapesSwitcher
|
||||||
appState={appState}
|
appState={appState}
|
||||||
canvas={canvas}
|
canvas={canvas}
|
||||||
@@ -103,20 +85,6 @@ export const MobileMenu = ({
|
|||||||
</Island>
|
</Island>
|
||||||
{renderTopRightUI && renderTopRightUI(true, appState)}
|
{renderTopRightUI && renderTopRightUI(true, appState)}
|
||||||
<div className="mobile-misc-tools-container">
|
<div className="mobile-misc-tools-container">
|
||||||
<PenModeButton
|
|
||||||
checked={appState.penMode}
|
|
||||||
onChange={onPenModeToggle}
|
|
||||||
title={t("toolBar.penMode")}
|
|
||||||
isMobile
|
|
||||||
penDetected={appState.penDetected}
|
|
||||||
// penDetected={true}
|
|
||||||
/>
|
|
||||||
<LockButton
|
|
||||||
checked={appState.activeTool.locked}
|
|
||||||
onChange={onLockToggle}
|
|
||||||
title={t("toolBar.lock")}
|
|
||||||
isMobile
|
|
||||||
/>
|
|
||||||
{!appState.viewModeEnabled && (
|
{!appState.viewModeEnabled && (
|
||||||
<LibraryButton
|
<LibraryButton
|
||||||
appState={appState}
|
appState={appState}
|
||||||
@@ -124,6 +92,25 @@ export const MobileMenu = ({
|
|||||||
isMobile
|
isMobile
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<PenModeButton
|
||||||
|
checked={appState.penMode}
|
||||||
|
onChange={onPenModeToggle}
|
||||||
|
title={t("toolBar.penMode")}
|
||||||
|
isMobile
|
||||||
|
penDetected={appState.penDetected}
|
||||||
|
/>
|
||||||
|
<LockButton
|
||||||
|
checked={appState.activeTool.locked}
|
||||||
|
onChange={onLockToggle}
|
||||||
|
title={t("toolBar.lock")}
|
||||||
|
isMobile
|
||||||
|
/>
|
||||||
|
<HandButton
|
||||||
|
checked={isHandToolActive(appState)}
|
||||||
|
onChange={() => onHandToolToggle()}
|
||||||
|
title={t("toolBar.hand")}
|
||||||
|
isMobile
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
@@ -141,12 +128,16 @@ export const MobileMenu = ({
|
|||||||
|
|
||||||
const renderAppToolbar = () => {
|
const renderAppToolbar = () => {
|
||||||
if (appState.viewModeEnabled) {
|
if (appState.viewModeEnabled) {
|
||||||
return <div className="App-toolbar-content">{renderMenu()}</div>;
|
return (
|
||||||
|
<div className="App-toolbar-content">
|
||||||
|
<mainMenuTunnel.Out />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App-toolbar-content">
|
<div className="App-toolbar-content">
|
||||||
{renderMenu()}
|
<mainMenuTunnel.Out />
|
||||||
{actionManager.renderAction("toggleEditMenu")}
|
{actionManager.renderAction("toggleEditMenu")}
|
||||||
{actionManager.renderAction("undo")}
|
{actionManager.renderAction("undo")}
|
||||||
{actionManager.renderAction("redo")}
|
{actionManager.renderAction("redo")}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type ToolButtonBaseProps = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
size?: ToolButtonSize;
|
size?: ToolButtonSize;
|
||||||
keyBindingLabel?: string;
|
keyBindingLabel?: string | null;
|
||||||
showAriaLabel?: boolean;
|
showAriaLabel?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
import { actionLoadScene, actionShortcuts } from "../actions";
|
|
||||||
import { ActionManager } from "../actions/manager";
|
|
||||||
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
|
||||||
import { isExcalidrawPlusSignedUser } from "../constants";
|
|
||||||
import { t } from "../i18n";
|
|
||||||
import { AppState } from "../types";
|
|
||||||
import { ExcalLogo, HelpIcon, LoadIcon, PlusPromoIcon } from "./icons";
|
|
||||||
import "./WelcomeScreen.scss";
|
|
||||||
|
|
||||||
const WelcomeScreenItem = ({
|
|
||||||
label,
|
|
||||||
shortcut,
|
|
||||||
onClick,
|
|
||||||
icon,
|
|
||||||
link,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
shortcut: string | null;
|
|
||||||
onClick?: () => void;
|
|
||||||
icon: JSX.Element;
|
|
||||||
link?: string;
|
|
||||||
}) => {
|
|
||||||
if (link) {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
className="WelcomeScreen-item"
|
|
||||||
href={link}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<div className="WelcomeScreen-item__label">
|
|
||||||
{icon}
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button className="WelcomeScreen-item" type="button" onClick={onClick}>
|
|
||||||
<div className="WelcomeScreen-item__label">
|
|
||||||
{icon}
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
{shortcut && (
|
|
||||||
<div className="WelcomeScreen-item__shortcut">{shortcut}</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const WelcomeScreen = ({
|
|
||||||
appState,
|
|
||||||
actionManager,
|
|
||||||
}: {
|
|
||||||
appState: AppState;
|
|
||||||
actionManager: ActionManager;
|
|
||||||
}) => {
|
|
||||||
let subheadingJSX;
|
|
||||||
|
|
||||||
if (isExcalidrawPlusSignedUser) {
|
|
||||||
subheadingJSX = t("welcomeScreen.switchToPlusApp")
|
|
||||||
.split(/(Excalidraw\+)/)
|
|
||||||
.map((bit, idx) => {
|
|
||||||
if (bit === "Excalidraw+") {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
style={{ pointerEvents: "all" }}
|
|
||||||
href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
|
|
||||||
key={idx}
|
|
||||||
>
|
|
||||||
Excalidraw+
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return bit;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
subheadingJSX = t("welcomeScreen.data");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="WelcomeScreen-container">
|
|
||||||
<div className="WelcomeScreen-logo virgil WelcomeScreen-decor">
|
|
||||||
{ExcalLogo} Excalidraw
|
|
||||||
</div>
|
|
||||||
<div className="virgil WelcomeScreen-decor WelcomeScreen-decor--subheading">
|
|
||||||
{subheadingJSX}
|
|
||||||
</div>
|
|
||||||
<div className="WelcomeScreen-items">
|
|
||||||
{!appState.viewModeEnabled && (
|
|
||||||
<WelcomeScreenItem
|
|
||||||
// TODO barnabasmolnar/editor-redesign
|
|
||||||
// do we want the internationalized labels here that are currently
|
|
||||||
// in use elsewhere or new ones?
|
|
||||||
label={t("buttons.load")}
|
|
||||||
onClick={() => actionManager.executeAction(actionLoadScene)}
|
|
||||||
shortcut={getShortcutFromShortcutName("loadScene")}
|
|
||||||
icon={LoadIcon}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<WelcomeScreenItem
|
|
||||||
onClick={() => actionManager.executeAction(actionShortcuts)}
|
|
||||||
label={t("helpDialog.title")}
|
|
||||||
shortcut="?"
|
|
||||||
icon={HelpIcon}
|
|
||||||
/>
|
|
||||||
{!isExcalidrawPlusSignedUser && (
|
|
||||||
<WelcomeScreenItem
|
|
||||||
link="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
|
||||||
label="Try Excalidraw Plus!"
|
|
||||||
shortcut={null}
|
|
||||||
icon={PlusPromoIcon}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WelcomeScreen;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { ReactNode } from "react";
|
|
||||||
|
|
||||||
const WelcomeScreenDecor = ({
|
|
||||||
children,
|
|
||||||
shouldRender,
|
|
||||||
}: {
|
|
||||||
children: ReactNode;
|
|
||||||
shouldRender: boolean;
|
|
||||||
}) => (shouldRender ? <>{children}</> : null);
|
|
||||||
|
|
||||||
export default WelcomeScreenDecor;
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import React from "react";
|
||||||
|
import tunnel from "@dwelle/tunnel-rat";
|
||||||
|
|
||||||
|
type Tunnel = ReturnType<typeof tunnel>;
|
||||||
|
|
||||||
|
type TunnelsContextValue = {
|
||||||
|
mainMenuTunnel: Tunnel;
|
||||||
|
welcomeScreenMenuHintTunnel: Tunnel;
|
||||||
|
welcomeScreenToolbarHintTunnel: Tunnel;
|
||||||
|
welcomeScreenHelpHintTunnel: Tunnel;
|
||||||
|
welcomeScreenCenterTunnel: Tunnel;
|
||||||
|
footerCenterTunnel: Tunnel;
|
||||||
|
jotaiScope: symbol;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TunnelsContext = React.createContext<TunnelsContextValue>(null!);
|
||||||
|
|
||||||
|
export const useTunnels = () => React.useContext(TunnelsContext);
|
||||||
|
|
||||||
|
export const useInitializeTunnels = () => {
|
||||||
|
return React.useMemo((): TunnelsContextValue => {
|
||||||
|
return {
|
||||||
|
mainMenuTunnel: tunnel(),
|
||||||
|
welcomeScreenMenuHintTunnel: tunnel(),
|
||||||
|
welcomeScreenToolbarHintTunnel: tunnel(),
|
||||||
|
welcomeScreenHelpHintTunnel: tunnel(),
|
||||||
|
welcomeScreenCenterTunnel: tunnel(),
|
||||||
|
footerCenterTunnel: tunnel(),
|
||||||
|
jotaiScope: Symbol(),
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--button-hover);
|
background-color: var(--button-hover-bg);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,23 @@ import { Island } from "../Island";
|
|||||||
import { useDevice } from "../App";
|
import { useDevice } from "../App";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import Stack from "../Stack";
|
import Stack from "../Stack";
|
||||||
|
import React from "react";
|
||||||
|
import { DropdownMenuContentPropsContext } from "./common";
|
||||||
|
|
||||||
const MenuContent = ({
|
const MenuContent = ({
|
||||||
children,
|
children,
|
||||||
onClickOutside,
|
onClickOutside,
|
||||||
className = "",
|
className = "",
|
||||||
|
onSelect,
|
||||||
style,
|
style,
|
||||||
}: {
|
}: {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
onClickOutside?: () => void;
|
onClickOutside?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/**
|
||||||
|
* Called when any menu item is selected (clicked on).
|
||||||
|
*/
|
||||||
|
onSelect?: (event: Event) => void;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
}) => {
|
}) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
@@ -24,28 +31,32 @@ const MenuContent = ({
|
|||||||
const classNames = clsx(`dropdown-menu ${className}`, {
|
const classNames = clsx(`dropdown-menu ${className}`, {
|
||||||
"dropdown-menu--mobile": device.isMobile,
|
"dropdown-menu--mobile": device.isMobile,
|
||||||
}).trim();
|
}).trim();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<DropdownMenuContentPropsContext.Provider value={{ onSelect }}>
|
||||||
ref={menuRef}
|
<div
|
||||||
className={classNames}
|
ref={menuRef}
|
||||||
style={style}
|
className={classNames}
|
||||||
data-testid="dropdown-menu"
|
style={style}
|
||||||
>
|
data-testid="dropdown-menu"
|
||||||
{/* the zIndex ensures this menu has higher stacking order,
|
>
|
||||||
|
{/* the zIndex ensures this menu has higher stacking order,
|
||||||
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
||||||
{device.isMobile ? (
|
{device.isMobile ? (
|
||||||
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
|
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
|
||||||
) : (
|
) : (
|
||||||
<Island
|
<Island
|
||||||
className="dropdown-menu-container"
|
className="dropdown-menu-container"
|
||||||
padding={2}
|
padding={2}
|
||||||
style={{ zIndex: 1 }}
|
style={{ zIndex: 1 }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Island>
|
</Island>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</DropdownMenuContentPropsContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default MenuContent;
|
|
||||||
MenuContent.displayName = "DropdownMenuContent";
|
MenuContent.displayName = "DropdownMenuContent";
|
||||||
|
|
||||||
|
export default MenuContent;
|
||||||
|
|||||||
@@ -1,38 +1,33 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import {
|
||||||
|
getDropdownMenuItemClassName,
|
||||||
|
useHandleDropdownMenuItemClick,
|
||||||
|
} from "./common";
|
||||||
import MenuItemContent from "./DropdownMenuItemContent";
|
import MenuItemContent from "./DropdownMenuItemContent";
|
||||||
|
|
||||||
export const getDrodownMenuItemClassName = (className = "") => {
|
|
||||||
return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim();
|
|
||||||
};
|
|
||||||
|
|
||||||
const DropdownMenuItem = ({
|
const DropdownMenuItem = ({
|
||||||
icon,
|
icon,
|
||||||
onSelect,
|
onSelect,
|
||||||
children,
|
children,
|
||||||
dataTestId,
|
|
||||||
shortcut,
|
shortcut,
|
||||||
className,
|
className,
|
||||||
style,
|
...rest
|
||||||
ariaLabel,
|
|
||||||
}: {
|
}: {
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
onSelect: () => void;
|
onSelect: (event: Event) => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
dataTestId?: string;
|
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: React.CSSProperties;
|
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
|
||||||
ariaLabel?: string;
|
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
aria-label={ariaLabel}
|
{...rest}
|
||||||
onClick={onSelect}
|
onClick={handleClick}
|
||||||
data-testid={dataTestId}
|
|
||||||
title={ariaLabel}
|
|
||||||
type="button"
|
type="button"
|
||||||
className={getDrodownMenuItemClassName(className)}
|
className={getDropdownMenuItemClassName(className)}
|
||||||
style={style}
|
title={rest.title ?? rest["aria-label"]}
|
||||||
>
|
>
|
||||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
const DropdownMenuItemCustom = ({
|
const DropdownMenuItemCustom = ({
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
style,
|
...rest
|
||||||
dataTestId,
|
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: React.CSSProperties;
|
} & React.HTMLAttributes<HTMLDivElement>) => {
|
||||||
dataTestId?: string;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
{...rest}
|
||||||
className={`dropdown-menu-item-base dropdown-menu-item-custom ${className}`.trim()}
|
className={`dropdown-menu-item-base dropdown-menu-item-custom ${className}`.trim()}
|
||||||
style={style}
|
|
||||||
data-testid={dataTestId}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,35 +1,37 @@
|
|||||||
import MenuItemContent from "./DropdownMenuItemContent";
|
import MenuItemContent from "./DropdownMenuItemContent";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { getDrodownMenuItemClassName } from "./DropdownMenuItem";
|
import {
|
||||||
|
getDropdownMenuItemClassName,
|
||||||
|
useHandleDropdownMenuItemClick,
|
||||||
|
} from "./common";
|
||||||
|
|
||||||
const DropdownMenuItemLink = ({
|
const DropdownMenuItemLink = ({
|
||||||
icon,
|
icon,
|
||||||
dataTestId,
|
|
||||||
shortcut,
|
shortcut,
|
||||||
href,
|
href,
|
||||||
children,
|
children,
|
||||||
|
onSelect,
|
||||||
className = "",
|
className = "",
|
||||||
style,
|
...rest
|
||||||
ariaLabel,
|
|
||||||
}: {
|
}: {
|
||||||
|
href: string;
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
dataTestId?: string;
|
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
href: string;
|
onSelect?: (event: Event) => void;
|
||||||
style?: React.CSSProperties;
|
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||||
ariaLabel?: string;
|
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
|
{...rest}
|
||||||
href={href}
|
href={href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className={getDrodownMenuItemClassName(className)}
|
className={getDropdownMenuItemClassName(className)}
|
||||||
style={style}
|
title={rest.title ?? rest["aria-label"]}
|
||||||
data-testid={dataTestId}
|
onClick={handleClick}
|
||||||
title={ariaLabel}
|
|
||||||
aria-label={ariaLabel}
|
|
||||||
>
|
>
|
||||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import React, { useContext } from "react";
|
||||||
|
import { EVENT } from "../../constants";
|
||||||
|
import { composeEventHandlers } from "../../utils";
|
||||||
|
|
||||||
|
export const DropdownMenuContentPropsContext = React.createContext<{
|
||||||
|
onSelect?: (event: Event) => void;
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
export const getDropdownMenuItemClassName = (className = "") => {
|
||||||
|
return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useHandleDropdownMenuItemClick = (
|
||||||
|
origOnClick:
|
||||||
|
| React.MouseEventHandler<HTMLAnchorElement | HTMLButtonElement>
|
||||||
|
| undefined,
|
||||||
|
onSelect: ((event: Event) => void) | undefined,
|
||||||
|
) => {
|
||||||
|
const DropdownMenuContentProps = useContext(DropdownMenuContentPropsContext);
|
||||||
|
|
||||||
|
return composeEventHandlers(origOnClick, (event) => {
|
||||||
|
const itemSelectEvent = new CustomEvent(EVENT.MENU_ITEM_SELECT, {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
onSelect?.(itemSelectEvent);
|
||||||
|
if (!itemSelectEvent.defaultPrevented) {
|
||||||
|
DropdownMenuContentProps.onSelect?.(itemSelectEvent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { actionShortcuts } from "../../actions";
|
||||||
import { ActionManager } from "../../actions/manager";
|
import { ActionManager } from "../../actions/manager";
|
||||||
import { t } from "../../i18n";
|
import { AppState } from "../../types";
|
||||||
import { AppState, UIChildrenComponents } from "../../types";
|
|
||||||
import {
|
import {
|
||||||
ExitZenModeAction,
|
ExitZenModeAction,
|
||||||
FinalizeAction,
|
FinalizeAction,
|
||||||
@@ -9,24 +9,24 @@ import {
|
|||||||
ZoomActions,
|
ZoomActions,
|
||||||
} from "../Actions";
|
} from "../Actions";
|
||||||
import { useDevice } from "../App";
|
import { useDevice } from "../App";
|
||||||
import { WelcomeScreenHelpArrow } from "../icons";
|
import { useTunnels } from "../context/tunnels";
|
||||||
|
import { HelpButton } from "../HelpButton";
|
||||||
import { Section } from "../Section";
|
import { Section } from "../Section";
|
||||||
import Stack from "../Stack";
|
import Stack from "../Stack";
|
||||||
import WelcomeScreenDecor from "../WelcomeScreenDecor";
|
|
||||||
|
|
||||||
const Footer = ({
|
const Footer = ({
|
||||||
appState,
|
appState,
|
||||||
actionManager,
|
actionManager,
|
||||||
showExitZenModeBtn,
|
showExitZenModeBtn,
|
||||||
renderWelcomeScreen,
|
renderWelcomeScreen,
|
||||||
footerCenter,
|
|
||||||
}: {
|
}: {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
showExitZenModeBtn: boolean;
|
showExitZenModeBtn: boolean;
|
||||||
renderWelcomeScreen: boolean;
|
renderWelcomeScreen: boolean;
|
||||||
footerCenter: UIChildrenComponents["FooterCenter"];
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const { footerCenterTunnel, welcomeScreenHelpHintTunnel } = useTunnels();
|
||||||
|
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const showFinalize =
|
const showFinalize =
|
||||||
!appState.viewModeEnabled && appState.multiElement && device.isTouchScreen;
|
!appState.viewModeEnabled && appState.multiElement && device.isTouchScreen;
|
||||||
@@ -70,23 +70,17 @@ const Footer = ({
|
|||||||
</Section>
|
</Section>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
</div>
|
</div>
|
||||||
{footerCenter}
|
<footerCenterTunnel.Out />
|
||||||
<div
|
<div
|
||||||
className={clsx("layer-ui__wrapper__footer-right zen-mode-transition", {
|
className={clsx("layer-ui__wrapper__footer-right zen-mode-transition", {
|
||||||
"transition-right disable-pointerEvents": appState.zenModeEnabled,
|
"transition-right disable-pointerEvents": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div style={{ position: "relative" }}>
|
<div style={{ position: "relative" }}>
|
||||||
<WelcomeScreenDecor
|
{renderWelcomeScreen && <welcomeScreenHelpHintTunnel.Out />}
|
||||||
shouldRender={renderWelcomeScreen && !appState.isLoading}
|
<HelpButton
|
||||||
>
|
onClick={() => actionManager.executeAction(actionShortcuts)}
|
||||||
<div className="virgil WelcomeScreen-decor WelcomeScreen-decor--help-pointer">
|
/>
|
||||||
<div>{t("welcomeScreen.helpHints")}</div>
|
|
||||||
{WelcomeScreenHelpArrow}
|
|
||||||
</div>
|
|
||||||
</WelcomeScreenDecor>
|
|
||||||
|
|
||||||
{actionManager.renderAction("toggleShortcuts")}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ExitZenModeAction
|
<ExitZenModeAction
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useExcalidrawAppState } from "../App";
|
import { useExcalidrawAppState } from "../App";
|
||||||
|
import { useTunnels } from "../context/tunnels";
|
||||||
import "./FooterCenter.scss";
|
import "./FooterCenter.scss";
|
||||||
|
|
||||||
const FooterCenter = ({ children }: { children?: React.ReactNode }) => {
|
const FooterCenter = ({ children }: { children?: React.ReactNode }) => {
|
||||||
|
const { footerCenterTunnel } = useTunnels();
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
return (
|
return (
|
||||||
<div
|
<footerCenterTunnel.In>
|
||||||
className={clsx("footer-center zen-mode-transition", {
|
<div
|
||||||
"layer-ui__wrapper__footer-left--transition-bottom":
|
className={clsx("footer-center zen-mode-transition", {
|
||||||
appState.zenModeEnabled,
|
"layer-ui__wrapper__footer-left--transition-bottom":
|
||||||
})}
|
appState.zenModeEnabled,
|
||||||
>
|
})}
|
||||||
{children}
|
>
|
||||||
</div>
|
{children}
|
||||||
|
</div>
|
||||||
|
</footerCenterTunnel.In>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { atom, useAtom } from "jotai";
|
||||||
|
import React, { useLayoutEffect } from "react";
|
||||||
|
import { useTunnels } from "../context/tunnels";
|
||||||
|
|
||||||
|
export const withInternalFallback = <P,>(
|
||||||
|
componentName: string,
|
||||||
|
Component: React.FC<P>,
|
||||||
|
) => {
|
||||||
|
const counterAtom = atom(0);
|
||||||
|
// flag set on initial render to tell the fallback component to skip the
|
||||||
|
// render until mount counter are initialized. This is because the counter
|
||||||
|
// is initialized in an effect, and thus we could end rendering both
|
||||||
|
// components at the same time until counter is initialized.
|
||||||
|
let preferHost = false;
|
||||||
|
|
||||||
|
const WrapperComponent: React.FC<
|
||||||
|
P & {
|
||||||
|
__fallback?: boolean;
|
||||||
|
}
|
||||||
|
> = (props) => {
|
||||||
|
const { jotaiScope } = useTunnels();
|
||||||
|
const [counter, setCounter] = useAtom(counterAtom, jotaiScope);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setCounter((counter) => counter + 1);
|
||||||
|
return () => {
|
||||||
|
setCounter((counter) => counter - 1);
|
||||||
|
};
|
||||||
|
}, [setCounter]);
|
||||||
|
|
||||||
|
if (!props.__fallback) {
|
||||||
|
preferHost = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure we don't render fallback and host components at the same time
|
||||||
|
if (
|
||||||
|
// either before the counters are initialized
|
||||||
|
(!counter && props.__fallback && preferHost) ||
|
||||||
|
// or after the counters are initialized, and both are rendered
|
||||||
|
// (this is the default when host renders as well)
|
||||||
|
(counter > 1 && props.__fallback)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Component {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
WrapperComponent.displayName = componentName;
|
||||||
|
|
||||||
|
return WrapperComponent;
|
||||||
|
};
|
||||||
@@ -883,7 +883,7 @@ export const CenterHorizontallyIcon = createIcon(
|
|||||||
modifiedTablerIconProps,
|
modifiedTablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UsersIcon = createIcon(
|
export const usersIcon = createIcon(
|
||||||
<g strokeWidth="1.5">
|
<g strokeWidth="1.5">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
<circle cx="9" cy="7" r="4"></circle>
|
<circle cx="9" cy="7" r="4"></circle>
|
||||||
@@ -1532,3 +1532,14 @@ export const publishIcon = createIcon(
|
|||||||
export const eraser = createIcon(
|
export const eraser = createIcon(
|
||||||
<path d="M480 416C497.7 416 512 430.3 512 448C512 465.7 497.7 480 480 480H150.6C133.7 480 117.4 473.3 105.4 461.3L25.37 381.3C.3786 356.3 .3786 315.7 25.37 290.7L258.7 57.37C283.7 32.38 324.3 32.38 349.3 57.37L486.6 194.7C511.6 219.7 511.6 260.3 486.6 285.3L355.9 416H480zM265.4 416L332.7 348.7L195.3 211.3L70.63 336L150.6 416L265.4 416z" />,
|
<path d="M480 416C497.7 416 512 430.3 512 448C512 465.7 497.7 480 480 480H150.6C133.7 480 117.4 473.3 105.4 461.3L25.37 381.3C.3786 356.3 .3786 315.7 25.37 290.7L258.7 57.37C283.7 32.38 324.3 32.38 349.3 57.37L486.6 194.7C511.6 219.7 511.6 260.3 486.6 285.3L355.9 416H480zM265.4 416L332.7 348.7L195.3 211.3L70.63 336L150.6 416L265.4 416z" />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const handIcon = createIcon(
|
||||||
|
<g strokeWidth={1.25}>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M8 13v-7.5a1.5 1.5 0 0 1 3 0v6.5"></path>
|
||||||
|
<path d="M11 5.5v-2a1.5 1.5 0 1 1 3 0v8.5"></path>
|
||||||
|
<path d="M14 5.5a1.5 1.5 0 0 1 3 0v6.5"></path>
|
||||||
|
<path d="M17 7.5a1.5 1.5 0 0 1 3 0v8.5a6 6 0 0 1 -6 6h-2h.208a6 6 0 0 1 -5.012 -2.7a69.74 69.74 0 0 1 -.196 -.3c-.312 -.479 -1.407 -2.388 -3.286 -5.728a1.5 1.5 0 0 1 .536 -2.022a1.867 1.867 0 0 1 2.28 .28l1.47 1.47"></path>
|
||||||
|
</g>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
|||||||
+14
-21
@@ -1,30 +1,23 @@
|
|||||||
@import "../css/variables.module";
|
@import "../../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.collab-button {
|
.collab-button {
|
||||||
@include outlineButtonStyles;
|
--button-bg: var(--color-primary);
|
||||||
width: var(--lg-button-size);
|
--button-color: white;
|
||||||
height: var(--lg-button-size);
|
--button-border: var(--color-primary);
|
||||||
|
|
||||||
|
--button-width: var(--lg-button-size);
|
||||||
|
--button-height: var(--lg-button-size);
|
||||||
|
|
||||||
|
--button-hover-bg: var(--color-primary-darker);
|
||||||
|
--button-hover-border: var(--color-primary-darker);
|
||||||
|
|
||||||
|
--button-active-bg: var(--color-primary-darker);
|
||||||
|
|
||||||
svg {
|
|
||||||
width: var(--lg-icon-size);
|
|
||||||
height: var(--lg-icon-size);
|
|
||||||
}
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
color: white;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
&:hover {
|
// double .active to force specificity
|
||||||
background-color: var(--color-primary-darker);
|
&.active.active {
|
||||||
border-color: var(--color-primary-darker);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: var(--color-primary-darker);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: #0fb884;
|
background-color: #0fb884;
|
||||||
border-color: #0fb884;
|
border-color: #0fb884;
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { t } from "../../i18n";
|
||||||
|
import { usersIcon } from "../icons";
|
||||||
|
import { Button } from "../Button";
|
||||||
|
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { useExcalidrawAppState } from "../App";
|
||||||
|
|
||||||
|
import "./LiveCollaborationTrigger.scss";
|
||||||
|
|
||||||
|
const LiveCollaborationTrigger = ({
|
||||||
|
isCollaborating,
|
||||||
|
onSelect,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
isCollaborating: boolean;
|
||||||
|
onSelect: () => void;
|
||||||
|
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||||
|
const appState = useExcalidrawAppState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{...rest}
|
||||||
|
className={clsx("collab-button", { active: isCollaborating })}
|
||||||
|
type="button"
|
||||||
|
onSelect={onSelect}
|
||||||
|
style={{ position: "relative" }}
|
||||||
|
title={t("labels.liveCollaboration")}
|
||||||
|
>
|
||||||
|
{usersIcon}
|
||||||
|
{appState.collaborators.size > 0 && (
|
||||||
|
<div className="CollabButton-collaborators">
|
||||||
|
{appState.collaborators.size}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LiveCollaborationTrigger;
|
||||||
|
LiveCollaborationTrigger.displayName = "LiveCollaborationTrigger";
|
||||||
+121
-27
@@ -1,4 +1,3 @@
|
|||||||
import clsx from "clsx";
|
|
||||||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import {
|
import {
|
||||||
@@ -6,43 +5,91 @@ import {
|
|||||||
useExcalidrawSetAppState,
|
useExcalidrawSetAppState,
|
||||||
useExcalidrawActionManager,
|
useExcalidrawActionManager,
|
||||||
} from "../App";
|
} from "../App";
|
||||||
import { ExportIcon, ExportImageIcon, UsersIcon } from "../icons";
|
import {
|
||||||
|
ExportIcon,
|
||||||
|
ExportImageIcon,
|
||||||
|
HelpIcon,
|
||||||
|
LoadIcon,
|
||||||
|
MoonIcon,
|
||||||
|
save,
|
||||||
|
SunIcon,
|
||||||
|
TrashIcon,
|
||||||
|
usersIcon,
|
||||||
|
} from "../icons";
|
||||||
import { GithubIcon, DiscordIcon, TwitterIcon } from "../icons";
|
import { GithubIcon, DiscordIcon, TwitterIcon } from "../icons";
|
||||||
import DropdownMenuItem from "../dropdownMenu/DropdownMenuItem";
|
import DropdownMenuItem from "../dropdownMenu/DropdownMenuItem";
|
||||||
import DropdownMenuItemLink from "../dropdownMenu/DropdownMenuItemLink";
|
import DropdownMenuItemLink from "../dropdownMenu/DropdownMenuItemLink";
|
||||||
|
import {
|
||||||
|
actionClearCanvas,
|
||||||
|
actionLoadScene,
|
||||||
|
actionSaveToActiveFile,
|
||||||
|
actionShortcuts,
|
||||||
|
actionToggleTheme,
|
||||||
|
} from "../../actions";
|
||||||
|
|
||||||
|
import "./DefaultItems.scss";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { useSetAtom } from "jotai";
|
||||||
|
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
||||||
|
|
||||||
export const LoadScene = () => {
|
export const LoadScene = () => {
|
||||||
|
// FIXME Hack until we tie "t" to lang state
|
||||||
|
// eslint-disable-next-line
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
if (appState.viewModeEnabled) {
|
|
||||||
|
if (!actionManager.isActionEnabled(actionLoadScene)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return actionManager.renderAction("loadScene");
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
icon={LoadIcon}
|
||||||
|
onSelect={() => actionManager.executeAction(actionLoadScene)}
|
||||||
|
data-testid="load-button"
|
||||||
|
shortcut={getShortcutFromShortcutName("loadScene")}
|
||||||
|
aria-label={t("buttons.load")}
|
||||||
|
>
|
||||||
|
{t("buttons.load")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
LoadScene.displayName = "LoadScene";
|
LoadScene.displayName = "LoadScene";
|
||||||
|
|
||||||
export const SaveToActiveFile = () => {
|
export const SaveToActiveFile = () => {
|
||||||
|
// FIXME Hack until we tie "t" to lang state
|
||||||
|
// eslint-disable-next-line
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
if (!appState.fileHandle) {
|
|
||||||
|
if (!actionManager.isActionEnabled(actionSaveToActiveFile)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return actionManager.renderAction("saveToActiveFile");
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
shortcut={getShortcutFromShortcutName("saveScene")}
|
||||||
|
data-testid="save-button"
|
||||||
|
onSelect={() => actionManager.executeAction(actionSaveToActiveFile)}
|
||||||
|
icon={save}
|
||||||
|
aria-label={`${t("buttons.save")}`}
|
||||||
|
>{`${t("buttons.save")}`}</DropdownMenuItem>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
SaveToActiveFile.displayName = "SaveToActiveFile";
|
SaveToActiveFile.displayName = "SaveToActiveFile";
|
||||||
|
|
||||||
export const SaveAsImage = () => {
|
export const SaveAsImage = () => {
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
// Hack until we tie "t" to lang state
|
// FIXME Hack until we tie "t" to lang state
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
icon={ExportImageIcon}
|
icon={ExportImageIcon}
|
||||||
dataTestId="image-export-button"
|
data-testid="image-export-button"
|
||||||
onSelect={() => setAppState({ openDialog: "imageExport" })}
|
onSelect={() => setAppState({ openDialog: "imageExport" })}
|
||||||
shortcut={getShortcutFromShortcutName("imageExport")}
|
shortcut={getShortcutFromShortcutName("imageExport")}
|
||||||
ariaLabel={t("buttons.exportImage")}
|
aria-label={t("buttons.exportImage")}
|
||||||
>
|
>
|
||||||
{t("buttons.exportImage")}
|
{t("buttons.exportImage")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -51,32 +98,79 @@ export const SaveAsImage = () => {
|
|||||||
SaveAsImage.displayName = "SaveAsImage";
|
SaveAsImage.displayName = "SaveAsImage";
|
||||||
|
|
||||||
export const Help = () => {
|
export const Help = () => {
|
||||||
// Hack until we tie "t" to lang state
|
// FIXME Hack until we tie "t" to lang state
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
|
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
return actionManager.renderAction("toggleShortcuts", undefined, true);
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
data-testid="help-menu-item"
|
||||||
|
icon={HelpIcon}
|
||||||
|
onSelect={() => actionManager.executeAction(actionShortcuts)}
|
||||||
|
shortcut="?"
|
||||||
|
aria-label={t("helpDialog.title")}
|
||||||
|
>
|
||||||
|
{t("helpDialog.title")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
Help.displayName = "Help";
|
Help.displayName = "Help";
|
||||||
|
|
||||||
export const ClearCanvas = () => {
|
export const ClearCanvas = () => {
|
||||||
|
// FIXME Hack until we tie "t" to lang state
|
||||||
|
// eslint-disable-next-line
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
|
const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom);
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
if (appState.viewModeEnabled) {
|
if (!actionManager.isActionEnabled(actionClearCanvas)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return actionManager.renderAction("clearCanvas");
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
icon={TrashIcon}
|
||||||
|
onSelect={() => setActiveConfirmDialog("clearCanvas")}
|
||||||
|
data-testid="clear-canvas-button"
|
||||||
|
aria-label={t("buttons.clearReset")}
|
||||||
|
>
|
||||||
|
{t("buttons.clearReset")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
ClearCanvas.displayName = "ClearCanvas";
|
ClearCanvas.displayName = "ClearCanvas";
|
||||||
|
|
||||||
export const ToggleTheme = () => {
|
export const ToggleTheme = () => {
|
||||||
// Hack until we tie "t" to lang state
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
return actionManager.renderAction("toggleTheme");
|
|
||||||
|
if (!actionManager.isActionEnabled(actionToggleTheme)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={(event) => {
|
||||||
|
// do not close the menu when changing theme
|
||||||
|
event.preventDefault();
|
||||||
|
return actionManager.executeAction(actionToggleTheme);
|
||||||
|
}}
|
||||||
|
icon={appState.theme === "dark" ? SunIcon : MoonIcon}
|
||||||
|
data-testid="toggle-dark-mode"
|
||||||
|
shortcut={getShortcutFromShortcutName("toggleTheme")}
|
||||||
|
aria-label={
|
||||||
|
appState.theme === "dark"
|
||||||
|
? t("buttons.lightMode")
|
||||||
|
: t("buttons.darkMode")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{appState.theme === "dark"
|
||||||
|
? t("buttons.lightMode")
|
||||||
|
: t("buttons.darkMode")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
ToggleTheme.displayName = "ToggleTheme";
|
ToggleTheme.displayName = "ToggleTheme";
|
||||||
|
|
||||||
@@ -101,7 +195,7 @@ export const ChangeCanvasBackground = () => {
|
|||||||
ChangeCanvasBackground.displayName = "ChangeCanvasBackground";
|
ChangeCanvasBackground.displayName = "ChangeCanvasBackground";
|
||||||
|
|
||||||
export const Export = () => {
|
export const Export = () => {
|
||||||
// Hack until we tie "t" to lang state
|
// FIXME Hack until we tie "t" to lang state
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
@@ -111,8 +205,8 @@ export const Export = () => {
|
|||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setAppState({ openDialog: "jsonExport" });
|
setAppState({ openDialog: "jsonExport" });
|
||||||
}}
|
}}
|
||||||
dataTestId="json-export-button"
|
data-testid="json-export-button"
|
||||||
ariaLabel={t("buttons.export")}
|
aria-label={t("buttons.export")}
|
||||||
>
|
>
|
||||||
{t("buttons.export")}
|
{t("buttons.export")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -125,21 +219,21 @@ export const Socials = () => (
|
|||||||
<DropdownMenuItemLink
|
<DropdownMenuItemLink
|
||||||
icon={GithubIcon}
|
icon={GithubIcon}
|
||||||
href="https://github.com/excalidraw/excalidraw"
|
href="https://github.com/excalidraw/excalidraw"
|
||||||
ariaLabel="GitHub"
|
aria-label="GitHub"
|
||||||
>
|
>
|
||||||
GitHub
|
GitHub
|
||||||
</DropdownMenuItemLink>
|
</DropdownMenuItemLink>
|
||||||
<DropdownMenuItemLink
|
<DropdownMenuItemLink
|
||||||
icon={DiscordIcon}
|
icon={DiscordIcon}
|
||||||
href="https://discord.gg/UexuTaE"
|
href="https://discord.gg/UexuTaE"
|
||||||
ariaLabel="Discord"
|
aria-label="Discord"
|
||||||
>
|
>
|
||||||
Discord
|
Discord
|
||||||
</DropdownMenuItemLink>
|
</DropdownMenuItemLink>
|
||||||
<DropdownMenuItemLink
|
<DropdownMenuItemLink
|
||||||
icon={TwitterIcon}
|
icon={TwitterIcon}
|
||||||
href="https://twitter.com/excalidraw"
|
href="https://twitter.com/excalidraw"
|
||||||
ariaLabel="Twitter"
|
aria-label="Twitter"
|
||||||
>
|
>
|
||||||
Twitter
|
Twitter
|
||||||
</DropdownMenuItemLink>
|
</DropdownMenuItemLink>
|
||||||
@@ -147,20 +241,20 @@ export const Socials = () => (
|
|||||||
);
|
);
|
||||||
Socials.displayName = "Socials";
|
Socials.displayName = "Socials";
|
||||||
|
|
||||||
export const LiveCollaboration = ({
|
export const LiveCollaborationTrigger = ({
|
||||||
onSelect,
|
onSelect,
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
}: {
|
}: {
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
// Hack until we tie "t" to lang state
|
// FIXME Hack until we tie "t" to lang state
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
dataTestId="collab-button"
|
data-testid="collab-button"
|
||||||
icon={UsersIcon}
|
icon={usersIcon}
|
||||||
className={clsx({
|
className={clsx({
|
||||||
"active-collab": isCollaborating,
|
"active-collab": isCollaborating,
|
||||||
})}
|
})}
|
||||||
@@ -171,4 +265,4 @@ export const LiveCollaboration = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
LiveCollaboration.displayName = "LiveCollaboration";
|
LiveCollaborationTrigger.displayName = "LiveCollaborationTrigger";
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
useDevice,
|
||||||
|
useExcalidrawAppState,
|
||||||
|
useExcalidrawSetAppState,
|
||||||
|
} from "../App";
|
||||||
|
import DropdownMenu from "../dropdownMenu/DropdownMenu";
|
||||||
|
|
||||||
|
import * as DefaultItems from "./DefaultItems";
|
||||||
|
|
||||||
|
import { UserList } from "../UserList";
|
||||||
|
import { t } from "../../i18n";
|
||||||
|
import { HamburgerMenuIcon } from "../icons";
|
||||||
|
import { withInternalFallback } from "../hoc/withInternalFallback";
|
||||||
|
import { composeEventHandlers } from "../../utils";
|
||||||
|
import { useTunnels } from "../context/tunnels";
|
||||||
|
|
||||||
|
const MainMenu = Object.assign(
|
||||||
|
withInternalFallback(
|
||||||
|
"MainMenu",
|
||||||
|
({
|
||||||
|
children,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
/**
|
||||||
|
* Called when any menu item is selected (clicked on).
|
||||||
|
*/
|
||||||
|
onSelect?: (event: Event) => void;
|
||||||
|
}) => {
|
||||||
|
const { mainMenuTunnel } = useTunnels();
|
||||||
|
const device = useDevice();
|
||||||
|
const appState = useExcalidrawAppState();
|
||||||
|
const setAppState = useExcalidrawSetAppState();
|
||||||
|
const onClickOutside = device.isMobile
|
||||||
|
? undefined
|
||||||
|
: () => setAppState({ openMenu: null });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mainMenuTunnel.In>
|
||||||
|
<DropdownMenu open={appState.openMenu === "canvas"}>
|
||||||
|
<DropdownMenu.Trigger
|
||||||
|
onToggle={() => {
|
||||||
|
setAppState({
|
||||||
|
openMenu: appState.openMenu === "canvas" ? null : "canvas",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{HamburgerMenuIcon}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
onClickOutside={onClickOutside}
|
||||||
|
onSelect={composeEventHandlers(onSelect, () => {
|
||||||
|
setAppState({ openMenu: null });
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{device.isMobile && appState.collaborators.size > 0 && (
|
||||||
|
<fieldset className="UserList-Wrapper">
|
||||||
|
<legend>{t("labels.collaborators")}</legend>
|
||||||
|
<UserList
|
||||||
|
mobile={true}
|
||||||
|
collaborators={appState.collaborators}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
)}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu>
|
||||||
|
</mainMenuTunnel.In>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
{
|
||||||
|
Trigger: DropdownMenu.Trigger,
|
||||||
|
Item: DropdownMenu.Item,
|
||||||
|
ItemLink: DropdownMenu.ItemLink,
|
||||||
|
ItemCustom: DropdownMenu.ItemCustom,
|
||||||
|
Group: DropdownMenu.Group,
|
||||||
|
Separator: DropdownMenu.Separator,
|
||||||
|
DefaultItems,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default MainMenu;
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
useDevice,
|
|
||||||
useExcalidrawAppState,
|
|
||||||
useExcalidrawSetAppState,
|
|
||||||
} from "../App";
|
|
||||||
import DropdownMenu from "../dropdownMenu/DropdownMenu";
|
|
||||||
|
|
||||||
import * as DefaultItems from "./DefaultItems";
|
|
||||||
|
|
||||||
import { UserList } from "../UserList";
|
|
||||||
import { t } from "../../i18n";
|
|
||||||
import { HamburgerMenuIcon } from "../icons";
|
|
||||||
|
|
||||||
const MainMenu = ({ children }: { children?: React.ReactNode }) => {
|
|
||||||
const device = useDevice();
|
|
||||||
const appState = useExcalidrawAppState();
|
|
||||||
const setAppState = useExcalidrawSetAppState();
|
|
||||||
const onClickOutside = device.isMobile
|
|
||||||
? undefined
|
|
||||||
: () => setAppState({ openMenu: null });
|
|
||||||
return (
|
|
||||||
<DropdownMenu open={appState.openMenu === "canvas"}>
|
|
||||||
<DropdownMenu.Trigger
|
|
||||||
onToggle={() => {
|
|
||||||
setAppState({
|
|
||||||
openMenu: appState.openMenu === "canvas" ? null : "canvas",
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{HamburgerMenuIcon}
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
<DropdownMenu.Content onClickOutside={onClickOutside}>
|
|
||||||
{children}
|
|
||||||
{device.isMobile && appState.collaborators.size > 0 && (
|
|
||||||
<fieldset className="UserList-Wrapper">
|
|
||||||
<legend>{t("labels.collaborators")}</legend>
|
|
||||||
<UserList mobile={true} collaborators={appState.collaborators} />
|
|
||||||
</fieldset>
|
|
||||||
)}
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
MainMenu.Trigger = DropdownMenu.Trigger;
|
|
||||||
MainMenu.Item = DropdownMenu.Item;
|
|
||||||
MainMenu.ItemLink = DropdownMenu.ItemLink;
|
|
||||||
MainMenu.ItemCustom = DropdownMenu.ItemCustom;
|
|
||||||
MainMenu.Group = DropdownMenu.Group;
|
|
||||||
MainMenu.Separator = DropdownMenu.Separator;
|
|
||||||
MainMenu.DefaultItems = DefaultItems;
|
|
||||||
|
|
||||||
export default MainMenu;
|
|
||||||
|
|
||||||
MainMenu.displayName = "Menu";
|
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import { actionLoadScene, actionShortcuts } from "../../actions";
|
||||||
|
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||||
|
import { t } from "../../i18n";
|
||||||
|
import {
|
||||||
|
useDevice,
|
||||||
|
useExcalidrawActionManager,
|
||||||
|
useExcalidrawAppState,
|
||||||
|
} from "../App";
|
||||||
|
import { useTunnels } from "../context/tunnels";
|
||||||
|
import { ExcalLogo, HelpIcon, LoadIcon, usersIcon } from "../icons";
|
||||||
|
|
||||||
|
const WelcomeScreenMenuItemContent = ({
|
||||||
|
icon,
|
||||||
|
shortcut,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
icon?: JSX.Element;
|
||||||
|
shortcut?: string | null;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const device = useDevice();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="welcome-screen-menu-item__icon">{icon}</div>
|
||||||
|
<div className="welcome-screen-menu-item__text">{children}</div>
|
||||||
|
{shortcut && !device.isMobile && (
|
||||||
|
<div className="welcome-screen-menu-item__shortcut">{shortcut}</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
WelcomeScreenMenuItemContent.displayName = "WelcomeScreenMenuItemContent";
|
||||||
|
|
||||||
|
const WelcomeScreenMenuItem = ({
|
||||||
|
onSelect,
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
shortcut,
|
||||||
|
className = "",
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
onSelect: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
icon?: JSX.Element;
|
||||||
|
shortcut?: string | null;
|
||||||
|
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
type="button"
|
||||||
|
className={`welcome-screen-menu-item ${className}`}
|
||||||
|
onClick={onSelect}
|
||||||
|
>
|
||||||
|
<WelcomeScreenMenuItemContent icon={icon} shortcut={shortcut}>
|
||||||
|
{children}
|
||||||
|
</WelcomeScreenMenuItemContent>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
WelcomeScreenMenuItem.displayName = "WelcomeScreenMenuItem";
|
||||||
|
|
||||||
|
const WelcomeScreenMenuItemLink = ({
|
||||||
|
children,
|
||||||
|
href,
|
||||||
|
icon,
|
||||||
|
shortcut,
|
||||||
|
className = "",
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
href: string;
|
||||||
|
icon?: JSX.Element;
|
||||||
|
shortcut?: string | null;
|
||||||
|
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
{...props}
|
||||||
|
className={`welcome-screen-menu-item ${className}`}
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<WelcomeScreenMenuItemContent icon={icon} shortcut={shortcut}>
|
||||||
|
{children}
|
||||||
|
</WelcomeScreenMenuItemContent>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
WelcomeScreenMenuItemLink.displayName = "WelcomeScreenMenuItemLink";
|
||||||
|
|
||||||
|
const Center = ({ children }: { children?: React.ReactNode }) => {
|
||||||
|
const { welcomeScreenCenterTunnel } = useTunnels();
|
||||||
|
return (
|
||||||
|
<welcomeScreenCenterTunnel.In>
|
||||||
|
<div className="welcome-screen-center">
|
||||||
|
{children || (
|
||||||
|
<>
|
||||||
|
<Logo />
|
||||||
|
<Heading>{t("welcomeScreen.defaults.center_heading")}</Heading>
|
||||||
|
<Menu>
|
||||||
|
<MenuItemLoadScene />
|
||||||
|
<MenuItemHelp />
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</welcomeScreenCenterTunnel.In>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Center.displayName = "Center";
|
||||||
|
|
||||||
|
const Logo = ({ children }: { children?: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div className="welcome-screen-center__logo virgil welcome-screen-decor">
|
||||||
|
{children || <>{ExcalLogo} Excalidraw</>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Logo.displayName = "Logo";
|
||||||
|
|
||||||
|
const Heading = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div className="welcome-screen-center__heading welcome-screen-decor virgil">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Heading.displayName = "Heading";
|
||||||
|
|
||||||
|
const Menu = ({ children }: { children?: React.ReactNode }) => {
|
||||||
|
return <div className="welcome-screen-menu">{children}</div>;
|
||||||
|
};
|
||||||
|
Menu.displayName = "Menu";
|
||||||
|
|
||||||
|
const MenuItemHelp = () => {
|
||||||
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WelcomeScreenMenuItem
|
||||||
|
onSelect={() => actionManager.executeAction(actionShortcuts)}
|
||||||
|
shortcut="?"
|
||||||
|
icon={HelpIcon}
|
||||||
|
>
|
||||||
|
{t("helpDialog.title")}
|
||||||
|
</WelcomeScreenMenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
MenuItemHelp.displayName = "MenuItemHelp";
|
||||||
|
|
||||||
|
const MenuItemLoadScene = () => {
|
||||||
|
const appState = useExcalidrawAppState();
|
||||||
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
|
if (appState.viewModeEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WelcomeScreenMenuItem
|
||||||
|
onSelect={() => actionManager.executeAction(actionLoadScene)}
|
||||||
|
shortcut={getShortcutFromShortcutName("loadScene")}
|
||||||
|
icon={LoadIcon}
|
||||||
|
>
|
||||||
|
{t("buttons.load")}
|
||||||
|
</WelcomeScreenMenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
MenuItemLoadScene.displayName = "MenuItemLoadScene";
|
||||||
|
|
||||||
|
const MenuItemLiveCollaborationTrigger = ({
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
onSelect: () => any;
|
||||||
|
}) => {
|
||||||
|
// FIXME when we tie t() to lang state
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const appState = useExcalidrawAppState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WelcomeScreenMenuItem shortcut={null} onSelect={onSelect} icon={usersIcon}>
|
||||||
|
{t("labels.liveCollaboration")}
|
||||||
|
</WelcomeScreenMenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
MenuItemLiveCollaborationTrigger.displayName =
|
||||||
|
"MenuItemLiveCollaborationTrigger";
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Center.Logo = Logo;
|
||||||
|
Center.Heading = Heading;
|
||||||
|
Center.Menu = Menu;
|
||||||
|
Center.MenuItem = WelcomeScreenMenuItem;
|
||||||
|
Center.MenuItemLink = WelcomeScreenMenuItemLink;
|
||||||
|
Center.MenuItemHelp = MenuItemHelp;
|
||||||
|
Center.MenuItemLoadScene = MenuItemLoadScene;
|
||||||
|
Center.MenuItemLiveCollaborationTrigger = MenuItemLiveCollaborationTrigger;
|
||||||
|
|
||||||
|
export { Center };
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { t } from "../../i18n";
|
||||||
|
import { useTunnels } from "../context/tunnels";
|
||||||
|
import {
|
||||||
|
WelcomeScreenHelpArrow,
|
||||||
|
WelcomeScreenMenuArrow,
|
||||||
|
WelcomeScreenTopToolbarArrow,
|
||||||
|
} from "../icons";
|
||||||
|
|
||||||
|
const MenuHint = ({ children }: { children?: React.ReactNode }) => {
|
||||||
|
const { welcomeScreenMenuHintTunnel } = useTunnels();
|
||||||
|
return (
|
||||||
|
<welcomeScreenMenuHintTunnel.In>
|
||||||
|
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--menu">
|
||||||
|
{WelcomeScreenMenuArrow}
|
||||||
|
<div className="welcome-screen-decor-hint__label">
|
||||||
|
{children || t("welcomeScreen.defaults.menuHint")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</welcomeScreenMenuHintTunnel.In>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
MenuHint.displayName = "MenuHint";
|
||||||
|
|
||||||
|
const ToolbarHint = ({ children }: { children?: React.ReactNode }) => {
|
||||||
|
const { welcomeScreenToolbarHintTunnel } = useTunnels();
|
||||||
|
return (
|
||||||
|
<welcomeScreenToolbarHintTunnel.In>
|
||||||
|
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--toolbar">
|
||||||
|
<div className="welcome-screen-decor-hint__label">
|
||||||
|
{children || t("welcomeScreen.defaults.toolbarHint")}
|
||||||
|
</div>
|
||||||
|
{WelcomeScreenTopToolbarArrow}
|
||||||
|
</div>
|
||||||
|
</welcomeScreenToolbarHintTunnel.In>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
ToolbarHint.displayName = "ToolbarHint";
|
||||||
|
|
||||||
|
const HelpHint = ({ children }: { children?: React.ReactNode }) => {
|
||||||
|
const { welcomeScreenHelpHintTunnel } = useTunnels();
|
||||||
|
return (
|
||||||
|
<welcomeScreenHelpHintTunnel.In>
|
||||||
|
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--help">
|
||||||
|
<div>{children || t("welcomeScreen.defaults.helpHint")}</div>
|
||||||
|
{WelcomeScreenHelpArrow}
|
||||||
|
</div>
|
||||||
|
</welcomeScreenHelpHintTunnel.In>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
HelpHint.displayName = "HelpHint";
|
||||||
|
|
||||||
|
export { HelpHint, MenuHint, ToolbarHint };
|
||||||
+89
-78
@@ -3,29 +3,39 @@
|
|||||||
font-family: "Virgil";
|
font-family: "Virgil";
|
||||||
}
|
}
|
||||||
|
|
||||||
.WelcomeScreen-logo {
|
// WelcomeSreen common
|
||||||
display: flex;
|
// ---------------------------------------------------------------------------
|
||||||
align-items: center;
|
|
||||||
column-gap: 0.75rem;
|
|
||||||
font-size: 2.25rem;
|
|
||||||
|
|
||||||
svg {
|
.welcome-screen-decor {
|
||||||
width: 1.625rem;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.WelcomeScreen-decor {
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
color: var(--color-gray-40);
|
color: var(--color-gray-40);
|
||||||
|
}
|
||||||
|
|
||||||
&--subheading {
|
&.theme--dark {
|
||||||
font-size: 1.125rem;
|
.welcome-screen-decor {
|
||||||
text-align: center;
|
color: var(--color-gray-60);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WelcomeScreen.Hints
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
.welcome-screen-decor-hint {
|
||||||
|
@media (max-height: 599px) {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--help-pointer {
|
@media (max-width: 1024px), (max-width: 800px) {
|
||||||
|
.welcome-screen-decor {
|
||||||
|
&--help,
|
||||||
|
&--menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--help {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -49,7 +59,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--top-toolbar-pointer {
|
&--toolbar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -58,7 +68,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
|
|
||||||
&__label {
|
.welcome-screen-decor-hint__label {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -0.5rem;
|
top: -0.5rem;
|
||||||
@@ -74,7 +84,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--menu-pointer {
|
&--menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@@ -95,10 +105,19 @@
|
|||||||
transform: scaleX(-1);
|
transform: scaleX(-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.welcome-screen-decor-hint__label {
|
||||||
|
max-width: 160px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.WelcomeScreen-container {
|
// WelcomeSreen.Center
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
.welcome-screen-center {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
@@ -112,7 +131,24 @@
|
|||||||
bottom: 1rem;
|
bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.WelcomeScreen-items {
|
.welcome-screen-center__logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 0.75rem;
|
||||||
|
font-size: 2.25rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1.625rem;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-screen-center__heading {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-screen-menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
@@ -120,7 +156,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.WelcomeScreen-item {
|
.welcome-screen-menu-item {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
@@ -128,8 +164,10 @@
|
|||||||
color: var(--color-gray-50);
|
color: var(--color-gray-50);
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
display: flex;
|
max-width: 400px;
|
||||||
|
display: grid;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
@@ -140,44 +178,49 @@
|
|||||||
|
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: var(--border-radius-md);
|
||||||
|
|
||||||
&__label {
|
grid-template-columns: calc(var(--default-icon-size) + 0.5rem) 1fr 3rem;
|
||||||
|
|
||||||
|
&__text {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-right: auto;
|
||||||
|
text-align: left;
|
||||||
column-gap: 0.5rem;
|
column-gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
&__icon {
|
||||||
width: var(--default-icon-size);
|
width: var(--default-icon-size);
|
||||||
height: var(--default-icon-size);
|
height: var(--default-icon-size);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__shortcut {
|
&__shortcut {
|
||||||
|
margin-left: auto;
|
||||||
color: var(--color-gray-40);
|
color: var(--color-gray-40);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:active) .WelcomeScreen-item:hover {
|
&:not(:active) .welcome-screen-menu-item:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background: var(--color-gray-10);
|
background: var(--color-gray-10);
|
||||||
|
|
||||||
.WelcomeScreen-item__shortcut {
|
.welcome-screen-menu-item__shortcut {
|
||||||
color: var(--color-gray-50);
|
color: var(--color-gray-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
.WelcomeScreen-item__label {
|
.welcome-screen-menu-item__text {
|
||||||
color: var(--color-gray-100);
|
color: var(--color-gray-100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.WelcomeScreen-item:active {
|
.welcome-screen-menu-item:active {
|
||||||
background: var(--color-gray-20);
|
background: var(--color-gray-20);
|
||||||
|
|
||||||
.WelcomeScreen-item__shortcut {
|
.welcome-screen-menu-item__shortcut {
|
||||||
color: var(--color-gray-50);
|
color: var(--color-gray-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
.WelcomeScreen-item__label {
|
.welcome-screen-menu-item__text {
|
||||||
color: var(--color-gray-100);
|
color: var(--color-gray-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +228,7 @@
|
|||||||
color: var(--color-promo) !important;
|
color: var(--color-promo) !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.WelcomeScreen-item__label {
|
.welcome-screen-menu-item__text {
|
||||||
color: var(--color-promo) !important;
|
color: var(--color-promo) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,11 +236,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.theme--dark {
|
&.theme--dark {
|
||||||
.WelcomeScreen-decor {
|
.welcome-screen-menu-item {
|
||||||
color: var(--color-gray-60);
|
|
||||||
}
|
|
||||||
|
|
||||||
.WelcomeScreen-item {
|
|
||||||
color: var(--color-gray-60);
|
color: var(--color-gray-60);
|
||||||
|
|
||||||
&__shortcut {
|
&__shortcut {
|
||||||
@@ -205,69 +244,41 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:active) .WelcomeScreen-item:hover {
|
&:not(:active) .welcome-screen-menu-item:hover {
|
||||||
background: var(--color-gray-85);
|
background: var(--color-gray-85);
|
||||||
|
|
||||||
.WelcomeScreen-item__shortcut {
|
.welcome-screen-menu-item__shortcut {
|
||||||
color: var(--color-gray-50);
|
color: var(--color-gray-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
.WelcomeScreen-item__label {
|
.welcome-screen-menu-item__text {
|
||||||
color: var(--color-gray-10);
|
color: var(--color-gray-10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.WelcomeScreen-item:active {
|
.welcome-screen-menu-item:active {
|
||||||
background-color: var(--color-gray-90);
|
background-color: var(--color-gray-90);
|
||||||
.WelcomeScreen-item__label {
|
.welcome-screen-menu-item__text {
|
||||||
color: var(--color-gray-10);
|
color: var(--color-gray-10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Can tweak these values but for an initial effort, it looks OK to me
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.WelcomeScreen-decor {
|
|
||||||
&--help-pointer,
|
|
||||||
&--menu-pointer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @media (max-height: 400px) {
|
|
||||||
// .WelcomeScreen-container {
|
|
||||||
// margin-top: 0;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@media (max-height: 599px) {
|
@media (max-height: 599px) {
|
||||||
.WelcomeScreen-container {
|
.welcome-screen-center {
|
||||||
margin-top: 4rem;
|
margin-top: 4rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-height: 600px) and (max-height: 900px) {
|
@media (min-height: 600px) and (max-height: 900px) {
|
||||||
.WelcomeScreen-container {
|
.welcome-screen-center {
|
||||||
margin-top: 8rem;
|
margin-top: 8rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (max-height: 630px) {
|
@media (max-height: 500px), (max-width: 320px) {
|
||||||
.WelcomeScreen-decor--top-toolbar-pointer {
|
.welcome-screen-center {
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-height: 500px) {
|
|
||||||
.WelcomeScreen-container {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @media (max-height: 740px) {
|
// ---------------------------------------------------------------------------
|
||||||
// .WelcomeScreen-decor {
|
|
||||||
// &--help-pointer,
|
|
||||||
// &--top-toolbar-pointer,
|
|
||||||
// &--menu-pointer {
|
|
||||||
// display: none;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Center } from "./WelcomeScreen.Center";
|
||||||
|
import { MenuHint, ToolbarHint, HelpHint } from "./WelcomeScreen.Hints";
|
||||||
|
|
||||||
|
import "./WelcomeScreen.scss";
|
||||||
|
|
||||||
|
const WelcomeScreen = (props: { children?: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{props.children || (
|
||||||
|
<>
|
||||||
|
<Center />
|
||||||
|
<MenuHint />
|
||||||
|
<ToolbarHint />
|
||||||
|
<HelpHint />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
WelcomeScreen.displayName = "WelcomeScreen";
|
||||||
|
|
||||||
|
WelcomeScreen.Center = Center;
|
||||||
|
WelcomeScreen.Hints = { MenuHint, ToolbarHint, HelpHint };
|
||||||
|
|
||||||
|
export default WelcomeScreen;
|
||||||
+9
-8
@@ -2,6 +2,14 @@ import cssVariables from "./css/variables.module.scss";
|
|||||||
import { AppProps } from "./types";
|
import { AppProps } from "./types";
|
||||||
import { FontFamilyValues } from "./element/types";
|
import { FontFamilyValues } from "./element/types";
|
||||||
|
|
||||||
|
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
||||||
|
export const isWindows = /^Win/.test(navigator.platform);
|
||||||
|
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
|
||||||
|
export const isFirefox =
|
||||||
|
"netscape" in window &&
|
||||||
|
navigator.userAgent.indexOf("rv:") > 1 &&
|
||||||
|
navigator.userAgent.indexOf("Gecko") > 1;
|
||||||
|
|
||||||
export const APP_NAME = "Excalidraw";
|
export const APP_NAME = "Excalidraw";
|
||||||
|
|
||||||
export const DRAGGING_THRESHOLD = 10; // px
|
export const DRAGGING_THRESHOLD = 10; // px
|
||||||
@@ -54,6 +62,7 @@ export enum EVENT {
|
|||||||
SCROLL = "scroll",
|
SCROLL = "scroll",
|
||||||
// custom events
|
// custom events
|
||||||
EXCALIDRAW_LINK = "excalidraw-link",
|
EXCALIDRAW_LINK = "excalidraw-link",
|
||||||
|
MENU_ITEM_SELECT = "menu.itemSelect",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ENV = {
|
export const ENV = {
|
||||||
@@ -236,14 +245,6 @@ export const ROUNDNESS = {
|
|||||||
ADAPTIVE_RADIUS: 3,
|
ADAPTIVE_RADIUS: 3,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const COOKIES = {
|
|
||||||
AUTH_STATE_COOKIE: "excplus-auth",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/** key containt id of precedeing elemnt id we use in reconciliation during
|
/** key containt id of precedeing elemnt id we use in reconciliation during
|
||||||
* collaboration */
|
* collaboration */
|
||||||
export const PRECEDING_ELEMENT_KEY = "__precedingElement__";
|
export const PRECEDING_ELEMENT_KEY = "__precedingElement__";
|
||||||
|
|
||||||
export const isExcalidrawPlusSignedUser = document.cookie.includes(
|
|
||||||
COOKIES.AUTH_STATE_COOKIE,
|
|
||||||
);
|
|
||||||
|
|||||||
+9
-4
@@ -8,6 +8,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
|
--ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
|
||||||
|
Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
font-family: var(--ui-font);
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
@@ -408,7 +412,7 @@
|
|||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--button-hover);
|
background-color: var(--button-hover-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
@@ -540,15 +544,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mobile-misc-tools-container {
|
.mobile-misc-tools-container {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
top: 5rem;
|
top: calc(5rem - var(--editor-container-padding));
|
||||||
right: 0;
|
right: calc(var(--editor-container-padding) * -1);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border: 1px solid var(--sidebar-border-color);
|
border: 1px solid var(--sidebar-border-color);
|
||||||
border-top-left-radius: var(--border-radius-lg);
|
border-top-left-radius: var(--border-radius-lg);
|
||||||
border-bottom-left-radius: var(--border-radius-lg);
|
border-bottom-left-radius: var(--border-radius-lg);
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
background-color: var(--island-bg-color);
|
background-color: var(--island-bg-color);
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -35,13 +35,14 @@
|
|||||||
--shadow-island: 0px 7px 14px rgba(0, 0, 0, 0.05),
|
--shadow-island: 0px 7px 14px rgba(0, 0, 0, 0.05),
|
||||||
0px 0px 3.12708px rgba(0, 0, 0, 0.0798),
|
0px 0px 3.12708px rgba(0, 0, 0, 0.0798),
|
||||||
0px 0px 0.931014px rgba(0, 0, 0, 0.1702);
|
0px 0px 0.931014px rgba(0, 0, 0, 0.1702);
|
||||||
--button-hover: var(--color-gray-10);
|
--button-hover-bg: var(--color-gray-10);
|
||||||
--default-border-color: var(--color-gray-30);
|
--default-border-color: var(--color-gray-30);
|
||||||
|
|
||||||
--default-button-size: 2rem;
|
--default-button-size: 2rem;
|
||||||
--default-icon-size: 1rem;
|
--default-icon-size: 1rem;
|
||||||
--lg-button-size: 2.25rem;
|
--lg-button-size: 2.25rem;
|
||||||
--lg-icon-size: 1rem;
|
--lg-icon-size: 1rem;
|
||||||
|
--editor-container-padding: 1rem;
|
||||||
|
|
||||||
@media screen and (min-device-width: 1921px) {
|
@media screen and (min-device-width: 1921px) {
|
||||||
--lg-button-size: 2.5rem;
|
--lg-button-size: 2.5rem;
|
||||||
@@ -135,7 +136,7 @@
|
|||||||
--popup-text-inverted-color: #2c2c2c;
|
--popup-text-inverted-color: #2c2c2c;
|
||||||
--select-highlight-color: #{$oc-blue-4};
|
--select-highlight-color: #{$oc-blue-4};
|
||||||
--text-primary-color: var(--color-gray-40);
|
--text-primary-color: var(--color-gray-40);
|
||||||
--button-hover: var(--color-gray-80);
|
--button-hover-bg: var(--color-gray-80);
|
||||||
--default-border-color: var(--color-gray-80);
|
--default-border-color: var(--color-gray-80);
|
||||||
--shadow-island: 0px 13px 33px rgba(0, 0, 0, 0.07),
|
--shadow-island: 0px 13px 33px rgba(0, 0, 0, 0.07),
|
||||||
0px 4.13px 9.94853px rgba(0, 0, 0, 0.0456112),
|
0px 4.13px 9.94853px rgba(0, 0, 0, 0.0456112),
|
||||||
|
|||||||
@@ -39,11 +39,11 @@
|
|||||||
|
|
||||||
.ToolIcon__icon {
|
.ToolIcon__icon {
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--button-hover);
|
background: var(--button-hover-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background: var(--button-hover);
|
background: var(--button-hover-bg);
|
||||||
border: 1px solid var(--color-primary-darkest);
|
border: 1px solid var(--color-primary-darkest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,24 +54,30 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.625rem;
|
padding: 0.625rem;
|
||||||
width: var(--default-button-size);
|
width: var(--button-width, var(--default-button-size));
|
||||||
height: var(--default-button-size);
|
height: var(--button-height, var(--default-button-size));
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: var(--default-border-color);
|
border-color: var(--button-border, var(--default-border-color));
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: var(--border-radius-lg);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: transparent;
|
background-color: var(--button-bg, var(--island-bg-color));
|
||||||
color: var(--text-primary-color);
|
color: var(--button-color, var(--text-primary-color));
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: var(--button-width, var(--lg-icon-size));
|
||||||
|
height: var(--button-height, var(--lg-icon-size));
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--button-hover);
|
background-color: var(--button-hover-bg, var(--island-bg-color));
|
||||||
|
border-color: var(--button-hover-border, var(--default-border-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background-color: var(--button-hover);
|
background-color: var(--button-active-bg, var(--island-bg-color));
|
||||||
border-color: var(--color-primary-darkest);
|
border-color: var(--button-active-border, var(--color-primary-darkest));
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
@@ -83,7 +89,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
color: var(--color-primary-darker);
|
color: var(--button-color, var(--color-primary-darker));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-2
@@ -2,7 +2,7 @@ import {
|
|||||||
copyBlobToClipboardAsPng,
|
copyBlobToClipboardAsPng,
|
||||||
copyTextToSystemClipboard,
|
copyTextToSystemClipboard,
|
||||||
} from "../clipboard";
|
} from "../clipboard";
|
||||||
import { DEFAULT_EXPORT_PADDING, MIME_TYPES } from "../constants";
|
import { DEFAULT_EXPORT_PADDING, isFirefox, MIME_TYPES } from "../constants";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { exportToCanvas, exportToSvg } from "../scene/export";
|
import { exportToCanvas, exportToSvg } from "../scene/export";
|
||||||
@@ -97,10 +97,21 @@ export const exportCanvas = async (
|
|||||||
const blob = canvasToBlob(tempCanvas);
|
const blob = canvasToBlob(tempCanvas);
|
||||||
await copyBlobToClipboardAsPng(blob);
|
await copyBlobToClipboardAsPng(blob);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
console.warn(error);
|
||||||
if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
|
if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
throw new Error(t("alerts.couldNotCopyToClipboard"));
|
// TypeError *probably* suggests ClipboardItem not defined, which
|
||||||
|
// people on Firefox can enable through a flag, so let's tell them.
|
||||||
|
if (isFirefox && error.name === "TypeError") {
|
||||||
|
throw new Error(
|
||||||
|
`${t("alerts.couldNotCopyToClipboard")}\n\n${t(
|
||||||
|
"hints.firefox_clipboard_write",
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(t("alerts.couldNotCopyToClipboard"));
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
tempCanvas.remove();
|
tempCanvas.remove();
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -55,6 +55,7 @@ export const AllowedExcalidrawActiveTools: Record<
|
|||||||
freedraw: true,
|
freedraw: true,
|
||||||
eraser: false,
|
eraser: false,
|
||||||
custom: true,
|
custom: true,
|
||||||
|
hand: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RestoredDataState = {
|
export type RestoredDataState = {
|
||||||
@@ -465,7 +466,7 @@ export const restoreAppState = (
|
|||||||
? nextAppState.activeTool
|
? nextAppState.activeTool
|
||||||
: { type: "selection" },
|
: { type: "selection" },
|
||||||
),
|
),
|
||||||
lastActiveToolBeforeEraser: null,
|
lastActiveTool: null,
|
||||||
locked: nextAppState.activeTool.locked ?? false,
|
locked: nextAppState.activeTool.locked ?? false,
|
||||||
},
|
},
|
||||||
// Migrates from previous version where appState.zoom was a number
|
// Migrates from previous version where appState.zoom was a number
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ export const actionLink = register({
|
|||||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.K,
|
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.K,
|
||||||
contextItemLabel: (elements, appState) =>
|
contextItemLabel: (elements, appState) =>
|
||||||
getContextMenuLabel(elements, appState),
|
getContextMenuLabel(elements, appState),
|
||||||
contextItemPredicate: (elements, appState) => {
|
predicate: (elements, appState) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
return selectedElements.length === 1;
|
return selectedElements.length === 1;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -557,10 +557,10 @@ export const resizeSingleElement = (
|
|||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
scale: [
|
scale: [
|
||||||
// defaulting because scaleX/Y can be 0/-0
|
// defaulting because scaleX/Y can be 0/-0
|
||||||
(Math.sign(scaleX) || stateAtResizeStart.scale[0]) *
|
(Math.sign(newBoundsX2 - stateAtResizeStart.x) ||
|
||||||
stateAtResizeStart.scale[0],
|
stateAtResizeStart.scale[0]) * stateAtResizeStart.scale[0],
|
||||||
(Math.sign(scaleY) || stateAtResizeStart.scale[1]) *
|
(Math.sign(newBoundsY2 - stateAtResizeStart.y) ||
|
||||||
stateAtResizeStart.scale[1],
|
stateAtResizeStart.scale[1]) * stateAtResizeStart.scale[1],
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const showSelectedShapeActions = (
|
|||||||
appState.activeTool.type !== "custom" &&
|
appState.activeTool.type !== "custom" &&
|
||||||
(appState.editingElement ||
|
(appState.editingElement ||
|
||||||
(appState.activeTool.type !== "selection" &&
|
(appState.activeTool.type !== "selection" &&
|
||||||
appState.activeTool.type !== "eraser"))) ||
|
appState.activeTool.type !== "eraser" &&
|
||||||
|
appState.activeTool.type !== "hand"))) ||
|
||||||
getSelectedElements(elements, appState).length,
|
getSelectedElements(elements, appState).length,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,20 @@ describe("Test wrapText", () => {
|
|||||||
expect(res).toBe("Hello whats up ");
|
expect(res).toBe("Hello whats up ");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should work with emojis", () => {
|
||||||
|
const text = "😀";
|
||||||
|
const maxWidth = 1;
|
||||||
|
const res = wrapText(text, font, maxWidth);
|
||||||
|
expect(res).toBe("😀");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the text correctly when min width reached", () => {
|
||||||
|
const text = "Hello😀";
|
||||||
|
const maxWidth = 10;
|
||||||
|
const res = wrapText(text, font, maxWidth);
|
||||||
|
expect(res).toBe("H\ne\nl\nl\no\n😀");
|
||||||
|
});
|
||||||
|
|
||||||
describe("When text doesn't contain new lines", () => {
|
describe("When text doesn't contain new lines", () => {
|
||||||
const text = "Hello whats up";
|
const text = "Hello whats up";
|
||||||
[
|
[
|
||||||
@@ -157,7 +171,7 @@ describe("Test measureText", () => {
|
|||||||
|
|
||||||
expect(res.container).toMatchInlineSnapshot(`
|
expect(res.container).toMatchInlineSnapshot(`
|
||||||
<div
|
<div
|
||||||
style="position: absolute; white-space: pre-wrap; font: Emoji 20px 20px; min-height: 1em; width: 111px; overflow: hidden; word-break: break-word; line-height: 0px;"
|
style="position: absolute; white-space: pre-wrap; font: Emoji 20px 20px; min-height: 1em; max-width: 191px; overflow: hidden; word-break: break-word; line-height: 0px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
style="display: inline-block; overflow: hidden; width: 1px; height: 1px;"
|
style="display: inline-block; overflow: hidden; width: 1px; height: 1px;"
|
||||||
|
|||||||
+84
-88
@@ -61,30 +61,29 @@ export const redrawTextBoundingBox = (
|
|||||||
if (!isArrowElement(container)) {
|
if (!isArrowElement(container)) {
|
||||||
const containerDims = getContainerDims(container);
|
const containerDims = getContainerDims(container);
|
||||||
let nextHeight = containerDims.height;
|
let nextHeight = containerDims.height;
|
||||||
const boundTextElementPadding = getBoundTextElementOffset(textElement);
|
|
||||||
if (textElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
if (textElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
||||||
coordY = container.y + boundTextElementPadding;
|
coordY = container.y;
|
||||||
} else if (textElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
} else if (textElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||||
coordY =
|
coordY =
|
||||||
container.y +
|
container.y +
|
||||||
containerDims.height -
|
containerDims.height -
|
||||||
metrics.height -
|
metrics.height -
|
||||||
boundTextElementPadding;
|
BOUND_TEXT_PADDING;
|
||||||
} else {
|
} else {
|
||||||
coordY = container.y + containerDims.height / 2 - metrics.height / 2;
|
coordY = container.y + containerDims.height / 2 - metrics.height / 2;
|
||||||
if (metrics.height > getMaxContainerHeight(container)) {
|
if (metrics.height > getMaxContainerHeight(container)) {
|
||||||
nextHeight = metrics.height + boundTextElementPadding * 2;
|
nextHeight = metrics.height + BOUND_TEXT_PADDING * 2;
|
||||||
coordY = container.y + nextHeight / 2 - metrics.height / 2;
|
coordY = container.y + nextHeight / 2 - metrics.height / 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (textElement.textAlign === TEXT_ALIGN.LEFT) {
|
if (textElement.textAlign === TEXT_ALIGN.LEFT) {
|
||||||
coordX = container.x + boundTextElementPadding;
|
coordX = container.x + BOUND_TEXT_PADDING;
|
||||||
} else if (textElement.textAlign === TEXT_ALIGN.RIGHT) {
|
} else if (textElement.textAlign === TEXT_ALIGN.RIGHT) {
|
||||||
coordX =
|
coordX =
|
||||||
container.x +
|
container.x +
|
||||||
containerDims.width -
|
containerDims.width -
|
||||||
metrics.width -
|
metrics.width -
|
||||||
boundTextElementPadding;
|
BOUND_TEXT_PADDING;
|
||||||
} else {
|
} else {
|
||||||
coordX = container.x + containerDims.width / 2 - metrics.width / 2;
|
coordX = container.x + containerDims.width / 2 - metrics.width / 2;
|
||||||
}
|
}
|
||||||
@@ -271,12 +270,11 @@ export const measureText = (
|
|||||||
container.style.whiteSpace = "pre";
|
container.style.whiteSpace = "pre";
|
||||||
container.style.font = font;
|
container.style.font = font;
|
||||||
container.style.minHeight = "1em";
|
container.style.minHeight = "1em";
|
||||||
const textWidth = getTextWidth(text, font);
|
|
||||||
|
|
||||||
if (maxWidth) {
|
if (maxWidth) {
|
||||||
const lineHeight = getApproxLineHeight(font);
|
const lineHeight = getApproxLineHeight(font);
|
||||||
container.style.width = `${String(Math.min(textWidth, maxWidth) + 1)}px`;
|
// since we are adding a span of width 1px later
|
||||||
|
container.style.maxWidth = `${maxWidth + 1}px`;
|
||||||
container.style.overflow = "hidden";
|
container.style.overflow = "hidden";
|
||||||
container.style.wordBreak = "break-word";
|
container.style.wordBreak = "break-word";
|
||||||
container.style.lineHeight = `${String(lineHeight)}px`;
|
container.style.lineHeight = `${String(lineHeight)}px`;
|
||||||
@@ -293,11 +291,8 @@ export const measureText = (
|
|||||||
container.appendChild(span);
|
container.appendChild(span);
|
||||||
// Baseline is important for positioning text on canvas
|
// Baseline is important for positioning text on canvas
|
||||||
const baseline = span.offsetTop + span.offsetHeight;
|
const baseline = span.offsetTop + span.offsetHeight;
|
||||||
// Since span adds 1px extra width to the container
|
// since we are adding a span of width 1px
|
||||||
let width = container.offsetWidth;
|
const width = container.offsetWidth + 1;
|
||||||
if (maxWidth && textWidth > maxWidth) {
|
|
||||||
width = width - 1;
|
|
||||||
}
|
|
||||||
const height = container.offsetHeight;
|
const height = container.offsetHeight;
|
||||||
document.body.removeChild(container);
|
document.body.removeChild(container);
|
||||||
if (isTestEnv()) {
|
if (isTestEnv()) {
|
||||||
@@ -332,8 +327,11 @@ const getLineWidth = (text: string, font: FontString) => {
|
|||||||
if (isTestEnv()) {
|
if (isTestEnv()) {
|
||||||
return metrics.width * 10;
|
return metrics.width * 10;
|
||||||
}
|
}
|
||||||
|
// Since measureText behaves differently in different browsers
|
||||||
|
// OS so considering a adjustment factor of 0.2
|
||||||
|
const adjustmentFactor = 0.2;
|
||||||
|
|
||||||
return metrics.width;
|
return metrics.width + adjustmentFactor;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTextWidth = (text: string, font: FontString) => {
|
export const getTextWidth = (text: string, font: FontString) => {
|
||||||
@@ -359,96 +357,94 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
// This means its newline so push it
|
// This means its newline so push it
|
||||||
if (words.length === 1 && words[0] === "") {
|
if (words.length === 1 && words[0] === "") {
|
||||||
lines.push(words[0]);
|
lines.push(words[0]);
|
||||||
} else {
|
return; // continue
|
||||||
let currentLine = "";
|
}
|
||||||
let currentLineWidthTillNow = 0;
|
let currentLine = "";
|
||||||
|
let currentLineWidthTillNow = 0;
|
||||||
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
while (index < words.length) {
|
while (index < words.length) {
|
||||||
const currentWordWidth = getLineWidth(words[index], font);
|
const currentWordWidth = getLineWidth(words[index], font);
|
||||||
|
|
||||||
// Start breaking longer words exceeding max width
|
// Start breaking longer words exceeding max width
|
||||||
if (currentWordWidth >= maxWidth) {
|
if (currentWordWidth >= maxWidth) {
|
||||||
// push current line since the current word exceeds the max width
|
// push current line since the current word exceeds the max width
|
||||||
// so will be appended in next line
|
// so will be appended in next line
|
||||||
|
push(currentLine);
|
||||||
|
currentLine = "";
|
||||||
|
currentLineWidthTillNow = 0;
|
||||||
|
while (words[index].length > 0) {
|
||||||
|
const currentChar = String.fromCodePoint(
|
||||||
|
words[index].codePointAt(0)!,
|
||||||
|
);
|
||||||
|
const width = charWidth.calculate(currentChar, font);
|
||||||
|
currentLineWidthTillNow += width;
|
||||||
|
words[index] = words[index].slice(currentChar.length);
|
||||||
|
|
||||||
|
if (currentLineWidthTillNow >= maxWidth) {
|
||||||
|
// only remove last trailing space which we have added when joining words
|
||||||
|
if (currentLine.slice(-1) === " ") {
|
||||||
|
currentLine = currentLine.slice(0, -1);
|
||||||
|
}
|
||||||
|
push(currentLine);
|
||||||
|
currentLine = currentChar;
|
||||||
|
currentLineWidthTillNow = width;
|
||||||
|
} else {
|
||||||
|
currentLine += currentChar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// push current line if appending space exceeds max width
|
||||||
|
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
currentLine = "";
|
currentLine = "";
|
||||||
currentLineWidthTillNow = 0;
|
currentLineWidthTillNow = 0;
|
||||||
while (words[index].length > 0) {
|
|
||||||
const currentChar = words[index][0];
|
|
||||||
const width = charWidth.calculate(currentChar, font);
|
|
||||||
currentLineWidthTillNow += width;
|
|
||||||
words[index] = words[index].slice(1);
|
|
||||||
|
|
||||||
if (currentLineWidthTillNow >= maxWidth) {
|
|
||||||
// only remove last trailing space which we have added when joining words
|
|
||||||
if (currentLine.slice(-1) === " ") {
|
|
||||||
currentLine = currentLine.slice(0, -1);
|
|
||||||
}
|
|
||||||
push(currentLine);
|
|
||||||
currentLine = currentChar;
|
|
||||||
currentLineWidthTillNow = width;
|
|
||||||
if (currentLineWidthTillNow === maxWidth) {
|
|
||||||
currentLine = "";
|
|
||||||
currentLineWidthTillNow = 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
currentLine += currentChar;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// push current line if appending space exceeds max width
|
|
||||||
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
|
||||||
push(currentLine);
|
|
||||||
currentLine = "";
|
|
||||||
currentLineWidthTillNow = 0;
|
|
||||||
} else {
|
|
||||||
// space needs to be appended before next word
|
|
||||||
// as currentLine contains chars which couldn't be appended
|
|
||||||
// to previous line
|
|
||||||
currentLine += " ";
|
|
||||||
currentLineWidthTillNow += spaceWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
} else {
|
} else {
|
||||||
// Start appending words in a line till max width reached
|
// space needs to be appended before next word
|
||||||
while (currentLineWidthTillNow < maxWidth && index < words.length) {
|
// as currentLine contains chars which couldn't be appended
|
||||||
const word = words[index];
|
// to previous line
|
||||||
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
|
currentLine += " ";
|
||||||
|
currentLineWidthTillNow += spaceWidth;
|
||||||
|
}
|
||||||
|
|
||||||
if (currentLineWidthTillNow >= maxWidth) {
|
index++;
|
||||||
push(currentLine);
|
} else {
|
||||||
currentLineWidthTillNow = 0;
|
// Start appending words in a line till max width reached
|
||||||
currentLine = "";
|
while (currentLineWidthTillNow < maxWidth && index < words.length) {
|
||||||
|
const word = words[index];
|
||||||
|
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
|
||||||
|
|
||||||
break;
|
if (currentLineWidthTillNow >= maxWidth) {
|
||||||
}
|
push(currentLine);
|
||||||
index++;
|
currentLineWidthTillNow = 0;
|
||||||
currentLine += `${word} `;
|
currentLine = "";
|
||||||
|
|
||||||
// Push the word if appending space exceeds max width
|
break;
|
||||||
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
|
||||||
const word = currentLine.slice(0, -1);
|
|
||||||
push(word);
|
|
||||||
currentLine = "";
|
|
||||||
currentLineWidthTillNow = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (currentLineWidthTillNow === maxWidth) {
|
index++;
|
||||||
|
currentLine += `${word} `;
|
||||||
|
|
||||||
|
// Push the word if appending space exceeds max width
|
||||||
|
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
||||||
|
const word = currentLine.slice(0, -1);
|
||||||
|
push(word);
|
||||||
currentLine = "";
|
currentLine = "";
|
||||||
currentLineWidthTillNow = 0;
|
currentLineWidthTillNow = 0;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (currentLineWidthTillNow === maxWidth) {
|
||||||
if (currentLine) {
|
currentLine = "";
|
||||||
// only remove last trailing space which we have added when joining words
|
currentLineWidthTillNow = 0;
|
||||||
if (currentLine.slice(-1) === " ") {
|
|
||||||
currentLine = currentLine.slice(0, -1);
|
|
||||||
}
|
}
|
||||||
push(currentLine);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (currentLine) {
|
||||||
|
// only remove last trailing space which we have added when joining words
|
||||||
|
if (currentLine.slice(-1) === " ") {
|
||||||
|
currentLine = currentLine.slice(0, -1);
|
||||||
|
}
|
||||||
|
push(currentLine);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -862,7 +862,7 @@ describe("textWysiwyg", () => {
|
|||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
110,
|
109.5,
|
||||||
17,
|
17,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
@@ -910,7 +910,7 @@ describe("textWysiwyg", () => {
|
|||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
425,
|
424,
|
||||||
-539,
|
-539,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
@@ -1026,7 +1026,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.up(rectangle.x + 100, rectangle.y + 50);
|
mouse.up(rectangle.x + 100, rectangle.y + 50);
|
||||||
expect(rectangle.x).toBe(80);
|
expect(rectangle.x).toBe(80);
|
||||||
expect(rectangle.y).toBe(85);
|
expect(rectangle.y).toBe(85);
|
||||||
expect(text.x).toBe(90);
|
expect(text.x).toBe(89.5);
|
||||||
expect(text.y).toBe(90);
|
expect(text.y).toBe(90);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
@@ -1165,5 +1165,128 @@ describe("textWysiwyg", () => {
|
|||||||
).toEqual(36);
|
).toEqual(36);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("should align correctly", () => {
|
||||||
|
let editor: HTMLTextAreaElement;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
fireEvent.change(editor, { target: { value: "Hello" } });
|
||||||
|
editor.blur();
|
||||||
|
mouse.select(rectangle);
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
editor.select();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when top left", async () => {
|
||||||
|
fireEvent.click(screen.getByTitle("Left"));
|
||||||
|
fireEvent.click(screen.getByTitle("Align top"));
|
||||||
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
15,
|
||||||
|
20,
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when top center", async () => {
|
||||||
|
fireEvent.click(screen.getByTitle("Center"));
|
||||||
|
fireEvent.click(screen.getByTitle("Align top"));
|
||||||
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
94.5,
|
||||||
|
20,
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when top right", async () => {
|
||||||
|
fireEvent.click(screen.getByTitle("Right"));
|
||||||
|
fireEvent.click(screen.getByTitle("Align top"));
|
||||||
|
|
||||||
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
174,
|
||||||
|
20,
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when center left", async () => {
|
||||||
|
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||||
|
fireEvent.click(screen.getByTitle("Left"));
|
||||||
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
15,
|
||||||
|
25,
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when center center", async () => {
|
||||||
|
fireEvent.click(screen.getByTitle("Center"));
|
||||||
|
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||||
|
|
||||||
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
-25,
|
||||||
|
25,
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when center right", async () => {
|
||||||
|
fireEvent.click(screen.getByTitle("Right"));
|
||||||
|
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||||
|
|
||||||
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
174,
|
||||||
|
25,
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when bottom left", async () => {
|
||||||
|
fireEvent.click(screen.getByTitle("Left"));
|
||||||
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
|
|
||||||
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
15,
|
||||||
|
25,
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when bottom center", async () => {
|
||||||
|
fireEvent.click(screen.getByTitle("Center"));
|
||||||
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
94.5,
|
||||||
|
25,
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when bottom right", async () => {
|
||||||
|
fireEvent.click(screen.getByTitle("Right"));
|
||||||
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
174,
|
||||||
|
25,
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -142,11 +142,11 @@ export const textWysiwyg = ({
|
|||||||
const appState = app.state;
|
const appState = app.state;
|
||||||
const updatedTextElement =
|
const updatedTextElement =
|
||||||
Scene.getScene(element)?.getElement<ExcalidrawTextElement>(id);
|
Scene.getScene(element)?.getElement<ExcalidrawTextElement>(id);
|
||||||
|
|
||||||
if (!updatedTextElement) {
|
if (!updatedTextElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { textAlign, verticalAlign } = updatedTextElement;
|
const { textAlign, verticalAlign } = updatedTextElement;
|
||||||
|
|
||||||
const approxLineHeight = getApproxLineHeight(
|
const approxLineHeight = getApproxLineHeight(
|
||||||
getFontString(updatedTextElement),
|
getFontString(updatedTextElement),
|
||||||
);
|
);
|
||||||
@@ -161,6 +161,7 @@ export const textWysiwyg = ({
|
|||||||
// Set to element height by default since that's
|
// Set to element height by default since that's
|
||||||
// what is going to be used for unbounded text
|
// what is going to be used for unbounded text
|
||||||
let textElementHeight = updatedTextElement.height;
|
let textElementHeight = updatedTextElement.height;
|
||||||
|
|
||||||
if (container && updatedTextElement.containerId) {
|
if (container && updatedTextElement.containerId) {
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
const boundTextCoords =
|
const boundTextCoords =
|
||||||
@@ -206,7 +207,6 @@ export const textWysiwyg = ({
|
|||||||
maxHeight = getMaxContainerHeight(container);
|
maxHeight = getMaxContainerHeight(container);
|
||||||
|
|
||||||
// autogrow container height if text exceeds
|
// autogrow container height if text exceeds
|
||||||
|
|
||||||
if (!isArrowElement(container) && textElementHeight > maxHeight) {
|
if (!isArrowElement(container) && textElementHeight > maxHeight) {
|
||||||
const diff = Math.min(
|
const diff = Math.min(
|
||||||
textElementHeight - maxHeight,
|
textElementHeight - maxHeight,
|
||||||
@@ -276,7 +276,6 @@ export const textWysiwyg = ({
|
|||||||
// Make sure text editor height doesn't go beyond viewport
|
// Make sure text editor height doesn't go beyond viewport
|
||||||
const editorMaxHeight =
|
const editorMaxHeight =
|
||||||
(appState.height - viewportY) / appState.zoom.value;
|
(appState.height - viewportY) / appState.zoom.value;
|
||||||
|
|
||||||
Object.assign(editable.style, {
|
Object.assign(editable.style, {
|
||||||
font: getFontString(updatedTextElement),
|
font: getFontString(updatedTextElement),
|
||||||
// must be defined *after* font ¯\_(ツ)_/¯
|
// must be defined *after* font ¯\_(ツ)_/¯
|
||||||
@@ -395,11 +394,12 @@ export const textWysiwyg = ({
|
|||||||
// first line as well as setting height to "auto"
|
// first line as well as setting height to "auto"
|
||||||
// doubles the height as soon as user starts typing
|
// doubles the height as soon as user starts typing
|
||||||
if (isBoundToContainer(element) && lines > 1) {
|
if (isBoundToContainer(element) && lines > 1) {
|
||||||
|
const container = getContainerElement(element);
|
||||||
|
|
||||||
let height = "auto";
|
let height = "auto";
|
||||||
editable.style.height = "0px";
|
editable.style.height = "0px";
|
||||||
let heightSet = false;
|
let heightSet = false;
|
||||||
if (lines === 2) {
|
if (lines === 2) {
|
||||||
const container = getContainerElement(element);
|
|
||||||
const actualLineCount = wrapText(
|
const actualLineCount = wrapText(
|
||||||
editable.value,
|
editable.value,
|
||||||
font,
|
font,
|
||||||
@@ -416,6 +416,14 @@ export const textWysiwyg = ({
|
|||||||
heightSet = true;
|
heightSet = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const wrappedText = wrapText(
|
||||||
|
normalizeText(editable.value),
|
||||||
|
font,
|
||||||
|
getMaxContainerWidth(container!),
|
||||||
|
);
|
||||||
|
const width = getTextWidth(wrappedText, font);
|
||||||
|
editable.style.width = `${width}px`;
|
||||||
|
|
||||||
if (!heightSet) {
|
if (!heightSet) {
|
||||||
editable.style.height = `${editable.scrollHeight}px`;
|
editable.style.height = `${editable.scrollHeight}px`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,3 +38,11 @@ export const STORAGE_KEYS = {
|
|||||||
VERSION_DATA_STATE: "version-dataState",
|
VERSION_DATA_STATE: "version-dataState",
|
||||||
VERSION_FILES: "version-files",
|
VERSION_FILES: "version-files",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const COOKIES = {
|
||||||
|
AUTH_STATE_COOKIE: "excplus-auth",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const isExcalidrawPlusSignedUser = document.cookie.includes(
|
||||||
|
COOKIES.AUTH_STATE_COOKIE,
|
||||||
|
);
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Footer } from "../../packages/excalidraw/index";
|
||||||
|
import { EncryptedIcon } from "./EncryptedIcon";
|
||||||
|
import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
|
||||||
|
|
||||||
|
export const AppFooter = React.memo(() => {
|
||||||
|
return (
|
||||||
|
<Footer>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: ".5rem",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExcalidrawPlusAppLink />
|
||||||
|
<EncryptedIcon />
|
||||||
|
</div>
|
||||||
|
</Footer>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { PlusPromoIcon } from "../../components/icons";
|
||||||
|
import { MainMenu } from "../../packages/excalidraw/index";
|
||||||
|
import { LanguageList } from "./LanguageList";
|
||||||
|
|
||||||
|
export const AppMainMenu: React.FC<{
|
||||||
|
setCollabDialogShown: (toggle: boolean) => any;
|
||||||
|
isCollaborating: boolean;
|
||||||
|
}> = React.memo((props) => {
|
||||||
|
return (
|
||||||
|
<MainMenu>
|
||||||
|
<MainMenu.DefaultItems.LoadScene />
|
||||||
|
<MainMenu.DefaultItems.SaveToActiveFile />
|
||||||
|
<MainMenu.DefaultItems.Export />
|
||||||
|
<MainMenu.DefaultItems.SaveAsImage />
|
||||||
|
<MainMenu.DefaultItems.LiveCollaborationTrigger
|
||||||
|
isCollaborating={props.isCollaborating}
|
||||||
|
onSelect={() => props.setCollabDialogShown(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MainMenu.DefaultItems.Help />
|
||||||
|
<MainMenu.DefaultItems.ClearCanvas />
|
||||||
|
<MainMenu.Separator />
|
||||||
|
<MainMenu.ItemLink
|
||||||
|
icon={PlusPromoIcon}
|
||||||
|
href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger"
|
||||||
|
className="ExcalidrawPlus"
|
||||||
|
>
|
||||||
|
Excalidraw+
|
||||||
|
</MainMenu.ItemLink>
|
||||||
|
<MainMenu.DefaultItems.Socials />
|
||||||
|
<MainMenu.Separator />
|
||||||
|
<MainMenu.DefaultItems.ToggleTheme />
|
||||||
|
<MainMenu.ItemCustom>
|
||||||
|
<LanguageList style={{ width: "100%" }} />
|
||||||
|
</MainMenu.ItemCustom>
|
||||||
|
<MainMenu.DefaultItems.ChangeCanvasBackground />
|
||||||
|
</MainMenu>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { PlusPromoIcon } from "../../components/icons";
|
||||||
|
import { t } from "../../i18n";
|
||||||
|
import { WelcomeScreen } from "../../packages/excalidraw/index";
|
||||||
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
|
|
||||||
|
export const AppWelcomeScreen: React.FC<{
|
||||||
|
setCollabDialogShown: (toggle: boolean) => any;
|
||||||
|
}> = React.memo((props) => {
|
||||||
|
let headingContent;
|
||||||
|
|
||||||
|
if (isExcalidrawPlusSignedUser) {
|
||||||
|
headingContent = t("welcomeScreen.app.center_heading_plus")
|
||||||
|
.split(/(Excalidraw\+)/)
|
||||||
|
.map((bit, idx) => {
|
||||||
|
if (bit === "Excalidraw+") {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
style={{ pointerEvents: "all" }}
|
||||||
|
href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
|
||||||
|
key={idx}
|
||||||
|
>
|
||||||
|
Excalidraw+
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return bit;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
headingContent = t("welcomeScreen.app.center_heading");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WelcomeScreen>
|
||||||
|
<WelcomeScreen.Hints.MenuHint>
|
||||||
|
{t("welcomeScreen.app.menuHint")}
|
||||||
|
</WelcomeScreen.Hints.MenuHint>
|
||||||
|
<WelcomeScreen.Hints.ToolbarHint />
|
||||||
|
<WelcomeScreen.Hints.HelpHint />
|
||||||
|
<WelcomeScreen.Center>
|
||||||
|
<WelcomeScreen.Center.Logo />
|
||||||
|
<WelcomeScreen.Center.Heading>
|
||||||
|
{headingContent}
|
||||||
|
</WelcomeScreen.Center.Heading>
|
||||||
|
<WelcomeScreen.Center.Menu>
|
||||||
|
<WelcomeScreen.Center.MenuItemLoadScene />
|
||||||
|
<WelcomeScreen.Center.MenuItemHelp />
|
||||||
|
<WelcomeScreen.Center.MenuItemLiveCollaborationTrigger
|
||||||
|
onSelect={() => props.setCollabDialogShown(true)}
|
||||||
|
/>
|
||||||
|
{!isExcalidrawPlusSignedUser && (
|
||||||
|
<WelcomeScreen.Center.MenuItemLink
|
||||||
|
href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
||||||
|
shortcut={null}
|
||||||
|
icon={PlusPromoIcon}
|
||||||
|
>
|
||||||
|
Try Excalidraw Plus!
|
||||||
|
</WelcomeScreen.Center.MenuItemLink>
|
||||||
|
)}
|
||||||
|
</WelcomeScreen.Center.Menu>
|
||||||
|
</WelcomeScreen.Center>
|
||||||
|
</WelcomeScreen>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isExcalidrawPlusSignedUser } from "../../constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
|
|
||||||
export const ExcalidrawPlusAppLink = () => {
|
export const ExcalidrawPlusAppLink = () => {
|
||||||
if (!isExcalidrawPlusSignedUser) {
|
if (!isExcalidrawPlusSignedUser) {
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ import { t } from "../i18n";
|
|||||||
import {
|
import {
|
||||||
Excalidraw,
|
Excalidraw,
|
||||||
defaultLang,
|
defaultLang,
|
||||||
Footer,
|
LiveCollaborationTrigger,
|
||||||
MainMenu,
|
|
||||||
} from "../packages/excalidraw/index";
|
} from "../packages/excalidraw/index";
|
||||||
import {
|
import {
|
||||||
AppState,
|
AppState,
|
||||||
@@ -82,10 +81,9 @@ import { atom, Provider, useAtom } from "jotai";
|
|||||||
import { jotaiStore, useAtomWithInitialValue } from "../jotai";
|
import { jotaiStore, useAtomWithInitialValue } from "../jotai";
|
||||||
import { reconcileElements } from "./collab/reconciliation";
|
import { reconcileElements } from "./collab/reconciliation";
|
||||||
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
||||||
import { EncryptedIcon } from "./components/EncryptedIcon";
|
import { AppMainMenu } from "./components/AppMainMenu";
|
||||||
import { ExcalidrawPlusAppLink } from "./components/ExcalidrawPlusAppLink";
|
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
||||||
import { LanguageList } from "./components/LanguageList";
|
import { AppFooter } from "./components/AppFooter";
|
||||||
import { PlusPromoIcon } from "../components/icons";
|
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
@@ -601,39 +599,6 @@ const ExcalidrawWrapper = () => {
|
|||||||
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
|
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderMenu = () => {
|
|
||||||
return (
|
|
||||||
<MainMenu>
|
|
||||||
<MainMenu.DefaultItems.LoadScene />
|
|
||||||
<MainMenu.DefaultItems.SaveToActiveFile />
|
|
||||||
<MainMenu.DefaultItems.Export />
|
|
||||||
<MainMenu.DefaultItems.SaveAsImage />
|
|
||||||
<MainMenu.DefaultItems.LiveCollaboration
|
|
||||||
isCollaborating={isCollaborating}
|
|
||||||
onSelect={() => setCollabDialogShown(true)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MainMenu.DefaultItems.Help />
|
|
||||||
<MainMenu.DefaultItems.ClearCanvas />
|
|
||||||
<MainMenu.Separator />
|
|
||||||
<MainMenu.ItemLink
|
|
||||||
icon={PlusPromoIcon}
|
|
||||||
href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger"
|
|
||||||
className="ExcalidrawPlus"
|
|
||||||
>
|
|
||||||
Excalidraw+
|
|
||||||
</MainMenu.ItemLink>
|
|
||||||
<MainMenu.DefaultItems.Socials />
|
|
||||||
<MainMenu.Separator />
|
|
||||||
<MainMenu.DefaultItems.ToggleTheme />
|
|
||||||
<MainMenu.ItemCustom>
|
|
||||||
<LanguageList style={{ width: "100%" }} />
|
|
||||||
</MainMenu.ItemCustom>
|
|
||||||
<MainMenu.DefaultItems.ChangeCanvasBackground />
|
|
||||||
</MainMenu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ height: "100%" }}
|
style={{ height: "100%" }}
|
||||||
@@ -645,7 +610,6 @@ const ExcalidrawWrapper = () => {
|
|||||||
ref={excalidrawRefCallback}
|
ref={excalidrawRefCallback}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
initialData={initialStatePromiseRef.current.promise}
|
initialData={initialStatePromiseRef.current.promise}
|
||||||
onCollabButtonClick={() => setCollabDialogShown(true)}
|
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
onPointerUpdate={collabAPI?.onPointerUpdate}
|
onPointerUpdate={collabAPI?.onPointerUpdate}
|
||||||
UIOptions={{
|
UIOptions={{
|
||||||
@@ -679,14 +643,24 @@ const ExcalidrawWrapper = () => {
|
|||||||
onLibraryChange={onLibraryChange}
|
onLibraryChange={onLibraryChange}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
renderTopRightUI={(isMobile) => {
|
||||||
|
if (isMobile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<LiveCollaborationTrigger
|
||||||
|
isCollaborating={isCollaborating}
|
||||||
|
onSelect={() => setCollabDialogShown(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{renderMenu()}
|
<AppMainMenu
|
||||||
<Footer>
|
setCollabDialogShown={setCollabDialogShown}
|
||||||
<div style={{ display: "flex", gap: ".5rem", alignItems: "center" }}>
|
isCollaborating={isCollaborating}
|
||||||
<ExcalidrawPlusAppLink />
|
/>
|
||||||
<EncryptedIcon />
|
<AppWelcomeScreen setCollabDialogShown={setCollabDialogShown} />
|
||||||
</div>
|
<AppFooter />
|
||||||
</Footer>
|
|
||||||
</Excalidraw>
|
</Excalidraw>
|
||||||
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
|
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
|
|||||||
+1
-3
@@ -1,6 +1,4 @@
|
|||||||
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
|
import { isDarwin } from "./constants";
|
||||||
export const isWindows = /^Win/.test(window.navigator.platform);
|
|
||||||
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
|
|
||||||
|
|
||||||
export const CODES = {
|
export const CODES = {
|
||||||
EQUAL: "Equal",
|
EQUAL: "Equal",
|
||||||
|
|||||||
+19
-11
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"labels": {
|
"labels": {
|
||||||
"paste": "لصق",
|
"paste": "لصق",
|
||||||
"pasteAsPlaintext": "",
|
"pasteAsPlaintext": "اللصق كنص عادي",
|
||||||
"pasteCharts": "لصق الرسوم البيانية",
|
"pasteCharts": "لصق الرسوم البيانية",
|
||||||
"selectAll": "تحديد الكل",
|
"selectAll": "تحديد الكل",
|
||||||
"multiSelect": "إضافة عنصر للتحديد",
|
"multiSelect": "إضافة عنصر للتحديد",
|
||||||
@@ -66,13 +66,13 @@
|
|||||||
"cartoonist": "كرتوني",
|
"cartoonist": "كرتوني",
|
||||||
"fileTitle": "إسم الملف",
|
"fileTitle": "إسم الملف",
|
||||||
"colorPicker": "منتقي اللون",
|
"colorPicker": "منتقي اللون",
|
||||||
"canvasColors": "",
|
"canvasColors": "تستخدم على القماش",
|
||||||
"canvasBackground": "خلفية اللوحة",
|
"canvasBackground": "خلفية اللوحة",
|
||||||
"drawingCanvas": "لوحة الرسم",
|
"drawingCanvas": "لوحة الرسم",
|
||||||
"layers": "الطبقات",
|
"layers": "الطبقات",
|
||||||
"actions": "الإجراءات",
|
"actions": "الإجراءات",
|
||||||
"language": "اللغة",
|
"language": "اللغة",
|
||||||
"liveCollaboration": "",
|
"liveCollaboration": "التعاون المباشر...",
|
||||||
"duplicateSelection": "تكرار",
|
"duplicateSelection": "تكرار",
|
||||||
"untitled": "غير معنون",
|
"untitled": "غير معنون",
|
||||||
"name": "الاسم",
|
"name": "الاسم",
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
"excalidrawLib": "مكتبتنا",
|
"excalidrawLib": "مكتبتنا",
|
||||||
"decreaseFontSize": "تصغير حجم الخط",
|
"decreaseFontSize": "تصغير حجم الخط",
|
||||||
"increaseFontSize": "تكبير حجم الخط",
|
"increaseFontSize": "تكبير حجم الخط",
|
||||||
"unbindText": "",
|
"unbindText": "فك ربط النص",
|
||||||
"bindText": "",
|
"bindText": "",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "تعديل الرابط",
|
"edit": "تعديل الرابط",
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
"scale": "مقاس",
|
"scale": "مقاس",
|
||||||
"save": "احفظ للملف الحالي",
|
"save": "احفظ للملف الحالي",
|
||||||
"saveAs": "حفظ كـ",
|
"saveAs": "حفظ كـ",
|
||||||
"load": "",
|
"load": "فتح",
|
||||||
"getShareableLink": "احصل على رابط المشاركة",
|
"getShareableLink": "احصل على رابط المشاركة",
|
||||||
"close": "غلق",
|
"close": "غلق",
|
||||||
"selectLanguage": "اختر اللغة",
|
"selectLanguage": "اختر اللغة",
|
||||||
@@ -201,7 +201,9 @@
|
|||||||
"svgImageInsertError": "تعذر إدراج صورة SVG. يبدو أن ترميز SVG غير صحيح.",
|
"svgImageInsertError": "تعذر إدراج صورة SVG. يبدو أن ترميز SVG غير صحيح.",
|
||||||
"invalidSVGString": "SVG غير صالح.",
|
"invalidSVGString": "SVG غير صالح.",
|
||||||
"cannotResolveCollabServer": "",
|
"cannotResolveCollabServer": "",
|
||||||
"importLibraryError": ""
|
"importLibraryError": "",
|
||||||
|
"collabSaveFailed": "",
|
||||||
|
"collabSaveFailed_sizeExceeded": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "تحديد",
|
"selection": "تحديد",
|
||||||
@@ -445,10 +447,16 @@
|
|||||||
"d9480f": "برتقالي 9"
|
"d9480f": "برتقالي 9"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"data": "",
|
"app": {
|
||||||
"switchToPlusApp": "",
|
"center_heading": "",
|
||||||
"menuHints": "",
|
"center_heading_plus": "",
|
||||||
"toolbarHints": "",
|
"menuHint": ""
|
||||||
"helpHints": ""
|
},
|
||||||
|
"defaults": {
|
||||||
|
"menuHint": "",
|
||||||
|
"center_heading": "",
|
||||||
|
"toolbarHint": "",
|
||||||
|
"helpHint": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-6
@@ -201,7 +201,9 @@
|
|||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": "",
|
"invalidSVGString": "",
|
||||||
"cannotResolveCollabServer": "",
|
"cannotResolveCollabServer": "",
|
||||||
"importLibraryError": ""
|
"importLibraryError": "",
|
||||||
|
"collabSaveFailed": "",
|
||||||
|
"collabSaveFailed_sizeExceeded": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Селекция",
|
"selection": "Селекция",
|
||||||
@@ -445,10 +447,16 @@
|
|||||||
"d9480f": ""
|
"d9480f": ""
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"data": "",
|
"app": {
|
||||||
"switchToPlusApp": "",
|
"center_heading": "",
|
||||||
"menuHints": "",
|
"center_heading_plus": "",
|
||||||
"toolbarHints": "",
|
"menuHint": ""
|
||||||
"helpHints": ""
|
},
|
||||||
|
"defaults": {
|
||||||
|
"menuHint": "",
|
||||||
|
"center_heading": "",
|
||||||
|
"toolbarHint": "",
|
||||||
|
"helpHint": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-6
@@ -201,7 +201,9 @@
|
|||||||
"svgImageInsertError": "এসভীজী ছবি সন্নিবেশ করা যায়নি। এসভীজী মার্কআপটি অবৈধ মনে হচ্ছে৷",
|
"svgImageInsertError": "এসভীজী ছবি সন্নিবেশ করা যায়নি। এসভীজী মার্কআপটি অবৈধ মনে হচ্ছে৷",
|
||||||
"invalidSVGString": "এসভীজী মার্কআপটি অবৈধ মনে হচ্ছে৷",
|
"invalidSVGString": "এসভীজী মার্কআপটি অবৈধ মনে হচ্ছে৷",
|
||||||
"cannotResolveCollabServer": "কোল্যাব সার্ভারের সাথে সংযোগ করা যায়নি। পৃষ্ঠাটি পুনরায় লোড করে আবার চেষ্টা করুন।",
|
"cannotResolveCollabServer": "কোল্যাব সার্ভারের সাথে সংযোগ করা যায়নি। পৃষ্ঠাটি পুনরায় লোড করে আবার চেষ্টা করুন।",
|
||||||
"importLibraryError": "সংগ্রহ লোড করা যায়নি"
|
"importLibraryError": "সংগ্রহ লোড করা যায়নি",
|
||||||
|
"collabSaveFailed": "",
|
||||||
|
"collabSaveFailed_sizeExceeded": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "বাছাই",
|
"selection": "বাছাই",
|
||||||
@@ -445,10 +447,16 @@
|
|||||||
"d9480f": ""
|
"d9480f": ""
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"data": "",
|
"app": {
|
||||||
"switchToPlusApp": "",
|
"center_heading": "",
|
||||||
"menuHints": "",
|
"center_heading_plus": "",
|
||||||
"toolbarHints": "",
|
"menuHint": ""
|
||||||
"helpHints": ""
|
},
|
||||||
|
"defaults": {
|
||||||
|
"menuHint": "",
|
||||||
|
"center_heading": "",
|
||||||
|
"toolbarHint": "",
|
||||||
|
"helpHint": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-6
@@ -201,7 +201,9 @@
|
|||||||
"svgImageInsertError": "No ha estat possible inserir la imatge SVG. Les marques SVG semblen invàlides.",
|
"svgImageInsertError": "No ha estat possible inserir la imatge SVG. Les marques SVG semblen invàlides.",
|
||||||
"invalidSVGString": "SVG no vàlid.",
|
"invalidSVGString": "SVG no vàlid.",
|
||||||
"cannotResolveCollabServer": "No ha estat possible connectar amb el servidor collab. Si us plau recarregueu la pàgina i torneu a provar.",
|
"cannotResolveCollabServer": "No ha estat possible connectar amb el servidor collab. Si us plau recarregueu la pàgina i torneu a provar.",
|
||||||
"importLibraryError": "No s'ha pogut carregar la biblioteca"
|
"importLibraryError": "No s'ha pogut carregar la biblioteca",
|
||||||
|
"collabSaveFailed": "",
|
||||||
|
"collabSaveFailed_sizeExceeded": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selecció",
|
"selection": "Selecció",
|
||||||
@@ -445,10 +447,16 @@
|
|||||||
"d9480f": "Taronja 9"
|
"d9480f": "Taronja 9"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"data": "",
|
"app": {
|
||||||
"switchToPlusApp": "",
|
"center_heading": "",
|
||||||
"menuHints": "",
|
"center_heading_plus": "",
|
||||||
"toolbarHints": "",
|
"menuHint": ""
|
||||||
"helpHints": ""
|
},
|
||||||
|
"defaults": {
|
||||||
|
"menuHint": "",
|
||||||
|
"center_heading": "",
|
||||||
|
"toolbarHint": "",
|
||||||
|
"helpHint": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-6
@@ -201,7 +201,9 @@
|
|||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": "",
|
"invalidSVGString": "",
|
||||||
"cannotResolveCollabServer": "",
|
"cannotResolveCollabServer": "",
|
||||||
"importLibraryError": ""
|
"importLibraryError": "",
|
||||||
|
"collabSaveFailed": "",
|
||||||
|
"collabSaveFailed_sizeExceeded": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Výběr",
|
"selection": "Výběr",
|
||||||
@@ -445,10 +447,16 @@
|
|||||||
"d9480f": "Oranzova"
|
"d9480f": "Oranzova"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"data": "",
|
"app": {
|
||||||
"switchToPlusApp": "",
|
"center_heading": "",
|
||||||
"menuHints": "",
|
"center_heading_plus": "",
|
||||||
"toolbarHints": "",
|
"menuHint": ""
|
||||||
"helpHints": ""
|
},
|
||||||
|
"defaults": {
|
||||||
|
"menuHint": "",
|
||||||
|
"center_heading": "",
|
||||||
|
"toolbarHint": "",
|
||||||
|
"helpHint": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-6
@@ -201,7 +201,9 @@
|
|||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": "",
|
"invalidSVGString": "",
|
||||||
"cannotResolveCollabServer": "",
|
"cannotResolveCollabServer": "",
|
||||||
"importLibraryError": ""
|
"importLibraryError": "",
|
||||||
|
"collabSaveFailed": "",
|
||||||
|
"collabSaveFailed_sizeExceeded": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "",
|
"selection": "",
|
||||||
@@ -445,10 +447,16 @@
|
|||||||
"d9480f": ""
|
"d9480f": ""
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"data": "",
|
"app": {
|
||||||
"switchToPlusApp": "",
|
"center_heading": "",
|
||||||
"menuHints": "",
|
"center_heading_plus": "",
|
||||||
"toolbarHints": "",
|
"menuHint": ""
|
||||||
"helpHints": ""
|
},
|
||||||
|
"defaults": {
|
||||||
|
"menuHint": "",
|
||||||
|
"center_heading": "",
|
||||||
|
"toolbarHint": "",
|
||||||
|
"helpHint": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-14
@@ -99,14 +99,14 @@
|
|||||||
"flipHorizontal": "Horizontal spiegeln",
|
"flipHorizontal": "Horizontal spiegeln",
|
||||||
"flipVertical": "Vertikal spiegeln",
|
"flipVertical": "Vertikal spiegeln",
|
||||||
"viewMode": "Ansichtsmodus",
|
"viewMode": "Ansichtsmodus",
|
||||||
"toggleExportColorScheme": "Exportfarbschema umschalten",
|
"toggleExportColorScheme": "Farbschema für Export umschalten",
|
||||||
"share": "Teilen",
|
"share": "Teilen",
|
||||||
"showStroke": "Auswahl für Strichfarbe anzeigen",
|
"showStroke": "Auswahl für Strichfarbe anzeigen",
|
||||||
"showBackground": "Hintergrundfarbe auswählen",
|
"showBackground": "Hintergrundfarbe auswählen",
|
||||||
"toggleTheme": "Thema umschalten",
|
"toggleTheme": "Design umschalten",
|
||||||
"personalLib": "Persönliche Bibliothek",
|
"personalLib": "Persönliche Bibliothek",
|
||||||
"excalidrawLib": "Excalidraw-Bibliothek",
|
"excalidrawLib": "Excalidraw Bibliothek",
|
||||||
"decreaseFontSize": "Schrift verkleinern",
|
"decreaseFontSize": "Schriftgröße verkleinern",
|
||||||
"increaseFontSize": "Schrift vergrößern",
|
"increaseFontSize": "Schrift vergrößern",
|
||||||
"unbindText": "Text lösen",
|
"unbindText": "Text lösen",
|
||||||
"bindText": "Text an Container binden",
|
"bindText": "Text an Container binden",
|
||||||
@@ -161,8 +161,8 @@
|
|||||||
"resetLibrary": "Bibliothek zurücksetzen",
|
"resetLibrary": "Bibliothek zurücksetzen",
|
||||||
"createNewRoom": "Neuen Raum erstellen",
|
"createNewRoom": "Neuen Raum erstellen",
|
||||||
"fullScreen": "Vollbildanzeige",
|
"fullScreen": "Vollbildanzeige",
|
||||||
"darkMode": "Dunkler Modus",
|
"darkMode": "Dunkles Design",
|
||||||
"lightMode": "Heller Modus",
|
"lightMode": "Helles Design",
|
||||||
"zenMode": "Zen-Modus",
|
"zenMode": "Zen-Modus",
|
||||||
"exitZenMode": "Zen-Modus verlassen",
|
"exitZenMode": "Zen-Modus verlassen",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
@@ -201,7 +201,9 @@
|
|||||||
"svgImageInsertError": "SVG-Bild konnte nicht eingefügt werden. Das SVG-Markup sieht ungültig aus.",
|
"svgImageInsertError": "SVG-Bild konnte nicht eingefügt werden. Das SVG-Markup sieht ungültig aus.",
|
||||||
"invalidSVGString": "Ungültige SVG.",
|
"invalidSVGString": "Ungültige SVG.",
|
||||||
"cannotResolveCollabServer": "Konnte keine Verbindung zum Collab-Server herstellen. Bitte lade die Seite neu und versuche es erneut.",
|
"cannotResolveCollabServer": "Konnte keine Verbindung zum Collab-Server herstellen. Bitte lade die Seite neu und versuche es erneut.",
|
||||||
"importLibraryError": "Bibliothek konnte nicht geladen werden"
|
"importLibraryError": "Bibliothek konnte nicht geladen werden",
|
||||||
|
"collabSaveFailed": "Keine Speicherung in der Backend-Datenbank möglich. Wenn die Probleme weiterhin bestehen, solltest Du Deine Datei lokal speichern, um sicherzustellen, dass Du Deine Arbeit nicht verlierst.",
|
||||||
|
"collabSaveFailed_sizeExceeded": "Keine Speicherung in der Backend-Datenbank möglich, die Zeichenfläche scheint zu groß zu sein. Du solltest Deine Datei lokal speichern, um sicherzustellen, dass Du Deine Arbeit nicht verlierst."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Auswahl",
|
"selection": "Auswahl",
|
||||||
@@ -312,8 +314,8 @@
|
|||||||
"zoomToFit": "Zoomen um alle Elemente einzupassen",
|
"zoomToFit": "Zoomen um alle Elemente einzupassen",
|
||||||
"zoomToSelection": "Auf Auswahl zoomen",
|
"zoomToSelection": "Auf Auswahl zoomen",
|
||||||
"toggleElementLock": "Auswahl sperren/entsperren",
|
"toggleElementLock": "Auswahl sperren/entsperren",
|
||||||
"movePageUpDown": "",
|
"movePageUpDown": "Seite nach oben/unten verschieben",
|
||||||
"movePageLeftRight": ""
|
"movePageLeftRight": "Seite nach links/rechts verschieben"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": "Zeichenfläche löschen"
|
"title": "Zeichenfläche löschen"
|
||||||
@@ -445,10 +447,16 @@
|
|||||||
"d9480f": "Orange 9"
|
"d9480f": "Orange 9"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"data": "Alle Daten werden lokal in Deinem Browser gespeichert.",
|
"app": {
|
||||||
"switchToPlusApp": "Möchtest du stattdessen zu Excalidraw+ gehen?",
|
"center_heading": "Alle Daten werden lokal in Deinem Browser gespeichert.",
|
||||||
"menuHints": "Exportieren, Einstellungen, Sprachen, ...",
|
"center_heading_plus": "Möchtest du stattdessen zu Excalidraw+ gehen?",
|
||||||
"toolbarHints": "Wähle ein Werkzeug & beginne zu zeichnen!",
|
"menuHint": "Exportieren, Einstellungen, Sprachen, ..."
|
||||||
"helpHints": "Kurzbefehle & Hilfe"
|
},
|
||||||
|
"defaults": {
|
||||||
|
"menuHint": "Exportieren, Einstellungen und mehr...",
|
||||||
|
"center_heading": "Diagramme. Einfach. Gemacht.",
|
||||||
|
"toolbarHint": "Wähle ein Werkzeug & beginne zu zeichnen!",
|
||||||
|
"helpHint": "Kurzbefehle & Hilfe"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-18
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"labels": {
|
"labels": {
|
||||||
"paste": "Επικόλληση",
|
"paste": "Επικόλληση",
|
||||||
"pasteAsPlaintext": "",
|
"pasteAsPlaintext": "Επικόλληση ως απλό κείμενο",
|
||||||
"pasteCharts": "Επικόλληση γραφημάτων",
|
"pasteCharts": "Επικόλληση γραφημάτων",
|
||||||
"selectAll": "Επιλογή όλων",
|
"selectAll": "Επιλογή όλων",
|
||||||
"multiSelect": "Προσθέστε το στοιχείο στην επιλογή",
|
"multiSelect": "Προσθέστε το στοιχείο στην επιλογή",
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
"layers": "Στρώματα",
|
"layers": "Στρώματα",
|
||||||
"actions": "Ενέργειες",
|
"actions": "Ενέργειες",
|
||||||
"language": "Γλώσσα",
|
"language": "Γλώσσα",
|
||||||
"liveCollaboration": "",
|
"liveCollaboration": "Live συνεργασία...",
|
||||||
"duplicateSelection": "Δημιουργία αντιγράφου",
|
"duplicateSelection": "Δημιουργία αντιγράφου",
|
||||||
"untitled": "Χωρίς τίτλο",
|
"untitled": "Χωρίς τίτλο",
|
||||||
"name": "Όνομα",
|
"name": "Όνομα",
|
||||||
@@ -116,8 +116,8 @@
|
|||||||
"label": "Σύνδεσμος"
|
"label": "Σύνδεσμος"
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "",
|
"edit": "Επεξεργασία γραμμής",
|
||||||
"exit": ""
|
"exit": "Έξοδος επεξεργαστή κειμένου"
|
||||||
},
|
},
|
||||||
"elementLock": {
|
"elementLock": {
|
||||||
"lock": "Κλείδωμα",
|
"lock": "Κλείδωμα",
|
||||||
@@ -136,8 +136,8 @@
|
|||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Επαναφορά του καμβά",
|
"clearReset": "Επαναφορά του καμβά",
|
||||||
"exportJSON": "Εξαγωγή σε αρχείο",
|
"exportJSON": "Εξαγωγή σε αρχείο",
|
||||||
"exportImage": "",
|
"exportImage": "Εξαγωγή εικόνας...",
|
||||||
"export": "",
|
"export": "Αποθήκευση ως...",
|
||||||
"exportToPng": "Εξαγωγή σε PNG",
|
"exportToPng": "Εξαγωγή σε PNG",
|
||||||
"exportToSvg": "Εξαγωγή σε SVG",
|
"exportToSvg": "Εξαγωγή σε SVG",
|
||||||
"copyToClipboard": "Αντιγραφή στο πρόχειρο",
|
"copyToClipboard": "Αντιγραφή στο πρόχειρο",
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
"scale": "Κλίμακα",
|
"scale": "Κλίμακα",
|
||||||
"save": "Αποθήκευση στο τρέχον αρχείο",
|
"save": "Αποθήκευση στο τρέχον αρχείο",
|
||||||
"saveAs": "Αποθήκευση ως",
|
"saveAs": "Αποθήκευση ως",
|
||||||
"load": "",
|
"load": "Άνοιγμα",
|
||||||
"getShareableLink": "Δημόσιος σύνδεσμος",
|
"getShareableLink": "Δημόσιος σύνδεσμος",
|
||||||
"close": "Κλείσιμο",
|
"close": "Κλείσιμο",
|
||||||
"selectLanguage": "Επιλογή γλώσσας",
|
"selectLanguage": "Επιλογή γλώσσας",
|
||||||
@@ -201,7 +201,9 @@
|
|||||||
"svgImageInsertError": "Αδυναμία εισαγωγής εικόνας SVG. Η σήμανση της SVG δεν φαίνεται έγκυρη.",
|
"svgImageInsertError": "Αδυναμία εισαγωγής εικόνας SVG. Η σήμανση της SVG δεν φαίνεται έγκυρη.",
|
||||||
"invalidSVGString": "Μη έγκυρο SVG.",
|
"invalidSVGString": "Μη έγκυρο SVG.",
|
||||||
"cannotResolveCollabServer": "Αδυναμία σύνδεσης με τον διακομιστή συνεργασίας. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά.",
|
"cannotResolveCollabServer": "Αδυναμία σύνδεσης με τον διακομιστή συνεργασίας. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά.",
|
||||||
"importLibraryError": "Αδυναμία φόρτωσης βιβλιοθήκης"
|
"importLibraryError": "Αδυναμία φόρτωσης βιβλιοθήκης",
|
||||||
|
"collabSaveFailed": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή. Αν το προβλήματα παραμείνει, θα πρέπει να αποθηκεύσετε το αρχείο σας τοπικά για να βεβαιωθείτε ότι δεν χάνετε την εργασία σας.",
|
||||||
|
"collabSaveFailed_sizeExceeded": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή, ο καμβάς φαίνεται να είναι πολύ μεγάλος. Θα πρέπει να αποθηκεύσετε το αρχείο τοπικά για να βεβαιωθείτε ότι δεν θα χάσετε την εργασία σας."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Επιλογή",
|
"selection": "Επιλογή",
|
||||||
@@ -215,7 +217,7 @@
|
|||||||
"text": "Κείμενο",
|
"text": "Κείμενο",
|
||||||
"library": "Βιβλιοθήκη",
|
"library": "Βιβλιοθήκη",
|
||||||
"lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο",
|
"lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο",
|
||||||
"penMode": "",
|
"penMode": "Λειτουργία μολυβιού - αποτροπή αφής",
|
||||||
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
|
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
|
||||||
"eraser": "Γόμα"
|
"eraser": "Γόμα"
|
||||||
},
|
},
|
||||||
@@ -236,7 +238,7 @@
|
|||||||
"resize": "Μπορείς να περιορίσεις τις αναλογίες κρατώντας το SHIFT ενώ αλλάζεις μέγεθος,\nκράτησε πατημένο το ALT για αλλαγή μεγέθους από το κέντρο",
|
"resize": "Μπορείς να περιορίσεις τις αναλογίες κρατώντας το SHIFT ενώ αλλάζεις μέγεθος,\nκράτησε πατημένο το ALT για αλλαγή μεγέθους από το κέντρο",
|
||||||
"resizeImage": "Μπορείτε να αλλάξετε το μέγεθος ελεύθερα κρατώντας πατημένο το SHIFT,\nκρατήστε πατημένο το ALT για να αλλάξετε το μέγεθος από το κέντρο",
|
"resizeImage": "Μπορείτε να αλλάξετε το μέγεθος ελεύθερα κρατώντας πατημένο το SHIFT,\nκρατήστε πατημένο το ALT για να αλλάξετε το μέγεθος από το κέντρο",
|
||||||
"rotate": "Μπορείς να περιορίσεις τις γωνίες κρατώντας πατημένο το πλήκτρο SHIFT κατά την περιστροφή",
|
"rotate": "Μπορείς να περιορίσεις τις γωνίες κρατώντας πατημένο το πλήκτρο SHIFT κατά την περιστροφή",
|
||||||
"lineEditor_info": "",
|
"lineEditor_info": "Κρατήστε πατημένο Ctrl ή Cmd και πατήστε το πλήκτρο Ctrl ή Cmd + Enter για επεξεργασία σημείων",
|
||||||
"lineEditor_pointSelected": "Πατήστε Διαγραφή για αφαίρεση σημείου(ων),\nCtrlOrCmd+D για αντιγραφή, ή σύρετε για μετακίνηση",
|
"lineEditor_pointSelected": "Πατήστε Διαγραφή για αφαίρεση σημείου(ων),\nCtrlOrCmd+D για αντιγραφή, ή σύρετε για μετακίνηση",
|
||||||
"lineEditor_nothingSelected": "Επιλέξτε ένα σημείο για να επεξεργαστείτε (κρατήστε πατημένο το SHIFT για να επιλέξετε πολλαπλά),\nή κρατήστε πατημένο το Alt και κάντε κλικ για να προσθέσετε νέα σημεία",
|
"lineEditor_nothingSelected": "Επιλέξτε ένα σημείο για να επεξεργαστείτε (κρατήστε πατημένο το SHIFT για να επιλέξετε πολλαπλά),\nή κρατήστε πατημένο το Alt και κάντε κλικ για να προσθέσετε νέα σημεία",
|
||||||
"placeImage": "Κάντε κλικ για να τοποθετήσετε την εικόνα ή κάντε κλικ και σύρετε για να ορίσετε το μέγεθός της χειροκίνητα",
|
"placeImage": "Κάντε κλικ για να τοποθετήσετε την εικόνα ή κάντε κλικ και σύρετε για να ορίσετε το μέγεθός της χειροκίνητα",
|
||||||
@@ -312,8 +314,8 @@
|
|||||||
"zoomToFit": "Zoom ώστε να χωρέσουν όλα τα στοιχεία",
|
"zoomToFit": "Zoom ώστε να χωρέσουν όλα τα στοιχεία",
|
||||||
"zoomToSelection": "Ζουμ στην επιλογή",
|
"zoomToSelection": "Ζουμ στην επιλογή",
|
||||||
"toggleElementLock": "Κλείδωμα/Ξεκλείδωμα επιλογής",
|
"toggleElementLock": "Κλείδωμα/Ξεκλείδωμα επιλογής",
|
||||||
"movePageUpDown": "",
|
"movePageUpDown": "Μετακίνηση σελίδας πάνω/κάτω",
|
||||||
"movePageLeftRight": ""
|
"movePageLeftRight": "Μετακίνηση σελίδας αριστερά/δεξιά"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": "Καθαρισμός καμβά"
|
"title": "Καθαρισμός καμβά"
|
||||||
@@ -395,7 +397,7 @@
|
|||||||
"fileSavedToFilename": "Αποθηκεύτηκε στο {filename}",
|
"fileSavedToFilename": "Αποθηκεύτηκε στο {filename}",
|
||||||
"canvas": "καμβάς",
|
"canvas": "καμβάς",
|
||||||
"selection": "επιλογή",
|
"selection": "επιλογή",
|
||||||
"pasteAsSingleElement": ""
|
"pasteAsSingleElement": "Χρησιμοποίησε το {{shortcut}} για να επικολλήσεις ως ένα μόνο στοιχείο,\nή να επικολλήσεις σε έναν υπάρχοντα επεξεργαστή κειμένου"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"ffffff": "Λευκό",
|
"ffffff": "Λευκό",
|
||||||
@@ -445,10 +447,16 @@
|
|||||||
"d9480f": "Πορτοκαλί 9"
|
"d9480f": "Πορτοκαλί 9"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"data": "",
|
"app": {
|
||||||
"switchToPlusApp": "",
|
"center_heading": "Όλα τα δεδομένα σας αποθηκεύονται τοπικά στο πρόγραμμα περιήγησης.",
|
||||||
"menuHints": "",
|
"center_heading_plus": "Μήπως θέλατε να πάτε στο Excalidraw+;",
|
||||||
"toolbarHints": "",
|
"menuHint": "Εξαγωγή, προτιμήσεις, γλώσσες, ..."
|
||||||
"helpHints": ""
|
},
|
||||||
|
"defaults": {
|
||||||
|
"menuHint": "Εξαγωγή, προτιμήσεις και άλλες επιλογές...",
|
||||||
|
"center_heading": "Διαγράμματα. Εύκολα. Γρήγορα.",
|
||||||
|
"toolbarHint": "Επιλέξτε ένα εργαλείο και ξεκινήστε να σχεδιάζεται!",
|
||||||
|
"helpHint": "Συντομεύσεις και βοήθεια"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-8
@@ -220,7 +220,8 @@
|
|||||||
"lock": "Keep selected tool active after drawing",
|
"lock": "Keep selected tool active after drawing",
|
||||||
"penMode": "Pen mode - prevent touch",
|
"penMode": "Pen mode - prevent touch",
|
||||||
"link": "Add/ Update link for a selected shape",
|
"link": "Add/ Update link for a selected shape",
|
||||||
"eraser": "Eraser"
|
"eraser": "Eraser",
|
||||||
|
"hand": "Hand (panning tool)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Canvas actions",
|
"canvasActions": "Canvas actions",
|
||||||
@@ -228,7 +229,7 @@
|
|||||||
"shapes": "Shapes"
|
"shapes": "Shapes"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "To move canvas, hold mouse wheel or spacebar while dragging",
|
"canvasPanning": "To move canvas, hold mouse wheel or spacebar while dragging, or use the hand tool",
|
||||||
"linearElement": "Click to start multiple points, drag for single line",
|
"linearElement": "Click to start multiple points, drag for single line",
|
||||||
"freeDraw": "Click and drag, release when you're finished",
|
"freeDraw": "Click and drag, release when you're finished",
|
||||||
"text": "Tip: you can also add text by double-clicking anywhere with the selection tool",
|
"text": "Tip: you can also add text by double-clicking anywhere with the selection tool",
|
||||||
@@ -246,7 +247,8 @@
|
|||||||
"publishLibrary": "Publish your own library",
|
"publishLibrary": "Publish your own library",
|
||||||
"bindTextToElement": "Press enter to add text",
|
"bindTextToElement": "Press enter to add text",
|
||||||
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging",
|
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging",
|
||||||
"eraserRevert": "Hold Alt to revert the elements marked for deletion"
|
"eraserRevert": "Hold Alt to revert the elements marked for deletion",
|
||||||
|
"firefox_clipboard_write": "This feature can likely be enabled by setting the \"dom.events.asyncClipboard.clipboardItem\" flag to \"true\". To change the browser flags in Firefox, visit the \"about:config\" page."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Cannot show preview",
|
"cannotShowPreview": "Cannot show preview",
|
||||||
@@ -448,10 +450,16 @@
|
|||||||
"d9480f": "Orange 9"
|
"d9480f": "Orange 9"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"data": "All your data is saved locally in your browser.",
|
"app": {
|
||||||
"switchToPlusApp": "Did you want to go to the Excalidraw+ instead?",
|
"center_heading": "All your data is saved locally in your browser.",
|
||||||
"menuHints": "Export, preferences, languages, ...",
|
"center_heading_plus": "Did you want to go to the Excalidraw+ instead?",
|
||||||
"toolbarHints": "Pick a tool & Start drawing!",
|
"menuHint": "Export, preferences, languages, ..."
|
||||||
"helpHints": "Shortcuts & help"
|
},
|
||||||
|
"defaults": {
|
||||||
|
"menuHint": "Export, preferences, and more...",
|
||||||
|
"center_heading": "Diagrams. Made. Simple.",
|
||||||
|
"toolbarHint": "Pick a tool & Start drawing!",
|
||||||
|
"helpHint": "Shortcuts & help"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-6
@@ -201,7 +201,9 @@
|
|||||||
"svgImageInsertError": "No se pudo insertar la imagen SVG. El código SVG parece inválido.",
|
"svgImageInsertError": "No se pudo insertar la imagen SVG. El código SVG parece inválido.",
|
||||||
"invalidSVGString": "SVG no válido.",
|
"invalidSVGString": "SVG no válido.",
|
||||||
"cannotResolveCollabServer": "No se pudo conectar al servidor colaborador. Por favor, vuelva a cargar la página y vuelva a intentarlo.",
|
"cannotResolveCollabServer": "No se pudo conectar al servidor colaborador. Por favor, vuelva a cargar la página y vuelva a intentarlo.",
|
||||||
"importLibraryError": "No se pudo cargar la librería"
|
"importLibraryError": "No se pudo cargar la librería",
|
||||||
|
"collabSaveFailed": "No se pudo guardar en la base de datos del backend. Si los problemas persisten, debería guardar su archivo localmente para asegurarse de que no pierde su trabajo.",
|
||||||
|
"collabSaveFailed_sizeExceeded": "No se pudo guardar en la base de datos del backend, el lienzo parece ser demasiado grande. Debería guardar el archivo localmente para asegurarse de que no pierde su trabajo."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selección",
|
"selection": "Selección",
|
||||||
@@ -445,10 +447,16 @@
|
|||||||
"d9480f": "Naranja 9"
|
"d9480f": "Naranja 9"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"data": "Toda su información es guardada localmente en su navegador.",
|
"app": {
|
||||||
"switchToPlusApp": "¿Quieres ir a Excalidraw+ en su lugar?",
|
"center_heading": "Toda su información es guardada localmente en su navegador.",
|
||||||
"menuHints": "Exportar, preferencias, idiomas, ...",
|
"center_heading_plus": "¿Quieres ir a Excalidraw+?",
|
||||||
"toolbarHints": "¡Escoge una herramienta & Empiece a dibujar!",
|
"menuHint": "Exportar, preferencias, idiomas, ..."
|
||||||
"helpHints": "Atajos & ayuda"
|
},
|
||||||
|
"defaults": {
|
||||||
|
"menuHint": "Exportar, preferencias y más...",
|
||||||
|
"center_heading": "Diagramas. Hecho. Simplemente.",
|
||||||
|
"toolbarHint": "¡Elige una herramienta y empieza a dibujar!",
|
||||||
|
"helpHint": "Atajos & ayuda"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-11
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"labels": {
|
"labels": {
|
||||||
"paste": "Itsatsi",
|
"paste": "Itsatsi",
|
||||||
"pasteAsPlaintext": "",
|
"pasteAsPlaintext": "Itsatsi testu arrunt gisa",
|
||||||
"pasteCharts": "Itsatsi grafikoak",
|
"pasteCharts": "Itsatsi grafikoak",
|
||||||
"selectAll": "Hautatu dena",
|
"selectAll": "Hautatu dena",
|
||||||
"multiSelect": "Gehitu elementua hautapenera",
|
"multiSelect": "Gehitu elementua hautapenera",
|
||||||
@@ -201,7 +201,9 @@
|
|||||||
"svgImageInsertError": "Ezin izan da SVG irudia txertatu. SVG markak baliogabea dirudi.",
|
"svgImageInsertError": "Ezin izan da SVG irudia txertatu. SVG markak baliogabea dirudi.",
|
||||||
"invalidSVGString": "SVG baliogabea.",
|
"invalidSVGString": "SVG baliogabea.",
|
||||||
"cannotResolveCollabServer": "Ezin izan da elkarlaneko zerbitzarira konektatu. Mesedez, berriro kargatu orria eta saiatu berriro.",
|
"cannotResolveCollabServer": "Ezin izan da elkarlaneko zerbitzarira konektatu. Mesedez, berriro kargatu orria eta saiatu berriro.",
|
||||||
"importLibraryError": "Ezin izan da liburutegia kargatu"
|
"importLibraryError": "Ezin izan da liburutegia kargatu",
|
||||||
|
"collabSaveFailed": "Ezin izan da backend datu-basean gorde. Arazoak jarraitzen badu, zure fitxategia lokalean gorde beharko zenuke zure lana ez duzula galtzen ziurtatzeko.",
|
||||||
|
"collabSaveFailed_sizeExceeded": "Ezin izan da backend datu-basean gorde, ohiala handiegia dela dirudi. Fitxategia lokalean gorde beharko zenuke zure lana galtzen ez duzula ziurtatzeko."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Hautapena",
|
"selection": "Hautapena",
|
||||||
@@ -236,7 +238,7 @@
|
|||||||
"resize": "Proportzioak mantendu ditzakezu SHIFT sakatuta tamaina aldatzen duzun bitartean.\nsakatu ALT erditik tamaina aldatzeko",
|
"resize": "Proportzioak mantendu ditzakezu SHIFT sakatuta tamaina aldatzen duzun bitartean.\nsakatu ALT erditik tamaina aldatzeko",
|
||||||
"resizeImage": "Tamaina libreki alda dezakezu SHIFT sakatuta,\nsakatu ALT erditik tamaina aldatzeko",
|
"resizeImage": "Tamaina libreki alda dezakezu SHIFT sakatuta,\nsakatu ALT erditik tamaina aldatzeko",
|
||||||
"rotate": "Angeluak mantendu ditzakezu SHIFT sakatuta biratzen duzun bitartean",
|
"rotate": "Angeluak mantendu ditzakezu SHIFT sakatuta biratzen duzun bitartean",
|
||||||
"lineEditor_info": "",
|
"lineEditor_info": "Eutsi sakatuta Ctrl edo Cmd eta egin klik bikoitza edo sakatu Ctrl edo Cmd + Sartu puntuak editatzeko",
|
||||||
"lineEditor_pointSelected": "Sakatu Ezabatu puntuak kentzeko,\nKtrl+D bikoizteko, edo arrastatu mugitzeko",
|
"lineEditor_pointSelected": "Sakatu Ezabatu puntuak kentzeko,\nKtrl+D bikoizteko, edo arrastatu mugitzeko",
|
||||||
"lineEditor_nothingSelected": "Hautatu editatzeko puntu bat (SHIFT sakatuta anitz hautatzeko),\nedo eduki Alt sakatuta eta egin klik puntu berriak gehitzeko",
|
"lineEditor_nothingSelected": "Hautatu editatzeko puntu bat (SHIFT sakatuta anitz hautatzeko),\nedo eduki Alt sakatuta eta egin klik puntu berriak gehitzeko",
|
||||||
"placeImage": "Egin klik irudia kokatzeko, edo egin klik eta arrastatu bere tamaina eskuz ezartzeko",
|
"placeImage": "Egin klik irudia kokatzeko, edo egin klik eta arrastatu bere tamaina eskuz ezartzeko",
|
||||||
@@ -312,8 +314,8 @@
|
|||||||
"zoomToFit": "Egin zoom elementu guztiak ikusteko",
|
"zoomToFit": "Egin zoom elementu guztiak ikusteko",
|
||||||
"zoomToSelection": "Zooma hautapenera",
|
"zoomToSelection": "Zooma hautapenera",
|
||||||
"toggleElementLock": "Blokeatu/desbloketatu hautapena",
|
"toggleElementLock": "Blokeatu/desbloketatu hautapena",
|
||||||
"movePageUpDown": "",
|
"movePageUpDown": "Mugitu orria gora/behera",
|
||||||
"movePageLeftRight": ""
|
"movePageLeftRight": "Mugitu orria ezker/eskuin"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": "Garbitu oihala"
|
"title": "Garbitu oihala"
|
||||||
@@ -395,7 +397,7 @@
|
|||||||
"fileSavedToFilename": "{filename}-n gorde da",
|
"fileSavedToFilename": "{filename}-n gorde da",
|
||||||
"canvas": "oihala",
|
"canvas": "oihala",
|
||||||
"selection": "hautapena",
|
"selection": "hautapena",
|
||||||
"pasteAsSingleElement": ""
|
"pasteAsSingleElement": "Erabili {{shortcut}} elementu bakar gisa itsasteko,\nedo itsatsi lehendik dagoen testu-editore batean"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"ffffff": "Zuria",
|
"ffffff": "Zuria",
|
||||||
@@ -445,10 +447,16 @@
|
|||||||
"d9480f": "Laranja 9"
|
"d9480f": "Laranja 9"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"data": "Zure datu guztiak modu lokalean gordetzen dira zure nabigatzailean.",
|
"app": {
|
||||||
"switchToPlusApp": "Horren ordez Excalidraw+-ra joan nahi al zenuen?",
|
"center_heading": "Zure datu guztiak lokalean gordetzen dira zure nabigatzailean.",
|
||||||
"menuHints": "Esportatu, hobespenak, hizkuntzak,...",
|
"center_heading_plus": "Horren ordez Excalidraw+-era joan nahi al zenuen?",
|
||||||
"toolbarHints": "Aukeratu tresna bat eta hasi marrazten!",
|
"menuHint": "Esportatu, hobespenak, hizkuntzak..."
|
||||||
"helpHints": "Lasterbideak eta laguntza"
|
},
|
||||||
|
"defaults": {
|
||||||
|
"menuHint": "Esportatu, hobespenak eta gehiago...",
|
||||||
|
"center_heading": "Diagramak. Egina. Sinplea.",
|
||||||
|
"toolbarHint": "Aukeratu tresna bat eta hasi marrazten!",
|
||||||
|
"helpHint": "Lasterbideak eta laguntza"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-6
@@ -201,7 +201,9 @@
|
|||||||
"svgImageInsertError": "تصویر SVG وارد نشد. نشانه گذاری SVG نامعتبر به نظر می رسد.",
|
"svgImageInsertError": "تصویر SVG وارد نشد. نشانه گذاری SVG نامعتبر به نظر می رسد.",
|
||||||
"invalidSVGString": "SVG نادرست.",
|
"invalidSVGString": "SVG نادرست.",
|
||||||
"cannotResolveCollabServer": "به سرور collab متصل نشد. لطفا صفحه را مجددا بارگذاری کنید و دوباره تلاش کنید.",
|
"cannotResolveCollabServer": "به سرور collab متصل نشد. لطفا صفحه را مجددا بارگذاری کنید و دوباره تلاش کنید.",
|
||||||
"importLibraryError": "دادهها بارگذاری نشدند"
|
"importLibraryError": "دادهها بارگذاری نشدند",
|
||||||
|
"collabSaveFailed": "",
|
||||||
|
"collabSaveFailed_sizeExceeded": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "گزینش",
|
"selection": "گزینش",
|
||||||
@@ -445,10 +447,16 @@
|
|||||||
"d9480f": "نارنجی 9"
|
"d9480f": "نارنجی 9"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"data": "همه ی داده های شما به صورت محلی در مرورگر ذخیره میشود.",
|
"app": {
|
||||||
"switchToPlusApp": "آیا ترجیح میدهید به Excalidraw+ بروید؟",
|
"center_heading": "",
|
||||||
"menuHints": "خروجی گرفتن، تنظیمات، زبانها، ...",
|
"center_heading_plus": "",
|
||||||
"toolbarHints": "یک ابزار را انتخاب کنید و ترسیم را شروع کنید!",
|
"menuHint": ""
|
||||||
"helpHints": "میانبرها و کمک"
|
},
|
||||||
|
"defaults": {
|
||||||
|
"menuHint": "",
|
||||||
|
"center_heading": "",
|
||||||
|
"toolbarHint": "",
|
||||||
|
"helpHint": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-6
@@ -201,7 +201,9 @@
|
|||||||
"svgImageInsertError": "SVG- kuvaa ei voitu lisätä. Tiedoston SVG-sisältö näyttää virheelliseltä.",
|
"svgImageInsertError": "SVG- kuvaa ei voitu lisätä. Tiedoston SVG-sisältö näyttää virheelliseltä.",
|
||||||
"invalidSVGString": "Virheellinen SVG.",
|
"invalidSVGString": "Virheellinen SVG.",
|
||||||
"cannotResolveCollabServer": "Yhteyden muodostaminen collab-palvelimeen epäonnistui. Virkistä sivu ja yritä uudelleen.",
|
"cannotResolveCollabServer": "Yhteyden muodostaminen collab-palvelimeen epäonnistui. Virkistä sivu ja yritä uudelleen.",
|
||||||
"importLibraryError": ""
|
"importLibraryError": "",
|
||||||
|
"collabSaveFailed": "",
|
||||||
|
"collabSaveFailed_sizeExceeded": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Valinta",
|
"selection": "Valinta",
|
||||||
@@ -445,10 +447,16 @@
|
|||||||
"d9480f": "Oranssi 9"
|
"d9480f": "Oranssi 9"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"data": "",
|
"app": {
|
||||||
"switchToPlusApp": "",
|
"center_heading": "",
|
||||||
"menuHints": "",
|
"center_heading_plus": "",
|
||||||
"toolbarHints": "",
|
"menuHint": ""
|
||||||
"helpHints": ""
|
},
|
||||||
|
"defaults": {
|
||||||
|
"menuHint": "",
|
||||||
|
"center_heading": "",
|
||||||
|
"toolbarHint": "",
|
||||||
|
"helpHint": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-11
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"labels": {
|
"labels": {
|
||||||
"paste": "Coller",
|
"paste": "Coller",
|
||||||
"pasteAsPlaintext": "",
|
"pasteAsPlaintext": "Coller comme texte brut",
|
||||||
"pasteCharts": "Coller les graphiques",
|
"pasteCharts": "Coller les graphiques",
|
||||||
"selectAll": "Tout sélectionner",
|
"selectAll": "Tout sélectionner",
|
||||||
"multiSelect": "Ajouter l'élément à la sélection",
|
"multiSelect": "Ajouter l'élément à la sélection",
|
||||||
@@ -201,7 +201,9 @@
|
|||||||
"svgImageInsertError": "Impossible d'insérer l'image SVG. Le balisage SVG semble invalide.",
|
"svgImageInsertError": "Impossible d'insérer l'image SVG. Le balisage SVG semble invalide.",
|
||||||
"invalidSVGString": "SVG invalide.",
|
"invalidSVGString": "SVG invalide.",
|
||||||
"cannotResolveCollabServer": "Impossible de se connecter au serveur collaboratif. Veuillez recharger la page et réessayer.",
|
"cannotResolveCollabServer": "Impossible de se connecter au serveur collaboratif. Veuillez recharger la page et réessayer.",
|
||||||
"importLibraryError": "Impossible de charger la bibliothèque"
|
"importLibraryError": "Impossible de charger la bibliothèque",
|
||||||
|
"collabSaveFailed": "Impossible d'enregistrer dans la base de données en arrière-plan. Si des problèmes persistent, vous devriez enregistrer votre fichier localement pour vous assurer de ne pas perdre votre travail.",
|
||||||
|
"collabSaveFailed_sizeExceeded": "Impossible d'enregistrer dans la base de données en arrière-plan, le tableau semble trop grand. Vous devriez enregistrer le fichier localement pour vous assurer de ne pas perdre votre travail."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Sélection",
|
"selection": "Sélection",
|
||||||
@@ -236,7 +238,7 @@
|
|||||||
"resize": "Vous pouvez conserver les proportions en maintenant la touche MAJ pendant le redimensionnement, maintenez la touche ALT pour redimensionner par rapport au centre",
|
"resize": "Vous pouvez conserver les proportions en maintenant la touche MAJ pendant le redimensionnement, maintenez la touche ALT pour redimensionner par rapport au centre",
|
||||||
"resizeImage": "Vous pouvez redimensionner librement en maintenant SHIFT,\nmaintenez ALT pour redimensionner depuis le centre",
|
"resizeImage": "Vous pouvez redimensionner librement en maintenant SHIFT,\nmaintenez ALT pour redimensionner depuis le centre",
|
||||||
"rotate": "Vous pouvez restreindre les angles en maintenant MAJ pendant la rotation",
|
"rotate": "Vous pouvez restreindre les angles en maintenant MAJ pendant la rotation",
|
||||||
"lineEditor_info": "",
|
"lineEditor_info": "Maintenez CtrlOrCmd et Double-cliquez ou appuyez sur CtrlOrCmd + Entrée pour modifier les points",
|
||||||
"lineEditor_pointSelected": "Appuyer sur Suppr. pour supprimer des points, Ctrl ou Cmd+D pour dupliquer, ou faire glisser pour déplacer",
|
"lineEditor_pointSelected": "Appuyer sur Suppr. pour supprimer des points, Ctrl ou Cmd+D pour dupliquer, ou faire glisser pour déplacer",
|
||||||
"lineEditor_nothingSelected": "Sélectionner un point pour éditer (maintenir la touche MAJ pour en sélectionner plusieurs),\nou maintenir la touche Alt enfoncée et cliquer pour ajouter de nouveaux points",
|
"lineEditor_nothingSelected": "Sélectionner un point pour éditer (maintenir la touche MAJ pour en sélectionner plusieurs),\nou maintenir la touche Alt enfoncée et cliquer pour ajouter de nouveaux points",
|
||||||
"placeImage": "Cliquez pour placer l'image, ou cliquez et faites glisser pour définir sa taille manuellement",
|
"placeImage": "Cliquez pour placer l'image, ou cliquez et faites glisser pour définir sa taille manuellement",
|
||||||
@@ -312,8 +314,8 @@
|
|||||||
"zoomToFit": "Zoomer pour voir tous les éléments",
|
"zoomToFit": "Zoomer pour voir tous les éléments",
|
||||||
"zoomToSelection": "Zoomer sur la sélection",
|
"zoomToSelection": "Zoomer sur la sélection",
|
||||||
"toggleElementLock": "Verrouiller/déverrouiller la sélection",
|
"toggleElementLock": "Verrouiller/déverrouiller la sélection",
|
||||||
"movePageUpDown": "",
|
"movePageUpDown": "Déplacer la page vers le haut/bas",
|
||||||
"movePageLeftRight": ""
|
"movePageLeftRight": "Déplacer la page vers la gauche/droite"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": "Effacer la zone de dessin"
|
"title": "Effacer la zone de dessin"
|
||||||
@@ -395,7 +397,7 @@
|
|||||||
"fileSavedToFilename": "Enregistré sous {filename}",
|
"fileSavedToFilename": "Enregistré sous {filename}",
|
||||||
"canvas": "canevas",
|
"canvas": "canevas",
|
||||||
"selection": "sélection",
|
"selection": "sélection",
|
||||||
"pasteAsSingleElement": ""
|
"pasteAsSingleElement": "Utiliser {{shortcut}} pour coller comme un seul élément,\nou coller dans un éditeur de texte existant"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"ffffff": "Blanc",
|
"ffffff": "Blanc",
|
||||||
@@ -445,10 +447,16 @@
|
|||||||
"d9480f": "Orange 9"
|
"d9480f": "Orange 9"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"data": "Toutes vos données sont sauvegardées en local dans votre navigateur.",
|
"app": {
|
||||||
"switchToPlusApp": "Vous vouliez plutôt aller à Excalidraw+ ?",
|
"center_heading": "Toutes vos données sont sauvegardées en local dans votre navigateur.",
|
||||||
"menuHints": "Exportation, préférences, langues, ...",
|
"center_heading_plus": "Vouliez-vous plutôt aller à Excalidraw+ à la place ?",
|
||||||
"toolbarHints": "Choisissez un outil et commencez à dessiner !",
|
"menuHint": "Exportation, préférences, langues, ..."
|
||||||
"helpHints": "Raccourcis et aide"
|
},
|
||||||
|
"defaults": {
|
||||||
|
"menuHint": "Exportation, préférences et plus...",
|
||||||
|
"center_heading": "Diagrammes. Rendus. Simples.",
|
||||||
|
"toolbarHint": "Choisissez un outil et commencez à dessiner !",
|
||||||
|
"helpHint": "Raccourcis et aide"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-11
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"labels": {
|
"labels": {
|
||||||
"paste": "Pegar",
|
"paste": "Pegar",
|
||||||
"pasteAsPlaintext": "",
|
"pasteAsPlaintext": "Pegar coma texto sen formato",
|
||||||
"pasteCharts": "Pegar gráficos",
|
"pasteCharts": "Pegar gráficos",
|
||||||
"selectAll": "Seleccionar todo",
|
"selectAll": "Seleccionar todo",
|
||||||
"multiSelect": "Engadir elemento á selección",
|
"multiSelect": "Engadir elemento á selección",
|
||||||
@@ -201,7 +201,9 @@
|
|||||||
"svgImageInsertError": "Non se puido inserir como imaxe SVG. O marcado SVG semella inválido.",
|
"svgImageInsertError": "Non se puido inserir como imaxe SVG. O marcado SVG semella inválido.",
|
||||||
"invalidSVGString": "SVG inválido.",
|
"invalidSVGString": "SVG inválido.",
|
||||||
"cannotResolveCollabServer": "Non se puido conectar ao servidor de colaboración. Por favor recargue a páxina e probe de novo.",
|
"cannotResolveCollabServer": "Non se puido conectar ao servidor de colaboración. Por favor recargue a páxina e probe de novo.",
|
||||||
"importLibraryError": "Non se puido cargar a biblioteca"
|
"importLibraryError": "Non se puido cargar a biblioteca",
|
||||||
|
"collabSaveFailed": "Non se puido gardar na base de datos. Se o problema persiste, deberías gardar o teu arquivo de maneira local para asegurarte de non perdelo teu traballo.",
|
||||||
|
"collabSaveFailed_sizeExceeded": "Non se puido gardar na base de datos, o lenzo semella demasiado grande. Deberías gardar o teu arquivo de maneira local para asegurarte de non perdelo teu traballo."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selección",
|
"selection": "Selección",
|
||||||
@@ -236,7 +238,7 @@
|
|||||||
"resize": "Pode reducir as proporcións mantendo SHIFT mentres axusta o tamaño,\nmanteña ALT para axustalo dende o centro",
|
"resize": "Pode reducir as proporcións mantendo SHIFT mentres axusta o tamaño,\nmanteña ALT para axustalo dende o centro",
|
||||||
"resizeImage": "Pode axustar o tamaño libremente mantendo SHIFT,\nmanteña ALT para axustalo dende o centro",
|
"resizeImage": "Pode axustar o tamaño libremente mantendo SHIFT,\nmanteña ALT para axustalo dende o centro",
|
||||||
"rotate": "Podes reducir os ángulos mantendo SHIFT mentres os rotas",
|
"rotate": "Podes reducir os ángulos mantendo SHIFT mentres os rotas",
|
||||||
"lineEditor_info": "",
|
"lineEditor_info": "Manteña pulsado CtrlOrCmd e faga dobre clic ou prema CtrlOrCmd + Enter para editar puntos",
|
||||||
"lineEditor_pointSelected": "Prema Suprimir para eliminar o(s) punto(s)\nCtrlOrCmd+D para duplicalos, ou arrastre para movelos",
|
"lineEditor_pointSelected": "Prema Suprimir para eliminar o(s) punto(s)\nCtrlOrCmd+D para duplicalos, ou arrastre para movelos",
|
||||||
"lineEditor_nothingSelected": "Seleccione un punto para editar (manteña pulsado SHIFT para selección múltiple),\nou manteña pulsado Alt e faga clic para engadir novos puntos",
|
"lineEditor_nothingSelected": "Seleccione un punto para editar (manteña pulsado SHIFT para selección múltiple),\nou manteña pulsado Alt e faga clic para engadir novos puntos",
|
||||||
"placeImage": "Faga clic para colocar a imaxe, ou faga clic e arrastre para establecer o seu tamaño manualmente",
|
"placeImage": "Faga clic para colocar a imaxe, ou faga clic e arrastre para establecer o seu tamaño manualmente",
|
||||||
@@ -312,8 +314,8 @@
|
|||||||
"zoomToFit": "Zoom que se axuste a todos os elementos",
|
"zoomToFit": "Zoom que se axuste a todos os elementos",
|
||||||
"zoomToSelection": "Zoom á selección",
|
"zoomToSelection": "Zoom á selección",
|
||||||
"toggleElementLock": "Bloquear/desbloquear selección",
|
"toggleElementLock": "Bloquear/desbloquear selección",
|
||||||
"movePageUpDown": "",
|
"movePageUpDown": "Mover páxina cara enriba/abaixo",
|
||||||
"movePageLeftRight": ""
|
"movePageLeftRight": "Mover páxina cara a esquerda/dereita"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": "Limpar lenzo"
|
"title": "Limpar lenzo"
|
||||||
@@ -395,7 +397,7 @@
|
|||||||
"fileSavedToFilename": "Gardado en {filename}",
|
"fileSavedToFilename": "Gardado en {filename}",
|
||||||
"canvas": "lenzo",
|
"canvas": "lenzo",
|
||||||
"selection": "selección",
|
"selection": "selección",
|
||||||
"pasteAsSingleElement": ""
|
"pasteAsSingleElement": "Usa {{shortcut}} para pegar como un único elemento\nou pega nun editor de texto existente"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"ffffff": "Branco",
|
"ffffff": "Branco",
|
||||||
@@ -445,10 +447,16 @@
|
|||||||
"d9480f": "Laranxa 9"
|
"d9480f": "Laranxa 9"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"data": "Toda a información é gardada de maneira local no seu navegador.",
|
"app": {
|
||||||
"switchToPlusApp": "Queres ir a Excalidraw+ no seu lugar?",
|
"center_heading": "Toda a información é gardada de maneira local no seu navegador.",
|
||||||
"menuHints": "Exportar, preferencias, idiomas, ...",
|
"center_heading_plus": "Queres ir a Excalidraw+ no seu lugar?",
|
||||||
"toolbarHints": "Escolle unha ferramenta & Comeza a debuxar!",
|
"menuHint": "Exportar, preferencias, idiomas, ..."
|
||||||
"helpHints": "Atallos & axuda"
|
},
|
||||||
|
"defaults": {
|
||||||
|
"menuHint": "Exportar, preferencias, e máis...",
|
||||||
|
"center_heading": "Diagramas. Feito. Sinxelo.",
|
||||||
|
"toolbarHint": "Escolle unha ferramenta & Comeza a debuxar!",
|
||||||
|
"helpHint": "Atallos & axuda"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user