Merge remote-tracking branch 'origin/master' into aakansha-create-text-containers-programmatically

This commit is contained in:
Aakansha Doshi 2023-05-12 17:19:45 +05:30
commit acde193a64
137 changed files with 4087 additions and 3653 deletions

View file

@ -12,24 +12,14 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
excalidraw/excalidraw
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v3
with: with:
context: . context: .
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: excalidraw/excalidraw:latest
labels: ${{ steps.meta.outputs.labels }}

View file

@ -19,7 +19,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@dwelle/tunnel-rat": "0.1.1", "@radix-ui/react-tabs": "1.0.2",
"@sentry/browser": "6.2.5", "@sentry/browser": "6.2.5",
"@sentry/integrations": "6.2.5", "@sentry/integrations": "6.2.5",
"@testing-library/jest-dom": "5.16.2", "@testing-library/jest-dom": "5.16.2",
@ -51,7 +51,7 @@
"roughjs": "4.5.2", "roughjs": "4.5.2",
"sass": "1.51.0", "sass": "1.51.0",
"socket.io-client": "2.3.1", "socket.io-client": "2.3.1",
"tunnel-rat": "0.1.0", "tunnel-rat": "0.1.2",
"workbox-background-sync": "^6.5.4", "workbox-background-sync": "^6.5.4",
"workbox-broadcast-update": "^6.5.4", "workbox-broadcast-update": "^6.5.4",
"workbox-cacheable-response": "^6.5.4", "workbox-cacheable-response": "^6.5.4",

View file

@ -58,7 +58,7 @@ export const getDefaultAppState = (): Omit<
fileHandle: null, fileHandle: null,
gridSize: null, gridSize: null,
isBindingEnabled: true, isBindingEnabled: true,
isSidebarDocked: false, defaultSidebarDockedPreference: false,
isLoading: false, isLoading: false,
isResizing: false, isResizing: false,
isRotating: false, isRotating: false,
@ -150,7 +150,11 @@ const APP_STATE_STORAGE_CONF = (<
gridSize: { browser: true, export: true, server: true }, gridSize: { browser: true, export: true, server: true },
height: { browser: false, export: false, server: false }, height: { browser: false, export: false, server: false },
isBindingEnabled: { browser: false, export: false, server: false }, isBindingEnabled: { browser: false, export: false, server: false },
isSidebarDocked: { browser: true, export: false, server: false }, defaultSidebarDockedPreference: {
browser: true,
export: false,
server: false,
},
isLoading: { browser: false, export: false, server: false }, isLoading: { browser: false, export: false, server: false },
isResizing: { browser: false, export: false, server: false }, isResizing: { browser: false, export: false, server: false },
isRotating: { browser: false, export: false, server: false }, isRotating: { browser: false, export: false, server: false },

View file

@ -14,7 +14,7 @@ import {
hasText, hasText,
} from "../scene"; } from "../scene";
import { SHAPES } from "../shapes"; import { SHAPES } from "../shapes";
import { AppState, Zoom } from "../types"; import { UIAppState, Zoom } from "../types";
import { import {
capitalizeString, capitalizeString,
isTransparent, isTransparent,
@ -28,19 +28,20 @@ import { trackEvent } from "../analytics";
import { hasBoundTextElement } from "../element/typeChecks"; import { hasBoundTextElement } from "../element/typeChecks";
import clsx from "clsx"; import clsx from "clsx";
import { actionToggleZenMode } from "../actions"; import { actionToggleZenMode } from "../actions";
import "./Actions.scss";
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
import { import {
shouldAllowVerticalAlign, shouldAllowVerticalAlign,
suppportsHorizontalAlign, suppportsHorizontalAlign,
} from "../element/textElement"; } from "../element/textElement";
import "./Actions.scss";
export const SelectedShapeActions = ({ export const SelectedShapeActions = ({
appState, appState,
elements, elements,
renderAction, renderAction,
}: { }: {
appState: AppState; appState: UIAppState;
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
renderAction: ActionManager["renderAction"]; renderAction: ActionManager["renderAction"];
}) => { }) => {
@ -215,10 +216,10 @@ export const ShapesSwitcher = ({
appState, appState,
}: { }: {
canvas: HTMLCanvasElement | null; canvas: HTMLCanvasElement | null;
activeTool: AppState["activeTool"]; activeTool: UIAppState["activeTool"];
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, UIAppState>["setState"];
onImageAction: (data: { pointerType: PointerType | null }) => void; onImageAction: (data: { pointerType: PointerType | null }) => void;
appState: AppState; appState: UIAppState;
}) => ( }) => (
<> <>
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => { {SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {

View file

@ -211,6 +211,8 @@ import {
PointerDownState, PointerDownState,
SceneData, SceneData,
Device, Device,
SidebarName,
SidebarTabName,
} from "../types"; } from "../types";
import { import {
debounce, debounce,
@ -300,6 +302,9 @@ import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
import { actionWrapTextInContainer } from "../actions/actionBoundText"; import { actionWrapTextInContainer } from "../actions/actionBoundText";
import BraveMeasureTextError from "./BraveMeasureTextError"; import BraveMeasureTextError from "./BraveMeasureTextError";
const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!);
const deviceContextInitialValue = { const deviceContextInitialValue = {
isSmScreen: false, isSmScreen: false,
isMobile: false, isMobile: false,
@ -341,6 +346,8 @@ const ExcalidrawActionManagerContext = React.createContext<ActionManager>(
); );
ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext"; ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext";
export const useApp = () => useContext(AppContext);
export const useAppProps = () => useContext(AppPropsContext);
export const useDevice = () => useContext<Device>(DeviceContext); export const useDevice = () => useContext<Device>(DeviceContext);
export const useExcalidrawContainer = () => export const useExcalidrawContainer = () =>
useContext(ExcalidrawContainerContext); useContext(ExcalidrawContainerContext);
@ -401,7 +408,7 @@ class App extends React.Component<AppProps, AppState> {
private nearestScrollableContainer: HTMLElement | Document | undefined; private nearestScrollableContainer: HTMLElement | Document | undefined;
public library: AppClassProperties["library"]; public library: AppClassProperties["library"];
public libraryItemsFromStorage: LibraryItems | undefined; public libraryItemsFromStorage: LibraryItems | undefined;
private id: string; public id: string;
private history: History; private history: History;
private excalidrawContainerValue: { private excalidrawContainerValue: {
container: HTMLDivElement | null; container: HTMLDivElement | null;
@ -439,7 +446,7 @@ class App extends React.Component<AppProps, AppState> {
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight,
showHyperlinkPopup: false, showHyperlinkPopup: false,
isSidebarDocked: false, defaultSidebarDockedPreference: false,
}; };
this.id = nanoid(); this.id = nanoid();
@ -470,7 +477,7 @@ class App extends React.Component<AppProps, AppState> {
setActiveTool: this.setActiveTool, setActiveTool: this.setActiveTool,
setCursor: this.setCursor, setCursor: this.setCursor,
resetCursor: this.resetCursor, resetCursor: this.resetCursor,
toggleMenu: this.toggleMenu, toggleSidebar: this.toggleSidebar,
} as const; } as const;
if (typeof excalidrawRef === "function") { if (typeof excalidrawRef === "function") {
excalidrawRef(api); excalidrawRef(api);
@ -578,101 +585,91 @@ class App extends React.Component<AppProps, AppState> {
this.props.handleKeyboardGlobally ? undefined : this.onKeyDown this.props.handleKeyboardGlobally ? undefined : this.onKeyDown
} }
> >
<ExcalidrawContainerContext.Provider <AppContext.Provider value={this}>
value={this.excalidrawContainerValue} <AppPropsContext.Provider value={this.props}>
> <ExcalidrawContainerContext.Provider
<DeviceContext.Provider value={this.device}> value={this.excalidrawContainerValue}
<ExcalidrawSetAppStateContext.Provider value={this.setAppState}> >
<ExcalidrawAppStateContext.Provider value={this.state}> <DeviceContext.Provider value={this.device}>
<ExcalidrawElementsContext.Provider <ExcalidrawSetAppStateContext.Provider value={this.setAppState}>
value={this.scene.getNonDeletedElements()} <ExcalidrawAppStateContext.Provider value={this.state}>
> <ExcalidrawElementsContext.Provider
<ExcalidrawActionManagerContext.Provider value={this.scene.getNonDeletedElements()}
value={this.actionManager}
>
<LayerUI
canvas={this.canvas}
appState={this.state}
files={this.files}
setAppState={this.setAppState}
actionManager={this.actionManager}
elements={this.scene.getNonDeletedElements()}
onLockToggle={this.toggleLock}
onPenModeToggle={this.togglePenMode}
onHandToolToggle={this.onHandToolToggle}
onInsertElements={(elements) =>
this.addElementsFromPasteOrLibrary({
elements,
position: "center",
files: null,
})
}
langCode={getLanguage().code}
renderTopRightUI={renderTopRightUI}
renderCustomStats={renderCustomStats}
renderCustomSidebar={this.props.renderSidebar}
showExitZenModeBtn={
typeof this.props?.zenModeEnabled === "undefined" &&
this.state.zenModeEnabled
}
libraryReturnUrl={this.props.libraryReturnUrl}
UIOptions={this.props.UIOptions}
focusContainer={this.focusContainer}
library={this.library}
id={this.id}
onImageAction={this.onImageAction}
renderWelcomeScreen={
!this.state.isLoading &&
this.state.showWelcomeScreen &&
this.state.activeTool.type === "selection" &&
!this.scene.getElementsIncludingDeleted().length
}
> >
{this.props.children} <ExcalidrawActionManagerContext.Provider
</LayerUI> value={this.actionManager}
<div className="excalidraw-textEditorContainer" /> >
<div className="excalidraw-contextMenuContainer" /> <LayerUI
{selectedElement.length === 1 && canvas={this.canvas}
!this.state.contextMenu && appState={this.state}
this.state.showHyperlinkPopup && ( files={this.files}
<Hyperlink
key={selectedElement[0].id}
element={selectedElement[0]}
setAppState={this.setAppState} setAppState={this.setAppState}
onLinkOpen={this.props.onLinkOpen} actionManager={this.actionManager}
/> elements={this.scene.getNonDeletedElements()}
)} onLockToggle={this.toggleLock}
{this.state.toast !== null && ( onPenModeToggle={this.togglePenMode}
<Toast onHandToolToggle={this.onHandToolToggle}
message={this.state.toast.message} langCode={getLanguage().code}
onClose={() => this.setToast(null)} renderTopRightUI={renderTopRightUI}
duration={this.state.toast.duration} renderCustomStats={renderCustomStats}
closable={this.state.toast.closable} showExitZenModeBtn={
/> typeof this.props?.zenModeEnabled === "undefined" &&
)} this.state.zenModeEnabled
{this.state.contextMenu && ( }
<ContextMenu UIOptions={this.props.UIOptions}
items={this.state.contextMenu.items} onImageAction={this.onImageAction}
top={this.state.contextMenu.top} renderWelcomeScreen={
left={this.state.contextMenu.left} !this.state.isLoading &&
actionManager={this.actionManager} this.state.showWelcomeScreen &&
/> this.state.activeTool.type === "selection" &&
)} !this.scene.getElementsIncludingDeleted().length
<main>{this.renderCanvas()}</main> }
</ExcalidrawActionManagerContext.Provider> >
</ExcalidrawElementsContext.Provider>{" "} {this.props.children}
</ExcalidrawAppStateContext.Provider> </LayerUI>
</ExcalidrawSetAppStateContext.Provider> <div className="excalidraw-textEditorContainer" />
</DeviceContext.Provider> <div className="excalidraw-contextMenuContainer" />
</ExcalidrawContainerContext.Provider> {selectedElement.length === 1 &&
!this.state.contextMenu &&
this.state.showHyperlinkPopup && (
<Hyperlink
key={selectedElement[0].id}
element={selectedElement[0]}
setAppState={this.setAppState}
onLinkOpen={this.props.onLinkOpen}
/>
)}
{this.state.toast !== null && (
<Toast
message={this.state.toast.message}
onClose={() => this.setToast(null)}
duration={this.state.toast.duration}
closable={this.state.toast.closable}
/>
)}
{this.state.contextMenu && (
<ContextMenu
items={this.state.contextMenu.items}
top={this.state.contextMenu.top}
left={this.state.contextMenu.left}
actionManager={this.actionManager}
/>
)}
<main>{this.renderCanvas()}</main>
</ExcalidrawActionManagerContext.Provider>
</ExcalidrawElementsContext.Provider>{" "}
</ExcalidrawAppStateContext.Provider>
</ExcalidrawSetAppStateContext.Provider>
</DeviceContext.Provider>
</ExcalidrawContainerContext.Provider>
</AppPropsContext.Provider>
</AppContext.Provider>
</div> </div>
); );
} }
public focusContainer: AppClassProperties["focusContainer"] = () => { public focusContainer: AppClassProperties["focusContainer"] = () => {
if (this.props.autoFocus) { this.excalidrawContainerRef.current?.focus();
this.excalidrawContainerRef.current?.focus();
}
}; };
public getSceneElementsIncludingDeleted = () => { public getSceneElementsIncludingDeleted = () => {
@ -683,6 +680,14 @@ class App extends React.Component<AppProps, AppState> {
return this.scene.getNonDeletedElements(); return this.scene.getNonDeletedElements();
}; };
public onInsertElements = (elements: readonly ExcalidrawElement[]) => {
this.addElementsFromPasteOrLibrary({
elements,
position: "center",
files: null,
});
};
private syncActionResult = withBatchedUpdates( private syncActionResult = withBatchedUpdates(
(actionResult: ActionResult) => { (actionResult: ActionResult) => {
if (this.unmounted || actionResult === false) { if (this.unmounted || actionResult === false) {
@ -952,7 +957,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.addCallback(this.onSceneUpdated); this.scene.addCallback(this.onSceneUpdated);
this.addEventListeners(); this.addEventListeners();
if (this.excalidrawContainerRef.current) { if (this.props.autoFocus && this.excalidrawContainerRef.current) {
this.focusContainer(); this.focusContainer();
} }
@ -1680,7 +1685,7 @@ class App extends React.Component<AppProps, AppState> {
openSidebar: openSidebar:
this.state.openSidebar && this.state.openSidebar &&
this.device.canDeviceFitSidebar && this.device.canDeviceFitSidebar &&
this.state.isSidebarDocked this.state.defaultSidebarDockedPreference
? this.state.openSidebar ? this.state.openSidebar
: null, : null,
selectedElementIds: newElements.reduce( selectedElementIds: newElements.reduce(
@ -2020,30 +2025,24 @@ class App extends React.Component<AppProps, AppState> {
/** /**
* @returns whether the menu was toggled on or off * @returns whether the menu was toggled on or off
*/ */
public toggleMenu = ( public toggleSidebar = ({
type: "library" | "customSidebar", name,
force?: boolean, tab,
): boolean => { force,
if (type === "customSidebar" && !this.props.renderSidebar) { }: {
console.warn( name: SidebarName;
`attempting to toggle "customSidebar", but no "props.renderSidebar" is defined`, tab?: SidebarTabName;
); force?: boolean;
return false; }): boolean => {
let nextName;
if (force === undefined) {
nextName = this.state.openSidebar?.name === name ? null : name;
} else {
nextName = force ? name : null;
} }
this.setState({ openSidebar: nextName ? { name: nextName, tab } : null });
if (type === "library" || type === "customSidebar") { return !!nextName;
let nextValue;
if (force === undefined) {
nextValue = this.state.openSidebar === type ? null : type;
} else {
nextValue = force ? type : null;
}
this.setState({ openSidebar: nextValue });
return !!nextValue;
}
return false;
}; };
private updateCurrentCursorPosition = withBatchedUpdates( private updateCurrentCursorPosition = withBatchedUpdates(

View file

@ -1,39 +1,40 @@
import { t } from "../i18n"; import Trans from "./Trans";
const BraveMeasureTextError = () => { const BraveMeasureTextError = () => {
return ( return (
<div data-testid="brave-measure-text-error"> <div data-testid="brave-measure-text-error">
<p> <p>
{t("errors.brave_measure_text_error.start")} &nbsp; <Trans
<span style={{ fontWeight: 600 }}> i18nKey="errors.brave_measure_text_error.line1"
{t("errors.brave_measure_text_error.aggressive_block_fingerprint")} bold={(el) => <span style={{ fontWeight: 600 }}>{el}</span>}
</span>{" "} />
{t("errors.brave_measure_text_error.setting_enabled")}.
<br />
<br />
{t("errors.brave_measure_text_error.break")}{" "}
<span style={{ fontWeight: 600 }}>
{t("errors.brave_measure_text_error.text_elements")}
</span>{" "}
{t("errors.brave_measure_text_error.in_your_drawings")}.
</p> </p>
<p> <p>
{t("errors.brave_measure_text_error.strongly_recommend")}{" "} <Trans
<a href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser"> i18nKey="errors.brave_measure_text_error.line2"
{" "} bold={(el) => <span style={{ fontWeight: 600 }}>{el}</span>}
{t("errors.brave_measure_text_error.steps")} />
</a>{" "}
{t("errors.brave_measure_text_error.how")}.
</p> </p>
<p> <p>
{t("errors.brave_measure_text_error.disable_setting")}{" "} <Trans
<a href="https://github.com/excalidraw/excalidraw/issues/new"> i18nKey="errors.brave_measure_text_error.line3"
{t("errors.brave_measure_text_error.issue")} link={(el) => (
</a>{" "} <a href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser">
{t("errors.brave_measure_text_error.write")}{" "} {el}
<a href="https://discord.gg/UexuTaE"> </a>
{t("errors.brave_measure_text_error.discord")} )}
</a> />
. </p>
<p>
<Trans
i18nKey="errors.brave_measure_text_error.line4"
issueLink={(el) => (
<a href="https://github.com/excalidraw/excalidraw/issues/new">
{el}
</a>
)}
discordLink={(el) => <a href="https://discord.gg/UexuTaE">{el}.</a>}
/>
</p> </p>
</div> </div>
); );

View file

@ -1,8 +1,12 @@
import clsx from "clsx";
import { composeEventHandlers } from "../utils";
import "./Button.scss"; import "./Button.scss";
interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> { interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
type?: "button" | "submit" | "reset"; type?: "button" | "submit" | "reset";
onSelect: () => any; onSelect: () => any;
/** whether button is in active state */
selected?: boolean;
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
} }
@ -15,18 +19,18 @@ interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
export const Button = ({ export const Button = ({
type = "button", type = "button",
onSelect, onSelect,
selected,
children, children,
className = "", className = "",
...rest ...rest
}: ButtonProps) => { }: ButtonProps) => {
return ( return (
<button <button
onClick={(event) => { onClick={composeEventHandlers(rest.onClick, (event) => {
onSelect(); onSelect();
rest.onClick?.(event); })}
}}
type={type} type={type}
className={`excalidraw-button ${className}`} className={clsx("excalidraw-button", className, { selected })}
{...rest} {...rest}
> >
{children} {children}

View file

@ -4,8 +4,8 @@ import { Dialog, DialogProps } from "./Dialog";
import "./ConfirmDialog.scss"; import "./ConfirmDialog.scss";
import DialogActionButton from "./DialogActionButton"; import DialogActionButton from "./DialogActionButton";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent"; import { isLibraryMenuOpenAtom } from "./LibraryMenu";
import { useExcalidrawSetAppState } from "./App"; import { useExcalidrawContainer, useExcalidrawSetAppState } from "./App";
import { jotaiScope } from "../jotai"; import { jotaiScope } from "../jotai";
interface Props extends Omit<DialogProps, "onCloseRequest"> { interface Props extends Omit<DialogProps, "onCloseRequest"> {
@ -26,6 +26,7 @@ const ConfirmDialog = (props: Props) => {
} = props; } = props;
const setAppState = useExcalidrawSetAppState(); const setAppState = useExcalidrawSetAppState();
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope); const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope);
const { container } = useExcalidrawContainer();
return ( return (
<Dialog <Dialog
@ -42,6 +43,7 @@ const ConfirmDialog = (props: Props) => {
setAppState({ openMenu: null }); setAppState({ openMenu: null });
setIsLibraryMenuOpen(false); setIsLibraryMenuOpen(false);
onCancel(); onCancel();
container?.focus();
}} }}
/> />
<DialogActionButton <DialogActionButton
@ -50,6 +52,7 @@ const ConfirmDialog = (props: Props) => {
setAppState({ openMenu: null }); setAppState({ openMenu: null });
setIsLibraryMenuOpen(false); setIsLibraryMenuOpen(false);
onConfirm(); onConfirm();
container?.focus();
}} }}
actionType="danger" actionType="danger"
/> />

View file

@ -0,0 +1,144 @@
import React from "react";
import { DEFAULT_SIDEBAR } from "../constants";
import { DefaultSidebar } from "../packages/excalidraw/index";
import {
fireEvent,
waitFor,
withExcalidrawDimensions,
} from "../tests/test-utils";
import {
assertExcalidrawWithSidebar,
assertSidebarDockButton,
} from "./Sidebar/Sidebar.test";
const { h } = window;
describe("DefaultSidebar", () => {
it("when `docked={undefined}` & `onDock={undefined}`, should allow docking", async () => {
await assertExcalidrawWithSidebar(
<DefaultSidebar />,
DEFAULT_SIDEBAR.name,
async () => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
const { dockButton } = await assertSidebarDockButton(true);
fireEvent.click(dockButton);
await waitFor(() => {
expect(h.state.defaultSidebarDockedPreference).toBe(true);
expect(dockButton).toHaveClass("selected");
});
fireEvent.click(dockButton);
await waitFor(() => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
expect(dockButton).not.toHaveClass("selected");
});
},
);
});
it("when `docked={undefined}` & `onDock`, should allow docking", async () => {
await assertExcalidrawWithSidebar(
<DefaultSidebar onDock={() => {}} />,
DEFAULT_SIDEBAR.name,
async () => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
const { dockButton } = await assertSidebarDockButton(true);
fireEvent.click(dockButton);
await waitFor(() => {
expect(h.state.defaultSidebarDockedPreference).toBe(true);
expect(dockButton).toHaveClass("selected");
});
fireEvent.click(dockButton);
await waitFor(() => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
expect(dockButton).not.toHaveClass("selected");
});
},
);
});
it("when `docked={true}` & `onDock`, should allow docking", async () => {
await assertExcalidrawWithSidebar(
<DefaultSidebar onDock={() => {}} />,
DEFAULT_SIDEBAR.name,
async () => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
const { dockButton } = await assertSidebarDockButton(true);
fireEvent.click(dockButton);
await waitFor(() => {
expect(h.state.defaultSidebarDockedPreference).toBe(true);
expect(dockButton).toHaveClass("selected");
});
fireEvent.click(dockButton);
await waitFor(() => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
expect(dockButton).not.toHaveClass("selected");
});
},
);
});
it("when `onDock={false}`, should disable docking", async () => {
await assertExcalidrawWithSidebar(
<DefaultSidebar onDock={false} />,
DEFAULT_SIDEBAR.name,
async () => {
await withExcalidrawDimensions(
{ width: 1920, height: 1080 },
async () => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
await assertSidebarDockButton(false);
},
);
},
);
});
it("when `docked={true}` & `onDock={false}`, should force-dock sidebar", async () => {
await assertExcalidrawWithSidebar(
<DefaultSidebar docked onDock={false} />,
DEFAULT_SIDEBAR.name,
async () => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
const { sidebar } = await assertSidebarDockButton(false);
expect(sidebar).toHaveClass("sidebar--docked");
},
);
});
it("when `docked={true}` & `onDock={undefined}`, should force-dock sidebar", async () => {
await assertExcalidrawWithSidebar(
<DefaultSidebar docked />,
DEFAULT_SIDEBAR.name,
async () => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
const { sidebar } = await assertSidebarDockButton(false);
expect(sidebar).toHaveClass("sidebar--docked");
},
);
});
it("when `docked={false}` & `onDock={undefined}`, should force-undock sidebar", async () => {
await assertExcalidrawWithSidebar(
<DefaultSidebar docked={false} />,
DEFAULT_SIDEBAR.name,
async () => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
const { sidebar } = await assertSidebarDockButton(false);
expect(sidebar).not.toHaveClass("sidebar--docked");
},
);
});
});

View file

@ -0,0 +1,118 @@
import clsx from "clsx";
import { DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_TAB } from "../constants";
import { useTunnels } from "../context/tunnels";
import { useUIAppState } from "../context/ui-appState";
import { t } from "../i18n";
import { MarkOptional, Merge } from "../utility-types";
import { composeEventHandlers } from "../utils";
import { useExcalidrawSetAppState } from "./App";
import { withInternalFallback } from "./hoc/withInternalFallback";
import { LibraryMenu } from "./LibraryMenu";
import { SidebarProps, SidebarTriggerProps } from "./Sidebar/common";
import { Sidebar } from "./Sidebar/Sidebar";
const DefaultSidebarTrigger = withInternalFallback(
"DefaultSidebarTrigger",
(
props: Omit<SidebarTriggerProps, "name"> &
React.HTMLAttributes<HTMLDivElement>,
) => {
const { DefaultSidebarTriggerTunnel } = useTunnels();
return (
<DefaultSidebarTriggerTunnel.In>
<Sidebar.Trigger
{...props}
className="default-sidebar-trigger"
name={DEFAULT_SIDEBAR.name}
/>
</DefaultSidebarTriggerTunnel.In>
);
},
);
DefaultSidebarTrigger.displayName = "DefaultSidebarTrigger";
const DefaultTabTriggers = ({
children,
...rest
}: { children: React.ReactNode } & React.HTMLAttributes<HTMLDivElement>) => {
const { DefaultSidebarTabTriggersTunnel } = useTunnels();
return (
<DefaultSidebarTabTriggersTunnel.In>
<Sidebar.TabTriggers {...rest}>{children}</Sidebar.TabTriggers>
</DefaultSidebarTabTriggersTunnel.In>
);
};
DefaultTabTriggers.displayName = "DefaultTabTriggers";
export const DefaultSidebar = Object.assign(
withInternalFallback(
"DefaultSidebar",
({
children,
className,
onDock,
docked,
...rest
}: Merge<
MarkOptional<Omit<SidebarProps, "name">, "children">,
{
/** pass `false` to disable docking */
onDock?: SidebarProps["onDock"] | false;
}
>) => {
const appState = useUIAppState();
const setAppState = useExcalidrawSetAppState();
const { DefaultSidebarTabTriggersTunnel } = useTunnels();
return (
<Sidebar
{...rest}
name="default"
key="default"
className={clsx("default-sidebar", className)}
docked={docked ?? appState.defaultSidebarDockedPreference}
onDock={
// `onDock=false` disables docking.
// if `docked` passed, but no onDock passed, disable manual docking.
onDock === false || (!onDock && docked != null)
? undefined
: // compose to allow the host app to listen on default behavior
composeEventHandlers(onDock, (docked) => {
setAppState({ defaultSidebarDockedPreference: docked });
})
}
>
<Sidebar.Tabs>
<Sidebar.Header>
{rest.__fallback && (
<div
style={{
color: "var(--color-primary)",
fontSize: "1.2em",
fontWeight: "bold",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
paddingRight: "1em",
}}
>
{t("toolBar.library")}
</div>
)}
<DefaultSidebarTabTriggersTunnel.Out />
</Sidebar.Header>
<Sidebar.Tab tab={LIBRARY_SIDEBAR_TAB}>
<LibraryMenu />
</Sidebar.Tab>
{children}
</Sidebar.Tabs>
</Sidebar>
);
},
),
{
Trigger: DefaultSidebarTrigger,
TabTriggers: DefaultTabTriggers,
},
);

View file

@ -15,7 +15,7 @@ import { Modal } from "./Modal";
import { AppState } from "../types"; import { AppState } from "../types";
import { queryFocusableElements } from "../utils"; import { queryFocusableElements } from "../utils";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent"; import { isLibraryMenuOpenAtom } from "./LibraryMenu";
import { jotaiScope } from "../jotai"; import { jotaiScope } from "../jotai";
export interface DialogProps { export interface DialogProps {

View file

@ -1,9 +1,7 @@
import { t } from "../i18n"; import { t } from "../i18n";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { getSelectedElements } from "../scene"; import { getSelectedElements } from "../scene";
import { Device, UIAppState } from "../types";
import "./HintViewer.scss";
import { AppState, Device } from "../types";
import { import {
isImageElement, isImageElement,
isLinearElement, isLinearElement,
@ -13,8 +11,10 @@ import {
import { getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
import { isEraserActive } from "../appState"; import { isEraserActive } from "../appState";
import "./HintViewer.scss";
interface HintViewerProps { interface HintViewerProps {
appState: AppState; appState: UIAppState;
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
isMobile: boolean; isMobile: boolean;
device: Device; device: Device;
@ -29,7 +29,7 @@ const getHints = ({
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState; const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
const multiMode = appState.multiElement !== null; const multiMode = appState.multiElement !== null;
if (appState.openSidebar === "library" && !device.canDeviceFitSidebar) { if (appState.openSidebar && !device.canDeviceFitSidebar) {
return null; return null;
} }

View file

@ -4,11 +4,10 @@ import { canvasToBlob } from "../data/blob";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { getSelectedElements, isSomeElementSelected } from "../scene"; import { getSelectedElements, isSomeElementSelected } from "../scene";
import { AppState, BinaryFiles } from "../types"; import { BinaryFiles, UIAppState } from "../types";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import { clipboard } from "./icons"; import { clipboard } from "./icons";
import Stack from "./Stack"; import Stack from "./Stack";
import "./ExportDialog.scss";
import OpenColor from "open-color"; import OpenColor from "open-color";
import { CheckboxItem } from "./CheckboxItem"; import { CheckboxItem } from "./CheckboxItem";
import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants"; import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants";
@ -16,6 +15,8 @@ import { nativeFileSystemSupported } from "../data/filesystem";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { exportToCanvas } from "../packages/utils"; import { exportToCanvas } from "../packages/utils";
import "./ExportDialog.scss";
const supportsContextFilters = const supportsContextFilters =
"filter" in document.createElement("canvas").getContext("2d")!; "filter" in document.createElement("canvas").getContext("2d")!;
@ -70,7 +71,7 @@ const ImageExportModal = ({
onExportToSvg, onExportToSvg,
onExportToClipboard, onExportToClipboard,
}: { }: {
appState: AppState; appState: UIAppState;
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles; files: BinaryFiles;
exportPadding?: number; exportPadding?: number;
@ -216,8 +217,8 @@ export const ImageExportDialog = ({
onExportToSvg, onExportToSvg,
onExportToClipboard, onExportToClipboard,
}: { }: {
appState: AppState; appState: UIAppState;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, UIAppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles; files: BinaryFiles;
exportPadding?: number; exportPadding?: number;

View file

@ -2,7 +2,7 @@ import React from "react";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { AppState, ExportOpts, BinaryFiles } from "../types"; import { ExportOpts, BinaryFiles, UIAppState } from "../types";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import { exportToFileIcon, LinkIcon } from "./icons"; import { exportToFileIcon, LinkIcon } from "./icons";
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
@ -28,7 +28,7 @@ const JSONExportModal = ({
exportOpts, exportOpts,
canvas, canvas,
}: { }: {
appState: AppState; appState: UIAppState;
files: BinaryFiles; files: BinaryFiles;
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
actionManager: ActionManager; actionManager: ActionManager;
@ -96,12 +96,12 @@ export const JSONExportDialog = ({
setAppState, setAppState,
}: { }: {
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
appState: AppState; appState: UIAppState;
files: BinaryFiles; files: BinaryFiles;
actionManager: ActionManager; actionManager: ActionManager;
exportOpts: ExportOpts; exportOpts: ExportOpts;
canvas: HTMLCanvasElement | null; canvas: HTMLCanvasElement | null;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, UIAppState>["setState"];
}) => { }) => {
const handleClose = React.useCallback(() => { const handleClose = React.useCallback(() => {
setAppState({ openDialog: null }); setAppState({ openDialog: null });

View file

@ -1,15 +1,21 @@
import clsx from "clsx"; import clsx from "clsx";
import React from "react"; import React from "react";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { CLASSES, LIBRARY_SIDEBAR_WIDTH } from "../constants"; import { CLASSES, DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_WIDTH } from "../constants";
import { exportCanvas } from "../data"; import { exportCanvas } from "../data";
import { isTextElement, showSelectedShapeActions } from "../element"; import { isTextElement, showSelectedShapeActions } from "../element";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { Language, t } from "../i18n"; import { Language, t } from "../i18n";
import { calculateScrollCenter } from "../scene"; import { calculateScrollCenter } from "../scene";
import { ExportType } from "../scene/types"; import { ExportType } from "../scene/types";
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types"; import {
import { isShallowEqual, muteFSAbortError } from "../utils"; AppProps,
AppState,
ExcalidrawProps,
BinaryFiles,
UIAppState,
} from "../types";
import { capitalizeString, isShallowEqual, muteFSAbortError } from "../utils";
import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
import { ErrorDialog } from "./ErrorDialog"; import { ErrorDialog } from "./ErrorDialog";
import { ExportCB, ImageExportDialog } from "./ImageExportDialog"; import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
@ -24,32 +30,32 @@ import { Section } from "./Section";
import { HelpDialog } from "./HelpDialog"; import { HelpDialog } from "./HelpDialog";
import Stack from "./Stack"; import Stack from "./Stack";
import { UserList } from "./UserList"; import { UserList } from "./UserList";
import Library from "../data/library";
import { JSONExportDialog } from "./JSONExportDialog"; import { JSONExportDialog } from "./JSONExportDialog";
import { LibraryButton } from "./LibraryButton";
import { isImageFileHandle } from "../data/blob"; import { isImageFileHandle } from "../data/blob";
import { LibraryMenu } from "./LibraryMenu";
import "./LayerUI.scss";
import "./Toolbar.scss";
import { PenModeButton } from "./PenModeButton"; import { PenModeButton } from "./PenModeButton";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { useDevice } from "../components/App"; import { useDevice } from "../components/App";
import { Stats } from "./Stats"; import { Stats } from "./Stats";
import { actionToggleStats } from "../actions/actionToggleStats"; import { actionToggleStats } from "../actions/actionToggleStats";
import Footer from "./footer/Footer"; import Footer from "./footer/Footer";
import { hostSidebarCountersAtom } from "./Sidebar/Sidebar"; import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
import { jotaiScope } from "../jotai"; import { jotaiScope } from "../jotai";
import { Provider, useAtom } from "jotai"; import { Provider, useAtomValue } from "jotai";
import MainMenu from "./main-menu/MainMenu"; import MainMenu from "./main-menu/MainMenu";
import { ActiveConfirmDialog } from "./ActiveConfirmDialog"; import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
import { HandButton } from "./HandButton"; import { HandButton } from "./HandButton";
import { isHandToolActive } from "../appState"; import { isHandToolActive } from "../appState";
import { TunnelsContext, useInitializeTunnels } from "./context/tunnels"; import { TunnelsContext, useInitializeTunnels } from "../context/tunnels";
import { LibraryIcon } from "./icons";
import { UIAppStateContext } from "../context/ui-appState";
import { DefaultSidebar } from "./DefaultSidebar";
import "./LayerUI.scss";
import "./Toolbar.scss";
interface LayerUIProps { interface LayerUIProps {
actionManager: ActionManager; actionManager: ActionManager;
appState: AppState; appState: UIAppState;
files: BinaryFiles; files: BinaryFiles;
canvas: HTMLCanvasElement | null; canvas: HTMLCanvasElement | null;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
@ -57,17 +63,11 @@ interface LayerUIProps {
onLockToggle: () => void; onLockToggle: () => void;
onHandToolToggle: () => void; onHandToolToggle: () => void;
onPenModeToggle: () => void; onPenModeToggle: () => void;
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
showExitZenModeBtn: boolean; showExitZenModeBtn: boolean;
langCode: Language["code"]; langCode: Language["code"];
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"]; renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
renderCustomStats?: ExcalidrawProps["renderCustomStats"]; renderCustomStats?: ExcalidrawProps["renderCustomStats"];
renderCustomSidebar?: ExcalidrawProps["renderSidebar"];
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
UIOptions: AppProps["UIOptions"]; UIOptions: AppProps["UIOptions"];
focusContainer: () => void;
library: Library;
id: string;
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void; onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
renderWelcomeScreen: boolean; renderWelcomeScreen: boolean;
children?: React.ReactNode; children?: React.ReactNode;
@ -109,16 +109,10 @@ const LayerUI = ({
onLockToggle, onLockToggle,
onHandToolToggle, onHandToolToggle,
onPenModeToggle, onPenModeToggle,
onInsertElements,
showExitZenModeBtn, showExitZenModeBtn,
renderTopRightUI, renderTopRightUI,
renderCustomStats, renderCustomStats,
renderCustomSidebar,
libraryReturnUrl,
UIOptions, UIOptions,
focusContainer,
library,
id,
onImageAction, onImageAction,
renderWelcomeScreen, renderWelcomeScreen,
children, children,
@ -156,7 +150,8 @@ const LayerUI = ({
const fileHandle = await exportCanvas( const fileHandle = await exportCanvas(
type, type,
exportedElements, exportedElements,
appState, // FIXME once we split UI canvas from element canvas
appState as AppState,
files, files,
{ {
exportBackground: appState.exportBackground, exportBackground: appState.exportBackground,
@ -197,8 +192,8 @@ const LayerUI = ({
<div style={{ position: "relative" }}> <div style={{ position: "relative" }}>
{/* wrapping to Fragment stops React from occasionally complaining {/* wrapping to Fragment stops React from occasionally complaining
about identical Keys */} about identical Keys */}
<tunnels.mainMenuTunnel.Out /> <tunnels.MainMenuTunnel.Out />
{renderWelcomeScreen && <tunnels.welcomeScreenMenuHintTunnel.Out />} {renderWelcomeScreen && <tunnels.WelcomeScreenMenuHintTunnel.Out />}
</div> </div>
); );
@ -250,7 +245,7 @@ const LayerUI = ({
{(heading: React.ReactNode) => ( {(heading: React.ReactNode) => (
<div style={{ position: "relative" }}> <div style={{ position: "relative" }}>
{renderWelcomeScreen && ( {renderWelcomeScreen && (
<tunnels.welcomeScreenToolbarHintTunnel.Out /> <tunnels.WelcomeScreenToolbarHintTunnel.Out />
)} )}
<Stack.Col gap={4} align="start"> <Stack.Col gap={4} align="start">
<Stack.Row <Stack.Row
@ -324,9 +319,12 @@ const LayerUI = ({
> >
<UserList collaborators={appState.collaborators} /> <UserList collaborators={appState.collaborators} />
{renderTopRightUI?.(device.isMobile, appState)} {renderTopRightUI?.(device.isMobile, appState)}
{!appState.viewModeEnabled && ( {!appState.viewModeEnabled &&
<LibraryButton appState={appState} setAppState={setAppState} /> // hide button when sidebar docked
)} (!isSidebarDocked ||
appState.openSidebar?.name !== DEFAULT_SIDEBAR.name) && (
<tunnels.DefaultSidebarTriggerTunnel.Out />
)}
</div> </div>
</div> </div>
</FixedSideContainer> </FixedSideContainer>
@ -334,21 +332,21 @@ const LayerUI = ({
}; };
const renderSidebars = () => { const renderSidebars = () => {
return appState.openSidebar === "customSidebar" ? ( return (
renderCustomSidebar?.() || null <DefaultSidebar
) : appState.openSidebar === "library" ? ( __fallback
<LibraryMenu onDock={(docked) => {
appState={appState} trackEvent(
onInsertElements={onInsertElements} "sidebar",
libraryReturnUrl={libraryReturnUrl} `toggleDock (${docked ? "dock" : "undock"})`,
focusContainer={focusContainer} `(${device.isMobile ? "mobile" : "desktop"})`,
library={library} );
id={id} }}
/> />
) : null; );
}; };
const [hostSidebarCounters] = useAtom(hostSidebarCountersAtom, jotaiScope); const isSidebarDocked = useAtomValue(isSidebarDockedAtom, jotaiScope);
const layerUIJSX = ( const layerUIJSX = (
<> <>
@ -358,8 +356,25 @@ const LayerUI = ({
{children} {children}
{/* render component fallbacks. Can be rendered anywhere as they'll be {/* render component fallbacks. Can be rendered anywhere as they'll be
tunneled away. We only render tunneled components that actually tunneled away. We only render tunneled components that actually
have defaults when host do not render anything. */} have defaults when host do not render anything. */}
<DefaultMainMenu UIOptions={UIOptions} /> <DefaultMainMenu UIOptions={UIOptions} />
<DefaultSidebar.Trigger
__fallback
icon={LibraryIcon}
title={capitalizeString(t("toolBar.library"))}
onToggle={(open) => {
if (open) {
trackEvent(
"sidebar",
`${DEFAULT_SIDEBAR.name} (open)`,
`button (${device.isMobile ? "mobile" : "desktop"})`,
);
}
}}
tab={DEFAULT_SIDEBAR.defaultTab}
>
{t("toolBar.library")}
</DefaultSidebar.Trigger>
{/* ------------------------------------------------------------------ */} {/* ------------------------------------------------------------------ */}
{appState.isLoading && <LoadingMessage delay={250} />} {appState.isLoading && <LoadingMessage delay={250} />}
@ -382,7 +397,6 @@ const LayerUI = ({
<PasteChartDialog <PasteChartDialog
setAppState={setAppState} setAppState={setAppState}
appState={appState} appState={appState}
onInsertChart={onInsertElements}
onClose={() => onClose={() =>
setAppState({ setAppState({
pasteDialog: { shown: false, data: null }, pasteDialog: { shown: false, data: null },
@ -410,7 +424,6 @@ const LayerUI = ({
renderWelcomeScreen={renderWelcomeScreen} renderWelcomeScreen={renderWelcomeScreen}
/> />
)} )}
{!device.isMobile && ( {!device.isMobile && (
<> <>
<div <div
@ -422,15 +435,14 @@ const LayerUI = ({
!isTextElement(appState.editingElement)), !isTextElement(appState.editingElement)),
})} })}
style={ style={
((appState.openSidebar === "library" && appState.openSidebar &&
appState.isSidebarDocked) || isSidebarDocked &&
hostSidebarCounters.docked) &&
device.canDeviceFitSidebar device.canDeviceFitSidebar
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` } ? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
: {} : {}
} }
> >
{renderWelcomeScreen && <tunnels.welcomeScreenCenterTunnel.Out />} {renderWelcomeScreen && <tunnels.WelcomeScreenCenterTunnel.Out />}
{renderFixedSideContainer()} {renderFixedSideContainer()}
<Footer <Footer
appState={appState} appState={appState}
@ -453,9 +465,9 @@ const LayerUI = ({
<button <button
className="scroll-back-to-content" className="scroll-back-to-content"
onClick={() => { onClick={() => {
setAppState({ setAppState((appState) => ({
...calculateScrollCenter(elements, appState, canvas), ...calculateScrollCenter(elements, appState, canvas),
}); }));
}} }}
> >
{t("buttons.scrollBackToContent")} {t("buttons.scrollBackToContent")}
@ -469,19 +481,25 @@ const LayerUI = ({
); );
return ( return (
<Provider scope={tunnels.jotaiScope}> <UIAppStateContext.Provider value={appState}>
<TunnelsContext.Provider value={tunnels}> <Provider scope={tunnels.jotaiScope}>
{layerUIJSX} <TunnelsContext.Provider value={tunnels}>
</TunnelsContext.Provider> {layerUIJSX}
</Provider> </TunnelsContext.Provider>
</Provider>
</UIAppStateContext.Provider>
); );
}; };
const stripIrrelevantAppStateProps = ( const stripIrrelevantAppStateProps = (appState: AppState): UIAppState => {
appState: AppState, const {
): Partial<AppState> => { suggestedBindings,
const { suggestedBindings, startBoundElement, cursorButton, ...ret } = startBoundElement,
appState; cursorButton,
scrollX,
scrollY,
...ret
} = appState;
return ret; return ret;
}; };
@ -491,24 +509,19 @@ const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => {
return false; return false;
} }
const { const { canvas: _prevCanvas, appState: prevAppState, ...prev } = prevProps;
canvas: _prevCanvas, const { canvas: _nextCanvas, appState: nextAppState, ...next } = nextProps;
// not stable, but shouldn't matter in our case
onInsertElements: _prevOnInsertElements,
appState: prevAppState,
...prev
} = prevProps;
const {
canvas: _nextCanvas,
onInsertElements: _nextOnInsertElements,
appState: nextAppState,
...next
} = nextProps;
return ( return (
isShallowEqual( isShallowEqual(
stripIrrelevantAppStateProps(prevAppState), // asserting AppState because we're being passed the whole AppState
stripIrrelevantAppStateProps(nextAppState), // but resolve to only the UI-relevant props
stripIrrelevantAppStateProps(prevAppState as AppState),
stripIrrelevantAppStateProps(nextAppState as AppState),
{
selectedElementIds: isShallowEqual,
selectedGroupIds: isShallowEqual,
},
) && isShallowEqual(prev, next) ) && isShallowEqual(prev, next)
); );
}; };

View file

@ -1,32 +0,0 @@
@import "../css/variables.module";
.library-button {
@include outlineButtonStyles;
background-color: var(--island-bg-color);
width: auto;
height: var(--lg-button-size);
display: flex;
align-items: center;
gap: 0.5rem;
line-height: 0;
font-size: 0.75rem;
letter-spacing: 0.4px;
svg {
width: var(--lg-icon-size);
height: var(--lg-icon-size);
}
&__label {
display: none;
@media screen and (min-width: 1024px) {
display: block;
}
}
}

View file

@ -1,57 +0,0 @@
import React from "react";
import { t } from "../i18n";
import { AppState } from "../types";
import { capitalizeString } from "../utils";
import { trackEvent } from "../analytics";
import { useDevice } from "./App";
import "./LibraryButton.scss";
import { LibraryIcon } from "./icons";
export const LibraryButton: React.FC<{
appState: AppState;
setAppState: React.Component<any, AppState>["setState"];
isMobile?: boolean;
}> = ({ appState, setAppState, isMobile }) => {
const device = useDevice();
const showLabel = !isMobile;
// TODO barnabasmolnar/redesign
// not great, toolbar jumps in a jarring manner
if (appState.isSidebarDocked && appState.openSidebar === "library") {
return null;
}
return (
<label title={`${capitalizeString(t("toolBar.library"))}`}>
<input
className="ToolIcon_type_checkbox"
type="checkbox"
name="editor-library"
onChange={(event) => {
document
.querySelector(".layer-ui__wrapper")
?.classList.remove("animate");
const isOpen = event.target.checked;
setAppState({ openSidebar: isOpen ? "library" : null });
// track only openings
if (isOpen) {
trackEvent(
"library",
"toggleLibrary (open)",
`toolbar (${device.isMobile ? "mobile" : "desktop"})`,
);
}
}}
checked={appState.openSidebar === "library"}
aria-label={capitalizeString(t("toolBar.library"))}
aria-keyshortcuts="0"
/>
<div className="library-button">
<div>{LibraryIcon}</div>
{showLabel && (
<div className="library-button__label">{t("toolBar.library")}</div>
)}
</div>
</label>
);
};

View file

@ -1,9 +1,9 @@
@import "open-color/open-color"; @import "open-color/open-color";
.excalidraw { .excalidraw {
.layer-ui__library-sidebar { .library-menu-items-container {
display: flex; height: 100%;
flex-direction: column; width: 100%;
} }
.layer-ui__library { .layer-ui__library {
@ -11,28 +11,6 @@
flex-direction: column; flex-direction: column;
flex: 1 1 auto; flex: 1 1 auto;
.layer-ui__library-header {
display: flex;
align-items: center;
width: 100%;
margin: 2px 0 15px 0;
.Spinner {
margin-right: 1rem;
}
button {
// 2px from the left to account for focus border of left-most button
margin: 0 2px;
}
}
}
.layer-ui__sidebar {
.library-menu-items-container {
height: 100%;
width: 100%;
}
} }
.library-actions-counter { .library-actions-counter {
@ -87,10 +65,17 @@
} }
} }
.library-menu-browse-button { .library-menu-control-buttons {
margin: 1rem auto; display: flex;
align-items: center;
justify-content: center;
gap: 0.625rem;
}
padding: 0.875rem 1rem; .library-menu-browse-button {
flex: 1;
height: var(--lg-button-size);
display: flex; display: flex;
align-items: center; align-items: center;
@ -122,30 +107,19 @@
} }
} }
.library-menu-browse-button--mobile { &.excalidraw--mobile .library-menu-browse-button {
min-height: 22px; height: var(--default-button-size);
margin-left: auto;
a {
padding-right: 0;
}
} }
.layer-ui__sidebar__header .dropdown-menu { .layer-ui__library .dropdown-menu {
&.dropdown-menu--mobile { width: auto;
top: 100%; top: initial;
} right: 0;
left: initial;
bottom: 100%;
margin-bottom: 0.625rem;
.dropdown-menu-container { .dropdown-menu-container {
--gap: 0;
z-index: 1;
position: absolute;
top: 100%;
left: 0;
:root[dir="rtl"] & {
right: 0;
left: auto;
}
width: 196px; width: 196px;
box-shadow: var(--library-dropdown-shadow); box-shadow: var(--library-dropdown-shadow);
border-radius: var(--border-radius-lg); border-radius: var(--border-radius-lg);

View file

@ -1,77 +1,39 @@
import { import React, { useState, useCallback } from "react";
useRef,
useState,
useEffect,
useCallback,
RefObject,
forwardRef,
} from "react";
import Library, { import Library, {
distributeLibraryItemsOnSquareGrid, distributeLibraryItemsOnSquareGrid,
libraryItemsAtom, libraryItemsAtom,
} from "../data/library"; } from "../data/library";
import { t } from "../i18n"; import { t } from "../i18n";
import { randomId } from "../random"; import { randomId } from "../random";
import { LibraryItems, LibraryItem, AppState, ExcalidrawProps } from "../types"; import {
LibraryItems,
import "./LibraryMenu.scss"; LibraryItem,
ExcalidrawProps,
UIAppState,
} from "../types";
import LibraryMenuItems from "./LibraryMenuItems"; import LibraryMenuItems from "./LibraryMenuItems";
import { EVENT } from "../constants";
import { KEYS } from "../keys";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import { jotaiScope } from "../jotai"; import { jotaiScope } from "../jotai";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
import { import {
useDevice, useApp,
useAppProps,
useExcalidrawElements, useExcalidrawElements,
useExcalidrawSetAppState, useExcalidrawSetAppState,
} from "./App"; } from "./App";
import { Sidebar } from "./Sidebar/Sidebar";
import { getSelectedElements } from "../scene"; import { getSelectedElements } from "../scene";
import { NonDeletedExcalidrawElement } from "../element/types"; import { useUIAppState } from "../context/ui-appState";
import { LibraryMenuHeader } from "./LibraryMenuHeaderContent";
import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
const useOnClickOutside = ( import "./LibraryMenu.scss";
ref: RefObject<HTMLElement>, import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
cb: (event: MouseEvent) => void,
) => {
useEffect(() => {
const listener = (event: MouseEvent) => {
if (!ref.current) {
return;
}
if ( export const isLibraryMenuOpenAtom = atom(false);
event.target instanceof Element &&
(ref.current.contains(event.target) ||
!document.body.contains(event.target))
) {
return;
}
cb(event); const LibraryMenuWrapper = ({ children }: { children: React.ReactNode }) => {
}; return <div className="layer-ui__library">{children}</div>;
document.addEventListener("pointerdown", listener, false);
return () => {
document.removeEventListener("pointerdown", listener);
};
}, [ref, cb]);
}; };
const LibraryMenuWrapper = forwardRef<
HTMLDivElement,
{ children: React.ReactNode }
>(({ children }, ref) => {
return (
<div ref={ref} className="layer-ui__library">
{children}
</div>
);
});
export const LibraryMenuContent = ({ export const LibraryMenuContent = ({
onInsertLibraryItems, onInsertLibraryItems,
pendingElements, pendingElements,
@ -87,11 +49,11 @@ export const LibraryMenuContent = ({
pendingElements: LibraryItem["elements"]; pendingElements: LibraryItem["elements"];
onInsertLibraryItems: (libraryItems: LibraryItems) => void; onInsertLibraryItems: (libraryItems: LibraryItems) => void;
onAddToLibrary: () => void; onAddToLibrary: () => void;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, UIAppState>["setState"];
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
library: Library; library: Library;
id: string; id: string;
appState: AppState; appState: UIAppState;
selectedItems: LibraryItem["id"][]; selectedItems: LibraryItem["id"][];
onSelectItems: (id: LibraryItem["id"][]) => void; onSelectItems: (id: LibraryItem["id"][]) => void;
}) => { }) => {
@ -158,81 +120,31 @@ export const LibraryMenuContent = ({
theme={appState.theme} theme={appState.theme}
/> />
{showBtn && ( {showBtn && (
<LibraryMenuBrowseButton <LibraryMenuControlButtons
style={{ padding: "16px 12px 0 12px" }}
id={id} id={id}
libraryReturnUrl={libraryReturnUrl} libraryReturnUrl={libraryReturnUrl}
theme={appState.theme} theme={appState.theme}
selectedItems={selectedItems}
onSelectItems={onSelectItems}
/> />
)} )}
</LibraryMenuWrapper> </LibraryMenuWrapper>
); );
}; };
export const LibraryMenu: React.FC<{ /**
appState: AppState; * This component is meant to be rendered inside <Sidebar.Tab/> inside our
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void; * <DefaultSidebar/> or host apps Sidebar components.
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; */
focusContainer: () => void; export const LibraryMenu = () => {
library: Library; const { library, id, onInsertElements } = useApp();
id: string; const appProps = useAppProps();
}> = ({ const appState = useUIAppState();
appState,
onInsertElements,
libraryReturnUrl,
focusContainer,
library,
id,
}) => {
const setAppState = useExcalidrawSetAppState(); const setAppState = useExcalidrawSetAppState();
const elements = useExcalidrawElements(); const elements = useExcalidrawElements();
const device = useDevice();
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]); const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
const ref = useRef<HTMLDivElement | null>(null);
const closeLibrary = useCallback(() => {
const isDialogOpen = !!document.querySelector(".Dialog");
// Prevent closing if any dialog is open
if (isDialogOpen) {
return;
}
setAppState({ openSidebar: null });
}, [setAppState]);
useOnClickOutside(
ref,
useCallback(
(event) => {
// If click on the library icon, do nothing so that LibraryButton
// can toggle library menu
if ((event.target as Element).closest(".ToolIcon__library")) {
return;
}
if (!appState.isSidebarDocked || !device.canDeviceFitSidebar) {
closeLibrary();
}
},
[closeLibrary, appState.isSidebarDocked, device.canDeviceFitSidebar],
),
);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === KEYS.ESCAPE &&
(!appState.isSidebarDocked || !device.canDeviceFitSidebar)
) {
closeLibrary();
}
};
document.addEventListener(EVENT.KEYDOWN, handleKeyDown);
return () => {
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
};
}, [closeLibrary, appState.isSidebarDocked, device.canDeviceFitSidebar]);
const deselectItems = useCallback(() => { const deselectItems = useCallback(() => {
setAppState({ setAppState({
@ -241,69 +153,20 @@ export const LibraryMenu: React.FC<{
}); });
}, [setAppState]); }, [setAppState]);
const removeFromLibrary = useCallback(
async (libraryItems: LibraryItems) => {
const nextItems = libraryItems.filter(
(item) => !selectedItems.includes(item.id),
);
library.setLibrary(nextItems).catch(() => {
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
});
setSelectedItems([]);
},
[library, setAppState, selectedItems, setSelectedItems],
);
const resetLibrary = useCallback(() => {
library.resetLibrary();
focusContainer();
}, [library, focusContainer]);
return ( return (
<Sidebar <LibraryMenuContent
__isInternal pendingElements={getSelectedElements(elements, appState, true)}
// necessary to remount when switching between internal onInsertLibraryItems={(libraryItems) => {
// and custom (host app) sidebar, so that the `props.onClose` onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
// is colled correctly
key="library"
className="layer-ui__library-sidebar"
initialDockedState={appState.isSidebarDocked}
onDock={(docked) => {
trackEvent(
"library",
`toggleLibraryDock (${docked ? "dock" : "undock"})`,
`sidebar (${device.isMobile ? "mobile" : "desktop"})`,
);
}} }}
ref={ref} onAddToLibrary={deselectItems}
> setAppState={setAppState}
<Sidebar.Header className="layer-ui__library-header"> libraryReturnUrl={appProps.libraryReturnUrl}
<LibraryMenuHeader library={library}
appState={appState} id={id}
setAppState={setAppState} appState={appState}
selectedItems={selectedItems} selectedItems={selectedItems}
onSelectItems={setSelectedItems} onSelectItems={setSelectedItems}
library={library} />
onRemoveFromLibrary={() =>
removeFromLibrary(libraryItemsData.libraryItems)
}
resetLibrary={resetLibrary}
/>
</Sidebar.Header>
<LibraryMenuContent
pendingElements={getSelectedElements(elements, appState, true)}
onInsertLibraryItems={(libraryItems) => {
onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
}}
onAddToLibrary={deselectItems}
setAppState={setAppState}
libraryReturnUrl={libraryReturnUrl}
library={library}
id={id}
appState={appState}
selectedItems={selectedItems}
onSelectItems={setSelectedItems}
/>
</Sidebar>
); );
}; };

View file

@ -1,6 +1,6 @@
import { VERSIONS } from "../constants"; import { VERSIONS } from "../constants";
import { t } from "../i18n"; import { t } from "../i18n";
import { AppState, ExcalidrawProps } from "../types"; import { ExcalidrawProps, UIAppState } from "../types";
const LibraryMenuBrowseButton = ({ const LibraryMenuBrowseButton = ({
theme, theme,
@ -8,7 +8,7 @@ const LibraryMenuBrowseButton = ({
libraryReturnUrl, libraryReturnUrl,
}: { }: {
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
theme: AppState["theme"]; theme: UIAppState["theme"];
id: string; id: string;
}) => { }) => {
const referrer = const referrer =

View file

@ -0,0 +1,33 @@
import { LibraryItem, ExcalidrawProps, UIAppState } from "../types";
import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
import { LibraryDropdownMenu } from "./LibraryMenuHeaderContent";
export const LibraryMenuControlButtons = ({
selectedItems,
onSelectItems,
libraryReturnUrl,
theme,
id,
style,
}: {
selectedItems: LibraryItem["id"][];
onSelectItems: (id: LibraryItem["id"][]) => void;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
theme: UIAppState["theme"];
id: string;
style: React.CSSProperties;
}) => {
return (
<div className="library-menu-control-buttons" style={style}>
<LibraryMenuBrowseButton
id={id}
libraryReturnUrl={libraryReturnUrl}
theme={theme}
/>
<LibraryDropdownMenu
selectedItems={selectedItems}
onSelectItems={onSelectItems}
/>
</div>
);
};

View file

@ -1,8 +1,11 @@
import React, { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { t } from "../i18n";
import Trans from "./Trans";
import { jotaiScope } from "../jotai";
import { LibraryItem, LibraryItems, UIAppState } from "../types";
import { useApp, useExcalidrawSetAppState } from "./App";
import { saveLibraryAsJSON } from "../data/json"; import { saveLibraryAsJSON } from "../data/json";
import Library, { libraryItemsAtom } from "../data/library"; import Library, { libraryItemsAtom } from "../data/library";
import { t } from "../i18n";
import { AppState, LibraryItem, LibraryItems } from "../types";
import { import {
DotsIcon, DotsIcon,
ExportIcon, ExportIcon,
@ -13,29 +16,27 @@ import {
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
import { fileOpen } from "../data/filesystem"; import { fileOpen } from "../data/filesystem";
import { muteFSAbortError } from "../utils"; import { muteFSAbortError } from "../utils";
import { atom, useAtom } from "jotai"; import { useAtom } from "jotai";
import { jotaiScope } from "../jotai";
import ConfirmDialog from "./ConfirmDialog"; import ConfirmDialog from "./ConfirmDialog";
import PublishLibrary from "./PublishLibrary"; import PublishLibrary from "./PublishLibrary";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import DropdownMenu from "./dropdownMenu/DropdownMenu"; import DropdownMenu from "./dropdownMenu/DropdownMenu";
import { isLibraryMenuOpenAtom } from "./LibraryMenu";
export const isLibraryMenuOpenAtom = atom(false); import { useUIAppState } from "../context/ui-appState";
const getSelectedItems = ( const getSelectedItems = (
libraryItems: LibraryItems, libraryItems: LibraryItems,
selectedItems: LibraryItem["id"][], selectedItems: LibraryItem["id"][],
) => libraryItems.filter((item) => selectedItems.includes(item.id)); ) => libraryItems.filter((item) => selectedItems.includes(item.id));
export const LibraryMenuHeader: React.FC<{ export const LibraryDropdownMenuButton: React.FC<{
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, UIAppState>["setState"];
selectedItems: LibraryItem["id"][]; selectedItems: LibraryItem["id"][];
library: Library; library: Library;
onRemoveFromLibrary: () => void; onRemoveFromLibrary: () => void;
resetLibrary: () => void; resetLibrary: () => void;
onSelectItems: (items: LibraryItem["id"][]) => void; onSelectItems: (items: LibraryItem["id"][]) => void;
appState: AppState; appState: UIAppState;
}> = ({ }> = ({
setAppState, setAppState,
selectedItems, selectedItems,
@ -50,6 +51,7 @@ export const LibraryMenuHeader: React.FC<{
isLibraryMenuOpenAtom, isLibraryMenuOpenAtom,
jotaiScope, jotaiScope,
); );
const renderRemoveLibAlert = useCallback(() => { const renderRemoveLibAlert = useCallback(() => {
const content = selectedItems.length const content = selectedItems.length
? t("alerts.removeItemsFromsLibrary", { count: selectedItems.length }) ? t("alerts.removeItemsFromsLibrary", { count: selectedItems.length })
@ -104,16 +106,19 @@ export const LibraryMenuHeader: React.FC<{
small={true} small={true}
> >
<p> <p>
{t("publishSuccessDialog.content", { <Trans
authorName: publishLibSuccess!.authorName, i18nKey="publishSuccessDialog.content"
})}{" "} authorName={publishLibSuccess!.authorName}
<a link={(el) => (
href={publishLibSuccess?.url} <a
target="_blank" href={publishLibSuccess?.url}
rel="noopener noreferrer" target="_blank"
> rel="noopener noreferrer"
{t("publishSuccessDialog.link")} >
</a> {el}
</a>
)}
/>
</p> </p>
<ToolButton <ToolButton
type="button" type="button"
@ -181,7 +186,6 @@ export const LibraryMenuHeader: React.FC<{
return ( return (
<DropdownMenu open={isLibraryMenuOpen}> <DropdownMenu open={isLibraryMenuOpen}>
<DropdownMenu.Trigger <DropdownMenu.Trigger
className="Sidebar__dropdown-btn"
onToggle={() => setIsLibraryMenuOpen(!isLibraryMenuOpen)} onToggle={() => setIsLibraryMenuOpen(!isLibraryMenuOpen)}
> >
{DotsIcon} {DotsIcon}
@ -230,6 +234,7 @@ export const LibraryMenuHeader: React.FC<{
</DropdownMenu> </DropdownMenu>
); );
}; };
return ( return (
<div style={{ position: "relative" }}> <div style={{ position: "relative" }}>
{renderLibraryMenu()} {renderLibraryMenu()}
@ -261,3 +266,48 @@ export const LibraryMenuHeader: React.FC<{
</div> </div>
); );
}; };
export const LibraryDropdownMenu = ({
selectedItems,
onSelectItems,
}: {
selectedItems: LibraryItem["id"][];
onSelectItems: (id: LibraryItem["id"][]) => void;
}) => {
const { library } = useApp();
const appState = useUIAppState();
const setAppState = useExcalidrawSetAppState();
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
const removeFromLibrary = useCallback(
async (libraryItems: LibraryItems) => {
const nextItems = libraryItems.filter(
(item) => !selectedItems.includes(item.id),
);
library.setLibrary(nextItems).catch(() => {
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
});
onSelectItems([]);
},
[library, setAppState, selectedItems, onSelectItems],
);
const resetLibrary = useCallback(() => {
library.resetLibrary();
}, [library]);
return (
<LibraryDropdownMenuButton
appState={appState}
setAppState={setAppState}
selectedItems={selectedItems}
onSelectItems={onSelectItems}
library={library}
onRemoveFromLibrary={() =>
removeFromLibrary(libraryItemsData.libraryItems)
}
resetLibrary={resetLibrary}
/>
);
};

View file

@ -47,7 +47,7 @@
&__items { &__items {
row-gap: 0.5rem; row-gap: 0.5rem;
padding: var(--container-padding-y) var(--container-padding-x); padding: var(--container-padding-y) 0;
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
@ -61,7 +61,7 @@
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
&--excal { &--excal {
margin-top: 2.5rem; margin-top: 2rem;
} }
} }

View file

@ -2,17 +2,21 @@ import React, { useState } from "react";
import { serializeLibraryAsJSON } from "../data/json"; import { serializeLibraryAsJSON } from "../data/json";
import { ExcalidrawElement, NonDeleted } from "../element/types"; import { ExcalidrawElement, NonDeleted } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { AppState, ExcalidrawProps, LibraryItem, LibraryItems } from "../types"; import {
ExcalidrawProps,
LibraryItem,
LibraryItems,
UIAppState,
} from "../types";
import { arrayToMap, chunk } from "../utils"; import { arrayToMap, chunk } from "../utils";
import { LibraryUnit } from "./LibraryUnit"; import { LibraryUnit } from "./LibraryUnit";
import Stack from "./Stack"; import Stack from "./Stack";
import "./LibraryMenuItems.scss";
import { MIME_TYPES } from "../constants"; import { MIME_TYPES } from "../constants";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
import clsx from "clsx";
import { duplicateElements } from "../element/newElement"; import { duplicateElements } from "../element/newElement";
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
import "./LibraryMenuItems.scss";
const CELLS_PER_ROW = 4; const CELLS_PER_ROW = 4;
@ -36,7 +40,7 @@ const LibraryMenuItems = ({
selectedItems: LibraryItem["id"][]; selectedItems: LibraryItem["id"][];
onSelectItems: (id: LibraryItem["id"][]) => void; onSelectItems: (id: LibraryItem["id"][]) => void;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
theme: AppState["theme"]; theme: UIAppState["theme"];
id: string; id: string;
}) => { }) => {
const [lastSelectedItem, setLastSelectedItem] = useState< const [lastSelectedItem, setLastSelectedItem] = useState<
@ -201,11 +205,7 @@ const LibraryMenuItems = ({
(item) => item.status === "published", (item) => item.status === "published",
); );
const showBtn = const showBtn = !libraryItems.length && !pendingElements.length;
!libraryItems.length &&
!unpublishedItems.length &&
!publishedItems.length &&
!pendingElements.length;
return ( return (
<div <div
@ -215,7 +215,7 @@ const LibraryMenuItems = ({
unpublishedItems.length || unpublishedItems.length ||
publishedItems.length publishedItems.length
? { justifyContent: "flex-start" } ? { justifyContent: "flex-start" }
: {} : { borderBottom: 0 }
} }
> >
<Stack.Col <Stack.Col
@ -251,11 +251,7 @@ const LibraryMenuItems = ({
</div> </div>
{!pendingElements.length && !unpublishedItems.length ? ( {!pendingElements.length && !unpublishedItems.length ? (
<div className="library-menu-items__no-items"> <div className="library-menu-items__no-items">
<div <div className="library-menu-items__no-items__label">
className={clsx({
"library-menu-items__no-items__label": showBtn,
})}
>
{t("library.noItems")} {t("library.noItems")}
</div> </div>
<div className="library-menu-items__no-items__hint"> <div className="library-menu-items__no-items__hint">
@ -303,10 +299,13 @@ const LibraryMenuItems = ({
</> </>
{showBtn && ( {showBtn && (
<LibraryMenuBrowseButton <LibraryMenuControlButtons
style={{ padding: "16px 0", width: "100%" }}
id={id} id={id}
libraryReturnUrl={libraryReturnUrl} libraryReturnUrl={libraryReturnUrl}
theme={theme} theme={theme}
selectedItems={selectedItems}
onSelectItems={onSelectItems}
/> />
)} )}
</Stack.Col> </Stack.Col>

View file

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { AppState, Device, ExcalidrawProps } from "../types"; import { AppState, Device, ExcalidrawProps, UIAppState } from "../types";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { t } from "../i18n"; import { t } from "../i18n";
import Stack from "./Stack"; import Stack from "./Stack";
@ -13,16 +13,15 @@ import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
import { Section } from "./Section"; import { Section } from "./Section";
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars"; import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
import { LockButton } from "./LockButton"; import { LockButton } from "./LockButton";
import { LibraryButton } from "./LibraryButton";
import { PenModeButton } from "./PenModeButton"; import { PenModeButton } from "./PenModeButton";
import { Stats } from "./Stats"; import { Stats } from "./Stats";
import { actionToggleStats } from "../actions"; import { actionToggleStats } from "../actions";
import { HandButton } from "./HandButton"; import { HandButton } from "./HandButton";
import { isHandToolActive } from "../appState"; import { isHandToolActive } from "../appState";
import { useTunnels } from "./context/tunnels"; import { useTunnels } from "../context/tunnels";
type MobileMenuProps = { type MobileMenuProps = {
appState: AppState; appState: UIAppState;
actionManager: ActionManager; actionManager: ActionManager;
renderJSONExportDialog: () => React.ReactNode; renderJSONExportDialog: () => React.ReactNode;
renderImageExportDialog: () => React.ReactNode; renderImageExportDialog: () => React.ReactNode;
@ -36,7 +35,7 @@ type MobileMenuProps = {
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void; onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
renderTopRightUI?: ( renderTopRightUI?: (
isMobile: boolean, isMobile: boolean,
appState: AppState, appState: UIAppState,
) => JSX.Element | null; ) => JSX.Element | null;
renderCustomStats?: ExcalidrawProps["renderCustomStats"]; renderCustomStats?: ExcalidrawProps["renderCustomStats"];
renderSidebars: () => JSX.Element | null; renderSidebars: () => JSX.Element | null;
@ -60,11 +59,15 @@ export const MobileMenu = ({
device, device,
renderWelcomeScreen, renderWelcomeScreen,
}: MobileMenuProps) => { }: MobileMenuProps) => {
const { welcomeScreenCenterTunnel, mainMenuTunnel } = useTunnels(); const {
WelcomeScreenCenterTunnel,
MainMenuTunnel,
DefaultSidebarTriggerTunnel,
} = useTunnels();
const renderToolbar = () => { const renderToolbar = () => {
return ( return (
<FixedSideContainer side="top" className="App-top-bar"> <FixedSideContainer side="top" className="App-top-bar">
{renderWelcomeScreen && <welcomeScreenCenterTunnel.Out />} {renderWelcomeScreen && <WelcomeScreenCenterTunnel.Out />}
<Section heading="shapes"> <Section heading="shapes">
{(heading: React.ReactNode) => ( {(heading: React.ReactNode) => (
<Stack.Col gap={4} align="center"> <Stack.Col gap={4} align="center">
@ -88,11 +91,7 @@ export const MobileMenu = ({
{renderTopRightUI && renderTopRightUI(true, appState)} {renderTopRightUI && renderTopRightUI(true, appState)}
<div className="mobile-misc-tools-container"> <div className="mobile-misc-tools-container">
{!appState.viewModeEnabled && ( {!appState.viewModeEnabled && (
<LibraryButton <DefaultSidebarTriggerTunnel.Out />
appState={appState}
setAppState={setAppState}
isMobile
/>
)} )}
<PenModeButton <PenModeButton
checked={appState.penMode} checked={appState.penMode}
@ -132,14 +131,14 @@ export const MobileMenu = ({
if (appState.viewModeEnabled) { if (appState.viewModeEnabled) {
return ( return (
<div className="App-toolbar-content"> <div className="App-toolbar-content">
<mainMenuTunnel.Out /> <MainMenuTunnel.Out />
</div> </div>
); );
} }
return ( return (
<div className="App-toolbar-content"> <div className="App-toolbar-content">
<mainMenuTunnel.Out /> <MainMenuTunnel.Out />
{actionManager.renderAction("toggleEditMenu")} {actionManager.renderAction("toggleEditMenu")}
{actionManager.renderAction("undo")} {actionManager.renderAction("undo")}
{actionManager.renderAction("redo")} {actionManager.renderAction("redo")}
@ -190,13 +189,13 @@ export const MobileMenu = ({
{renderAppToolbar()} {renderAppToolbar()}
{appState.scrolledOutside && {appState.scrolledOutside &&
!appState.openMenu && !appState.openMenu &&
appState.openSidebar !== "library" && ( !appState.openSidebar && (
<button <button
className="scroll-back-to-content" className="scroll-back-to-content"
onClick={() => { onClick={() => {
setAppState({ setAppState((appState) => ({
...calculateScrollCenter(elements, appState, canvas), ...calculateScrollCenter(elements, appState, canvas),
}); }));
}} }}
> >
{t("buttons.scrollBackToContent")} {t("buttons.scrollBackToContent")}

View file

@ -5,8 +5,10 @@ import { ChartElements, renderSpreadsheet, Spreadsheet } from "../charts";
import { ChartType } from "../element/types"; import { ChartType } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { exportToSvg } from "../scene/export"; import { exportToSvg } from "../scene/export";
import { AppState, LibraryItem } from "../types"; import { UIAppState } from "../types";
import { useApp } from "./App";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import "./PasteChartDialog.scss"; import "./PasteChartDialog.scss";
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void; type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
@ -78,13 +80,12 @@ export const PasteChartDialog = ({
setAppState, setAppState,
appState, appState,
onClose, onClose,
onInsertChart,
}: { }: {
appState: AppState; appState: UIAppState;
onClose: () => void; onClose: () => void;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, UIAppState>["setState"];
onInsertChart: (elements: LibraryItem["elements"]) => void;
}) => { }) => {
const { onInsertElements } = useApp();
const handleClose = React.useCallback(() => { const handleClose = React.useCallback(() => {
if (onClose) { if (onClose) {
onClose(); onClose();
@ -92,7 +93,7 @@ export const PasteChartDialog = ({
}, [onClose]); }, [onClose]);
const handleChartClick = (chartType: ChartType, elements: ChartElements) => { const handleChartClick = (chartType: ChartType, elements: ChartElements) => {
onInsertChart(elements); onInsertElements(elements);
trackEvent("magic", "chart", chartType); trackEvent("magic", "chart", chartType);
setAppState({ setAppState({
currentChartType: chartType, currentChartType: chartType,

View file

@ -3,8 +3,9 @@ import OpenColor from "open-color";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import { t } from "../i18n"; import { t } from "../i18n";
import Trans from "./Trans";
import { AppState, LibraryItems, LibraryItem } from "../types"; import { LibraryItems, LibraryItem, UIAppState } from "../types";
import { exportToCanvas, exportToSvg } from "../packages/utils"; import { exportToCanvas, exportToSvg } from "../packages/utils";
import { import {
EXPORT_DATA_TYPES, EXPORT_DATA_TYPES,
@ -135,7 +136,7 @@ const SingleLibraryItem = ({
onRemove, onRemove,
}: { }: {
libItem: LibraryItem; libItem: LibraryItem;
appState: AppState; appState: UIAppState;
index: number; index: number;
onChange: (val: string, index: number) => void; onChange: (val: string, index: number) => void;
onRemove: (id: string) => void; onRemove: (id: string) => void;
@ -231,7 +232,7 @@ const PublishLibrary = ({
}: { }: {
onClose: () => void; onClose: () => void;
libraryItems: LibraryItems; libraryItems: LibraryItems;
appState: AppState; appState: UIAppState;
onSuccess: (data: { onSuccess: (data: {
url: string; url: string;
authorName: string; authorName: string;
@ -402,26 +403,32 @@ const PublishLibrary = ({
{shouldRenderForm ? ( {shouldRenderForm ? (
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<div className="publish-library-note"> <div className="publish-library-note">
{t("publishDialog.noteDescription.pre")} <Trans
<a i18nKey="publishDialog.noteDescription"
href="https://libraries.excalidraw.com" link={(el) => (
target="_blank" <a
rel="noopener noreferrer" href="https://libraries.excalidraw.com"
> target="_blank"
{t("publishDialog.noteDescription.link")} rel="noopener noreferrer"
</a>{" "} >
{t("publishDialog.noteDescription.post")} {el}
</a>
)}
/>
</div> </div>
<span className="publish-library-note"> <span className="publish-library-note">
{t("publishDialog.noteGuidelines.pre")} <Trans
<a i18nKey="publishDialog.noteGuidelines"
href="https://github.com/excalidraw/excalidraw-libraries#guidelines" link={(el) => (
target="_blank" <a
rel="noopener noreferrer" href="https://github.com/excalidraw/excalidraw-libraries#guidelines"
> target="_blank"
{t("publishDialog.noteGuidelines.link")} rel="noopener noreferrer"
</a> >
{t("publishDialog.noteGuidelines.post")} {el}
</a>
)}
/>
</span> </span>
<div className="publish-library-note"> <div className="publish-library-note">
@ -515,15 +522,18 @@ const PublishLibrary = ({
/> />
</label> </label>
<span className="publish-library-note"> <span className="publish-library-note">
{t("publishDialog.noteLicense.pre")} <Trans
<a i18nKey="publishDialog.noteLicense"
href="https://github.com/excalidraw/excalidraw-libraries/blob/main/LICENSE" link={(el) => (
target="_blank" <a
rel="noopener noreferrer" href="https://github.com/excalidraw/excalidraw-libraries/blob/main/LICENSE"
> target="_blank"
{t("publishDialog.noteLicense.link")} rel="noopener noreferrer"
</a> >
{t("publishDialog.noteLicense.post")} {el}
</a>
)}
/>
</span> </span>
</div> </div>
<div className="publish-library__buttons"> <div className="publish-library__buttons">

View file

@ -2,67 +2,26 @@
@import "../../css/variables.module"; @import "../../css/variables.module";
.excalidraw { .excalidraw {
.Sidebar { .sidebar {
&__close-btn, display: flex;
&__pin-btn, flex-direction: column;
&__dropdown-btn {
@include outlineButtonStyles;
width: var(--lg-button-size);
height: var(--lg-button-size);
padding: 0;
svg {
width: var(--lg-icon-size);
height: var(--lg-icon-size);
}
}
&__pin-btn {
&--pinned {
background-color: var(--color-primary);
border-color: var(--color-primary);
svg {
color: #fff;
}
&:hover,
&:active {
background-color: var(--color-primary-darker);
}
}
}
}
&.theme--dark {
.Sidebar {
&__pin-btn {
&--pinned {
svg {
color: var(--color-gray-90);
}
}
}
}
}
.layer-ui__sidebar {
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
right: 0; right: 0;
z-index: 5; z-index: 5;
margin: 0; margin: 0;
padding: 0;
box-sizing: border-box;
background-color: var(--sidebar-bg-color);
box-shadow: var(--sidebar-shadow);
:root[dir="rtl"] & { :root[dir="rtl"] & {
left: 0; left: 0;
right: auto; right: auto;
} }
background-color: var(--sidebar-bg-color);
box-shadow: var(--sidebar-shadow);
&--docked { &--docked {
box-shadow: none; box-shadow: none;
} }
@ -77,52 +36,134 @@
border-right: 1px solid var(--sidebar-border-color); border-right: 1px solid var(--sidebar-border-color);
border-left: 0; border-left: 0;
} }
padding: 0;
box-sizing: border-box;
.Island {
box-shadow: none;
}
.ToolIcon__icon {
border-radius: var(--border-radius-md);
}
.ToolIcon__icon__close {
.Modal__close {
width: calc(var(--space-factor) * 7);
height: calc(var(--space-factor) * 7);
display: flex;
justify-content: center;
align-items: center;
color: var(--color-text);
}
}
.Island {
--padding: 0;
background-color: var(--island-bg-color);
border-radius: var(--border-radius-lg);
padding: calc(var(--padding) * var(--space-factor));
position: relative;
transition: box-shadow 0.5s ease-in-out;
}
} }
.layer-ui__sidebar__header { // ---------------------------- sidebar header ------------------------------
.sidebar__header {
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
width: 100%; width: 100%;
padding: 1rem; padding-top: 1rem;
border-bottom: 1px solid var(--sidebar-border-color); padding-bottom: 1rem;
} }
.layer-ui__sidebar__header__buttons { .sidebar__header__buttons {
gap: 0;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.625rem; margin-left: auto;
button {
@include outlineButtonStyles;
--button-bg: transparent;
border: 0 !important;
width: var(--lg-button-size);
height: var(--lg-button-size);
padding: 0;
svg {
width: var(--lg-icon-size);
height: var(--lg-icon-size);
}
&:hover {
background: var(--button-hover-bg, var(--island-bg-color));
}
}
.sidebar__dock.selected {
svg {
stroke: var(--color-primary);
fill: var(--color-primary);
}
}
}
// ---------------------------- sidebar tabs ------------------------------
.sidebar-tabs-root {
display: flex;
flex-direction: column;
flex: 1 1 auto;
padding: 1rem 0.75rem;
[role="tabpanel"] {
flex: 1;
outline: none;
flex: 1 1 auto;
display: flex;
flex-direction: column;
outline: none;
}
[role="tabpanel"][data-state="inactive"] {
display: none !important;
}
[role="tablist"] {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
}
}
.sidebar-tabs-root > .sidebar__header {
padding-top: 0;
padding-bottom: 1rem;
}
.sidebar-tab-trigger {
--button-width: auto;
--button-bg: transparent;
--button-hover-bg: transparent;
--button-active-bg: var(--color-primary);
--button-hover-color: var(--color-primary);
--button-hover-border: var(--color-primary);
&[data-state="active"] {
--button-bg: var(--color-primary);
--button-hover-bg: var(--color-primary-darker);
--button-hover-color: var(--color-icon-white);
--button-border: var(--color-primary);
color: var(--color-icon-white);
}
}
// ---------------------------- default sidebar ------------------------------
.default-sidebar {
display: flex;
flex-direction: column;
.sidebar-triggers {
$padding: 2px;
$border: 1px;
display: flex;
gap: 0;
padding: $padding;
// offset by padding + border to vertically center the list with sibling
// buttons (both from top and bototm, due to flex layout)
margin-top: -#{$padding + $border};
margin-bottom: -#{$padding + $border};
border: $border solid var(--sidebar-border-color);
background: var(--default-bg-color);
border-radius: 0.625rem;
.sidebar-tab-trigger {
height: var(--lg-button-size);
width: var(--lg-button-size);
border: none;
}
}
.sidebar__header {
border-bottom: 1px solid var(--sidebar-border-color);
}
} }
} }

View file

@ -1,8 +1,9 @@
import React from "react"; import React from "react";
import { DEFAULT_SIDEBAR } from "../../constants";
import { Excalidraw, Sidebar } from "../../packages/excalidraw/index"; import { Excalidraw, Sidebar } from "../../packages/excalidraw/index";
import { import {
act,
fireEvent, fireEvent,
GlobalTestState,
queryAllByTestId, queryAllByTestId,
queryByTestId, queryByTestId,
render, render,
@ -10,346 +11,321 @@ import {
withExcalidrawDimensions, withExcalidrawDimensions,
} from "../../tests/test-utils"; } from "../../tests/test-utils";
export const assertSidebarDockButton = async <T extends boolean>(
hasDockButton: T,
): Promise<
T extends false
? { dockButton: null; sidebar: HTMLElement }
: { dockButton: HTMLElement; sidebar: HTMLElement }
> => {
const sidebar =
GlobalTestState.renderResult.container.querySelector<HTMLElement>(
".sidebar",
);
expect(sidebar).not.toBe(null);
const dockButton = queryByTestId(sidebar!, "sidebar-dock");
if (hasDockButton) {
expect(dockButton).not.toBe(null);
return { dockButton: dockButton!, sidebar: sidebar! } as any;
}
expect(dockButton).toBe(null);
return { dockButton: null, sidebar: sidebar! } as any;
};
export const assertExcalidrawWithSidebar = async (
sidebar: React.ReactNode,
name: string,
test: () => void,
) => {
await render(
<Excalidraw initialData={{ appState: { openSidebar: { name } } }}>
{sidebar}
</Excalidraw>,
);
await withExcalidrawDimensions({ width: 1920, height: 1080 }, test);
};
describe("Sidebar", () => { describe("Sidebar", () => {
it("should render custom sidebar", async () => { describe("General behavior", () => {
const { container } = await render( it("should render custom sidebar", async () => {
<Excalidraw const { container } = await render(
initialData={{ appState: { openSidebar: "customSidebar" } }} <Excalidraw
renderSidebar={() => ( initialData={{ appState: { openSidebar: { name: "customSidebar" } } }}
<Sidebar> >
<Sidebar name="customSidebar">
<div id="test-sidebar-content">42</div> <div id="test-sidebar-content">42</div>
</Sidebar> </Sidebar>
)} </Excalidraw>,
/>, );
);
const node = container.querySelector("#test-sidebar-content"); const node = container.querySelector("#test-sidebar-content");
expect(node).not.toBe(null); expect(node).not.toBe(null);
});
it("should render only one sidebar and prefer the custom one", async () => {
const { container } = await render(
<Excalidraw
initialData={{ appState: { openSidebar: { name: "customSidebar" } } }}
>
<Sidebar name="customSidebar">
<div id="test-sidebar-content">42</div>
</Sidebar>
</Excalidraw>,
);
await waitFor(() => {
// make sure the custom sidebar is rendered
const node = container.querySelector("#test-sidebar-content");
expect(node).not.toBe(null);
// make sure only one sidebar is rendered
const sidebars = container.querySelectorAll(".sidebar");
expect(sidebars.length).toBe(1);
});
});
it("should toggle sidebar using props.toggleMenu()", async () => {
const { container } = await render(
<Excalidraw>
<Sidebar name="customSidebar">
<div id="test-sidebar-content">42</div>
</Sidebar>
</Excalidraw>,
);
// sidebar isn't rendered initially
// -------------------------------------------------------------------------
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).toBe(null);
});
// toggle sidebar on
// -------------------------------------------------------------------------
expect(window.h.app.toggleSidebar({ name: "customSidebar" })).toBe(true);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).not.toBe(null);
});
// toggle sidebar off
// -------------------------------------------------------------------------
expect(window.h.app.toggleSidebar({ name: "customSidebar" })).toBe(false);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).toBe(null);
});
// force-toggle sidebar off (=> still hidden)
// -------------------------------------------------------------------------
expect(
window.h.app.toggleSidebar({ name: "customSidebar", force: false }),
).toBe(false);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).toBe(null);
});
// force-toggle sidebar on
// -------------------------------------------------------------------------
expect(
window.h.app.toggleSidebar({ name: "customSidebar", force: true }),
).toBe(true);
expect(
window.h.app.toggleSidebar({ name: "customSidebar", force: true }),
).toBe(true);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).not.toBe(null);
});
// toggle library (= hide custom sidebar)
// -------------------------------------------------------------------------
expect(window.h.app.toggleSidebar({ name: DEFAULT_SIDEBAR.name })).toBe(
true,
);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).toBe(null);
// make sure only one sidebar is rendered
const sidebars = container.querySelectorAll(".sidebar");
expect(sidebars.length).toBe(1);
});
});
}); });
it("should render custom sidebar header", async () => { describe("<Sidebar.Header/>", () => {
const { container } = await render( it("should render custom sidebar header", async () => {
<Excalidraw const { container } = await render(
initialData={{ appState: { openSidebar: "customSidebar" } }} <Excalidraw
renderSidebar={() => ( initialData={{ appState: { openSidebar: { name: "customSidebar" } } }}
<Sidebar> >
<Sidebar name="customSidebar">
<Sidebar.Header> <Sidebar.Header>
<div id="test-sidebar-header-content">42</div> <div id="test-sidebar-header-content">42</div>
</Sidebar.Header> </Sidebar.Header>
</Sidebar> </Sidebar>
)} </Excalidraw>,
/>, );
);
const node = container.querySelector("#test-sidebar-header-content"); const node = container.querySelector("#test-sidebar-header-content");
expect(node).not.toBe(null);
// make sure we don't render the default fallback header,
// just the custom one
expect(queryAllByTestId(container, "sidebar-header").length).toBe(1);
});
it("should render only one sidebar and prefer the custom one", async () => {
const { container } = await render(
<Excalidraw
initialData={{ appState: { openSidebar: "customSidebar" } }}
renderSidebar={() => (
<Sidebar>
<div id="test-sidebar-content">42</div>
</Sidebar>
)}
/>,
);
await waitFor(() => {
// make sure the custom sidebar is rendered
const node = container.querySelector("#test-sidebar-content");
expect(node).not.toBe(null); expect(node).not.toBe(null);
// make sure we don't render the default fallback header,
// make sure only one sidebar is rendered // just the custom one
const sidebars = container.querySelectorAll(".layer-ui__sidebar"); expect(queryAllByTestId(container, "sidebar-header").length).toBe(1);
expect(sidebars.length).toBe(1);
}); });
});
it("should always render custom sidebar with close button & close on click", async () => { it("should not render <Sidebar.Header> for custom sidebars by default", async () => {
const onClose = jest.fn(); const CustomExcalidraw = () => {
const CustomExcalidraw = () => { return (
return ( <Excalidraw
<Excalidraw initialData={{
initialData={{ appState: { openSidebar: "customSidebar" } }} appState: { openSidebar: { name: "customSidebar" } },
renderSidebar={() => ( }}
<Sidebar className="test-sidebar" onClose={onClose}> >
<Sidebar name="customSidebar" className="test-sidebar">
hello hello
</Sidebar> </Sidebar>
)} </Excalidraw>
/> );
); };
};
const { container } = await render(<CustomExcalidraw />); const { container } = await render(<CustomExcalidraw />);
const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
expect(sidebar).not.toBe(null);
const closeButton = queryByTestId(sidebar!, "sidebar-close")!;
expect(closeButton).not.toBe(null);
fireEvent.click(closeButton);
await waitFor(() => {
expect(container.querySelector<HTMLElement>(".test-sidebar")).toBe(null);
expect(onClose).toHaveBeenCalled();
});
});
it("should render custom sidebar with dock (irrespective of onDock prop)", async () => {
const CustomExcalidraw = () => {
return (
<Excalidraw
initialData={{ appState: { openSidebar: "customSidebar" } }}
renderSidebar={() => (
<Sidebar className="test-sidebar">hello</Sidebar>
)}
/>
);
};
const { container } = await render(<CustomExcalidraw />);
// should show dock button when the sidebar fits to be docked
// -------------------------------------------------------------------------
await withExcalidrawDimensions({ width: 1920, height: 1080 }, () => {
const sidebar = container.querySelector<HTMLElement>(".test-sidebar"); const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
expect(sidebar).not.toBe(null); expect(sidebar).not.toBe(null);
const closeButton = queryByTestId(sidebar!, "sidebar-dock"); const closeButton = queryByTestId(sidebar!, "sidebar-close");
expect(closeButton).not.toBe(null);
});
// should not show dock button when the sidebar does not fit to be docked
// -------------------------------------------------------------------------
await withExcalidrawDimensions({ width: 400, height: 1080 }, () => {
const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
expect(sidebar).not.toBe(null);
const closeButton = queryByTestId(sidebar!, "sidebar-dock");
expect(closeButton).toBe(null); expect(closeButton).toBe(null);
}); });
});
it("should support controlled docking", async () => { it("<Sidebar.Header> should render close button", async () => {
let _setDockable: (dockable: boolean) => void = null!; const onStateChange = jest.fn();
const CustomExcalidraw = () => {
const CustomExcalidraw = () => { return (
const [dockable, setDockable] = React.useState(false); <Excalidraw
_setDockable = setDockable; initialData={{
return ( appState: { openSidebar: { name: "customSidebar" } },
<Excalidraw }}
initialData={{ appState: { openSidebar: "customSidebar" } }} >
renderSidebar={() => (
<Sidebar <Sidebar
name="customSidebar"
className="test-sidebar" className="test-sidebar"
docked={false} onStateChange={onStateChange}
dockable={dockable}
> >
hello <Sidebar.Header />
</Sidebar> </Sidebar>
)} </Excalidraw>
/> );
); };
};
const { container } = await render(<CustomExcalidraw />); const { container } = await render(<CustomExcalidraw />);
await withExcalidrawDimensions({ width: 1920, height: 1080 }, async () => { // initial open
// should not show dock button when `dockable` is `false` expect(onStateChange).toHaveBeenCalledWith({ name: "customSidebar" });
// -------------------------------------------------------------------------
act(() => { const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
_setDockable(false); expect(sidebar).not.toBe(null);
}); const closeButton = queryByTestId(sidebar!, "sidebar-close")!;
expect(closeButton).not.toBe(null);
fireEvent.click(closeButton);
await waitFor(() => { await waitFor(() => {
const sidebar = container.querySelector<HTMLElement>(".test-sidebar"); expect(container.querySelector<HTMLElement>(".test-sidebar")).toBe(
expect(sidebar).not.toBe(null); null,
const closeButton = queryByTestId(sidebar!, "sidebar-dock"); );
expect(closeButton).toBe(null); expect(onStateChange).toHaveBeenCalledWith(null);
});
// should show dock button when `dockable` is `true`, even if `docked`
// prop is set
// -------------------------------------------------------------------------
act(() => {
_setDockable(true);
});
await waitFor(() => {
const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
expect(sidebar).not.toBe(null);
const closeButton = queryByTestId(sidebar!, "sidebar-dock");
expect(closeButton).not.toBe(null);
}); });
}); });
}); });
it("should support controlled docking", async () => { describe("Docking behavior", () => {
let _setDocked: (docked?: boolean) => void = null!; it("shouldn't be user-dockable if `onDock` not supplied", async () => {
await assertExcalidrawWithSidebar(
<Sidebar name="customSidebar">
<Sidebar.Header />
</Sidebar>,
"customSidebar",
async () => {
await assertSidebarDockButton(false);
},
);
});
const CustomExcalidraw = () => { it("shouldn't be user-dockable if `onDock` not supplied & `docked={true}`", async () => {
const [docked, setDocked] = React.useState<boolean | undefined>(); await assertExcalidrawWithSidebar(
_setDocked = setDocked; <Sidebar name="customSidebar" docked={true}>
return ( <Sidebar.Header />
</Sidebar>,
"customSidebar",
async () => {
await assertSidebarDockButton(false);
},
);
});
it("shouldn't be user-dockable if `onDock` not supplied & docked={false}`", async () => {
await assertExcalidrawWithSidebar(
<Sidebar name="customSidebar" docked={false}>
<Sidebar.Header />
</Sidebar>,
"customSidebar",
async () => {
await assertSidebarDockButton(false);
},
);
});
it("should be user-dockable when both `onDock` and `docked` supplied", async () => {
await render(
<Excalidraw <Excalidraw
initialData={{ appState: { openSidebar: "customSidebar" } }} initialData={{ appState: { openSidebar: { name: "customSidebar" } } }}
renderSidebar={() => ( >
<Sidebar className="test-sidebar" docked={docked}> <Sidebar
hello name="customSidebar"
</Sidebar> className="test-sidebar"
)} onDock={() => {}}
/> docked
); >
}; <Sidebar.Header />
const { container } = await render(<CustomExcalidraw />);
const { h } = window;
await withExcalidrawDimensions({ width: 1920, height: 1080 }, async () => {
const dockButton = await waitFor(() => {
const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
expect(sidebar).not.toBe(null);
const dockBotton = queryByTestId(sidebar!, "sidebar-dock");
expect(dockBotton).not.toBe(null);
return dockBotton!;
});
const dockButtonInput = dockButton.querySelector("input")!;
// should not show dock button when `dockable` is `false`
// -------------------------------------------------------------------------
expect(h.state.isSidebarDocked).toBe(false);
fireEvent.click(dockButtonInput);
await waitFor(() => {
expect(h.state.isSidebarDocked).toBe(true);
expect(dockButtonInput).toBeChecked();
});
fireEvent.click(dockButtonInput);
await waitFor(() => {
expect(h.state.isSidebarDocked).toBe(false);
expect(dockButtonInput).not.toBeChecked();
});
// shouldn't update `appState.isSidebarDocked` when the sidebar
// is controlled (`docked` prop is set), as host apps should handle
// the state themselves
// -------------------------------------------------------------------------
act(() => {
_setDocked(true);
});
await waitFor(() => {
expect(dockButtonInput).toBeChecked();
expect(h.state.isSidebarDocked).toBe(false);
expect(dockButtonInput).toBeChecked();
});
fireEvent.click(dockButtonInput);
await waitFor(() => {
expect(h.state.isSidebarDocked).toBe(false);
expect(dockButtonInput).toBeChecked();
});
// the `appState.isSidebarDocked` should remain untouched when
// `props.docked` is set to `false`, and user toggles
// -------------------------------------------------------------------------
act(() => {
_setDocked(false);
h.setState({ isSidebarDocked: true });
});
await waitFor(() => {
expect(h.state.isSidebarDocked).toBe(true);
expect(dockButtonInput).not.toBeChecked();
});
fireEvent.click(dockButtonInput);
await waitFor(() => {
expect(dockButtonInput).not.toBeChecked();
expect(h.state.isSidebarDocked).toBe(true);
});
});
});
it("should toggle sidebar using props.toggleMenu()", async () => {
const { container } = await render(
<Excalidraw
renderSidebar={() => (
<Sidebar>
<div id="test-sidebar-content">42</div>
</Sidebar> </Sidebar>
)} </Excalidraw>,
/>, );
);
// sidebar isn't rendered initially await withExcalidrawDimensions(
// ------------------------------------------------------------------------- { width: 1920, height: 1080 },
await waitFor(() => { async () => {
const node = container.querySelector("#test-sidebar-content"); await assertSidebarDockButton(true);
expect(node).toBe(null); },
);
}); });
// toggle sidebar on it("shouldn't be user-dockable when only `onDock` supplied w/o `docked`", async () => {
// ------------------------------------------------------------------------- await render(
expect(window.h.app.toggleMenu("customSidebar")).toBe(true); <Excalidraw
initialData={{ appState: { openSidebar: { name: "customSidebar" } } }}
>
<Sidebar
name="customSidebar"
className="test-sidebar"
onDock={() => {}}
>
<Sidebar.Header />
</Sidebar>
</Excalidraw>,
);
await waitFor(() => { await withExcalidrawDimensions(
const node = container.querySelector("#test-sidebar-content"); { width: 1920, height: 1080 },
expect(node).not.toBe(null); async () => {
}); await assertSidebarDockButton(false);
},
// toggle sidebar off );
// -------------------------------------------------------------------------
expect(window.h.app.toggleMenu("customSidebar")).toBe(false);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).toBe(null);
});
// force-toggle sidebar off (=> still hidden)
// -------------------------------------------------------------------------
expect(window.h.app.toggleMenu("customSidebar", false)).toBe(false);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).toBe(null);
});
// force-toggle sidebar on
// -------------------------------------------------------------------------
expect(window.h.app.toggleMenu("customSidebar", true)).toBe(true);
expect(window.h.app.toggleMenu("customSidebar", true)).toBe(true);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).not.toBe(null);
});
// toggle library (= hide custom sidebar)
// -------------------------------------------------------------------------
expect(window.h.app.toggleMenu("library")).toBe(true);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).toBe(null);
// make sure only one sidebar is rendered
const sidebars = container.querySelectorAll(".layer-ui__sidebar");
expect(sidebars.length).toBe(1);
}); });
}); });
}); });

View file

@ -1,151 +1,246 @@
import { import React, {
useEffect, useEffect,
useLayoutEffect, useLayoutEffect,
useRef, useRef,
useState, useState,
forwardRef, forwardRef,
useImperativeHandle,
useCallback,
RefObject,
} from "react"; } from "react";
import { Island } from ".././Island"; import { Island } from ".././Island";
import { atom, useAtom } from "jotai"; import { atom, useSetAtom } from "jotai";
import { jotaiScope } from "../../jotai"; import { jotaiScope } from "../../jotai";
import { import {
SidebarPropsContext, SidebarPropsContext,
SidebarProps, SidebarProps,
SidebarPropsContextValue, SidebarPropsContextValue,
} from "./common"; } from "./common";
import { SidebarHeader } from "./SidebarHeader";
import { SidebarHeaderComponents } from "./SidebarHeader"; import clsx from "clsx";
import { useDevice, useExcalidrawSetAppState } from "../App";
import { updateObject } from "../../utils";
import { KEYS } from "../../keys";
import { EVENT } from "../../constants";
import { SidebarTrigger } from "./SidebarTrigger";
import { SidebarTabTriggers } from "./SidebarTabTriggers";
import { SidebarTabTrigger } from "./SidebarTabTrigger";
import { SidebarTabs } from "./SidebarTabs";
import { SidebarTab } from "./SidebarTab";
import "./Sidebar.scss"; import "./Sidebar.scss";
import clsx from "clsx"; import { useUIAppState } from "../../context/ui-appState";
import { useExcalidrawSetAppState } from "../App";
import { updateObject } from "../../utils";
/** using a counter instead of boolean to handle race conditions where // FIXME replace this with the implem from ColorPicker once it's merged
* the host app may render (mount/unmount) multiple different sidebar */ const useOnClickOutside = (
export const hostSidebarCountersAtom = atom({ rendered: 0, docked: 0 }); ref: RefObject<HTMLElement>,
cb: (event: MouseEvent) => void,
export const Sidebar = Object.assign( ) => {
forwardRef( useEffect(() => {
( const listener = (event: MouseEvent) => {
{ if (!ref.current) {
children, return;
onClose,
onDock,
docked,
/** Undocumented, may be removed later. Generally should either be
* `props.docked` or `appState.isSidebarDocked`. Currently serves to
* prevent unwanted animation of the shadow if initially docked. */
//
// NOTE we'll want to remove this after we sort out how to subscribe to
// individual appState properties
initialDockedState = docked,
dockable = true,
className,
__isInternal,
}: SidebarProps<{
// NOTE sidebars we use internally inside the editor must have this flag set.
// It indicates that this sidebar should have lower precedence over host
// sidebars, if both are open.
/** @private internal */
__isInternal?: boolean;
}>,
ref: React.ForwardedRef<HTMLDivElement>,
) => {
const [hostSidebarCounters, setHostSidebarCounters] = useAtom(
hostSidebarCountersAtom,
jotaiScope,
);
const setAppState = useExcalidrawSetAppState();
const [isDockedFallback, setIsDockedFallback] = useState(
docked ?? initialDockedState ?? false,
);
useLayoutEffect(() => {
if (docked === undefined) {
// ugly hack to get initial state out of AppState without subscribing
// to it as a whole (once we have granular subscriptions, we'll move
// to that)
//
// NOTE this means that is updated `state.isSidebarDocked` changes outside
// of this compoent, it won't be reflected here. Currently doesn't happen.
setAppState((state) => {
setIsDockedFallback(state.isSidebarDocked);
// bail from update
return null;
});
}
}, [setAppState, docked]);
useLayoutEffect(() => {
if (!__isInternal) {
setHostSidebarCounters((s) => ({
rendered: s.rendered + 1,
docked: isDockedFallback ? s.docked + 1 : s.docked,
}));
return () => {
setHostSidebarCounters((s) => ({
rendered: s.rendered - 1,
docked: isDockedFallback ? s.docked - 1 : s.docked,
}));
};
}
}, [__isInternal, setHostSidebarCounters, isDockedFallback]);
const onCloseRef = useRef(onClose);
onCloseRef.current = onClose;
useEffect(() => {
return () => {
onCloseRef.current?.();
};
}, []);
const headerPropsRef = useRef<SidebarPropsContextValue>({});
headerPropsRef.current.onClose = () => {
setAppState({ openSidebar: null });
};
headerPropsRef.current.onDock = (isDocked) => {
if (docked === undefined) {
setAppState({ isSidebarDocked: isDocked });
setIsDockedFallback(isDocked);
}
onDock?.(isDocked);
};
// renew the ref object if the following props change since we want to
// rerender. We can't pass down as component props manually because
// the <Sidebar.Header/> can be rendered upsream.
headerPropsRef.current = updateObject(headerPropsRef.current, {
docked: docked ?? isDockedFallback,
dockable,
});
if (hostSidebarCounters.rendered > 0 && __isInternal) {
return null;
} }
return ( if (
<Island event.target instanceof Element &&
className={clsx( (ref.current.contains(event.target) ||
"layer-ui__sidebar", !document.body.contains(event.target))
{ "layer-ui__sidebar--docked": isDockedFallback }, ) {
className, return;
)} }
ref={ref}
> cb(event);
<SidebarPropsContext.Provider value={headerPropsRef.current}> };
<SidebarHeaderComponents.Context> document.addEventListener("pointerdown", listener, false);
<SidebarHeaderComponents.Component __isFallback />
{children} return () => {
</SidebarHeaderComponents.Context> document.removeEventListener("pointerdown", listener);
</SidebarPropsContext.Provider> };
</Island> }, [ref, cb]);
};
/**
* Flags whether the currently rendered Sidebar is docked or not, for use
* in upstream components that need to act on this (e.g. LayerUI to shift the
* UI). We use an atom because of potential host app sidebars (for the default
* sidebar we could just read from appState.defaultSidebarDockedPreference).
*
* Since we can only render one Sidebar at a time, we can use a simple flag.
*/
export const isSidebarDockedAtom = atom(false);
export const SidebarInner = forwardRef(
(
{
name,
children,
onDock,
docked,
className,
...rest
}: SidebarProps & Omit<React.RefAttributes<HTMLDivElement>, "onSelect">,
ref: React.ForwardedRef<HTMLDivElement>,
) => {
if (process.env.NODE_ENV === "development" && onDock && docked == null) {
console.warn(
"Sidebar: `docked` must be set when `onDock` is supplied for the sidebar to be user-dockable. To hide this message, either pass `docked` or remove `onDock`",
); );
}, }
),
{ const setAppState = useExcalidrawSetAppState();
Header: SidebarHeaderComponents.Component,
const setIsSidebarDockedAtom = useSetAtom(isSidebarDockedAtom, jotaiScope);
useLayoutEffect(() => {
setIsSidebarDockedAtom(!!docked);
return () => {
setIsSidebarDockedAtom(false);
};
}, [setIsSidebarDockedAtom, docked]);
const headerPropsRef = useRef<SidebarPropsContextValue>(
{} as SidebarPropsContextValue,
);
headerPropsRef.current.onCloseRequest = () => {
setAppState({ openSidebar: null });
};
headerPropsRef.current.onDock = (isDocked) => onDock?.(isDocked);
// renew the ref object if the following props change since we want to
// rerender. We can't pass down as component props manually because
// the <Sidebar.Header/> can be rendered upstream.
headerPropsRef.current = updateObject(headerPropsRef.current, {
docked,
// explicit prop to rerender on update
shouldRenderDockButton: !!onDock && docked != null,
});
const islandRef = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => {
return islandRef.current!;
});
const device = useDevice();
const closeLibrary = useCallback(() => {
const isDialogOpen = !!document.querySelector(".Dialog");
// Prevent closing if any dialog is open
if (isDialogOpen) {
return;
}
setAppState({ openSidebar: null });
}, [setAppState]);
useOnClickOutside(
islandRef,
useCallback(
(event) => {
// If click on the library icon, do nothing so that LibraryButton
// can toggle library menu
if ((event.target as Element).closest(".sidebar-trigger")) {
return;
}
if (!docked || !device.canDeviceFitSidebar) {
closeLibrary();
}
},
[closeLibrary, docked, device.canDeviceFitSidebar],
),
);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === KEYS.ESCAPE &&
(!docked || !device.canDeviceFitSidebar)
) {
closeLibrary();
}
};
document.addEventListener(EVENT.KEYDOWN, handleKeyDown);
return () => {
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
};
}, [closeLibrary, docked, device.canDeviceFitSidebar]);
return (
<Island
{...rest}
className={clsx("sidebar", { "sidebar--docked": docked }, className)}
ref={islandRef}
>
<SidebarPropsContext.Provider value={headerPropsRef.current}>
{children}
</SidebarPropsContext.Provider>
</Island>
);
}, },
); );
SidebarInner.displayName = "SidebarInner";
export const Sidebar = Object.assign(
forwardRef((props: SidebarProps, ref: React.ForwardedRef<HTMLDivElement>) => {
const appState = useUIAppState();
const { onStateChange } = props;
const refPrevOpenSidebar = useRef(appState.openSidebar);
useEffect(() => {
if (
// closing sidebar
((!appState.openSidebar &&
refPrevOpenSidebar?.current?.name === props.name) ||
// opening current sidebar
(appState.openSidebar?.name === props.name &&
refPrevOpenSidebar?.current?.name !== props.name) ||
// switching tabs or switching to a different sidebar
refPrevOpenSidebar.current?.name === props.name) &&
appState.openSidebar !== refPrevOpenSidebar.current
) {
onStateChange?.(
appState.openSidebar?.name !== props.name
? null
: appState.openSidebar,
);
}
refPrevOpenSidebar.current = appState.openSidebar;
}, [appState.openSidebar, onStateChange, props.name]);
const [mounted, setMounted] = useState(false);
useLayoutEffect(() => {
setMounted(true);
return () => setMounted(false);
}, []);
// We want to render in the next tick (hence `mounted` flag) so that it's
// guaranteed to happen after unmount of the previous sidebar (in case the
// previous sidebar is mounted after the next one). This is necessary to
// prevent flicker of subcomponents that support fallbacks
// (e.g. SidebarHeader). This is because we're using flags to determine
// whether prefer the fallback component or not (otherwise both will render
// initially), and the flag won't be reset in time if the unmount order
// it not correct.
//
// Alternative, and more general solution would be to namespace the fallback
// HoC so that state is not shared between subcomponents when the wrapping
// component is of the same type (e.g. Sidebar -> SidebarHeader).
const shouldRender = mounted && appState.openSidebar?.name === props.name;
if (!shouldRender) {
return null;
}
return <SidebarInner {...props} ref={ref} key={props.name} />;
}),
{
Header: SidebarHeader,
TabTriggers: SidebarTabTriggers,
TabTrigger: SidebarTabTrigger,
Tabs: SidebarTabs,
Tab: SidebarTab,
Trigger: SidebarTrigger,
},
);
Sidebar.displayName = "Sidebar";

View file

@ -4,86 +4,54 @@ import { t } from "../../i18n";
import { useDevice } from "../App"; import { useDevice } from "../App";
import { SidebarPropsContext } from "./common"; import { SidebarPropsContext } from "./common";
import { CloseIcon, PinIcon } from "../icons"; import { CloseIcon, PinIcon } from "../icons";
import { withUpstreamOverride } from "../hoc/withUpstreamOverride";
import { Tooltip } from "../Tooltip"; import { Tooltip } from "../Tooltip";
import { Button } from "../Button";
export const SidebarDockButton = (props: { export const SidebarHeader = ({
checked: boolean; children,
onChange?(): void; className,
}) => { }: {
return (
<div className="layer-ui__sidebar-dock-button" data-testid="sidebar-dock">
<Tooltip label={t("labels.sidebarLock")}>
<label
className={clsx(
"ToolIcon ToolIcon__lock ToolIcon_type_floating",
`ToolIcon_size_medium`,
)}
>
<input
className="ToolIcon_type_checkbox"
type="checkbox"
onChange={props.onChange}
checked={props.checked}
aria-label={t("labels.sidebarLock")}
/>{" "}
<div
className={clsx("Sidebar__pin-btn", {
"Sidebar__pin-btn--pinned": props.checked,
})}
tabIndex={0}
>
{PinIcon}
</div>{" "}
</label>{" "}
</Tooltip>
</div>
);
};
const _SidebarHeader: React.FC<{
children?: React.ReactNode; children?: React.ReactNode;
className?: string; className?: string;
}> = ({ children, className }) => { }) => {
const device = useDevice(); const device = useDevice();
const props = useContext(SidebarPropsContext); const props = useContext(SidebarPropsContext);
const renderDockButton = !!(device.canDeviceFitSidebar && props.dockable); const renderDockButton = !!(
const renderCloseButton = !!props.onClose; device.canDeviceFitSidebar && props.shouldRenderDockButton
);
return ( return (
<div <div
className={clsx("layer-ui__sidebar__header", className)} className={clsx("sidebar__header", className)}
data-testid="sidebar-header" data-testid="sidebar-header"
> >
{children} {children}
{(renderDockButton || renderCloseButton) && ( <div className="sidebar__header__buttons">
<div className="layer-ui__sidebar__header__buttons"> {renderDockButton && (
{renderDockButton && ( <Tooltip label={t("labels.sidebarLock")}>
<SidebarDockButton <Button
checked={!!props.docked} onSelect={() => props.onDock?.(!props.docked)}
onChange={() => { selected={!!props.docked}
props.onDock?.(!props.docked); className="sidebar__dock"
}} data-testid="sidebar-dock"
/> aria-label={t("labels.sidebarLock")}
)}
{renderCloseButton && (
<button
data-testid="sidebar-close"
className="Sidebar__close-btn"
onClick={props.onClose}
aria-label={t("buttons.close")}
> >
{CloseIcon} {PinIcon}
</button> </Button>
)} </Tooltip>
</div> )}
)} <Button
data-testid="sidebar-close"
className="sidebar__close"
onSelect={props.onCloseRequest}
aria-label={t("buttons.close")}
>
{CloseIcon}
</Button>
</div>
</div> </div>
); );
}; };
const [Context, Component] = withUpstreamOverride(_SidebarHeader); SidebarHeader.displayName = "SidebarHeader";
/** @private */
export const SidebarHeaderComponents = { Context, Component };

View file

@ -0,0 +1,18 @@
import * as RadixTabs from "@radix-ui/react-tabs";
import { SidebarTabName } from "../../types";
export const SidebarTab = ({
tab,
children,
...rest
}: {
tab: SidebarTabName;
children: React.ReactNode;
} & React.HTMLAttributes<HTMLDivElement>) => {
return (
<RadixTabs.Content {...rest} value={tab}>
{children}
</RadixTabs.Content>
);
};
SidebarTab.displayName = "SidebarTab";

View file

@ -0,0 +1,26 @@
import * as RadixTabs from "@radix-ui/react-tabs";
import { SidebarTabName } from "../../types";
export const SidebarTabTrigger = ({
children,
tab,
onSelect,
...rest
}: {
children: React.ReactNode;
tab: SidebarTabName;
onSelect?: React.ReactEventHandler<HTMLButtonElement> | undefined;
} & Omit<React.HTMLAttributes<HTMLButtonElement>, "onSelect">) => {
return (
<RadixTabs.Trigger value={tab} asChild onSelect={onSelect}>
<button
type={"button"}
className={`excalidraw-button sidebar-tab-trigger`}
{...rest}
>
{children}
</button>
</RadixTabs.Trigger>
);
};
SidebarTabTrigger.displayName = "SidebarTabTrigger";

View file

@ -0,0 +1,16 @@
import * as RadixTabs from "@radix-ui/react-tabs";
export const SidebarTabTriggers = ({
children,
...rest
}: { children: React.ReactNode } & Omit<
React.RefAttributes<HTMLDivElement>,
"onSelect"
>) => {
return (
<RadixTabs.List className="sidebar-triggers" {...rest}>
{children}
</RadixTabs.List>
);
};
SidebarTabTriggers.displayName = "SidebarTabTriggers";

View file

@ -0,0 +1,36 @@
import * as RadixTabs from "@radix-ui/react-tabs";
import { useUIAppState } from "../../context/ui-appState";
import { useExcalidrawSetAppState } from "../App";
export const SidebarTabs = ({
children,
...rest
}: {
children: React.ReactNode;
} & Omit<React.RefAttributes<HTMLDivElement>, "onSelect">) => {
const appState = useUIAppState();
const setAppState = useExcalidrawSetAppState();
if (!appState.openSidebar) {
return null;
}
const { name } = appState.openSidebar;
return (
<RadixTabs.Root
className="sidebar-tabs-root"
value={appState.openSidebar.tab}
onValueChange={(tab) =>
setAppState((state) => ({
...state,
openSidebar: { ...state.openSidebar, name, tab },
}))
}
{...rest}
>
{children}
</RadixTabs.Root>
);
};
SidebarTabs.displayName = "SidebarTabs";

View file

@ -0,0 +1,34 @@
@import "../../css/variables.module";
.excalidraw {
.sidebar-trigger {
@include outlineButtonStyles;
background-color: var(--island-bg-color);
width: auto;
height: var(--lg-button-size);
display: flex;
align-items: center;
gap: 0.5rem;
line-height: 0;
font-size: 0.75rem;
letter-spacing: 0.4px;
svg {
width: var(--lg-icon-size);
height: var(--lg-icon-size);
}
}
.default-sidebar-trigger .sidebar-trigger__label {
display: none;
@media screen and (min-width: 1024px) {
display: block;
}
}
}

View file

@ -0,0 +1,45 @@
import { useExcalidrawSetAppState } from "../App";
import { SidebarTriggerProps } from "./common";
import { useUIAppState } from "../../context/ui-appState";
import clsx from "clsx";
import "./SidebarTrigger.scss";
export const SidebarTrigger = ({
name,
tab,
icon,
title,
children,
onToggle,
className,
style,
}: SidebarTriggerProps) => {
const setAppState = useExcalidrawSetAppState();
const appState = useUIAppState();
return (
<label title={title}>
<input
className="ToolIcon_type_checkbox"
type="checkbox"
onChange={(event) => {
document
.querySelector(".layer-ui__wrapper")
?.classList.remove("animate");
const isOpen = event.target.checked;
setAppState({ openSidebar: isOpen ? { name, tab } : null });
onToggle?.(isOpen);
}}
checked={appState.openSidebar?.name === name}
aria-label={title}
aria-keyshortcuts="0"
/>
<div className={clsx("sidebar-trigger", className)} style={style}>
{icon && <div>{icon}</div>}
{children && <div className="sidebar-trigger__label">{children}</div>}
</div>
</label>
);
};
SidebarTrigger.displayName = "SidebarTrigger";

View file

@ -1,23 +1,41 @@
import React from "react"; import React from "react";
import { AppState, SidebarName, SidebarTabName } from "../../types";
export type SidebarTriggerProps = {
name: SidebarName;
tab?: SidebarTabName;
icon?: JSX.Element;
children?: React.ReactNode;
title?: string;
className?: string;
onToggle?: (open: boolean) => void;
style?: React.CSSProperties;
};
export type SidebarProps<P = {}> = { export type SidebarProps<P = {}> = {
name: SidebarName;
children: React.ReactNode; children: React.ReactNode;
/** /**
* Called on sidebar close (either by user action or by the editor). * Called on sidebar open/close or tab change.
*/
onStateChange?: (state: AppState["openSidebar"]) => void;
/**
* supply alongside `docked` prop in order to make the Sidebar user-dockable
*/ */
onClose?: () => void | boolean;
/** if not supplied, sidebar won't be dockable */
onDock?: (docked: boolean) => void; onDock?: (docked: boolean) => void;
docked?: boolean; docked?: boolean;
initialDockedState?: boolean;
dockable?: boolean;
className?: string; className?: string;
// NOTE sidebars we use internally inside the editor must have this flag set.
// It indicates that this sidebar should have lower precedence over host
// sidebars, if both are open.
/** @private internal */
__fallback?: boolean;
} & P; } & P;
export type SidebarPropsContextValue = Pick< export type SidebarPropsContextValue = Pick<
SidebarProps, SidebarProps,
"onClose" | "onDock" | "docked" | "dockable" "onDock" | "docked"
>; > & { onCloseRequest: () => void; shouldRenderDockButton: boolean };
export const SidebarPropsContext = export const SidebarPropsContext =
React.createContext<SidebarPropsContextValue>({}); React.createContext<SidebarPropsContextValue>({} as SidebarPropsContextValue);

View file

@ -3,14 +3,14 @@ import { getCommonBounds } from "../element/bounds";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { getTargetElements } from "../scene"; import { getTargetElements } from "../scene";
import { AppState, ExcalidrawProps } from "../types"; import { ExcalidrawProps, UIAppState } from "../types";
import { CloseIcon } from "./icons"; import { CloseIcon } from "./icons";
import { Island } from "./Island"; import { Island } from "./Island";
import "./Stats.scss"; import "./Stats.scss";
export const Stats = (props: { export const Stats = (props: {
appState: AppState; appState: UIAppState;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, UIAppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
onClose: () => void; onClose: () => void;
renderCustomStats: ExcalidrawProps["renderCustomStats"]; renderCustomStats: ExcalidrawProps["renderCustomStats"];

View file

@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import * as Sentry from "@sentry/browser"; import * as Sentry from "@sentry/browser";
import { t } from "../i18n"; import { t } from "../i18n";
import Trans from "./Trans";
interface TopErrorBoundaryState { interface TopErrorBoundaryState {
hasError: boolean; hasError: boolean;
@ -74,25 +75,31 @@ export class TopErrorBoundary extends React.Component<
<div className="ErrorSplash excalidraw"> <div className="ErrorSplash excalidraw">
<div className="ErrorSplash-messageContainer"> <div className="ErrorSplash-messageContainer">
<div className="ErrorSplash-paragraph bigger align-center"> <div className="ErrorSplash-paragraph bigger align-center">
{t("errorSplash.headingMain_pre")} <Trans
<button onClick={() => window.location.reload()}> i18nKey="errorSplash.headingMain"
{t("errorSplash.headingMain_button")} button={(el) => (
</button> <button onClick={() => window.location.reload()}>{el}</button>
)}
/>
</div> </div>
<div className="ErrorSplash-paragraph align-center"> <div className="ErrorSplash-paragraph align-center">
{t("errorSplash.clearCanvasMessage")} <Trans
<button i18nKey="errorSplash.clearCanvasMessage"
onClick={() => { button={(el) => (
try { <button
localStorage.clear(); onClick={() => {
window.location.reload(); try {
} catch (error: any) { localStorage.clear();
console.error(error); window.location.reload();
} } catch (error: any) {
}} console.error(error);
> }
{t("errorSplash.clearCanvasMessage_button")} }}
</button> >
{el}
</button>
)}
/>
<br /> <br />
<div className="smaller"> <div className="smaller">
<span role="img" aria-label="warning"> <span role="img" aria-label="warning">
@ -106,16 +113,17 @@ export class TopErrorBoundary extends React.Component<
</div> </div>
<div> <div>
<div className="ErrorSplash-paragraph"> <div className="ErrorSplash-paragraph">
{t("errorSplash.trackedToSentry_pre")} {t("errorSplash.trackedToSentry", {
{this.state.sentryEventId} eventId: this.state.sentryEventId,
{t("errorSplash.trackedToSentry_post")} })}
</div> </div>
<div className="ErrorSplash-paragraph"> <div className="ErrorSplash-paragraph">
{t("errorSplash.openIssueMessage_pre")} <Trans
<button onClick={() => this.createGithubIssue()}> i18nKey="errorSplash.openIssueMessage"
{t("errorSplash.openIssueMessage_button")} button={(el) => (
</button> <button onClick={() => this.createGithubIssue()}>{el}</button>
{t("errorSplash.openIssueMessage_post")} )}
/>
</div> </div>
<div className="ErrorSplash-paragraph"> <div className="ErrorSplash-paragraph">
<div className="ErrorSplash-details"> <div className="ErrorSplash-details">

View file

@ -0,0 +1,67 @@
import { render } from "@testing-library/react";
import fallbackLangData from "../locales/en.json";
import Trans from "./Trans";
describe("Test <Trans/>", () => {
it("should translate the the strings correctly", () => {
//@ts-ignore
fallbackLangData.transTest = {
key1: "Hello {{audience}}",
key2: "Please <link>click the button</link> to continue.",
key3: "Please <link>click {{location}}</link> to continue.",
key4: "Please <link>click <bold>{{location}}</bold></link> to continue.",
key5: "Please <connect-link>click the button</connect-link> to continue.",
};
const { getByTestId } = render(
<>
<div data-testid="test1">
<Trans i18nKey="transTest.key1" audience="world" />
</div>
<div data-testid="test2">
<Trans
i18nKey="transTest.key2"
link={(el) => <a href="https://example.com">{el}</a>}
/>
</div>
<div data-testid="test3">
<Trans
i18nKey="transTest.key3"
link={(el) => <a href="https://example.com">{el}</a>}
location="the button"
/>
</div>
<div data-testid="test4">
<Trans
i18nKey="transTest.key4"
link={(el) => <a href="https://example.com">{el}</a>}
location="the button"
bold={(el) => <strong>{el}</strong>}
/>
</div>
<div data-testid="test5">
<Trans
i18nKey="transTest.key5"
connect-link={(el) => <a href="https://example.com">{el}</a>}
/>
</div>
</>,
);
expect(getByTestId("test1").innerHTML).toEqual("Hello world");
expect(getByTestId("test2").innerHTML).toEqual(
`Please <a href="https://example.com">click the button</a> to continue.`,
);
expect(getByTestId("test3").innerHTML).toEqual(
`Please <a href="https://example.com">click the button</a> to continue.`,
);
expect(getByTestId("test4").innerHTML).toEqual(
`Please <a href="https://example.com">click <strong>the button</strong></a> to continue.`,
);
expect(getByTestId("test5").innerHTML).toEqual(
`Please <a href="https://example.com">click the button</a> to continue.`,
);
});
});

169
src/components/Trans.tsx Normal file
View file

@ -0,0 +1,169 @@
import React from "react";
import { useI18n } from "../i18n";
// Used for splitting i18nKey into tokens in Trans component
// Example:
// "Please <link>click {{location}}</link> to continue.".split(SPLIT_REGEX).filter(Boolean)
// produces
// ["Please ", "<link>", "click ", "{{location}}", "</link>", " to continue."]
const SPLIT_REGEX = /({{[\w-]+}})|(<[\w-]+>)|(<\/[\w-]+>)/g;
// Used for extracting "location" from "{{location}}"
const KEY_REGEXP = /{{([\w-]+)}}/;
// Used for extracting "link" from "<link>"
const TAG_START_REGEXP = /<([\w-]+)>/;
// Used for extracting "link" from "</link>"
const TAG_END_REGEXP = /<\/([\w-]+)>/;
const getTransChildren = (
format: string,
props: {
[key: string]: React.ReactNode | ((el: React.ReactNode) => React.ReactNode);
},
): React.ReactNode[] => {
const stack: { name: string; children: React.ReactNode[] }[] = [
{
name: "",
children: [],
},
];
format
.split(SPLIT_REGEX)
.filter(Boolean)
.forEach((match) => {
const tagStartMatch = match.match(TAG_START_REGEXP);
const tagEndMatch = match.match(TAG_END_REGEXP);
const keyMatch = match.match(KEY_REGEXP);
if (tagStartMatch !== null) {
// The match is <tag>. Set the tag name as the name if it's one of the
// props, e.g. for "Please <link>click the button</link> to continue"
// tagStartMatch[1] = "link" and props contain "link" then it will be
// pushed to stack.
const name = tagStartMatch[1];
if (props.hasOwnProperty(name)) {
stack.push({
name,
children: [],
});
} else {
console.warn(
`Trans: missed to pass in prop ${name} for interpolating ${format}`,
);
}
} else if (tagEndMatch !== null) {
// If tag end match is found, this means we need to replace the content with
// its actual value in prop e.g. format = "Please <link>click the
// button</link> to continue", tagEndMatch is for "</link>", stack last item name =
// "link" and props.link = (el) => <a
// href="https://example.com">{el}</a> then its prop value will be
// pushed to "link"'s children so on DOM when rendering it's rendered as
// <a href="https://example.com">click the button</a>
const name = tagEndMatch[1];
if (name === stack[stack.length - 1].name) {
const item = stack.pop()!;
const itemChildren = React.createElement(
React.Fragment,
{},
...item.children,
);
const fn = props[item.name];
if (typeof fn === "function") {
stack[stack.length - 1].children.push(fn(itemChildren));
}
} else {
console.warn(
`Trans: unexpected end tag ${match} for interpolating ${format}`,
);
}
} else if (keyMatch !== null) {
// The match is for {{key}}. Check if the key is present in props and set
// the prop value as children of last stack item e.g. format = "Hello
// {{name}}", key = "name" and props.name = "Excalidraw" then its prop
// value will be pushed to "name"'s children so it's rendered on DOM as
// "Hello Excalidraw"
const name = keyMatch[1];
if (props.hasOwnProperty(name)) {
stack[stack.length - 1].children.push(props[name] as React.ReactNode);
} else {
console.warn(
`Trans: key ${name} not in props for interpolating ${format}`,
);
}
} else {
// If none of cases match means we just need to push the string
// to stack eg - "Hello {{name}} Whats up?" "Hello", "Whats up" will be pushed
stack[stack.length - 1].children.push(match);
}
});
if (stack.length !== 1) {
console.warn(`Trans: stack not empty for interpolating ${format}`);
}
return stack[0].children;
};
/*
Trans component is used for translating JSX.
```json
{
"example1": "Hello {{audience}}",
"example2": "Please <link>click the button</link> to continue.",
"example3": "Please <link>click {{location}}</link> to continue.",
"example4": "Please <link>click <bold>{{location}}</bold></link> to continue.",
}
```
```jsx
<Trans i18nKey="example1" audience="world" />
<Trans
i18nKey="example2"
connectLink={(el) => <a href="https://example.com">{el}</a>}
/>
<Trans
i18nKey="example3"
connectLink={(el) => <a href="https://example.com">{el}</a>}
location="the button"
/>
<Trans
i18nKey="example4"
connectLink={(el) => <a href="https://example.com">{el}</a>}
location="the button"
bold={(el) => <strong>{el}</strong>}
/>
```
Output:
```html
Hello world
Please <a href="https://example.com">click the button</a> to continue.
Please <a href="https://example.com">click the button</a> to continue.
Please <a href="https://example.com">click <strong>the button</strong></a> to continue.
```
*/
const Trans = ({
i18nKey,
children,
...props
}: {
i18nKey: string;
[key: string]: React.ReactNode | ((el: React.ReactNode) => React.ReactNode);
}) => {
const { t } = useI18n();
// This is needed to avoid unique key error in list which gets rendered from getTransChildren
return React.createElement(
React.Fragment,
{},
...getTransChildren(t(i18nKey), props),
);
};
export default Trans;

View file

@ -6,58 +6,45 @@ exports[`Test <App/> should show error modal when using brave and measureText AP
> >
<p> <p>
Looks like you are using Brave browser with the Looks like you are using Brave browser with the
 
<span <span
style="font-weight: 600;" style="font-weight: 600;"
> >
Aggressively Block Fingerprinting Aggressively Block Fingerprinting
</span> </span>
setting enabled.
setting enabled </p>
. <p>
<br />
<br />
This could result in breaking the This could result in breaking the
<span <span
style="font-weight: 600;" style="font-weight: 600;"
> >
Text Elements Text Elements
</span> </span>
in your drawings.
in your drawings
.
</p> </p>
<p> <p>
We strongly recommend disabling this setting. You can follow We strongly recommend disabling this setting. You can follow
<a <a
href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser" href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser"
> >
these steps these steps
</a> </a>
on how to do so.
on how to do so
.
</p> </p>
<p> <p>
If disabling this setting doesn't fix the display of text elements, please open an If disabling this setting doesn't fix the display of text elements, please open an
<a <a
href="https://github.com/excalidraw/excalidraw/issues/new" href="https://github.com/excalidraw/excalidraw/issues/new"
> >
issue issue
</a> </a>
on our GitHub, or write us on
on our GitHub, or write us on
<a <a
href="https://discord.gg/UexuTaE" href="https://discord.gg/UexuTaE"
> >
Discord Discord
.
</a> </a>
.
</p> </p>
</div> </div>
`; `;

View file

@ -1,32 +0,0 @@
import React from "react";
import tunnel from "@dwelle/tunnel-rat";
type Tunnel = ReturnType<typeof tunnel>;
type TunnelsContextValue = {
mainMenuTunnel: Tunnel;
welcomeScreenMenuHintTunnel: Tunnel;
welcomeScreenToolbarHintTunnel: Tunnel;
welcomeScreenHelpHintTunnel: Tunnel;
welcomeScreenCenterTunnel: Tunnel;
footerCenterTunnel: Tunnel;
jotaiScope: symbol;
};
export const TunnelsContext = React.createContext<TunnelsContextValue>(null!);
export const useTunnels = () => React.useContext(TunnelsContext);
export const useInitializeTunnels = () => {
return React.useMemo((): TunnelsContextValue => {
return {
mainMenuTunnel: tunnel(),
welcomeScreenMenuHintTunnel: tunnel(),
welcomeScreenToolbarHintTunnel: tunnel(),
welcomeScreenHelpHintTunnel: tunnel(),
welcomeScreenCenterTunnel: tunnel(),
footerCenterTunnel: tunnel(),
jotaiScope: Symbol(),
};
}, []);
};

View file

@ -1,4 +1,4 @@
import { useOutsideClickHook } from "../../hooks/useOutsideClick"; import { useOutsideClick } from "../../hooks/useOutsideClick";
import { Island } from "../Island"; import { Island } from "../Island";
import { useDevice } from "../App"; import { useDevice } from "../App";
@ -24,7 +24,7 @@ const MenuContent = ({
style?: React.CSSProperties; style?: React.CSSProperties;
}) => { }) => {
const device = useDevice(); const device = useDevice();
const menuRef = useOutsideClickHook(() => { const menuRef = useOutsideClick(() => {
onClickOutside?.(); onClickOutside?.();
}); });

View file

@ -1,5 +1,6 @@
import clsx from "clsx"; import clsx from "clsx";
import { useDevice, useExcalidrawAppState } from "../App"; import { useUIAppState } from "../../context/ui-appState";
import { useDevice } from "../App";
const MenuTrigger = ({ const MenuTrigger = ({
className = "", className = "",
@ -10,7 +11,7 @@ const MenuTrigger = ({
children: React.ReactNode; children: React.ReactNode;
onToggle: () => void; onToggle: () => void;
}) => { }) => {
const appState = useExcalidrawAppState(); const appState = useUIAppState();
const device = useDevice(); const device = useDevice();
const classNames = clsx( const classNames = clsx(
`dropdown-menu-button ${className}`, `dropdown-menu-button ${className}`,

View file

@ -1,7 +1,6 @@
import clsx from "clsx"; import clsx from "clsx";
import { actionShortcuts } from "../../actions"; import { actionShortcuts } from "../../actions";
import { ActionManager } from "../../actions/manager"; import { ActionManager } from "../../actions/manager";
import { AppState } from "../../types";
import { import {
ExitZenModeAction, ExitZenModeAction,
FinalizeAction, FinalizeAction,
@ -9,10 +8,11 @@ import {
ZoomActions, ZoomActions,
} from "../Actions"; } from "../Actions";
import { useDevice } from "../App"; import { useDevice } from "../App";
import { useTunnels } from "../context/tunnels"; import { useTunnels } from "../../context/tunnels";
import { HelpButton } from "../HelpButton"; import { HelpButton } from "../HelpButton";
import { Section } from "../Section"; import { Section } from "../Section";
import Stack from "../Stack"; import Stack from "../Stack";
import { UIAppState } from "../../types";
const Footer = ({ const Footer = ({
appState, appState,
@ -20,12 +20,12 @@ const Footer = ({
showExitZenModeBtn, showExitZenModeBtn,
renderWelcomeScreen, renderWelcomeScreen,
}: { }: {
appState: AppState; appState: UIAppState;
actionManager: ActionManager; actionManager: ActionManager;
showExitZenModeBtn: boolean; showExitZenModeBtn: boolean;
renderWelcomeScreen: boolean; renderWelcomeScreen: boolean;
}) => { }) => {
const { footerCenterTunnel, welcomeScreenHelpHintTunnel } = useTunnels(); const { FooterCenterTunnel, WelcomeScreenHelpHintTunnel } = useTunnels();
const device = useDevice(); const device = useDevice();
const showFinalize = const showFinalize =
@ -70,14 +70,14 @@ const Footer = ({
</Section> </Section>
</Stack.Col> </Stack.Col>
</div> </div>
<footerCenterTunnel.Out /> <FooterCenterTunnel.Out />
<div <div
className={clsx("layer-ui__wrapper__footer-right zen-mode-transition", { className={clsx("layer-ui__wrapper__footer-right zen-mode-transition", {
"transition-right disable-pointerEvents": appState.zenModeEnabled, "transition-right disable-pointerEvents": appState.zenModeEnabled,
})} })}
> >
<div style={{ position: "relative" }}> <div style={{ position: "relative" }}>
{renderWelcomeScreen && <welcomeScreenHelpHintTunnel.Out />} {renderWelcomeScreen && <WelcomeScreenHelpHintTunnel.Out />}
<HelpButton <HelpButton
onClick={() => actionManager.executeAction(actionShortcuts)} onClick={() => actionManager.executeAction(actionShortcuts)}
/> />

View file

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

View file

@ -1,32 +1,46 @@
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import React, { useLayoutEffect } from "react"; import React, { useLayoutEffect } from "react";
import { useTunnels } from "../context/tunnels"; import { useTunnels } from "../../context/tunnels";
export const withInternalFallback = <P,>( export const withInternalFallback = <P,>(
componentName: string, componentName: string,
Component: React.FC<P>, Component: React.FC<P>,
) => { ) => {
const counterAtom = atom(0); const renderAtom = atom(0);
// flag set on initial render to tell the fallback component to skip the // flag set on initial render to tell the fallback component to skip the
// render until mount counter are initialized. This is because the counter // render until mount counter are initialized. This is because the counter
// is initialized in an effect, and thus we could end rendering both // is initialized in an effect, and thus we could end rendering both
// components at the same time until counter is initialized. // components at the same time until counter is initialized.
let preferHost = false; let preferHost = false;
let counter = 0;
const WrapperComponent: React.FC< const WrapperComponent: React.FC<
P & { P & {
__fallback?: boolean; __fallback?: boolean;
} }
> = (props) => { > = (props) => {
const { jotaiScope } = useTunnels(); const { jotaiScope } = useTunnels();
const [counter, setCounter] = useAtom(counterAtom, jotaiScope); const [, setRender] = useAtom(renderAtom, jotaiScope);
useLayoutEffect(() => { useLayoutEffect(() => {
setCounter((counter) => counter + 1); setRender((c) => {
const next = c + 1;
counter = next;
return next;
});
return () => { return () => {
setCounter((counter) => counter - 1); setRender((c) => {
const next = c - 1;
counter = next;
if (!next) {
preferHost = false;
}
return next;
});
}; };
}, [setCounter]); }, [setRender]);
if (!props.__fallback) { if (!props.__fallback) {
preferHost = true; preferHost = true;

View file

@ -1,63 +0,0 @@
import React, {
useMemo,
useContext,
useLayoutEffect,
useState,
createContext,
} from "react";
export const withUpstreamOverride = <P,>(Component: React.ComponentType<P>) => {
type ContextValue = [boolean, React.Dispatch<React.SetStateAction<boolean>>];
const DefaultComponentContext = createContext<ContextValue>([
false,
() => {},
]);
const ComponentContext: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [isRenderedUpstream, setIsRenderedUpstream] = useState(false);
const contextValue: ContextValue = useMemo(
() => [isRenderedUpstream, setIsRenderedUpstream],
[isRenderedUpstream],
);
return (
<DefaultComponentContext.Provider value={contextValue}>
{children}
</DefaultComponentContext.Provider>
);
};
const DefaultComponent = (
props: P & {
// indicates whether component should render when not rendered upstream
/** @private internal */
__isFallback?: boolean;
},
) => {
const [isRenderedUpstream, setIsRenderedUpstream] = useContext(
DefaultComponentContext,
);
useLayoutEffect(() => {
if (!props.__isFallback) {
setIsRenderedUpstream(true);
return () => setIsRenderedUpstream(false);
}
}, [props.__isFallback, setIsRenderedUpstream]);
if (props.__isFallback && isRenderedUpstream) {
return null;
}
return <Component {...props} />;
};
if (Component.name) {
DefaultComponent.displayName = `${Component.name}_upstreamOverrideWrapper`;
ComponentContext.displayName = `${Component.name}_upstreamOverrideContextWrapper`;
}
return [ComponentContext, DefaultComponent] as const;
};

View file

@ -3,9 +3,9 @@ import { usersIcon } from "../icons";
import { Button } from "../Button"; import { Button } from "../Button";
import clsx from "clsx"; import clsx from "clsx";
import { useExcalidrawAppState } from "../App";
import "./LiveCollaborationTrigger.scss"; import "./LiveCollaborationTrigger.scss";
import { useUIAppState } from "../../context/ui-appState";
const LiveCollaborationTrigger = ({ const LiveCollaborationTrigger = ({
isCollaborating, isCollaborating,
@ -15,7 +15,7 @@ const LiveCollaborationTrigger = ({
isCollaborating: boolean; isCollaborating: boolean;
onSelect: () => void; onSelect: () => void;
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => { } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
const appState = useExcalidrawAppState(); const appState = useUIAppState();
return ( return (
<Button <Button

View file

@ -1,10 +1,6 @@
import { getShortcutFromShortcutName } from "../../actions/shortcuts"; import { getShortcutFromShortcutName } from "../../actions/shortcuts";
import { useI18n } from "../../i18n"; import { useI18n } from "../../i18n";
import { import { useExcalidrawSetAppState, useExcalidrawActionManager } from "../App";
useExcalidrawAppState,
useExcalidrawSetAppState,
useExcalidrawActionManager,
} from "../App";
import { import {
ExportIcon, ExportIcon,
ExportImageIcon, ExportImageIcon,
@ -32,6 +28,7 @@ import clsx from "clsx";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog"; import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
import { jotaiScope } from "../../jotai"; import { jotaiScope } from "../../jotai";
import { useUIAppState } from "../../context/ui-appState";
export const LoadScene = () => { export const LoadScene = () => {
const { t } = useI18n(); const { t } = useI18n();
@ -139,7 +136,7 @@ ClearCanvas.displayName = "ClearCanvas";
export const ToggleTheme = () => { export const ToggleTheme = () => {
const { t } = useI18n(); const { t } = useI18n();
const appState = useExcalidrawAppState(); const appState = useUIAppState();
const actionManager = useExcalidrawActionManager(); const actionManager = useExcalidrawActionManager();
if (!actionManager.isActionEnabled(actionToggleTheme)) { if (!actionManager.isActionEnabled(actionToggleTheme)) {
@ -172,7 +169,7 @@ ToggleTheme.displayName = "ToggleTheme";
export const ChangeCanvasBackground = () => { export const ChangeCanvasBackground = () => {
const { t } = useI18n(); const { t } = useI18n();
const appState = useExcalidrawAppState(); const appState = useUIAppState();
const actionManager = useExcalidrawActionManager(); const actionManager = useExcalidrawActionManager();
if (appState.viewModeEnabled) { if (appState.viewModeEnabled) {

View file

@ -1,9 +1,5 @@
import React from "react"; import React from "react";
import { import { useDevice, useExcalidrawSetAppState } from "../App";
useDevice,
useExcalidrawAppState,
useExcalidrawSetAppState,
} from "../App";
import DropdownMenu from "../dropdownMenu/DropdownMenu"; import DropdownMenu from "../dropdownMenu/DropdownMenu";
import * as DefaultItems from "./DefaultItems"; import * as DefaultItems from "./DefaultItems";
@ -13,7 +9,8 @@ import { t } from "../../i18n";
import { HamburgerMenuIcon } from "../icons"; import { HamburgerMenuIcon } from "../icons";
import { withInternalFallback } from "../hoc/withInternalFallback"; import { withInternalFallback } from "../hoc/withInternalFallback";
import { composeEventHandlers } from "../../utils"; import { composeEventHandlers } from "../../utils";
import { useTunnels } from "../context/tunnels"; import { useTunnels } from "../../context/tunnels";
import { useUIAppState } from "../../context/ui-appState";
const MainMenu = Object.assign( const MainMenu = Object.assign(
withInternalFallback( withInternalFallback(
@ -28,16 +25,16 @@ const MainMenu = Object.assign(
*/ */
onSelect?: (event: Event) => void; onSelect?: (event: Event) => void;
}) => { }) => {
const { mainMenuTunnel } = useTunnels(); const { MainMenuTunnel } = useTunnels();
const device = useDevice(); const device = useDevice();
const appState = useExcalidrawAppState(); const appState = useUIAppState();
const setAppState = useExcalidrawSetAppState(); const setAppState = useExcalidrawSetAppState();
const onClickOutside = device.isMobile const onClickOutside = device.isMobile
? undefined ? undefined
: () => setAppState({ openMenu: null }); : () => setAppState({ openMenu: null });
return ( return (
<mainMenuTunnel.In> <MainMenuTunnel.In>
<DropdownMenu open={appState.openMenu === "canvas"}> <DropdownMenu open={appState.openMenu === "canvas"}>
<DropdownMenu.Trigger <DropdownMenu.Trigger
onToggle={() => { onToggle={() => {
@ -66,7 +63,7 @@ const MainMenu = Object.assign(
)} )}
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu> </DropdownMenu>
</mainMenuTunnel.In> </MainMenuTunnel.In>
); );
}, },
), ),

View file

@ -1,13 +1,10 @@
import { actionLoadScene, actionShortcuts } from "../../actions"; import { actionLoadScene, actionShortcuts } from "../../actions";
import { getShortcutFromShortcutName } from "../../actions/shortcuts"; import { getShortcutFromShortcutName } from "../../actions/shortcuts";
import { t, useI18n } from "../../i18n"; import { t, useI18n } from "../../i18n";
import { import { useDevice, useExcalidrawActionManager } from "../App";
useDevice, import { useTunnels } from "../../context/tunnels";
useExcalidrawActionManager,
useExcalidrawAppState,
} from "../App";
import { useTunnels } from "../context/tunnels";
import { ExcalLogo, HelpIcon, LoadIcon, usersIcon } from "../icons"; import { ExcalLogo, HelpIcon, LoadIcon, usersIcon } from "../icons";
import { useUIAppState } from "../../context/ui-appState";
const WelcomeScreenMenuItemContent = ({ const WelcomeScreenMenuItemContent = ({
icon, icon,
@ -89,9 +86,9 @@ const WelcomeScreenMenuItemLink = ({
WelcomeScreenMenuItemLink.displayName = "WelcomeScreenMenuItemLink"; WelcomeScreenMenuItemLink.displayName = "WelcomeScreenMenuItemLink";
const Center = ({ children }: { children?: React.ReactNode }) => { const Center = ({ children }: { children?: React.ReactNode }) => {
const { welcomeScreenCenterTunnel } = useTunnels(); const { WelcomeScreenCenterTunnel } = useTunnels();
return ( return (
<welcomeScreenCenterTunnel.In> <WelcomeScreenCenterTunnel.In>
<div className="welcome-screen-center"> <div className="welcome-screen-center">
{children || ( {children || (
<> <>
@ -104,7 +101,7 @@ const Center = ({ children }: { children?: React.ReactNode }) => {
</> </>
)} )}
</div> </div>
</welcomeScreenCenterTunnel.In> </WelcomeScreenCenterTunnel.In>
); );
}; };
Center.displayName = "Center"; Center.displayName = "Center";
@ -148,7 +145,7 @@ const MenuItemHelp = () => {
MenuItemHelp.displayName = "MenuItemHelp"; MenuItemHelp.displayName = "MenuItemHelp";
const MenuItemLoadScene = () => { const MenuItemLoadScene = () => {
const appState = useExcalidrawAppState(); const appState = useUIAppState();
const actionManager = useExcalidrawActionManager(); const actionManager = useExcalidrawActionManager();
if (appState.viewModeEnabled) { if (appState.viewModeEnabled) {

View file

@ -1,5 +1,5 @@
import { t } from "../../i18n"; import { t } from "../../i18n";
import { useTunnels } from "../context/tunnels"; import { useTunnels } from "../../context/tunnels";
import { import {
WelcomeScreenHelpArrow, WelcomeScreenHelpArrow,
WelcomeScreenMenuArrow, WelcomeScreenMenuArrow,
@ -7,44 +7,44 @@ import {
} from "../icons"; } from "../icons";
const MenuHint = ({ children }: { children?: React.ReactNode }) => { const MenuHint = ({ children }: { children?: React.ReactNode }) => {
const { welcomeScreenMenuHintTunnel } = useTunnels(); const { WelcomeScreenMenuHintTunnel } = useTunnels();
return ( return (
<welcomeScreenMenuHintTunnel.In> <WelcomeScreenMenuHintTunnel.In>
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--menu"> <div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--menu">
{WelcomeScreenMenuArrow} {WelcomeScreenMenuArrow}
<div className="welcome-screen-decor-hint__label"> <div className="welcome-screen-decor-hint__label">
{children || t("welcomeScreen.defaults.menuHint")} {children || t("welcomeScreen.defaults.menuHint")}
</div> </div>
</div> </div>
</welcomeScreenMenuHintTunnel.In> </WelcomeScreenMenuHintTunnel.In>
); );
}; };
MenuHint.displayName = "MenuHint"; MenuHint.displayName = "MenuHint";
const ToolbarHint = ({ children }: { children?: React.ReactNode }) => { const ToolbarHint = ({ children }: { children?: React.ReactNode }) => {
const { welcomeScreenToolbarHintTunnel } = useTunnels(); const { WelcomeScreenToolbarHintTunnel } = useTunnels();
return ( return (
<welcomeScreenToolbarHintTunnel.In> <WelcomeScreenToolbarHintTunnel.In>
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--toolbar"> <div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--toolbar">
<div className="welcome-screen-decor-hint__label"> <div className="welcome-screen-decor-hint__label">
{children || t("welcomeScreen.defaults.toolbarHint")} {children || t("welcomeScreen.defaults.toolbarHint")}
</div> </div>
{WelcomeScreenTopToolbarArrow} {WelcomeScreenTopToolbarArrow}
</div> </div>
</welcomeScreenToolbarHintTunnel.In> </WelcomeScreenToolbarHintTunnel.In>
); );
}; };
ToolbarHint.displayName = "ToolbarHint"; ToolbarHint.displayName = "ToolbarHint";
const HelpHint = ({ children }: { children?: React.ReactNode }) => { const HelpHint = ({ children }: { children?: React.ReactNode }) => {
const { welcomeScreenHelpHintTunnel } = useTunnels(); const { WelcomeScreenHelpHintTunnel } = useTunnels();
return ( return (
<welcomeScreenHelpHintTunnel.In> <WelcomeScreenHelpHintTunnel.In>
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--help"> <div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--help">
<div>{children || t("welcomeScreen.defaults.helpHint")}</div> <div>{children || t("welcomeScreen.defaults.helpHint")}</div>
{WelcomeScreenHelpArrow} {WelcomeScreenHelpArrow}
</div> </div>
</welcomeScreenHelpHintTunnel.In> </WelcomeScreenHelpHintTunnel.In>
); );
}; };
HelpHint.displayName = "HelpHint"; HelpHint.displayName = "HelpHint";

View file

@ -275,3 +275,10 @@ export const DEFAULT_ELEMENT_PROPS: {
opacity: 100, opacity: 100,
locked: false, locked: false,
}; };
export const LIBRARY_SIDEBAR_TAB = "library";
export const DEFAULT_SIDEBAR = {
name: "default",
defaultTab: LIBRARY_SIDEBAR_TAB,
} as const;

36
src/context/tunnels.ts Normal file
View file

@ -0,0 +1,36 @@
import React from "react";
import tunnel from "tunnel-rat";
export type Tunnel = ReturnType<typeof tunnel>;
type TunnelsContextValue = {
MainMenuTunnel: Tunnel;
WelcomeScreenMenuHintTunnel: Tunnel;
WelcomeScreenToolbarHintTunnel: Tunnel;
WelcomeScreenHelpHintTunnel: Tunnel;
WelcomeScreenCenterTunnel: Tunnel;
FooterCenterTunnel: Tunnel;
DefaultSidebarTriggerTunnel: Tunnel;
DefaultSidebarTabTriggersTunnel: Tunnel;
jotaiScope: symbol;
};
export const TunnelsContext = React.createContext<TunnelsContextValue>(null!);
export const useTunnels = () => React.useContext(TunnelsContext);
export const useInitializeTunnels = () => {
return React.useMemo((): TunnelsContextValue => {
return {
MainMenuTunnel: tunnel(),
WelcomeScreenMenuHintTunnel: tunnel(),
WelcomeScreenToolbarHintTunnel: tunnel(),
WelcomeScreenHelpHintTunnel: tunnel(),
WelcomeScreenCenterTunnel: tunnel(),
FooterCenterTunnel: tunnel(),
DefaultSidebarTriggerTunnel: tunnel(),
DefaultSidebarTabTriggersTunnel: tunnel(),
jotaiScope: Symbol(),
};
}, []);
};

View file

@ -0,0 +1,5 @@
import React from "react";
import { UIAppState } from "../types";
export const UIAppStateContext = React.createContext<UIAppState>(null!);
export const useUIAppState = () => React.useContext(UIAppStateContext);

View file

@ -567,7 +567,7 @@
border-radius: 0; border-radius: 0;
} }
.library-button { .default-sidebar-trigger {
border: 0; border: 0;
} }
} }

View file

@ -78,10 +78,13 @@
--color-selection: #6965db; --color-selection: #6965db;
--color-icon-white: #{$oc-white};
--color-primary: #6965db; --color-primary: #6965db;
--color-primary-darker: #5b57d1; --color-primary-darker: #5b57d1;
--color-primary-darkest: #4a47b1; --color-primary-darkest: #4a47b1;
--color-primary-light: #e3e2fe; --color-primary-light: #e3e2fe;
--color-primary-light-darker: #d7d5ff;
--color-gray-10: #f5f5f5; --color-gray-10: #f5f5f5;
--color-gray-20: #ebebeb; --color-gray-20: #ebebeb;
@ -161,10 +164,13 @@
// will be inverted to a lighter color. // will be inverted to a lighter color.
--color-selection: #3530c4; --color-selection: #3530c4;
--color-icon-white: var(--color-gray-90);
--color-primary: #a8a5ff; --color-primary: #a8a5ff;
--color-primary-darker: #b2aeff; --color-primary-darker: #b2aeff;
--color-primary-darkest: #beb9ff; --color-primary-darkest: #beb9ff;
--color-primary-light: #4f4d6f; --color-primary-light: #4f4d6f;
--color-primary-light-darker: #43415e;
--color-text-warning: var(--color-gray-80); --color-text-warning: var(--color-gray-80);

View file

@ -72,7 +72,14 @@
&:hover { &:hover {
background-color: var(--button-hover-bg, var(--island-bg-color)); background-color: var(--button-hover-bg, var(--island-bg-color));
border-color: var(--button-hover-border, var(--default-border-color)); border-color: var(
--button-hover-border,
var(--button-border, var(--default-border-color))
);
color: var(
--button-hover-color,
var(--button-color, var(--text-primary-color, inherit))
);
} }
&:active { &:active {
@ -81,11 +88,14 @@
} }
&.active { &.active {
background-color: var(--color-primary-light); background-color: var(--button-selected-bg, var(--color-primary-light));
border-color: var(--color-primary-light); border-color: var(--button-selected-border, var(--color-primary-light));
&:hover { &:hover {
background-color: var(--color-primary-light); background-color: var(
--button-selected-hover-bg,
var(--color-primary-light)
);
} }
svg { svg {

View file

@ -14,7 +14,14 @@ import { getCommonBoundingBox } from "../element/bounds";
import { AbortError } from "../errors"; import { AbortError } from "../errors";
import { t } from "../i18n"; import { t } from "../i18n";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { URL_HASH_KEYS, URL_QUERY_KEYS, APP_NAME, EVENT } from "../constants"; import {
URL_HASH_KEYS,
URL_QUERY_KEYS,
APP_NAME,
EVENT,
DEFAULT_SIDEBAR,
LIBRARY_SIDEBAR_TAB,
} from "../constants";
export const libraryItemsAtom = atom<{ export const libraryItemsAtom = atom<{
status: "loading" | "loaded"; status: "loading" | "loaded";
@ -148,7 +155,9 @@ class Library {
defaultStatus?: "unpublished" | "published"; defaultStatus?: "unpublished" | "published";
}): Promise<LibraryItems> => { }): Promise<LibraryItems> => {
if (openLibraryMenu) { if (openLibraryMenu) {
this.app.setState({ openSidebar: "library" }); this.app.setState({
openSidebar: { name: DEFAULT_SIDEBAR.name, tab: LIBRARY_SIDEBAR_TAB },
});
} }
return this.setLibrary(() => { return this.setLibrary(() => {
@ -174,6 +183,13 @@ class Library {
}), }),
) )
) { ) {
if (prompt) {
// focus container if we've prompted. We focus conditionally
// lest `props.autoFocus` is disabled (in which case we should
// focus only on user action such as prompt confirm)
this.app.focusContainer();
}
if (merge) { if (merge) {
resolve(mergeLibraryItems(this.lastLibraryItems, nextItems)); resolve(mergeLibraryItems(this.lastLibraryItems, nextItems));
} else { } else {
@ -186,8 +202,6 @@ class Library {
reject(error); reject(error);
} }
}); });
}).finally(() => {
this.app.focusContainer();
}); });
}; };

View file

@ -27,6 +27,7 @@ import {
PRECEDING_ELEMENT_KEY, PRECEDING_ELEMENT_KEY,
FONT_FAMILY, FONT_FAMILY,
ROUNDNESS, ROUNDNESS,
DEFAULT_SIDEBAR,
} from "../constants"; } from "../constants";
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../appState";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
@ -440,21 +441,15 @@ const LegacyAppStateMigrations: {
defaultAppState: ReturnType<typeof getDefaultAppState>, defaultAppState: ReturnType<typeof getDefaultAppState>,
) => [LegacyAppState[K][1], AppState[LegacyAppState[K][1]]]; ) => [LegacyAppState[K][1], AppState[LegacyAppState[K][1]]];
} = { } = {
isLibraryOpen: (appState, defaultAppState) => { isSidebarDocked: (appState, defaultAppState) => {
return [ return [
"openSidebar", "defaultSidebarDockedPreference",
"isLibraryOpen" in appState appState.isSidebarDocked ??
? appState.isLibraryOpen coalesceAppStateValue(
? "library" "defaultSidebarDockedPreference",
: null appState,
: coalesceAppStateValue("openSidebar", appState, defaultAppState), defaultAppState,
]; ),
},
isLibraryMenuDocked: (appState, defaultAppState) => {
return [
"isSidebarDocked",
appState.isLibraryMenuDocked ??
coalesceAppStateValue("isSidebarDocked", appState, defaultAppState),
]; ];
}, },
}; };
@ -526,13 +521,10 @@ export const restoreAppState = (
: appState.zoom?.value : appState.zoom?.value
? appState.zoom ? appState.zoom
: defaultAppState.zoom, : defaultAppState.zoom,
// when sidebar docked and user left it open in last session,
// keep it open. If not docked, keep it closed irrespective of last state.
openSidebar: openSidebar:
nextAppState.openSidebar === "library" // string (legacy)
? nextAppState.isSidebarDocked typeof (appState.openSidebar as any as string) === "string"
? "library" ? { name: DEFAULT_SIDEBAR.name }
: null
: nextAppState.openSidebar, : nextAppState.openSidebar,
}; };
}; };

View file

@ -27,10 +27,8 @@ export interface ExportedDataState {
* Don't consume on its own. * Don't consume on its own.
*/ */
export type LegacyAppState = { export type LegacyAppState = {
/** @deprecated #5663 TODO remove 22-12-15 */ /** @deprecated #6213 TODO remove 23-06-01 */
isLibraryOpen: [boolean, "openSidebar"]; isSidebarDocked: [boolean, "defaultSidebarDockedPreference"];
/** @deprecated #5663 TODO remove 22-12-15 */
isLibraryMenuDocked: [boolean, "isSidebarDocked"];
}; };
export interface ImportedDataState { export interface ImportedDataState {

View file

@ -1,4 +1,4 @@
import { AppState, ExcalidrawProps, Point } from "../types"; import { AppState, ExcalidrawProps, Point, UIAppState } from "../types";
import { import {
getShortcutKey, getShortcutKey,
sceneCoordsToViewportCoords, sceneCoordsToViewportCoords,
@ -297,10 +297,11 @@ export const getContextMenuLabel = (
: "labels.link.create"; : "labels.link.create";
return label; return label;
}; };
export const getLinkHandleFromCoords = ( export const getLinkHandleFromCoords = (
[x1, y1, x2, y2]: Bounds, [x1, y1, x2, y2]: Bounds,
angle: number, angle: number,
appState: AppState, appState: UIAppState,
): [x: number, y: number, width: number, height: number] => { ): [x: number, y: number, width: number, height: number] => {
const size = DEFAULT_LINK_SIZE; const size = DEFAULT_LINK_SIZE;
const linkWidth = size / appState.zoom.value; const linkWidth = size / appState.zoom.value;

View file

@ -1,9 +1,9 @@
import { AppState } from "../types";
import { NonDeletedExcalidrawElement } from "./types"; import { NonDeletedExcalidrawElement } from "./types";
import { getSelectedElements } from "../scene"; import { getSelectedElements } from "../scene";
import { UIAppState } from "../types";
export const showSelectedShapeActions = ( export const showSelectedShapeActions = (
appState: AppState, appState: UIAppState,
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
) => ) =>
Boolean( Boolean(

View file

@ -7,8 +7,9 @@ import {
import { DEFAULT_VERSION } from "../constants"; import { DEFAULT_VERSION } from "../constants";
import { t } from "../i18n"; import { t } from "../i18n";
import { copyTextToSystemClipboard } from "../clipboard"; import { copyTextToSystemClipboard } from "../clipboard";
import { AppState } from "../types";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { UIAppState } from "../types";
type StorageSizes = { scene: number; total: number }; type StorageSizes = { scene: number; total: number };
const STORAGE_SIZE_TIMEOUT = 500; const STORAGE_SIZE_TIMEOUT = 500;
@ -23,7 +24,7 @@ const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
type Props = { type Props = {
setToast: (message: string) => void; setToast: (message: string) => void;
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
appState: AppState; appState: UIAppState;
}; };
const CustomStats = (props: Props) => { const CustomStats = (props: Props) => {
const [storageSizes, setStorageSizes] = useState<StorageSizes>({ const [storageSizes, setStorageSizes] = useState<StorageSizes>({

View file

@ -18,7 +18,7 @@ import { getFrame } from "../../utils";
const exportToExcalidrawPlus = async ( const exportToExcalidrawPlus = async (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
appState: AppState, appState: Partial<AppState>,
files: BinaryFiles, files: BinaryFiles,
) => { ) => {
const firebase = await loadFirebaseStorage(); const firebase = await loadFirebaseStorage();
@ -75,7 +75,7 @@ const exportToExcalidrawPlus = async (
export const ExportToExcalidrawPlus: React.FC<{ export const ExportToExcalidrawPlus: React.FC<{
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
appState: AppState; appState: Partial<AppState>;
files: BinaryFiles; files: BinaryFiles;
onError: (error: Error) => void; onError: (error: Error) => void;
}> = ({ elements, appState, files, onError }) => { }> = ({ elements, appState, files, onError }) => {

View file

@ -285,7 +285,7 @@ export const loadScene = async (
export const exportToBackend = async ( export const exportToBackend = async (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: Partial<AppState>,
files: BinaryFiles, files: BinaryFiles,
) => { ) => {
const encryptionKey = await generateEncryptionKey("string"); const encryptionKey = await generateEncryptionKey("string");

View file

@ -32,6 +32,7 @@ import {
ExcalidrawImperativeAPI, ExcalidrawImperativeAPI,
BinaryFiles, BinaryFiles,
ExcalidrawInitialDataState, ExcalidrawInitialDataState,
UIAppState,
} from "../types"; } from "../types";
import { import {
debounce, debounce,
@ -552,7 +553,7 @@ const ExcalidrawWrapper = () => {
const onExportToBackend = async ( const onExportToBackend = async (
exportedElements: readonly NonDeletedExcalidrawElement[], exportedElements: readonly NonDeletedExcalidrawElement[],
appState: AppState, appState: Partial<AppState>,
files: BinaryFiles, files: BinaryFiles,
canvas: HTMLCanvasElement | null, canvas: HTMLCanvasElement | null,
) => { ) => {
@ -583,7 +584,7 @@ const ExcalidrawWrapper = () => {
const renderCustomStats = ( const renderCustomStats = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
appState: AppState, appState: UIAppState,
) => { ) => {
return ( return (
<CustomStats <CustomStats

View file

@ -1,6 +1,6 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
export const useOutsideClickHook = (handler: (event: Event) => void) => { export const useOutsideClick = (handler: (event: Event) => void) => {
const ref = useRef(null); const ref = useRef(null);
useEffect( useEffect(

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "تعذر الحفظ في قاعدة البيانات. إذا استمرت المشاكل، يفضل أن تحفظ ملفك محليا كي لا تفقد عملك.", "collabSaveFailed": "تعذر الحفظ في قاعدة البيانات. إذا استمرت المشاكل، يفضل أن تحفظ ملفك محليا كي لا تفقد عملك.",
"collabSaveFailed_sizeExceeded": "تعذر الحفظ في قاعدة البيانات، يبدو أن القماش كبير للغاية، يفضّل حفظ الملف محليا كي لا تفقد عملك.", "collabSaveFailed_sizeExceeded": "تعذر الحفظ في قاعدة البيانات، يبدو أن القماش كبير للغاية، يفضّل حفظ الملف محليا كي لا تفقد عملك.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "", "line1": "",
"aggressive_block_fingerprint": "", "line2": "",
"setting_enabled": "", "line3": "",
"break": "", "line4": ""
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
} }
}, },
"toolBar": { "toolBar": {

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "", "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "", "line1": "",
"aggressive_block_fingerprint": "", "line2": "",
"setting_enabled": "", "line3": "",
"break": "", "line4": ""
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "Подсказка: пробвайте да приближите далечните елементи по-близко." "canvasTooBigTip": "Подсказка: пробвайте да приближите далечните елементи по-близко."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "Среща грешка. Опитайте ", "headingMain": "Среща грешка. Опитайте <button>презареждане на страницата.</button>",
"headingMain_button": "презареждане на страницата.", "clearCanvasMessage": "Ако презареждането не работи, опитайте <button>изчистване на платното.</button>",
"clearCanvasMessage": "Ако презареждането не работи, опитайте ",
"clearCanvasMessage_button": "изчистване на платното.",
"clearCanvasCaveat": " Това ще доведе до загуба на работа ", "clearCanvasCaveat": " Това ще доведе до загуба на работа ",
"trackedToSentry_pre": "Грешката с идентификатор ", "trackedToSentry": "Грешката с идентификатор {{eventId}} беше проследен в нашата система.",
"trackedToSentry_post": " беше проследен в нашата система.", "openIssueMessage": "Бяхме много предпазливи да не включите информацията за вашата сцена при грешката. Ако сцената ви не е частна, моля, помислете за последващи действия на нашата <button>тракер за грешки.</button> Моля, включете информация по-долу, като я копирате и добавите в GitHub.",
"openIssueMessage_pre": "Бяхме много предпазливи да не включите информацията за вашата сцена при грешката. Ако сцената ви не е частна, моля, помислете за последващи действия на нашата ",
"openIssueMessage_button": "тракер за грешки.",
"openIssueMessage_post": " Моля, включете информация по-долу, като я копирате и добавите в GitHub.",
"sceneContent": "Съдържание на сцената:" "sceneContent": "Съдържание на сцената:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "", "required": "",
"website": "" "website": ""
}, },
"noteDescription": { "noteDescription": "",
"pre": "", "noteGuidelines": "",
"link": "", "noteLicense": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "", "noteItems": "",
"atleastOneLibItem": "", "atleastOneLibItem": "",
"republishWarning": "" "republishWarning": ""
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "", "title": "",
"content": "", "content": ""
"link": ""
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "", "resetLibrary": "",

View file

@ -1,7 +1,7 @@
{ {
"labels": { "labels": {
"paste": "পেস্ট করুন", "paste": "পেস্ট করুন",
"pasteAsPlaintext": "", "pasteAsPlaintext": "প্লেইনটেক্সট হিসাবে পেস্ট করুন",
"pasteCharts": "চার্ট পেস্ট করুন", "pasteCharts": "চার্ট পেস্ট করুন",
"selectAll": "সবটা সিলেক্ট করুন", "selectAll": "সবটা সিলেক্ট করুন",
"multiSelect": "একাধিক সিলেক্ট করুন", "multiSelect": "একাধিক সিলেক্ট করুন",
@ -54,7 +54,7 @@
"veryLarge": "অনেক বড়", "veryLarge": "অনেক বড়",
"solid": "দৃঢ়", "solid": "দৃঢ়",
"hachure": "ভ্রুলেখা", "hachure": "ভ্রুলেখা",
"zigzag": "", "zigzag": "আঁকাবাঁকা",
"crossHatch": "ক্রস হ্যাচ", "crossHatch": "ক্রস হ্যাচ",
"thin": "পাতলা", "thin": "পাতলা",
"bold": "পুরু", "bold": "পুরু",
@ -73,7 +73,7 @@
"layers": "মাত্রা", "layers": "মাত্রা",
"actions": "ক্রিয়া", "actions": "ক্রিয়া",
"language": "ভাষা", "language": "ভাষা",
"liveCollaboration": "", "liveCollaboration": "সরাসরি পারস্পরিক সহযোগিতা...",
"duplicateSelection": "সদৃশ সিলেক্ট", "duplicateSelection": "সদৃশ সিলেক্ট",
"untitled": "অনামী", "untitled": "অনামী",
"name": "নাম", "name": "নাম",
@ -208,19 +208,10 @@
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "", "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "", "line1": "",
"aggressive_block_fingerprint": "", "line2": "",
"setting_enabled": "", "line3": "",
"break": "", "line4": ""
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "বিশেষ্য: দূরতম উপাদানগুলোকে একটু কাছাকাছি নিয়ে যাওয়ার চেষ্টা করুন।" "canvasTooBigTip": "বিশেষ্য: দূরতম উপাদানগুলোকে একটু কাছাকাছি নিয়ে যাওয়ার চেষ্টা করুন।"
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "একটি ত্রুটির সম্মুখীন হয়েছে৷ চেষ্টা করুন ", "headingMain": "একটি ত্রুটির সম্মুখীন হয়েছে৷ চেষ্টা করুন <button>পৃষ্ঠাটি পুনরায় লোড করার।</button>",
"headingMain_button": "পৃষ্ঠাটি পুনরায় লোড করার।", "clearCanvasMessage": "যদি পুনরায় লোড করা কাজ না করে, চেষ্টা করুন <button>ক্যানভাস পরিষ্কার করার।</button>",
"clearCanvasMessage": "যদি পুনরায় লোড করা কাজ না করে, চেষ্টা করুন ",
"clearCanvasMessage_button": "ক্যানভাস পরিষ্কার করার।",
"clearCanvasCaveat": " এর ফলে কাজের ক্ষতি হবে ", "clearCanvasCaveat": " এর ফলে কাজের ক্ষতি হবে ",
"trackedToSentry_pre": "ত্রুটি ", "trackedToSentry": "ত্রুটি {{eventId}} আমাদের সিস্টেমে ট্র্যাক করা হয়েছিল।",
"trackedToSentry_post": " আমাদের সিস্টেমে ট্র্যাক করা হয়েছিল।", "openIssueMessage": "আমরা ত্রুটিতে আপনার দৃশ্যের তথ্য অন্তর্ভুক্ত না করার জন্য খুব সতর্ক ছিলাম। আপনার দৃশ্য ব্যক্তিগত না হলে, আমাদের অনুসরণ করার কথা বিবেচনা করুন <button>ত্রুটি ইতিবৃত্ত।</button> অনুগ্রহ করে GitHub ইস্যুতে অনুলিপি এবং পেস্ট করে নীচের তথ্য অন্তর্ভুক্ত করুন।",
"openIssueMessage_pre": "আমরা ত্রুটিতে আপনার দৃশ্যের তথ্য অন্তর্ভুক্ত না করার জন্য খুব সতর্ক ছিলাম। আপনার দৃশ্য ব্যক্তিগত না হলে, আমাদের অনুসরণ করার কথা বিবেচনা করুন ",
"openIssueMessage_button": "ত্রুটি ইতিবৃত্ত।",
"openIssueMessage_post": " অনুগ্রহ করে GitHub ইস্যুতে অনুলিপি এবং পেস্ট করে নীচের তথ্য অন্তর্ভুক্ত করুন।",
"sceneContent": "দৃশ্য বিষয়বস্তু:" "sceneContent": "দৃশ্য বিষয়বস্তু:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "", "required": "",
"website": "" "website": ""
}, },
"noteDescription": { "noteDescription": "",
"pre": "", "noteGuidelines": "",
"link": "", "noteLicense": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "", "noteItems": "",
"atleastOneLibItem": "", "atleastOneLibItem": "",
"republishWarning": "" "republishWarning": ""
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "", "title": "",
"content": "", "content": ""
"link": ""
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "", "resetLibrary": "",

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "No s'ha pogut desar a la base de dades de fons. Si els problemes persisteixen, hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball.", "collabSaveFailed": "No s'ha pogut desar a la base de dades de fons. Si els problemes persisteixen, hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball.",
"collabSaveFailed_sizeExceeded": "No s'ha pogut desar a la base de dades de fons, sembla que el llenç és massa gran. Hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball.", "collabSaveFailed_sizeExceeded": "No s'ha pogut desar a la base de dades de fons, sembla que el llenç és massa gran. Hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "", "line1": "",
"aggressive_block_fingerprint": "", "line2": "",
"setting_enabled": "", "line3": "",
"break": "", "line4": ""
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "Consell: proveu dacostar una mica els elements més allunyats." "canvasTooBigTip": "Consell: proveu dacostar una mica els elements més allunyats."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "S'ha produït un error. Proveu ", "headingMain": "S'ha produït un error. Proveu <button>recarregar la pàgina.</button>",
"headingMain_button": "recarregar la pàgina.", "clearCanvasMessage": "Si la recàrrega no funciona, proveu <button>esborrar el llenç.</button>",
"clearCanvasMessage": "Si la recàrrega no funciona, proveu ",
"clearCanvasMessage_button": "esborrar el llenç.",
"clearCanvasCaveat": " Això resultarà en la pèrdua de feina ", "clearCanvasCaveat": " Això resultarà en la pèrdua de feina ",
"trackedToSentry_pre": "L'error amb l'identificador ", "trackedToSentry": "L'error amb l'identificador {{eventId}} s'ha rastrejat en el nostre sistema.",
"trackedToSentry_post": " s'ha rastrejat en el nostre sistema.", "openIssueMessage": "Anàvem amb molta cura de no incloure la informació de la vostra escena en l'error. Si l'escena no és privada, podeu fer-ne el seguiment al nostre <button>rastrejador d'errors.</button> Incloeu la informació a continuació copiant i enganxant a GitHub Issues.",
"openIssueMessage_pre": "Anàvem amb molta cura de no incloure la informació de la vostra escena en l'error. Si l'escena no és privada, podeu fer-ne el seguiment al nostre ",
"openIssueMessage_button": "rastrejador d'errors.",
"openIssueMessage_post": " Incloeu la informació a continuació copiant i enganxant a GitHub Issues.",
"sceneContent": "Contingut de l'escena:" "sceneContent": "Contingut de l'escena:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "Requerit", "required": "Requerit",
"website": "Introduïu una URL vàlida" "website": "Introduïu una URL vàlida"
}, },
"noteDescription": { "noteDescription": "Envieu la vostra biblioteca perquè sigui inclosa al <link>repositori públic</link>per tal que altres persones puguin fer-ne ús en els seus dibuixos.",
"pre": "Envieu la vostra biblioteca perquè sigui inclosa al ", "noteGuidelines": "La biblioteca ha de ser aprovada manualment. Si us plau, llegiu les <link>directrius</link> abans d'enviar-hi res. Necessitareu un compte de GitHub per a comunicar i fer-hi canvis si cal, però no és requisit imprescindible.",
"link": "repositori públic", "noteLicense": "Quan l'envieu, accepteu que la biblioteca sigui publicada sota la <link>llicència MIT, </link>que, en resum, vol dir que qualsevol persona pot fer-ne ús sense restriccions.",
"post": "per tal que altres persones puguin fer-ne ús en els seus dibuixos."
},
"noteGuidelines": {
"pre": "La biblioteca ha de ser aprovada manualment. Si us plau, llegiu les ",
"link": "directrius",
"post": " abans d'enviar-hi res. Necessitareu un compte de GitHub per a comunicar i fer-hi canvis si cal, però no és requisit imprescindible."
},
"noteLicense": {
"pre": "Quan l'envieu, accepteu que la biblioteca sigui publicada sota la ",
"link": "llicència MIT, ",
"post": "que, en resum, vol dir que qualsevol persona pot fer-ne ús sense restriccions."
},
"noteItems": "Cada element de la biblioteca ha de tenir el seu propi nom per tal que sigui filtrable. S'hi inclouran els elements següents:", "noteItems": "Cada element de la biblioteca ha de tenir el seu propi nom per tal que sigui filtrable. S'hi inclouran els elements següents:",
"atleastOneLibItem": "Si us plau, seleccioneu si més no un element de la biblioteca per a començar", "atleastOneLibItem": "Si us plau, seleccioneu si més no un element de la biblioteca per a començar",
"republishWarning": "Nota: alguns dels elements seleccionats s'han marcat com a publicats/enviats. Només hauríeu de reenviar elements quan actualitzeu una biblioteca existent." "republishWarning": "Nota: alguns dels elements seleccionats s'han marcat com a publicats/enviats. Només hauríeu de reenviar elements quan actualitzeu una biblioteca existent."
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "Biblioteca enviada", "title": "Biblioteca enviada",
"content": "Gràcies, {{authorName}}. La vostra biblioteca ha estat enviada per a ser revisada. Podeu comprovar-ne l'estat", "content": "Gràcies, {{authorName}}. La vostra biblioteca ha estat enviada per a ser revisada. Podeu comprovar-ne l'estat<link>aquí</link>"
"link": "aquí"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "Restableix la biblioteca", "resetLibrary": "Restableix la biblioteca",

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "", "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "", "line1": "",
"aggressive_block_fingerprint": "", "line2": "",
"setting_enabled": "", "line3": "",
"break": "", "line4": ""
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "" "canvasTooBigTip": ""
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "", "headingMain": "",
"headingMain_button": "",
"clearCanvasMessage": "", "clearCanvasMessage": "",
"clearCanvasMessage_button": "",
"clearCanvasCaveat": "", "clearCanvasCaveat": "",
"trackedToSentry_pre": "Chyba identifikátoru ", "trackedToSentry": "Chyba identifikátoru {{eventId}} byl zaznamenán v našem systému.",
"trackedToSentry_post": " byl zaznamenán v našem systému.", "openIssueMessage": "",
"openIssueMessage_pre": "",
"openIssueMessage_button": "",
"openIssueMessage_post": "",
"sceneContent": "" "sceneContent": ""
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "Povinné", "required": "Povinné",
"website": "Zadejte platnou URL adresu" "website": "Zadejte platnou URL adresu"
}, },
"noteDescription": { "noteDescription": "Odešlete svou knihovnu, pro zařazení do <link>veřejného úložiště knihoven</link>, odkud ji budou moci při kreslení využít i ostatní uživatelé.",
"pre": "Odešlete svou knihovnu, pro zařazení do ", "noteGuidelines": "Knihovna musí být nejdříve ručně schválena. Přečtěte si prosím <link>pokyny</link>",
"link": "veřejného úložiště knihoven", "noteLicense": "",
"post": ", odkud ji budou moci při kreslení využít i ostatní uživatelé."
},
"noteGuidelines": {
"pre": "Knihovna musí být nejdříve ručně schválena. Přečtěte si prosím ",
"link": "pokyny",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "", "noteItems": "",
"atleastOneLibItem": "", "atleastOneLibItem": "",
"republishWarning": "" "republishWarning": ""
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "Knihovna byla odeslána", "title": "Knihovna byla odeslána",
"content": "", "content": ""
"link": ""
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "", "resetLibrary": "",

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "", "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "", "line1": "",
"aggressive_block_fingerprint": "", "line2": "",
"setting_enabled": "", "line3": "",
"break": "", "line4": ""
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "" "canvasTooBigTip": ""
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "", "headingMain": "",
"headingMain_button": "",
"clearCanvasMessage": "", "clearCanvasMessage": "",
"clearCanvasMessage_button": "",
"clearCanvasCaveat": "", "clearCanvasCaveat": "",
"trackedToSentry_pre": "", "trackedToSentry": "",
"trackedToSentry_post": "", "openIssueMessage": "<button></button> Kopiere og indsæt venligst oplysningerne nedenfor i et GitHub problem.",
"openIssueMessage_pre": "",
"openIssueMessage_button": "",
"openIssueMessage_post": " Kopiere og indsæt venligst oplysningerne nedenfor i et GitHub problem.",
"sceneContent": "Scene indhold:" "sceneContent": "Scene indhold:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "", "required": "",
"website": "" "website": ""
}, },
"noteDescription": { "noteDescription": "",
"pre": "", "noteGuidelines": "",
"link": "", "noteLicense": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "", "noteItems": "",
"atleastOneLibItem": "", "atleastOneLibItem": "",
"republishWarning": "" "republishWarning": ""
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "", "title": "",
"content": "", "content": ""
"link": ""
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "", "resetLibrary": "",

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "Keine Speicherung in der Backend-Datenbank möglich. Wenn die Probleme weiterhin bestehen, solltest Du Deine Datei lokal speichern, um sicherzustellen, dass Du Deine Arbeit nicht verlierst.", "collabSaveFailed": "Keine Speicherung in der Backend-Datenbank möglich. Wenn die Probleme weiterhin bestehen, solltest Du Deine Datei lokal speichern, um sicherzustellen, dass Du Deine Arbeit nicht verlierst.",
"collabSaveFailed_sizeExceeded": "Keine Speicherung in der Backend-Datenbank möglich, die Zeichenfläche scheint zu groß zu sein. Du solltest Deine Datei lokal speichern, um sicherzustellen, dass Du Deine Arbeit nicht verlierst.", "collabSaveFailed_sizeExceeded": "Keine Speicherung in der Backend-Datenbank möglich, die Zeichenfläche scheint zu groß zu sein. Du solltest Deine Datei lokal speichern, um sicherzustellen, dass Du Deine Arbeit nicht verlierst.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "Sieht so aus, als ob du den Brave Browser benutzt mit der", "line1": "Sieht so aus, als ob Du den Brave-Browser verwendest und die <bold>aggressive Blockierung von Fingerabdrücken</bold> aktiviert hast.",
"aggressive_block_fingerprint": "\"Fingerprinting aggressiv blockieren\"", "line2": "Dies könnte dazu führen, dass die <bold>Textelemente</bold> in Ihren Zeichnungen zerstört werden.",
"setting_enabled": "Einstellung aktiviert", "line3": "Wir empfehlen dringend, diese Einstellung zu deaktivieren. Dazu kannst Du <link>diesen Schritten</link> folgen.",
"break": "Dies könnte zur inkorrekten Darstellung der", "line4": "Wenn die Deaktivierung dieser Einstellung die fehlerhafte Anzeige von Textelementen nicht behebt, öffne bitte ein <issueLink>Ticket</issueLink> auf unserem GitHub oder schreibe uns auf <discordLink>Discord</discordLink>"
"text_elements": "Textelemente",
"in_your_drawings": "in deinen Zeichnungen führen",
"strongly_recommend": "Wir empfehlen dringend, diese Einstellung zu deaktivieren. Du kannst",
"steps": "diesen Schritten entsprechend",
"how": "folgen",
"disable_setting": " Wenn die Deaktivierung dieser Einstellung nicht zu einer korrekten Textdarstellung führt, öffne bitte einen",
"issue": "Issue",
"write": "auf GitHub, oder schreibe uns auf",
"discord": "Discord"
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "Tipp: Schiebe die am weitesten entfernten Elemente ein wenig näher zusammen." "canvasTooBigTip": "Tipp: Schiebe die am weitesten entfernten Elemente ein wenig näher zusammen."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "Es ist ein Fehler aufgetreten. Versuche ", "headingMain": "Es ist ein Fehler aufgetreten. Versuche <button>die Seite neu zu laden.</button>",
"headingMain_button": "die Seite neu zu laden.", "clearCanvasMessage": "Wenn das Neuladen nicht funktioniert, versuche <button>die Zeichenfläche zu löschen.</button>",
"clearCanvasMessage": "Wenn das Neuladen nicht funktioniert, versuche ",
"clearCanvasMessage_button": "die Zeichenfläche zu löschen.",
"clearCanvasCaveat": " Dies wird zum Verlust von Daten führen ", "clearCanvasCaveat": " Dies wird zum Verlust von Daten führen ",
"trackedToSentry_pre": "Der Fehler mit der Kennung ", "trackedToSentry": "Der Fehler mit der Kennung {{eventId}} wurde in unserem System registriert.",
"trackedToSentry_post": " wurde in unserem System registriert.", "openIssueMessage": "Wir waren sehr vorsichtig und haben deine Zeichnungsinformationen nicht in die Fehlerinformationen aufgenommen. Wenn deine Zeichnung nicht privat ist, unterstütze uns bitte über unseren <button>Bug-Tracker.</button> Bitte teile die unten stehenden Informationen mit uns im GitHub Issue (Kopieren und Einfügen).",
"openIssueMessage_pre": "Wir waren sehr vorsichtig und haben deine Zeichnungsinformationen nicht in die Fehlerinformationen aufgenommen. Wenn deine Zeichnung nicht privat ist, unterstütze uns bitte über unseren ",
"openIssueMessage_button": "Bug-Tracker.",
"openIssueMessage_post": " Bitte teile die unten stehenden Informationen mit uns im GitHub Issue (Kopieren und Einfügen).",
"sceneContent": "Zeichnungsinhalt:" "sceneContent": "Zeichnungsinhalt:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "Erforderlich", "required": "Erforderlich",
"website": "Gültige URL eingeben" "website": "Gültige URL eingeben"
}, },
"noteDescription": { "noteDescription": "Sende deine Bibliothek ein, um in die <link>öffentliche Bibliotheks-Repository aufgenommen zu werden</link>damit andere Nutzer sie in ihren Zeichnungen verwenden können.",
"pre": "Sende deine Bibliothek ein, um in die ", "noteGuidelines": "Die Bibliothek muss zuerst manuell freigegeben werden. Bitte lies die <link>Richtlinien</link> vor dem Absenden. Du benötigst ein GitHub-Konto, um zu kommunizieren und Änderungen vorzunehmen, falls erforderlich, aber es ist nicht unbedingt erforderlich.",
"link": "öffentliche Bibliotheks-Repository aufgenommen zu werden", "noteLicense": "Mit dem Absenden stimmst du zu, dass die Bibliothek unter der <link>MIT-Lizenz, </link>die zusammengefasst beinhaltet, dass jeder sie ohne Einschränkungen nutzen kann.",
"post": "damit andere Nutzer sie in ihren Zeichnungen verwenden können."
},
"noteGuidelines": {
"pre": "Die Bibliothek muss zuerst manuell freigegeben werden. Bitte lies die ",
"link": "Richtlinien",
"post": " vor dem Absenden. Du benötigst ein GitHub-Konto, um zu kommunizieren und Änderungen vorzunehmen, falls erforderlich, aber es ist nicht unbedingt erforderlich."
},
"noteLicense": {
"pre": "Mit dem Absenden stimmst du zu, dass die Bibliothek unter der ",
"link": "MIT-Lizenz, ",
"post": "die zusammengefasst beinhaltet, dass jeder sie ohne Einschränkungen nutzen kann."
},
"noteItems": "Jedes Bibliothekselement muss einen eigenen Namen haben, damit es gefiltert werden kann. Die folgenden Bibliothekselemente werden hinzugefügt:", "noteItems": "Jedes Bibliothekselement muss einen eigenen Namen haben, damit es gefiltert werden kann. Die folgenden Bibliothekselemente werden hinzugefügt:",
"atleastOneLibItem": "Bitte wähle mindestens ein Bibliothekselement aus, um zu beginnen", "atleastOneLibItem": "Bitte wähle mindestens ein Bibliothekselement aus, um zu beginnen",
"republishWarning": "Hinweis: Einige der ausgewählten Elemente sind bereits als veröffentlicht/eingereicht markiert. Du solltest Elemente nur erneut einreichen, wenn Du eine existierende Bibliothek oder Einreichung aktualisierst." "republishWarning": "Hinweis: Einige der ausgewählten Elemente sind bereits als veröffentlicht/eingereicht markiert. Du solltest Elemente nur erneut einreichen, wenn Du eine existierende Bibliothek oder Einreichung aktualisierst."
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "Bibliothek übermittelt", "title": "Bibliothek übermittelt",
"content": "Vielen Dank {{authorName}}. Deine Bibliothek wurde zur Überprüfung eingereicht. Du kannst den Status verfolgen", "content": "Vielen Dank {{authorName}}. Deine Bibliothek wurde zur Überprüfung eingereicht. Du kannst den Status verfolgen<link>hier</link>"
"link": "hier"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "Bibliothek zurücksetzen", "resetLibrary": "Bibliothek zurücksetzen",

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή. Αν το προβλήματα παραμείνει, θα πρέπει να αποθηκεύσετε το αρχείο σας τοπικά για να βεβαιωθείτε ότι δεν χάνετε την εργασία σας.", "collabSaveFailed": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή. Αν το προβλήματα παραμείνει, θα πρέπει να αποθηκεύσετε το αρχείο σας τοπικά για να βεβαιωθείτε ότι δεν χάνετε την εργασία σας.",
"collabSaveFailed_sizeExceeded": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή, ο καμβάς φαίνεται να είναι πολύ μεγάλος. Θα πρέπει να αποθηκεύσετε το αρχείο τοπικά για να βεβαιωθείτε ότι δεν θα χάσετε την εργασία σας.", "collabSaveFailed_sizeExceeded": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή, ο καμβάς φαίνεται να είναι πολύ μεγάλος. Θα πρέπει να αποθηκεύσετε το αρχείο τοπικά για να βεβαιωθείτε ότι δεν θα χάσετε την εργασία σας.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "Φαίνεται ότι χρησιμοποιείτε το Brave browser με το", "line1": "",
"aggressive_block_fingerprint": "Αποκλεισμός \"Δακτυλικών Αποτυπωμάτων\"", "line2": "",
"setting_enabled": "ρύθμιση ενεργοποιημένη", "line3": "",
"break": "Αυτό θα μπορούσε να σπάσει το", "line4": ""
"text_elements": "Στοιχεία Κειμένου",
"in_your_drawings": "στα σχέδιά σας",
"strongly_recommend": "Συνιστούμε να απενεργοποιήσετε αυτή τη ρύθμιση. Μπορείτε να ακολουθήσετε",
"steps": "αυτά τα βήματα",
"how": "για το πώς να το κάνετε",
"disable_setting": " Εάν η απενεργοποίηση αυτής της ρύθμισης δεν διορθώνει την εμφάνιση των στοιχείων κειμένου, παρακαλώ ανοίξτε ένα",
"issue": "πρόβλημα",
"write": "στο GitHub, ή γράψτε μας στο",
"discord": "Discord"
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "Συμβουλή: προσπαθήστε να μετακινήσετε τα πιο απομακρυσμένα στοιχεία λίγο πιο κοντά μαζί." "canvasTooBigTip": "Συμβουλή: προσπαθήστε να μετακινήσετε τα πιο απομακρυσμένα στοιχεία λίγο πιο κοντά μαζί."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "Συνέβη κάποιο σφάλμα. Προσπάθησε ", "headingMain": "Συνέβη κάποιο σφάλμα. Προσπάθησε <button>φόρτωσε ξανά την σελίδα.</button>",
"headingMain_button": "φόρτωσε ξανά την σελίδα.", "clearCanvasMessage": "Εάν το παραπάνω δεν δουλέψει, προσπάθησε <button>καθαρίσετε τον κανβά.</button>",
"clearCanvasMessage": "Εάν το παραπάνω δεν δουλέψει, προσπάθησε ",
"clearCanvasMessage_button": "καθαρίσετε τον κανβά.",
"clearCanvasCaveat": " Αυτό θα προκαλέσει απώλεια της δουλειάς σου ", "clearCanvasCaveat": " Αυτό θα προκαλέσει απώλεια της δουλειάς σου ",
"trackedToSentry_pre": "Το σφάλμα με αναγνωριστικό ", "trackedToSentry": "Το σφάλμα με αναγνωριστικό {{eventId}} παρακολουθήθηκε στο σύστημά μας.",
"trackedToSentry_post": " παρακολουθήθηκε στο σύστημά μας.", "openIssueMessage": "Ήμασταν πολύ προσεκτικοί για να μην συμπεριλάβουμε τις πληροφορίες της σκηνής σου στο σφάλμα. Αν η σκηνή σου δεν είναι ιδιωτική, παρακαλώ σκέψου να ακολουθήσεις το δικό μας <button>ανιχνευτής σφαλμάτων.</button> Παρακαλώ να συμπεριλάβετε τις παρακάτω πληροφορίες, αντιγράφοντας και επικολλώντας το ζήτημα στο GitHub.",
"openIssueMessage_pre": "Ήμασταν πολύ προσεκτικοί για να μην συμπεριλάβουμε τις πληροφορίες της σκηνής σου στο σφάλμα. Αν η σκηνή σου δεν είναι ιδιωτική, παρακαλώ σκέψου να ακολουθήσεις το δικό μας ",
"openIssueMessage_button": "ανιχνευτής σφαλμάτων.",
"openIssueMessage_post": " Παρακαλώ να συμπεριλάβετε τις παρακάτω πληροφορίες, αντιγράφοντας και επικολλώντας το ζήτημα στο GitHub.",
"sceneContent": "Περιεχόμενο σκηνής:" "sceneContent": "Περιεχόμενο σκηνής:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "Απαιτείται", "required": "Απαιτείται",
"website": "Εισάγετε μια έγκυρη διεύθυνση URL" "website": "Εισάγετε μια έγκυρη διεύθυνση URL"
}, },
"noteDescription": { "noteDescription": "Υποβάλετε τη βιβλιοθήκη σας για να συμπεριληφθεί στο <link>δημόσιο αποθετήριο βιβλιοθήκης</link>ώστε να χρησιμοποιηθεί από άλλα άτομα στα σχέδιά τους.",
"pre": "Υποβάλετε τη βιβλιοθήκη σας για να συμπεριληφθεί στο ", "noteGuidelines": "Η βιβλιοθήκη πρέπει πρώτα να εγκριθεί χειροκίνητα. Παρακαλώ διαβάστε τους <link>οδηγίες</link> πριν την υποβολή. Θα χρειαστείτε έναν λογαριασμό GitHub για την επικοινωνία και για να προβείτε σε αλλαγές εφ' όσον χρειαστεί, αλλά δεν είναι αυστηρή απαίτηση.",
"link": "δημόσιο αποθετήριο βιβλιοθήκης", "noteLicense": "Με την υποβολή, συμφωνείτε ότι η βιβλιοθήκη θα δημοσιευθεί υπό την <link>Άδεια MIT, </link>που εν συντομία σημαίνει ότι ο καθένας μπορεί να τα χρησιμοποιήσει χωρίς περιορισμούς.",
"post": "ώστε να χρησιμοποιηθεί από άλλα άτομα στα σχέδιά τους."
},
"noteGuidelines": {
"pre": "Η βιβλιοθήκη πρέπει πρώτα να εγκριθεί χειροκίνητα. Παρακαλώ διαβάστε τους ",
"link": "οδηγίες",
"post": " πριν την υποβολή. Θα χρειαστείτε έναν λογαριασμό GitHub για την επικοινωνία και για να προβείτε σε αλλαγές εφ' όσον χρειαστεί, αλλά δεν είναι αυστηρή απαίτηση."
},
"noteLicense": {
"pre": "Με την υποβολή, συμφωνείτε ότι η βιβλιοθήκη θα δημοσιευθεί υπό την ",
"link": "Άδεια MIT, ",
"post": "που εν συντομία σημαίνει ότι ο καθένας μπορεί να τα χρησιμοποιήσει χωρίς περιορισμούς."
},
"noteItems": "Κάθε αντικείμενο της βιβλιοθήκης πρέπει να έχει το δικό του όνομα ώστε να μπορεί να φιλτραριστεί. Θα συμπεριληφθούν τα ακόλουθα αντικείμενα βιβλιοθήκης:", "noteItems": "Κάθε αντικείμενο της βιβλιοθήκης πρέπει να έχει το δικό του όνομα ώστε να μπορεί να φιλτραριστεί. Θα συμπεριληφθούν τα ακόλουθα αντικείμενα βιβλιοθήκης:",
"atleastOneLibItem": "Παρακαλώ επιλέξτε τουλάχιστον ένα αντικείμενο βιβλιοθήκης για να ξεκινήσετε", "atleastOneLibItem": "Παρακαλώ επιλέξτε τουλάχιστον ένα αντικείμενο βιβλιοθήκης για να ξεκινήσετε",
"republishWarning": "Σημείωση: μερικά από τα επιλεγμένα αντικέιμενα έχουν ήδη επισημανθεί ως δημοσιευμένα/υποβεβλημένα. Θα πρέπει να υποβάλετε αντικείμενα εκ νέου μόνο για να ενημερώσετε μία ήδη υπάρχουσα βιβλιοθήκη ή υποβολή." "republishWarning": "Σημείωση: μερικά από τα επιλεγμένα αντικέιμενα έχουν ήδη επισημανθεί ως δημοσιευμένα/υποβεβλημένα. Θα πρέπει να υποβάλετε αντικείμενα εκ νέου μόνο για να ενημερώσετε μία ήδη υπάρχουσα βιβλιοθήκη ή υποβολή."
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "Η βιβλιοθήκη υποβλήθηκε", "title": "Η βιβλιοθήκη υποβλήθηκε",
"content": "Ευχαριστούμε {{authorName}}. Η βιβλιοθήκη σας έχει υποβληθεί για αξιολόγηση. Μπορείτε να παρακολουθείτε τη διαδικασία", "content": "Ευχαριστούμε {{authorName}}. Η βιβλιοθήκη σας έχει υποβληθεί για αξιολόγηση. Μπορείτε να παρακολουθείτε τη διαδικασία<link>εδώ</link>"
"link": "εδώ"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "Καθαρισμός βιβλιοθήκης", "resetLibrary": "Καθαρισμός βιβλιοθήκης",

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.", "collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.",
"collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work.", "collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "Looks like you are using Brave browser with the", "line1": "Looks like you are using Brave browser with the <bold>Aggressively Block Fingerprinting</bold> setting enabled.",
"aggressive_block_fingerprint": "Aggressively Block Fingerprinting", "line2": "This could result in breaking the <bold>Text Elements</bold> in your drawings.",
"setting_enabled": "setting enabled", "line3": "We strongly recommend disabling this setting. You can follow <link>these steps</link> on how to do so.",
"break": "This could result in breaking the", "line4": "If disabling this setting doesn't fix the display of text elements, please open an <issueLink>issue</issueLink> on our GitHub, or write us on <discordLink>Discord</discordLink>"
"text_elements": "Text Elements",
"in_your_drawings": "in your drawings",
"strongly_recommend": "We strongly recommend disabling this setting. You can follow",
"steps": "these steps",
"how": "on how to do so",
"disable_setting": " If disabling this setting doesn't fix the display of text elements, please open an",
"issue": "issue",
"write": "on our GitHub, or write us on",
"discord": "Discord"
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "Tip: try moving the farthest elements a bit closer together." "canvasTooBigTip": "Tip: try moving the farthest elements a bit closer together."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "Encountered an error. Try ", "headingMain": "Encountered an error. Try <button>reloading the page</button>.",
"headingMain_button": "reloading the page.", "clearCanvasMessage": "If reloading doesn't work, try <button>clearing the canvas</button>.",
"clearCanvasMessage": "If reloading doesn't work, try ",
"clearCanvasMessage_button": "clearing the canvas.",
"clearCanvasCaveat": " This will result in loss of work ", "clearCanvasCaveat": " This will result in loss of work ",
"trackedToSentry_pre": "The error with identifier ", "trackedToSentry": "The error with identifier {{eventId}} was tracked on our system.",
"trackedToSentry_post": " was tracked on our system.", "openIssueMessage": "We were very cautious not to include your scene information on the error. If your scene is not private, please consider following up on our <button>bug tracker</button>. Please include information below by copying and pasting into the GitHub issue.",
"openIssueMessage_pre": "We were very cautious not to include your scene information on the error. If your scene is not private, please consider following up on our ",
"openIssueMessage_button": "bug tracker.",
"openIssueMessage_post": " Please include information below by copying and pasting into the GitHub issue.",
"sceneContent": "Scene content:" "sceneContent": "Scene content:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "Required", "required": "Required",
"website": "Enter a valid URL" "website": "Enter a valid URL"
}, },
"noteDescription": { "noteDescription": "Submit your library to be included in the <link>public library repository</link> for other people to use in their drawings.",
"pre": "Submit your library to be included in the ", "noteGuidelines": "The library needs to be manually approved first. Please read the <link>guidelines</link> before submitting. You will need a GitHub account to communicate and make changes if requested, but it is not strictly required.",
"link": "public library repository", "noteLicense": "By submitting, you agree the library will be published under the <link>MIT License</link>, which in short means anyone can use them without restrictions.",
"post": "for other people to use in their drawings."
},
"noteGuidelines": {
"pre": "The library needs to be manually approved first. Please read the ",
"link": "guidelines",
"post": " before submitting. You will need a GitHub account to communicate and make changes if requested, but it is not strictly required."
},
"noteLicense": {
"pre": "By submitting, you agree the library will be published under the ",
"link": "MIT License, ",
"post": "which in short means anyone can use them without restrictions."
},
"noteItems": "Each library item must have its own name so it's filterable. The following library items will be included:", "noteItems": "Each library item must have its own name so it's filterable. The following library items will be included:",
"atleastOneLibItem": "Please select at least one library item to get started", "atleastOneLibItem": "Please select at least one library item to get started",
"republishWarning": "Note: some of the selected items are marked as already published/submitted. You should only resubmit items when updating an existing library or submission." "republishWarning": "Note: some of the selected items are marked as already published/submitted. You should only resubmit items when updating an existing library or submission."
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "Library submitted", "title": "Library submitted",
"content": "Thank you {{authorName}}. Your library has been submitted for review. You can track the status", "content": "Thank you {{authorName}}. Your library has been submitted for review. You can track the status <link>here</link>"
"link": "here"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "Reset library", "resetLibrary": "Reset library",

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "No se pudo guardar en la base de datos del backend. Si los problemas persisten, debería guardar su archivo localmente para asegurarse de que no pierde su trabajo.", "collabSaveFailed": "No se pudo guardar en la base de datos del backend. Si los problemas persisten, debería guardar su archivo localmente para asegurarse de que no pierde su trabajo.",
"collabSaveFailed_sizeExceeded": "No se pudo guardar en la base de datos del backend, el lienzo parece ser demasiado grande. Debería guardar el archivo localmente para asegurarse de que no pierde su trabajo.", "collabSaveFailed_sizeExceeded": "No se pudo guardar en la base de datos del backend, el lienzo parece ser demasiado grande. Debería guardar el archivo localmente para asegurarse de que no pierde su trabajo.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "Parece que estás usando el navegador Brave", "line1": "",
"aggressive_block_fingerprint": "Bloquear huellas dactilares agresivamente", "line2": "",
"setting_enabled": "ajuste activado", "line3": "",
"break": "Esto podría resultar en romper los", "line4": ""
"text_elements": "Elementos de texto",
"in_your_drawings": "en tus dibujos",
"strongly_recommend": "Recomendamos desactivar esta configuración. Puedes seguir",
"steps": "estos pasos",
"how": "sobre cómo hacerlo",
"disable_setting": " Si deshabilitar esta opción no arregla la visualización de elementos de texto, por favor abre un",
"issue": "issue",
"write": "en GitHub, o escríbenos en",
"discord": "Discord"
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "Sugerencia: intenta acercar un poco más los elementos más lejanos." "canvasTooBigTip": "Sugerencia: intenta acercar un poco más los elementos más lejanos."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "Se encontró un error. Intente ", "headingMain": "Se encontró un error. Intente <button>recargando la página.</button>",
"headingMain_button": "recargando la página.", "clearCanvasMessage": "Si la recarga no funciona, intente <button>limpiando el lienzo.</button>",
"clearCanvasMessage": "Si la recarga no funciona, intente ",
"clearCanvasMessage_button": "limpiando el lienzo.",
"clearCanvasCaveat": " Esto provocará la pérdida de su trabajo ", "clearCanvasCaveat": " Esto provocará la pérdida de su trabajo ",
"trackedToSentry_pre": "El error con el identificador ", "trackedToSentry": "El error con el identificador {{eventId}} fue rastreado en nuestro sistema.",
"trackedToSentry_post": " fue rastreado en nuestro sistema.", "openIssueMessage": "Fuimos muy cautelosos de no incluir la información de tu escena en el error. Si tu escena no es privada, por favor considera seguir nuestro <button>rastreador de errores.</button> Por favor, incluya la siguiente información copiándola y pegándola en el issue de GitHub.",
"openIssueMessage_pre": "Fuimos muy cautelosos de no incluir la información de tu escena en el error. Si tu escena no es privada, por favor considera seguir nuestro ",
"openIssueMessage_button": "rastreador de errores.",
"openIssueMessage_post": " Por favor, incluya la siguiente información copiándola y pegándola en el issue de GitHub.",
"sceneContent": "Contenido de la escena:" "sceneContent": "Contenido de la escena:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "Requerido", "required": "Requerido",
"website": "Introduce una URL válida" "website": "Introduce una URL válida"
}, },
"noteDescription": { "noteDescription": "Envía tu biblioteca para ser incluida en el <link>repositorio de librería pública</link>para que otras personas utilicen en sus dibujos.",
"pre": "Envía tu biblioteca para ser incluida en el ", "noteGuidelines": "La biblioteca debe ser aprobada manualmente primero. Por favor, lea la <link>pautas</link> antes de enviar. Necesitará una cuenta de GitHub para comunicarse y hacer cambios si se solicita, pero no es estrictamente necesario.",
"link": "repositorio de librería pública", "noteLicense": "Al enviar, usted acepta que la biblioteca se publicará bajo el <link>Licencia MIT </link>que en breve significa que cualquiera puede utilizarlos sin restricciones.",
"post": "para que otras personas utilicen en sus dibujos."
},
"noteGuidelines": {
"pre": "La biblioteca debe ser aprobada manualmente primero. Por favor, lea la ",
"link": "pautas",
"post": " antes de enviar. Necesitará una cuenta de GitHub para comunicarse y hacer cambios si se solicita, pero no es estrictamente necesario."
},
"noteLicense": {
"pre": "Al enviar, usted acepta que la biblioteca se publicará bajo el ",
"link": "Licencia MIT ",
"post": "que en breve significa que cualquiera puede utilizarlos sin restricciones."
},
"noteItems": "Cada elemento de la biblioteca debe tener su propio nombre para que sea filtrable. Los siguientes elementos de la biblioteca serán incluidos:", "noteItems": "Cada elemento de la biblioteca debe tener su propio nombre para que sea filtrable. Los siguientes elementos de la biblioteca serán incluidos:",
"atleastOneLibItem": "Por favor, seleccione al menos un elemento de la biblioteca para empezar", "atleastOneLibItem": "Por favor, seleccione al menos un elemento de la biblioteca para empezar",
"republishWarning": "Nota: algunos de los elementos seleccionados están marcados como ya publicados/enviados. Sólo debería volver a enviar elementos cuando se actualice una biblioteca o envío." "republishWarning": "Nota: algunos de los elementos seleccionados están marcados como ya publicados/enviados. Sólo debería volver a enviar elementos cuando se actualice una biblioteca o envío."
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "Biblioteca enviada", "title": "Biblioteca enviada",
"content": "Gracias {{authorName}}. Su biblioteca ha sido enviada para ser revisada. Puede seguir el estado", "content": "Gracias {{authorName}}. Su biblioteca ha sido enviada para ser revisada. Puede seguir el estado<link>aquí</link>"
"link": "aquí"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "Reiniciar biblioteca", "resetLibrary": "Reiniciar biblioteca",

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "Ezin izan da backend datu-basean gorde. Arazoak jarraitzen badu, zure fitxategia lokalean gorde beharko zenuke zure lana ez duzula galtzen ziurtatzeko.", "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.", "collabSaveFailed_sizeExceeded": "Ezin izan da backend datu-basean gorde, ohiala handiegia dela dirudi. Fitxategia lokalean gorde beharko zenuke zure lana galtzen ez duzula ziurtatzeko.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "Brave nabigatzailea erabiltzen ari zarela dirudi", "line1": "",
"aggressive_block_fingerprint": "Aggressively Block Fingerprinting", "line2": "",
"setting_enabled": "ezarpena gaituta", "line3": "",
"break": "Honek honen haustea eragin dezake", "line4": ""
"text_elements": "Testu-elementuak",
"in_your_drawings": "zure marrazkietan",
"strongly_recommend": "Ezarpen hau desgaitzea gomendatzen dugu. Jarrai dezakezu",
"steps": "urrats hauek",
"how": "jakiteko nola egin",
"disable_setting": " Ezarpen hau desgaitzeak testu-elementuen bistaratzea konpontzen ez badu, ireki",
"issue": "eskaera (issue) bat",
"write": "gure Github-en edo idatz iezaguzu",
"discord": "Discord-en"
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "Aholkua: saiatu urrunen dauden elementuak pixka bat hurbiltzen." "canvasTooBigTip": "Aholkua: saiatu urrunen dauden elementuak pixka bat hurbiltzen."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "Errore bat aurkitu da. Saiatu ", "headingMain": "Errore bat aurkitu da. Saiatu <button>orria birkargatzen.</button>",
"headingMain_button": "orria birkargatzen.", "clearCanvasMessage": "Birkargatzea ez bada burutzen, saiatu <button>oihala garbitzen.</button>",
"clearCanvasMessage": "Birkargatzea ez bada burutzen, saiatu ",
"clearCanvasMessage_button": "oihala garbitzen.",
"clearCanvasCaveat": " Honen ondorioz lana galduko da ", "clearCanvasCaveat": " Honen ondorioz lana galduko da ",
"trackedToSentry_pre": "Identifikatzailearen errorea ", "trackedToSentry": "Identifikatzailearen errorea {{eventId}} gure sistemak behatu du.",
"trackedToSentry_post": " gure sistemak behatu du.", "openIssueMessage": "Oso kontuz ibili gara zure eszenaren informazioa errorean ez sartzeko. Zure eszena pribatua ez bada, kontuan hartu gure <button>erroreen jarraipena egitea.</button> Sartu beheko informazioa kopiatu eta itsatsi bidez GitHub issue-n.",
"openIssueMessage_pre": "Oso kontuz ibili gara zure eszenaren informazioa errorean ez sartzeko. Zure eszena pribatua ez bada, kontuan hartu gure ",
"openIssueMessage_button": "erroreen jarraipena egitea.",
"openIssueMessage_post": " Sartu beheko informazioa kopiatu eta itsatsi bidez GitHub issue-n.",
"sceneContent": "Eszenaren edukia:" "sceneContent": "Eszenaren edukia:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "Beharrezkoa", "required": "Beharrezkoa",
"website": "Sartu baliozko URL bat" "website": "Sartu baliozko URL bat"
}, },
"noteDescription": { "noteDescription": "Bidali zure liburutegira sartu ahal izateko <link>zure liburutegiko biltegian</link>beste jendeak bere marrazkietan erabili ahal izateko.",
"pre": "Bidali zure liburutegira sartu ahal izateko ", "noteGuidelines": "Liburutegia eskuz onartu behar da. Irakurri <link>gidalerroak</link> bidali aurretik. GitHub kontu bat edukitzea komeni da komunikatzeko eta aldaketak egin ahal izateko, baina ez da guztiz beharrezkoa.",
"link": "zure liburutegiko biltegian", "noteLicense": "Bidaltzen baduzu, onartzen duzu liburutegia <link>MIT lizentziarekin argitaratuko dela, </link>zeinak, laburbilduz, esan nahi du edozeinek erabiltzen ahal duela murrizketarik gabe.",
"post": "beste jendeak bere marrazkietan erabili ahal izateko."
},
"noteGuidelines": {
"pre": "Liburutegia eskuz onartu behar da. Irakurri ",
"link": "gidalerroak",
"post": " bidali aurretik. GitHub kontu bat edukitzea komeni da komunikatzeko eta aldaketak egin ahal izateko, baina ez da guztiz beharrezkoa."
},
"noteLicense": {
"pre": "Bidaltzen baduzu, onartzen duzu liburutegia ",
"link": "MIT lizentziarekin argitaratuko dela, ",
"post": "zeinak, laburbilduz, esan nahi du edozeinek erabiltzen ahal duela murrizketarik gabe."
},
"noteItems": "Liburutegiko elementu bakoitzak bere izena eduki behar du iragazi ahal izateko. Liburutegiko hurrengo elementuak barne daude:", "noteItems": "Liburutegiko elementu bakoitzak bere izena eduki behar du iragazi ahal izateko. Liburutegiko hurrengo elementuak barne daude:",
"atleastOneLibItem": "Hautatu gutxienez liburutegiko elementu bat gutxienez hasi ahal izateko", "atleastOneLibItem": "Hautatu gutxienez liburutegiko elementu bat gutxienez hasi ahal izateko",
"republishWarning": "Oharra: hautatutako elementu batzuk dagoeneko argitaratuta/bidalita bezala markatuta daude. Elementuak berriro bidali behar dituzu lehendik dagoen liburutegi edo bidalketa eguneratzen duzunean." "republishWarning": "Oharra: hautatutako elementu batzuk dagoeneko argitaratuta/bidalita bezala markatuta daude. Elementuak berriro bidali behar dituzu lehendik dagoen liburutegi edo bidalketa eguneratzen duzunean."
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "Liburutegia bidali da", "title": "Liburutegia bidali da",
"content": "Eskerrik asko {{authorName}}. Zure liburutegia bidali da berrikustera. Jarraitu dezakezu haren egoera", "content": "Eskerrik asko {{authorName}}. Zure liburutegia bidali da berrikustera. Jarraitu dezakezu haren egoera<link>hemen</link>"
"link": "hemen"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "Leheneratu liburutegia", "resetLibrary": "Leheneratu liburutegia",

View file

@ -1,7 +1,7 @@
{ {
"labels": { "labels": {
"paste": "جای گذاری", "paste": "جای گذاری",
"pasteAsPlaintext": "", "pasteAsPlaintext": "جای‌گذاری به عنوان متن ساده",
"pasteCharts": "قراردادن نمودارها", "pasteCharts": "قراردادن نمودارها",
"selectAll": "انتخاب همه", "selectAll": "انتخاب همه",
"multiSelect": "یک ایتم به انتخاب شده ها اضافه کنید.", "multiSelect": "یک ایتم به انتخاب شده ها اضافه کنید.",
@ -54,7 +54,7 @@
"veryLarge": "بسیار بزرگ", "veryLarge": "بسیار بزرگ",
"solid": "توپر", "solid": "توپر",
"hachure": "هاشور", "hachure": "هاشور",
"zigzag": "", "zigzag": "زیگزاگ",
"crossHatch": "هاشور متقاطع", "crossHatch": "هاشور متقاطع",
"thin": "نازک", "thin": "نازک",
"bold": "ضخیم", "bold": "ضخیم",
@ -111,7 +111,7 @@
"increaseFontSize": "افزایش دادن اندازه فونت", "increaseFontSize": "افزایش دادن اندازه فونت",
"unbindText": "بازکردن نوشته", "unbindText": "بازکردن نوشته",
"bindText": "بستن نوشته", "bindText": "بستن نوشته",
"createContainerFromText": "", "createContainerFromText": "متن را در یک جایگاه بپیچید",
"link": { "link": {
"edit": "ویرایش لینک", "edit": "ویرایش لینک",
"create": "ایجاد پیوند", "create": "ایجاد پیوند",
@ -195,7 +195,7 @@
"resetLibrary": "ین کار کل صفحه را پاک میکند. آیا مطمئنید?", "resetLibrary": "ین کار کل صفحه را پاک میکند. آیا مطمئنید?",
"removeItemsFromsLibrary": "حذف {{count}} آیتم(ها) از کتابخانه?", "removeItemsFromsLibrary": "حذف {{count}} آیتم(ها) از کتابخانه?",
"invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است.", "invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است.",
"collabOfflineWarning": "" "collabOfflineWarning": "اتصال به اینترنت در دسترس نیست.\nتغییرات شما ذخیره نمی شود!"
}, },
"errors": { "errors": {
"unsupportedFileType": "نوع فایل پشتیبانی نشده.", "unsupportedFileType": "نوع فایل پشتیبانی نشده.",
@ -205,22 +205,13 @@
"invalidSVGString": "SVG نادرست.", "invalidSVGString": "SVG نادرست.",
"cannotResolveCollabServer": "به سرور collab متصل نشد. لطفا صفحه را مجددا بارگذاری کنید و دوباره تلاش کنید.", "cannotResolveCollabServer": "به سرور collab متصل نشد. لطفا صفحه را مجددا بارگذاری کنید و دوباره تلاش کنید.",
"importLibraryError": "داده‌ها بارگذاری نشدند", "importLibraryError": "داده‌ها بارگذاری نشدند",
"collabSaveFailed": "", "collabSaveFailed": "در پایگاه داده باطن ذخیره نشد. اگر مشکلات همچنان ادامه داشت، باید فایل خود را به صورت محلی ذخیره کنید تا مطمئن شوید کار خود را از دست نمی دهید.",
"collabSaveFailed_sizeExceeded": "", "collabSaveFailed_sizeExceeded": "در پایگاه داده بکند ذخیره نشد. اگر مشکلات همچنان ادامه داشت، باید فایل خود را به صورت محلی ذخیره کنید تا مطمئن شوید کار خود را از دست نمی دهید.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "", "line1": "به نظر می‌رسد از مرورگر Brave با تنظیم <bold> مسدود کردن شدید اثرانگشت</bold> استفاده می‌کنید.",
"aggressive_block_fingerprint": "", "line2": "این می تواند منجر به شکستن <bold>عناصر متن</bold> در نقاشی های شما شود.",
"setting_enabled": "", "line3": "اکیداً توصیه می کنیم این تنظیم را غیرفعال کنید. برای نحوه انجام این کار می‌توانید <link>این مراحل</link> را دنبال کنید.",
"break": "", "line4": "اگر غیرفعال کردن این تنظیم نمایش عناصر متنی را برطرف نکرد، لطفاً یک <issueLink>مشکل</issueLink> را در GitHub ما باز کنید یا برای ما در <discordLink>Discord</discordLink> بنویسید."
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
} }
}, },
"toolBar": { "toolBar": {
@ -238,7 +229,7 @@
"penMode": "حالت قلم - جلوگیری از تماس", "penMode": "حالت قلم - جلوگیری از تماس",
"link": "افزودن/به‌روزرسانی پیوند برای شکل انتخابی", "link": "افزودن/به‌روزرسانی پیوند برای شکل انتخابی",
"eraser": "پاک کن", "eraser": "پاک کن",
"hand": "" "hand": "دست (ابزار پانینگ)"
}, },
"headings": { "headings": {
"canvasActions": "عملیات روی بوم", "canvasActions": "عملیات روی بوم",
@ -246,7 +237,7 @@
"shapes": "شکل‌ها" "shapes": "شکل‌ها"
}, },
"hints": { "hints": {
"canvasPanning": "", "canvasPanning": "برای حرکت دادن بوم، چرخ ماوس یا فاصله را در حین کشیدن نگه دارید یا از ابزار دستی استفاده کنید",
"linearElement": "برای چند نقطه کلیک و برای یک خط بکشید", "linearElement": "برای چند نقطه کلیک و برای یک خط بکشید",
"freeDraw": "کلیک کنید و بکشید و وقتی کار تمام شد رها کنید", "freeDraw": "کلیک کنید و بکشید و وقتی کار تمام شد رها کنید",
"text": "نکته: با برنامه انتخاب شده شما میتوانید با دوبار کلیک کردن هرکجا میخواید متن اظاف کنید", "text": "نکته: با برنامه انتخاب شده شما میتوانید با دوبار کلیک کردن هرکجا میخواید متن اظاف کنید",
@ -257,7 +248,7 @@
"resize": "می توانید با نگه داشتن SHIFT در هنگام تغییر اندازه، نسبت ها را محدود کنید،ALT را برای تغییر اندازه از مرکز نگه دارید", "resize": "می توانید با نگه داشتن SHIFT در هنگام تغییر اندازه، نسبت ها را محدود کنید،ALT را برای تغییر اندازه از مرکز نگه دارید",
"resizeImage": "با نگه داشتن SHIFT می توانید آزادانه اندازه را تغییر دهید،\nبرای تغییر اندازه از مرکز، ALT را نگه دارید", "resizeImage": "با نگه داشتن SHIFT می توانید آزادانه اندازه را تغییر دهید،\nبرای تغییر اندازه از مرکز، ALT را نگه دارید",
"rotate": "با نگه داشتن SHIFT هنگام چرخش می توانید زاویه ها را محدود کنید", "rotate": "با نگه داشتن SHIFT هنگام چرخش می توانید زاویه ها را محدود کنید",
"lineEditor_info": "", "lineEditor_info": "CtrlOrCmd را نگه دارید و دوبار کلیک کنید یا CtrlOrCmd + Enter را فشار دهید تا نقاط را ویرایش کنید.",
"lineEditor_pointSelected": "برای حذف نقطه Delete برای کپی زدن Ctrl یا Cmd+D را بزنید و یا برای جابجایی بکشید", "lineEditor_pointSelected": "برای حذف نقطه Delete برای کپی زدن Ctrl یا Cmd+D را بزنید و یا برای جابجایی بکشید",
"lineEditor_nothingSelected": "یک نقطه را برای ویرایش انتخاب کنید (SHIFT را برای انتخاب چندگانه نگه دارید)،\nیا Alt را نگه دارید و برای افزودن نقاط جدید کلیک کنید", "lineEditor_nothingSelected": "یک نقطه را برای ویرایش انتخاب کنید (SHIFT را برای انتخاب چندگانه نگه دارید)،\nیا Alt را نگه دارید و برای افزودن نقاط جدید کلیک کنید",
"placeImage": "برای قرار دادن تصویر کلیک کنید، یا کلیک کنید و بکشید تا اندازه آن به صورت دستی تنظیم شود", "placeImage": "برای قرار دادن تصویر کلیک کنید، یا کلیک کنید و بکشید تا اندازه آن به صورت دستی تنظیم شود",
@ -265,7 +256,7 @@
"bindTextToElement": "برای افزودن اینتر را بزنید", "bindTextToElement": "برای افزودن اینتر را بزنید",
"deepBoxSelect": "CtrlOrCmd را برای انتخاب عمیق و جلوگیری از کشیدن نگه دارید", "deepBoxSelect": "CtrlOrCmd را برای انتخاب عمیق و جلوگیری از کشیدن نگه دارید",
"eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند", "eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند",
"firefox_clipboard_write": "" "firefox_clipboard_write": "احتمالاً می‌توان این ویژگی را با تنظیم پرچم «dom.events.asyncClipboard.clipboardItem» روی «true» فعال کرد. برای تغییر پرچم های مرورگر در فایرفاکس، از صفحه \"about:config\" دیدن کنید."
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "پیش نمایش نشان داده نمی شود", "cannotShowPreview": "پیش نمایش نشان داده نمی شود",
@ -320,8 +311,8 @@
"doubleClick": "دابل کلیک", "doubleClick": "دابل کلیک",
"drag": "کشیدن", "drag": "کشیدن",
"editor": "ویرایشگر", "editor": "ویرایشگر",
"editLineArrowPoints": "", "editLineArrowPoints": "نقاط خط/پیکان را ویرایش کنید",
"editText": "", "editText": "ویرایش متن / افزودن برچسب",
"github": "اشکالی می بینید؟ گزارش دهید", "github": "اشکالی می بینید؟ گزارش دهید",
"howto": "راهنمای ما را دنبال کنید", "howto": "راهنمای ما را دنبال کنید",
"or": "یا", "or": "یا",
@ -335,8 +326,8 @@
"zoomToFit": "بزرگنمایی برای دیدن تمام آیتم ها", "zoomToFit": "بزرگنمایی برای دیدن تمام آیتم ها",
"zoomToSelection": "بزرگنمایی قسمت انتخاب شده", "zoomToSelection": "بزرگنمایی قسمت انتخاب شده",
"toggleElementLock": "قفل/بازکردن انتخاب شده ها", "toggleElementLock": "قفل/بازکردن انتخاب شده ها",
"movePageUpDown": "", "movePageUpDown": "حرکت صفحه به بالا/پایین",
"movePageLeftRight": "" "movePageLeftRight": "حرکت صفحه به چپ/راست"
}, },
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "پاک کردن بوم" "title": "پاک کردن بوم"
@ -418,7 +409,7 @@
"fileSavedToFilename": "ذخیره در {filename}", "fileSavedToFilename": "ذخیره در {filename}",
"canvas": "بوم", "canvas": "بوم",
"selection": "انتخاب", "selection": "انتخاب",
"pasteAsSingleElement": "" "pasteAsSingleElement": "از {{shortcut}} برای چسباندن به عنوان یک عنصر استفاده کنید،\nیا در یک ویرایشگر متن موجود جایگذاری کنید"
}, },
"colors": { "colors": {
"ffffff": "سفید", "ffffff": "سفید",
@ -469,15 +460,15 @@
}, },
"welcomeScreen": { "welcomeScreen": {
"app": { "app": {
"center_heading": "", "center_heading": "تمام داده های شما به صورت محلی در مرورگر شما ذخیره می شود.",
"center_heading_plus": "", "center_heading_plus": "آیا می‌خواهید به جای آن به Excalidraw+ بروید؟",
"menuHint": "" "menuHint": "خروجی، ترجیحات، زبان ها، ..."
}, },
"defaults": { "defaults": {
"menuHint": "", "menuHint": "خروجی، ترجیحات، وبیشتر ...",
"center_heading": "", "center_heading": "نمودارها .ساخته شده. ساده.",
"toolbarHint": "", "toolbarHint": "ابزاری را انتخاب کنید و نقاشی را شروع کنید!",
"helpHint": "" "helpHint": "میانبرها و راهنما"
} }
} }
} }

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi.", "collabSaveFailed": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi.",
"collabSaveFailed_sizeExceeded": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi.", "collabSaveFailed_sizeExceeded": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "", "line1": "",
"aggressive_block_fingerprint": "", "line2": "",
"setting_enabled": "", "line3": "",
"break": "", "line4": ""
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "Vinkki: yritä siirtää kaukaisimpia elementtejä hieman lähemmäs toisiaan." "canvasTooBigTip": "Vinkki: yritä siirtää kaukaisimpia elementtejä hieman lähemmäs toisiaan."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "Tapahtui virhe. Yritä ", "headingMain": "Tapahtui virhe. Yritä <button>sivun lataamista uudelleen.</button>",
"headingMain_button": "sivun lataamista uudelleen.", "clearCanvasMessage": "Mikäli sivun lataaminen uudelleen ei auta, yritä <button>tyhjentää piirtoalue.</button>",
"clearCanvasMessage": "Mikäli sivun lataaminen uudelleen ei auta, yritä ",
"clearCanvasMessage_button": "tyhjentää piirtoalue.",
"clearCanvasCaveat": " Tämä johtaa työn menetykseen ", "clearCanvasCaveat": " Tämä johtaa työn menetykseen ",
"trackedToSentry_pre": "Virhe tunnisteella ", "trackedToSentry": "Virhe tunnisteella {{eventId}} tallennettiin järjestelmäämme.",
"trackedToSentry_post": " tallennettiin järjestelmäämme.", "openIssueMessage": "Olimme varovaisia emmekä sisällyttäneet tietoa piirroksestasi virheeseen. Mikäli piirroksesi ei ole yksityinen, harkitsethan kertovasi meille <button>virheenseurantajärjestelmässämme.</button> Sisällytä alla olevat tiedot kopioimalla ne GitHub-ongelmaan.",
"openIssueMessage_pre": "Olimme varovaisia emmekä sisällyttäneet tietoa piirroksestasi virheeseen. Mikäli piirroksesi ei ole yksityinen, harkitsethan kertovasi meille ",
"openIssueMessage_button": "virheenseurantajärjestelmässämme.",
"openIssueMessage_post": " Sisällytä alla olevat tiedot kopioimalla ne GitHub-ongelmaan.",
"sceneContent": "Piirroksen tiedot:" "sceneContent": "Piirroksen tiedot:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "Pakollinen", "required": "Pakollinen",
"website": "Syötä oikeamuotoinen URL-osoite" "website": "Syötä oikeamuotoinen URL-osoite"
}, },
"noteDescription": { "noteDescription": "Lähetä kirjastosi, jotta se voidaan sisällyttää <link>julkisessa kirjastolistauksessa</link>muiden käyttöön omissa piirrustuksissaan.",
"pre": "Lähetä kirjastosi, jotta se voidaan sisällyttää ", "noteGuidelines": "Kirjasto on ensin hyväksyttävä manuaalisesti. Ole hyvä ja lue <link>ohjeet</link> ennen lähettämistä. Tarvitset GitHub-tilin, jotta voit viestiä ja tehdä muutoksia pyydettäessä, mutta se ei ole ehdottoman välttämätöntä.",
"link": "julkisessa kirjastolistauksessa", "noteLicense": "Lähettämällä hyväksyt että kirjasto julkaistaan <link>MIT-lisenssin </link>alla, mikä lyhyesti antaa muiden käyttää sitä ilman rajoituksia.",
"post": "muiden käyttöön omissa piirrustuksissaan."
},
"noteGuidelines": {
"pre": "Kirjasto on ensin hyväksyttävä manuaalisesti. Ole hyvä ja lue ",
"link": "ohjeet",
"post": " ennen lähettämistä. Tarvitset GitHub-tilin, jotta voit viestiä ja tehdä muutoksia pyydettäessä, mutta se ei ole ehdottoman välttämätöntä."
},
"noteLicense": {
"pre": "Lähettämällä hyväksyt että kirjasto julkaistaan ",
"link": "MIT-lisenssin ",
"post": "alla, mikä lyhyesti antaa muiden käyttää sitä ilman rajoituksia."
},
"noteItems": "Jokaisella kirjaston kohteella on oltava oma nimensä suodatusta varten. Seuraavat kirjaston kohteet sisältyvät:", "noteItems": "Jokaisella kirjaston kohteella on oltava oma nimensä suodatusta varten. Seuraavat kirjaston kohteet sisältyvät:",
"atleastOneLibItem": "Valitse vähintään yksi kirjaston kohde aloittaaksesi", "atleastOneLibItem": "Valitse vähintään yksi kirjaston kohde aloittaaksesi",
"republishWarning": "Huom! Osa valituista kohteista on merkitty jo julkaistu/lähetetyiksi. Lähetä kohteita uudelleen vain päivitettäessä olemassa olevaa kirjastoa tai ehdotusta." "republishWarning": "Huom! Osa valituista kohteista on merkitty jo julkaistu/lähetetyiksi. Lähetä kohteita uudelleen vain päivitettäessä olemassa olevaa kirjastoa tai ehdotusta."
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "Kirjasto lähetetty", "title": "Kirjasto lähetetty",
"content": "Kiitos {{authorName}}. Kirjastosi on lähetetty tarkistettavaksi. Voit seurata sen tilaa", "content": "Kiitos {{authorName}}. Kirjastosi on lähetetty tarkistettavaksi. Voit seurata sen tilaa<link>täällä</link>"
"link": "täällä"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "Tyhjennä kirjasto", "resetLibrary": "Tyhjennä kirjasto",

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "Impossible d'enregistrer dans la base de données en arrière-plan. Si des problèmes persistent, vous devriez enregistrer votre fichier localement pour vous assurer de ne pas perdre votre travail.", "collabSaveFailed": "Impossible d'enregistrer dans la base de données en arrière-plan. Si des problèmes persistent, vous devriez enregistrer votre fichier localement pour vous assurer de ne pas perdre votre travail.",
"collabSaveFailed_sizeExceeded": "Impossible d'enregistrer dans la base de données en arrière-plan, le tableau semble trop grand. Vous devriez enregistrer le fichier localement pour vous assurer de ne pas perdre votre travail.", "collabSaveFailed_sizeExceeded": "Impossible d'enregistrer dans la base de données en arrière-plan, le tableau semble trop grand. Vous devriez enregistrer le fichier localement pour vous assurer de ne pas perdre votre travail.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "Il semble que vous utilisiez le navigateur Brave avec le", "line1": "",
"aggressive_block_fingerprint": "blocage d'empreinte agressif", "line2": "",
"setting_enabled": "activé", "line3": "",
"break": "Ceci pourrait avoir pour conséquence de « casser » les", "line4": ""
"text_elements": "éléments texte",
"in_your_drawings": "dans vos dessins",
"strongly_recommend": "Nous recommandons fortement de désactiver ce paramètre. Vous pouvez suivre",
"steps": "ces étapes",
"how": "sur la manière de procéder",
"disable_setting": " Si la désactivation de ce paramètre ne résout pas l'affichage des éléments texte, veuillez ouvrir un",
"issue": "ticket",
"write": "sur notre GitHub, ou nous écrire sur",
"discord": "Discord"
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "Astuce : essayez de rapprocher un peu les éléments les plus éloignés." "canvasTooBigTip": "Astuce : essayez de rapprocher un peu les éléments les plus éloignés."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "Une erreur est survenue. Essayez ", "headingMain": "Une erreur est survenue. Essayez <button>de recharger la page.</button>",
"headingMain_button": "de recharger la page.", "clearCanvasMessage": "Si le rechargement ne résout pas l'erreur, essayez <button>effacement du canevas.</button>",
"clearCanvasMessage": "Si le rechargement ne résout pas l'erreur, essayez ",
"clearCanvasMessage_button": "effacement du canevas.",
"clearCanvasCaveat": " Cela entraînera une perte du travail ", "clearCanvasCaveat": " Cela entraînera une perte du travail ",
"trackedToSentry_pre": "L'erreur avec l'identifiant ", "trackedToSentry": "L'erreur avec l'identifiant {{eventId}} a été enregistrée dans notre système.",
"trackedToSentry_post": " a été enregistrée dans notre système.", "openIssueMessage": "Nous avons fait très attention à ne pas inclure les informations de votre scène dans l'erreur. Si votre scène n'est pas privée, veuillez envisager de poursuivre sur notre <button>outil de suivi des bugs.</button> Veuillez inclure les informations ci-dessous en les copiant-collant dans le ticket GitHub.",
"openIssueMessage_pre": "Nous avons fait très attention à ne pas inclure les informations de votre scène dans l'erreur. Si votre scène n'est pas privée, veuillez envisager de poursuivre sur notre ",
"openIssueMessage_button": "outil de suivi des bugs.",
"openIssueMessage_post": " Veuillez inclure les informations ci-dessous en les copiant-collant dans le ticket GitHub.",
"sceneContent": "Contenu de la scène :" "sceneContent": "Contenu de la scène :"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "Requis", "required": "Requis",
"website": "Entrer une URL valide" "website": "Entrer une URL valide"
}, },
"noteDescription": { "noteDescription": "Soumets ta bibliothèque pour l'inclure au <link>dépôt de bibliothèque publique</link>pour permettre son utilisation par autrui dans leurs dessins.",
"pre": "Soumets ta bibliothèque pour l'inclure au ", "noteGuidelines": "La bibliothèque doit d'abord être approuvée manuellement. Veuillez lire les <link>lignes directrices</link> avant de la soumettre. Vous aurez besoin d'un compte GitHub pour communiquer et apporter des modifications si demandé, mais ce n'est pas obligatoire.",
"link": "dépôt de bibliothèque publique", "noteLicense": "En soumettant, vous acceptez que la bibliothèque soit publiée sous la <link>Licence MIT, </link>ce qui en gros signifie que tout le monde peut l'utiliser sans restrictions.",
"post": "pour permettre son utilisation par autrui dans leurs dessins."
},
"noteGuidelines": {
"pre": "La bibliothèque doit d'abord être approuvée manuellement. Veuillez lire les ",
"link": "lignes directrices",
"post": " avant de la soumettre. Vous aurez besoin d'un compte GitHub pour communiquer et apporter des modifications si demandé, mais ce n'est pas obligatoire."
},
"noteLicense": {
"pre": "En soumettant, vous acceptez que la bibliothèque soit publiée sous la ",
"link": "Licence MIT, ",
"post": "ce qui en gros signifie que tout le monde peut l'utiliser sans restrictions."
},
"noteItems": "Chaque élément de la bibliothèque doit avoir son propre nom afin qu'il soit filtrable. Les éléments de bibliothèque suivants seront inclus :", "noteItems": "Chaque élément de la bibliothèque doit avoir son propre nom afin qu'il soit filtrable. Les éléments de bibliothèque suivants seront inclus :",
"atleastOneLibItem": "Veuillez sélectionner au moins un élément de bibliothèque pour commencer", "atleastOneLibItem": "Veuillez sélectionner au moins un élément de bibliothèque pour commencer",
"republishWarning": "Remarque : certains des éléments sélectionnés sont marqués comme étant déjà publiés/soumis. Vous devez uniquement resoumettre des éléments lors de la mise à jour d'une bibliothèque ou d'une soumission existante." "republishWarning": "Remarque : certains des éléments sélectionnés sont marqués comme étant déjà publiés/soumis. Vous devez uniquement resoumettre des éléments lors de la mise à jour d'une bibliothèque ou d'une soumission existante."
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "Bibliothèque soumise", "title": "Bibliothèque soumise",
"content": "Merci {{authorName}}. Votre bibliothèque a été soumise pour examen. Vous pouvez suivre le statut", "content": "Merci {{authorName}}. Votre bibliothèque a été soumise pour examen. Vous pouvez suivre le statut<link>ici</link>"
"link": "ici"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "Réinitialiser la bibliothèque", "resetLibrary": "Réinitialiser la bibliothèque",

View file

@ -208,19 +208,10 @@
"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": "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.", "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.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "Semella que estás usando o navegador Brave coa opción", "line1": "",
"aggressive_block_fingerprint": "Aggressively Block Fingerprinting", "line2": "",
"setting_enabled": "activada", "line3": "",
"break": "Isto podería provocar unha ruptura dos", "line4": ""
"text_elements": "Elementos de Texto",
"in_your_drawings": "nos seus debuxos",
"strongly_recommend": "Recomendámoslle encarecidamente que desactive esa opción. Pode seguir",
"steps": "estes pasos",
"how": "sobre como facelo",
"disable_setting": " Se ao desactivar esta opción non se arranxa o problema ao mostrar os elementos de texto, por favor abra unha",
"issue": "issue",
"write": "no noso GitHub, ou escríbenos ao",
"discord": "Discord"
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "Consello: Probe a acercar un pouco os elementos máis afastados." "canvasTooBigTip": "Consello: Probe a acercar un pouco os elementos máis afastados."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "Atopouse un erro. Probe ", "headingMain": "Atopouse un erro. Probe <button>recargando a páxina.</button>",
"headingMain_button": "recargando a páxina.", "clearCanvasMessage": "Se recargar non funcionou, probe <button>limpando o lenzo.</button>",
"clearCanvasMessage": "Se recargar non funcionou, probe ",
"clearCanvasMessage_button": "limpando o lenzo.",
"clearCanvasCaveat": " Isto resultará nunha perda do seu traballo ", "clearCanvasCaveat": " Isto resultará nunha perda do seu traballo ",
"trackedToSentry_pre": "O erro con identificador ", "trackedToSentry": "O erro con identificador {{eventId}} foi rastrexado no noso sistema.",
"trackedToSentry_post": " foi rastrexado no noso sistema.", "openIssueMessage": "Fomos moi cautelosos de non incluír a información da súa escena no erro. Se a súa escena non é privada, por favor, considere o seguimento do noso <button>rastrexador de erros.</button> Por favor inclúa a seguinte información copiándoa e pegándoa na issue de Github.",
"openIssueMessage_pre": "Fomos moi cautelosos de non incluír a información da súa escena no erro. Se a súa escena non é privada, por favor, considere o seguimento do noso ",
"openIssueMessage_button": "rastrexador de erros.",
"openIssueMessage_post": " Por favor inclúa a seguinte información copiándoa e pegándoa na issue de Github.",
"sceneContent": "Contido da escena:" "sceneContent": "Contido da escena:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "Obrigatorio", "required": "Obrigatorio",
"website": "Introduza unha URL válida" "website": "Introduza unha URL válida"
}, },
"noteDescription": { "noteDescription": "Envíe a súa biblioteca para que sexa incluída no <link>repositorio público de bibliotecas</link>para que outra xente a poida usar nos seus debuxos.",
"pre": "Envíe a súa biblioteca para que sexa incluída no ", "noteGuidelines": "A biblioteca necesita ser aprobada manualmente primeiro. Por favor, lea as <link>normas</link> antes de ser enviado. Necesitarás unha conta de GitHub para comunicarte ou facer cambios se se solicitan, pero non é estritamente necesario.",
"link": "repositorio público de bibliotecas", "noteLicense": "Ao enviar, estás de acordo con que a biblioteca sexa publicada baixo a <link>Licenza MIT, </link>o cal significa que, en resumo, calquera pode usalo sen restricións.",
"post": "para que outra xente a poida usar nos seus debuxos."
},
"noteGuidelines": {
"pre": "A biblioteca necesita ser aprobada manualmente primeiro. Por favor, lea as ",
"link": "normas",
"post": " antes de ser enviado. Necesitarás unha conta de GitHub para comunicarte ou facer cambios se se solicitan, pero non é estritamente necesario."
},
"noteLicense": {
"pre": "Ao enviar, estás de acordo con que a biblioteca sexa publicada baixo a ",
"link": "Licenza MIT, ",
"post": "o cal significa que, en resumo, calquera pode usalo sen restricións."
},
"noteItems": "Cada elemento da biblioteca debe ter o seu nome propio para que se poida filtrar. Os seguintes elementos da biblioteca serán incluídos:", "noteItems": "Cada elemento da biblioteca debe ter o seu nome propio para que se poida filtrar. Os seguintes elementos da biblioteca serán incluídos:",
"atleastOneLibItem": "Por favor seleccione polo menos un elemento da biblioteca para comezar", "atleastOneLibItem": "Por favor seleccione polo menos un elemento da biblioteca para comezar",
"republishWarning": "Nota: algúns dos elementos seleccionados están marcados como xa publicados/enviados. Só deberías reenviar elementos cando se actualice unha biblioteca ou envío." "republishWarning": "Nota: algúns dos elementos seleccionados están marcados como xa publicados/enviados. Só deberías reenviar elementos cando se actualice unha biblioteca ou envío."
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "Biblioteca enviada", "title": "Biblioteca enviada",
"content": "Grazas {{authorName}}. A súa biblioteca foi enviada para ser revisada. Pode seguir o estado", "content": "Grazas {{authorName}}. A súa biblioteca foi enviada para ser revisada. Pode seguir o estado<link>aquí</link>"
"link": "aquí"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "Restablecer biblioteca", "resetLibrary": "Restablecer biblioteca",

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "לא הצלחתי להתחבר למסד הנתונים האחורי. אם הבעיה ממשיכה, כדאי שתשמור את הקובץ מקומית כדי לוודא שלא תאבד את העבודה שלך.", "collabSaveFailed": "לא הצלחתי להתחבר למסד הנתונים האחורי. אם הבעיה ממשיכה, כדאי שתשמור את הקובץ מקומית כדי לוודא שלא תאבד את העבודה שלך.",
"collabSaveFailed_sizeExceeded": "לא הצלחתי לשמור למסד הנתונים האחורי, נראה שהקנבס שלך גדול מדי. כדאי שתשמור את הקובץ מקומית כדי לוודא שלא תאבד את העבודה שלך.", "collabSaveFailed_sizeExceeded": "לא הצלחתי לשמור למסד הנתונים האחורי, נראה שהקנבס שלך גדול מדי. כדאי שתשמור את הקובץ מקומית כדי לוודא שלא תאבד את העבודה שלך.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "נראה שאתה משתמש בדפדפן Brave עם ה-", "line1": "",
"aggressive_block_fingerprint": "חסימת Fingerprinting אגרסיבית", "line2": "",
"setting_enabled": "ההגדרה מופעלת", "line3": "",
"break": "זה יכול לגרום לבעיה ב-", "line4": ""
"text_elements": "רכיבי טקסט",
"in_your_drawings": "בציורים שלך",
"strongly_recommend": "אנו ממליצים בחום לנטרל את ההגדרה הזו. אתה יכול לעקוב",
"steps": "הצעדים הבאים",
"how": "כיצד לעשות את זה",
"disable_setting": " אם נטרול ההגדרה לא מתקן את תצוגת רכיבי הטקסט, אנא פתח",
"issue": "בעיה",
"write": "ב- GitHub שלנו, או כתוב לנו ב-",
"discord": "Discord"
} }
}, },
"toolBar": { "toolBar": {

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "किसी कारण वश अंदरूनी डेटाबेस में सहेजा नहीं जा सका। यदि समस्या बनी रहती है, तो किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।", "collabSaveFailed": "किसी कारण वश अंदरूनी डेटाबेस में सहेजा नहीं जा सका। यदि समस्या बनी रहती है, तो किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।",
"collabSaveFailed_sizeExceeded": "लगता है कि पृष्ठ तल काफ़ी बड़ा है, इस्कारण अंदरूनी डेटाबेस में सहेजा नहीं जा सका। किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।", "collabSaveFailed_sizeExceeded": "लगता है कि पृष्ठ तल काफ़ी बड़ा है, इस्कारण अंदरूनी डेटाबेस में सहेजा नहीं जा सका। किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "ऐसा लगता है कि आप ब्रेव ब्राउज़र का उपयोग कर रहे है, उसके साथ", "line1": "",
"aggressive_block_fingerprint": "उग्रतापूर्वक उंगलियों के निशान रोकने की", "line2": "",
"setting_enabled": "सेटिंग सक्रिय की गई है।", "line3": "",
"break": "इसके परिणाम स्वरूम टूट सकता है यह", "line4": ""
"text_elements": "मूल पाठ अवयव टूट सकते हैं",
"in_your_drawings": "आपके चित्र में उपस्थित",
"strongly_recommend": "हमारा शशक्त अनुरोध इस सेटिंग्स को अक्षम करने का है. आप",
"steps": "इन दिए हुए स्टेप्स को अनुकरीत करके देख सकते है कि",
"how": "ये कैसे करे",
"disable_setting": " यदि इन सेटिंग्स को निष्क्रिय करने पर भी, पाठ्य दिखना ठीक न हो तो",
"issue": "समस्या",
"write": "हमारे \"GitHub\" पर, अथवा",
"discord": "\"Discord\" पर लिख सकते हैं."
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "कैनवास बहुत बड़ा टिप" "canvasTooBigTip": "कैनवास बहुत बड़ा टिप"
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "एक त्रुटि का सामना करना पड़ा। प्रयत्न ", "headingMain": "एक त्रुटि का सामना करना पड़ा। प्रयत्न <button>इस पृष्ठ को पुनः लोड करें</button>",
"headingMain_button": "इस पृष्ठ को पुनः लोड करें", "clearCanvasMessage": "यदि पुनः लोड करना काम नहीं करता है, तो प्रयास करें <button>कैनवास साफ करना।</button>",
"clearCanvasMessage": "यदि पुनः लोड करना काम नहीं करता है, तो प्रयास करें ",
"clearCanvasMessage_button": "कैनवास साफ करना।",
"clearCanvasCaveat": " इससे काम का नुकसान होगा ", "clearCanvasCaveat": " इससे काम का नुकसान होगा ",
"trackedToSentry_pre": "पहचानकर्ता के साथ त्रुटि ", "trackedToSentry": "पहचानकर्ता के साथ त्रुटि {{eventId}} हमारे सिस्टम पर नज़र रखी गई थी।",
"trackedToSentry_post": " हमारे सिस्टम पर नज़र रखी गई थी।", "openIssueMessage": "हम बहुत सतर्क थे कि त्रुटि पर आपकी दृश्य जानकारी शामिल न करें। यदि आपका दृश्य निजी नहीं है, तो कृपया हमारे बारे में विचार करें <button>बग ट्रैकर</button> कृपया GitHub मुद्दे को कॉपी और पेस्ट करके नीचे दी गई जानकारी शामिल करें।",
"openIssueMessage_pre": "हम बहुत सतर्क थे कि त्रुटि पर आपकी दृश्य जानकारी शामिल न करें। यदि आपका दृश्य निजी नहीं है, तो कृपया हमारे बारे में विचार करें ",
"openIssueMessage_button": "बग ट्रैकर",
"openIssueMessage_post": " कृपया GitHub मुद्दे को कॉपी और पेस्ट करके नीचे दी गई जानकारी शामिल करें।",
"sceneContent": "दृश्य सामग्री:" "sceneContent": "दृश्य सामग्री:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "", "required": "",
"website": "मान्य URL प्रविष्ट करें" "website": "मान्य URL प्रविष्ट करें"
}, },
"noteDescription": { "noteDescription": "संग्रह सम्मिलित करने हेतु प्रस्तुत करें <link>सार्वजनिक संग्रहालय</link>अन्य वक्तियों को उनके चित्रकारी में उपयोग के लिये",
"pre": "संग्रह सम्मिलित करने हेतु प्रस्तुत करें ", "noteGuidelines": "संग्रह को पहले स्वीकृति आवश्यक कृपया यह पढ़ें <link>दिशा-निर्देश</link>",
"link": "सार्वजनिक संग्रहालय", "noteLicense": "",
"post": "अन्य वक्तियों को उनके चित्रकारी में उपयोग के लिये"
},
"noteGuidelines": {
"pre": "संग्रह को पहले स्वीकृति आवश्यक कृपया यह पढ़ें ",
"link": "दिशा-निर्देश",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "", "noteItems": "",
"atleastOneLibItem": "", "atleastOneLibItem": "",
"republishWarning": "टिप्पणी: कुछ चुने हुवे आइटम पहले ही प्रकाशित/प्रस्तुत किए जा चुके हैं। किसी प्रकाशित संग्रह को अद्यतन करते समय या पहले से प्रस्तुत आइटम को पुन्हा प्रस्तुत करते समय, आप बस उसे केवल अद्यतन करें ।" "republishWarning": "टिप्पणी: कुछ चुने हुवे आइटम पहले ही प्रकाशित/प्रस्तुत किए जा चुके हैं। किसी प्रकाशित संग्रह को अद्यतन करते समय या पहले से प्रस्तुत आइटम को पुन्हा प्रस्तुत करते समय, आप बस उसे केवल अद्यतन करें ।"
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "", "title": "",
"content": "", "content": ""
"link": ""
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "", "resetLibrary": "",

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "", "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "", "line1": "",
"aggressive_block_fingerprint": "", "line2": "",
"setting_enabled": "", "line3": "",
"break": "", "line4": ""
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "Tipp: próbáld meg a legtávolabbi elemeket közelebb hozni egy máshoz." "canvasTooBigTip": "Tipp: próbáld meg a legtávolabbi elemeket közelebb hozni egy máshoz."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "Hiba történt. Próbáld ", "headingMain": "Hiba történt. Próbáld <button>újratölteni az oldalt.</button>",
"headingMain_button": "újratölteni az oldalt.", "clearCanvasMessage": "Ha az újratöltés nem működik, próbáld <button>letörölni a vászont.</button>",
"clearCanvasMessage": "Ha az újratöltés nem működik, próbáld ",
"clearCanvasMessage_button": "letörölni a vászont.",
"clearCanvasCaveat": " Ezzel az eddigi munka elveszik ", "clearCanvasCaveat": " Ezzel az eddigi munka elveszik ",
"trackedToSentry_pre": "A hibakód azonosítóval ", "trackedToSentry": "A hibakód azonosítóval {{eventId}} nyomon van követve a rendszerünkben.",
"trackedToSentry_post": " nyomon van követve a rendszerünkben.", "openIssueMessage": "Vigyáztunk arra, hogy a jelenthez tartozó információ ne jelenjen meg a hibaüzenetben. Ha a jeleneted nem bizalmas, kérjük add hozzá a <button>hibakövető rendszerünkhöz.</button> Kérjük, másolja be az alábbi információkat a GitHub problémába.",
"openIssueMessage_pre": "Vigyáztunk arra, hogy a jelenthez tartozó információ ne jelenjen meg a hibaüzenetben. Ha a jeleneted nem bizalmas, kérjük add hozzá a ",
"openIssueMessage_button": "hibakövető rendszerünkhöz.",
"openIssueMessage_post": " Kérjük, másolja be az alábbi információkat a GitHub problémába.",
"sceneContent": "Jelenet tartalma:" "sceneContent": "Jelenet tartalma:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "Kötelező", "required": "Kötelező",
"website": "Adj meg egy érvényes URL-t" "website": "Adj meg egy érvényes URL-t"
}, },
"noteDescription": { "noteDescription": "Küld be könyvtáradat, hogy bekerüljön a <link>nyilvános könyvtár tárolóba</link>hogy mások is felhasználhassák a rajzaikban.",
"pre": "Küld be könyvtáradat, hogy bekerüljön a ", "noteGuidelines": "A könyvtárat először manuálisan kell jóváhagyni. Kérjük, olvassa el a <link>segédletet</link> benyújtása előtt. Szüksége lesz egy GitHub-fiókra a kommunikációhoz és a módosításokhoz, ha kérik, de ez nem feltétlenül szükséges.",
"link": "nyilvános könyvtár tárolóba", "noteLicense": "A beküldéssel elfogadja, hogy a könyvtár a következő alatt kerül közzétételre <link>MIT Licensz </link>ami röviden azt jelenti, hogy bárki korlátozás nélkül használhatja őket.",
"post": "hogy mások is felhasználhassák a rajzaikban."
},
"noteGuidelines": {
"pre": "A könyvtárat először manuálisan kell jóváhagyni. Kérjük, olvassa el a ",
"link": "segédletet",
"post": " benyújtása előtt. Szüksége lesz egy GitHub-fiókra a kommunikációhoz és a módosításokhoz, ha kérik, de ez nem feltétlenül szükséges."
},
"noteLicense": {
"pre": "A beküldéssel elfogadja, hogy a könyvtár a következő alatt kerül közzétételre ",
"link": "MIT Licensz ",
"post": "ami röviden azt jelenti, hogy bárki korlátozás nélkül használhatja őket."
},
"noteItems": "Minden könyvtárelemnek saját nevével kell rendelkeznie, hogy szűrhető legyen. A következő könyvtári tételek kerülnek bele:", "noteItems": "Minden könyvtárelemnek saját nevével kell rendelkeznie, hogy szűrhető legyen. A következő könyvtári tételek kerülnek bele:",
"atleastOneLibItem": "A kezdéshez válassz ki legalább egy könyvtári elemet", "atleastOneLibItem": "A kezdéshez válassz ki legalább egy könyvtári elemet",
"republishWarning": "" "republishWarning": ""
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "A könyvtár beküldve", "title": "A könyvtár beküldve",
"content": "Köszönjük {{authorName}}. Könyvtáradat elküldtük felülvizsgálatra. Nyomon követheted az állapotot", "content": "Köszönjük {{authorName}}. Könyvtáradat elküldtük felülvizsgálatra. Nyomon követheted az állapotot<link>itt</link>"
"link": "itt"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "Könyvtár alaphelyzetbe állítása", "resetLibrary": "Könyvtár alaphelyzetbe állítása",

View file

@ -208,19 +208,10 @@
"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": "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.", "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.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "Sepertinya kamu menggunakan browser Brave dengan", "line1": "",
"aggressive_block_fingerprint": "", "line2": "",
"setting_enabled": "pengaturan diaktifkan", "line3": "",
"break": "", "line4": ""
"text_elements": "Elemen Teks",
"in_your_drawings": "dalam gambar anda",
"strongly_recommend": "Kami sangat menyarankan untuk mematikan pengaturan ini. Kamu dapat mengikuti",
"steps": "langkah-langkah ini",
"how": "dalam bagaimana melakukan itu",
"disable_setting": " Jika pengaturan ini dimatikan tidak mengatasi masalah tampilan dari elemen teks, silahkan buka",
"issue": "isu",
"write": "di GitHub kami, atau tulis kami di",
"discord": "Discord"
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "Tip: coba pindahkan elemen-terjauh lebih dekat bersama." "canvasTooBigTip": "Tip: coba pindahkan elemen-terjauh lebih dekat bersama."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "Mengalami sebuah kesalahan. Cobalah ", "headingMain": "Mengalami sebuah kesalahan. Cobalah <button>muat ulang halaman.</button>",
"headingMain_button": "muat ulang halaman.", "clearCanvasMessage": "Jika memuat ulang tidak bekerja, cobalah <button>bersihkan canvas.</button>",
"clearCanvasMessage": "Jika memuat ulang tidak bekerja, cobalah ",
"clearCanvasMessage_button": "bersihkan canvas.",
"clearCanvasCaveat": " Ini akan menghasilkan hilangnya pekerjaan ", "clearCanvasCaveat": " Ini akan menghasilkan hilangnya pekerjaan ",
"trackedToSentry_pre": "Kesalahan dengan pengidentifikasi ", "trackedToSentry": "Kesalahan dengan pengidentifikasi {{eventId}} dilacak di sistem kami.",
"trackedToSentry_post": " dilacak di sistem kami.", "openIssueMessage": "Kami sangat berhati-hati untuk tidak menyertakan informasi pemandangan Anda pada kesalahan. Jika pemandangan Anda tidak bersifat pribadi, mohon pertimbangkan menindak lanjut pada <button>pelacak bug.</button> Mohon sertakan informasi dibawah ini dengan menyalin dan menempelkan di Github issue.",
"openIssueMessage_pre": "Kami sangat berhati-hati untuk tidak menyertakan informasi pemandangan Anda pada kesalahan. Jika pemandangan Anda tidak bersifat pribadi, mohon pertimbangkan menindak lanjut pada ",
"openIssueMessage_button": "pelacak bug.",
"openIssueMessage_post": " Mohon sertakan informasi dibawah ini dengan menyalin dan menempelkan di Github issue.",
"sceneContent": "Pemandangan konten:" "sceneContent": "Pemandangan konten:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "Dibutuhkan", "required": "Dibutuhkan",
"website": "Masukkan URL valid" "website": "Masukkan URL valid"
}, },
"noteDescription": { "noteDescription": "Kirimkan pustaka Anda untuk disertakan di <link>repositori pustaka publik</link>untuk orang lain menggunakannya dalam gambar mereka.",
"pre": "Kirimkan pustaka Anda untuk disertakan di ", "noteGuidelines": "Pustaka butuh disetujui secara manual terlebih dahulu. Baca <link>pedoman</link> sebelum mengirim. Anda butuh akun GitHub untuk berkomunikasi dan membuat perubahan jika dibutuhkan, tetapi tidak wajib dibutukan.",
"link": "repositori pustaka publik", "noteLicense": "Dengan mengkirimkannya, Anda setuju pustaka akan diterbitkan dibawah <link>Lisensi MIT, </link>yang artinya siapa pun dapat menggunakannya tanpa batasan.",
"post": "untuk orang lain menggunakannya dalam gambar mereka."
},
"noteGuidelines": {
"pre": "Pustaka butuh disetujui secara manual terlebih dahulu. Baca ",
"link": "pedoman",
"post": " sebelum mengirim. Anda butuh akun GitHub untuk berkomunikasi dan membuat perubahan jika dibutuhkan, tetapi tidak wajib dibutukan."
},
"noteLicense": {
"pre": "Dengan mengkirimkannya, Anda setuju pustaka akan diterbitkan dibawah ",
"link": "Lisensi MIT, ",
"post": "yang artinya siapa pun dapat menggunakannya tanpa batasan."
},
"noteItems": "Setiap item pustaka harus memiliki nama, sehingga bisa disortir. Item pustaka di bawah ini akan dimasukan:", "noteItems": "Setiap item pustaka harus memiliki nama, sehingga bisa disortir. Item pustaka di bawah ini akan dimasukan:",
"atleastOneLibItem": "Pilih setidaknya satu item pustaka untuk mulai", "atleastOneLibItem": "Pilih setidaknya satu item pustaka untuk mulai",
"republishWarning": "Catatan: beberapa item yang dipilih telah ditandai sebagai sudah dipublikasikan/diserahkan. Anda hanya dapat menyerahkan kembali item-item ketika memperbarui pustaka atau pengumpulan." "republishWarning": "Catatan: beberapa item yang dipilih telah ditandai sebagai sudah dipublikasikan/diserahkan. Anda hanya dapat menyerahkan kembali item-item ketika memperbarui pustaka atau pengumpulan."
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "Pustaka telah dikirm", "title": "Pustaka telah dikirm",
"content": "Terima kasih {{authorName}}. pustaka Anda telah diserahkan untuk ditinjau ulang. Anda dapat cek statusnya", "content": "Terima kasih {{authorName}}. pustaka Anda telah diserahkan untuk ditinjau ulang. Anda dapat cek statusnya<link>di sini</link>"
"link": "di sini"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "Reset pustaka", "resetLibrary": "Reset pustaka",

View file

@ -208,19 +208,10 @@
"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": "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.", "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.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "Sembra che tu stia usando il browser Brave con il", "line1": "",
"aggressive_block_fingerprint": "Blocco Aggressivamente Impronte Digitali", "line2": "",
"setting_enabled": "impostazioni abilitate", "line3": "",
"break": "Questo potrebbe portare a rompere gli", "line4": ""
"text_elements": "Elementi Di Testo",
"in_your_drawings": "nei tuoi disegni",
"strongly_recommend": "Si consiglia vivamente di disabilitare questa impostazione. È possibile seguire",
"steps": "questi passaggi",
"how": "su come fare",
"disable_setting": " Se la disabilitazione di questa impostazione non corregge la visualizzazione degli elementi di testo, apri un",
"issue": "problema",
"write": "sul nostro GitHub, o scrivici su",
"discord": "Discord"
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "Suggerimento: prova a spostare gli elementi più lontani più vicini tra loro." "canvasTooBigTip": "Suggerimento: prova a spostare gli elementi più lontani più vicini tra loro."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "Si è verificato un errore. Provare ", "headingMain": "Si è verificato un errore. Provare <button>ricaricando la pagina.</button>",
"headingMain_button": "ricaricando la pagina.", "clearCanvasMessage": "Se ricaricare non funziona, prova <button>pulire la tela.</button>",
"clearCanvasMessage": "Se ricaricare non funziona, prova ",
"clearCanvasMessage_button": "pulire la tela.",
"clearCanvasCaveat": " Questo risulterà nella perdita del lavoro ", "clearCanvasCaveat": " Questo risulterà nella perdita del lavoro ",
"trackedToSentry_pre": "L'errore con identificativo ", "trackedToSentry": "L'errore con identificativo {{eventId}} è stato tracciato nel nostro sistema.",
"trackedToSentry_post": " è stato tracciato nel nostro sistema.", "openIssueMessage": "Siamo stati molto cauti nel non includere informazioni della scena nell'errore. Se la tua scena non è privata, ti preghiamo di considerare la sua inclusione nel nostro <button>bug tracker.</button> Per favore includi le informazioni riportate qui sotto copiandole e incollandole nella issue di GitHub.",
"openIssueMessage_pre": "Siamo stati molto cauti nel non includere informazioni della scena nell'errore. Se la tua scena non è privata, ti preghiamo di considerare la sua inclusione nel nostro ",
"openIssueMessage_button": "bug tracker.",
"openIssueMessage_post": " Per favore includi le informazioni riportate qui sotto copiandole e incollandole nella issue di GitHub.",
"sceneContent": "Contenuto della scena:" "sceneContent": "Contenuto della scena:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "Obbligatorio", "required": "Obbligatorio",
"website": "Inserisci un URL valido" "website": "Inserisci un URL valido"
}, },
"noteDescription": { "noteDescription": "Invia la tua libreria da includere nella <link>repository della libreria pubblica</link>perché sia usata da altri nei loro disegni.",
"pre": "Invia la tua libreria da includere nella ", "noteGuidelines": "La libreria dev'esser prima approvata manualmente. Sei pregato di leggere le <link>linee guida</link> prima di inviarla. Necessiterai di un profilo di GitHub per comunicare ed effettuare modifiche se richiesto, ma non è strettamente necessario.",
"link": "repository della libreria pubblica", "noteLicense": "Inviando, acconsenti che la libreria sarà pubblicata sotto la <link>Licenza MIT, </link>che in breve significa che chiunque possa usarla senza restrizioni.",
"post": "perché sia usata da altri nei loro disegni."
},
"noteGuidelines": {
"pre": "La libreria dev'esser prima approvata manualmente. Sei pregato di leggere le ",
"link": "linee guida",
"post": " prima di inviarla. Necessiterai di un profilo di GitHub per comunicare ed effettuare modifiche se richiesto, ma non è strettamente necessario."
},
"noteLicense": {
"pre": "Inviando, acconsenti che la libreria sarà pubblicata sotto la ",
"link": "Licenza MIT, ",
"post": "che in breve significa che chiunque possa usarla senza restrizioni."
},
"noteItems": "Ogni elemento della libreria deve avere il proprio nome, così che sia filtrabile. Gli elementi della seguente libreria saranno inclusi:", "noteItems": "Ogni elemento della libreria deve avere il proprio nome, così che sia filtrabile. Gli elementi della seguente libreria saranno inclusi:",
"atleastOneLibItem": "Sei pregato di selezionare almeno un elemento della libreria per iniziare", "atleastOneLibItem": "Sei pregato di selezionare almeno un elemento della libreria per iniziare",
"republishWarning": "Nota: alcuni degli elementi selezionati sono contrassegnati come già pubblicati/presentati. È necessario reinviare gli elementi solo quando si aggiorna una libreria o una presentazione esistente." "republishWarning": "Nota: alcuni degli elementi selezionati sono contrassegnati come già pubblicati/presentati. È necessario reinviare gli elementi solo quando si aggiorna una libreria o una presentazione esistente."
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "Libreria inviata", "title": "Libreria inviata",
"content": "Grazie {{authorName}}. La tua libreria è stata inviata per la revisione. Puoi monitorarne lo stato", "content": "Grazie {{authorName}}. La tua libreria è stata inviata per la revisione. Puoi monitorarne lo stato<link>qui</link>"
"link": "qui"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "Ripristina la libreria", "resetLibrary": "Ripristina la libreria",

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。", "collabSaveFailed": "バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。",
"collabSaveFailed_sizeExceeded": "キャンバスが大きすぎるため、バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。", "collabSaveFailed_sizeExceeded": "キャンバスが大きすぎるため、バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "", "line1": "",
"aggressive_block_fingerprint": "", "line2": "",
"setting_enabled": "設定が有効化されました", "line3": "",
"break": "", "line4": ""
"text_elements": "テキスト要素",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": "Discord"
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "ヒント: 最も遠い要素をもう少し近づけてみてください。" "canvasTooBigTip": "ヒント: 最も遠い要素をもう少し近づけてみてください。"
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "エラーが発生しました。もう一度やり直してください。 ", "headingMain": "エラーが発生しました。もう一度やり直してください。 <button>ページを再読み込みする。</button>",
"headingMain_button": "ページを再読み込みする。", "clearCanvasMessage": "再読み込みがうまくいかない場合は、 <button>キャンバスを消去しています</button>",
"clearCanvasMessage": "再読み込みがうまくいかない場合は、 ",
"clearCanvasMessage_button": "キャンバスを消去しています",
"clearCanvasCaveat": " これにより作業が失われます ", "clearCanvasCaveat": " これにより作業が失われます ",
"trackedToSentry_pre": "識別子のエラー ", "trackedToSentry": "識別子のエラー {{eventId}} が我々のシステムで追跡されました。",
"trackedToSentry_post": " が我々のシステムで追跡されました。", "openIssueMessage": "エラーに関するシーン情報を含めないように非常に慎重に設定しました。もしあなたのシーンがプライベートでない場合は、私たちのフォローアップを検討してください。 <button>バグ報告</button> GitHub のIssueに以下の情報をコピーして貼り付けてください。",
"openIssueMessage_pre": "エラーに関するシーン情報を含めないように非常に慎重に設定しました。もしあなたのシーンがプライベートでない場合は、私たちのフォローアップを検討してください。 ",
"openIssueMessage_button": "バグ報告",
"openIssueMessage_post": " GitHub のIssueに以下の情報をコピーして貼り付けてください。",
"sceneContent": "シーンの内容:" "sceneContent": "シーンの内容:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "必須項目", "required": "必須項目",
"website": "有効な URL を入力してください" "website": "有効な URL を入力してください"
}, },
"noteDescription": { "noteDescription": "以下に含めるライブラリを提出してください <link>公開ライブラリのリポジトリ</link>他の人が作図に使えるようにするためです",
"pre": "以下に含めるライブラリを提出してください ", "noteGuidelines": "最初にライブラリを手動で承認する必要があります。次をお読みください <link>ガイドライン</link> 送信する前に、GitHubアカウントが必要になりますが、必須ではありません。",
"link": "公開ライブラリのリポジトリ", "noteLicense": "提出することにより、ライブラリが次の下で公開されることに同意します: <link>MIT ライセンス </link>つまり誰でも制限なく使えるということです",
"post": "他の人が作図に使えるようにするためです"
},
"noteGuidelines": {
"pre": "最初にライブラリを手動で承認する必要があります。次をお読みください ",
"link": "ガイドライン",
"post": " 送信する前に、GitHubアカウントが必要になりますが、必須ではありません。"
},
"noteLicense": {
"pre": "提出することにより、ライブラリが次の下で公開されることに同意します: ",
"link": "MIT ライセンス ",
"post": "つまり誰でも制限なく使えるということです"
},
"noteItems": "各ライブラリ項目は、フィルタリングのために独自の名前を持つ必要があります。以下のライブラリアイテムが含まれます:", "noteItems": "各ライブラリ項目は、フィルタリングのために独自の名前を持つ必要があります。以下のライブラリアイテムが含まれます:",
"atleastOneLibItem": "開始するには少なくとも1つのライブラリ項目を選択してください", "atleastOneLibItem": "開始するには少なくとも1つのライブラリ項目を選択してください",
"republishWarning": "注意: 選択された項目の中には、すでに公開/投稿済みと表示されているものがあります。既存のライブラリや投稿を更新する場合のみ、アイテムを再投稿してください。" "republishWarning": "注意: 選択された項目の中には、すでに公開/投稿済みと表示されているものがあります。既存のライブラリや投稿を更新する場合のみ、アイテムを再投稿してください。"
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "ライブラリを送信しました", "title": "ライブラリを送信しました",
"content": "{{authorName}} さん、ありがとうございます。あなたのライブラリはレビューのために提出されました。状況を追跡できます。", "content": "{{authorName}} さん、ありがとうございます。あなたのライブラリはレビューのために提出されました。状況を追跡できます。<link>こちら</link>"
"link": "こちら"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "ライブラリをリセット", "resetLibrary": "ライブラリをリセット",

474
src/locales/kaa.json Normal file
View file

@ -0,0 +1,474 @@
{
"labels": {
"paste": "",
"pasteAsPlaintext": "",
"pasteCharts": "",
"selectAll": "Barlıǵın tańlaw",
"multiSelect": "",
"moveCanvas": "",
"cut": "",
"copy": "Kóshirip alıw",
"copyAsPng": "",
"copyAsSvg": "",
"copyText": "",
"bringForward": "",
"sendToBack": "",
"bringToFront": "",
"sendBackward": "",
"delete": "Óshiriw",
"copyStyles": "",
"pasteStyles": "",
"stroke": "",
"background": "",
"fill": "",
"strokeWidth": "",
"strokeStyle": "",
"strokeStyle_solid": "",
"strokeStyle_dashed": "",
"strokeStyle_dotted": "",
"sloppiness": "",
"opacity": "",
"textAlign": "",
"edges": "",
"sharp": "",
"round": "",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "",
"arrowhead_bar": "",
"arrowhead_dot": "",
"arrowhead_triangle": "",
"fontSize": "Shrift ólshemi",
"fontFamily": "",
"onlySelected": "",
"withBackground": "",
"exportEmbedScene": "",
"exportEmbedScene_details": "",
"addWatermark": "",
"handDrawn": "",
"normal": "",
"code": "",
"small": "",
"medium": "",
"large": "",
"veryLarge": "",
"solid": "",
"hachure": "",
"zigzag": "",
"crossHatch": "",
"thin": "",
"bold": "",
"left": "",
"center": "",
"right": "",
"extraBold": "",
"architect": "",
"artist": "",
"cartoonist": "",
"fileTitle": "Fayl ataması",
"colorPicker": "",
"canvasColors": "",
"canvasBackground": "",
"drawingCanvas": "",
"layers": "",
"actions": "Háreketler",
"language": "Til",
"liveCollaboration": "",
"duplicateSelection": "Nusqa",
"untitled": "",
"name": "Ataması",
"yourName": "Atıńız",
"madeWithExcalidraw": "",
"group": "",
"ungroup": "",
"collaborators": "",
"showGrid": "",
"addToLibrary": "Kitapxanaǵa qosıw",
"removeFromLibrary": "Kitapxanadan alıp taslaw",
"libraryLoadingMessage": "Kitapxana júklenbekte…",
"libraries": "Kitapxanalardı kóriw",
"loadingScene": "",
"align": "",
"alignTop": "",
"alignBottom": "",
"alignLeft": "",
"alignRight": "",
"centerVertically": "",
"centerHorizontally": "",
"distributeHorizontally": "",
"distributeVertically": "",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "Kóriw rejimi",
"toggleExportColorScheme": "",
"share": "Bólisiw",
"showStroke": "",
"showBackground": "",
"toggleTheme": "",
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": "",
"unbindText": "",
"bindText": "",
"createContainerFromText": "",
"link": {
"edit": "Siltemeni ózgertiw",
"create": "Siltemeni jaratıw",
"label": "Silteme"
},
"lineEditor": {
"edit": "",
"exit": ""
},
"elementLock": {
"lock": "Qulıplaw",
"unlock": "Qulıptan shıǵarıw",
"lockAll": "Barlıǵın qulıplaw",
"unlockAll": "Barlıǵın qulıptan shıǵarıw"
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
},
"buttons": {
"clearReset": "",
"exportJSON": "",
"exportImage": "",
"export": "",
"exportToPng": "",
"exportToSvg": "",
"copyToClipboard": "Almasıw buferine kóshirip alındı",
"copyPngToClipboard": "",
"scale": "",
"save": "",
"saveAs": "",
"load": "Ashıw",
"getShareableLink": "",
"close": "Jabıw",
"selectLanguage": "Tildi tańlaw",
"scrollBackToContent": "",
"zoomIn": "",
"zoomOut": "",
"resetZoom": "",
"menu": "Menyu",
"done": "Tayın",
"edit": "Ózgertiw",
"undo": "",
"redo": "",
"resetLibrary": "",
"createNewRoom": "",
"fullScreen": "Tolıq ekran",
"darkMode": "Qarańǵı tema",
"lightMode": "Jaqtı tema",
"zenMode": "",
"exitZenMode": "",
"cancel": "Biykarlaw",
"clear": "Tazalaw",
"remove": "Óshiriw",
"publishLibrary": "",
"submit": "Jiberiw",
"confirm": "Tastıyıqlaw"
},
"alerts": {
"clearReset": "",
"couldNotCreateShareableLink": "",
"couldNotCreateShareableLinkTooBig": "",
"couldNotLoadInvalidFile": "",
"importBackendFailed": "",
"cannotExportEmptyCanvas": "",
"couldNotCopyToClipboard": "Almasıw buferine kóshirip alıw ámelge aspadı.",
"decryptFailed": "",
"uploadedSecurly": "",
"loadSceneOverridePrompt": "",
"collabStopOverridePrompt": "",
"errorAddingToLibrary": "",
"errorRemovingFromLibrary": "",
"confirmAddLibrary": "",
"imageDoesNotContainScene": "",
"cannotRestoreFromImage": "",
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"collabOfflineWarning": ""
},
"errors": {
"unsupportedFileType": "",
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": "Jaramsız SVG.",
"cannotResolveCollabServer": "",
"importLibraryError": "Kitapxananı júklew ámelge aspadı",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"line1": "",
"line2": "",
"line3": "",
"line4": ""
}
},
"toolBar": {
"selection": "",
"image": "Súwret qoyıw",
"rectangle": "",
"diamond": "",
"ellipse": "",
"arrow": "",
"line": "Sızıq",
"freedraw": "Sızıw",
"text": "Tekst",
"library": "Kitapxana",
"lock": "",
"penMode": "",
"link": "",
"eraser": "Óshirgish",
"hand": ""
},
"headings": {
"canvasActions": "",
"selectedShapeActions": "",
"shapes": ""
},
"hints": {
"canvasPanning": "",
"linearElement": "",
"freeDraw": "",
"text": "",
"text_selected": "",
"text_editing": "",
"linearElementMulti": "",
"lockAngle": "",
"resize": "",
"resizeImage": "",
"rotate": "",
"lineEditor_info": "",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"placeImage": "",
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "",
"canvasTooBig": "",
"canvasTooBigTip": ""
},
"errorSplash": {
"headingMain_pre": "",
"headingMain_button": "",
"clearCanvasMessage": "",
"clearCanvasMessage_button": "",
"clearCanvasCaveat": "",
"trackedToSentry_pre": "",
"trackedToSentry_post": "",
"openIssueMessage_pre": "",
"openIssueMessage_button": "",
"openIssueMessage_post": "",
"sceneContent": ""
},
"roomDialog": {
"desc_intro": "",
"desc_privacy": "",
"button_startSession": "",
"button_stopSession": "",
"desc_inProgressIntro": "",
"desc_shareLink": "",
"desc_exitSession": "",
"shareTitle": ""
},
"errorDialog": {
"title": "Qátelik"
},
"exportDialog": {
"disk_title": "",
"disk_details": "",
"disk_button": "",
"link_title": "",
"link_details": "",
"link_button": "",
"excalidrawplus_description": "",
"excalidrawplus_button": "Eksportlaw",
"excalidrawplus_exportError": ""
},
"helpDialog": {
"blog": "",
"click": "basıw",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "",
"curvedLine": "",
"documentation": "Hújjetshilik",
"doubleClick": "",
"drag": "",
"editor": "",
"editLineArrowPoints": "",
"editText": "",
"github": "",
"howto": "",
"or": "yamasa",
"preventBinding": "",
"tools": "Ásbaplar",
"shortcuts": "",
"textFinish": "",
"textNewLine": "",
"title": "Járdem",
"view": "Kóriw",
"zoomToFit": "",
"zoomToSelection": "",
"toggleElementLock": "",
"movePageUpDown": "",
"movePageLeftRight": ""
},
"clearCanvasDialog": {
"title": ""
},
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "Avtor atı",
"githubUsername": "GitHub paydalanıwshı atı",
"twitterUsername": "Twitter paydalanıwshı atı",
"libraryName": "Kitapxana ataması",
"libraryDesc": "",
"website": "Veb-sayt",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": "",
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": "usı jerde"
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": {
"tooltip": "",
"link": ""
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "Tańlandı",
"storage": "",
"title": "",
"total": "",
"version": "Versiya",
"versionCopy": "",
"versionNotAvailable": "",
"width": ""
},
"toast": {
"addedToLibrary": "",
"copyStyles": "",
"copyToClipboard": "",
"copyToClipboardAsPng": "",
"fileSaved": "Fayl saqlandı.",
"fileSavedToFilename": "",
"canvas": "",
"selection": "",
"pasteAsSingleElement": ""
},
"colors": {
"ffffff": "Aq",
"f8f9fa": "",
"f1f3f5": "",
"fff5f5": "Qızıl 0",
"fff0f6": "",
"f8f0fc": "",
"f3f0ff": "",
"edf2ff": "",
"e7f5ff": "Kók",
"e3fafc": "",
"e6fcf5": "",
"ebfbee": "",
"f4fce3": "",
"fff9db": "Sarı 0",
"fff4e6": "Qıılt sarı 0",
"transparent": "",
"ced4da": "",
"868e96": "",
"fa5252": "Qızıl 6",
"e64980": "",
"be4bdb": "",
"7950f2": "",
"4c6ef5": "",
"228be6": "Kók 6",
"15aabf": "",
"12b886": "",
"40c057": "Jasıl 6",
"82c91e": "",
"fab005": "Sarı 6",
"fd7e14": "",
"000000": "Qara",
"343a40": "",
"495057": "",
"c92a2a": "Qızıl 9",
"a61e4d": "",
"862e9c": "",
"5f3dc4": "",
"364fc7": "",
"1864ab": "Kók 9",
"0b7285": "",
"087f5b": "",
"2b8a3e": "Jasıl 9",
"5c940d": "",
"e67700": "Sarı 9",
"d9480f": "Qıılt sarı 9"
},
"welcomeScreen": {
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": "Eksportlaw, sazlawlar, tiller, ..."
},
"defaults": {
"menuHint": "Eksportlaw, sazlawlar hám basqa...",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "Ulamek asekles deg uzadur n yisefka deg ugilal. Ma ikemmel wugur, isefk ad teskelseḍ afaylu s wudem adigan akken ad tetḥeqqeḍ ur tesruḥuyeḍ ara amahil-inek•inem.", "collabSaveFailed": "Ulamek asekles deg uzadur n yisefka deg ugilal. Ma ikemmel wugur, isefk ad teskelseḍ afaylu s wudem adigan akken ad tetḥeqqeḍ ur tesruḥuyeḍ ara amahil-inek•inem.",
"collabSaveFailed_sizeExceeded": "Ulamek asekles deg uzadur n yisefka deg ugilal, taɣzut n usuneɣ tettban-d temqer aṭas. Isefk ad teskelseḍ afaylu s wudem adigan akken ad tetḥeqqeḍ ur tesruḥuyeḍ ara amahil-inek•inem.", "collabSaveFailed_sizeExceeded": "Ulamek asekles deg uzadur n yisefka deg ugilal, taɣzut n usuneɣ tettban-d temqer aṭas. Isefk ad teskelseḍ afaylu s wudem adigan akken ad tetḥeqqeḍ ur tesruḥuyeḍ ara amahil-inek•inem.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "Ittban-d am wakken tsseqdaceḍ iminig Brave akked", "line1": "",
"aggressive_block_fingerprint": "", "line2": "",
"setting_enabled": "yermed", "line3": "",
"break": "Ayagi yezmer ad d-iseglu s truẓi n", "line4": ""
"text_elements": "Iferdisen iḍrisen",
"in_your_drawings": "deg wunuɣen-inek",
"strongly_recommend": "",
"steps": "isurifen-agi",
"how": "",
"disable_setting": "",
"issue": "",
"write": "di GitHub inek neɣ aru-yaɣ-d di",
"discord": ""
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "Tixidest: eɛreḍ ad tesqerbeḍ ciṭ iferdisen yembaɛaden." "canvasTooBigTip": "Tixidest: eɛreḍ ad tesqerbeḍ ciṭ iferdisen yembaɛaden."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "Teḍra-d tuccḍa. Eɛreḍ ", "headingMain": "Teḍra-d tuccḍa. Eɛreḍ <button>asali n usebter tikkelt-nniḍen.</button>",
"headingMain_button": "asali n usebter tikkelt-nniḍen.", "clearCanvasMessage": "Ma yella tulsa n usali ur tefri ara ugur, eɛreḍ <button>asfaḍ n teɣzut n usuneɣ.</button>",
"clearCanvasMessage": "Ma yella tulsa n usali ur tefri ara ugur, eɛreḍ ",
"clearCanvasMessage_button": "asfaḍ n teɣzut n usuneɣ.",
"clearCanvasCaveat": " Ayagi ad d-iglu s usṛuḥu n umahil ", "clearCanvasCaveat": " Ayagi ad d-iglu s usṛuḥu n umahil ",
"trackedToSentry_pre": "Tuccḍa akked umesmagi ", "trackedToSentry": "Tuccḍa akked umesmagi {{eventId}} tettwasekles deg unagraw-nneɣ.",
"trackedToSentry_post": " tettwasekles deg unagraw-nneɣ.", "openIssueMessage": "Nḥuder aṭas akken ur nseddu ara talɣut n usayes-inek (m) di tuccḍa. Ma yella asayes-inek (m) mačči d amaẓlay, ttxil-k (m) xemmem ad ḍefreḍ <button>afecku n weḍfar n yibugen.</button> Ma ulac uɣilif seddu talɣut ukessar-agi s wenɣal akked usenṭeḍ di GitHub issue.",
"openIssueMessage_pre": "Nḥuder aṭas akken ur nseddu ara talɣut n usayes-inek (m) di tuccḍa. Ma yella asayes-inek (m) mačči d amaẓlay, ttxil-k (m) xemmem ad ḍefreḍ ",
"openIssueMessage_button": "afecku n weḍfar n yibugen.",
"openIssueMessage_post": " Ma ulac uɣilif seddu talɣut ukessar-agi s wenɣal akked usenṭeḍ di GitHub issue.",
"sceneContent": "Agbur n usayes:" "sceneContent": "Agbur n usayes:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "Yettwasra", "required": "Yettwasra",
"website": "Sekcem URL ameɣtu" "website": "Sekcem URL ameɣtu"
}, },
"noteDescription": { "noteDescription": "Azen tamkarḍit-inek•inem akken ad teddu di <link>akaram azayez n temkarḍit</link>i yimdanen-nniḍen ara isqedcen deg wunuɣen-nnsen.",
"pre": "Azen tamkarḍit-inek•inem akken ad teddu di ", "noteGuidelines": "Tamkarḍit teḥwaǧ ad tettwaqbel s ufus qbel. Ma ulac uɣilif ɣer <link>iwellihen</link> send ad tazneḍ. Tesriḍ amiḍan n GitHub akken ad tmmeslayeḍ yerna ad tgeḍ ibeddilen ma yelaq, maca mačči d ayen yettwaḥetmen.",
"link": "akaram azayez n temkarḍit", "noteLicense": "Mi tuzneḍ ad tqebleḍ akken tamkarḍit ad d-teffeɣ s <link>Turagt MIT, </link>ayen yebɣan ad d-yini belli yal yiwen izmer ad ten-iseqdec war tilist.",
"post": "i yimdanen-nniḍen ara isqedcen deg wunuɣen-nnsen."
},
"noteGuidelines": {
"pre": "Tamkarḍit teḥwaǧ ad tettwaqbel s ufus qbel. Ma ulac uɣilif ɣer ",
"link": "iwellihen",
"post": " send ad tazneḍ. Tesriḍ amiḍan n GitHub akken ad tmmeslayeḍ yerna ad tgeḍ ibeddilen ma yelaq, maca mačči d ayen yettwaḥetmen."
},
"noteLicense": {
"pre": "Mi tuzneḍ ad tqebleḍ akken tamkarḍit ad d-teffeɣ s ",
"link": "Turagt MIT, ",
"post": "ayen yebɣan ad d-yini belli yal yiwen izmer ad ten-iseqdec war tilist."
},
"noteItems": "Yal aferdis n temkarḍit isefk ad isɛu isem-is i yiman-is akken ad yili wamek ara yettusizdeg. Iferdisen-agi n temkarḍit ad ddun:", "noteItems": "Yal aferdis n temkarḍit isefk ad isɛu isem-is i yiman-is akken ad yili wamek ara yettusizdeg. Iferdisen-agi n temkarḍit ad ddun:",
"atleastOneLibItem": "Ma ulac uɣilif fern ma drus yiwen n uferdis n temkarḍit akken ad tebduḍ", "atleastOneLibItem": "Ma ulac uɣilif fern ma drus yiwen n uferdis n temkarḍit akken ad tebduḍ",
"republishWarning": "Tamawt: kra n yiferdisen yettwafernen ttwacerḍen ffeɣen-d/ttwaznen. Isefk ad talseḍ tuzzna n yiferdisen anagar mi ara tleqqemeḍ tamkarḍit neɣ tuzzna yellan." "republishWarning": "Tamawt: kra n yiferdisen yettwafernen ttwacerḍen ffeɣen-d/ttwaznen. Isefk ad talseḍ tuzzna n yiferdisen anagar mi ara tleqqemeḍ tamkarḍit neɣ tuzzna yellan."
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "Tamkarḍit tettwazen", "title": "Tamkarḍit tettwazen",
"content": "Tanemmirt-ik•im {{authorName}}. Tamkarḍit-inek•inem tettwazen i weselken. Tzemreḍ ad tḍefreḍ aẓayer", "content": "Tanemmirt-ik•im {{authorName}}. Tamkarḍit-inek•inem tettwazen i weselken. Tzemreḍ ad tḍefreḍ aẓayer<link>dagi</link>"
"link": "dagi"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "Ales awennez n temkarḍit", "resetLibrary": "Ales awennez n temkarḍit",

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "", "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "", "line1": "",
"aggressive_block_fingerprint": "", "line2": "",
"setting_enabled": "", "line3": "",
"break": "", "line4": ""
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "" "canvasTooBigTip": ""
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "", "headingMain": "",
"headingMain_button": "",
"clearCanvasMessage": "", "clearCanvasMessage": "",
"clearCanvasMessage_button": "",
"clearCanvasCaveat": "", "clearCanvasCaveat": "",
"trackedToSentry_pre": "", "trackedToSentry": "",
"trackedToSentry_post": "", "openIssueMessage": "",
"openIssueMessage_pre": "",
"openIssueMessage_button": "",
"openIssueMessage_post": "",
"sceneContent": "" "sceneContent": ""
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "", "required": "",
"website": "" "website": ""
}, },
"noteDescription": { "noteDescription": "",
"pre": "", "noteGuidelines": "",
"link": "", "noteLicense": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "", "noteItems": "",
"atleastOneLibItem": "", "atleastOneLibItem": "",
"republishWarning": "" "republishWarning": ""
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "", "title": "",
"content": "", "content": ""
"link": ""
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "", "resetLibrary": "",

474
src/locales/km-KH.json Normal file
View file

@ -0,0 +1,474 @@
{
"labels": {
"paste": "",
"pasteAsPlaintext": "",
"pasteCharts": "",
"selectAll": "",
"multiSelect": "",
"moveCanvas": "",
"cut": "",
"copy": "",
"copyAsPng": "",
"copyAsSvg": "",
"copyText": "",
"bringForward": "",
"sendToBack": "",
"bringToFront": "",
"sendBackward": "",
"delete": "",
"copyStyles": "",
"pasteStyles": "",
"stroke": "",
"background": "",
"fill": "",
"strokeWidth": "",
"strokeStyle": "",
"strokeStyle_solid": "",
"strokeStyle_dashed": "",
"strokeStyle_dotted": "",
"sloppiness": "",
"opacity": "",
"textAlign": "",
"edges": "",
"sharp": "",
"round": "",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "",
"arrowhead_bar": "",
"arrowhead_dot": "",
"arrowhead_triangle": "",
"fontSize": "",
"fontFamily": "",
"onlySelected": "",
"withBackground": "",
"exportEmbedScene": "",
"exportEmbedScene_details": "",
"addWatermark": "",
"handDrawn": "",
"normal": "",
"code": "",
"small": "",
"medium": "",
"large": "",
"veryLarge": "",
"solid": "",
"hachure": "",
"zigzag": "",
"crossHatch": "",
"thin": "",
"bold": "",
"left": "",
"center": "",
"right": "",
"extraBold": "",
"architect": "",
"artist": "",
"cartoonist": "",
"fileTitle": "",
"colorPicker": "",
"canvasColors": "",
"canvasBackground": "",
"drawingCanvas": "",
"layers": "",
"actions": "",
"language": "",
"liveCollaboration": "",
"duplicateSelection": "",
"untitled": "",
"name": "",
"yourName": "",
"madeWithExcalidraw": "",
"group": "",
"ungroup": "",
"collaborators": "",
"showGrid": "",
"addToLibrary": "",
"removeFromLibrary": "",
"libraryLoadingMessage": "",
"libraries": "",
"loadingScene": "",
"align": "",
"alignTop": "",
"alignBottom": "",
"alignLeft": "",
"alignRight": "",
"centerVertically": "",
"centerHorizontally": "",
"distributeHorizontally": "",
"distributeVertically": "",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "",
"toggleExportColorScheme": "",
"share": "",
"showStroke": "",
"showBackground": "",
"toggleTheme": "",
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": "",
"unbindText": "",
"bindText": "",
"createContainerFromText": "",
"link": {
"edit": "",
"create": "",
"label": ""
},
"lineEditor": {
"edit": "",
"exit": ""
},
"elementLock": {
"lock": "",
"unlock": "",
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
},
"buttons": {
"clearReset": "",
"exportJSON": "",
"exportImage": "",
"export": "",
"exportToPng": "",
"exportToSvg": "",
"copyToClipboard": "",
"copyPngToClipboard": "",
"scale": "",
"save": "",
"saveAs": "",
"load": "",
"getShareableLink": "",
"close": "",
"selectLanguage": "",
"scrollBackToContent": "",
"zoomIn": "",
"zoomOut": "",
"resetZoom": "",
"menu": "",
"done": "",
"edit": "",
"undo": "",
"redo": "",
"resetLibrary": "",
"createNewRoom": "",
"fullScreen": "",
"darkMode": "",
"lightMode": "",
"zenMode": "",
"exitZenMode": "",
"cancel": "",
"clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
},
"alerts": {
"clearReset": "",
"couldNotCreateShareableLink": "",
"couldNotCreateShareableLinkTooBig": "",
"couldNotLoadInvalidFile": "",
"importBackendFailed": "",
"cannotExportEmptyCanvas": "",
"couldNotCopyToClipboard": "",
"decryptFailed": "",
"uploadedSecurly": "",
"loadSceneOverridePrompt": "",
"collabStopOverridePrompt": "",
"errorAddingToLibrary": "",
"errorRemovingFromLibrary": "",
"confirmAddLibrary": "",
"imageDoesNotContainScene": "",
"cannotRestoreFromImage": "",
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"collabOfflineWarning": ""
},
"errors": {
"unsupportedFileType": "",
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": "",
"cannotResolveCollabServer": "",
"importLibraryError": "",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"line1": "",
"line2": "",
"line3": "",
"line4": ""
}
},
"toolBar": {
"selection": "",
"image": "",
"rectangle": "",
"diamond": "",
"ellipse": "",
"arrow": "",
"line": "",
"freedraw": "",
"text": "",
"library": "",
"lock": "",
"penMode": "",
"link": "",
"eraser": "",
"hand": ""
},
"headings": {
"canvasActions": "",
"selectedShapeActions": "",
"shapes": ""
},
"hints": {
"canvasPanning": "",
"linearElement": "",
"freeDraw": "",
"text": "",
"text_selected": "",
"text_editing": "",
"linearElementMulti": "",
"lockAngle": "",
"resize": "",
"resizeImage": "",
"rotate": "",
"lineEditor_info": "",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"placeImage": "",
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "",
"canvasTooBig": "",
"canvasTooBigTip": ""
},
"errorSplash": {
"headingMain_pre": "",
"headingMain_button": "",
"clearCanvasMessage": "",
"clearCanvasMessage_button": "",
"clearCanvasCaveat": "",
"trackedToSentry_pre": "",
"trackedToSentry_post": "",
"openIssueMessage_pre": "",
"openIssueMessage_button": "",
"openIssueMessage_post": "",
"sceneContent": ""
},
"roomDialog": {
"desc_intro": "",
"desc_privacy": "",
"button_startSession": "",
"button_stopSession": "",
"desc_inProgressIntro": "",
"desc_shareLink": "",
"desc_exitSession": "",
"shareTitle": ""
},
"errorDialog": {
"title": ""
},
"exportDialog": {
"disk_title": "",
"disk_details": "",
"disk_button": "",
"link_title": "",
"link_details": "",
"link_button": "",
"excalidrawplus_description": "",
"excalidrawplus_button": "",
"excalidrawplus_exportError": ""
},
"helpDialog": {
"blog": "",
"click": "",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "",
"curvedLine": "",
"documentation": "",
"doubleClick": "",
"drag": "",
"editor": "",
"editLineArrowPoints": "",
"editText": "",
"github": "",
"howto": "",
"or": "",
"preventBinding": "",
"tools": "",
"shortcuts": "",
"textFinish": "",
"textNewLine": "",
"title": "",
"view": "",
"zoomToFit": "",
"zoomToSelection": "",
"toggleElementLock": "",
"movePageUpDown": "",
"movePageLeftRight": ""
},
"clearCanvasDialog": {
"title": ""
},
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": "",
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": {
"tooltip": "",
"link": ""
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"version": "",
"versionCopy": "",
"versionNotAvailable": "",
"width": ""
},
"toast": {
"addedToLibrary": "",
"copyStyles": "",
"copyToClipboard": "",
"copyToClipboardAsPng": "",
"fileSaved": "",
"fileSavedToFilename": "",
"canvas": "",
"selection": "",
"pasteAsSingleElement": ""
},
"colors": {
"ffffff": "",
"f8f9fa": "",
"f1f3f5": "",
"fff5f5": "",
"fff0f6": "",
"f8f0fc": "",
"f3f0ff": "",
"edf2ff": "",
"e7f5ff": "",
"e3fafc": "",
"e6fcf5": "",
"ebfbee": "",
"f4fce3": "",
"fff9db": "",
"fff4e6": "",
"transparent": "",
"ced4da": "",
"868e96": "",
"fa5252": "",
"e64980": "",
"be4bdb": "",
"7950f2": "",
"4c6ef5": "",
"228be6": "",
"15aabf": "",
"12b886": "",
"40c057": "",
"82c91e": "",
"fab005": "",
"fd7e14": "",
"000000": "",
"343a40": "",
"495057": "",
"c92a2a": "",
"a61e4d": "",
"862e9c": "",
"5f3dc4": "",
"364fc7": "",
"1864ab": "",
"0b7285": "",
"087f5b": "",
"2b8a3e": "",
"5c940d": "",
"e67700": "",
"d9480f": ""
},
"welcomeScreen": {
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "데이터베이스에 저장하지 못했습니다. 문제가 계속 된다면, 작업 내용을 잃지 않도록 로컬 저장소에 저장해 주세요.", "collabSaveFailed": "데이터베이스에 저장하지 못했습니다. 문제가 계속 된다면, 작업 내용을 잃지 않도록 로컬 저장소에 저장해 주세요.",
"collabSaveFailed_sizeExceeded": "데이터베이스에 저장하지 못했습니다. 캔버스가 너무 큰 거 같습니다. 문제가 계속 된다면, 작업 내용을 잃지 않도록 로컬 저장소에 저장해 주세요.", "collabSaveFailed_sizeExceeded": "데이터베이스에 저장하지 못했습니다. 캔버스가 너무 큰 거 같습니다. 문제가 계속 된다면, 작업 내용을 잃지 않도록 로컬 저장소에 저장해 주세요.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "귀하께서는", "line1": "귀하께서는 <bold>강력한 지문 차단 설정</bold>이 활성화된 Brave browser를 사용하고 계신 것 같습니다.",
"aggressive_block_fingerprint": "강력한 지문 차단", "line2": "이 기능으로 인해 화이트보드의 <bold>텍스트 요소들</bold>이 손상될 수 있습니다.",
"setting_enabled": "설정이 활성화된 Brave browser를 사용하고 계신 것 같습니다", "line3": "저희는 해당 기능을 비활성화하는 것을 강력히 권장 드립니다. 비활성화 방법에 대해서는 <link>이 게시글</link>을 참고해주세요.",
"break": "이 기능으로 인해 화이트보드의", "line4": "만약 이 설정을 껐음에도 텍스트 요소들이 올바르게 표시되지 않는다면, 저희 Github에 <issueLink>이슈</issueLink>를 올려주시거나 <discordLink>Discord</discordLink>로 알려주세요."
"text_elements": "텍스트 요소들이",
"in_your_drawings": "손상될 수 있습니다",
"strongly_recommend": "해당 기능을 설정에서 비활성화하는 것을 강력히 권장 드립니다. 비활성화 방법에 대해서는",
"steps": "이 게시글을",
"how": "참고해주세요.",
"disable_setting": " 만약 이 설정을 껐음에도 텍스트 요소들이 올바르게 표시되지 않는다면, 저희",
"issue": "Github에 이슈를",
"write": "올려주시거나",
"discord": "Discord에 제보해주세요"
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "팁: 멀리 있는 요소들을 좀 더 가까이로 붙여 보세요." "canvasTooBigTip": "팁: 멀리 있는 요소들을 좀 더 가까이로 붙여 보세요."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "오류가 발생했습니다. ", "headingMain": "오류가 발생했습니다. <button>페이지 새로고침</button>",
"headingMain_button": "페이지 새로고침", "clearCanvasMessage": "새로고침으로 해결되지 않을 경우, <button>캔버스 비우기</button>",
"clearCanvasMessage": "새로고침으로 해결되지 않을 경우, ",
"clearCanvasMessage_button": "캔버스 비우기",
"clearCanvasCaveat": " 작업 내용을 잃게 됩니다 ", "clearCanvasCaveat": " 작업 내용을 잃게 됩니다 ",
"trackedToSentry_pre": "오류 ", "trackedToSentry": "오류 {{eventId}} 가 시스템에서 발견되었습니다.",
"trackedToSentry_post": " 가 시스템에서 발견되었습니다.", "openIssueMessage": "저희는 화면 정보를 오류에 포함하지 않도록 매우 주의하고 있습니다. 혹시 화면에 민감한 내용이 없다면 이곳에 업로드를 고려해주세요.<button>버그 트래커</button> 아래 정보를 GitHub 이슈에 복사 및 붙여넣기해 주세요.",
"openIssueMessage_pre": "저희는 화면 정보를 오류에 포함하지 않도록 매우 주의하고 있습니다. 혹시 화면에 민감한 내용이 없다면 이곳에 업로드를 고려해주세요.",
"openIssueMessage_button": "버그 트래커",
"openIssueMessage_post": " 아래 정보를 GitHub 이슈에 복사 및 붙여넣기해 주세요.",
"sceneContent": "화면 내용:" "sceneContent": "화면 내용:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "필수사항", "required": "필수사항",
"website": "유효한 URL을 입력하세요" "website": "유효한 URL을 입력하세요"
}, },
"noteDescription": { "noteDescription": "당신의 라이브러리를 제출하여 <link>공개 라이브러리 저장소</link>에서 다른 사람들의 그림에 사용할 수 있도록 하세요.",
"pre": "당신의 라이브러리를 제출하여 ", "noteGuidelines": "라이브러리는 먼저 수동으로 승인되어야 합니다. 제출하기 전에 <link>가이드라인</link>을 먼저 읽어보세요. 의견을 공유하거나 변경사항을 만들기 위해선 GitHub 계정이 필요하지만, 반드시 필요하진 않습니다.",
"link": "공개 라이브러리 저장소", "noteLicense": "제출함으로써, 당신은 라이브러리가 <link>MIT 라이선스 </link>하에 배포됨을, 즉 아무나 제약 없이 사용할 수 있음에 동의합니다.",
"post": "에서 다른 사람들의 그림에 사용할 수 있도록 하세요."
},
"noteGuidelines": {
"pre": "라이브러리는 먼저 수동으로 승인되어야 합니다. 제출하기 전에 ",
"link": "가이드라인",
"post": "을 먼저 읽어보세요. 의견을 공유하거나 변경사항을 만들기 위해선 GitHub 계정이 필요하지만, 반드시 필요하진 않습니다."
},
"noteLicense": {
"pre": "제출함으로써, 당신은 라이브러리가 ",
"link": "MIT 라이선스 ",
"post": "하에 배포됨을, 즉 아무나 제약 없이 사용할 수 있음에 동의합니다."
},
"noteItems": "각각의 라이브러리는 분류할 수 있도록 고유한 이름을 가져야 합니다. 다음의 라이브러리 항목이 포함됩니다:", "noteItems": "각각의 라이브러리는 분류할 수 있도록 고유한 이름을 가져야 합니다. 다음의 라이브러리 항목이 포함됩니다:",
"atleastOneLibItem": "최소한 하나의 라이브러리를 선택해주세요", "atleastOneLibItem": "최소한 하나의 라이브러리를 선택해주세요",
"republishWarning": "참고: 선택된 항목의 일부는 이미 제출/게시되었습니다. 기존의 라이브러리나 제출물을 업데이트하는 경우에만 제출하세요." "republishWarning": "참고: 선택된 항목의 일부는 이미 제출/게시되었습니다. 기존의 라이브러리나 제출물을 업데이트하는 경우에만 제출하세요."
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "라이브러리 제출됨", "title": "라이브러리 제출됨",
"content": "{{authorName}}님 감사합니다. 당신의 라이브러리가 심사를 위해 제출되었습니다. 진행 상황을", "content": "{{authorName}}님 감사합니다. 당신의 라이브러리가 심사를 위해 제출되었습니다. 진행 상황을<link>여기에서 확인하실 수 있습니다.</link>"
"link": "여기에서 확인하실 수 있습니다."
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "라이브러리 리셋", "resetLibrary": "라이브러리 리셋",

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "نەتوانرا لە بنکەدراوەی ڕاژەدا پاشەکەوت بکرێت. ئەگەر کێشەکان بەردەوام بوون، پێویستە فایلەکەت لە ناوخۆدا هەڵبگریت بۆ ئەوەی دڵنیا بیت کە کارەکانت لەدەست نادەیت.", "collabSaveFailed": "نەتوانرا لە بنکەدراوەی ڕاژەدا پاشەکەوت بکرێت. ئەگەر کێشەکان بەردەوام بوون، پێویستە فایلەکەت لە ناوخۆدا هەڵبگریت بۆ ئەوەی دڵنیا بیت کە کارەکانت لەدەست نادەیت.",
"collabSaveFailed_sizeExceeded": "نەتوانرا لە بنکەدراوەی ڕاژەدا پاشەکەوت بکرێت، پێدەچێت تابلۆکە زۆر گەورە بێت. پێویستە فایلەکە لە ناوخۆدا هەڵبگریت بۆ ئەوەی دڵنیا بیت کە کارەکانت لەدەست نادەیت.", "collabSaveFailed_sizeExceeded": "نەتوانرا لە بنکەدراوەی ڕاژەدا پاشەکەوت بکرێت، پێدەچێت تابلۆکە زۆر گەورە بێت. پێویستە فایلەکە لە ناوخۆدا هەڵبگریت بۆ ئەوەی دڵنیا بیت کە کارەکانت لەدەست نادەیت.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "پێدەچێت وێبگەڕی Brave بەکاربهێنیت لەگەڵ", "line1": "وادیارە وێبگەڕی Brave بەکاردەهێنیت و ڕێکخستنی <bold>Aggressively Block Fingerprinting</bold> ـت چالاک کردووە.",
"aggressive_block_fingerprint": "بلۆککردنی Fingerprinting بەشێوەیەکی توندوتیژانە", "line2": "ئەمە ئەکرێ ببێتە هۆی تێکدانی <bold>دانە دەقییەکان</bold> لە وێنەکێشانەکانتدا.",
"setting_enabled": "ڕێکخستن چالاک کراوە", "line3": "ئێمە بە توندی پێشنیاری لەکارخستنی ئەم ڕێکخستنە دەکەین. بۆ لە کارخستنی دەتوانیت بەم <link>هەنگاوانە</link>دا بڕۆیت.",
"break": "ئەمە دەکرێت ببێتە هۆی تێکدانی", "line4": "ئەگەر لەکارخستنی ئەم ڕێکخستنە نەبوە هۆی چاککردنەوەی پێشاندانی دانە دەقییەکان، تکایە <issueLink>کێشە</issueLink>یەک بکەرەوە لەسەر گیتهەبەکەمان، یان بۆمان بنوسە لەسەر <discordLink>دیسکۆرد</discordLink>"
"text_elements": "دانە دەقییەکان",
"in_your_drawings": "لە وێنەکێشانەکانتدا",
"strongly_recommend": "بە توندی پێشنیار دەکەین ئەم ڕێکخستنە لەکاربخەیت. دەتوانیت بڕۆیت بە دوای",
"steps": "ئەم هەنگاوانەدا",
"how": "بۆ ئەوەی ئەنجامی بدەیت",
"disable_setting": " ئەگەر لەکارخستنی ئەم ڕێکخستنە پیشاندانی توخمەکانی دەق چاک نەکاتەوە، تکایە هەڵبستە بە کردنەوەی",
"issue": "کێشەیەک",
"write": "لەسەر گیتهەبەکەمان، یان بۆمان بنوسە لە",
"discord": "دیسکۆرد"
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "زانیاری: هەوڵ بدە دوورترین توخمەکان کەمێک لە یەکتر نزیک بکەوە." "canvasTooBigTip": "زانیاری: هەوڵ بدە دوورترین توخمەکان کەمێک لە یەکتر نزیک بکەوە."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "تووشی هەڵەیەک بوو. هەوڵ بدە ", "headingMain": "تووشی هەڵەیەک بوو. هەوڵ بدە <button>دووبارە بارکردنی لاپەڕەکە.</button>",
"headingMain_button": "دووبارە بارکردنی لاپەڕەکە.", "clearCanvasMessage": "ئەگەر دووبارە بارکردنەوە کار ناکات، هەوڵبدە <button>خاوێنکردنەوەی تابلۆکە.</button>",
"clearCanvasMessage": "ئەگەر دووبارە بارکردنەوە کار ناکات، هەوڵبدە ",
"clearCanvasMessage_button": "خاوێنکردنەوەی تابلۆکە.",
"clearCanvasCaveat": " ئەمە دەبێتە هۆی لەدەستدانی ئەوەی کە کردوتە ", "clearCanvasCaveat": " ئەمە دەبێتە هۆی لەدەستدانی ئەوەی کە کردوتە ",
"trackedToSentry_pre": "هەڵەکە لەگەڵ ناسێنەری ", "trackedToSentry": "هەڵەکە لەگەڵ ناسێنەری {{eventId}} لەسەر سیستەمەکەمان بەدواداچوونی بۆ کرا.",
"trackedToSentry_post": " لەسەر سیستەمەکەمان بەدواداچوونی بۆ کرا.", "openIssueMessage": "ئێمە زۆر وریا بووین کە زانیارییەکانی دیمەنەکەت لەسەر هەڵەکە نەخەینەڕوو. ئەگەر دیمەنەکەت تایبەت نییە، تکایە بیر لە بەدواداچوون بکەنەوە بۆ ئێمە <button>شوێنپێهەڵگری هەڵە.</button> تکایە ئەم زانیارییانەی خوارەوە کۆپی بکە و لە بەشی کێشەکانی Github دایبنێ.",
"openIssueMessage_pre": "ئێمە زۆر وریا بووین کە زانیارییەکانی دیمەنەکەت لەسەر هەڵەکە نەخەینەڕوو. ئەگەر دیمەنەکەت تایبەت نییە، تکایە بیر لە بەدواداچوون بکەنەوە بۆ ئێمە ",
"openIssueMessage_button": "شوێنپێهەڵگری هەڵە.",
"openIssueMessage_post": " تکایە ئەم زانیارییانەی خوارەوە کۆپی بکە و لە بەشی کێشەکانی Github دایبنێ.",
"sceneContent": "پێکهاتەی ناو دیمەنەکە:" "sceneContent": "پێکهاتەی ناو دیمەنەکە:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "داواکراوە", "required": "داواکراوە",
"website": "URLێکی دروست تێبنووسە" "website": "URLێکی دروست تێبنووسە"
}, },
"noteDescription": { "noteDescription": "کتێبخانەکەت بنێرە بۆ ئەوەی بخرێتە ناو <link>کۆگای کتێبخانەی گشتی</link>بۆ ئەوەی کەسانی تر لە وێنەکێشانەکانیاندا بەکاری بهێنن.",
"pre": "کتێبخانەکەت بنێرە بۆ ئەوەی بخرێتە ناو ", "noteGuidelines": "کتێبخانەکە پێویستە سەرەتا بە دەست پەسەند بکرێت. تکایە بفەرمو بە خوێندنەوەی <link>ڕێنماییەکان</link> پێش پێشکەشکردن. پێویستت بە ئەژمێری GitHub دەبێت بۆ پەیوەندیکردن و گۆڕانکاری ئەگەر داوای لێکرا، بەڵام بە توندی پێویست نییە.",
"link": "کۆگای کتێبخانەی گشتی", "noteLicense": "بە پێشکەشکردن، تۆ ڕەزامەندیت لەسەر بڵاوکردنەوەی کتێبخانەکە بەپێی <link>مۆڵەتی MIT، </link>کە بە کورتی مانای ئەوەیە کە هەرکەسێک دەتوانێت بە بێ سنوور بەکاری بهێنێت",
"post": "بۆ ئەوەی کەسانی تر لە وێنەکێشانەکانیاندا بەکاری بهێنن."
},
"noteGuidelines": {
"pre": "کتێبخانەکە پێویستە سەرەتا بە دەست پەسەند بکرێت. تکایە بفەرمو بە خوێندنەوەی ",
"link": "ڕێنماییەکان",
"post": " پێش پێشکەشکردن. پێویستت بە ئەژمێری GitHub دەبێت بۆ پەیوەندیکردن و گۆڕانکاری ئەگەر داوای لێکرا، بەڵام بە توندی پێویست نییە."
},
"noteLicense": {
"pre": "بە پێشکەشکردن، تۆ ڕەزامەندیت لەسەر بڵاوکردنەوەی کتێبخانەکە بەپێی ",
"link": "مۆڵەتی MIT، ",
"post": "کە بە کورتی مانای ئەوەیە کە هەرکەسێک دەتوانێت بە بێ سنوور بەکاری بهێنێت"
},
"noteItems": "هەر شتێکی کتێبخانە دەبێت ناوی تایبەتی خۆی هەبێت بۆ ئەوەی بتوانرێت فلتەر بکرێت. ئەم بابەتانەی کتێبخانانەی خوارەوە لەخۆدەگرێت:", "noteItems": "هەر شتێکی کتێبخانە دەبێت ناوی تایبەتی خۆی هەبێت بۆ ئەوەی بتوانرێت فلتەر بکرێت. ئەم بابەتانەی کتێبخانانەی خوارەوە لەخۆدەگرێت:",
"atleastOneLibItem": "تکایە بەلایەنی کەمەوە یەک بڕگەی کتێبخانە دیاریبکە بۆ دەستپێکردن", "atleastOneLibItem": "تکایە بەلایەنی کەمەوە یەک بڕگەی کتێبخانە دیاریبکە بۆ دەستپێکردن",
"republishWarning": "تێبینی: هەندێک لە ئایتمە دیاریکراوەکان نیشانکراون وەک ئەوەی پێشتر بڵاوکراونەتەوە/نێردراون. تەنها پێویستە شتەکان دووبارە پێشکەش بکەیتەوە لە کاتی نوێکردنەوەی کتێبخانەیەکی هەبوو یان پێشکەشکردن." "republishWarning": "تێبینی: هەندێک لە ئایتمە دیاریکراوەکان نیشانکراون وەک ئەوەی پێشتر بڵاوکراونەتەوە/نێردراون. تەنها پێویستە شتەکان دووبارە پێشکەش بکەیتەوە لە کاتی نوێکردنەوەی کتێبخانەیەکی هەبوو یان پێشکەشکردن."
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "کتێبخانە پێشکەش کرا", "title": "کتێبخانە پێشکەش کرا",
"content": "سوپاس {{authorName}}. کتێبخانەکەت پێشکەش کراوە بۆ پێداچوونەوە. دەتوانیت بەدواداچوون بۆ دۆخەکە بکەیت", "content": "سوپاس {{authorName}}. کتێبخانەکەت پێشکەش کراوە بۆ پێداچوونەوە. دەتوانیت بەدواداچوون بۆ دۆخەکە بکەیت<link>لێرە</link>"
"link": "لێرە"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "ڕێکخستنەوەی کتێبخانە", "resetLibrary": "ڕێکخستنەوەی کتێبخانە",

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "", "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "", "line1": "",
"aggressive_block_fingerprint": "", "line2": "",
"setting_enabled": "", "line3": "",
"break": "", "line4": ""
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "" "canvasTooBigTip": ""
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "", "headingMain": "",
"headingMain_button": "",
"clearCanvasMessage": "", "clearCanvasMessage": "",
"clearCanvasMessage_button": "",
"clearCanvasCaveat": "", "clearCanvasCaveat": "",
"trackedToSentry_pre": "", "trackedToSentry": "",
"trackedToSentry_post": "", "openIssueMessage": "",
"openIssueMessage_pre": "",
"openIssueMessage_button": "",
"openIssueMessage_post": "",
"sceneContent": "" "sceneContent": ""
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "Privalomas", "required": "Privalomas",
"website": "Įveskite teisingą nuorodą (URL)" "website": "Įveskite teisingą nuorodą (URL)"
}, },
"noteDescription": { "noteDescription": "Pateik savo biblioteką, jog ji galėtų būti įtraukta į <link></link>jog kiti žmonės galėtų tai naudoti savo piešiniuose.",
"pre": "Pateik savo biblioteką, jog ji galėtų būti įtraukta į ", "noteGuidelines": "Visų pirma, biblioteka turi būti rankiniu būdu patvirtinta. Prašome paskaityti <link>gairės</link>",
"link": "", "noteLicense": "<link>MIT licencija, </link>",
"post": "jog kiti žmonės galėtų tai naudoti savo piešiniuose."
},
"noteGuidelines": {
"pre": "Visų pirma, biblioteka turi būti rankiniu būdu patvirtinta. Prašome paskaityti ",
"link": "gairės",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "MIT licencija, ",
"post": ""
},
"noteItems": "", "noteItems": "",
"atleastOneLibItem": "", "atleastOneLibItem": "",
"republishWarning": "" "republishWarning": ""
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "Biblioteka pateikta", "title": "Biblioteka pateikta",
"content": "Ačiū {{authorName}}. Tavo biblioteka buvo pateikta peržiūrai. Gali sekti būseną", "content": "Ačiū {{authorName}}. Tavo biblioteka buvo pateikta peržiūrai. Gali sekti būseną<link>čia</link>"
"link": "čia"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "Atstatyti biblioteką", "resetLibrary": "Atstatyti biblioteką",

View file

@ -208,19 +208,10 @@
"collabSaveFailed": "Darbs nav saglabāts datubāzē. Ja problēma turpinās, saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.", "collabSaveFailed": "Darbs nav saglabāts datubāzē. Ja problēma turpinās, saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.",
"collabSaveFailed_sizeExceeded": "Darbs nav saglabāts datubāzē, šķiet, ka tāfele ir pārāk liela. Saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.", "collabSaveFailed_sizeExceeded": "Darbs nav saglabāts datubāzē, šķiet, ka tāfele ir pārāk liela. Saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.",
"brave_measure_text_error": { "brave_measure_text_error": {
"start": "Izskatās, ka izmanto Brave interneta plārlūku ar ieslēgtu", "line1": "",
"aggressive_block_fingerprint": "Aggressively Block Fingerprinting", "line2": "",
"setting_enabled": "ieslēgtu iestatījumu", "line3": "",
"break": "Tas var salauzt", "line4": ""
"text_elements": "Teksta elementus",
"in_your_drawings": "tavos zīmējumos",
"strongly_recommend": "Mēs iesakām izslēgt šo iestatījumu. Tu vari sekot",
"steps": "šiem soļiem",
"how": "kā to izdarīt",
"disable_setting": " Ja šī iestatījuma izslēgšana neatrisina teksta elementu attēlošanu, tad, lūdzu, atver",
"issue": "problēmu",
"write": "mūsu GitHub vai raksti mums",
"discord": "Discord"
} }
}, },
"toolBar": { "toolBar": {
@ -273,16 +264,11 @@
"canvasTooBigTip": "Ieteikums: mēģiniet satuvināt pašus tālākos elementus." "canvasTooBigTip": "Ieteikums: mēģiniet satuvināt pašus tālākos elementus."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "Notikusi kļūda. Mēģiniet ", "headingMain": "Notikusi kļūda. Mēģiniet <button>pārlādēt lapu.</button>",
"headingMain_button": "pārlādēt lapu.", "clearCanvasMessage": "Ja pārlādēšana nestrādā, mēģiniet <button>notīrot tāfeli.</button>",
"clearCanvasMessage": "Ja pārlādēšana nestrādā, mēģiniet ",
"clearCanvasMessage_button": "notīrot tāfeli.",
"clearCanvasCaveat": " Tas novedīs pie darba zaudēšanas ", "clearCanvasCaveat": " Tas novedīs pie darba zaudēšanas ",
"trackedToSentry_pre": "Kļūda ar kodu ", "trackedToSentry": "Kļūda ar kodu {{eventId}} tika noteikta mūsu sistēmā.",
"trackedToSentry_post": " tika noteikta mūsu sistēmā.", "openIssueMessage": "Mēs uzmanījāmies, lai neiekļautu jūsu ainas informāciju šajā kļūdā. Ja jūsu aina nav privāta, lūdzu ziņojiet par šo kļūdu mūsu <button>kļūdu uzskaitē.</button> Lūdzu, miniet sekojošo informāciju to kopējot un ielīmējot jūsu ziņojumā platformā GitHub.",
"openIssueMessage_pre": "Mēs uzmanījāmies, lai neiekļautu jūsu ainas informāciju šajā kļūdā. Ja jūsu aina nav privāta, lūdzu ziņojiet par šo kļūdu mūsu ",
"openIssueMessage_button": "kļūdu uzskaitē.",
"openIssueMessage_post": " Lūdzu, miniet sekojošo informāciju to kopējot un ielīmējot jūsu ziņojumā platformā GitHub.",
"sceneContent": "Ainas saturs:" "sceneContent": "Ainas saturs:"
}, },
"roomDialog": { "roomDialog": {
@ -362,29 +348,16 @@
"required": "Obligāts", "required": "Obligāts",
"website": "Ievadiet derīgu URL" "website": "Ievadiet derīgu URL"
}, },
"noteDescription": { "noteDescription": "Iesniegt savu bibliotēku iekļaušanai <link>publiskajā bibliotēku datubāzē</link>, lai citi to varētu izmantot savos zīmējumos.",
"pre": "Iesniegt savu bibliotēku iekļaušanai ", "noteGuidelines": "Šai bibliotēkai vispirms jātiek manuāli apstiprinātai. Lūdzu, izlasiet <link>norādījumus</link> pirms iesniegšanas. Jums vajadzēs GitHub kontu, lai sazinātos un veiktu izmaiņas, ja tādas būs pieprasītas, bet tas nav absolūti nepieciešams.",
"link": "publiskajā bibliotēku datubāzē", "noteLicense": "Iesniedzot bibliotēku, jūs piekrītat tās publicēšanai saskaņā ar <link>MIT Licenci, </link>kas īsumā nozīmē, ka jebkurš to varēs izmantot bez ierobežojumiem.",
"post": ", lai citi to varētu izmantot savos zīmējumos."
},
"noteGuidelines": {
"pre": "Šai bibliotēkai vispirms jātiek manuāli apstiprinātai. Lūdzu, izlasiet ",
"link": "norādījumus",
"post": " pirms iesniegšanas. Jums vajadzēs GitHub kontu, lai sazinātos un veiktu izmaiņas, ja tādas būs pieprasītas, bet tas nav absolūti nepieciešams."
},
"noteLicense": {
"pre": "Iesniedzot bibliotēku, jūs piekrītat tās publicēšanai saskaņā ar ",
"link": "MIT Licenci, ",
"post": "kas īsumā nozīmē, ka jebkurš to varēs izmantot bez ierobežojumiem."
},
"noteItems": "Katram bibliotēkas vienumam jābūt savam nosaukumam, lai to varētu atrast filtrējot. Tiks iekļauti sekojošie bibliotēkas vienumi:", "noteItems": "Katram bibliotēkas vienumam jābūt savam nosaukumam, lai to varētu atrast filtrējot. Tiks iekļauti sekojošie bibliotēkas vienumi:",
"atleastOneLibItem": "Lūdzu, atlasiet vismaz vienu bibliotēkas vienumu, lai sāktu darbu", "atleastOneLibItem": "Lūdzu, atlasiet vismaz vienu bibliotēkas vienumu, lai sāktu darbu",
"republishWarning": "Ievēro: daži no atzīmētajiem objektiem jau atzīmēti kā publicēti vai iesniegti publicēšanai. Tos vajadzētu atkārtoti iesniegt tikai tad, ja vēlies labot esošo bibliotēku." "republishWarning": "Ievēro: daži no atzīmētajiem objektiem jau atzīmēti kā publicēti vai iesniegti publicēšanai. Tos vajadzētu atkārtoti iesniegt tikai tad, ja vēlies labot esošo bibliotēku."
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "Bibliotēka iesniegta", "title": "Bibliotēka iesniegta",
"content": "Paldies, {{authorName}}! Jūsu bibliotēka iesniegta izskatīšanai. Jūs varat izsekot iesnieguma statusam", "content": "Paldies, {{authorName}}! Jūsu bibliotēka iesniegta izskatīšanai. Jūs varat izsekot iesnieguma statusam<link>šeit</link>"
"link": "šeit"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "Atiestatīt bibliotēku", "resetLibrary": "Atiestatīt bibliotēku",

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