Compare commits

...

13 Commits

Author SHA1 Message Date
ff5405bacc chore(deps): bump socket.io-client from 2.3.1 to 4.5.1
Bumps [socket.io-client](https://github.com/socketio/socket.io-client) from 2.3.1 to 4.5.1.
- [Release notes](https://github.com/socketio/socket.io-client/releases)
- [Changelog](https://github.com/socketio/socket.io-client/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io-client/compare/2.3.1...4.5.1)

---
updated-dependencies:
- dependency-name: socket.io-client
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-31 12:55:12 +00:00
e6de1fe4a4 feat: rewrite public UI component rendering using tunnels (#6117)
* feat: rewrite public UI component rendering using tunnels

* factor out into components

* comments

* fix variable naming

* fix not hiding welcomeScreen

* factor out AppFooter and memoize components

* remove `UIOptions.welcomeScreen` and render only from host app

* factor out tunnels into own file

* update changelog. Keep `UIOptions.welcomeScreen` as deprecated

* update changelog

* lint

---------

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2023-01-31 13:53:20 +01:00
3a141ca77a fix: add 1px width to the container to calculate more accurately (#6174)
* fix: add 1px width to the container to calculate accurately

* fix tests
2023-01-30 18:52:56 +05:30
5ae39c9292 fix: quick typo fix (#6167) 2023-01-29 14:22:25 +01:00
e41ea9562b fix: set the width correctly using measureText in editor (#6162) 2023-01-28 12:09:53 +01:00
b52c8943e4 fix: 🐛 broken emojis when wrap text (#6153)
* fix: 🐛 broken emojis when wrap text

* refactor: Delete unnecessary "else" (reduce indentation)

* fix: remove code block that causes the emojis to disappear

* Apply suggestions from code review

Co-authored-by: David Luzar <luzar.david@gmail.com>

* fix: 🚑 possibly undefined value

* Add spec

Co-authored-by: David Luzar <luzar.david@gmail.com>
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2023-01-26 11:49:21 +05:30
cf38c0f933 fix: declare css variable for font in excalidraw so its available in host (#6160)
declar css variable for font in excalidraw so its available in host
2023-01-25 15:44:20 +05:30
1db078a3dc feat: close MainMenu and Library dropdown on item select (#6152) 2023-01-23 16:54:35 +01:00
d4afd66268 feat: add hand/panning tool (#6141)
* feat: add hand/panning tool

* move hand tool right of tool lock separator

* tweak i18n

* rename `panning` -> `hand`

* toggle between last tool and hand on `H` shortcut

* hide properties sidebar when `hand` active

* revert to rendering HandButton manually due to mobile toolbar
2023-01-23 16:12:28 +01:00
849e6a0c86 fix: button background and svg sizes (#6155)
* fix: button background color fallback

* fix svg width/height
2023-01-23 16:10:04 +01:00
f03f5c948d style: change in ExportButton style (#6147) (#6148)
Co-authored-by: David Luzar <luzar.david@gmail.com>
2023-01-22 12:37:18 +00:00
d2b698093c feat: show copy-as-png export button on firefox and show steps how to enable it (#6125)
* feat: hide copy-as-png shortcut from help dialog if not supported

* fix: support firefox if clipboard.write supported

* show shrotcut in firefox and instead show error message how to enable the flag support

* widen to TypeError because minification

* show copy-as-png on firefox even if it will throw
2023-01-22 12:33:15 +01:00
0f1720be61 chore: Update translations from Crowdin (#6077) 2023-01-22 12:19:21 +01:00
113 changed files with 1704 additions and 1173 deletions

View File

@ -54,7 +54,8 @@
"react-scripts": "5.0.1",
"roughjs": "4.5.2",
"sass": "1.51.0",
"socket.io-client": "2.3.1",
"socket.io-client": "4.5.4",
"tunnel-rat": "0.1.0",
"typescript": "4.9.4",
"workbox-background-sync": "^6.5.4",
"workbox-broadcast-update": "^6.5.4",

View File

@ -167,9 +167,6 @@
body,
html {
margin: 0;
--ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system,
Segoe UI, Roboto, Helvetica, Arial, sans-serif;
font-family: var(--ui-font);
-webkit-text-size-adjust: 100%;
width: 100%;

View File

@ -1,7 +1,7 @@
import { ColorPicker } from "../components/ColorPicker";
import { eraser, 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";
@ -10,12 +10,15 @@ 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 clsx from "clsx";
import {
getDefaultAppState,
isEraserActive,
isHandToolActive,
} from "../appState";
export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor",
@ -306,15 +309,15 @@ export const actionToggleTheme = register({
},
});
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,
@ -337,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,
});

View File

@ -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,

View File

@ -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[],

View File

@ -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",

View File

@ -1,5 +1,5 @@
import { isDarwin } from "../constants";
import { t } from "../i18n";
import { isDarwin } from "../keys";
import { getShortcutKey } from "../utils";
import { ActionName } from "./types";

View File

@ -109,10 +109,11 @@ export type ActionName =
| "decreaseFontSize"
| "unbindText"
| "hyperlink"
| "eraser"
| "bindText"
| "toggleLock"
| "toggleLinearEditor";
| "toggleLinearEditor"
| "toggleEraserTool"
| "toggleHandTool";
export type PanelComponentProps = {
elements: readonly ExcalidrawElement[];

View File

@ -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";
};

View File

@ -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) => {

View File

@ -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}`}

View 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;
};

View File

@ -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,
@ -575,6 +580,7 @@ class App extends React.Component<AppProps, AppState> {
elements={this.scene.getNonDeletedElements()}
onLockToggle={this.toggleLock}
onPenModeToggle={this.togglePenMode}
onHandToolToggle={this.onHandToolToggle}
onInsertElements={(elements) =>
this.addElementsFromPasteOrLibrary({
elements,
@ -583,7 +589,6 @@ class App extends React.Component<AppProps, AppState> {
})
}
langCode={getLanguage().code}
isCollaborating={this.props.isCollaborating}
renderTopRightUI={renderTopRightUI}
renderCustomStats={renderCustomStats}
renderCustomSidebar={this.props.renderSidebar}
@ -599,7 +604,6 @@ class App extends React.Component<AppProps, AppState> {
onImageAction={this.onImageAction}
renderWelcomeScreen={
!this.state.isLoading &&
this.props.UIOptions.welcomeScreen &&
this.state.showWelcomeScreen &&
this.state.activeTool.type === "selection" &&
!this.scene.getElementsIncludingDeleted().length
@ -1812,6 +1816,10 @@ class App extends React.Component<AppProps, AppState> {
});
};
onHandToolToggle = () => {
this.actionManager.executeAction(actionToggleHandTool);
};
scrollToContent = (
target:
| ExcalidrawElement
@ -2229,11 +2237,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)) {
@ -2904,7 +2914,12 @@ class App extends React.Component<AppProps, AppState> {
null;
}
if (isHoldingSpace || isPanning || isDraggingScrollBar) {
if (
isHoldingSpace ||
isPanning ||
isDraggingScrollBar ||
isHandToolActive(this.state)
) {
return;
}
@ -3496,7 +3511,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,
@ -3607,6 +3625,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)

View File

@ -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);

View 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?.()}
/>
);
};

View File

@ -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")]}

View File

@ -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)}

View File

@ -8,15 +8,8 @@ import { NonDeletedExcalidrawElement } from "../element/types";
import { Language, t } from "../i18n";
import { calculateScrollCenter } from "../scene";
import { ExportType } from "../scene/types";
import {
AppProps,
AppState,
ExcalidrawProps,
BinaryFiles,
UIChildrenComponents,
UIWelcomeScreenComponents,
} from "../types";
import { isShallowEqual, muteFSAbortError, getReactChildren } from "../utils";
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
import { isShallowEqual, muteFSAbortError } from "../utils";
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
import { ErrorDialog } from "./ErrorDialog";
import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
@ -45,11 +38,19 @@ import { useDevice } from "../components/App";
import { Stats } from "./Stats";
import { actionToggleStats } from "../actions/actionToggleStats";
import Footer from "./footer/Footer";
import WelcomeScreen from "./welcome-screen/WelcomeScreen";
import { hostSidebarCountersAtom } from "./Sidebar/Sidebar";
import { jotaiScope } from "../jotai";
import { useAtom } from "jotai";
import MainMenu from "./main-menu/MainMenu";
import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
import { HandButton } from "./HandButton";
import { isHandToolActive } from "../appState";
import {
mainMenuTunnel,
welcomeScreenMenuHintTunnel,
welcomeScreenToolbarHintTunnel,
welcomeScreenCenterTunnel,
} from "./tunnels";
interface LayerUIProps {
actionManager: ActionManager;
@ -59,11 +60,11 @@ interface LayerUIProps {
setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[];
onLockToggle: () => void;
onHandToolToggle: () => void;
onPenModeToggle: () => void;
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
showExitZenModeBtn: boolean;
langCode: Language["code"];
isCollaborating: boolean;
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
renderCustomSidebar?: ExcalidrawProps["renderSidebar"];
@ -77,6 +78,32 @@ interface LayerUIProps {
children?: React.ReactNode;
}
const DefaultMainMenu: React.FC<{
UIOptions: AppProps["UIOptions"];
}> = ({ UIOptions }) => {
return (
<MainMenu __fallback>
<MainMenu.DefaultItems.LoadScene />
<MainMenu.DefaultItems.SaveToActiveFile />
{/* FIXME we should to test for this inside the item itself */}
{UIOptions.canvasActions.export && <MainMenu.DefaultItems.Export />}
{/* FIXME we should to test for this inside the item itself */}
{UIOptions.canvasActions.saveAsImage && (
<MainMenu.DefaultItems.SaveAsImage />
)}
<MainMenu.DefaultItems.Help />
<MainMenu.DefaultItems.ClearCanvas />
<MainMenu.Separator />
<MainMenu.Group title="Excalidraw links">
<MainMenu.DefaultItems.Socials />
</MainMenu.Group>
<MainMenu.Separator />
<MainMenu.DefaultItems.ToggleTheme />
<MainMenu.DefaultItems.ChangeCanvasBackground />
</MainMenu>
);
};
const LayerUI = ({
actionManager,
appState,
@ -85,10 +112,10 @@ const LayerUI = ({
elements,
canvas,
onLockToggle,
onHandToolToggle,
onPenModeToggle,
onInsertElements,
showExitZenModeBtn,
isCollaborating,
renderTopRightUI,
renderCustomStats,
renderCustomSidebar,
@ -103,28 +130,6 @@ const LayerUI = ({
}: LayerUIProps) => {
const device = useDevice();
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) {
return null;
@ -192,37 +197,12 @@ const LayerUI = ({
);
};
const renderMenu = () => {
return (
childrenComponents.Menu || (
<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 />
)}
<MainMenu.DefaultItems.Help />
<MainMenu.DefaultItems.ClearCanvas />
<MainMenu.Separator />
<MainMenu.Group title="Excalidraw links">
<MainMenu.DefaultItems.Socials />
</MainMenu.Group>
<MainMenu.Separator />
<MainMenu.DefaultItems.ToggleTheme />
<MainMenu.DefaultItems.ChangeCanvasBackground />
</MainMenu>
)
);
};
const renderCanvasActions = () => (
<div style={{ position: "relative" }}>
{WelcomeScreenComponents.MenuHint}
{/* wrapping to Fragment stops React from occasionally complaining
about identical Keys */}
<>{renderMenu()}</>
<mainMenuTunnel.Out />
{renderWelcomeScreen && <welcomeScreenMenuHintTunnel.Out />}
</div>
);
@ -259,7 +239,6 @@ const LayerUI = ({
return (
<FixedSideContainer side="top">
{WelcomeScreenComponents.Center}
<div className="App-menu App-menu_top">
<Stack.Col
gap={6}
@ -274,7 +253,9 @@ const LayerUI = ({
<Section heading="shapes" className="shapes-section">
{(heading: React.ReactNode) => (
<div style={{ position: "relative" }}>
{WelcomeScreenComponents.ToolbarHint}
{renderWelcomeScreen && (
<welcomeScreenToolbarHintTunnel.Out />
)}
<Stack.Col gap={4} align="start">
<Stack.Row
gap={1}
@ -304,13 +285,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}
@ -322,9 +310,6 @@ const LayerUI = ({
});
}}
/>
{/* {actionManager.renderAction("eraser", {
// size: "small",
})} */}
</Stack.Row>
</Island>
</Stack.Row>
@ -371,7 +356,16 @@ const LayerUI = ({
return (
<>
{restChildren}
{/* ------------------------- tunneled UI ---------------------------- */}
{/* make sure we render host app components first so that we can detect
them first on initial render to optimize layout shift */}
{children}
{/* render component fallbacks. Can be rendered anywhere as they'll be
tunneled away. We only render tunneled components that actually
have defaults when host do not render anything. */}
<DefaultMainMenu UIOptions={UIOptions} />
{/* ------------------------------------------------------------------ */}
{appState.isLoading && <LoadingMessage delay={250} />}
{appState.errorMessage && (
<ErrorDialog
@ -386,6 +380,7 @@ const LayerUI = ({
}}
/>
)}
<ActiveConfirmDialog />
{renderImageExportDialog()}
{renderJSONExportDialog()}
{appState.pasteDialog.shown && (
@ -408,7 +403,8 @@ const LayerUI = ({
renderJSONExportDialog={renderJSONExportDialog}
renderImageExportDialog={renderImageExportDialog}
setAppState={setAppState}
onLockToggle={() => onLockToggle()}
onLockToggle={onLockToggle}
onHandToolToggle={onHandToolToggle}
onPenModeToggle={onPenModeToggle}
canvas={canvas}
onImageAction={onImageAction}
@ -416,8 +412,6 @@ const LayerUI = ({
renderCustomStats={renderCustomStats}
renderSidebars={renderSidebars}
device={device}
renderMenu={renderMenu}
welcomeScreenCenter={WelcomeScreenComponents.Center}
/>
)}
@ -440,13 +434,13 @@ const LayerUI = ({
: {}
}
>
{renderWelcomeScreen && <welcomeScreenCenterTunnel.Out />}
{renderFixedSideContainer()}
<Footer
appState={appState}
actionManager={actionManager}
showExitZenModeBtn={showExitZenModeBtn}
footerCenter={childrenComponents.FooterCenter}
welcomeScreenHelp={WelcomeScreenComponents.HelpHint}
renderWelcomeScreen={renderWelcomeScreen}
/>
{appState.showStats && (
<Stats

View File

@ -187,6 +187,7 @@ export const LibraryMenuHeader: React.FC<{
</DropdownMenu.Trigger>
<DropdownMenu.Content
onClickOutside={() => setIsLibraryMenuOpen(false)}
onSelect={() => setIsLibraryMenuOpen(false)}
className="library-menu"
>
{!itemsSelected && (

View File

@ -9,7 +9,6 @@ type LockIconProps = {
name?: string;
checked: boolean;
onChange?(): void;
zenModeEnabled?: boolean;
isMobile?: boolean;
};

View File

@ -1,10 +1,5 @@
import React from "react";
import {
AppState,
Device,
ExcalidrawProps,
UIWelcomeScreenComponents,
} from "../types";
import { AppState, Device, ExcalidrawProps } from "../types";
import { ActionManager } from "../actions/manager";
import { t } from "../i18n";
import Stack from "./Stack";
@ -22,6 +17,9 @@ import { LibraryButton } from "./LibraryButton";
import { PenModeButton } from "./PenModeButton";
import { Stats } from "./Stats";
import { actionToggleStats } from "../actions";
import { HandButton } from "./HandButton";
import { isHandToolActive } from "../appState";
import { mainMenuTunnel, welcomeScreenCenterTunnel } from "./tunnels";
type MobileMenuProps = {
appState: AppState;
@ -31,6 +29,7 @@ type MobileMenuProps = {
setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[];
onLockToggle: () => void;
onHandToolToggle: () => void;
onPenModeToggle: () => void;
canvas: HTMLCanvasElement | null;
@ -42,8 +41,6 @@ type MobileMenuProps = {
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
renderSidebars: () => JSX.Element | null;
device: Device;
renderMenu: () => React.ReactNode;
welcomeScreenCenter: UIWelcomeScreenComponents["Center"];
};
export const MobileMenu = ({
@ -52,6 +49,7 @@ export const MobileMenu = ({
actionManager,
setAppState,
onLockToggle,
onHandToolToggle,
onPenModeToggle,
canvas,
onImageAction,
@ -59,13 +57,11 @@ export const MobileMenu = ({
renderCustomStats,
renderSidebars,
device,
renderMenu,
welcomeScreenCenter,
}: MobileMenuProps) => {
const renderToolbar = () => {
return (
<FixedSideContainer side="top" className="App-top-bar">
{welcomeScreenCenter}
<welcomeScreenCenterTunnel.Out />
<Section heading="shapes">
{(heading: React.ReactNode) => (
<Stack.Col gap={4} align="center">
@ -88,6 +84,13 @@ export const MobileMenu = ({
</Island>
{renderTopRightUI && renderTopRightUI(true, appState)}
<div className="mobile-misc-tools-container">
{!appState.viewModeEnabled && (
<LibraryButton
appState={appState}
setAppState={setAppState}
isMobile
/>
)}
<PenModeButton
checked={appState.penMode}
onChange={onPenModeToggle}
@ -101,13 +104,12 @@ export const MobileMenu = ({
title={t("toolBar.lock")}
isMobile
/>
{!appState.viewModeEnabled && (
<LibraryButton
appState={appState}
setAppState={setAppState}
isMobile
/>
)}
<HandButton
checked={isHandToolActive(appState)}
onChange={() => onHandToolToggle()}
title={t("toolBar.hand")}
isMobile
/>
</div>
</Stack.Row>
</Stack.Col>
@ -125,12 +127,16 @@ export const MobileMenu = ({
const renderAppToolbar = () => {
if (appState.viewModeEnabled) {
return <div className="App-toolbar-content">{renderMenu()}</div>;
return (
<div className="App-toolbar-content">
<mainMenuTunnel.Out />
</div>
);
}
return (
<div className="App-toolbar-content">
{renderMenu()}
<mainMenuTunnel.Out />
{actionManager.renderAction("toggleEditMenu")}
{actionManager.renderAction("undo")}
{actionManager.renderAction("redo")}

View File

@ -19,7 +19,7 @@ type ToolButtonBaseProps = {
name?: string;
id?: string;
size?: ToolButtonSize;
keyBindingLabel?: string;
keyBindingLabel?: string | null;
showAriaLabel?: boolean;
hidden?: boolean;
visible?: boolean;

View File

@ -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;

View File

@ -1,10 +1,10 @@
import React from "react";
import {
getDropdownMenuItemClassName,
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,
@ -14,17 +14,19 @@ const DropdownMenuItem = ({
...rest
}: {
icon?: JSX.Element;
onSelect: () => void;
onSelect: (event: Event) => void;
children: React.ReactNode;
shortcut?: string;
className?: string;
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
return (
<button
{...rest}
onClick={onSelect}
onClick={handleClick}
type="button"
className={getDrodownMenuItemClassName(className)}
className={getDropdownMenuItemClassName(className)}
title={rest.title ?? rest["aria-label"]}
>
<MenuItemContent icon={icon} shortcut={shortcut}>

View File

@ -1,28 +1,37 @@
import MenuItemContent from "./DropdownMenuItemContent";
import React from "react";
import { getDrodownMenuItemClassName } from "./DropdownMenuItem";
import {
getDropdownMenuItemClassName,
useHandleDropdownMenuItemClick,
} from "./common";
const DropdownMenuItemLink = ({
icon,
shortcut,
href,
children,
onSelect,
className = "",
...rest
}: {
href: string;
icon?: JSX.Element;
children: React.ReactNode;
shortcut?: string;
className?: string;
href: 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)}
className={getDropdownMenuItemClassName(className)}
title={rest.title ?? rest["aria-label"]}
onClick={handleClick}
>
<MenuItemContent icon={icon} shortcut={shortcut}>
{children}

View 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 getDropdownMenuItemClassName = (className = "") => {
return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim();
};
export const useHandleDropdownMenuItemClick = (
origOnClick:
| React.MouseEventHandler<HTMLAnchorElement | HTMLButtonElement>
| undefined,
onSelect: ((event: Event) => void) | undefined,
) => {
const DropdownMenuContentProps = useContext(DropdownMenuContentPropsContext);
return composeEventHandlers(origOnClick, (event) => {
const itemSelectEvent = new CustomEvent(EVENT.MENU_ITEM_SELECT, {
bubbles: true,
cancelable: true,
});
onSelect?.(itemSelectEvent);
if (!itemSelectEvent.defaultPrevented) {
DropdownMenuContentProps.onSelect?.(itemSelectEvent);
}
});
};

View File

@ -1,11 +1,7 @@
import clsx from "clsx";
import { actionShortcuts } from "../../actions";
import { ActionManager } from "../../actions/manager";
import {
AppState,
UIChildrenComponents,
UIWelcomeScreenComponents,
} from "../../types";
import { AppState } from "../../types";
import {
ExitZenModeAction,
FinalizeAction,
@ -16,19 +12,18 @@ import { useDevice } from "../App";
import { HelpButton } from "../HelpButton";
import { Section } from "../Section";
import Stack from "../Stack";
import { footerCenterTunnel, welcomeScreenHelpHintTunnel } from "../tunnels";
const Footer = ({
appState,
actionManager,
showExitZenModeBtn,
footerCenter,
welcomeScreenHelp,
renderWelcomeScreen,
}: {
appState: AppState;
actionManager: ActionManager;
showExitZenModeBtn: boolean;
footerCenter: UIChildrenComponents["FooterCenter"];
welcomeScreenHelp: UIWelcomeScreenComponents["HelpHint"];
renderWelcomeScreen: boolean;
}) => {
const device = useDevice();
const showFinalize =
@ -73,14 +68,14 @@ const Footer = ({
</Section>
</Stack.Col>
</div>
{footerCenter}
<footerCenterTunnel.Out />
<div
className={clsx("layer-ui__wrapper__footer-right zen-mode-transition", {
"transition-right disable-pointerEvents": appState.zenModeEnabled,
})}
>
<div style={{ position: "relative" }}>
{welcomeScreenHelp}
{renderWelcomeScreen && <welcomeScreenHelpHintTunnel.Out />}
<HelpButton
onClick={() => actionManager.executeAction(actionShortcuts)}
/>

View File

@ -1,18 +1,21 @@
import clsx from "clsx";
import { useExcalidrawAppState } from "../App";
import { footerCenterTunnel } from "../tunnels";
import "./FooterCenter.scss";
const FooterCenter = ({ children }: { children?: React.ReactNode }) => {
const appState = useExcalidrawAppState();
return (
<div
className={clsx("footer-center zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-bottom":
appState.zenModeEnabled,
})}
>
{children}
</div>
<footerCenterTunnel.In>
<div
className={clsx("footer-center zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-bottom":
appState.zenModeEnabled,
})}
>
{children}
</div>
</footerCenterTunnel.In>
);
};

View File

@ -0,0 +1,50 @@
import { atom, useAtom } from "jotai";
import React, { useLayoutEffect } from "react";
export const withInternalFallback = <P,>(
componentName: string,
Component: React.FC<P>,
) => {
const counterAtom = atom(0);
// flag set on initial render to tell the fallback component to skip the
// render until mount counter are initialized. This is because the counter
// is initialized in an effect, and thus we could end rendering both
// components at the same time until counter is initialized.
let preferHost = false;
const WrapperComponent: React.FC<
P & {
__fallback?: boolean;
}
> = (props) => {
const [counter, setCounter] = useAtom(counterAtom);
useLayoutEffect(() => {
setCounter((counter) => counter + 1);
return () => {
setCounter((counter) => counter - 1);
};
}, [setCounter]);
if (!props.__fallback) {
preferHost = true;
}
// ensure we don't render fallback and host components at the same time
if (
// either before the counters are initialized
(!counter && props.__fallback && preferHost) ||
// or after the counters are initialized, and both are rendered
// (this is the default when host renders as well)
(counter > 1 && props.__fallback)
) {
return null;
}
return <Component {...props} />;
};
WrapperComponent.displayName = componentName;
return WrapperComponent;
};

View File

@ -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,
);

View File

@ -28,9 +28,9 @@ import {
} from "../../actions";
import "./DefaultItems.scss";
import { useState } from "react";
import ConfirmDialog from "../ConfirmDialog";
import clsx from "clsx";
import { useSetAtom } from "jotai";
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
export const LoadScene = () => {
// FIXME Hack until we tie "t" to lang state
@ -122,41 +122,22 @@ 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();
const [showDialog, setShowDialog] = useState(false);
const toggleDialog = () => setShowDialog(!showDialog);
if (!actionManager.isActionEnabled(actionClearCanvas)) {
return null;
}
return (
<>
<DropdownMenuItem
icon={TrashIcon}
onSelect={toggleDialog}
data-testid="clear-canvas-button"
aria-label={t("buttons.clearReset")}
>
{t("buttons.clearReset")}
</DropdownMenuItem>
{/* FIXME this should live outside MainMenu so it stays open
if menu is closed */}
{showDialog && (
<ConfirmDialog
onConfirm={() => {
actionManager.executeAction(actionClearCanvas);
toggleDialog();
}}
onCancel={toggleDialog}
title={t("clearCanvasDialog.title")}
>
<p className="clear-canvas__content"> {t("alerts.clearReset")}</p>
</ConfirmDialog>
)}
</>
<DropdownMenuItem
icon={TrashIcon}
onSelect={() => setActiveConfirmDialog("clearCanvas")}
data-testid="clear-canvas-button"
aria-label={t("buttons.clearReset")}
>
{t("buttons.clearReset")}
</DropdownMenuItem>
);
};
ClearCanvas.displayName = "ClearCanvas";
@ -171,7 +152,9 @@ export const ToggleTheme = () => {
return (
<DropdownMenuItem
onSelect={() => {
onSelect={(event) => {
// do not close the menu when changing theme
event.preventDefault();
return actionManager.executeAction(actionToggleTheme);
}}
icon={appState.theme === "dark" ? SunIcon : MoonIcon}

View File

@ -11,46 +11,73 @@ import * as DefaultItems from "./DefaultItems";
import { UserList } from "../UserList";
import { t } from "../../i18n";
import { HamburgerMenuIcon } from "../icons";
import { withInternalFallback } from "../hoc/withInternalFallback";
import { composeEventHandlers } from "../../utils";
import { mainMenuTunnel } from "../tunnels";
const MainMenu = ({ children }: { children?: React.ReactNode }) => {
const device = useDevice();
const appState = useExcalidrawAppState();
const setAppState = useExcalidrawSetAppState();
const onClickOutside = device.isMobile
? undefined
: () => setAppState({ openMenu: null });
return (
<DropdownMenu open={appState.openMenu === "canvas"}>
<DropdownMenu.Trigger
onToggle={() => {
setAppState({
openMenu: appState.openMenu === "canvas" ? null : "canvas",
});
}}
>
{HamburgerMenuIcon}
</DropdownMenu.Trigger>
<DropdownMenu.Content onClickOutside={onClickOutside}>
{children}
{device.isMobile && appState.collaborators.size > 0 && (
<fieldset className="UserList-Wrapper">
<legend>{t("labels.collaborators")}</legend>
<UserList mobile={true} collaborators={appState.collaborators} />
</fieldset>
)}
</DropdownMenu.Content>
</DropdownMenu>
);
};
const MainMenu = Object.assign(
withInternalFallback(
"MainMenu",
({
children,
onSelect,
}: {
children?: React.ReactNode;
/**
* Called when any menu item is selected (clicked on).
*/
onSelect?: (event: Event) => void;
}) => {
const device = useDevice();
const appState = useExcalidrawAppState();
const setAppState = useExcalidrawSetAppState();
const onClickOutside = device.isMobile
? undefined
: () => setAppState({ openMenu: null });
MainMenu.Trigger = DropdownMenu.Trigger;
MainMenu.Item = DropdownMenu.Item;
MainMenu.ItemLink = DropdownMenu.ItemLink;
MainMenu.ItemCustom = DropdownMenu.ItemCustom;
MainMenu.Group = DropdownMenu.Group;
MainMenu.Separator = DropdownMenu.Separator;
MainMenu.DefaultItems = DefaultItems;
return (
<mainMenuTunnel.In>
<DropdownMenu open={appState.openMenu === "canvas"}>
<DropdownMenu.Trigger
onToggle={() => {
setAppState({
openMenu: appState.openMenu === "canvas" ? null : "canvas",
});
}}
>
{HamburgerMenuIcon}
</DropdownMenu.Trigger>
<DropdownMenu.Content
onClickOutside={onClickOutside}
onSelect={composeEventHandlers(onSelect, () => {
setAppState({ openMenu: null });
})}
>
{children}
{device.isMobile && appState.collaborators.size > 0 && (
<fieldset className="UserList-Wrapper">
<legend>{t("labels.collaborators")}</legend>
<UserList
mobile={true}
collaborators={appState.collaborators}
/>
</fieldset>
)}
</DropdownMenu.Content>
</DropdownMenu>
</mainMenuTunnel.In>
);
},
),
{
Trigger: DropdownMenu.Trigger,
Item: DropdownMenu.Item,
ItemLink: DropdownMenu.ItemLink,
ItemCustom: DropdownMenu.ItemCustom,
Group: DropdownMenu.Group,
Separator: DropdownMenu.Separator,
DefaultItems,
},
);
export default MainMenu;
MainMenu.displayName = "Menu";

View File

@ -0,0 +1,8 @@
import tunnel from "tunnel-rat";
export const mainMenuTunnel = tunnel();
export const welcomeScreenMenuHintTunnel = tunnel();
export const welcomeScreenToolbarHintTunnel = tunnel();
export const welcomeScreenHelpHintTunnel = tunnel();
export const welcomeScreenCenterTunnel = tunnel();
export const footerCenterTunnel = tunnel();

View File

@ -7,6 +7,7 @@ import {
useExcalidrawAppState,
} from "../App";
import { ExcalLogo, HelpIcon, LoadIcon, usersIcon } from "../icons";
import { welcomeScreenCenterTunnel } from "../tunnels";
const WelcomeScreenMenuItemContent = ({
icon,
@ -89,18 +90,20 @@ 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>
<welcomeScreenCenterTunnel.In>
<div className="welcome-screen-center">
{children || (
<>
<Logo />
<Heading>{t("welcomeScreen.defaults.center_heading")}</Heading>
<Menu>
<MenuItemLoadScene />
<MenuItemHelp />
</Menu>
</>
)}
</div>
</welcomeScreenCenterTunnel.In>
);
};
Center.displayName = "Center";

View File

@ -4,37 +4,48 @@ import {
WelcomeScreenMenuArrow,
WelcomeScreenTopToolbarArrow,
} from "../icons";
import {
welcomeScreenMenuHintTunnel,
welcomeScreenToolbarHintTunnel,
welcomeScreenHelpHintTunnel,
} from "../tunnels";
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")}
<welcomeScreenMenuHintTunnel.In>
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--menu">
{WelcomeScreenMenuArrow}
<div className="welcome-screen-decor-hint__label">
{children || t("welcomeScreen.defaults.menuHint")}
</div>
</div>
</div>
</welcomeScreenMenuHintTunnel.In>
);
};
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")}
<welcomeScreenToolbarHintTunnel.In>
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--toolbar">
<div className="welcome-screen-decor-hint__label">
{children || t("welcomeScreen.defaults.toolbarHint")}
</div>
{WelcomeScreenTopToolbarArrow}
</div>
{WelcomeScreenTopToolbarArrow}
</div>
</welcomeScreenToolbarHintTunnel.In>
);
};
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>
<welcomeScreenHelpHintTunnel.In>
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--help">
<div>{children || t("welcomeScreen.defaults.helpHint")}</div>
{WelcomeScreenHelpArrow}
</div>
</welcomeScreenHelpHintTunnel.In>
);
};
HelpHint.displayName = "HelpHint";

View File

@ -3,12 +3,21 @@ 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;
const WelcomeScreen = (props: { children?: React.ReactNode }) => {
return (
<>
{props.children || (
<>
<Center />
<MenuHint />
<ToolbarHint />
<HelpHint />
</>
)}
</>
);
};
WelcomeScreen.displayName = "WelcomeScreen";
WelcomeScreen.Center = Center;

View File

@ -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,7 +159,6 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
toggleTheme: null,
saveAsImage: true,
},
welcomeScreen: true,
};
// breakpoints

View File

@ -8,6 +8,10 @@
}
.excalidraw {
--ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
Roboto, Helvetica, Arial, sans-serif;
font-family: var(--ui-font);
position: relative;
overflow: hidden;
color: var(--text-primary-color);
@ -549,6 +553,7 @@
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);

View File

@ -65,13 +65,18 @@
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-bg);
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-active-bg);
background-color: var(--button-active-bg, var(--island-bg-color));
border-color: var(--button-active-border, var(--color-primary-darkest));
}
@ -85,9 +90,6 @@
svg {
color: var(--button-color, var(--color-primary-darker));
width: var(--button-width, var(--lg-icon-size));
height: var(--button-height, var(--lg-icon-size));
}
}
}

View File

@ -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();
}

View File

@ -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

View File

@ -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,
);

View File

@ -12,6 +12,20 @@ describe("Test wrapText", () => {
expect(res).toBe("Hello whats up ");
});
it("should work with emojis", () => {
const text = "😀";
const maxWidth = 1;
const res = wrapText(text, font, maxWidth);
expect(res).toBe("😀");
});
it("should show the text correctly when min width reached", () => {
const text = "Hello😀";
const maxWidth = 10;
const res = wrapText(text, font, maxWidth);
expect(res).toBe("H\ne\nl\nl\no\n😀");
});
describe("When text doesn't contain new lines", () => {
const text = "Hello whats up";
[
@ -157,7 +171,7 @@ describe("Test measureText", () => {
expect(res.container).toMatchInlineSnapshot(`
<div
style="position: absolute; white-space: pre-wrap; font: Emoji 20px 20px; min-height: 1em; width: 111px; overflow: hidden; word-break: break-word; line-height: 0px;"
style="position: absolute; white-space: pre-wrap; font: Emoji 20px 20px; min-height: 1em; max-width: 191px; overflow: hidden; word-break: break-word; line-height: 0px;"
>
<span
style="display: inline-block; overflow: hidden; width: 1px; height: 1px;"

View File

@ -271,12 +271,11 @@ export const measureText = (
container.style.whiteSpace = "pre";
container.style.font = font;
container.style.minHeight = "1em";
const textWidth = getTextWidth(text, font);
if (maxWidth) {
const lineHeight = getApproxLineHeight(font);
container.style.width = `${String(Math.min(textWidth, maxWidth) + 1)}px`;
// since we are adding a span of width 1px later
container.style.maxWidth = `${maxWidth + 1}px`;
container.style.overflow = "hidden";
container.style.wordBreak = "break-word";
container.style.lineHeight = `${String(lineHeight)}px`;
@ -293,11 +292,8 @@ export const measureText = (
container.appendChild(span);
// Baseline is important for positioning text on canvas
const baseline = span.offsetTop + span.offsetHeight;
// Since span adds 1px extra width to the container
let width = container.offsetWidth;
if (maxWidth && textWidth > maxWidth) {
width = width - 1;
}
// since we are adding a span of width 1px
const width = container.offsetWidth + 1;
const height = container.offsetHeight;
document.body.removeChild(container);
if (isTestEnv()) {
@ -332,8 +328,11 @@ const getLineWidth = (text: string, font: FontString) => {
if (isTestEnv()) {
return metrics.width * 10;
}
// Since measureText behaves differently in different browsers
// OS so considering a adjustment factor of 0.2
const adjustmentFactor = 0.2;
return metrics.width;
return metrics.width + adjustmentFactor;
};
export const getTextWidth = (text: string, font: FontString) => {
@ -359,96 +358,94 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
// This means its newline so push it
if (words.length === 1 && words[0] === "") {
lines.push(words[0]);
} else {
let currentLine = "";
let currentLineWidthTillNow = 0;
return; // continue
}
let currentLine = "";
let currentLineWidthTillNow = 0;
let index = 0;
while (index < words.length) {
const currentWordWidth = getLineWidth(words[index], font);
let index = 0;
while (index < words.length) {
const currentWordWidth = getLineWidth(words[index], font);
// Start breaking longer words exceeding max width
if (currentWordWidth >= maxWidth) {
// push current line since the current word exceeds the max width
// so will be appended in next line
// Start breaking longer words exceeding max width
if (currentWordWidth >= maxWidth) {
// push current line since the current word exceeds the max width
// so will be appended in next line
push(currentLine);
currentLine = "";
currentLineWidthTillNow = 0;
while (words[index].length > 0) {
const currentChar = String.fromCodePoint(
words[index].codePointAt(0)!,
);
const width = charWidth.calculate(currentChar, font);
currentLineWidthTillNow += width;
words[index] = words[index].slice(currentChar.length);
if (currentLineWidthTillNow >= maxWidth) {
// only remove last trailing space which we have added when joining words
if (currentLine.slice(-1) === " ") {
currentLine = currentLine.slice(0, -1);
}
push(currentLine);
currentLine = currentChar;
currentLineWidthTillNow = width;
} else {
currentLine += currentChar;
}
}
// push current line if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
push(currentLine);
currentLine = "";
currentLineWidthTillNow = 0;
while (words[index].length > 0) {
const currentChar = words[index][0];
const width = charWidth.calculate(currentChar, font);
currentLineWidthTillNow += width;
words[index] = words[index].slice(1);
if (currentLineWidthTillNow >= maxWidth) {
// only remove last trailing space which we have added when joining words
if (currentLine.slice(-1) === " ") {
currentLine = currentLine.slice(0, -1);
}
push(currentLine);
currentLine = currentChar;
currentLineWidthTillNow = width;
if (currentLineWidthTillNow === maxWidth) {
currentLine = "";
currentLineWidthTillNow = 0;
}
} else {
currentLine += currentChar;
}
}
// push current line if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
push(currentLine);
currentLine = "";
currentLineWidthTillNow = 0;
} else {
// space needs to be appended before next word
// as currentLine contains chars which couldn't be appended
// to previous line
currentLine += " ";
currentLineWidthTillNow += spaceWidth;
}
index++;
} else {
// Start appending words in a line till max width reached
while (currentLineWidthTillNow < maxWidth && index < words.length) {
const word = words[index];
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
// space needs to be appended before next word
// as currentLine contains chars which couldn't be appended
// to previous line
currentLine += " ";
currentLineWidthTillNow += spaceWidth;
}
if (currentLineWidthTillNow >= maxWidth) {
push(currentLine);
currentLineWidthTillNow = 0;
currentLine = "";
index++;
} else {
// Start appending words in a line till max width reached
while (currentLineWidthTillNow < maxWidth && index < words.length) {
const word = words[index];
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
break;
}
index++;
currentLine += `${word} `;
if (currentLineWidthTillNow >= maxWidth) {
push(currentLine);
currentLineWidthTillNow = 0;
currentLine = "";
// Push the word if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
const word = currentLine.slice(0, -1);
push(word);
currentLine = "";
currentLineWidthTillNow = 0;
break;
}
break;
}
if (currentLineWidthTillNow === maxWidth) {
index++;
currentLine += `${word} `;
// Push the word if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
const word = currentLine.slice(0, -1);
push(word);
currentLine = "";
currentLineWidthTillNow = 0;
break;
}
}
}
if (currentLine) {
// only remove last trailing space which we have added when joining words
if (currentLine.slice(-1) === " ") {
currentLine = currentLine.slice(0, -1);
if (currentLineWidthTillNow === maxWidth) {
currentLine = "";
currentLineWidthTillNow = 0;
}
push(currentLine);
}
}
if (currentLine) {
// only remove last trailing space which we have added when joining words
if (currentLine.slice(-1) === " ") {
currentLine = currentLine.slice(0, -1);
}
push(currentLine);
}
});
return lines.join("\n");
};

View File

@ -862,7 +862,7 @@ describe("textWysiwyg", () => {
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
110,
109.5,
17,
]
`);
@ -910,7 +910,7 @@ describe("textWysiwyg", () => {
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
425,
424,
-539,
]
`);
@ -1026,7 +1026,7 @@ describe("textWysiwyg", () => {
mouse.up(rectangle.x + 100, rectangle.y + 50);
expect(rectangle.x).toBe(80);
expect(rectangle.y).toBe(85);
expect(text.x).toBe(90);
expect(text.x).toBe(89.5);
expect(text.y).toBe(90);
Keyboard.withModifierKeys({ ctrl: true }, () => {

View File

@ -142,11 +142,11 @@ export const textWysiwyg = ({
const appState = app.state;
const updatedTextElement =
Scene.getScene(element)?.getElement<ExcalidrawTextElement>(id);
if (!updatedTextElement) {
return;
}
const { textAlign, verticalAlign } = updatedTextElement;
const approxLineHeight = getApproxLineHeight(
getFontString(updatedTextElement),
);
@ -161,6 +161,7 @@ export const textWysiwyg = ({
// Set to element height by default since that's
// what is going to be used for unbounded text
let textElementHeight = updatedTextElement.height;
if (container && updatedTextElement.containerId) {
if (isArrowElement(container)) {
const boundTextCoords =
@ -206,7 +207,6 @@ export const textWysiwyg = ({
maxHeight = getMaxContainerHeight(container);
// autogrow container height if text exceeds
if (!isArrowElement(container) && textElementHeight > maxHeight) {
const diff = Math.min(
textElementHeight - maxHeight,
@ -276,7 +276,6 @@ export const textWysiwyg = ({
// Make sure text editor height doesn't go beyond viewport
const editorMaxHeight =
(appState.height - viewportY) / appState.zoom.value;
Object.assign(editable.style, {
font: getFontString(updatedTextElement),
// must be defined *after* font ¯\_(ツ)_/¯
@ -395,11 +394,12 @@ export const textWysiwyg = ({
// first line as well as setting height to "auto"
// doubles the height as soon as user starts typing
if (isBoundToContainer(element) && lines > 1) {
const container = getContainerElement(element);
let height = "auto";
editable.style.height = "0px";
let heightSet = false;
if (lines === 2) {
const container = getContainerElement(element);
const actualLineCount = wrapText(
editable.value,
font,
@ -416,6 +416,14 @@ export const textWysiwyg = ({
heightSet = true;
}
}
const wrappedText = wrapText(
normalizeText(editable.value),
font,
getMaxContainerWidth(container!),
);
const width = getTextWidth(wrappedText, font);
editable.style.width = `${width}px`;
if (!heightSet) {
editable.style.height = `${editable.scrollHeight}px`;
}

View File

@ -0,0 +1,21 @@
import React from "react";
import { Footer } from "../../packages/excalidraw/index";
import { EncryptedIcon } from "./EncryptedIcon";
import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
export const AppFooter = React.memo(() => {
return (
<Footer>
<div
style={{
display: "flex",
gap: ".5rem",
alignItems: "center",
}}
>
<ExcalidrawPlusAppLink />
<EncryptedIcon />
</div>
</Footer>
);
});

View File

@ -0,0 +1,40 @@
import React from "react";
import { PlusPromoIcon } from "../../components/icons";
import { MainMenu } from "../../packages/excalidraw/index";
import { LanguageList } from "./LanguageList";
export const AppMainMenu: React.FC<{
setCollabDialogShown: (toggle: boolean) => any;
isCollaborating: boolean;
}> = React.memo((props) => {
return (
<MainMenu>
<MainMenu.DefaultItems.LoadScene />
<MainMenu.DefaultItems.SaveToActiveFile />
<MainMenu.DefaultItems.Export />
<MainMenu.DefaultItems.SaveAsImage />
<MainMenu.DefaultItems.LiveCollaborationTrigger
isCollaborating={props.isCollaborating}
onSelect={() => props.setCollabDialogShown(true)}
/>
<MainMenu.DefaultItems.Help />
<MainMenu.DefaultItems.ClearCanvas />
<MainMenu.Separator />
<MainMenu.ItemLink
icon={PlusPromoIcon}
href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger"
className="ExcalidrawPlus"
>
Excalidraw+
</MainMenu.ItemLink>
<MainMenu.DefaultItems.Socials />
<MainMenu.Separator />
<MainMenu.DefaultItems.ToggleTheme />
<MainMenu.ItemCustom>
<LanguageList style={{ width: "100%" }} />
</MainMenu.ItemCustom>
<MainMenu.DefaultItems.ChangeCanvasBackground />
</MainMenu>
);
});

View File

@ -0,0 +1,64 @@
import React from "react";
import { PlusPromoIcon } from "../../components/icons";
import { t } from "../../i18n";
import { WelcomeScreen } from "../../packages/excalidraw/index";
import { isExcalidrawPlusSignedUser } from "../app_constants";
export const AppWelcomeScreen: React.FC<{
setCollabDialogShown: (toggle: boolean) => any;
}> = React.memo((props) => {
let headingContent;
if (isExcalidrawPlusSignedUser) {
headingContent = t("welcomeScreen.app.center_heading_plus")
.split(/(Excalidraw\+)/)
.map((bit, idx) => {
if (bit === "Excalidraw+") {
return (
<a
style={{ pointerEvents: "all" }}
href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
key={idx}
>
Excalidraw+
</a>
);
}
return bit;
});
} else {
headingContent = t("welcomeScreen.app.center_heading");
}
return (
<WelcomeScreen>
<WelcomeScreen.Hints.MenuHint>
{t("welcomeScreen.app.menuHint")}
</WelcomeScreen.Hints.MenuHint>
<WelcomeScreen.Hints.ToolbarHint />
<WelcomeScreen.Hints.HelpHint />
<WelcomeScreen.Center>
<WelcomeScreen.Center.Logo />
<WelcomeScreen.Center.Heading>
{headingContent}
</WelcomeScreen.Center.Heading>
<WelcomeScreen.Center.Menu>
<WelcomeScreen.Center.MenuItemLoadScene />
<WelcomeScreen.Center.MenuItemHelp />
<WelcomeScreen.Center.MenuItemLiveCollaborationTrigger
onSelect={() => props.setCollabDialogShown(true)}
/>
{!isExcalidrawPlusSignedUser && (
<WelcomeScreen.Center.MenuItemLink
href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
shortcut={null}
icon={PlusPromoIcon}
>
Try Excalidraw Plus!
</WelcomeScreen.Center.MenuItemLink>
)}
</WelcomeScreen.Center.Menu>
</WelcomeScreen.Center>
</WelcomeScreen>
);
});

View File

@ -1,6 +1,6 @@
import polyfill from "../polyfill";
import LanguageDetector from "i18next-browser-languagedetector";
import { useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { trackEvent } from "../analytics";
import { getDefaultAppState } from "../appState";
import { ErrorDialog } from "../components/ErrorDialog";
@ -24,10 +24,7 @@ import { t } from "../i18n";
import {
Excalidraw,
defaultLang,
Footer,
MainMenu,
LiveCollaborationTrigger,
WelcomeScreen,
} from "../packages/excalidraw/index";
import {
AppState,
@ -47,7 +44,6 @@ import {
} from "../utils";
import {
FIREBASE_STORAGE_PREFIXES,
isExcalidrawPlusSignedUser,
STORAGE_KEYS,
SYNC_BROWSER_TABS_TIMEOUT,
} from "./app_constants";
@ -85,10 +81,9 @@ import { atom, Provider, useAtom } from "jotai";
import { jotaiStore, useAtomWithInitialValue } from "../jotai";
import { reconcileElements } from "./collab/reconciliation";
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
import { EncryptedIcon } from "./components/EncryptedIcon";
import { ExcalidrawPlusAppLink } from "./components/ExcalidrawPlusAppLink";
import { LanguageList } from "./components/LanguageList";
import { PlusPromoIcon } from "../components/icons";
import { AppMainMenu } from "./components/AppMainMenu";
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
import { AppFooter } from "./components/AppFooter";
polyfill();
@ -604,96 +599,6 @@ const ExcalidrawWrapper = () => {
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
};
const renderMenu = () => {
return (
<MainMenu>
<MainMenu.DefaultItems.LoadScene />
<MainMenu.DefaultItems.SaveToActiveFile />
<MainMenu.DefaultItems.Export />
<MainMenu.DefaultItems.SaveAsImage />
<MainMenu.DefaultItems.LiveCollaborationTrigger
isCollaborating={isCollaborating}
onSelect={() => setCollabDialogShown(true)}
/>
<MainMenu.DefaultItems.Help />
<MainMenu.DefaultItems.ClearCanvas />
<MainMenu.Separator />
<MainMenu.ItemLink
icon={PlusPromoIcon}
href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger"
className="ExcalidrawPlus"
>
Excalidraw+
</MainMenu.ItemLink>
<MainMenu.DefaultItems.Socials />
<MainMenu.Separator />
<MainMenu.DefaultItems.ToggleTheme />
<MainMenu.ItemCustom>
<LanguageList style={{ width: "100%" }} />
</MainMenu.ItemCustom>
<MainMenu.DefaultItems.ChangeCanvasBackground />
</MainMenu>
);
};
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%" }}
@ -750,15 +655,12 @@ const ExcalidrawWrapper = () => {
);
}}
>
{renderMenu()}
<Footer>
<div style={{ display: "flex", gap: ".5rem", alignItems: "center" }}>
<ExcalidrawPlusAppLink />
<EncryptedIcon />
</div>
</Footer>
{welcomeScreenJSX}
<AppMainMenu
setCollabDialogShown={setCollabDialogShown}
isCollaborating={isCollaborating}
/>
<AppWelcomeScreen setCollabDialogShown={setCollabDialogShown} />
<AppFooter />
</Excalidraw>
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
{errorMessage && (

View File

@ -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",

View File

@ -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": "اختر اللغة",
@ -447,10 +447,16 @@
"d9480f": "برتقالي 9"
},
"welcomeScreen": {
"data": "",
"switchToPlusApp": "",
"menuHints": "",
"toolbarHints": "",
"helpHints": ""
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": ""
},
"welcomeScreen": {
"data": "",
"switchToPlusApp": "",
"menuHints": "",
"toolbarHints": "",
"helpHints": ""
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": ""
},
"welcomeScreen": {
"data": "",
"switchToPlusApp": "",
"menuHints": "",
"toolbarHints": "",
"helpHints": ""
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -447,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": ""
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": "Oranzova"
},
"welcomeScreen": {
"data": "",
"switchToPlusApp": "",
"menuHints": "",
"toolbarHints": "",
"helpHints": ""
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": ""
},
"welcomeScreen": {
"data": "",
"switchToPlusApp": "",
"menuHints": "",
"toolbarHints": "",
"helpHints": ""
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -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",
@ -447,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"
}
}
}

View File

@ -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": "Επιλογή γλώσσας",
@ -202,8 +202,8 @@
"invalidSVGString": "Μη έγκυρο SVG.",
"cannotResolveCollabServer": "Αδυναμία σύνδεσης με τον διακομιστή συνεργασίας. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά.",
"importLibraryError": "Αδυναμία φόρτωσης βιβλιοθήκης",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": ""
"collabSaveFailed": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή. Αν το προβλήματα παραμείνει, θα πρέπει να αποθηκεύσετε το αρχείο σας τοπικά για να βεβαιωθείτε ότι δεν χάνετε την εργασία σας.",
"collabSaveFailed_sizeExceeded": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή, ο καμβάς φαίνεται να είναι πολύ μεγάλος. Θα πρέπει να αποθηκεύσετε το αρχείο τοπικά για να βεβαιωθείτε ότι δεν θα χάσετε την εργασία σας."
},
"toolBar": {
"selection": "Επιλογή",
@ -217,7 +217,7 @@
"text": "Κείμενο",
"library": "Βιβλιοθήκη",
"lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο",
"penMode": "",
"penMode": "Λειτουργία μολυβιού - αποτροπή αφής",
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
"eraser": "Γόμα"
},
@ -238,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": "Κάντε κλικ για να τοποθετήσετε την εικόνα ή κάντε κλικ και σύρετε για να ορίσετε το μέγεθός της χειροκίνητα",
@ -314,8 +314,8 @@
"zoomToFit": "Zoom ώστε να χωρέσουν όλα τα στοιχεία",
"zoomToSelection": "Ζουμ στην επιλογή",
"toggleElementLock": "Κλείδωμα/Ξεκλείδωμα επιλογής",
"movePageUpDown": "",
"movePageLeftRight": ""
"movePageUpDown": "Μετακίνηση σελίδας πάνω/κάτω",
"movePageLeftRight": "Μετακίνηση σελίδας αριστερά/δεξιά"
},
"clearCanvasDialog": {
"title": "Καθαρισμός καμβά"
@ -397,7 +397,7 @@
"fileSavedToFilename": "Αποθηκεύτηκε στο {filename}",
"canvas": "καμβάς",
"selection": "επιλογή",
"pasteAsSingleElement": ""
"pasteAsSingleElement": "Χρησιμοποίησε το {{shortcut}} για να επικολλήσεις ως ένα μόνο στοιχείο,\nή να επικολλήσεις σε έναν υπάρχοντα επεξεργαστή κειμένου"
},
"colors": {
"ffffff": "Λευκό",
@ -447,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": "Συντομεύσεις και βοήθεια"
}
}
}

View File

@ -220,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",
@ -228,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",
@ -246,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",

View File

@ -447,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"
}
}
}

View File

@ -1,7 +1,7 @@
{
"labels": {
"paste": "Itsatsi",
"pasteAsPlaintext": "",
"pasteAsPlaintext": "Itsatsi testu arrunt gisa",
"pasteCharts": "Itsatsi grafikoak",
"selectAll": "Hautatu dena",
"multiSelect": "Gehitu elementua hautapenera",
@ -202,8 +202,8 @@
"invalidSVGString": "SVG baliogabea.",
"cannotResolveCollabServer": "Ezin izan da elkarlaneko zerbitzarira konektatu. Mesedez, berriro kargatu orria eta saiatu berriro.",
"importLibraryError": "Ezin izan da liburutegia kargatu",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": ""
"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",
@ -238,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",
@ -314,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"
@ -397,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",
@ -447,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"
}
}
}

View File

@ -447,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": ""
}
}
}

View File

@ -447,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": ""
}
}
}

View File

@ -447,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"
}
}
}

View File

@ -202,8 +202,8 @@
"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",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": ""
"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",
@ -447,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"
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": "כתום 9"
},
"welcomeScreen": {
"data": "",
"switchToPlusApp": "",
"menuHints": "",
"toolbarHints": "",
"helpHints": ""
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": "नारंगी"
},
"welcomeScreen": {
"data": "आपका सर्व डेटा ब्राउज़र के भीतर स्थानिक जगह पे सुरक्षित किया गया.",
"switchToPlusApp": "बजाय आपको Excalidraw+ जगह जाना है?",
"menuHints": "निर्यात, पसंद, भाषायें, ...",
"toolbarHints": "औजार चुने और चित्रकारी प्रारंभ करे!",
"helpHints": "शॉर्ट्कट & सहाय्य"
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -447,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": ""
}
}
}

View File

@ -202,8 +202,8 @@
"invalidSVGString": "SVG tidak valid.",
"cannotResolveCollabServer": "Tidak dapat terhubung ke server kolab. Muat ulang laman dan coba lagi.",
"importLibraryError": "Tidak dapat memuat pustaka",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": ""
"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",
@ -447,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"
}
}
}

View File

@ -202,8 +202,8 @@
"invalidSVGString": "SVG non valido.",
"cannotResolveCollabServer": "Impossibile connettersi al server di collab. Ricarica la pagina e riprova.",
"importLibraryError": "Impossibile caricare la libreria",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": ""
"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",
@ -447,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"
}
}
}

View File

@ -447,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": "ショートカットとヘルプ"
}
}
}

View File

@ -72,7 +72,7 @@
"layers": "Tissiyin",
"actions": "Tigawin",
"language": "Tutlayt",
"liveCollaboration": "",
"liveCollaboration": "Amɛiwen s srid...",
"duplicateSelection": "Sisleg",
"untitled": "War azwel",
"name": "Isem",
@ -314,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ɣ"
@ -447,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": ""
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": ""
},
"welcomeScreen": {
"data": "",
"switchToPlusApp": "",
"menuHints": "",
"toolbarHints": "",
"helpHints": ""
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -1,7 +1,7 @@
{
"labels": {
"paste": "붙여넣기",
"pasteAsPlaintext": "",
"pasteAsPlaintext": "일반 텍스트로 붙여넣기",
"pasteCharts": "차트 붙여넣기",
"selectAll": "전체 선택",
"multiSelect": "선택 영역에 추가하기",
@ -72,7 +72,7 @@
"layers": "레이어",
"actions": "동작",
"language": "언어",
"liveCollaboration": "",
"liveCollaboration": "실시간 협업...",
"duplicateSelection": "복제",
"untitled": "제목 없음",
"name": "이름",
@ -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": "언어 선택",
@ -202,8 +202,8 @@
"invalidSVGString": "유효하지 않은 SVG입니다.",
"cannotResolveCollabServer": "협업 서버에 접속하는데 실패했습니다. 페이지를 새로고침하고 다시 시도해보세요.",
"importLibraryError": "라이브러리를 불러오지 못했습니다.",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": ""
"collabSaveFailed": "데이터베이스에 저장하지 못했습니다. 문제가 계속 된다면, 작업 내용을 잃지 않도록 로컬 저장소에 저장해 주세요.",
"collabSaveFailed_sizeExceeded": "데이터베이스에 저장하지 못했습니다. 캔버스가 너무 큰 거 같습니다. 문제가 계속 된다면, 작업 내용을 잃지 않도록 로컬 저장소에 저장해 주세요."
},
"toolBar": {
"selection": "선택",
@ -217,7 +217,7 @@
"text": "텍스트",
"library": "라이브러리",
"lock": "선택된 도구 유지하기",
"penMode": "",
"penMode": "펜 모드 - 터치 방지",
"link": "선택한 도형에 대해서 링크를 추가/업데이트",
"eraser": "지우개"
},
@ -238,7 +238,7 @@
"resize": "SHIFT 키를 누르면서 조정하면 크기의 비율이 제한됩니다.\nALT를 누르면서 조정하면 중앙을 기준으로 크기를 조정합니다.",
"resizeImage": "SHIFT를 눌러서 자유롭게 크기를 변경하거나,\nALT를 눌러서 중앙을 고정하고 크기를 변경하기",
"rotate": "SHIFT 키를 누르면서 회전하면 각도를 제한할 수 있습니다.",
"lineEditor_info": "",
"lineEditor_info": "포인트를 편집하려면 Ctrl/Cmd을 누르고 더블 클릭을 하거나 Ctrl/Cmd + Enter를 누르세요",
"lineEditor_pointSelected": "Delete 키로 꼭짓점을 제거하거나,\nCtrlOrCmd+D 로 복제하거나, 드래그 해서 이동시키기",
"lineEditor_nothingSelected": "꼭짓점을 선택해서 수정하거나 (SHIFT를 눌러서 여러개 선택),\nAlt를 누르고 클릭해서 새로운 꼭짓점 추가하기",
"placeImage": "클릭해서 이미지를 배치하거나, 클릭하고 드래그해서 사이즈를 조정하기",
@ -314,8 +314,8 @@
"zoomToFit": "모든 요소가 보이도록 확대/축소",
"zoomToSelection": "선택 영역으로 확대/축소",
"toggleElementLock": "선택한 항목을 잠금/잠금 해제",
"movePageUpDown": "",
"movePageLeftRight": ""
"movePageUpDown": "페이지 움직이기 위/아래",
"movePageLeftRight": "페이지 움직이기 좌/우"
},
"clearCanvasDialog": {
"title": "캔버스 지우기"
@ -397,7 +397,7 @@
"fileSavedToFilename": "{filename} 로 저장되었습니다",
"canvas": "캔버스",
"selection": "선택한 요소",
"pasteAsSingleElement": ""
"pasteAsSingleElement": "단일 요소로 붙여넣거나, 기존 텍스트 에디터에 붙여넣으려면 {{shortcut}} 을 사용하세요."
},
"colors": {
"ffffff": "화이트",
@ -447,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": "단축키 & 도움말"
}
}
}

View File

@ -447,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": ""
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": "Oranžinė 9"
},
"welcomeScreen": {
"data": "Visi tavo duomenys išsaugoti lokaliai naršyklėje.",
"switchToPlusApp": "Ar vietoj to norėjai patekti į Excalidraw+?",
"menuHints": "Eksportavimas, parinktys, kalbos, ...",
"toolbarHints": "Pasirink įrankį ir Pradėk piešti!",
"helpHints": "Spartieji klavišai ir pagalba"
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": "Oranžs 9"
},
"welcomeScreen": {
"data": "Visi jūsu dati tiek glabāti uz vietas jūsu pārlūkā.",
"switchToPlusApp": "",
"menuHints": "Eksportēšana, iestatījumi, valodas...",
"toolbarHints": "Izvēlieties rīku un sāciet zīmēt!",
"helpHints": "Saīsnes un palīdzība"
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -447,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": ""
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": ""
},
"welcomeScreen": {
"data": "",
"switchToPlusApp": "",
"menuHints": "",
"toolbarHints": "",
"helpHints": ""
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": "Oransje 9"
},
"welcomeScreen": {
"data": "Alle dine data lagres lokalt i din nettleser.",
"switchToPlusApp": "Ønsker du å gå til Excalidraw+ i stedet?",
"menuHints": "Eksporter, innstillinger, språk, ...",
"toolbarHints": "Velg et verktøy og start å tegne!",
"helpHints": "Snarveier & hjelp"
"app": {
"center_heading": "Alle dine data lagres lokalt i din nettleser.",
"center_heading_plus": "Ønsker du å gå til Excalidraw+ i stedet?",
"menuHint": "Eksporter, innstillinger, språk, ..."
},
"defaults": {
"menuHint": "Eksporter, innstillinger og mer...",
"center_heading": "Diagrammer. Gjort. Enkelt.",
"toolbarHint": "Velg et verktøy og start å tegne!",
"helpHint": "Snarveier & hjelp"
}
}
}

View File

@ -343,7 +343,7 @@
},
"noteDescription": {
"pre": "",
"link": "",
"link": "openbare repository",
"post": ""
},
"noteGuidelines": {
@ -447,10 +447,16 @@
"d9480f": "Oranje 9"
},
"welcomeScreen": {
"data": "",
"switchToPlusApp": "",
"menuHints": "",
"toolbarHints": "",
"helpHints": ""
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": "Oransj 9"
},
"welcomeScreen": {
"data": "",
"switchToPlusApp": "",
"menuHints": "",
"toolbarHints": "",
"helpHints": ""
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": "Irange 9"
},
"welcomeScreen": {
"data": "Totas las donadas son enregistradas dins vòstre navegador.",
"switchToPlusApp": "Volètz puslèu utilizar Excalidraw+?",
"menuHints": "Exportar, preferéncias, lengas, ...",
"toolbarHints": "Prenètz un esplech e començatz de dessenhar!",
"helpHints": "Acorchis e ajuda"
"app": {
"center_heading": "Totas las donadas son enregistradas dins vòstre navegador.",
"center_heading_plus": "Voliatz puslèu utilizar Excalidraw+ a la plaça?",
"menuHint": "Exportar, preferéncias, lengas, ..."
},
"defaults": {
"menuHint": "Exportar, preferéncias, e mai...",
"center_heading": "Diagram. Tot. Simplament.",
"toolbarHint": "Prenètz un esplech e començatz de dessenhar!",
"helpHint": "Acorchis e ajuda"
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": "ਸੰਤਰੀ 9"
},
"welcomeScreen": {
"data": "",
"switchToPlusApp": "",
"menuHints": "",
"toolbarHints": "",
"helpHints": ""
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -1,50 +1,50 @@
{
"ar-SA": 86,
"ar-SA": 87,
"bg-BG": 55,
"bn-BD": 60,
"ca-ES": 94,
"cs-CZ": 75,
"da-DK": 33,
"de-DE": 100,
"el-GR": 95,
"el-GR": 100,
"en": 100,
"es-ES": 100,
"eu-ES": 98,
"fa-IR": 98,
"eu-ES": 100,
"fa-IR": 96,
"fi-FI": 93,
"fr-FR": 100,
"gl-ES": 99,
"he-IL": 91,
"hi-IN": 71,
"hu-HU": 90,
"id-ID": 99,
"it-IT": 99,
"gl-ES": 100,
"he-IL": 90,
"hi-IN": 69,
"hu-HU": 89,
"id-ID": 100,
"it-IT": 100,
"ja-JP": 100,
"kab-KAB": 94,
"kab-KAB": 93,
"kk-KZ": 21,
"ko-KR": 95,
"ku-TR": 98,
"lt-LT": 66,
"lv-LV": 99,
"mr-IN": 100,
"my-MM": 42,
"ko-KR": 99,
"ku-TR": 96,
"lt-LT": 64,
"lv-LV": 98,
"mr-IN": 98,
"my-MM": 41,
"nb-NO": 100,
"nl-NL": 91,
"nn-NO": 91,
"nn-NO": 90,
"oc-FR": 98,
"pa-IN": 83,
"pl-PL": 85,
"pt-BR": 100,
"pt-PT": 98,
"pt-BR": 98,
"pt-PT": 100,
"ro-RO": 100,
"ru-RU": 100,
"ru-RU": 98,
"si-LK": 8,
"sk-SK": 99,
"sk-SK": 100,
"sl-SI": 100,
"sv-SE": 98,
"sv-SE": 96,
"ta-IN": 93,
"tr-TR": 98,
"uk-UA": 97,
"uk-UA": 96,
"vi-VN": 20,
"zh-CN": 100,
"zh-HK": 26,

View File

@ -447,10 +447,16 @@
"d9480f": "Pomarańczowy 9"
},
"welcomeScreen": {
"data": "",
"switchToPlusApp": "",
"menuHints": "",
"toolbarHints": "",
"helpHints": ""
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": "Laranja 9"
},
"welcomeScreen": {
"data": "Todos os dados são salvos localmente no seu navegador.",
"switchToPlusApp": "Você queria ir para o Excalidraw+ em vez disso?",
"menuHints": "Exportar, preferências, idiomas, ...",
"toolbarHints": "Escolha uma ferramenta & Comece a desenhar!",
"helpHints": "Atalhos & ajuda"
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -1,7 +1,7 @@
{
"labels": {
"paste": "Colar",
"pasteAsPlaintext": "",
"pasteAsPlaintext": "Colar como texto simples",
"pasteCharts": "Colar gráficos",
"selectAll": "Selecionar tudo",
"multiSelect": "Adicionar elemento à seleção",
@ -202,8 +202,8 @@
"invalidSVGString": "SVG inválido.",
"cannotResolveCollabServer": "Não foi possível fazer a ligação ao servidor colaborativo. Por favor, volte a carregar a página e tente novamente.",
"importLibraryError": "Não foi possível carregar a biblioteca",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": ""
"collabSaveFailed": "Não foi possível guardar na base de dados de backend. Se os problemas persistirem, guarde o ficheiro localmente para garantir que não perde o seu trabalho.",
"collabSaveFailed_sizeExceeded": "Não foi possível guardar na base de dados de backend, o ecrã parece estar muito grande. Deve guardar o ficheiro localmente para garantir que não perde o seu trabalho."
},
"toolBar": {
"selection": "Seleção",
@ -238,7 +238,7 @@
"resize": "Pode restringir as proporções mantendo a tecla SHIFT premida enquanto redimensiona,\nmantenha a tecla ALT premida para redimensionar a partir do centro",
"resizeImage": "Pode redimensionar livremente mantendo pressionada a tecla SHIFT,\nmantenha pressionada a tecla ALT para redimensionar do centro",
"rotate": "Pode restringir os ângulos mantendo a tecla SHIFT premida enquanto roda",
"lineEditor_info": "",
"lineEditor_info": "Pressione CtrlOrCmd e faça um duplo-clique ou pressione CtrlOrCmd + Enter para editar pontos",
"lineEditor_pointSelected": "Carregue na tecla Delete para remover o(s) ponto(s), CtrlOuCmd+D para duplicar, ou arraste para mover",
"lineEditor_nothingSelected": "Seleccione um ponto para editar (carregue em SHIFT para seleccionar vários),\nou carregue em Alt e clique para acrescentar novos pontos",
"placeImage": "Clique para colocar a imagem ou clique e arraste para definir o seu tamanho manualmente",
@ -314,8 +314,8 @@
"zoomToFit": "Ajustar para todos os elementos caberem",
"zoomToSelection": "Ampliar a seleção",
"toggleElementLock": "Trancar/destrancar selecção",
"movePageUpDown": "",
"movePageLeftRight": ""
"movePageUpDown": "Mover página para cima / baixo",
"movePageLeftRight": "Mover página para esquerda / direita"
},
"clearCanvasDialog": {
"title": "Apagar tela"
@ -397,7 +397,7 @@
"fileSavedToFilename": "Guardado como {filename}",
"canvas": "área de desenho",
"selection": "seleção",
"pasteAsSingleElement": ""
"pasteAsSingleElement": "Usar {{shortcut}} para colar como um único elemento,\nou colar num editor de texto existente"
},
"colors": {
"ffffff": "Branco",
@ -447,10 +447,16 @@
"d9480f": "Laranja 9"
},
"welcomeScreen": {
"data": "Todos os dados estão guardados no seu navegador local.",
"switchToPlusApp": "Queria antes ir para o Excalidraw+?",
"menuHints": "Exportar, preferências, idiomas...",
"toolbarHints": "Escolha uma ferramenta e comece a desenhar!",
"helpHints": "Atalhos e ajuda"
"app": {
"center_heading": "Todos os dados são guardados no seu navegador local.",
"center_heading_plus": "Queria antes ir para o Excalidraw+?",
"menuHint": "Exportar, preferências, idiomas..."
},
"defaults": {
"menuHint": "Exportar, preferências e outros...",
"center_heading": "Diagramas. Feito. Simples.",
"toolbarHint": "Escolha uma ferramenta e comece a desenhar!",
"helpHint": "Atalhos e ajuda"
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": "Portocaliu 9"
},
"welcomeScreen": {
"data": "Toate datele tale sunt salvate local în navigatorul tău.",
"switchToPlusApp": "Ai vrut să mergi în schimb la Excalidraw+?",
"menuHints": "Exportare, preferințe, limbi, ...",
"toolbarHints": "Alege un instrument și începe să desenezi!",
"helpHints": "Comenzi rapide și ajutor"
"app": {
"center_heading": "Toate datele tale sunt salvate local în navigatorul tău.",
"center_heading_plus": "Ai vrut să mergi în schimb la Excalidraw+?",
"menuHint": "Exportare, preferințe, limbi, ..."
},
"defaults": {
"menuHint": "Exportare, preferințe și mai multe...",
"center_heading": "Diagrame. Făcute. Simple.",
"toolbarHint": "Alege un instrument și începe să desenezi!",
"helpHint": "Comenzi rapide și ajutor"
}
}
}

View File

@ -447,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": ""
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": ""
},
"welcomeScreen": {
"data": "",
"switchToPlusApp": "",
"menuHints": "",
"toolbarHints": "",
"helpHints": ""
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -202,8 +202,8 @@
"invalidSVGString": "Nevalidné SVG.",
"cannotResolveCollabServer": "Nepodarilo sa pripojiť ku kolaboračnému serveru. Prosím obnovte stránku a skúste to znovu.",
"importLibraryError": "Nepodarilo sa načítať knižnicu",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": ""
"collabSaveFailed": "Uloženie do databázy sa nepodarilo. Ak tento problém pretrváva uložte si váš súbor lokálne aby ste nestratili vašu prácu.",
"collabSaveFailed_sizeExceeded": "Uloženie do databázy sa nepodarilo, pretože veľkosť plátna je príliš veľká. Uložte si váš súbor lokálne aby ste nestratili vašu prácu."
},
"toolBar": {
"selection": "Výber",
@ -447,10 +447,16 @@
"d9480f": "Oranžová 9"
},
"welcomeScreen": {
"data": "Všetky vaše dáta sú uložené lokálne vo vašom prehliadači.",
"switchToPlusApp": "Chceli ste namiesto toho prejsť do Excalidraw+?",
"menuHints": "Exportovanie, nastavenia, jazyky, ...",
"toolbarHints": "Zvoľte nástroj a začnite kresliť!",
"helpHints": "Klávesové skratky a pomocník"
"app": {
"center_heading": "Všetky vaše dáta sú uložené lokálne vo vašom prehliadači.",
"center_heading_plus": "Chceli ste namiesto toho prejsť do Excalidraw+?",
"menuHint": "Exportovanie, nastavenia, jazyky, ..."
},
"defaults": {
"menuHint": "Exportovanie, nastavenia a ďalšie...",
"center_heading": "Diagramy. Jednoducho.",
"toolbarHint": "Zvoľte nástroj a začnite kresliť!",
"helpHint": "Klávesové skratky a pomocník"
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": "Oranžna 9"
},
"welcomeScreen": {
"data": "Vsi vaši podatki so shranjeni lokalno v vašem brskalniku.",
"switchToPlusApp": "Ste namesto tega želeli odpreti Excalidraw+?",
"menuHints": "Izvoz, nastavitve, jeziki, ...",
"toolbarHints": "Izberi orodje in začni z risanjem!",
"helpHints": "Bljižnice in pomoč"
"app": {
"center_heading": "Vsi vaši podatki so shranjeni lokalno v vašem brskalniku.",
"center_heading_plus": "Ste namesto tega želeli odpreti Excalidraw+?",
"menuHint": "Izvoz, nastavitve, jeziki, ..."
},
"defaults": {
"menuHint": "Izvoz, nastavitve in več ...",
"center_heading": "Diagrami. Enostavno.",
"toolbarHint": "Izberi orodje in začni z risanjem!",
"helpHint": "Bližnjice in pomoč"
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": "Orange 9"
},
"welcomeScreen": {
"data": "All din data sparas lokalt i din webbläsare.",
"switchToPlusApp": "Ville du gå till Excalidraw+ istället?",
"menuHints": "Export, inställningar, språk, ...",
"toolbarHints": "Välj ett verktyg och börja rita!",
"helpHints": "Genvägar och hjälp"
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": "ஆரஞ்சு 9"
},
"welcomeScreen": {
"data": "",
"switchToPlusApp": "",
"menuHints": "",
"toolbarHints": "",
"helpHints": ""
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -202,8 +202,8 @@
"invalidSVGString": "Geçersiz SVG.",
"cannotResolveCollabServer": "İş birliği sunucusuna bağlanılamıyor. Lütfen sayfayı yenileyip tekrar deneyin.",
"importLibraryError": "Kütüphane yüklenemedi",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": ""
"collabSaveFailed": "Backend veritabanına kaydedilemedi. Eğer problem devam ederse, çalışmanızı korumak için dosyayı yerel olarak kaydetmelisiniz.",
"collabSaveFailed_sizeExceeded": "Backend veritabanına kaydedilemedi; tuval çok büyük. Çalışmanızı korumak için dosyayı yerel olarak kaydetmelisiniz."
},
"toolBar": {
"selection": "Seçme",
@ -314,8 +314,8 @@
"zoomToFit": "Tüm öğeleri sığdırmak için yakınlaştır",
"zoomToSelection": "Seçime yakınlaş",
"toggleElementLock": "Seçimi Kilitle/çöz",
"movePageUpDown": "",
"movePageLeftRight": ""
"movePageUpDown": "Sayfayı yukarı/aşağı kaydır",
"movePageLeftRight": "Sayfayı sola/sağa kaydır"
},
"clearCanvasDialog": {
"title": "Tuvali temizle"
@ -397,7 +397,7 @@
"fileSavedToFilename": "{filename} kaydedildi",
"canvas": "tuval",
"selection": "seçim",
"pasteAsSingleElement": ""
"pasteAsSingleElement": "Tekil obje olarak yapıştırmak için veya var olan bir metin editörüne yapıştırmak için {{shortcut}} kullanın"
},
"colors": {
"ffffff": "Beyaz",
@ -447,10 +447,16 @@
"d9480f": "Turuncu 9"
},
"welcomeScreen": {
"data": "Tüm veri internet gezgininize yerel olarak kaydedildi.",
"switchToPlusApp": "Excalidraw+ kullanmak ister miydiniz?",
"menuHints": "Dışa aktar, seçenkeler, diller, ...",
"toolbarHints": "Bir araç seçin & Çizime başlayın!",
"helpHints": "Kısayollar & yardım"
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": "Помаранчевий 9"
},
"welcomeScreen": {
"data": "",
"switchToPlusApp": "",
"menuHints": "",
"toolbarHints": "Оберіть інструмент і почніть малювати!",
"helpHints": "Комбінації клавіш і допомога"
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -447,10 +447,16 @@
"d9480f": ""
},
"welcomeScreen": {
"data": "",
"switchToPlusApp": "",
"menuHints": "",
"toolbarHints": "",
"helpHints": ""
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View File

@ -447,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": "快捷键和帮助"
}
}
}

Some files were not shown because too many files have changed in this diff Show More