mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-10-16 12:40:51 +02:00
Compare commits
30 Commits
dwelle/bum
...
feat-actio
Author | SHA1 | Date | |
---|---|---|---|
![]() |
091123286b | ||
![]() |
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,2 +1,2 @@
|
||||
#!/bin/sh
|
||||
yarn lint-staged
|
||||
# yarn lint-staged
|
||||
|
@@ -4692,9 +4692,9 @@ json-schema-traverse@^1.0.0:
|
||||
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
|
||||
|
||||
json5@^2.1.2, json5@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
|
||||
integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
|
||||
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
|
||||
|
||||
jsonfile@^6.0.1:
|
||||
version "6.1.0"
|
||||
|
@@ -28,6 +28,7 @@
|
||||
"@types/pica": "5.1.3",
|
||||
"@types/react": "18.0.15",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"@types/socket.io-client": "1.4.36",
|
||||
"browser-fs-access": "0.29.1",
|
||||
"clsx": "1.1.1",
|
||||
"cross-env": "7.0.3",
|
||||
@@ -53,7 +54,7 @@
|
||||
"react-scripts": "5.0.1",
|
||||
"roughjs": "4.5.2",
|
||||
"sass": "1.51.0",
|
||||
"socket.io-client": "4.5.4",
|
||||
"socket.io-client": "2.3.1",
|
||||
"typescript": "4.9.4",
|
||||
"workbox-background-sync": "^6.5.4",
|
||||
"workbox-broadcast-update": "^6.5.4",
|
||||
|
@@ -26,7 +26,7 @@ export const actionUnbindText = register({
|
||||
name: "unbindText",
|
||||
contextItemLabel: "labels.unbindText",
|
||||
trackEvent: { category: "element" },
|
||||
contextItemPredicate: (elements, appState) => {
|
||||
predicate: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
return selectedElements.some((element) => hasBoundTextElement(element));
|
||||
},
|
||||
@@ -76,7 +76,7 @@ export const actionBindText = register({
|
||||
name: "bindText",
|
||||
contextItemLabel: "labels.bindText",
|
||||
trackEvent: { category: "element" },
|
||||
contextItemPredicate: (elements, appState) => {
|
||||
predicate: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
|
||||
if (selectedElements.length === 2) {
|
||||
|
@@ -1,13 +1,7 @@
|
||||
import { ColorPicker } from "../components/ColorPicker";
|
||||
import {
|
||||
eraser,
|
||||
MoonIcon,
|
||||
SunIcon,
|
||||
ZoomInIcon,
|
||||
ZoomOutIcon,
|
||||
} from "../components/icons";
|
||||
import { ZoomInIcon, ZoomOutIcon } from "../components/icons";
|
||||
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 { ExcalidrawElement } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
@@ -16,19 +10,25 @@ import { getNormalizedZoom, getSelectedElements } from "../scene";
|
||||
import { centerScrollOn } from "../scene/scroll";
|
||||
import { getStateForZoom } from "../scene/zoom";
|
||||
import { AppState, NormalizedZoomValue } from "../types";
|
||||
import { getShortcutKey, updateActiveTool } from "../utils";
|
||||
import { getShortcutKey, setCursor, updateActiveTool } from "../utils";
|
||||
import { register } from "./register";
|
||||
import { Tooltip } from "../components/Tooltip";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import { getDefaultAppState, isEraserActive } from "../appState";
|
||||
import ClearCanvas from "../components/ClearCanvas";
|
||||
import clsx from "clsx";
|
||||
import DropdownMenuItem from "../components/dropdownMenu/DropdownMenuItem";
|
||||
import { getShortcutFromShortcutName } from "./shortcuts";
|
||||
import {
|
||||
getDefaultAppState,
|
||||
isEraserActive,
|
||||
isHandToolActive,
|
||||
} from "../appState";
|
||||
|
||||
export const actionChangeViewBackgroundColor = register({
|
||||
name: "changeViewBackgroundColor",
|
||||
trackEvent: false,
|
||||
predicate: (elements, appState, props, app) => {
|
||||
return (
|
||||
!!app.props.UIOptions.canvasActions.changeViewBackgroundColor &&
|
||||
!appState.viewModeEnabled
|
||||
);
|
||||
},
|
||||
perform: (_, appState, value) => {
|
||||
return {
|
||||
appState: { ...appState, ...value },
|
||||
@@ -36,6 +36,7 @@ export const actionChangeViewBackgroundColor = register({
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => {
|
||||
// FIXME move me to src/components/mainMenu/DefaultItems.tsx
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<ColorPicker
|
||||
@@ -59,6 +60,12 @@ export const actionChangeViewBackgroundColor = register({
|
||||
export const actionClearCanvas = register({
|
||||
name: "clearCanvas",
|
||||
trackEvent: { category: "canvas" },
|
||||
predicate: (elements, appState, props, app) => {
|
||||
return (
|
||||
!!app.props.UIOptions.canvasActions.clearCanvas &&
|
||||
!appState.viewModeEnabled
|
||||
);
|
||||
},
|
||||
perform: (elements, appState, _, app) => {
|
||||
app.imageCache.clear();
|
||||
return {
|
||||
@@ -84,8 +91,6 @@ export const actionClearCanvas = register({
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
|
||||
PanelComponent: ({ updateData }) => <ClearCanvas onConfirm={updateData} />,
|
||||
});
|
||||
|
||||
export const actionZoomIn = register({
|
||||
@@ -298,37 +303,21 @@ export const actionToggleTheme = register({
|
||||
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,
|
||||
predicate: (elements, appState, props, app) => {
|
||||
return !!app.props.UIOptions.canvasActions.toggleTheme;
|
||||
},
|
||||
});
|
||||
|
||||
export const actionErase = register({
|
||||
name: "eraser",
|
||||
export const actionToggleEraserTool = register({
|
||||
name: "toggleEraserTool",
|
||||
trackEvent: { category: "toolbar" },
|
||||
perform: (elements, appState) => {
|
||||
let activeTool: AppState["activeTool"];
|
||||
|
||||
if (isEraserActive(appState)) {
|
||||
activeTool = updateActiveTool(appState, {
|
||||
...(appState.activeTool.lastActiveToolBeforeEraser || {
|
||||
...(appState.activeTool.lastActiveTool || {
|
||||
type: "selection",
|
||||
}),
|
||||
lastActiveToolBeforeEraser: null,
|
||||
@@ -351,17 +340,38 @@ export const actionErase = register({
|
||||
};
|
||||
},
|
||||
keyTest: (event) => event.key === KEYS.E,
|
||||
PanelComponent: ({ elements, appState, updateData, data }) => (
|
||||
<ToolButton
|
||||
type="button"
|
||||
icon={eraser}
|
||||
className={clsx("eraser", { active: isEraserActive(appState) })}
|
||||
title={`${t("toolBar.eraser")}-${getShortcutKey("E")}`}
|
||||
aria-label={t("toolBar.eraser")}
|
||||
onClick={() => {
|
||||
updateData(null);
|
||||
}}
|
||||
size={data?.size || "medium"}
|
||||
></ToolButton>
|
||||
),
|
||||
});
|
||||
|
||||
export const actionToggleHandTool = register({
|
||||
name: "toggleHandTool",
|
||||
trackEvent: { category: "toolbar" },
|
||||
perform: (elements, appState, _, app) => {
|
||||
let activeTool: AppState["activeTool"];
|
||||
|
||||
if (isHandToolActive(appState)) {
|
||||
activeTool = updateActiveTool(appState, {
|
||||
...(appState.activeTool.lastActiveTool || {
|
||||
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,
|
||||
};
|
||||
},
|
||||
contextItemPredicate: (elements, appState, appProps, app) => {
|
||||
predicate: (elements, appState, appProps, app) => {
|
||||
return app.device.isMobile && !!navigator.clipboard;
|
||||
},
|
||||
contextItemLabel: "labels.copy",
|
||||
@@ -41,7 +41,7 @@ export const actionPaste = register({
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
contextItemPredicate: (elements, appState, appProps, app) => {
|
||||
predicate: (elements, appState, appProps, app) => {
|
||||
return app.device.isMobile && !!navigator.clipboard;
|
||||
},
|
||||
contextItemLabel: "labels.paste",
|
||||
@@ -56,7 +56,7 @@ export const actionCut = register({
|
||||
actionCopy.perform(elements, appState, data, app);
|
||||
return actionDeleteSelected.perform(elements, appState);
|
||||
},
|
||||
contextItemPredicate: (elements, appState, appProps, app) => {
|
||||
predicate: (elements, appState, appProps, app) => {
|
||||
return app.device.isMobile && !!navigator.clipboard;
|
||||
},
|
||||
contextItemLabel: "labels.cut",
|
||||
@@ -101,7 +101,7 @@ export const actionCopyAsSvg = register({
|
||||
};
|
||||
}
|
||||
},
|
||||
contextItemPredicate: (elements) => {
|
||||
predicate: (elements) => {
|
||||
return probablySupportsClipboardWriteText && elements.length > 0;
|
||||
},
|
||||
contextItemLabel: "labels.copyAsSvg",
|
||||
@@ -158,7 +158,7 @@ export const actionCopyAsPng = register({
|
||||
};
|
||||
}
|
||||
},
|
||||
contextItemPredicate: (elements) => {
|
||||
predicate: (elements) => {
|
||||
return probablySupportsClipboardBlob && elements.length > 0;
|
||||
},
|
||||
contextItemLabel: "labels.copyAsPng",
|
||||
@@ -188,7 +188,7 @@ export const copyText = register({
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
contextItemPredicate: (elements, appState) => {
|
||||
predicate: (elements, appState) => {
|
||||
return (
|
||||
probablySupportsClipboardWriteText &&
|
||||
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 { ToolButton } from "../components/ToolButton";
|
||||
import "../components/ToolIcon.scss";
|
||||
import { Tooltip } from "../components/Tooltip";
|
||||
import { DarkModeToggle } from "../components/DarkModeToggle";
|
||||
import { loadFromJSON, saveAsJSON } from "../data";
|
||||
@@ -15,12 +14,11 @@ import { getExportSize } from "../scene/export";
|
||||
import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES, THEME } from "../constants";
|
||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { ActiveFile } from "../components/ActiveFile";
|
||||
import { isImageFileHandle } from "../data/blob";
|
||||
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||
import { Theme } from "../element/types";
|
||||
import DropdownMenuItem from "../components/dropdownMenu/DropdownMenuItem";
|
||||
import { getShortcutFromShortcutName } from "./shortcuts";
|
||||
|
||||
import "../components/ToolIcon.scss";
|
||||
|
||||
export const actionChangeProjectName = register({
|
||||
name: "changeProjectName",
|
||||
@@ -133,6 +131,13 @@ export const actionChangeExportEmbedScene = register({
|
||||
export const actionSaveToActiveFile = register({
|
||||
name: "saveToActiveFile",
|
||||
trackEvent: { category: "export" },
|
||||
predicate: (elements, appState, props, app) => {
|
||||
return (
|
||||
!!app.props.UIOptions.canvasActions.saveToActiveFile &&
|
||||
!!appState.fileHandle &&
|
||||
!appState.viewModeEnabled
|
||||
);
|
||||
},
|
||||
perform: async (elements, appState, value, app) => {
|
||||
const fileHandleExists = !!appState.fileHandle;
|
||||
|
||||
@@ -169,12 +174,6 @@ export const actionSaveToActiveFile = register({
|
||||
},
|
||||
keyTest: (event) =>
|
||||
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({
|
||||
@@ -220,6 +219,11 @@ export const actionSaveFileToDisk = register({
|
||||
export const actionLoadScene = register({
|
||||
name: "loadScene",
|
||||
trackEvent: { category: "export" },
|
||||
predicate: (elements, appState, props, app) => {
|
||||
return (
|
||||
!!app.props.UIOptions.canvasActions.loadScene && !appState.viewModeEnabled
|
||||
);
|
||||
},
|
||||
perform: async (elements, appState, _, app) => {
|
||||
try {
|
||||
const {
|
||||
@@ -247,19 +251,6 @@ export const actionLoadScene = register({
|
||||
}
|
||||
},
|
||||
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({
|
||||
|
@@ -145,7 +145,7 @@ export const actionFinalize = register({
|
||||
let activeTool: AppState["activeTool"];
|
||||
if (appState.activeTool.type === "eraser") {
|
||||
activeTool = updateActiveTool(appState, {
|
||||
...(appState.activeTool.lastActiveToolBeforeEraser || {
|
||||
...(appState.activeTool.lastActiveTool || {
|
||||
type: "selection",
|
||||
}),
|
||||
lastActiveToolBeforeEraser: null,
|
||||
|
@@ -50,7 +50,7 @@ export const actionFlipHorizontal = register({
|
||||
},
|
||||
keyTest: (event) => event.shiftKey && event.code === "KeyH",
|
||||
contextItemLabel: "labels.flipHorizontal",
|
||||
contextItemPredicate: (elements, appState) =>
|
||||
predicate: (elements, appState) =>
|
||||
enableActionFlipHorizontal(elements, appState),
|
||||
});
|
||||
|
||||
@@ -67,7 +67,7 @@ export const actionFlipVertical = register({
|
||||
keyTest: (event) =>
|
||||
event.shiftKey && event.code === "KeyV" && !event[KEYS.CTRL_OR_CMD],
|
||||
contextItemLabel: "labels.flipVertical",
|
||||
contextItemPredicate: (elements, appState) =>
|
||||
predicate: (elements, appState) =>
|
||||
enableActionFlipVertical(elements, appState),
|
||||
});
|
||||
|
||||
|
@@ -129,8 +129,7 @@ export const actionGroup = register({
|
||||
};
|
||||
},
|
||||
contextItemLabel: "labels.group",
|
||||
contextItemPredicate: (elements, appState) =>
|
||||
enableActionGroup(elements, appState),
|
||||
predicate: (elements, appState) => enableActionGroup(elements, appState),
|
||||
keyTest: (event) =>
|
||||
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
@@ -193,8 +192,7 @@ export const actionUngroup = register({
|
||||
event[KEYS.CTRL_OR_CMD] &&
|
||||
event.key === KEYS.G.toUpperCase(),
|
||||
contextItemLabel: "labels.ungroup",
|
||||
contextItemPredicate: (elements, appState) =>
|
||||
getSelectedGroupIds(appState).length > 0,
|
||||
predicate: (elements, appState) => getSelectedGroupIds(appState).length > 0,
|
||||
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<ToolButton
|
||||
|
@@ -5,10 +5,11 @@ import { t } from "../i18n";
|
||||
import History, { HistoryEntry } from "../history";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppState } from "../types";
|
||||
import { isWindows, KEYS } from "../keys";
|
||||
import { KEYS } from "../keys";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import { fixBindingsAfterDeletion } from "../element/binding";
|
||||
import { arrayToMap } from "../utils";
|
||||
import { isWindows } from "../constants";
|
||||
|
||||
const writeData = (
|
||||
prevElements: readonly ExcalidrawElement[],
|
||||
|
@@ -10,7 +10,7 @@ export const actionToggleLinearEditor = register({
|
||||
trackEvent: {
|
||||
category: "element",
|
||||
},
|
||||
contextItemPredicate: (elements, appState) => {
|
||||
predicate: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
||||
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 { t } from "../i18n";
|
||||
import { showSelectedShapeActions, getNonDeletedElements } from "../element";
|
||||
import { register } from "./register";
|
||||
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
|
||||
import { KEYS } from "../keys";
|
||||
import { HelpButton } from "../components/HelpButton";
|
||||
import DropdownMenuItem from "../components/dropdownMenu/DropdownMenuItem";
|
||||
|
||||
export const actionToggleCanvasMenu = register({
|
||||
name: "toggleCanvasMenu",
|
||||
@@ -88,19 +86,5 @@ export const actionShortcuts = register({
|
||||
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,
|
||||
});
|
||||
|
@@ -20,7 +20,7 @@ export const actionToggleGridMode = register({
|
||||
};
|
||||
},
|
||||
checked: (appState: AppState) => appState.gridSize !== null,
|
||||
contextItemPredicate: (element, appState, props) => {
|
||||
predicate: (element, appState, props) => {
|
||||
return typeof props.gridModeEnabled === "undefined";
|
||||
},
|
||||
contextItemLabel: "labels.showGrid",
|
||||
|
@@ -18,7 +18,7 @@ export const actionToggleViewMode = register({
|
||||
};
|
||||
},
|
||||
checked: (appState) => appState.viewModeEnabled,
|
||||
contextItemPredicate: (elements, appState, appProps) => {
|
||||
predicate: (elements, appState, appProps) => {
|
||||
return typeof appProps.viewModeEnabled === "undefined";
|
||||
},
|
||||
contextItemLabel: "labels.viewMode",
|
||||
|
@@ -18,7 +18,7 @@ export const actionToggleZenMode = register({
|
||||
};
|
||||
},
|
||||
checked: (appState) => appState.zenModeEnabled,
|
||||
contextItemPredicate: (elements, appState, appProps) => {
|
||||
predicate: (elements, appState, appProps) => {
|
||||
return typeof appProps.zenModeEnabled === "undefined";
|
||||
},
|
||||
contextItemLabel: "buttons.zenMode",
|
||||
|
@@ -5,7 +5,7 @@ import {
|
||||
moveAllLeft,
|
||||
moveAllRight,
|
||||
} from "../zindex";
|
||||
import { KEYS, isDarwin, CODES } from "../keys";
|
||||
import { KEYS, CODES } from "../keys";
|
||||
import { t } from "../i18n";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import { register } from "./register";
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
SendBackwardIcon,
|
||||
SendToBackIcon,
|
||||
} from "../components/icons";
|
||||
import { isDarwin } from "../constants";
|
||||
|
||||
export const actionSendBackward = register({
|
||||
name: "sendBackward",
|
||||
|
@@ -131,11 +131,7 @@ export class ActionManager {
|
||||
/**
|
||||
* @param data additional data sent to the PanelComponent
|
||||
*/
|
||||
renderAction = (
|
||||
name: ActionName,
|
||||
data?: PanelComponentProps["data"],
|
||||
isInHamburgerMenu = false,
|
||||
) => {
|
||||
renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => {
|
||||
const canvasActions = this.app.props.UIOptions.canvasActions;
|
||||
|
||||
if (
|
||||
@@ -170,11 +166,20 @@ export class ActionManager {
|
||||
updateData={updateData}
|
||||
appProps={this.app.props}
|
||||
data={data}
|
||||
isInHamburgerMenu={isInHamburgerMenu}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 { isDarwin } from "../keys";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import { ActionName } from "./types";
|
||||
|
||||
|
@@ -34,85 +34,92 @@ type ActionFn = (
|
||||
export type UpdaterFn = (res: ActionResult) => void;
|
||||
export type ActionFilterFn = (action: Action) => void;
|
||||
|
||||
export type ActionName =
|
||||
| "copy"
|
||||
| "cut"
|
||||
| "paste"
|
||||
| "copyAsPng"
|
||||
| "copyAsSvg"
|
||||
| "copyText"
|
||||
| "sendBackward"
|
||||
| "bringForward"
|
||||
| "sendToBack"
|
||||
| "bringToFront"
|
||||
| "copyStyles"
|
||||
| "selectAll"
|
||||
| "pasteStyles"
|
||||
| "gridMode"
|
||||
| "zenMode"
|
||||
| "stats"
|
||||
| "changeStrokeColor"
|
||||
| "changeBackgroundColor"
|
||||
| "changeFillStyle"
|
||||
| "changeStrokeWidth"
|
||||
| "changeStrokeShape"
|
||||
| "changeSloppiness"
|
||||
| "changeStrokeStyle"
|
||||
| "changeArrowhead"
|
||||
| "changeOpacity"
|
||||
| "changeFontSize"
|
||||
| "toggleCanvasMenu"
|
||||
| "toggleEditMenu"
|
||||
| "undo"
|
||||
| "redo"
|
||||
| "finalize"
|
||||
| "changeProjectName"
|
||||
| "changeExportBackground"
|
||||
| "changeExportEmbedScene"
|
||||
| "changeExportScale"
|
||||
| "saveToActiveFile"
|
||||
| "saveFileToDisk"
|
||||
| "loadScene"
|
||||
| "duplicateSelection"
|
||||
| "deleteSelectedElements"
|
||||
| "changeViewBackgroundColor"
|
||||
| "clearCanvas"
|
||||
| "zoomIn"
|
||||
| "zoomOut"
|
||||
| "resetZoom"
|
||||
| "zoomToFit"
|
||||
| "zoomToSelection"
|
||||
| "changeFontFamily"
|
||||
| "changeTextAlign"
|
||||
| "changeVerticalAlign"
|
||||
| "toggleFullScreen"
|
||||
| "toggleShortcuts"
|
||||
| "group"
|
||||
| "ungroup"
|
||||
| "goToCollaborator"
|
||||
| "addToLibrary"
|
||||
| "changeRoundness"
|
||||
| "alignTop"
|
||||
| "alignBottom"
|
||||
| "alignLeft"
|
||||
| "alignRight"
|
||||
| "alignVerticallyCentered"
|
||||
| "alignHorizontallyCentered"
|
||||
| "distributeHorizontally"
|
||||
| "distributeVertically"
|
||||
| "flipHorizontal"
|
||||
| "flipVertical"
|
||||
| "viewMode"
|
||||
| "exportWithDarkMode"
|
||||
| "toggleTheme"
|
||||
| "increaseFontSize"
|
||||
| "decreaseFontSize"
|
||||
| "unbindText"
|
||||
| "hyperlink"
|
||||
| "eraser"
|
||||
| "bindText"
|
||||
| "toggleLock"
|
||||
| "toggleLinearEditor";
|
||||
const actionNames = [
|
||||
"copy",
|
||||
"cut",
|
||||
"paste",
|
||||
"copyAsPng",
|
||||
"copyAsSvg",
|
||||
"copyText",
|
||||
"sendBackward",
|
||||
"bringForward",
|
||||
"sendToBack",
|
||||
"bringToFront",
|
||||
"copyStyles",
|
||||
"selectAll",
|
||||
"pasteStyles",
|
||||
"gridMode",
|
||||
"zenMode",
|
||||
"stats",
|
||||
"changeStrokeColor",
|
||||
"changeBackgroundColor",
|
||||
"changeFillStyle",
|
||||
"changeStrokeWidth",
|
||||
"changeStrokeShape",
|
||||
"changeSloppiness",
|
||||
"changeStrokeStyle",
|
||||
"changeArrowhead",
|
||||
"changeOpacity",
|
||||
"changeFontSize",
|
||||
"toggleCanvasMenu",
|
||||
"toggleEditMenu",
|
||||
"undo",
|
||||
"redo",
|
||||
"finalize",
|
||||
"changeProjectName",
|
||||
"changeExportBackground",
|
||||
"changeExportEmbedScene",
|
||||
"changeExportScale",
|
||||
"saveToActiveFile",
|
||||
"saveFileToDisk",
|
||||
"loadScene",
|
||||
"duplicateSelection",
|
||||
"deleteSelectedElements",
|
||||
"changeViewBackgroundColor",
|
||||
"clearCanvas",
|
||||
"zoomIn",
|
||||
"zoomOut",
|
||||
"resetZoom",
|
||||
"zoomToFit",
|
||||
"zoomToSelection",
|
||||
"changeFontFamily",
|
||||
"changeTextAlign",
|
||||
"changeVerticalAlign",
|
||||
"toggleFullScreen",
|
||||
"toggleShortcuts",
|
||||
"group",
|
||||
"ungroup",
|
||||
"goToCollaborator",
|
||||
"addToLibrary",
|
||||
"changeRoundness",
|
||||
"alignTop",
|
||||
"alignBottom",
|
||||
"alignLeft",
|
||||
"alignRight",
|
||||
"alignVerticallyCentered",
|
||||
"alignHorizontallyCentered",
|
||||
"distributeHorizontally",
|
||||
"distributeVertically",
|
||||
"flipHorizontal",
|
||||
"flipVertical",
|
||||
"viewMode",
|
||||
"exportWithDarkMode",
|
||||
"toggleTheme",
|
||||
"increaseFontSize",
|
||||
"decreaseFontSize",
|
||||
"unbindText",
|
||||
"hyperlink",
|
||||
"bindText",
|
||||
"toggleLock",
|
||||
"toggleLinearEditor",
|
||||
"toggleEraserTool",
|
||||
"toggleHandTool",
|
||||
] as const;
|
||||
|
||||
// So we can have the `isActionName` type guard
|
||||
export type ActionName = typeof actionNames[number];
|
||||
export const isActionName = (n: any): n is ActionName =>
|
||||
actionNames.includes(n);
|
||||
|
||||
export type PanelComponentProps = {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
@@ -124,9 +131,7 @@ export type PanelComponentProps = {
|
||||
|
||||
export interface Action {
|
||||
name: ActionName;
|
||||
PanelComponent?: React.FC<
|
||||
PanelComponentProps & { isInHamburgerMenu: boolean }
|
||||
>;
|
||||
PanelComponent?: React.FC<PanelComponentProps>;
|
||||
perform: ActionFn;
|
||||
keyPriority?: number;
|
||||
keyTest?: (
|
||||
@@ -140,7 +145,7 @@ export interface Action {
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: Readonly<AppState>,
|
||||
) => string);
|
||||
contextItemPredicate?: (
|
||||
predicate?: (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
appProps: ExcalidrawProps,
|
||||
|
@@ -45,7 +45,7 @@ export const getDefaultAppState = (): Omit<
|
||||
type: "selection",
|
||||
customType: null,
|
||||
locked: false,
|
||||
lastActiveToolBeforeEraser: null,
|
||||
lastActiveTool: null,
|
||||
},
|
||||
penMode: false,
|
||||
penDetected: false,
|
||||
@@ -228,3 +228,11 @@ export const isEraserActive = ({
|
||||
}: {
|
||||
activeTool: AppState["activeTool"];
|
||||
}) => activeTool.type === "eraser";
|
||||
|
||||
export const isHandToolActive = ({
|
||||
activeTool,
|
||||
}: {
|
||||
activeTool: AppState["activeTool"];
|
||||
}) => {
|
||||
return activeTool.type === "hand";
|
||||
};
|
||||
|
@@ -180,16 +180,16 @@ export const parseClipboard = async (
|
||||
};
|
||||
|
||||
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
||||
let promise;
|
||||
try {
|
||||
// in Safari so far we need to construct the ClipboardItem synchronously
|
||||
// (i.e. in the same tick) otherwise browser will complain for lack of
|
||||
// user intent. Using a Promise ClipboardItem constructor solves this.
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=222262
|
||||
//
|
||||
// not await so that we can detect whether the thrown error likely relates
|
||||
// to a lack of support for the Promise ClipboardItem constructor
|
||||
promise = navigator.clipboard.write([
|
||||
// Note that Firefox (and potentially others) seems to support Promise
|
||||
// ClipboardItem constructor, but throws on an unrelated MIME type error.
|
||||
// So we need to await this and fallback to awaiting the blob if applicable.
|
||||
await navigator.clipboard.write([
|
||||
new window.ClipboardItem({
|
||||
[MIME_TYPES.png]: blob,
|
||||
}),
|
||||
@@ -207,7 +207,6 @@ export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await promise;
|
||||
};
|
||||
|
||||
export const copyTextToSystemClipboard = async (text: string | null) => {
|
||||
|
@@ -219,9 +219,10 @@ export const ShapesSwitcher = ({
|
||||
<>
|
||||
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
||||
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
|
||||
? `${capitalizeString(letter)} ${t("helpDialog.or")} ${numericKey}`
|
||||
? `${letter} ${t("helpDialog.or")} ${numericKey}`
|
||||
: `${numericKey}`;
|
||||
return (
|
||||
<ToolButton
|
||||
@@ -232,7 +233,7 @@ export const ShapesSwitcher = ({
|
||||
checked={activeTool.type === value}
|
||||
name="editor-current-shape"
|
||||
title={`${capitalizeString(label)} — ${shortcut}`}
|
||||
keyBindingLabel={numericKey}
|
||||
keyBindingLabel={numericKey || letter}
|
||||
aria-label={capitalizeString(label)}
|
||||
aria-keyshortcuts={shortcut}
|
||||
data-testid={`toolbar-${value}`}
|
||||
|
35
src/components/ActiveConfirmDialog.tsx
Normal file
35
src/components/ActiveConfirmDialog.tsx
Normal file
@@ -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>
|
||||
);
|
@@ -41,7 +41,11 @@ import { ActionManager } from "../actions/manager";
|
||||
import { actions } from "../actions/register";
|
||||
import { ActionResult } from "../actions/types";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { getDefaultAppState, isEraserActive } from "../appState";
|
||||
import {
|
||||
getDefaultAppState,
|
||||
isEraserActive,
|
||||
isHandToolActive,
|
||||
} from "../appState";
|
||||
import { parseClipboard } from "../clipboard";
|
||||
import {
|
||||
APP_NAME,
|
||||
@@ -57,6 +61,7 @@ import {
|
||||
EVENT,
|
||||
GRID_SIZE,
|
||||
IMAGE_RENDER_TIMEOUT,
|
||||
isAndroid,
|
||||
LINE_CONFIRM_THRESHOLD,
|
||||
MAX_ALLOWED_FILE_BYTES,
|
||||
MIME_TYPES,
|
||||
@@ -166,7 +171,6 @@ import {
|
||||
shouldRotateWithDiscreteAngle,
|
||||
isArrowKey,
|
||||
KEYS,
|
||||
isAndroid,
|
||||
} from "../keys";
|
||||
import { distance2d, getGridPoint, isPathALoop } from "../math";
|
||||
import { renderScene } from "../renderer/renderScene";
|
||||
@@ -274,6 +278,7 @@ import {
|
||||
import { shouldShowBoundingBox } from "../element/transformHandles";
|
||||
import { Fonts } from "../scene/Fonts";
|
||||
import { actionPaste } from "../actions/actionClipboard";
|
||||
import { actionToggleHandTool } from "../actions/actionCanvas";
|
||||
|
||||
const deviceContextInitialValue = {
|
||||
isSmScreen: false,
|
||||
@@ -283,15 +288,12 @@ const deviceContextInitialValue = {
|
||||
};
|
||||
const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
|
||||
DeviceContext.displayName = "DeviceContext";
|
||||
export const useDevice = () => useContext<Device>(DeviceContext);
|
||||
|
||||
export const ExcalidrawContainerContext = React.createContext<{
|
||||
container: HTMLDivElement | null;
|
||||
id: string | null;
|
||||
}>({ container: null, id: null });
|
||||
ExcalidrawContainerContext.displayName = "ExcalidrawContainerContext";
|
||||
export const useExcalidrawContainer = () =>
|
||||
useContext(ExcalidrawContainerContext);
|
||||
|
||||
const ExcalidrawElementsContext = React.createContext<
|
||||
readonly NonDeletedExcalidrawElement[]
|
||||
@@ -309,14 +311,19 @@ ExcalidrawAppStateContext.displayName = "ExcalidrawAppStateContext";
|
||||
|
||||
const ExcalidrawSetAppStateContext = React.createContext<
|
||||
React.Component<any, AppState>["setState"]
|
||||
>(() => {});
|
||||
>(() => {
|
||||
console.warn("unitialized ExcalidrawSetAppStateContext context!");
|
||||
});
|
||||
ExcalidrawSetAppStateContext.displayName = "ExcalidrawSetAppStateContext";
|
||||
|
||||
const ExcalidrawActionManagerContext = React.createContext<
|
||||
ActionManager | { renderAction: ActionManager["renderAction"] }
|
||||
>({ renderAction: () => null });
|
||||
const ExcalidrawActionManagerContext = React.createContext<ActionManager>(
|
||||
null!,
|
||||
);
|
||||
ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext";
|
||||
|
||||
export const useDevice = () => useContext<Device>(DeviceContext);
|
||||
export const useExcalidrawContainer = () =>
|
||||
useContext(ExcalidrawContainerContext);
|
||||
export const useExcalidrawElements = () =>
|
||||
useContext(ExcalidrawElementsContext);
|
||||
export const useExcalidrawAppState = () =>
|
||||
@@ -537,8 +544,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
);
|
||||
const { onCollabButtonClick, renderTopRightUI, renderCustomStats } =
|
||||
this.props;
|
||||
const { renderTopRightUI, renderCustomStats } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -572,9 +578,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
setAppState={this.setAppState}
|
||||
actionManager={this.actionManager}
|
||||
elements={this.scene.getNonDeletedElements()}
|
||||
onCollabButtonClick={onCollabButtonClick}
|
||||
onLockToggle={this.toggleLock}
|
||||
onPenModeToggle={this.togglePenMode}
|
||||
onHandToolToggle={this.onHandToolToggle}
|
||||
onInsertElements={(elements) =>
|
||||
this.addElementsFromPasteOrLibrary({
|
||||
elements,
|
||||
@@ -598,6 +604,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
id={this.id}
|
||||
onImageAction={this.onImageAction}
|
||||
renderWelcomeScreen={
|
||||
!this.state.isLoading &&
|
||||
this.props.UIOptions.welcomeScreen &&
|
||||
this.state.showWelcomeScreen &&
|
||||
this.state.activeTool.type === "selection" &&
|
||||
!this.scene.getElementsIncludingDeleted().length
|
||||
@@ -1810,6 +1818,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
};
|
||||
|
||||
onHandToolToggle = () => {
|
||||
this.actionManager.executeAction(actionToggleHandTool);
|
||||
};
|
||||
|
||||
scrollToContent = (
|
||||
target:
|
||||
| ExcalidrawElement
|
||||
@@ -2227,11 +2239,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
private setActiveTool = (
|
||||
tool:
|
||||
| { type: typeof SHAPES[number]["value"] | "eraser" }
|
||||
| { type: typeof SHAPES[number]["value"] | "eraser" | "hand" }
|
||||
| { type: "custom"; customType: string },
|
||||
) => {
|
||||
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);
|
||||
}
|
||||
if (isToolIcon(document.activeElement)) {
|
||||
@@ -2902,7 +2916,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
null;
|
||||
}
|
||||
|
||||
if (isHoldingSpace || isPanning || isDraggingScrollBar) {
|
||||
if (
|
||||
isHoldingSpace ||
|
||||
isPanning ||
|
||||
isDraggingScrollBar ||
|
||||
isHandToolActive(this.state)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3494,7 +3513,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
} else if (this.state.activeTool.type === "custom") {
|
||||
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.state.activeTool.type,
|
||||
pointerDownState,
|
||||
@@ -3605,6 +3627,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
gesture.pointers.size <= 1 &&
|
||||
(event.button === POINTER_BUTTON.WHEEL ||
|
||||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
|
||||
isHandToolActive(this.state) ||
|
||||
this.state.viewModeEnabled)
|
||||
) ||
|
||||
isTextElement(this.state.editingElement)
|
||||
|
7
src/components/Button.scss
Normal file
7
src/components/Button.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
@import "../css/theme";
|
||||
|
||||
.excalidraw {
|
||||
.excalidraw-button {
|
||||
@include outlineButtonStyles;
|
||||
}
|
||||
}
|
35
src/components/Button.tsx
Normal file
35
src/components/Button.tsx
Normal file
@@ -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 (
|
||||
item &&
|
||||
(item === CONTEXT_MENU_SEPARATOR ||
|
||||
!item.contextItemPredicate ||
|
||||
item.contextItemPredicate(
|
||||
!item.predicate ||
|
||||
item.predicate(
|
||||
elements,
|
||||
appState,
|
||||
actionManager.app.props,
|
||||
|
@@ -96,6 +96,10 @@
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
margin: 0 0.2em;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-radius: 1rem;
|
||||
background-color: var(--button-color);
|
||||
|
@@ -1,3 +1,5 @@
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.FixedSideContainer {
|
||||
position: absolute;
|
||||
@@ -9,10 +11,10 @@
|
||||
}
|
||||
|
||||
.FixedSideContainer_side_top {
|
||||
left: 1rem;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
left: var(--editor-container-padding);
|
||||
top: var(--editor-container-padding);
|
||||
right: var(--editor-container-padding);
|
||||
bottom: var(--editor-container-padding);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
|
32
src/components/HandButton.tsx
Normal file
32
src/components/HandButton.tsx
Normal file
@@ -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 { t } from "../i18n";
|
||||
import { isDarwin, isWindows, KEYS } from "../keys";
|
||||
import { KEYS } from "../keys";
|
||||
import { Dialog } from "./Dialog";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import "./HelpDialog.scss";
|
||||
import { ExternalLinkIcon } from "./icons";
|
||||
import { probablySupportsClipboardBlob } from "../clipboard";
|
||||
import { isDarwin, isFirefox, isWindows } from "../constants";
|
||||
|
||||
const 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 = ({
|
||||
label,
|
||||
shortcuts,
|
||||
@@ -81,7 +87,9 @@ const Shortcut = ({
|
||||
? [...shortcut.slice(0, -2).split("+"), "+"]
|
||||
: shortcut.split("+");
|
||||
|
||||
return keys.map((key) => <ShortcutKey key={key}>{key}</ShortcutKey>);
|
||||
return keys.map((key) => (
|
||||
<ShortcutKey key={key}>{upperCaseSingleChars(key)}</ShortcutKey>
|
||||
));
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -118,6 +126,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||
className="HelpDialog__island--tools"
|
||||
caption={t("helpDialog.tools")}
|
||||
>
|
||||
<Shortcut label={t("toolBar.hand")} shortcuts={[KEYS.H]} />
|
||||
<Shortcut
|
||||
label={t("toolBar.selection")}
|
||||
shortcuts={[KEYS.V, KEYS["1"]]}
|
||||
@@ -304,10 +313,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||
label={t("labels.pasteAsPlaintext")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+V")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.copyAsPng")}
|
||||
shortcuts={[getShortcutKey("Shift+Alt+C")]}
|
||||
/>
|
||||
{/* firefox supports clipboard API under a flag, so we'll
|
||||
show users what they can do in the error message */}
|
||||
{(probablySupportsClipboardBlob || isFirefox) && (
|
||||
<Shortcut
|
||||
label={t("labels.copyAsPng")}
|
||||
shortcuts={[getShortcutKey("Shift+Alt+C")]}
|
||||
/>
|
||||
)}
|
||||
<Shortcut
|
||||
label={t("labels.copyStyles")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]}
|
||||
|
@@ -12,7 +12,7 @@ import Stack from "./Stack";
|
||||
import "./ExportDialog.scss";
|
||||
import OpenColor from "open-color";
|
||||
import { CheckboxItem } from "./CheckboxItem";
|
||||
import { DEFAULT_EXPORT_PADDING } from "../constants";
|
||||
import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants";
|
||||
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
|
||||
@@ -190,7 +190,9 @@ const ImageExportModal = ({
|
||||
>
|
||||
SVG
|
||||
</ExportButton>
|
||||
{probablySupportsClipboardBlob && (
|
||||
{/* firefox supports clipboard API under a flag,
|
||||
so let's throw and tell people what they can do */}
|
||||
{(probablySupportsClipboardBlob || isFirefox) && (
|
||||
<ExportButton
|
||||
title={t("buttons.copyPngToClipboard")}
|
||||
onClick={() => onExportToClipboard(exportedElements)}
|
||||
|
@@ -14,10 +14,10 @@ import {
|
||||
ExcalidrawProps,
|
||||
BinaryFiles,
|
||||
UIChildrenComponents,
|
||||
UIWelcomeScreenComponents,
|
||||
} from "../types";
|
||||
import { muteFSAbortError, ReactChildrenToObject } from "../utils";
|
||||
import { isShallowEqual, muteFSAbortError, getReactChildren } from "../utils";
|
||||
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
||||
import CollabButton from "./CollabButton";
|
||||
import { ErrorDialog } from "./ErrorDialog";
|
||||
import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
|
||||
import { FixedSideContainer } from "./FixedSideContainer";
|
||||
@@ -45,13 +45,14 @@ import { useDevice } from "../components/App";
|
||||
import { Stats } from "./Stats";
|
||||
import { actionToggleStats } from "../actions/actionToggleStats";
|
||||
import Footer from "./footer/Footer";
|
||||
import { WelcomeScreenMenuArrow, WelcomeScreenTopToolbarArrow } from "./icons";
|
||||
import WelcomeScreen from "./WelcomeScreen";
|
||||
import WelcomeScreen from "./welcome-screen/WelcomeScreen";
|
||||
import { hostSidebarCountersAtom } from "./Sidebar/Sidebar";
|
||||
import { jotaiScope } from "../jotai";
|
||||
import { useAtom } from "jotai";
|
||||
import WelcomeScreenDecor from "./WelcomeScreenDecor";
|
||||
import MainMenu from "./mainMenu/MainMenu";
|
||||
import MainMenu from "./main-menu/MainMenu";
|
||||
import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
|
||||
import { HandButton } from "./HandButton";
|
||||
import { isHandToolActive } from "../appState";
|
||||
|
||||
interface LayerUIProps {
|
||||
actionManager: ActionManager;
|
||||
@@ -60,8 +61,8 @@ interface LayerUIProps {
|
||||
canvas: HTMLCanvasElement | null;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
onCollabButtonClick?: () => void;
|
||||
onLockToggle: () => void;
|
||||
onHandToolToggle: () => void;
|
||||
onPenModeToggle: () => void;
|
||||
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||
showExitZenModeBtn: boolean;
|
||||
@@ -87,8 +88,8 @@ const LayerUI = ({
|
||||
setAppState,
|
||||
elements,
|
||||
canvas,
|
||||
onCollabButtonClick,
|
||||
onLockToggle,
|
||||
onHandToolToggle,
|
||||
onPenModeToggle,
|
||||
onInsertElements,
|
||||
showExitZenModeBtn,
|
||||
@@ -107,8 +108,27 @@ const LayerUI = ({
|
||||
}: LayerUIProps) => {
|
||||
const device = useDevice();
|
||||
|
||||
const childrenComponents =
|
||||
ReactChildrenToObject<UIChildrenComponents>(children);
|
||||
const [childrenComponents, restChildren] =
|
||||
getReactChildren<UIChildrenComponents>(children, {
|
||||
Menu: true,
|
||||
FooterCenter: true,
|
||||
WelcomeScreen: true,
|
||||
});
|
||||
|
||||
const [WelcomeScreenComponents] = getReactChildren<UIWelcomeScreenComponents>(
|
||||
renderWelcomeScreen
|
||||
? (
|
||||
childrenComponents?.WelcomeScreen ?? (
|
||||
<WelcomeScreen>
|
||||
<WelcomeScreen.Center />
|
||||
<WelcomeScreen.Hints.MenuHint />
|
||||
<WelcomeScreen.Hints.ToolbarHint />
|
||||
<WelcomeScreen.Hints.HelpHint />
|
||||
</WelcomeScreen>
|
||||
)
|
||||
)?.props?.children
|
||||
: null,
|
||||
);
|
||||
|
||||
const renderJSONExportDialog = () => {
|
||||
if (!UIOptions.canvasActions.export) {
|
||||
@@ -183,16 +203,12 @@ const LayerUI = ({
|
||||
<MainMenu>
|
||||
<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 />
|
||||
)}
|
||||
{onCollabButtonClick && (
|
||||
<MainMenu.DefaultItems.LiveCollaboration
|
||||
onSelect={onCollabButtonClick}
|
||||
isCollaborating={isCollaborating}
|
||||
/>
|
||||
)}
|
||||
<MainMenu.DefaultItems.Help />
|
||||
<MainMenu.DefaultItems.ClearCanvas />
|
||||
<MainMenu.Separator />
|
||||
@@ -208,15 +224,10 @@ const LayerUI = ({
|
||||
};
|
||||
const renderCanvasActions = () => (
|
||||
<div style={{ position: "relative" }}>
|
||||
<WelcomeScreenDecor
|
||||
shouldRender={renderWelcomeScreen && !appState.isLoading}
|
||||
>
|
||||
<div className="virgil WelcomeScreen-decor WelcomeScreen-decor--menu-pointer">
|
||||
{WelcomeScreenMenuArrow}
|
||||
<div>{t("welcomeScreen.menuHints")}</div>
|
||||
</div>
|
||||
</WelcomeScreenDecor>
|
||||
{renderMenu()}
|
||||
{WelcomeScreenComponents.MenuHint}
|
||||
{/* wrapping to Fragment stops React from occasionally complaining
|
||||
about identical Keys */}
|
||||
<>{renderMenu()}</>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -253,9 +264,7 @@ const LayerUI = ({
|
||||
|
||||
return (
|
||||
<FixedSideContainer side="top">
|
||||
{renderWelcomeScreen && !appState.isLoading && (
|
||||
<WelcomeScreen appState={appState} actionManager={actionManager} />
|
||||
)}
|
||||
{WelcomeScreenComponents.Center}
|
||||
<div className="App-menu App-menu_top">
|
||||
<Stack.Col
|
||||
gap={6}
|
||||
@@ -270,17 +279,7 @@ const LayerUI = ({
|
||||
<Section heading="shapes" className="shapes-section">
|
||||
{(heading: React.ReactNode) => (
|
||||
<div style={{ position: "relative" }}>
|
||||
<WelcomeScreenDecor
|
||||
shouldRender={renderWelcomeScreen && !appState.isLoading}
|
||||
>
|
||||
<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>
|
||||
|
||||
{WelcomeScreenComponents.ToolbarHint}
|
||||
<Stack.Col gap={4} align="start">
|
||||
<Stack.Row
|
||||
gap={1}
|
||||
@@ -310,13 +309,20 @@ const LayerUI = ({
|
||||
penDetected={appState.penDetected}
|
||||
/>
|
||||
<LockButton
|
||||
zenModeEnabled={appState.zenModeEnabled}
|
||||
checked={appState.activeTool.locked}
|
||||
onChange={() => onLockToggle()}
|
||||
onChange={onLockToggle}
|
||||
title={t("toolBar.lock")}
|
||||
/>
|
||||
|
||||
<div className="App-toolbar__divider"></div>
|
||||
|
||||
<HandButton
|
||||
checked={isHandToolActive(appState)}
|
||||
onChange={() => onHandToolToggle()}
|
||||
title={t("toolBar.hand")}
|
||||
isMobile
|
||||
/>
|
||||
|
||||
<ShapesSwitcher
|
||||
appState={appState}
|
||||
canvas={canvas}
|
||||
@@ -328,9 +334,6 @@ const LayerUI = ({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/* {actionManager.renderAction("eraser", {
|
||||
// size: "small",
|
||||
})} */}
|
||||
</Stack.Row>
|
||||
</Island>
|
||||
</Stack.Row>
|
||||
@@ -348,14 +351,6 @@ const LayerUI = ({
|
||||
)}
|
||||
>
|
||||
<UserList collaborators={appState.collaborators} />
|
||||
{onCollabButtonClick && (
|
||||
<CollabButton
|
||||
isInHamburgerMenu={false}
|
||||
isCollaborating={isCollaborating}
|
||||
collaboratorCount={appState.collaborators.size}
|
||||
onClick={onCollabButtonClick}
|
||||
/>
|
||||
)}
|
||||
{renderTopRightUI?.(device.isMobile, appState)}
|
||||
{!appState.viewModeEnabled && (
|
||||
<LibraryButton appState={appState} setAppState={setAppState} />
|
||||
@@ -385,6 +380,7 @@ const LayerUI = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{restChildren}
|
||||
{appState.isLoading && <LoadingMessage delay={250} />}
|
||||
{appState.errorMessage && (
|
||||
<ErrorDialog
|
||||
@@ -399,6 +395,7 @@ const LayerUI = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ActiveConfirmDialog />
|
||||
{renderImageExportDialog()}
|
||||
{renderJSONExportDialog()}
|
||||
{appState.pasteDialog.shown && (
|
||||
@@ -415,24 +412,23 @@ const LayerUI = ({
|
||||
)}
|
||||
{device.isMobile && (
|
||||
<MobileMenu
|
||||
renderWelcomeScreen={renderWelcomeScreen}
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
actionManager={actionManager}
|
||||
renderJSONExportDialog={renderJSONExportDialog}
|
||||
renderImageExportDialog={renderImageExportDialog}
|
||||
setAppState={setAppState}
|
||||
onCollabButtonClick={onCollabButtonClick}
|
||||
onLockToggle={() => onLockToggle()}
|
||||
onLockToggle={onLockToggle}
|
||||
onHandToolToggle={onHandToolToggle}
|
||||
onPenModeToggle={onPenModeToggle}
|
||||
canvas={canvas}
|
||||
isCollaborating={isCollaborating}
|
||||
onImageAction={onImageAction}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderCustomStats={renderCustomStats}
|
||||
renderSidebars={renderSidebars}
|
||||
device={device}
|
||||
renderMenu={renderMenu}
|
||||
welcomeScreenCenter={WelcomeScreenComponents.Center}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -457,13 +453,12 @@ const LayerUI = ({
|
||||
>
|
||||
{renderFixedSideContainer()}
|
||||
<Footer
|
||||
renderWelcomeScreen={renderWelcomeScreen}
|
||||
appState={appState}
|
||||
actionManager={actionManager}
|
||||
showExitZenModeBtn={showExitZenModeBtn}
|
||||
footerCenter={childrenComponents.FooterCenter}
|
||||
welcomeScreenHelp={WelcomeScreenComponents.HelpHint}
|
||||
/>
|
||||
|
||||
{appState.showStats && (
|
||||
<Stats
|
||||
appState={appState}
|
||||
@@ -495,28 +490,39 @@ 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 stripIrrelevantAppStateProps = (
|
||||
appState: AppState,
|
||||
): Partial<AppState> => {
|
||||
const { suggestedBindings, startBoundElement, cursorButton, ...ret } =
|
||||
appState;
|
||||
return ret;
|
||||
};
|
||||
|
||||
const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
|
||||
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 (
|
||||
prev.renderTopRightUI === next.renderTopRightUI &&
|
||||
prev.renderCustomStats === next.renderCustomStats &&
|
||||
prev.renderCustomSidebar === next.renderCustomSidebar &&
|
||||
prev.langCode === next.langCode &&
|
||||
prev.elements === next.elements &&
|
||||
prev.files === next.files &&
|
||||
keys.every((key) => prevAppState[key] === nextAppState[key])
|
||||
isShallowEqual(
|
||||
stripIrrelevantAppStateProps(prevAppState),
|
||||
stripIrrelevantAppStateProps(nextAppState),
|
||||
) && isShallowEqual(prev, next)
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -187,13 +187,14 @@ export const LibraryMenuHeader: React.FC<{
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
onClickOutside={() => setIsLibraryMenuOpen(false)}
|
||||
onSelect={() => setIsLibraryMenuOpen(false)}
|
||||
className="library-menu"
|
||||
>
|
||||
{!itemsSelected && (
|
||||
<DropdownMenu.Item
|
||||
onSelect={onLibraryImport}
|
||||
icon={LoadIcon}
|
||||
dataTestId="lib-dropdown--load"
|
||||
data-testid="lib-dropdown--load"
|
||||
>
|
||||
{t("buttons.load")}
|
||||
</DropdownMenu.Item>
|
||||
@@ -202,7 +203,7 @@ export const LibraryMenuHeader: React.FC<{
|
||||
<DropdownMenu.Item
|
||||
onSelect={onLibraryExport}
|
||||
icon={ExportIcon}
|
||||
dataTestId="lib-dropdown--export"
|
||||
data-testid="lib-dropdown--export"
|
||||
>
|
||||
{t("buttons.export")}
|
||||
</DropdownMenu.Item>
|
||||
@@ -219,7 +220,7 @@ export const LibraryMenuHeader: React.FC<{
|
||||
<DropdownMenu.Item
|
||||
icon={publishIcon}
|
||||
onSelect={() => setShowPublishLibraryDialog(true)}
|
||||
dataTestId="lib-dropdown--remove"
|
||||
data-testid="lib-dropdown--remove"
|
||||
>
|
||||
{t("buttons.publishLibrary")}
|
||||
</DropdownMenu.Item>
|
||||
|
@@ -9,7 +9,6 @@ type LockIconProps = {
|
||||
name?: string;
|
||||
checked: boolean;
|
||||
onChange?(): void;
|
||||
zenModeEnabled?: boolean;
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
|
@@ -1,5 +1,10 @@
|
||||
import React from "react";
|
||||
import { AppState, Device, ExcalidrawProps } from "../types";
|
||||
import {
|
||||
AppState,
|
||||
Device,
|
||||
ExcalidrawProps,
|
||||
UIWelcomeScreenComponents,
|
||||
} from "../types";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { t } from "../i18n";
|
||||
import Stack from "./Stack";
|
||||
@@ -17,7 +22,8 @@ import { LibraryButton } from "./LibraryButton";
|
||||
import { PenModeButton } from "./PenModeButton";
|
||||
import { Stats } from "./Stats";
|
||||
import { actionToggleStats } from "../actions";
|
||||
import WelcomeScreen from "./WelcomeScreen";
|
||||
import { HandButton } from "./HandButton";
|
||||
import { isHandToolActive } from "../appState";
|
||||
|
||||
type MobileMenuProps = {
|
||||
appState: AppState;
|
||||
@@ -26,11 +32,10 @@ type MobileMenuProps = {
|
||||
renderImageExportDialog: () => React.ReactNode;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
onCollabButtonClick?: () => void;
|
||||
onLockToggle: () => void;
|
||||
onHandToolToggle: () => void;
|
||||
onPenModeToggle: () => void;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
isCollaborating: boolean;
|
||||
|
||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||
renderTopRightUI?: (
|
||||
@@ -40,8 +45,8 @@ type MobileMenuProps = {
|
||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||
renderSidebars: () => JSX.Element | null;
|
||||
device: Device;
|
||||
renderWelcomeScreen?: boolean;
|
||||
renderMenu: () => React.ReactNode;
|
||||
welcomeScreenCenter: UIWelcomeScreenComponents["Center"];
|
||||
};
|
||||
|
||||
export const MobileMenu = ({
|
||||
@@ -50,23 +55,21 @@ export const MobileMenu = ({
|
||||
actionManager,
|
||||
setAppState,
|
||||
onLockToggle,
|
||||
onHandToolToggle,
|
||||
onPenModeToggle,
|
||||
canvas,
|
||||
isCollaborating,
|
||||
onImageAction,
|
||||
renderTopRightUI,
|
||||
renderCustomStats,
|
||||
renderSidebars,
|
||||
device,
|
||||
renderWelcomeScreen,
|
||||
renderMenu,
|
||||
welcomeScreenCenter,
|
||||
}: MobileMenuProps) => {
|
||||
const renderToolbar = () => {
|
||||
return (
|
||||
<FixedSideContainer side="top" className="App-top-bar">
|
||||
{renderWelcomeScreen && !appState.isLoading && (
|
||||
<WelcomeScreen appState={appState} actionManager={actionManager} />
|
||||
)}
|
||||
{welcomeScreenCenter}
|
||||
<Section heading="shapes">
|
||||
{(heading: React.ReactNode) => (
|
||||
<Stack.Col gap={4} align="center">
|
||||
@@ -74,20 +77,6 @@ export const MobileMenu = ({
|
||||
<Island padding={1} className="App-toolbar App-toolbar--mobile">
|
||||
{heading}
|
||||
<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
|
||||
appState={appState}
|
||||
canvas={canvas}
|
||||
@@ -103,20 +92,6 @@ export const MobileMenu = ({
|
||||
</Island>
|
||||
{renderTopRightUI && renderTopRightUI(true, appState)}
|
||||
<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 && (
|
||||
<LibraryButton
|
||||
appState={appState}
|
||||
@@ -124,6 +99,25 @@ export const MobileMenu = ({
|
||||
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>
|
||||
</Stack.Row>
|
||||
</Stack.Col>
|
||||
|
@@ -3,7 +3,7 @@
|
||||
.excalidraw {
|
||||
&.excalidraw-modal-container {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.Modal {
|
||||
|
@@ -19,7 +19,7 @@ type ToolButtonBaseProps = {
|
||||
name?: string;
|
||||
id?: string;
|
||||
size?: ToolButtonSize;
|
||||
keyBindingLabel?: string;
|
||||
keyBindingLabel?: string | null;
|
||||
showAriaLabel?: boolean;
|
||||
hidden?: 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;
|
@@ -73,7 +73,7 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--button-hover);
|
||||
background-color: var(--button-hover-bg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
@@ -4,16 +4,23 @@ import { Island } from "../Island";
|
||||
import { useDevice } from "../App";
|
||||
import clsx from "clsx";
|
||||
import Stack from "../Stack";
|
||||
import React from "react";
|
||||
import { DropdownMenuContentPropsContext } from "./common";
|
||||
|
||||
const MenuContent = ({
|
||||
children,
|
||||
onClickOutside,
|
||||
className = "",
|
||||
onSelect,
|
||||
style,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
onClickOutside?: () => void;
|
||||
className?: string;
|
||||
/**
|
||||
* Called when any menu item is selected (clicked on).
|
||||
*/
|
||||
onSelect?: (event: Event) => void;
|
||||
style?: React.CSSProperties;
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
@@ -24,28 +31,32 @@ const MenuContent = ({
|
||||
const classNames = clsx(`dropdown-menu ${className}`, {
|
||||
"dropdown-menu--mobile": device.isMobile,
|
||||
}).trim();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={classNames}
|
||||
style={style}
|
||||
data-testid="dropdown-menu"
|
||||
>
|
||||
{/* the zIndex ensures this menu has higher stacking order,
|
||||
<DropdownMenuContentPropsContext.Provider value={{ onSelect }}>
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={classNames}
|
||||
style={style}
|
||||
data-testid="dropdown-menu"
|
||||
>
|
||||
{/* the zIndex ensures this menu has higher stacking order,
|
||||
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
||||
{device.isMobile ? (
|
||||
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
|
||||
) : (
|
||||
<Island
|
||||
className="dropdown-menu-container"
|
||||
padding={2}
|
||||
style={{ zIndex: 1 }}
|
||||
>
|
||||
{children}
|
||||
</Island>
|
||||
)}
|
||||
</div>
|
||||
{device.isMobile ? (
|
||||
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
|
||||
) : (
|
||||
<Island
|
||||
className="dropdown-menu-container"
|
||||
padding={2}
|
||||
style={{ zIndex: 1 }}
|
||||
>
|
||||
{children}
|
||||
</Island>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContentPropsContext.Provider>
|
||||
);
|
||||
};
|
||||
export default MenuContent;
|
||||
MenuContent.displayName = "DropdownMenuContent";
|
||||
|
||||
export default MenuContent;
|
||||
|
@@ -1,38 +1,33 @@
|
||||
import React from "react";
|
||||
import {
|
||||
getDrodownMenuItemClassName,
|
||||
useHandleDropdownMenuItemClick,
|
||||
} from "./common";
|
||||
import MenuItemContent from "./DropdownMenuItemContent";
|
||||
|
||||
export const getDrodownMenuItemClassName = (className = "") => {
|
||||
return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim();
|
||||
};
|
||||
|
||||
const DropdownMenuItem = ({
|
||||
icon,
|
||||
onSelect,
|
||||
children,
|
||||
dataTestId,
|
||||
shortcut,
|
||||
className,
|
||||
style,
|
||||
ariaLabel,
|
||||
...rest
|
||||
}: {
|
||||
icon?: JSX.Element;
|
||||
onSelect: () => void;
|
||||
onSelect: (event: Event) => void;
|
||||
children: React.ReactNode;
|
||||
dataTestId?: string;
|
||||
shortcut?: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
ariaLabel?: string;
|
||||
}) => {
|
||||
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
|
||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||
|
||||
return (
|
||||
<button
|
||||
aria-label={ariaLabel}
|
||||
onClick={onSelect}
|
||||
data-testid={dataTestId}
|
||||
title={ariaLabel}
|
||||
{...rest}
|
||||
onClick={handleClick}
|
||||
type="button"
|
||||
className={getDrodownMenuItemClassName(className)}
|
||||
style={style}
|
||||
title={rest.title ?? rest["aria-label"]}
|
||||
>
|
||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||
{children}
|
||||
|
@@ -1,19 +1,17 @@
|
||||
import React from "react";
|
||||
|
||||
const DropdownMenuItemCustom = ({
|
||||
children,
|
||||
className = "",
|
||||
style,
|
||||
dataTestId,
|
||||
...rest
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
dataTestId?: string;
|
||||
}) => {
|
||||
} & React.HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
className={`dropdown-menu-item-base dropdown-menu-item-custom ${className}`.trim()}
|
||||
style={style}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
@@ -1,35 +1,37 @@
|
||||
import MenuItemContent from "./DropdownMenuItemContent";
|
||||
import React from "react";
|
||||
import { getDrodownMenuItemClassName } from "./DropdownMenuItem";
|
||||
import {
|
||||
getDrodownMenuItemClassName,
|
||||
useHandleDropdownMenuItemClick,
|
||||
} from "./common";
|
||||
|
||||
const DropdownMenuItemLink = ({
|
||||
icon,
|
||||
dataTestId,
|
||||
shortcut,
|
||||
href,
|
||||
children,
|
||||
onSelect,
|
||||
className = "",
|
||||
style,
|
||||
ariaLabel,
|
||||
...rest
|
||||
}: {
|
||||
href: string;
|
||||
icon?: JSX.Element;
|
||||
children: React.ReactNode;
|
||||
dataTestId?: string;
|
||||
shortcut?: string;
|
||||
className?: string;
|
||||
href: string;
|
||||
style?: React.CSSProperties;
|
||||
ariaLabel?: string;
|
||||
}) => {
|
||||
onSelect?: (event: Event) => void;
|
||||
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||
|
||||
return (
|
||||
<a
|
||||
{...rest}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={getDrodownMenuItemClassName(className)}
|
||||
style={style}
|
||||
data-testid={dataTestId}
|
||||
title={ariaLabel}
|
||||
aria-label={ariaLabel}
|
||||
title={rest.title ?? rest["aria-label"]}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||
{children}
|
||||
|
31
src/components/dropdownMenu/common.ts
Normal file
31
src/components/dropdownMenu/common.ts
Normal file
@@ -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 getDrodownMenuItemClassName = (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,11 @@
|
||||
import clsx from "clsx";
|
||||
import { actionShortcuts } from "../../actions";
|
||||
import { ActionManager } from "../../actions/manager";
|
||||
import { t } from "../../i18n";
|
||||
import { AppState, UIChildrenComponents } from "../../types";
|
||||
import {
|
||||
AppState,
|
||||
UIChildrenComponents,
|
||||
UIWelcomeScreenComponents,
|
||||
} from "../../types";
|
||||
import {
|
||||
ExitZenModeAction,
|
||||
FinalizeAction,
|
||||
@@ -9,23 +13,22 @@ import {
|
||||
ZoomActions,
|
||||
} from "../Actions";
|
||||
import { useDevice } from "../App";
|
||||
import { WelcomeScreenHelpArrow } from "../icons";
|
||||
import { HelpButton } from "../HelpButton";
|
||||
import { Section } from "../Section";
|
||||
import Stack from "../Stack";
|
||||
import WelcomeScreenDecor from "../WelcomeScreenDecor";
|
||||
|
||||
const Footer = ({
|
||||
appState,
|
||||
actionManager,
|
||||
showExitZenModeBtn,
|
||||
renderWelcomeScreen,
|
||||
footerCenter,
|
||||
welcomeScreenHelp,
|
||||
}: {
|
||||
appState: AppState;
|
||||
actionManager: ActionManager;
|
||||
showExitZenModeBtn: boolean;
|
||||
renderWelcomeScreen: boolean;
|
||||
footerCenter: UIChildrenComponents["FooterCenter"];
|
||||
welcomeScreenHelp: UIWelcomeScreenComponents["HelpHint"];
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
const showFinalize =
|
||||
@@ -77,16 +80,10 @@ const Footer = ({
|
||||
})}
|
||||
>
|
||||
<div style={{ position: "relative" }}>
|
||||
<WelcomeScreenDecor
|
||||
shouldRender={renderWelcomeScreen && !appState.isLoading}
|
||||
>
|
||||
<div className="virgil WelcomeScreen-decor WelcomeScreen-decor--help-pointer">
|
||||
<div>{t("welcomeScreen.helpHints")}</div>
|
||||
{WelcomeScreenHelpArrow}
|
||||
</div>
|
||||
</WelcomeScreenDecor>
|
||||
|
||||
{actionManager.renderAction("toggleShortcuts")}
|
||||
{welcomeScreenHelp}
|
||||
<HelpButton
|
||||
onClick={() => actionManager.executeAction(actionShortcuts)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ExitZenModeAction
|
||||
|
@@ -883,7 +883,7 @@ export const CenterHorizontallyIcon = createIcon(
|
||||
modifiedTablerIconProps,
|
||||
);
|
||||
|
||||
export const UsersIcon = createIcon(
|
||||
export const usersIcon = createIcon(
|
||||
<g strokeWidth="1.5">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
@@ -1532,3 +1532,14 @@ export const publishIcon = 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" />,
|
||||
);
|
||||
|
||||
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,
|
||||
);
|
||||
|
@@ -1,30 +1,23 @@
|
||||
@import "../css/variables.module";
|
||||
@import "../../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.collab-button {
|
||||
@include outlineButtonStyles;
|
||||
width: var(--lg-button-size);
|
||||
height: var(--lg-button-size);
|
||||
--button-bg: var(--color-primary);
|
||||
--button-color: white;
|
||||
--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;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-darker);
|
||||
border-color: var(--color-primary-darker);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--color-primary-darker);
|
||||
}
|
||||
|
||||
&.active {
|
||||
// double .active to force specificity
|
||||
&.active.active {
|
||||
background-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";
|
@@ -1,4 +1,3 @@
|
||||
import clsx from "clsx";
|
||||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||
import { t } from "../../i18n";
|
||||
import {
|
||||
@@ -6,43 +5,91 @@ import {
|
||||
useExcalidrawSetAppState,
|
||||
useExcalidrawActionManager,
|
||||
} 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 DropdownMenuItem from "../dropdownMenu/DropdownMenuItem";
|
||||
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 = () => {
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
if (appState.viewModeEnabled) {
|
||||
|
||||
if (!actionManager.isActionEnabled(actionLoadScene)) {
|
||||
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";
|
||||
|
||||
export const SaveToActiveFile = () => {
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
if (!appState.fileHandle) {
|
||||
|
||||
if (!actionManager.isActionEnabled(actionSaveToActiveFile)) {
|
||||
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";
|
||||
|
||||
export const SaveAsImage = () => {
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
// Hack until we tie "t" to lang state
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
icon={ExportImageIcon}
|
||||
dataTestId="image-export-button"
|
||||
data-testid="image-export-button"
|
||||
onSelect={() => setAppState({ openDialog: "imageExport" })}
|
||||
shortcut={getShortcutFromShortcutName("imageExport")}
|
||||
ariaLabel={t("buttons.exportImage")}
|
||||
aria-label={t("buttons.exportImage")}
|
||||
>
|
||||
{t("buttons.exportImage")}
|
||||
</DropdownMenuItem>
|
||||
@@ -51,32 +98,79 @@ export const SaveAsImage = () => {
|
||||
SaveAsImage.displayName = "SaveAsImage";
|
||||
|
||||
export const Help = () => {
|
||||
// Hack until we tie "t" to lang state
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
|
||||
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";
|
||||
|
||||
export const ClearCanvas = () => {
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom);
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
if (appState.viewModeEnabled) {
|
||||
if (!actionManager.isActionEnabled(actionClearCanvas)) {
|
||||
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";
|
||||
|
||||
export const ToggleTheme = () => {
|
||||
// Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
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";
|
||||
|
||||
@@ -101,7 +195,7 @@ export const ChangeCanvasBackground = () => {
|
||||
ChangeCanvasBackground.displayName = "ChangeCanvasBackground";
|
||||
|
||||
export const Export = () => {
|
||||
// Hack until we tie "t" to lang state
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
@@ -111,8 +205,8 @@ export const Export = () => {
|
||||
onSelect={() => {
|
||||
setAppState({ openDialog: "jsonExport" });
|
||||
}}
|
||||
dataTestId="json-export-button"
|
||||
ariaLabel={t("buttons.export")}
|
||||
data-testid="json-export-button"
|
||||
aria-label={t("buttons.export")}
|
||||
>
|
||||
{t("buttons.export")}
|
||||
</DropdownMenuItem>
|
||||
@@ -125,21 +219,21 @@ export const Socials = () => (
|
||||
<DropdownMenuItemLink
|
||||
icon={GithubIcon}
|
||||
href="https://github.com/excalidraw/excalidraw"
|
||||
ariaLabel="GitHub"
|
||||
aria-label="GitHub"
|
||||
>
|
||||
GitHub
|
||||
</DropdownMenuItemLink>
|
||||
<DropdownMenuItemLink
|
||||
icon={DiscordIcon}
|
||||
href="https://discord.gg/UexuTaE"
|
||||
ariaLabel="Discord"
|
||||
aria-label="Discord"
|
||||
>
|
||||
Discord
|
||||
</DropdownMenuItemLink>
|
||||
<DropdownMenuItemLink
|
||||
icon={TwitterIcon}
|
||||
href="https://twitter.com/excalidraw"
|
||||
ariaLabel="Twitter"
|
||||
aria-label="Twitter"
|
||||
>
|
||||
Twitter
|
||||
</DropdownMenuItemLink>
|
||||
@@ -147,20 +241,20 @@ export const Socials = () => (
|
||||
);
|
||||
Socials.displayName = "Socials";
|
||||
|
||||
export const LiveCollaboration = ({
|
||||
export const LiveCollaborationTrigger = ({
|
||||
onSelect,
|
||||
isCollaborating,
|
||||
}: {
|
||||
onSelect: () => void;
|
||||
isCollaborating: boolean;
|
||||
}) => {
|
||||
// Hack until we tie "t" to lang state
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
dataTestId="collab-button"
|
||||
icon={UsersIcon}
|
||||
data-testid="collab-button"
|
||||
icon={usersIcon}
|
||||
className={clsx({
|
||||
"active-collab": isCollaborating,
|
||||
})}
|
||||
@@ -171,4 +265,4 @@ export const LiveCollaboration = ({
|
||||
);
|
||||
};
|
||||
|
||||
LiveCollaboration.displayName = "LiveCollaboration";
|
||||
LiveCollaborationTrigger.displayName = "LiveCollaborationTrigger";
|
@@ -11,14 +11,25 @@ import * as DefaultItems from "./DefaultItems";
|
||||
import { UserList } from "../UserList";
|
||||
import { t } from "../../i18n";
|
||||
import { HamburgerMenuIcon } from "../icons";
|
||||
import { composeEventHandlers } from "../../utils";
|
||||
|
||||
const MainMenu = ({ children }: { children?: React.ReactNode }) => {
|
||||
const MainMenu = ({
|
||||
children,
|
||||
onSelect,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
/**
|
||||
* Called when any menu item is selected (clicked on).
|
||||
*/
|
||||
onSelect?: (event: Event) => void;
|
||||
}) => {
|
||||
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
|
||||
@@ -30,7 +41,12 @@ const MainMenu = ({ children }: { children?: React.ReactNode }) => {
|
||||
>
|
||||
{HamburgerMenuIcon}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content onClickOutside={onClickOutside}>
|
||||
<DropdownMenu.Content
|
||||
onClickOutside={onClickOutside}
|
||||
onSelect={composeEventHandlers(onSelect, () => {
|
||||
setAppState({ openMenu: null });
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
{device.isMobile && appState.collaborators.size > 0 && (
|
||||
<fieldset className="UserList-Wrapper">
|
195
src/components/welcome-screen/WelcomeScreen.Center.tsx
Normal file
195
src/components/welcome-screen/WelcomeScreen.Center.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import { actionLoadScene, actionShortcuts } from "../../actions";
|
||||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||
import { t } from "../../i18n";
|
||||
import {
|
||||
useDevice,
|
||||
useExcalidrawActionManager,
|
||||
useExcalidrawAppState,
|
||||
} from "../App";
|
||||
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 }) => {
|
||||
return (
|
||||
<div className="welcome-screen-center">
|
||||
{children || (
|
||||
<>
|
||||
<Logo />
|
||||
<Heading>{t("welcomeScreen.defaults.center_heading")}</Heading>
|
||||
<Menu>
|
||||
<MenuItemLoadScene />
|
||||
<MenuItemHelp />
|
||||
</Menu>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
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 };
|
42
src/components/welcome-screen/WelcomeScreen.Hints.tsx
Normal file
42
src/components/welcome-screen/WelcomeScreen.Hints.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { t } from "../../i18n";
|
||||
import {
|
||||
WelcomeScreenHelpArrow,
|
||||
WelcomeScreenMenuArrow,
|
||||
WelcomeScreenTopToolbarArrow,
|
||||
} from "../icons";
|
||||
|
||||
const MenuHint = ({ children }: { children?: React.ReactNode }) => {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
MenuHint.displayName = "MenuHint";
|
||||
|
||||
const ToolbarHint = ({ children }: { children?: React.ReactNode }) => {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
ToolbarHint.displayName = "ToolbarHint";
|
||||
|
||||
const HelpHint = ({ children }: { children?: React.ReactNode }) => {
|
||||
return (
|
||||
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--help">
|
||||
<div>{children || t("welcomeScreen.defaults.helpHint")}</div>
|
||||
{WelcomeScreenHelpArrow}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
HelpHint.displayName = "HelpHint";
|
||||
|
||||
export { HelpHint, MenuHint, ToolbarHint };
|
@@ -3,29 +3,39 @@
|
||||
font-family: "Virgil";
|
||||
}
|
||||
|
||||
.WelcomeScreen-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 0.75rem;
|
||||
font-size: 2.25rem;
|
||||
// WelcomeSreen common
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
svg {
|
||||
width: 1.625rem;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.WelcomeScreen-decor {
|
||||
.welcome-screen-decor {
|
||||
pointer-events: none;
|
||||
|
||||
color: var(--color-gray-40);
|
||||
}
|
||||
|
||||
&--subheading {
|
||||
font-size: 1.125rem;
|
||||
text-align: center;
|
||||
&.theme--dark {
|
||||
.welcome-screen-decor {
|
||||
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;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@@ -49,7 +59,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&--top-toolbar-pointer {
|
||||
&--toolbar {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
@@ -58,7 +68,7 @@
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
&__label {
|
||||
.welcome-screen-decor-hint__label {
|
||||
width: 120px;
|
||||
position: relative;
|
||||
top: -0.5rem;
|
||||
@@ -74,7 +84,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&--menu-pointer {
|
||||
&--menu {
|
||||
position: absolute;
|
||||
width: 320px;
|
||||
font-size: 1rem;
|
||||
@@ -95,10 +105,19 @@
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.welcome-screen-decor-hint__label {
|
||||
max-width: 160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.WelcomeScreen-container {
|
||||
// WelcomeSreen.Center
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.welcome-screen-center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
@@ -112,7 +131,24 @@
|
||||
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;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
@@ -120,7 +156,7 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.WelcomeScreen-item {
|
||||
.welcome-screen-menu-item {
|
||||
box-sizing: border-box;
|
||||
|
||||
pointer-events: all;
|
||||
@@ -128,8 +164,10 @@
|
||||
color: var(--color-gray-50);
|
||||
font-size: 0.875rem;
|
||||
|
||||
width: 100%;
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
max-width: 400px;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -140,44 +178,49 @@
|
||||
|
||||
border-radius: var(--border-radius-md);
|
||||
|
||||
&__label {
|
||||
grid-template-columns: calc(var(--default-icon-size) + 0.5rem) 1fr 3rem;
|
||||
|
||||
&__text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: auto;
|
||||
text-align: left;
|
||||
column-gap: 0.5rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: var(--default-icon-size);
|
||||
height: var(--default-icon-size);
|
||||
}
|
||||
&__icon {
|
||||
width: var(--default-icon-size);
|
||||
height: var(--default-icon-size);
|
||||
}
|
||||
|
||||
&__shortcut {
|
||||
margin-left: auto;
|
||||
color: var(--color-gray-40);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:active) .WelcomeScreen-item:hover {
|
||||
&:not(:active) .welcome-screen-menu-item:hover {
|
||||
text-decoration: none;
|
||||
background: var(--color-gray-10);
|
||||
|
||||
.WelcomeScreen-item__shortcut {
|
||||
.welcome-screen-menu-item__shortcut {
|
||||
color: var(--color-gray-50);
|
||||
}
|
||||
|
||||
.WelcomeScreen-item__label {
|
||||
.welcome-screen-menu-item__text {
|
||||
color: var(--color-gray-100);
|
||||
}
|
||||
}
|
||||
|
||||
.WelcomeScreen-item:active {
|
||||
.welcome-screen-menu-item:active {
|
||||
background: var(--color-gray-20);
|
||||
|
||||
.WelcomeScreen-item__shortcut {
|
||||
.welcome-screen-menu-item__shortcut {
|
||||
color: var(--color-gray-50);
|
||||
}
|
||||
|
||||
.WelcomeScreen-item__label {
|
||||
.welcome-screen-menu-item__text {
|
||||
color: var(--color-gray-100);
|
||||
}
|
||||
|
||||
@@ -185,7 +228,7 @@
|
||||
color: var(--color-promo) !important;
|
||||
|
||||
&:hover {
|
||||
.WelcomeScreen-item__label {
|
||||
.welcome-screen-menu-item__text {
|
||||
color: var(--color-promo) !important;
|
||||
}
|
||||
}
|
||||
@@ -193,11 +236,7 @@
|
||||
}
|
||||
|
||||
&.theme--dark {
|
||||
.WelcomeScreen-decor {
|
||||
color: var(--color-gray-60);
|
||||
}
|
||||
|
||||
.WelcomeScreen-item {
|
||||
.welcome-screen-menu-item {
|
||||
color: var(--color-gray-60);
|
||||
|
||||
&__shortcut {
|
||||
@@ -205,69 +244,41 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:active) .WelcomeScreen-item:hover {
|
||||
&:not(:active) .welcome-screen-menu-item:hover {
|
||||
background: var(--color-gray-85);
|
||||
|
||||
.WelcomeScreen-item__shortcut {
|
||||
.welcome-screen-menu-item__shortcut {
|
||||
color: var(--color-gray-50);
|
||||
}
|
||||
|
||||
.WelcomeScreen-item__label {
|
||||
.welcome-screen-menu-item__text {
|
||||
color: var(--color-gray-10);
|
||||
}
|
||||
}
|
||||
|
||||
.WelcomeScreen-item:active {
|
||||
.welcome-screen-menu-item:active {
|
||||
background-color: var(--color-gray-90);
|
||||
.WelcomeScreen-item__label {
|
||||
.welcome-screen-menu-item__text {
|
||||
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) {
|
||||
.WelcomeScreen-container {
|
||||
.welcome-screen-center {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
@media (min-height: 600px) and (max-height: 900px) {
|
||||
.WelcomeScreen-container {
|
||||
.welcome-screen-center {
|
||||
margin-top: 8rem;
|
||||
}
|
||||
}
|
||||
@media (max-height: 630px) {
|
||||
.WelcomeScreen-decor--top-toolbar-pointer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media (max-height: 500px) {
|
||||
.WelcomeScreen-container {
|
||||
@media (max-height: 500px), (max-width: 320px) {
|
||||
.welcome-screen-center {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// @media (max-height: 740px) {
|
||||
// .WelcomeScreen-decor {
|
||||
// &--help-pointer,
|
||||
// &--top-toolbar-pointer,
|
||||
// &--menu-pointer {
|
||||
// display: none;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ---------------------------------------------------------------------------
|
||||
}
|
17
src/components/welcome-screen/WelcomeScreen.tsx
Normal file
17
src/components/welcome-screen/WelcomeScreen.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Center } from "./WelcomeScreen.Center";
|
||||
import { MenuHint, ToolbarHint, HelpHint } from "./WelcomeScreen.Hints";
|
||||
|
||||
import "./WelcomeScreen.scss";
|
||||
|
||||
const WelcomeScreen = (props: { children: React.ReactNode }) => {
|
||||
// NOTE this component is used as a dummy wrapper to retrieve child props
|
||||
// from, and will never be rendered to DOM directly. As such, we can't
|
||||
// do anything here (use hooks and such)
|
||||
return null;
|
||||
};
|
||||
WelcomeScreen.displayName = "WelcomeScreen";
|
||||
|
||||
WelcomeScreen.Center = Center;
|
||||
WelcomeScreen.Hints = { MenuHint, ToolbarHint, HelpHint };
|
||||
|
||||
export default WelcomeScreen;
|
@@ -2,6 +2,14 @@ import cssVariables from "./css/variables.module.scss";
|
||||
import { AppProps } from "./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 DRAGGING_THRESHOLD = 10; // px
|
||||
@@ -54,6 +62,7 @@ export enum EVENT {
|
||||
SCROLL = "scroll",
|
||||
// custom events
|
||||
EXCALIDRAW_LINK = "excalidraw-link",
|
||||
MENU_ITEM_SELECT = "menu.itemSelect",
|
||||
}
|
||||
|
||||
export const ENV = {
|
||||
@@ -150,6 +159,7 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
||||
toggleTheme: null,
|
||||
saveAsImage: true,
|
||||
},
|
||||
welcomeScreen: true,
|
||||
};
|
||||
|
||||
// breakpoints
|
||||
@@ -236,14 +246,6 @@ export const ROUNDNESS = {
|
||||
ADAPTIVE_RADIUS: 3,
|
||||
} as const;
|
||||
|
||||
export const COOKIES = {
|
||||
AUTH_STATE_COOKIE: "excplus-auth",
|
||||
} as const;
|
||||
|
||||
/** key containt id of precedeing elemnt id we use in reconciliation during
|
||||
* collaboration */
|
||||
export const PRECEDING_ELEMENT_KEY = "__precedingElement__";
|
||||
|
||||
export const isExcalidrawPlusSignedUser = document.cookie.includes(
|
||||
COOKIES.AUTH_STATE_COOKIE,
|
||||
);
|
||||
|
@@ -408,7 +408,7 @@
|
||||
pointer-events: all;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--button-hover);
|
||||
background-color: var(--button-hover-bg);
|
||||
}
|
||||
|
||||
&:active {
|
||||
@@ -540,15 +540,16 @@
|
||||
}
|
||||
|
||||
.mobile-misc-tools-container {
|
||||
position: fixed;
|
||||
top: 5rem;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
top: calc(5rem - var(--editor-container-padding));
|
||||
right: calc(var(--editor-container-padding) * -1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--sidebar-border-color);
|
||||
border-top-left-radius: var(--border-radius-lg);
|
||||
border-bottom-left-radius: var(--border-radius-lg);
|
||||
border-right: 0;
|
||||
overflow: hidden;
|
||||
|
||||
background-color: var(--island-bg-color);
|
||||
|
||||
|
@@ -35,13 +35,14 @@
|
||||
--shadow-island: 0px 7px 14px rgba(0, 0, 0, 0.05),
|
||||
0px 0px 3.12708px rgba(0, 0, 0, 0.0798),
|
||||
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-button-size: 2rem;
|
||||
--default-icon-size: 1rem;
|
||||
--lg-button-size: 2.25rem;
|
||||
--lg-icon-size: 1rem;
|
||||
--editor-container-padding: 1rem;
|
||||
|
||||
@media screen and (min-device-width: 1921px) {
|
||||
--lg-button-size: 2.5rem;
|
||||
@@ -135,7 +136,7 @@
|
||||
--popup-text-inverted-color: #2c2c2c;
|
||||
--select-highlight-color: #{$oc-blue-4};
|
||||
--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);
|
||||
--shadow-island: 0px 13px 33px rgba(0, 0, 0, 0.07),
|
||||
0px 4.13px 9.94853px rgba(0, 0, 0, 0.0456112),
|
||||
|
@@ -39,11 +39,11 @@
|
||||
|
||||
.ToolIcon__icon {
|
||||
&:hover {
|
||||
background: var(--button-hover);
|
||||
background: var(--button-hover-bg);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--button-hover);
|
||||
background: var(--button-hover-bg);
|
||||
border: 1px solid var(--color-primary-darkest);
|
||||
}
|
||||
}
|
||||
@@ -54,24 +54,30 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.625rem;
|
||||
width: var(--default-button-size);
|
||||
height: var(--default-button-size);
|
||||
width: var(--button-width, var(--default-button-size));
|
||||
height: var(--button-height, var(--default-button-size));
|
||||
box-sizing: border-box;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--default-border-color);
|
||||
border-color: var(--button-border, var(--default-border-color));
|
||||
border-radius: var(--border-radius-lg);
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
color: var(--text-primary-color);
|
||||
background-color: var(--button-bg, var(--island-bg-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 {
|
||||
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 {
|
||||
background-color: var(--button-hover);
|
||||
border-color: var(--color-primary-darkest);
|
||||
background-color: var(--button-active-bg, var(--island-bg-color));
|
||||
border-color: var(--button-active-border, var(--color-primary-darkest));
|
||||
}
|
||||
|
||||
&.active {
|
||||
@@ -83,7 +89,7 @@
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--color-primary-darker);
|
||||
color: var(--button-color, var(--color-primary-darker));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import {
|
||||
copyBlobToClipboardAsPng,
|
||||
copyTextToSystemClipboard,
|
||||
} 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 { t } from "../i18n";
|
||||
import { exportToCanvas, exportToSvg } from "../scene/export";
|
||||
@@ -97,10 +97,21 @@ export const exportCanvas = async (
|
||||
const blob = canvasToBlob(tempCanvas);
|
||||
await copyBlobToClipboardAsPng(blob);
|
||||
} catch (error: any) {
|
||||
console.warn(error);
|
||||
if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
|
||||
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 {
|
||||
tempCanvas.remove();
|
||||
}
|
||||
|
@@ -55,6 +55,7 @@ export const AllowedExcalidrawActiveTools: Record<
|
||||
freedraw: true,
|
||||
eraser: false,
|
||||
custom: true,
|
||||
hand: true,
|
||||
};
|
||||
|
||||
export type RestoredDataState = {
|
||||
@@ -465,7 +466,7 @@ export const restoreAppState = (
|
||||
? nextAppState.activeTool
|
||||
: { type: "selection" },
|
||||
),
|
||||
lastActiveToolBeforeEraser: null,
|
||||
lastActiveTool: null,
|
||||
locked: nextAppState.activeTool.locked ?? false,
|
||||
},
|
||||
// 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,
|
||||
contextItemLabel: (elements, appState) =>
|
||||
getContextMenuLabel(elements, appState),
|
||||
contextItemPredicate: (elements, appState) => {
|
||||
predicate: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
return selectedElements.length === 1;
|
||||
},
|
||||
|
@@ -557,10 +557,10 @@ export const resizeSingleElement = (
|
||||
mutateElement(element, {
|
||||
scale: [
|
||||
// defaulting because scaleX/Y can be 0/-0
|
||||
(Math.sign(scaleX) || stateAtResizeStart.scale[0]) *
|
||||
stateAtResizeStart.scale[0],
|
||||
(Math.sign(scaleY) || stateAtResizeStart.scale[1]) *
|
||||
stateAtResizeStart.scale[1],
|
||||
(Math.sign(newBoundsX2 - stateAtResizeStart.x) ||
|
||||
stateAtResizeStart.scale[0]) * stateAtResizeStart.scale[0],
|
||||
(Math.sign(newBoundsY2 - stateAtResizeStart.y) ||
|
||||
stateAtResizeStart.scale[1]) * stateAtResizeStart.scale[1],
|
||||
],
|
||||
});
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ export const showSelectedShapeActions = (
|
||||
appState.activeTool.type !== "custom" &&
|
||||
(appState.editingElement ||
|
||||
(appState.activeTool.type !== "selection" &&
|
||||
appState.activeTool.type !== "eraser"))) ||
|
||||
appState.activeTool.type !== "eraser" &&
|
||||
appState.activeTool.type !== "hand"))) ||
|
||||
getSelectedElements(elements, appState).length,
|
||||
);
|
||||
|
@@ -38,3 +38,11 @@ export const STORAGE_KEYS = {
|
||||
VERSION_DATA_STATE: "version-dataState",
|
||||
VERSION_FILES: "version-files",
|
||||
} as const;
|
||||
|
||||
export const COOKIES = {
|
||||
AUTH_STATE_COOKIE: "excplus-auth",
|
||||
} as const;
|
||||
|
||||
export const isExcalidrawPlusSignedUser = document.cookie.includes(
|
||||
COOKIES.AUTH_STATE_COOKIE,
|
||||
);
|
||||
|
@@ -263,6 +263,13 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
),
|
||||
);
|
||||
|
||||
if (this.portal.socket && this.fallbackInitializationHandler) {
|
||||
this.portal.socket.off(
|
||||
"connect_error",
|
||||
this.fallbackInitializationHandler,
|
||||
);
|
||||
}
|
||||
|
||||
if (!keepRemoteState) {
|
||||
LocalData.fileStorage.reset();
|
||||
this.destroySocketClient();
|
||||
@@ -358,6 +365,8 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
}
|
||||
};
|
||||
|
||||
private fallbackInitializationHandler: null | (() => any) = null;
|
||||
|
||||
startCollaboration = async (
|
||||
existingRoomLinkData: null | { roomId: string; roomKey: string },
|
||||
): Promise<ImportedDataState | null> => {
|
||||
@@ -396,6 +405,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
scenePromise.resolve(scene);
|
||||
});
|
||||
};
|
||||
this.fallbackInitializationHandler = fallbackInitializationHandler;
|
||||
|
||||
try {
|
||||
const socketServerData = await getCollabServer();
|
||||
@@ -410,27 +420,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
roomKey,
|
||||
);
|
||||
|
||||
this.portal.socket.on("disconnect", (reason) => {
|
||||
// reason `io server disconnect` probably means CORS issue
|
||||
console.warn(
|
||||
`${
|
||||
this.portal.socketInitialized ? "initialized" : "uninitialized"
|
||||
} socket disconnected from server: ${reason}`,
|
||||
);
|
||||
this.setState({
|
||||
errorMessage: this.portal.socketInitialized
|
||||
? t("errors.socketDisconnected")
|
||||
: t("errors.socketConnectionError"),
|
||||
});
|
||||
fallbackInitializationHandler();
|
||||
});
|
||||
|
||||
this.portal.socket.on("connect_error", () => {
|
||||
fallbackInitializationHandler();
|
||||
this.setState({
|
||||
errorMessage: t("errors.socketConnectionError"),
|
||||
});
|
||||
});
|
||||
this.portal.socket.once("connect_error", fallbackInitializationHandler);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
this.setState({ errorMessage: error.message });
|
||||
@@ -457,7 +447,6 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
this.saveCollabRoomToFirebase(getSyncableElements(elements));
|
||||
}
|
||||
|
||||
clearTimeout(this.socketInitializationTimer!);
|
||||
// fallback in case you're not alone in the room but still don't receive
|
||||
// initial SCENE_INIT message
|
||||
this.socketInitializationTimer = window.setTimeout(
|
||||
@@ -568,12 +557,12 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
}
|
||||
| { fetchScene: false; roomLinkData?: null }) => {
|
||||
clearTimeout(this.socketInitializationTimer!);
|
||||
|
||||
if (!this.portal.socket || this.portal.socketInitialized) {
|
||||
return null;
|
||||
if (this.portal.socket && this.fallbackInitializationHandler) {
|
||||
this.portal.socket.off(
|
||||
"connect_error",
|
||||
this.fallbackInitializationHandler,
|
||||
);
|
||||
}
|
||||
this.portal.socketInitialized = true;
|
||||
|
||||
if (fetchScene && roomLinkData && this.portal.socket) {
|
||||
this.excalidrawAPI.resetScene();
|
||||
|
||||
@@ -596,7 +585,11 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
} catch (error: any) {
|
||||
// log the error and move on. other peers will sync us the scene.
|
||||
console.error(error);
|
||||
} finally {
|
||||
this.portal.socketInitialized = true;
|
||||
}
|
||||
} else {
|
||||
this.portal.socketInitialized = true;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
@@ -19,11 +19,10 @@ import { newElementWith } from "../../element/mutateElement";
|
||||
import { BroadcastedExcalidrawElement } from "./reconciliation";
|
||||
import { encryptData } from "../../data/encryption";
|
||||
import { PRECEDING_ELEMENT_KEY } from "../../constants";
|
||||
import { Socket } from "socket.io-client";
|
||||
|
||||
class Portal {
|
||||
collab: TCollabClass;
|
||||
socket: Socket | null = null;
|
||||
socket: SocketIOClient.Socket | null = null;
|
||||
socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized
|
||||
roomId: string | null = null;
|
||||
roomKey: string | null = null;
|
||||
@@ -33,7 +32,7 @@ class Portal {
|
||||
this.collab = collab;
|
||||
}
|
||||
|
||||
open(socket: Socket, id: string, key: string) {
|
||||
open(socket: SocketIOClient.Socket, id: string, key: string) {
|
||||
this.socket = socket;
|
||||
this.roomId = id;
|
||||
this.roomKey = key;
|
||||
@@ -64,7 +63,6 @@ class Portal {
|
||||
return;
|
||||
}
|
||||
this.queueFileUpload.flush();
|
||||
this.socket.off();
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
this.roomId = null;
|
||||
@@ -144,12 +142,7 @@ class Portal {
|
||||
// periodically we'll resync the whole thing to make sure no one diverges
|
||||
// due to a dropped message (server goes down etc).
|
||||
const syncableElements = allElements.reduce(
|
||||
(
|
||||
acc: BroadcastedExcalidrawElement[],
|
||||
element: BroadcastedExcalidrawElement,
|
||||
idx,
|
||||
elements,
|
||||
) => {
|
||||
(acc, element: BroadcastedExcalidrawElement, idx, elements) => {
|
||||
if (
|
||||
(syncAll ||
|
||||
!this.broadcastedElementVersions.has(element.id) ||
|
||||
@@ -165,7 +158,7 @@ class Portal {
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
[] as BroadcastedExcalidrawElement[],
|
||||
);
|
||||
|
||||
const data: SocketUpdateDataSource[typeof updateType] = {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { isExcalidrawPlusSignedUser } from "../../constants";
|
||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||
|
||||
export const ExcalidrawPlusAppLink = () => {
|
||||
if (!isExcalidrawPlusSignedUser) {
|
||||
|
@@ -14,7 +14,6 @@ import { encryptData, decryptData } from "../../data/encryption";
|
||||
import { MIME_TYPES } from "../../constants";
|
||||
import { reconcileElements } from "../collab/reconciliation";
|
||||
import { getSyncableElements, SyncableExcalidrawElement } from ".";
|
||||
import { Socket } from "socket.io-client";
|
||||
|
||||
// private
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -130,12 +129,12 @@ const decryptElements = async (
|
||||
};
|
||||
|
||||
class FirebaseSceneVersionCache {
|
||||
private static cache = new WeakMap<Socket, number>();
|
||||
static get = (socket: Socket) => {
|
||||
private static cache = new WeakMap<SocketIOClient.Socket, number>();
|
||||
static get = (socket: SocketIOClient.Socket) => {
|
||||
return FirebaseSceneVersionCache.cache.get(socket);
|
||||
};
|
||||
static set = (
|
||||
socket: Socket,
|
||||
socket: SocketIOClient.Socket,
|
||||
elements: readonly SyncableExcalidrawElement[],
|
||||
) => {
|
||||
FirebaseSceneVersionCache.cache.set(socket, getSceneVersion(elements));
|
||||
@@ -277,7 +276,7 @@ export const saveToFirebase = async (
|
||||
export const loadFromFirebase = async (
|
||||
roomId: string,
|
||||
roomKey: string,
|
||||
socket: Socket | null,
|
||||
socket: SocketIOClient.Socket | null,
|
||||
): Promise<readonly ExcalidrawElement[] | null> => {
|
||||
const firebase = await loadFirestore();
|
||||
const db = firebase.firestore();
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import polyfill from "../polyfill";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import { ErrorDialog } from "../components/ErrorDialog";
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
defaultLang,
|
||||
Footer,
|
||||
MainMenu,
|
||||
LiveCollaborationTrigger,
|
||||
WelcomeScreen,
|
||||
} from "../packages/excalidraw/index";
|
||||
import {
|
||||
AppState,
|
||||
@@ -45,6 +47,7 @@ import {
|
||||
} from "../utils";
|
||||
import {
|
||||
FIREBASE_STORAGE_PREFIXES,
|
||||
isExcalidrawPlusSignedUser,
|
||||
STORAGE_KEYS,
|
||||
SYNC_BROWSER_TABS_TIMEOUT,
|
||||
} from "./app_constants";
|
||||
@@ -608,7 +611,7 @@ const ExcalidrawWrapper = () => {
|
||||
<MainMenu.DefaultItems.SaveToActiveFile />
|
||||
<MainMenu.DefaultItems.Export />
|
||||
<MainMenu.DefaultItems.SaveAsImage />
|
||||
<MainMenu.DefaultItems.LiveCollaboration
|
||||
<MainMenu.DefaultItems.LiveCollaborationTrigger
|
||||
isCollaborating={isCollaborating}
|
||||
onSelect={() => setCollabDialogShown(true)}
|
||||
/>
|
||||
@@ -634,6 +637,63 @@ const ExcalidrawWrapper = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const welcomeScreenJSX = useMemo(() => {
|
||||
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={() => 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>
|
||||
);
|
||||
}, [setCollabDialogShown]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ height: "100%" }}
|
||||
@@ -645,7 +705,6 @@ const ExcalidrawWrapper = () => {
|
||||
ref={excalidrawRefCallback}
|
||||
onChange={onChange}
|
||||
initialData={initialStatePromiseRef.current.promise}
|
||||
onCollabButtonClick={() => setCollabDialogShown(true)}
|
||||
isCollaborating={isCollaborating}
|
||||
onPointerUpdate={collabAPI?.onPointerUpdate}
|
||||
UIOptions={{
|
||||
@@ -679,14 +738,27 @@ const ExcalidrawWrapper = () => {
|
||||
onLibraryChange={onLibraryChange}
|
||||
autoFocus={true}
|
||||
theme={theme}
|
||||
renderTopRightUI={(isMobile) => {
|
||||
if (isMobile) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<LiveCollaborationTrigger
|
||||
isCollaborating={isCollaborating}
|
||||
onSelect={() => setCollabDialogShown(true)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
>
|
||||
{renderMenu()}
|
||||
|
||||
<Footer>
|
||||
<div style={{ display: "flex", gap: ".5rem", alignItems: "center" }}>
|
||||
<ExcalidrawPlusAppLink />
|
||||
<EncryptedIcon />
|
||||
</div>
|
||||
</Footer>
|
||||
{welcomeScreenJSX}
|
||||
</Excalidraw>
|
||||
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
|
||||
{errorMessage && (
|
||||
|
@@ -1,6 +1,4 @@
|
||||
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
|
||||
export const isWindows = /^Win/.test(window.navigator.platform);
|
||||
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
|
||||
import { isDarwin } from "./constants";
|
||||
|
||||
export const CODES = {
|
||||
EQUAL: "Equal",
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "لصق",
|
||||
"pasteAsPlaintext": "",
|
||||
"pasteAsPlaintext": "اللصق كنص عادي",
|
||||
"pasteCharts": "لصق الرسوم البيانية",
|
||||
"selectAll": "تحديد الكل",
|
||||
"multiSelect": "إضافة عنصر للتحديد",
|
||||
@@ -66,13 +66,13 @@
|
||||
"cartoonist": "كرتوني",
|
||||
"fileTitle": "إسم الملف",
|
||||
"colorPicker": "منتقي اللون",
|
||||
"canvasColors": "",
|
||||
"canvasColors": "تستخدم على القماش",
|
||||
"canvasBackground": "خلفية اللوحة",
|
||||
"drawingCanvas": "لوحة الرسم",
|
||||
"layers": "الطبقات",
|
||||
"actions": "الإجراءات",
|
||||
"language": "اللغة",
|
||||
"liveCollaboration": "",
|
||||
"liveCollaboration": "التعاون المباشر...",
|
||||
"duplicateSelection": "تكرار",
|
||||
"untitled": "غير معنون",
|
||||
"name": "الاسم",
|
||||
@@ -108,7 +108,7 @@
|
||||
"excalidrawLib": "مكتبتنا",
|
||||
"decreaseFontSize": "تصغير حجم الخط",
|
||||
"increaseFontSize": "تكبير حجم الخط",
|
||||
"unbindText": "",
|
||||
"unbindText": "فك ربط النص",
|
||||
"bindText": "",
|
||||
"link": {
|
||||
"edit": "تعديل الرابط",
|
||||
@@ -145,7 +145,7 @@
|
||||
"scale": "مقاس",
|
||||
"save": "احفظ للملف الحالي",
|
||||
"saveAs": "حفظ كـ",
|
||||
"load": "",
|
||||
"load": "فتح",
|
||||
"getShareableLink": "احصل على رابط المشاركة",
|
||||
"close": "غلق",
|
||||
"selectLanguage": "اختر اللغة",
|
||||
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "تعذر إدراج صورة SVG. يبدو أن ترميز SVG غير صحيح.",
|
||||
"invalidSVGString": "SVG غير صالح.",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": ""
|
||||
"importLibraryError": "",
|
||||
"collabSaveFailed": "",
|
||||
"collabSaveFailed_sizeExceeded": ""
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "تحديد",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": "برتقالي 9"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "",
|
||||
"switchToPlusApp": "",
|
||||
"menuHints": "",
|
||||
"toolbarHints": "",
|
||||
"helpHints": ""
|
||||
"app": {
|
||||
"center_heading": "",
|
||||
"center_heading_plus": "",
|
||||
"menuHint": ""
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "",
|
||||
"invalidSVGString": "",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": ""
|
||||
"importLibraryError": "",
|
||||
"collabSaveFailed": "",
|
||||
"collabSaveFailed_sizeExceeded": ""
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Селекция",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": ""
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "",
|
||||
"switchToPlusApp": "",
|
||||
"menuHints": "",
|
||||
"toolbarHints": "",
|
||||
"helpHints": ""
|
||||
"app": {
|
||||
"center_heading": "",
|
||||
"center_heading_plus": "",
|
||||
"menuHint": ""
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "এসভীজী ছবি সন্নিবেশ করা যায়নি। এসভীজী মার্কআপটি অবৈধ মনে হচ্ছে৷",
|
||||
"invalidSVGString": "এসভীজী মার্কআপটি অবৈধ মনে হচ্ছে৷",
|
||||
"cannotResolveCollabServer": "কোল্যাব সার্ভারের সাথে সংযোগ করা যায়নি। পৃষ্ঠাটি পুনরায় লোড করে আবার চেষ্টা করুন।",
|
||||
"importLibraryError": "সংগ্রহ লোড করা যায়নি"
|
||||
"importLibraryError": "সংগ্রহ লোড করা যায়নি",
|
||||
"collabSaveFailed": "",
|
||||
"collabSaveFailed_sizeExceeded": ""
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "বাছাই",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": ""
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "",
|
||||
"switchToPlusApp": "",
|
||||
"menuHints": "",
|
||||
"toolbarHints": "",
|
||||
"helpHints": ""
|
||||
"app": {
|
||||
"center_heading": "",
|
||||
"center_heading_plus": "",
|
||||
"menuHint": ""
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "No ha estat possible inserir la imatge SVG. Les marques SVG semblen invàlides.",
|
||||
"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.",
|
||||
"importLibraryError": "No s'ha pogut carregar la biblioteca"
|
||||
"importLibraryError": "No s'ha pogut carregar la biblioteca",
|
||||
"collabSaveFailed": "",
|
||||
"collabSaveFailed_sizeExceeded": ""
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Selecció",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": "Taronja 9"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "",
|
||||
"switchToPlusApp": "",
|
||||
"menuHints": "",
|
||||
"toolbarHints": "",
|
||||
"helpHints": ""
|
||||
"app": {
|
||||
"center_heading": "",
|
||||
"center_heading_plus": "",
|
||||
"menuHint": ""
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "",
|
||||
"invalidSVGString": "",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": ""
|
||||
"importLibraryError": "",
|
||||
"collabSaveFailed": "",
|
||||
"collabSaveFailed_sizeExceeded": ""
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Výběr",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": "Oranzova"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "",
|
||||
"switchToPlusApp": "",
|
||||
"menuHints": "",
|
||||
"toolbarHints": "",
|
||||
"helpHints": ""
|
||||
"app": {
|
||||
"center_heading": "",
|
||||
"center_heading_plus": "",
|
||||
"menuHint": ""
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "",
|
||||
"invalidSVGString": "",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": ""
|
||||
"importLibraryError": "",
|
||||
"collabSaveFailed": "",
|
||||
"collabSaveFailed_sizeExceeded": ""
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": ""
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "",
|
||||
"switchToPlusApp": "",
|
||||
"menuHints": "",
|
||||
"toolbarHints": "",
|
||||
"helpHints": ""
|
||||
"app": {
|
||||
"center_heading": "",
|
||||
"center_heading_plus": "",
|
||||
"menuHint": ""
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -99,14 +99,14 @@
|
||||
"flipHorizontal": "Horizontal spiegeln",
|
||||
"flipVertical": "Vertikal spiegeln",
|
||||
"viewMode": "Ansichtsmodus",
|
||||
"toggleExportColorScheme": "Exportfarbschema umschalten",
|
||||
"toggleExportColorScheme": "Farbschema für Export umschalten",
|
||||
"share": "Teilen",
|
||||
"showStroke": "Auswahl für Strichfarbe anzeigen",
|
||||
"showBackground": "Hintergrundfarbe auswählen",
|
||||
"toggleTheme": "Thema umschalten",
|
||||
"toggleTheme": "Design umschalten",
|
||||
"personalLib": "Persönliche Bibliothek",
|
||||
"excalidrawLib": "Excalidraw-Bibliothek",
|
||||
"decreaseFontSize": "Schrift verkleinern",
|
||||
"excalidrawLib": "Excalidraw Bibliothek",
|
||||
"decreaseFontSize": "Schriftgröße verkleinern",
|
||||
"increaseFontSize": "Schrift vergrößern",
|
||||
"unbindText": "Text lösen",
|
||||
"bindText": "Text an Container binden",
|
||||
@@ -161,8 +161,8 @@
|
||||
"resetLibrary": "Bibliothek zurücksetzen",
|
||||
"createNewRoom": "Neuen Raum erstellen",
|
||||
"fullScreen": "Vollbildanzeige",
|
||||
"darkMode": "Dunkler Modus",
|
||||
"lightMode": "Heller Modus",
|
||||
"darkMode": "Dunkles Design",
|
||||
"lightMode": "Helles Design",
|
||||
"zenMode": "Zen-Modus",
|
||||
"exitZenMode": "Zen-Modus verlassen",
|
||||
"cancel": "Abbrechen",
|
||||
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "SVG-Bild konnte nicht eingefügt werden. Das SVG-Markup sieht ungültig aus.",
|
||||
"invalidSVGString": "Ungültige SVG.",
|
||||
"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": {
|
||||
"selection": "Auswahl",
|
||||
@@ -312,8 +314,8 @@
|
||||
"zoomToFit": "Zoomen um alle Elemente einzupassen",
|
||||
"zoomToSelection": "Auf Auswahl zoomen",
|
||||
"toggleElementLock": "Auswahl sperren/entsperren",
|
||||
"movePageUpDown": "",
|
||||
"movePageLeftRight": ""
|
||||
"movePageUpDown": "Seite nach oben/unten verschieben",
|
||||
"movePageLeftRight": "Seite nach links/rechts verschieben"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": "Zeichenfläche löschen"
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": "Orange 9"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "Alle Daten werden lokal in Deinem Browser gespeichert.",
|
||||
"switchToPlusApp": "Möchtest du stattdessen zu Excalidraw+ gehen?",
|
||||
"menuHints": "Exportieren, Einstellungen, Sprachen, ...",
|
||||
"toolbarHints": "Wähle ein Werkzeug & beginne zu zeichnen!",
|
||||
"helpHints": "Kurzbefehle & Hilfe"
|
||||
"app": {
|
||||
"center_heading": "Alle Daten werden lokal in Deinem Browser gespeichert.",
|
||||
"center_heading_plus": "Möchtest du stattdessen zu Excalidraw+ gehen?",
|
||||
"menuHint": "Exportieren, Einstellungen, Sprachen, ..."
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "Exportieren, Einstellungen und mehr...",
|
||||
"center_heading": "Diagramme. Einfach. Gemacht.",
|
||||
"toolbarHint": "Wähle ein Werkzeug & beginne zu zeichnen!",
|
||||
"helpHint": "Kurzbefehle & Hilfe"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "Επικόλληση",
|
||||
"pasteAsPlaintext": "",
|
||||
"pasteAsPlaintext": "Επικόλληση ως απλό κείμενο",
|
||||
"pasteCharts": "Επικόλληση γραφημάτων",
|
||||
"selectAll": "Επιλογή όλων",
|
||||
"multiSelect": "Προσθέστε το στοιχείο στην επιλογή",
|
||||
@@ -72,7 +72,7 @@
|
||||
"layers": "Στρώματα",
|
||||
"actions": "Ενέργειες",
|
||||
"language": "Γλώσσα",
|
||||
"liveCollaboration": "",
|
||||
"liveCollaboration": "Live συνεργασία...",
|
||||
"duplicateSelection": "Δημιουργία αντιγράφου",
|
||||
"untitled": "Χωρίς τίτλο",
|
||||
"name": "Όνομα",
|
||||
@@ -116,8 +116,8 @@
|
||||
"label": "Σύνδεσμος"
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "",
|
||||
"exit": ""
|
||||
"edit": "Επεξεργασία γραμμής",
|
||||
"exit": "Έξοδος επεξεργαστή κειμένου"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "Κλείδωμα",
|
||||
@@ -136,8 +136,8 @@
|
||||
"buttons": {
|
||||
"clearReset": "Επαναφορά του καμβά",
|
||||
"exportJSON": "Εξαγωγή σε αρχείο",
|
||||
"exportImage": "",
|
||||
"export": "",
|
||||
"exportImage": "Εξαγωγή εικόνας...",
|
||||
"export": "Αποθήκευση ως...",
|
||||
"exportToPng": "Εξαγωγή σε PNG",
|
||||
"exportToSvg": "Εξαγωγή σε SVG",
|
||||
"copyToClipboard": "Αντιγραφή στο πρόχειρο",
|
||||
@@ -145,7 +145,7 @@
|
||||
"scale": "Κλίμακα",
|
||||
"save": "Αποθήκευση στο τρέχον αρχείο",
|
||||
"saveAs": "Αποθήκευση ως",
|
||||
"load": "",
|
||||
"load": "Άνοιγμα",
|
||||
"getShareableLink": "Δημόσιος σύνδεσμος",
|
||||
"close": "Κλείσιμο",
|
||||
"selectLanguage": "Επιλογή γλώσσας",
|
||||
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "Αδυναμία εισαγωγής εικόνας SVG. Η σήμανση της SVG δεν φαίνεται έγκυρη.",
|
||||
"invalidSVGString": "Μη έγκυρο SVG.",
|
||||
"cannotResolveCollabServer": "Αδυναμία σύνδεσης με τον διακομιστή συνεργασίας. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά.",
|
||||
"importLibraryError": "Αδυναμία φόρτωσης βιβλιοθήκης"
|
||||
"importLibraryError": "Αδυναμία φόρτωσης βιβλιοθήκης",
|
||||
"collabSaveFailed": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή. Αν το προβλήματα παραμείνει, θα πρέπει να αποθηκεύσετε το αρχείο σας τοπικά για να βεβαιωθείτε ότι δεν χάνετε την εργασία σας.",
|
||||
"collabSaveFailed_sizeExceeded": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή, ο καμβάς φαίνεται να είναι πολύ μεγάλος. Θα πρέπει να αποθηκεύσετε το αρχείο τοπικά για να βεβαιωθείτε ότι δεν θα χάσετε την εργασία σας."
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Επιλογή",
|
||||
@@ -215,7 +217,7 @@
|
||||
"text": "Κείμενο",
|
||||
"library": "Βιβλιοθήκη",
|
||||
"lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο",
|
||||
"penMode": "",
|
||||
"penMode": "Λειτουργία μολυβιού - αποτροπή αφής",
|
||||
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
|
||||
"eraser": "Γόμα"
|
||||
},
|
||||
@@ -236,7 +238,7 @@
|
||||
"resize": "Μπορείς να περιορίσεις τις αναλογίες κρατώντας το SHIFT ενώ αλλάζεις μέγεθος,\nκράτησε πατημένο το ALT για αλλαγή μεγέθους από το κέντρο",
|
||||
"resizeImage": "Μπορείτε να αλλάξετε το μέγεθος ελεύθερα κρατώντας πατημένο το SHIFT,\nκρατήστε πατημένο το ALT για να αλλάξετε το μέγεθος από το κέντρο",
|
||||
"rotate": "Μπορείς να περιορίσεις τις γωνίες κρατώντας πατημένο το πλήκτρο SHIFT κατά την περιστροφή",
|
||||
"lineEditor_info": "",
|
||||
"lineEditor_info": "Κρατήστε πατημένο Ctrl ή Cmd και πατήστε το πλήκτρο Ctrl ή Cmd + Enter για επεξεργασία σημείων",
|
||||
"lineEditor_pointSelected": "Πατήστε Διαγραφή για αφαίρεση σημείου(ων),\nCtrlOrCmd+D για αντιγραφή, ή σύρετε για μετακίνηση",
|
||||
"lineEditor_nothingSelected": "Επιλέξτε ένα σημείο για να επεξεργαστείτε (κρατήστε πατημένο το SHIFT για να επιλέξετε πολλαπλά),\nή κρατήστε πατημένο το Alt και κάντε κλικ για να προσθέσετε νέα σημεία",
|
||||
"placeImage": "Κάντε κλικ για να τοποθετήσετε την εικόνα ή κάντε κλικ και σύρετε για να ορίσετε το μέγεθός της χειροκίνητα",
|
||||
@@ -312,8 +314,8 @@
|
||||
"zoomToFit": "Zoom ώστε να χωρέσουν όλα τα στοιχεία",
|
||||
"zoomToSelection": "Ζουμ στην επιλογή",
|
||||
"toggleElementLock": "Κλείδωμα/Ξεκλείδωμα επιλογής",
|
||||
"movePageUpDown": "",
|
||||
"movePageLeftRight": ""
|
||||
"movePageUpDown": "Μετακίνηση σελίδας πάνω/κάτω",
|
||||
"movePageLeftRight": "Μετακίνηση σελίδας αριστερά/δεξιά"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": "Καθαρισμός καμβά"
|
||||
@@ -395,7 +397,7 @@
|
||||
"fileSavedToFilename": "Αποθηκεύτηκε στο {filename}",
|
||||
"canvas": "καμβάς",
|
||||
"selection": "επιλογή",
|
||||
"pasteAsSingleElement": ""
|
||||
"pasteAsSingleElement": "Χρησιμοποίησε το {{shortcut}} για να επικολλήσεις ως ένα μόνο στοιχείο,\nή να επικολλήσεις σε έναν υπάρχοντα επεξεργαστή κειμένου"
|
||||
},
|
||||
"colors": {
|
||||
"ffffff": "Λευκό",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": "Πορτοκαλί 9"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "",
|
||||
"switchToPlusApp": "",
|
||||
"menuHints": "",
|
||||
"toolbarHints": "",
|
||||
"helpHints": ""
|
||||
"app": {
|
||||
"center_heading": "Όλα τα δεδομένα σας αποθηκεύονται τοπικά στο πρόγραμμα περιήγησης.",
|
||||
"center_heading_plus": "Μήπως θέλατε να πάτε στο Excalidraw+;",
|
||||
"menuHint": "Εξαγωγή, προτιμήσεις, γλώσσες, ..."
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "Εξαγωγή, προτιμήσεις και άλλες επιλογές...",
|
||||
"center_heading": "Διαγράμματα. Εύκολα. Γρήγορα.",
|
||||
"toolbarHint": "Επιλέξτε ένα εργαλείο και ξεκινήστε να σχεδιάζεται!",
|
||||
"helpHint": "Συντομεύσεις και βοήθεια"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -203,8 +203,6 @@
|
||||
"invalidSVGString": "Invalid SVG.",
|
||||
"cannotResolveCollabServer": "Couldn't connect to the collab server. Please reload the page and try again.",
|
||||
"importLibraryError": "Couldn't load library",
|
||||
"socketDisconnected": "You've been disconnected from the server",
|
||||
"socketConnectionError": "Couldn't connect to the server",
|
||||
"collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.",
|
||||
"collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work."
|
||||
},
|
||||
@@ -222,7 +220,8 @@
|
||||
"lock": "Keep selected tool active after drawing",
|
||||
"penMode": "Pen mode - prevent touch",
|
||||
"link": "Add/ Update link for a selected shape",
|
||||
"eraser": "Eraser"
|
||||
"eraser": "Eraser",
|
||||
"hand": "Hand (panning tool)"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Canvas actions",
|
||||
@@ -230,7 +229,7 @@
|
||||
"shapes": "Shapes"
|
||||
},
|
||||
"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",
|
||||
"freeDraw": "Click and drag, release when you're finished",
|
||||
"text": "Tip: you can also add text by double-clicking anywhere with the selection tool",
|
||||
@@ -248,7 +247,8 @@
|
||||
"publishLibrary": "Publish your own library",
|
||||
"bindTextToElement": "Press enter to add text",
|
||||
"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": {
|
||||
"cannotShowPreview": "Cannot show preview",
|
||||
@@ -450,10 +450,16 @@
|
||||
"d9480f": "Orange 9"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "All your data is saved locally in your browser.",
|
||||
"switchToPlusApp": "Did you want to go to the Excalidraw+ instead?",
|
||||
"menuHints": "Export, preferences, languages, ...",
|
||||
"toolbarHints": "Pick a tool & Start drawing!",
|
||||
"helpHints": "Shortcuts & help"
|
||||
"app": {
|
||||
"center_heading": "All your data is saved locally in your browser.",
|
||||
"center_heading_plus": "Did you want to go to the Excalidraw+ instead?",
|
||||
"menuHint": "Export, preferences, languages, ..."
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "Export, preferences, and more...",
|
||||
"center_heading": "Diagrams. Made. Simple.",
|
||||
"toolbarHint": "Pick a tool & Start drawing!",
|
||||
"helpHint": "Shortcuts & help"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "No se pudo insertar la imagen SVG. El código SVG parece invá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.",
|
||||
"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": {
|
||||
"selection": "Selección",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": "Naranja 9"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "Toda su información es guardada localmente en su navegador.",
|
||||
"switchToPlusApp": "¿Quieres ir a Excalidraw+ en su lugar?",
|
||||
"menuHints": "Exportar, preferencias, idiomas, ...",
|
||||
"toolbarHints": "¡Escoge una herramienta & Empiece a dibujar!",
|
||||
"helpHints": "Atajos & ayuda"
|
||||
"app": {
|
||||
"center_heading": "Toda su información es guardada localmente en su navegador.",
|
||||
"center_heading_plus": "¿Quieres ir a Excalidraw+?",
|
||||
"menuHint": "Exportar, preferencias, idiomas, ..."
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "Exportar, preferencias y más...",
|
||||
"center_heading": "Diagramas. Hecho. Simplemente.",
|
||||
"toolbarHint": "¡Elige una herramienta y empieza a dibujar!",
|
||||
"helpHint": "Atajos & ayuda"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "Itsatsi",
|
||||
"pasteAsPlaintext": "",
|
||||
"pasteAsPlaintext": "Itsatsi testu arrunt gisa",
|
||||
"pasteCharts": "Itsatsi grafikoak",
|
||||
"selectAll": "Hautatu dena",
|
||||
"multiSelect": "Gehitu elementua hautapenera",
|
||||
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "Ezin izan da SVG irudia txertatu. SVG markak baliogabea dirudi.",
|
||||
"invalidSVGString": "SVG baliogabea.",
|
||||
"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": {
|
||||
"selection": "Hautapena",
|
||||
@@ -236,7 +238,7 @@
|
||||
"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",
|
||||
"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_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",
|
||||
@@ -312,8 +314,8 @@
|
||||
"zoomToFit": "Egin zoom elementu guztiak ikusteko",
|
||||
"zoomToSelection": "Zooma hautapenera",
|
||||
"toggleElementLock": "Blokeatu/desbloketatu hautapena",
|
||||
"movePageUpDown": "",
|
||||
"movePageLeftRight": ""
|
||||
"movePageUpDown": "Mugitu orria gora/behera",
|
||||
"movePageLeftRight": "Mugitu orria ezker/eskuin"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": "Garbitu oihala"
|
||||
@@ -395,7 +397,7 @@
|
||||
"fileSavedToFilename": "{filename}-n gorde da",
|
||||
"canvas": "oihala",
|
||||
"selection": "hautapena",
|
||||
"pasteAsSingleElement": ""
|
||||
"pasteAsSingleElement": "Erabili {{shortcut}} elementu bakar gisa itsasteko,\nedo itsatsi lehendik dagoen testu-editore batean"
|
||||
},
|
||||
"colors": {
|
||||
"ffffff": "Zuria",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": "Laranja 9"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "Zure datu guztiak modu lokalean gordetzen dira zure nabigatzailean.",
|
||||
"switchToPlusApp": "Horren ordez Excalidraw+-ra joan nahi al zenuen?",
|
||||
"menuHints": "Esportatu, hobespenak, hizkuntzak,...",
|
||||
"toolbarHints": "Aukeratu tresna bat eta hasi marrazten!",
|
||||
"helpHints": "Lasterbideak eta laguntza"
|
||||
"app": {
|
||||
"center_heading": "Zure datu guztiak lokalean gordetzen dira zure nabigatzailean.",
|
||||
"center_heading_plus": "Horren ordez Excalidraw+-era joan nahi al zenuen?",
|
||||
"menuHint": "Esportatu, hobespenak, hizkuntzak..."
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "Esportatu, hobespenak eta gehiago...",
|
||||
"center_heading": "Diagramak. Egina. Sinplea.",
|
||||
"toolbarHint": "Aukeratu tresna bat eta hasi marrazten!",
|
||||
"helpHint": "Lasterbideak eta laguntza"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "تصویر SVG وارد نشد. نشانه گذاری SVG نامعتبر به نظر می رسد.",
|
||||
"invalidSVGString": "SVG نادرست.",
|
||||
"cannotResolveCollabServer": "به سرور collab متصل نشد. لطفا صفحه را مجددا بارگذاری کنید و دوباره تلاش کنید.",
|
||||
"importLibraryError": "دادهها بارگذاری نشدند"
|
||||
"importLibraryError": "دادهها بارگذاری نشدند",
|
||||
"collabSaveFailed": "",
|
||||
"collabSaveFailed_sizeExceeded": ""
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "گزینش",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": "نارنجی 9"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "همه ی داده های شما به صورت محلی در مرورگر ذخیره میشود.",
|
||||
"switchToPlusApp": "آیا ترجیح میدهید به Excalidraw+ بروید؟",
|
||||
"menuHints": "خروجی گرفتن، تنظیمات، زبانها، ...",
|
||||
"toolbarHints": "یک ابزار را انتخاب کنید و ترسیم را شروع کنید!",
|
||||
"helpHints": "میانبرها و کمک"
|
||||
"app": {
|
||||
"center_heading": "",
|
||||
"center_heading_plus": "",
|
||||
"menuHint": ""
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "SVG- kuvaa ei voitu lisätä. Tiedoston SVG-sisältö näyttää virheelliseltä.",
|
||||
"invalidSVGString": "Virheellinen SVG.",
|
||||
"cannotResolveCollabServer": "Yhteyden muodostaminen collab-palvelimeen epäonnistui. Virkistä sivu ja yritä uudelleen.",
|
||||
"importLibraryError": ""
|
||||
"importLibraryError": "",
|
||||
"collabSaveFailed": "",
|
||||
"collabSaveFailed_sizeExceeded": ""
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Valinta",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": "Oranssi 9"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "",
|
||||
"switchToPlusApp": "",
|
||||
"menuHints": "",
|
||||
"toolbarHints": "",
|
||||
"helpHints": ""
|
||||
"app": {
|
||||
"center_heading": "",
|
||||
"center_heading_plus": "",
|
||||
"menuHint": ""
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "Coller",
|
||||
"pasteAsPlaintext": "",
|
||||
"pasteAsPlaintext": "Coller comme texte brut",
|
||||
"pasteCharts": "Coller les graphiques",
|
||||
"selectAll": "Tout sélectionner",
|
||||
"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.",
|
||||
"invalidSVGString": "SVG invalide.",
|
||||
"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": {
|
||||
"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",
|
||||
"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",
|
||||
"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_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",
|
||||
@@ -312,8 +314,8 @@
|
||||
"zoomToFit": "Zoomer pour voir tous les éléments",
|
||||
"zoomToSelection": "Zoomer sur la sélection",
|
||||
"toggleElementLock": "Verrouiller/déverrouiller la sélection",
|
||||
"movePageUpDown": "",
|
||||
"movePageLeftRight": ""
|
||||
"movePageUpDown": "Déplacer la page vers le haut/bas",
|
||||
"movePageLeftRight": "Déplacer la page vers la gauche/droite"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": "Effacer la zone de dessin"
|
||||
@@ -395,7 +397,7 @@
|
||||
"fileSavedToFilename": "Enregistré sous {filename}",
|
||||
"canvas": "canevas",
|
||||
"selection": "sélection",
|
||||
"pasteAsSingleElement": ""
|
||||
"pasteAsSingleElement": "Utiliser {{shortcut}} pour coller comme un seul élément,\nou coller dans un éditeur de texte existant"
|
||||
},
|
||||
"colors": {
|
||||
"ffffff": "Blanc",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": "Orange 9"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "Toutes vos données sont sauvegardées en local dans votre navigateur.",
|
||||
"switchToPlusApp": "Vous vouliez plutôt aller à Excalidraw+ ?",
|
||||
"menuHints": "Exportation, préférences, langues, ...",
|
||||
"toolbarHints": "Choisissez un outil et commencez à dessiner !",
|
||||
"helpHints": "Raccourcis et aide"
|
||||
"app": {
|
||||
"center_heading": "Toutes vos données sont sauvegardées en local dans votre navigateur.",
|
||||
"center_heading_plus": "Vouliez-vous plutôt aller à Excalidraw+ à la place ?",
|
||||
"menuHint": "Exportation, préférences, langues, ..."
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "Exportation, préférences et plus...",
|
||||
"center_heading": "Diagrammes. Rendus. Simples.",
|
||||
"toolbarHint": "Choisissez un outil et commencez à dessiner !",
|
||||
"helpHint": "Raccourcis et aide"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "Pegar",
|
||||
"pasteAsPlaintext": "",
|
||||
"pasteAsPlaintext": "Pegar coma texto sen formato",
|
||||
"pasteCharts": "Pegar gráficos",
|
||||
"selectAll": "Seleccionar todo",
|
||||
"multiSelect": "Engadir elemento á selección",
|
||||
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "Non se puido inserir como imaxe SVG. O marcado SVG semella 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.",
|
||||
"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": {
|
||||
"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",
|
||||
"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",
|
||||
"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_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",
|
||||
@@ -312,8 +314,8 @@
|
||||
"zoomToFit": "Zoom que se axuste a todos os elementos",
|
||||
"zoomToSelection": "Zoom á selección",
|
||||
"toggleElementLock": "Bloquear/desbloquear selección",
|
||||
"movePageUpDown": "",
|
||||
"movePageLeftRight": ""
|
||||
"movePageUpDown": "Mover páxina cara enriba/abaixo",
|
||||
"movePageLeftRight": "Mover páxina cara a esquerda/dereita"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": "Limpar lenzo"
|
||||
@@ -395,7 +397,7 @@
|
||||
"fileSavedToFilename": "Gardado en {filename}",
|
||||
"canvas": "lenzo",
|
||||
"selection": "selección",
|
||||
"pasteAsSingleElement": ""
|
||||
"pasteAsSingleElement": "Usa {{shortcut}} para pegar como un único elemento\nou pega nun editor de texto existente"
|
||||
},
|
||||
"colors": {
|
||||
"ffffff": "Branco",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": "Laranxa 9"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "Toda a información é gardada de maneira local no seu navegador.",
|
||||
"switchToPlusApp": "Queres ir a Excalidraw+ no seu lugar?",
|
||||
"menuHints": "Exportar, preferencias, idiomas, ...",
|
||||
"toolbarHints": "Escolle unha ferramenta & Comeza a debuxar!",
|
||||
"helpHints": "Atallos & axuda"
|
||||
"app": {
|
||||
"center_heading": "Toda a información é gardada de maneira local no seu navegador.",
|
||||
"center_heading_plus": "Queres ir a Excalidraw+ no seu lugar?",
|
||||
"menuHint": "Exportar, preferencias, idiomas, ..."
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "Exportar, preferencias, e máis...",
|
||||
"center_heading": "Diagramas. Feito. Sinxelo.",
|
||||
"toolbarHint": "Escolle unha ferramenta & Comeza a debuxar!",
|
||||
"helpHint": "Atallos & axuda"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "לא ניתן היה להטמיע את תמונת ה-SVG. קידוד ה-SVG אינו תקני.",
|
||||
"invalidSVGString": "SVG בלתי תקני.",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": "לא ניתן היה לטעון את הספריה"
|
||||
"importLibraryError": "לא ניתן היה לטעון את הספריה",
|
||||
"collabSaveFailed": "",
|
||||
"collabSaveFailed_sizeExceeded": ""
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "בחירה",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": "כתום 9"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "",
|
||||
"switchToPlusApp": "",
|
||||
"menuHints": "",
|
||||
"toolbarHints": "",
|
||||
"helpHints": ""
|
||||
"app": {
|
||||
"center_heading": "",
|
||||
"center_heading_plus": "",
|
||||
"menuHint": ""
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "चिपकाएँ",
|
||||
"pasteAsPlaintext": "",
|
||||
"pasteAsPlaintext": "सादे पाठ के रूप में चिपकाएं",
|
||||
"pasteCharts": "चार्ट चिपकाएँ",
|
||||
"selectAll": "सभी चुनें",
|
||||
"multiSelect": "आकार को चयन में जोड़ें",
|
||||
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "एसवीजी छवि सम्मिलित नहीं कर सके, एसवीजी रचना अनुचित हैं",
|
||||
"invalidSVGString": "अनुचित SVG",
|
||||
"cannotResolveCollabServer": "कॉलेब सर्वर से कनेक्शन नहीं हो पा रहा. कृपया पृष्ठ को पुनः लाने का प्रयास करे.",
|
||||
"importLibraryError": "संग्रह प्रतिष्ठापित नहीं किया जा सका"
|
||||
"importLibraryError": "संग्रह प्रतिष्ठापित नहीं किया जा सका",
|
||||
"collabSaveFailed": "किसी कारण वश अंदरूनी डेटाबेस में सहेजा नहीं जा सका। यदि समस्या बनी रहती है, तो किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।",
|
||||
"collabSaveFailed_sizeExceeded": "लगता है कि पृष्ठ तल काफ़ी बड़ा है, इस्कारण अंदरूनी डेटाबेस में सहेजा नहीं जा सका। किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "चयन",
|
||||
@@ -236,7 +238,7 @@
|
||||
"resize": "आकार बदलते समय आप SHIFT को पकड़ कर अनुपात में कमी कर सकते हैं,\nकेंद्र से आकार बदलने के लिए ALT दबाए रखें",
|
||||
"resizeImage": "",
|
||||
"rotate": "आप घूर्णन करते समय SHIFT पकड़कर कोणों को विवश कर सकते हैं",
|
||||
"lineEditor_info": "",
|
||||
"lineEditor_info": "बिंदुओं को सम्पादित करने के लिए CtrlOrCmd को दबायें रखते हुये डबल क्लिक करे, अथवा CtrlOrCmd + Enter साथ दबाये",
|
||||
"lineEditor_pointSelected": "",
|
||||
"lineEditor_nothingSelected": "",
|
||||
"placeImage": "",
|
||||
@@ -395,7 +397,7 @@
|
||||
"fileSavedToFilename": "",
|
||||
"canvas": "",
|
||||
"selection": "",
|
||||
"pasteAsSingleElement": ""
|
||||
"pasteAsSingleElement": "एक अवयव के रूप में चिपकाने के लिए {{shortcut}} का उपयोग करें,\nया किसी मौजूदा पाठ संपादक में चिपकायें"
|
||||
},
|
||||
"colors": {
|
||||
"ffffff": "सफेद",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": "नारंगी"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "आपका सर्व डेटा ब्राउज़र के भीतर स्थानिक जगह पे सुरक्षित किया गया.",
|
||||
"switchToPlusApp": "बजाय आपको Excalidraw+ जगह जाना है?",
|
||||
"menuHints": "निर्यात, पसंद, भाषायें, ...",
|
||||
"toolbarHints": "औजार चुने और चित्रकारी प्रारंभ करे!",
|
||||
"helpHints": "शॉर्ट्कट & सहाय्य"
|
||||
"app": {
|
||||
"center_heading": "",
|
||||
"center_heading_plus": "",
|
||||
"menuHint": ""
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "Nem sikerült beszúrni az SVG-képet. Az SVG szintaktika érvénytelennek tűnik.",
|
||||
"invalidSVGString": "Érvénytelen SVG.",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": ""
|
||||
"importLibraryError": "",
|
||||
"collabSaveFailed": "",
|
||||
"collabSaveFailed_sizeExceeded": ""
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Kijelölés",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": "Narancs 9"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "",
|
||||
"switchToPlusApp": "",
|
||||
"menuHints": "",
|
||||
"toolbarHints": "",
|
||||
"helpHints": ""
|
||||
"app": {
|
||||
"center_heading": "",
|
||||
"center_heading_plus": "",
|
||||
"menuHint": ""
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "Tidak dapat menyisipkan gambar SVG. Markup SVG sepertinya tidak valid.",
|
||||
"invalidSVGString": "SVG tidak valid.",
|
||||
"cannotResolveCollabServer": "Tidak dapat terhubung ke server kolab. Muat ulang laman dan coba lagi.",
|
||||
"importLibraryError": "Tidak dapat memuat pustaka"
|
||||
"importLibraryError": "Tidak dapat memuat pustaka",
|
||||
"collabSaveFailed": "Tidak dapat menyimpan ke dalam basis data server. Jika masih berlanjut, Anda sebaiknya simpan berkas Anda secara lokal untuk memastikan pekerjaan Anda tidak hilang.",
|
||||
"collabSaveFailed_sizeExceeded": "Tidak dapat menyimpan ke dalam basis data server, tampaknya ukuran kanvas terlalu besar. Anda sebaiknya simpan berkas Anda secara lokal untuk memastikan pekerjaan Anda tidak hilang."
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Pilihan",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": "Jingga 9"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "Semua data Anda tersimpan secara lokal di browser.",
|
||||
"switchToPlusApp": "Apa Anda ingin berpindah ke Excalidraw+?",
|
||||
"menuHints": "Ekspor, preferensi, bahasa, ...",
|
||||
"toolbarHints": "Ambil alat & mulai menggambar!",
|
||||
"helpHints": "Pintasan & bantuan"
|
||||
"app": {
|
||||
"center_heading": "Semua data Anda disimpan secara lokal di peramban Anda.",
|
||||
"center_heading_plus": "Apa Anda ingin berpindah ke Excalidraw+?",
|
||||
"menuHint": "Ekspor, preferensi, bahasa, ..."
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "Ekspor, preferensi, dan selebihnya...",
|
||||
"center_heading": "Diagram. Menjadi. Mudah.",
|
||||
"toolbarHint": "Pilih alat & mulai menggambar!",
|
||||
"helpHint": "Pintasan & bantuan"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "Impossibile inserire l'immagine SVG. Il markup SVG non sembra corretto.",
|
||||
"invalidSVGString": "SVG non valido.",
|
||||
"cannotResolveCollabServer": "Impossibile connettersi al server di collab. Ricarica la pagina e riprova.",
|
||||
"importLibraryError": "Impossibile caricare la libreria"
|
||||
"importLibraryError": "Impossibile caricare la libreria",
|
||||
"collabSaveFailed": "Impossibile salvare nel database di backend. Se i problemi persistono, dovresti salvare il tuo file localmente per assicurarti di non perdere il tuo lavoro.",
|
||||
"collabSaveFailed_sizeExceeded": "Impossibile salvare nel database di backend, la tela sembra essere troppo grande. Dovresti salvare il file localmente per assicurarti di non perdere il tuo lavoro."
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Selezione",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": "Arancio 9"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "Tutti i tuoi dati sono salvati localmente nel browser.",
|
||||
"switchToPlusApp": "Volevi invece andare su Excalidraw+?",
|
||||
"menuHints": "Esporta, preferenze, lingue, ...",
|
||||
"toolbarHints": "Scegli uno strumento & Inizia a disegnare!",
|
||||
"helpHints": "Scorciatoie & aiuto"
|
||||
"app": {
|
||||
"center_heading": "Tutti i tuoi dati sono salvati localmente nel browser.",
|
||||
"center_heading_plus": "Volevi invece andare su Excalidraw+?",
|
||||
"menuHint": "Esporta, preferenze, lingue, ..."
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "Esporta, preferenze, e altro...",
|
||||
"center_heading": "Diagrammi. Fatto. Semplice.",
|
||||
"toolbarHint": "Scegli uno strumento & Inizia a disegnare!",
|
||||
"helpHint": "Scorciatoie & aiuto"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "貼り付け",
|
||||
"pasteAsPlaintext": "",
|
||||
"pasteAsPlaintext": "書式なしテキストとして貼り付け",
|
||||
"pasteCharts": "チャートの貼り付け",
|
||||
"selectAll": "すべて選択",
|
||||
"multiSelect": "複数選択",
|
||||
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "SVGイメージを挿入できませんでした。SVGマークアップは無効に見えます。",
|
||||
"invalidSVGString": "無効なSVGです。",
|
||||
"cannotResolveCollabServer": "コラボレーションサーバに接続できませんでした。ページを再読み込みして、もう一度お試しください。",
|
||||
"importLibraryError": "ライブラリを読み込めませんでした。"
|
||||
"importLibraryError": "ライブラリを読み込めませんでした。",
|
||||
"collabSaveFailed": "バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。",
|
||||
"collabSaveFailed_sizeExceeded": "キャンバスが大きすぎるため、バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "選択",
|
||||
@@ -236,7 +238,7 @@
|
||||
"resize": "サイズを変更中にSHIFTを押すと縦横比を固定できます。Altを押すと中央からサイズを変更できます",
|
||||
"resizeImage": "SHIFTを長押しすると自由にサイズを変更できます。\n中央からサイズを変更するにはALTを長押しします",
|
||||
"rotate": "回転中にSHIFT キーを押すと角度を制限することができます",
|
||||
"lineEditor_info": "",
|
||||
"lineEditor_info": "CtrlOrCmd を押したままダブルクリックするか、CtrlOrCmd + Enter を押して点を編集します",
|
||||
"lineEditor_pointSelected": "Deleteキーを押すと点を削除、CtrlOrCmd+Dで複製、マウスドラッグで移動",
|
||||
"lineEditor_nothingSelected": "編集する点を選択(SHIFTを押したままで複数選択)、\nAltキーを押しながらクリックすると新しい点を追加",
|
||||
"placeImage": "クリックして画像を配置するか、クリックしてドラッグしてサイズを手動で設定します",
|
||||
@@ -312,8 +314,8 @@
|
||||
"zoomToFit": "すべての要素が収まるようにズーム",
|
||||
"zoomToSelection": "選択要素にズーム",
|
||||
"toggleElementLock": "選択したアイテムをロック/ロック解除",
|
||||
"movePageUpDown": "",
|
||||
"movePageLeftRight": ""
|
||||
"movePageUpDown": "ページを上下に移動",
|
||||
"movePageLeftRight": "ページを左右に移動"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": "キャンバスを消去"
|
||||
@@ -395,7 +397,7 @@
|
||||
"fileSavedToFilename": "{filename} に保存しました",
|
||||
"canvas": "キャンバス",
|
||||
"selection": "選択",
|
||||
"pasteAsSingleElement": ""
|
||||
"pasteAsSingleElement": "{{shortcut}} を使用して単一の要素として貼り付けるか、\n既存のテキストエディタに貼り付け"
|
||||
},
|
||||
"colors": {
|
||||
"ffffff": "ホワイト",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": "オレンジ 9"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "すべてのデータはブラウザにローカル保存されます。",
|
||||
"switchToPlusApp": "代わりにExcalidraw+を開きますか?",
|
||||
"menuHints": "エクスポート, 設定, 言語, ...",
|
||||
"toolbarHints": "ツールを選んで描き始めよう!",
|
||||
"helpHints": "ショートカットとヘルプ"
|
||||
"app": {
|
||||
"center_heading": "すべてのデータはブラウザにローカル保存されます。",
|
||||
"center_heading_plus": "代わりにExcalidraw+を開きますか?",
|
||||
"menuHint": "エクスポート、設定、言語..."
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "エクスポート、設定、その他...",
|
||||
"center_heading": "ダイアグラムを簡単に。",
|
||||
"toolbarHint": "ツールを選んで描き始めよう!",
|
||||
"helpHint": "ショートカットとヘルプ"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -72,7 +72,7 @@
|
||||
"layers": "Tissiyin",
|
||||
"actions": "Tigawin",
|
||||
"language": "Tutlayt",
|
||||
"liveCollaboration": "",
|
||||
"liveCollaboration": "Amɛiwen s srid...",
|
||||
"duplicateSelection": "Sisleg",
|
||||
"untitled": "War azwel",
|
||||
"name": "Isem",
|
||||
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "D awezɣi tugra n tugna SVG. Acraḍ SVG yettban-d d armeɣtu.",
|
||||
"invalidSVGString": "SVG armeɣtu.",
|
||||
"cannotResolveCollabServer": "Ulamek tuqqna s aqeddac n umyalel. Ma ulac uɣilif ales asali n usebter sakin eɛreḍ tikkelt-nniḍen.",
|
||||
"importLibraryError": "Ur d-ssalay ara tamkarḍit"
|
||||
"importLibraryError": "Ur d-ssalay ara tamkarḍit",
|
||||
"collabSaveFailed": "",
|
||||
"collabSaveFailed_sizeExceeded": ""
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Tafrayt",
|
||||
@@ -312,8 +314,8 @@
|
||||
"zoomToFit": "Simɣur akken ad twliḍ akk iferdisen",
|
||||
"zoomToSelection": "Simɣur ɣer tefrayt",
|
||||
"toggleElementLock": "Sekkeṛ/kkes asekker i tefrayt",
|
||||
"movePageUpDown": "",
|
||||
"movePageLeftRight": ""
|
||||
"movePageUpDown": "Smutti asebter d asawen/akessar",
|
||||
"movePageLeftRight": "Smutti asebter s azelmaḍ/ayfus"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": "Sfeḍ taɣzut n usuneɣ"
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": "Aččinawi 9"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "Akk isefka-inek•inem ttwakelsen s wudem adigan deg yiminig-inek•inem.",
|
||||
"switchToPlusApp": "Tebɣiḍ ad tedduḍ ɣer Excalidraw+ deg umḍiq?",
|
||||
"menuHints": "Asifeḍ, ismenyifen, tutlayin, ...",
|
||||
"toolbarHints": "Fren afecku tebduḍ asuneɣ!",
|
||||
"helpHints": "Inegzumen akked tallelt"
|
||||
"app": {
|
||||
"center_heading": "",
|
||||
"center_heading_plus": "",
|
||||
"menuHint": ""
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -201,7 +201,9 @@
|
||||
"svgImageInsertError": "",
|
||||
"invalidSVGString": "",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": ""
|
||||
"importLibraryError": "",
|
||||
"collabSaveFailed": "",
|
||||
"collabSaveFailed_sizeExceeded": ""
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "",
|
||||
@@ -445,10 +447,16 @@
|
||||
"d9480f": ""
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "",
|
||||
"switchToPlusApp": "",
|
||||
"menuHints": "",
|
||||
"toolbarHints": "",
|
||||
"helpHints": ""
|
||||
"app": {
|
||||
"center_heading": "",
|
||||
"center_heading_plus": "",
|
||||
"menuHint": ""
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user