mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-10-09 00:59:52 +02:00
Compare commits
103 Commits
v0.12.0
...
perf_debug
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1d65536360 | ||
![]() |
d731a6463c | ||
![]() |
9f325a626e | ||
![]() |
41200ea28d | ||
![]() |
46a61ad4df | ||
![]() |
f4b1a30bef | ||
![]() |
32aa79164b | ||
![]() |
b5fd904808 | ||
![]() |
8f8dd1105f | ||
![]() |
b914ad41fc | ||
![]() |
551c38f60b | ||
![]() |
38e8ae46c9 | ||
![]() |
ad0c4c4c78 | ||
![]() |
27cf5ed17e | ||
![]() |
fd946adbae | ||
![]() |
c37977af4b | ||
![]() |
a0d413ab4e | ||
![]() |
b67a2b4f65 | ||
![]() |
5a8dbe8030 | ||
![]() |
731093f631 | ||
![]() |
fe56975f19 | ||
![]() |
2d800feeeb | ||
![]() |
93cccd596a | ||
![]() |
45b592227d | ||
![]() |
b818df1098 | ||
![]() |
4359e2935d | ||
![]() |
3d9d398378 | ||
![]() |
0a5da0269f | ||
![]() |
08ce7c7fc3 | ||
![]() |
fe7fbff7f6 | ||
![]() |
501397cb61 | ||
![]() |
865d29388c | ||
![]() |
54c7ec416a | ||
![]() |
aca284057d | ||
![]() |
2820cd112e | ||
![]() |
426b5d9537 | ||
![]() |
e7d34677c6 | ||
![]() |
3d5356cb8e | ||
![]() |
46f5ce5ce0 | ||
![]() |
b00bd3d6c0 | ||
![]() |
91fc22182c | ||
![]() |
966ca2ffa6 | ||
![]() |
2b049b4a65 | ||
![]() |
339212e563 | ||
![]() |
f8b4bb66b4 | ||
![]() |
f4312bba5e | ||
![]() |
ac66665b64 | ||
![]() |
2b71a1f0bd | ||
![]() |
58845e450a | ||
![]() |
15d79f8fee | ||
![]() |
958ebeae61 | ||
![]() |
31f51ca53b | ||
![]() |
5abbf73050 | ||
![]() |
7cf766630d | ||
![]() |
59fccafeac | ||
![]() |
19a6996e6b | ||
![]() |
86c4f90910 | ||
![]() |
4d88112021 | ||
![]() |
de5c63e299 | ||
![]() |
da0853a121 | ||
![]() |
57cc4b6a29 | ||
![]() |
e2ddd7b27a | ||
![]() |
693de8501e | ||
![]() |
c6df6d444e | ||
![]() |
ad5692c5f8 | ||
![]() |
60ab3337af | ||
![]() |
dd847793d2 | ||
![]() |
6d6e9f0dd3 | ||
![]() |
0fe0d7ca6b | ||
![]() |
abcf1f1bae | ||
![]() |
7d0b03f754 | ||
![]() |
bd8931d3d1 | ||
![]() |
0d86c04939 | ||
![]() |
8436ebbf68 | ||
![]() |
824f94b3df | ||
![]() |
f9a8e686b2 | ||
![]() |
e442a44ba8 | ||
![]() |
f1fd29571a | ||
![]() |
6a482a7ba2 | ||
![]() |
bfea434a55 | ||
![]() |
acb22c5a64 | ||
![]() |
7cd1b621d1 | ||
![]() |
9c37b25bab | ||
![]() |
a8bb9a78ef | ||
![]() |
e4aff04061 | ||
![]() |
c5cadc7de3 | ||
![]() |
7dc0c0d96a | ||
![]() |
2c9c8c8e05 | ||
![]() |
b5d7ae57e5 | ||
![]() |
0f66ee3a41 | ||
![]() |
771372c66b | ||
![]() |
a7937681e9 | ||
![]() |
792f238d16 | ||
![]() |
ba16416c75 | ||
![]() |
6e0ac52a64 | ||
![]() |
5bc40402a6 | ||
![]() |
df14c69977 | ||
![]() |
1ea67ba93d | ||
![]() |
a7153d9d1d | ||
![]() |
e885057a71 | ||
![]() |
7efa081976 | ||
![]() |
5deb93a083 | ||
![]() |
e3908e6fe3 |
15
.github/workflows/publish-docker.yml
vendored
15
.github/workflows/publish-docker.yml
vendored
@@ -10,11 +10,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/build-push-action@v2
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
repository: excalidraw/excalidraw
|
||||
tag_with_ref: true
|
||||
tag_with_sha: true
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: excalidraw/excalidraw:latest
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,7 +19,6 @@ logs
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
package-lock.json
|
||||
static
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
src/packages/excalidraw/types
|
||||
|
20
dev-docs/.gitignore
vendored
Normal file
20
dev-docs/.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
41
dev-docs/README.md
Normal file
41
dev-docs/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Website
|
||||
|
||||
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
|
||||
|
||||
### Installation
|
||||
|
||||
```
|
||||
$ yarn
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
```
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||
|
||||
### Deployment
|
||||
|
||||
Using SSH:
|
||||
|
||||
```
|
||||
$ USE_SSH=true yarn deploy
|
||||
```
|
||||
|
||||
Not using SSH:
|
||||
|
||||
```
|
||||
$ GIT_USER=<Your GitHub username> yarn deploy
|
||||
```
|
||||
|
||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
3
dev-docs/babel.config.js
Normal file
3
dev-docs/babel.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
|
||||
};
|
6
dev-docs/docs/codebase/overview.md
Normal file
6
dev-docs/docs/codebase/overview.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: Overview
|
||||
---
|
||||
|
||||
In development. For now, refer to [excalidraw Readme](https://github.com/excalidraw/excalidraw/blob/master/README.md).
|
8
dev-docs/docs/get-started.md
Normal file
8
dev-docs/docs/get-started.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: Introduction
|
||||
---
|
||||
|
||||
Want to integrate Excalidraw into your app? Head over to the [package docs](/docs/package/overview).
|
||||
|
||||
If you're looking into the Excalidraw codebase itself, start [here](/docs/codebase/overview).
|
6
dev-docs/docs/package/overview.md
Normal file
6
dev-docs/docs/package/overview.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: Overview
|
||||
---
|
||||
|
||||
In development. For now, refer to [excalidraw package readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md).
|
121
dev-docs/docusaurus.config.js
Normal file
121
dev-docs/docusaurus.config.js
Normal file
@@ -0,0 +1,121 @@
|
||||
// @ts-check
|
||||
// Note: type annotations allow type checking and IDEs autocompletion
|
||||
|
||||
const lightCodeTheme = require("prism-react-renderer/themes/github");
|
||||
const darkCodeTheme = require("prism-react-renderer/themes/dracula");
|
||||
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
title: "Excalidraw developer docs",
|
||||
tagline:
|
||||
"For Excalidraw contributors or those integrating the Excalidraw editor",
|
||||
url: "https://docs.excalidraw.com.com",
|
||||
baseUrl: "/",
|
||||
onBrokenLinks: "throw",
|
||||
onBrokenMarkdownLinks: "warn",
|
||||
favicon: "img/favicon.ico",
|
||||
organizationName: "Excalidraw", // Usually your GitHub org/user name.
|
||||
projectName: "excalidraw", // Usually your repo name.
|
||||
|
||||
// Even if you don't use internalization, you can use this field to set useful
|
||||
// metadata like html lang. For example, if your site is Chinese, you may want
|
||||
// to replace "en" with "zh-Hans".
|
||||
i18n: {
|
||||
defaultLocale: "en",
|
||||
locales: ["en"],
|
||||
},
|
||||
|
||||
presets: [
|
||||
[
|
||||
"classic",
|
||||
/** @type {import('@docusaurus/preset-classic').Options} */
|
||||
({
|
||||
docs: {
|
||||
sidebarPath: require.resolve("./sidebars.js"),
|
||||
// Please change this to your repo.
|
||||
editUrl: "https://github.com/excalidraw/docs/tree/master/",
|
||||
},
|
||||
theme: {
|
||||
customCss: require.resolve("./src/css/custom.css"),
|
||||
},
|
||||
}),
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig:
|
||||
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
|
||||
({
|
||||
navbar: {
|
||||
title: "Excalidraw Docs",
|
||||
logo: {
|
||||
alt: "Excalidraw Logo",
|
||||
src: "img/logo.svg",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: "doc",
|
||||
docId: "get-started",
|
||||
position: "left",
|
||||
label: "Get started",
|
||||
},
|
||||
{
|
||||
to: "https://blog.excalidraw.com",
|
||||
label: "Blog",
|
||||
position: "left",
|
||||
},
|
||||
{
|
||||
to: "https://github.com/excalidraw/excalidraw",
|
||||
label: "GitHub",
|
||||
position: "right",
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: "dark",
|
||||
links: [
|
||||
{
|
||||
title: "Docs",
|
||||
items: [
|
||||
{
|
||||
label: "Get Started",
|
||||
to: "/docs/get-started",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Community",
|
||||
items: [
|
||||
{
|
||||
label: "Discord",
|
||||
href: "https://discord.gg/UexuTaE",
|
||||
},
|
||||
{
|
||||
label: "Twitter",
|
||||
href: "https://twitter.com/excalidraw",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "More",
|
||||
items: [
|
||||
{
|
||||
label: "Blog",
|
||||
to: "https://blog.excalidraw.com",
|
||||
},
|
||||
{
|
||||
label: "GitHub",
|
||||
to: "https://github.com/excalidraw/excalidraw",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
copyright: `Made with ❤️ Built with Docusaurus`,
|
||||
},
|
||||
prism: {
|
||||
theme: lightCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
module.exports = config;
|
46
dev-docs/package.json
Normal file
46
dev-docs/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start --port 3003",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "2.0.0-rc.1",
|
||||
"@docusaurus/preset-classic": "2.0.0-rc.1",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"clsx": "^1.2.1",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "2.0.0-rc.1",
|
||||
"@tsconfig/docusaurus": "^1.0.5",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14"
|
||||
}
|
||||
}
|
31
dev-docs/sidebars.js
Normal file
31
dev-docs/sidebars.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Creating a sidebar enables you to:
|
||||
- create an ordered group of docs
|
||||
- render a sidebar for each doc of that group
|
||||
- provide next/previous navigation
|
||||
|
||||
The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||
|
||||
Create as many sidebars as you want.
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
|
||||
const sidebars = {
|
||||
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||
tutorialSidebar: [{ type: "autogenerated", dirName: "." }],
|
||||
|
||||
// But you can create a sidebar manually
|
||||
/*
|
||||
tutorialSidebar: [
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Tutorial',
|
||||
items: ['hello'],
|
||||
},
|
||||
],
|
||||
*/
|
||||
};
|
||||
|
||||
module.exports = sidebars;
|
62
dev-docs/src/components/Homepage/index.js
Normal file
62
dev-docs/src/components/Homepage/index.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
const FeatureList = [
|
||||
{
|
||||
title: "Learn how Excalidraw works",
|
||||
Svg: require("@site/static/img/undraw_innovative.svg").default,
|
||||
description: (
|
||||
<>Want to contribute to Excalidraw but got lost in the codebase?</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Integrate Excalidraw",
|
||||
Svg: require("@site/static/img/undraw_blank_canvas.svg").default,
|
||||
description: (
|
||||
<>
|
||||
Want to build your own app powered by Excalidraw by don't know where to
|
||||
start?
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Help us improve",
|
||||
Svg: require("@site/static/img/undraw_add_files.svg").default,
|
||||
description: (
|
||||
<>
|
||||
Are the docs missing something? Anything you had trouble understanding
|
||||
or needs an explanation? Come contribute to the docs to make them even
|
||||
better!
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function Feature({ Svg, title, description }) {
|
||||
return (
|
||||
<div className={clsx("col col--4")}>
|
||||
<div className="text--center">
|
||||
<Svg className={styles.featureSvg} role="img" />
|
||||
</div>
|
||||
<div className="text--center padding-horiz--md">
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomepageFeatures() {
|
||||
return (
|
||||
<section className={styles.features}>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
{FeatureList.map((props, idx) => (
|
||||
<Feature key={idx} {...props} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
70
dev-docs/src/components/Homepage/index.tsx
Normal file
70
dev-docs/src/components/Homepage/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
type FeatureItem = {
|
||||
title: string;
|
||||
Svg: React.ComponentType<React.ComponentProps<"svg">>;
|
||||
description: JSX.Element;
|
||||
};
|
||||
|
||||
const FeatureList: FeatureItem[] = [
|
||||
{
|
||||
title: "Easy to Use",
|
||||
Svg: require("@site/static/img/undraw_docusaurus_mountain.svg").default,
|
||||
description: (
|
||||
<>
|
||||
Docusaurus was designed from the ground up to be easily installed and
|
||||
used to get your website up and running quickly.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Focus on What Matters",
|
||||
Svg: require("@site/static/img/undraw_docusaurus_tree.svg").default,
|
||||
description: (
|
||||
<>
|
||||
Docusaurus lets you focus on your docs, and we'll do the chores. Go
|
||||
ahead and move your docs into the <code>docs</code> directory.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Powered by React",
|
||||
Svg: require("@site/static/img/undraw_docusaurus_react.svg").default,
|
||||
description: (
|
||||
<>
|
||||
Extend or customize your website layout by reusing React. Docusaurus can
|
||||
be extended while reusing the same header and footer.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function Feature({ title, Svg, description }: FeatureItem) {
|
||||
return (
|
||||
<div className={clsx("col col--4")}>
|
||||
<div className="text--center">
|
||||
<Svg className={styles.featureSvg} role="img" />
|
||||
</div>
|
||||
<div className="text--center padding-horiz--md">
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomepageFeatures(): JSX.Element {
|
||||
return (
|
||||
<section className={styles.features}>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
{FeatureList.map((props, idx) => (
|
||||
<Feature key={idx} {...props} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
11
dev-docs/src/components/Homepage/styles.module.css
Normal file
11
dev-docs/src/components/Homepage/styles.module.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.features {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.featureSvg {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
}
|
43
dev-docs/src/css/custom.css
Normal file
43
dev-docs/src/css/custom.css
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Any CSS included here will be global. The classic template
|
||||
* bundles Infima by default. Infima is a CSS framework designed to
|
||||
* work well for content-centric websites.
|
||||
*/
|
||||
|
||||
/* You can override the default Infima variables here. */
|
||||
:root {
|
||||
--ifm-color-primary: #6965db;
|
||||
--ifm-color-primary-dark: #5b57d1;
|
||||
--ifm-color-primary-darker: #5b57d1;
|
||||
--ifm-color-primary-darkest: #4a47b1;
|
||||
--ifm-color-primary-light: #5b57d1;
|
||||
--ifm-color-primary-lighter: #5b57d1;
|
||||
--ifm-color-primary-lightest: #5b57d1;
|
||||
--ifm-code-font-size: 95%;
|
||||
}
|
||||
|
||||
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||
[data-theme="dark"] {
|
||||
--ifm-color-primary: #5650f0;
|
||||
--ifm-color-primary-dark: #4b46d8;
|
||||
--ifm-color-primary-darker: #4b46d8;
|
||||
--ifm-color-primary-darkest: #3e39be;
|
||||
--ifm-color-primary-light: #3f3d64;
|
||||
--ifm-color-primary-lighter: #3f3d64;
|
||||
--ifm-color-primary-lightest: #3f3d64;
|
||||
}
|
||||
|
||||
.docusaurus-highlight-code-line {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
display: block;
|
||||
margin: 0 calc(-1 * var(--ifm-pre-padding));
|
||||
padding: 0 var(--ifm-pre-padding);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .docusaurus-highlight-code-line {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .navbar__logo {
|
||||
filter: invert(93%) hue-rotate(180deg);
|
||||
}
|
42
dev-docs/src/pages/index.js
Normal file
42
dev-docs/src/pages/index.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import Layout from "@theme/Layout";
|
||||
import Link from "@docusaurus/Link";
|
||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||
import styles from "./index.module.css";
|
||||
import HomepageFeatures from "@site/src/components/Homepage";
|
||||
|
||||
function HomepageHeader() {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
return (
|
||||
<header className={clsx("hero hero--primary", styles.heroBanner)}>
|
||||
<div className="container">
|
||||
<h1 className="hero__title">{siteConfig.title}</h1>
|
||||
<p className="hero__subtitle">{siteConfig.tagline}</p>
|
||||
<div className={styles.buttons}>
|
||||
<Link
|
||||
className="button button--secondary button--lg"
|
||||
to="/docs/get-started"
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
return (
|
||||
<Layout
|
||||
title={`Hello from ${siteConfig.title}`}
|
||||
description="Description will go into a meta tag in <head />"
|
||||
>
|
||||
<HomepageHeader />
|
||||
<main>
|
||||
<HomepageFeatures />
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
27
dev-docs/src/pages/index.module.css
Normal file
27
dev-docs/src/pages/index.module.css
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* CSS files with the .module.css suffix will be treated as CSS modules
|
||||
* and scoped locally.
|
||||
*/
|
||||
|
||||
.heroBanner {
|
||||
padding: 4rem 0;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .heroBanner {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 996px) {
|
||||
.heroBanner {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
42
dev-docs/src/pages/index.tsx
Normal file
42
dev-docs/src/pages/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import Layout from "@theme/Layout";
|
||||
import Link from "@docusaurus/Link";
|
||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||
import styles from "./index.module.css";
|
||||
import HomepageFeatures from "@site/src/components/Homepage";
|
||||
|
||||
function HomepageHeader() {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
return (
|
||||
<header className={clsx("hero hero--primary", styles.heroBanner)}>
|
||||
<div className="container">
|
||||
<h1 className="hero__title">{siteConfig.title}</h1>
|
||||
<p className="hero__subtitle">{siteConfig.tagline}</p>
|
||||
<div className={styles.buttons}>
|
||||
<Link
|
||||
className="button button--secondary button--lg"
|
||||
to="/docs/get-started"
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
return (
|
||||
<Layout
|
||||
title={`Hello from ${siteConfig.title}`}
|
||||
description="Description will go into a meta tag in <head />"
|
||||
>
|
||||
<HomepageHeader />
|
||||
<main>
|
||||
<HomepageFeatures />
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
7
dev-docs/src/pages/markdown-page.md
Normal file
7
dev-docs/src/pages/markdown-page.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Markdown page example
|
||||
---
|
||||
|
||||
# Markdown page example
|
||||
|
||||
You don't need React to write simple standalone pages.
|
0
dev-docs/static/.nojekyll
Normal file
0
dev-docs/static/.nojekyll
Normal file
BIN
dev-docs/static/img/docusaurus.png
Normal file
BIN
dev-docs/static/img/docusaurus.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
BIN
dev-docs/static/img/favicon.ico
Normal file
BIN
dev-docs/static/img/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
4
dev-docs/static/img/logo.svg
Normal file
4
dev-docs/static/img/logo.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 80 180" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2">
|
||||
<path d="M22.197 150.382c-4.179-3.359-10.618-9.051-15.702-13.946l-4.01-3.813.734-5.009c.396-2.732 1.13-8.083 1.582-11.839.508-3.757 1.017-7.286 1.186-7.798.226-.683 0-1.025-.621-1.025-1.073 0-1.13.285 1.807-9.107a617.602 617.602 0 0 1 2.203-7.229c.113-.398.565-.569 1.073-.398.508.227.791.683.621 1.081-.169.455.113.911.565 1.082.621.227.565.683-.395 2.333-1.525 2.562-5.422 24.419-5.648 31.477-.17 5.009-.17 5.066 1.92 7.912 2.033 2.789 6.721 7.001 13.951 12.351 2.033 1.537 4.067 3.245 4.631 3.814.848 1.024 1.243.74 8.36-6.887 4.123-4.383 8.698-8.88 10.166-10.018l2.711-2.049-2.089-4.44c-1.13-2.391-5.705-11.612-10.223-20.377-9.433-18.442-7.513-16.678-18.47-16.849l-7.117-.056-2.372-2.733c-2.485-2.903-2.824-3.984-1.638-5.805.452-.627.791-1.651.791-2.277 0-1.025.395-1.196 2.655-1.309 1.412-.057 2.711-.228 2.88-.399.17-.171.396-3.7.565-7.855l.226-7.513-3.784-8.197C2.485 39.844 0 33.583 0 31.533c0-1.081.226-1.992.452-1.992.565 0 .565.057 23.553 48.382 10.675 22.426 20.785 43.544 22.479 47.016 1.695 3.472 3.22 6.659 3.333 7.115.113.512-3.785 4.439-9.998 9.961-5.591 5.008-10.505 9.562-10.957 10.074-1.299 1.594-3.219 1.082-6.665-1.707Zm1.921-65.458c-2.599-5.066-2.712-5.123-9.828-5.464-6.27-.342-6.383-.285-6.383.911 0 .683-.226 1.593-.508 2.049-.339.512-.113 1.423.678 2.675l1.242 1.935h5.649c3.106.057 6.664.285 7.907.512 1.243.228 2.316.342 2.429.285.113-.057-.452-1.366-1.186-2.903Zm-4.745-9.107c-.452-1.195-1.638-3.7-2.598-5.578-1.581-3.188-1.751-3.301-2.146-1.992-.226.797-.396 3.13-.452 5.236-.057 4.155-.17 4.098 4.575 4.383l1.525.057-.904-2.106Z" style="fill-rule:nonzero;stroke:#000;stroke-width:2px" transform="matrix(1.01351 0 0 -1 9.088 166.517)" />
|
||||
<path d="M23.892 136.835c-1.017-.74-1.299-1.48-1.299-3.358 0-2.22.169-2.562 1.694-3.188 1.525-.626 1.92-.569 3.671.626 2.316 1.594 2.373 1.992.678 4.554-1.468 2.22-2.937 2.618-4.744 1.366Zm3.219-2.049c.904-1.594.339-2.789-1.355-2.789-1.525 0-2.203 1.536-1.356 3.073.678 1.253 1.977 1.139 2.711-.284ZM59.306 124.028c0-.285-.339-.569-.735-.569-.339 0-1.299-1.594-2.033-3.529-2.259-5.92-24.852-50.943-24.908-49.52 0 .74-.339 1.252-.904 1.252-.791 0-.904-.456-.565-2.675.339-2.562.113-3.131-7.907-18.841-4.519-8.936-9.376-18.271-10.788-20.775-1.469-2.619-2.598-5.465-2.711-6.66-.17-2.049.056-2.334 4.97-6.603 2.824-2.504 6.439-5.635 8.02-7.058C28.862 2.504 32.194-.114 33.098.057c1.356.228 22.31 22.369 22.367 23.622 0 .569-1.017 9.221-2.259 19.238-2.147 17.076-4.18 37.055-3.954 38.99.169 1.196-.678 7.229-1.299 9.847-.509 2.05-.283 2.903 3.784 12.238 2.372 5.521 5.479 12.295 6.834 15.027 1.299 2.732 2.429 5.123 2.429 5.294 0 .17-.395.284-.847.284-.452 0-.847-.228-.847-.569ZM46.315 81.509c.621-3.984 1.864-13.547 2.767-21.231 1.751-14.116 3.785-29.769 4.349-33.753.339-1.993.113-2.391-3.558-6.489-6.382-7.229-13.16-14.344-15.476-16.165l-2.146-1.708-11.014 10.359C11.07 21.971 10.223 22.939 10.844 24.077c.339.626 3.22 5.92 6.383 11.725 3.163 5.806 7.342 13.547 9.263 17.19 1.977 3.7 3.784 6.887 4.123 7.058.395.228.508-5.521.395-17.759-.226-18.271-.169-18.328 1.638-17.929.226 0 .396 9.221.396 20.434v20.377l5.93 11.953c3.276 6.603 5.987 11.896 6.1 11.84.113-.058.678-3.416 1.243-7.457Z" style="fill-rule:nonzero;stroke:#000;stroke-width:2px" transform="matrix(1.01351 0 0 -1 9.088 166.517)" />
|
||||
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
1
dev-docs/static/img/undraw_add_files.svg
Normal file
1
dev-docs/static/img/undraw_add_files.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.7 KiB |
1
dev-docs/static/img/undraw_blank_canvas.svg
Normal file
1
dev-docs/static/img/undraw_blank_canvas.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 12 KiB |
1
dev-docs/static/img/undraw_innovative.svg
Normal file
1
dev-docs/static/img/undraw_innovative.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.4 KiB |
7
dev-docs/tsconfig.json
Normal file
7
dev-docs/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
// This file is not used in compilation. It is here just for a nice editor experience.
|
||||
"extends": "@tsconfig/docusaurus/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
7489
dev-docs/yarn.lock
Normal file
7489
dev-docs/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@@ -23,17 +23,19 @@
|
||||
"@sentry/integrations": "6.2.5",
|
||||
"@testing-library/jest-dom": "5.16.2",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@tldraw/vec": "1.4.3",
|
||||
"@tldraw/vec": "1.7.1",
|
||||
"@types/jest": "27.4.0",
|
||||
"@types/pica": "5.1.3",
|
||||
"@types/react": "17.0.39",
|
||||
"@types/react-dom": "17.0.11",
|
||||
"@types/react": "18.0.15",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"@types/socket.io-client": "1.4.36",
|
||||
"browser-fs-access": "0.29.1",
|
||||
"clsx": "1.1.1",
|
||||
"cross-env": "7.0.3",
|
||||
"fake-indexeddb": "3.1.7",
|
||||
"firebase": "8.3.3",
|
||||
"i18next-browser-languagedetector": "6.1.2",
|
||||
"http-server": "14.1.1",
|
||||
"i18next-browser-languagedetector": "6.1.4",
|
||||
"idb-keyval": "6.0.3",
|
||||
"image-blob-reduce": "3.0.1",
|
||||
"jotai": "1.6.4",
|
||||
@@ -47,8 +49,8 @@
|
||||
"png-chunks-extract": "1.0.0",
|
||||
"points-on-curve": "0.2.0",
|
||||
"pwacompat": "2.0.17",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"roughjs": "4.5.2",
|
||||
"sass": "1.51.0",
|
||||
@@ -59,11 +61,11 @@
|
||||
"@excalidraw/eslint-config": "1.0.0",
|
||||
"@excalidraw/prettier-config": "1.0.2",
|
||||
"@types/chai": "4.3.0",
|
||||
"@types/lodash.throttle": "4.1.6",
|
||||
"@types/lodash.throttle": "4.1.7",
|
||||
"@types/pako": "1.0.3",
|
||||
"@types/resize-observer-browser": "0.1.6",
|
||||
"@types/resize-observer-browser": "0.1.7",
|
||||
"chai": "4.3.6",
|
||||
"dotenv": "10.0.0",
|
||||
"dotenv": "16.0.1",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-plugin-prettier": "3.3.1",
|
||||
"husky": "7.0.4",
|
||||
@@ -71,7 +73,7 @@
|
||||
"lint-staged": "12.3.7",
|
||||
"pepjs": "0.5.3",
|
||||
"prettier": "2.6.2",
|
||||
"rewire": "5.0.0"
|
||||
"rewire": "6.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@typescript-eslint/typescript-estree": "5.10.2"
|
||||
@@ -91,8 +93,8 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build-node": "node ./scripts/build-node.js",
|
||||
"build:app:docker": "REACT_APP_DISABLE_SENTRY=true react-scripts build",
|
||||
"build:app": "REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
|
||||
"build:app:docker": "cross-env REACT_APP_DISABLE_SENTRY=true react-scripts build",
|
||||
"build:app": "cross-env REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
|
||||
"build:version": "node ./scripts/build-version.js",
|
||||
"build:prebuild": "node ./scripts/prebuild.js",
|
||||
"build": "yarn build:prebuild && yarn build:app && yarn build:version",
|
||||
@@ -105,6 +107,7 @@
|
||||
"prepare": "husky install",
|
||||
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
||||
"start": "react-scripts start",
|
||||
"start:build": "npm run build && npx http-server build -a localhost -p 3001 -o",
|
||||
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watchAll=false",
|
||||
"test:app": "react-scripts test --passWithNoTests",
|
||||
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
|
||||
|
21
scripts/buildDocs.js
Normal file
21
scripts/buildDocs.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const { exec } = require("child_process");
|
||||
|
||||
// get files changed between prev and head commit
|
||||
exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
|
||||
if (error || stderr) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
const changedFiles = stdout.trim().split("\n");
|
||||
|
||||
const docFiles = changedFiles.filter((file) => {
|
||||
return file.indexOf("docs") >= 0;
|
||||
});
|
||||
|
||||
if (!docFiles.length) {
|
||||
console.info("Skipping building docs as no valid diff found");
|
||||
process.exit(0);
|
||||
}
|
||||
// Exit code 1 to build the docs in ignoredBuildStep
|
||||
process.exit(1);
|
||||
});
|
@@ -36,6 +36,7 @@ const crowdinMap = {
|
||||
"ru-RU": "en-ru",
|
||||
"si-LK": "en-silk",
|
||||
"sk-SK": "en-sk",
|
||||
"sl-SI": "en-sl",
|
||||
"sv-SE": "en-sv",
|
||||
"ta-IN": "en-ta",
|
||||
"tr-TR": "en-tr",
|
||||
@@ -47,6 +48,8 @@ const crowdinMap = {
|
||||
"lv-LV": "en-lv",
|
||||
"cs-CZ": "en-cs",
|
||||
"kk-KZ": "en-kk",
|
||||
"vi-vn": "en-vi",
|
||||
"mr-in": "en-mr",
|
||||
};
|
||||
|
||||
const flags = {
|
||||
@@ -86,6 +89,7 @@ const flags = {
|
||||
"ru-RU": "🇷🇺",
|
||||
"si-LK": "🇱🇰",
|
||||
"sk-SK": "🇸🇰",
|
||||
"sl-SI": "🇸🇮",
|
||||
"sv-SE": "🇸🇪",
|
||||
"ta-IN": "🇮🇳",
|
||||
"tr-TR": "🇹🇷",
|
||||
@@ -93,6 +97,9 @@ const flags = {
|
||||
"zh-CN": "🇨🇳",
|
||||
"zh-HK": "🇭🇰",
|
||||
"zh-TW": "🇹🇼",
|
||||
"eu-ES": "🇪🇦",
|
||||
"vi-VN": "🇻🇳",
|
||||
"mr-IN": "🇮🇳",
|
||||
};
|
||||
|
||||
const languages = {
|
||||
@@ -133,6 +140,7 @@ const languages = {
|
||||
"ru-RU": "Русский",
|
||||
"si-LK": "සිංහල",
|
||||
"sk-SK": "Slovenčina",
|
||||
"sl-SI": "Slovenščina",
|
||||
"sv-SE": "Svenska",
|
||||
"ta-IN": "Tamil",
|
||||
"tr-TR": "Türkçe",
|
||||
@@ -140,6 +148,8 @@ const languages = {
|
||||
"zh-CN": "简体中文",
|
||||
"zh-HK": "繁體中文 (香港)",
|
||||
"zh-TW": "繁體中文",
|
||||
"vi-VN": "Tiếng Việt",
|
||||
"mr-IN": "मराठी",
|
||||
};
|
||||
|
||||
const percentages = fs.readFileSync(
|
||||
|
@@ -1,11 +1,12 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// for development purposes we want to have the service-worker.js file
|
||||
// accessible from the public folder. On build though, we need to compile it
|
||||
// and CRA expects that file to be in src/ folder.
|
||||
const moveServiceWorkerScript = () => {
|
||||
const oldPath = "./public/service-worker.js";
|
||||
const newPath = "./src/service-worker.js";
|
||||
const oldPath = path.resolve(__dirname, "../public/service-worker.js");
|
||||
const newPath = path.resolve(__dirname, "../src/service-worker.js");
|
||||
|
||||
fs.rename(oldPath, newPath, (error) => {
|
||||
if (error) {
|
||||
@@ -17,4 +18,6 @@ const moveServiceWorkerScript = () => {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
moveServiceWorkerScript();
|
||||
if (process.env.CI) {
|
||||
moveServiceWorkerScript();
|
||||
}
|
||||
|
@@ -42,7 +42,7 @@ export const actionAddToLibrary = register({
|
||||
commitToHistory: false,
|
||||
appState: {
|
||||
...appState,
|
||||
toastMessage: t("toast.addedToLibrary"),
|
||||
toast: { message: t("toast.addedToLibrary") },
|
||||
},
|
||||
};
|
||||
})
|
||||
|
@@ -107,14 +107,16 @@ export const actionCopyAsPng = register({
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
toastMessage: t("toast.copyToClipboardAsPng", {
|
||||
exportSelection: selectedElements.length
|
||||
? t("toast.selection")
|
||||
: t("toast.canvas"),
|
||||
exportColorScheme: appState.exportWithDarkMode
|
||||
? t("buttons.darkMode")
|
||||
: t("buttons.lightMode"),
|
||||
}),
|
||||
toast: {
|
||||
message: t("toast.copyToClipboardAsPng", {
|
||||
exportSelection: selectedElements.length
|
||||
? t("toast.selection")
|
||||
: t("toast.canvas"),
|
||||
exportColorScheme: appState.exportWithDarkMode
|
||||
? t("buttons.darkMode")
|
||||
: t("buttons.lightMode"),
|
||||
}),
|
||||
},
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
|
@@ -128,12 +128,15 @@ const duplicateElements = (
|
||||
{
|
||||
...appState,
|
||||
selectedGroupIds: {},
|
||||
selectedElementIds: newElements.reduce((acc, element) => {
|
||||
if (!isBoundToContainer(element)) {
|
||||
acc[element.id] = true;
|
||||
}
|
||||
return acc;
|
||||
}, {} as any),
|
||||
selectedElementIds: newElements.reduce(
|
||||
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||
if (!isBoundToContainer(element)) {
|
||||
acc[element.id] = true;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
),
|
||||
},
|
||||
getNonDeletedElements(finalElements),
|
||||
),
|
||||
|
@@ -144,13 +144,15 @@ export const actionSaveToActiveFile = register({
|
||||
appState: {
|
||||
...appState,
|
||||
fileHandle,
|
||||
toastMessage: fileHandleExists
|
||||
? fileHandle?.name
|
||||
? t("toast.fileSavedToFilename").replace(
|
||||
"{filename}",
|
||||
`"${fileHandle.name}"`,
|
||||
)
|
||||
: t("toast.fileSaved")
|
||||
toast: fileHandleExists
|
||||
? {
|
||||
message: fileHandle?.name
|
||||
? t("toast.fileSavedToFilename").replace(
|
||||
"{filename}",
|
||||
`"${fileHandle.name}"`,
|
||||
)
|
||||
: t("toast.fileSaved"),
|
||||
}
|
||||
: null,
|
||||
},
|
||||
};
|
||||
|
@@ -13,7 +13,7 @@ import {
|
||||
maybeBindLinearElement,
|
||||
bindOrUnbindLinearElement,
|
||||
} from "../element/binding";
|
||||
import { isBindingElement } from "../element/typeChecks";
|
||||
import { isBindingElement, isLinearElement } from "../element/typeChecks";
|
||||
import { AppState } from "../types";
|
||||
|
||||
export const actionFinalize = register({
|
||||
@@ -181,6 +181,11 @@ export const actionFinalize = register({
|
||||
[multiPointElement.id]: true,
|
||||
}
|
||||
: appState.selectedElementIds,
|
||||
// To select the linear element when user has finished mutipoint editing
|
||||
selectedLinearElement:
|
||||
multiPointElement && isLinearElement(multiPointElement)
|
||||
? new LinearElementEditor(multiPointElement, scene)
|
||||
: appState.selectedLinearElement,
|
||||
pendingImageElementId: null,
|
||||
},
|
||||
commitToHistory: appState.activeTool.type === "freedraw",
|
||||
|
@@ -2,29 +2,43 @@ import { KEYS } from "../keys";
|
||||
import { register } from "./register";
|
||||
import { selectGroupsForSelectedElements } from "../groups";
|
||||
import { getNonDeletedElements, isTextElement } from "../element";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { isLinearElement } from "../element/typeChecks";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
|
||||
export const actionSelectAll = register({
|
||||
name: "selectAll",
|
||||
trackEvent: { category: "canvas" },
|
||||
perform: (elements, appState) => {
|
||||
perform: (elements, appState, value, app) => {
|
||||
if (appState.editingLinearElement) {
|
||||
return false;
|
||||
}
|
||||
const selectedElementIds = elements.reduce(
|
||||
(map: Record<ExcalidrawElement["id"], true>, element) => {
|
||||
if (
|
||||
!element.isDeleted &&
|
||||
!(isTextElement(element) && element.containerId) &&
|
||||
!element.locked
|
||||
) {
|
||||
map[element.id] = true;
|
||||
}
|
||||
return map;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
return {
|
||||
appState: selectGroupsForSelectedElements(
|
||||
{
|
||||
...appState,
|
||||
selectedLinearElement:
|
||||
// single linear element selected
|
||||
Object.keys(selectedElementIds).length === 1 &&
|
||||
isLinearElement(elements[0])
|
||||
? new LinearElementEditor(elements[0], app.scene)
|
||||
: null,
|
||||
editingGroupId: null,
|
||||
selectedElementIds: elements.reduce((map, element) => {
|
||||
if (
|
||||
!element.isDeleted &&
|
||||
!(isTextElement(element) && element.containerId) &&
|
||||
element.locked === false
|
||||
) {
|
||||
map[element.id] = true;
|
||||
}
|
||||
return map;
|
||||
}, {} as any),
|
||||
selectedElementIds,
|
||||
},
|
||||
getNonDeletedElements(elements),
|
||||
),
|
||||
|
@@ -36,7 +36,7 @@ export const actionCopyStyles = register({
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
toastMessage: t("toast.copyStyles"),
|
||||
toast: { message: t("toast.copyStyles") },
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
|
@@ -17,16 +17,19 @@ export const actionToggleLock = register({
|
||||
|
||||
const operation = getOperation(selectedElements);
|
||||
const selectedElementsMap = arrayToMap(selectedElements);
|
||||
|
||||
const lock = operation === "lock";
|
||||
return {
|
||||
elements: elements.map((element) => {
|
||||
if (!selectedElementsMap.has(element.id)) {
|
||||
return element;
|
||||
}
|
||||
|
||||
return newElementWith(element, { locked: operation === "lock" });
|
||||
return newElementWith(element, { locked: lock });
|
||||
}),
|
||||
appState,
|
||||
appState: {
|
||||
...appState,
|
||||
selectedLinearElement: lock ? null : appState.selectedLinearElement,
|
||||
},
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
|
@@ -81,7 +81,7 @@ export const getDefaultAppState = (): Omit<
|
||||
showStats: false,
|
||||
startBoundElement: null,
|
||||
suggestedBindings: [],
|
||||
toastMessage: null,
|
||||
toast: null,
|
||||
viewBackgroundColor: oc.white,
|
||||
zenModeEnabled: false,
|
||||
zoom: {
|
||||
@@ -90,6 +90,7 @@ export const getDefaultAppState = (): Omit<
|
||||
viewModeEnabled: false,
|
||||
pendingImageElementId: null,
|
||||
showHyperlinkPopup: false,
|
||||
selectedLinearElement: null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -173,7 +174,7 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
showStats: { browser: true, export: false, server: false },
|
||||
startBoundElement: { browser: false, export: false, server: false },
|
||||
suggestedBindings: { browser: false, export: false, server: false },
|
||||
toastMessage: { browser: false, export: false, server: false },
|
||||
toast: { browser: false, export: false, server: false },
|
||||
viewBackgroundColor: { browser: true, export: true, server: true },
|
||||
width: { browser: false, export: false, server: false },
|
||||
zenModeEnabled: { browser: true, export: false, server: false },
|
||||
@@ -181,6 +182,7 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
viewModeEnabled: { browser: false, export: false, server: false },
|
||||
pendingImageElementId: { browser: false, export: false, server: false },
|
||||
showHyperlinkPopup: { browser: false, export: false, server: false },
|
||||
selectedLinearElement: { browser: true, export: false, server: false },
|
||||
});
|
||||
|
||||
const _clearAppStateForStorage = <
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import "./Card.scss";
|
||||
|
||||
export const Card: React.FC<{
|
||||
color: keyof OpenColor | "primary";
|
||||
children?: React.ReactNode;
|
||||
}> = ({ children, color }) => {
|
||||
return (
|
||||
<div
|
||||
|
@@ -8,6 +8,7 @@ export const CheckboxItem: React.FC<{
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean, event: React.MouseEvent) => void;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}> = ({ children, checked, onChange, className }) => {
|
||||
return (
|
||||
<div
|
||||
|
@@ -85,6 +85,7 @@ export const Dialog = (props: DialogProps) => {
|
||||
<button
|
||||
className="Modal__close"
|
||||
onClick={onClose}
|
||||
title={t("buttons.close")}
|
||||
aria-label={t("buttons.close")}
|
||||
>
|
||||
{useDevice().isMobile ? back : close}
|
||||
|
@@ -58,6 +58,7 @@ const ExportButton: React.FC<{
|
||||
onClick: () => void;
|
||||
title: string;
|
||||
shade?: number;
|
||||
children?: React.ReactNode;
|
||||
}> = ({ children, title, onClick, color, shade = 6 }) => {
|
||||
return (
|
||||
<button
|
||||
@@ -170,7 +171,9 @@ const ImageExportModal = ({
|
||||
<Stack.Row gap={2}>
|
||||
{actionManager.renderAction("changeExportScale")}
|
||||
</Stack.Row>
|
||||
<p style={{ marginLeft: "1em", userSelect: "none" }}>Scale</p>
|
||||
<p style={{ marginLeft: "1em", userSelect: "none" }}>
|
||||
{t("buttons.scale")}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
|
@@ -39,6 +39,7 @@ import { trackEvent } from "../analytics";
|
||||
import { useDevice } from "../components/App";
|
||||
import { Stats } from "./Stats";
|
||||
import { actionToggleStats } from "../actions/actionToggleStats";
|
||||
import { actionToggleZenMode } from "../actions";
|
||||
|
||||
interface LayerUIProps {
|
||||
actionManager: ActionManager;
|
||||
@@ -51,16 +52,13 @@ interface LayerUIProps {
|
||||
onLockToggle: () => void;
|
||||
onPenModeToggle: () => void;
|
||||
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||
zenModeEnabled: boolean;
|
||||
showExitZenModeBtn: boolean;
|
||||
showThemeBtn: boolean;
|
||||
toggleZenMode: () => void;
|
||||
langCode: Language["code"];
|
||||
isCollaborating: boolean;
|
||||
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
||||
renderCustomFooter?: ExcalidrawProps["renderFooter"];
|
||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||
viewModeEnabled: boolean;
|
||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||
UIOptions: AppProps["UIOptions"];
|
||||
focusContainer: () => void;
|
||||
@@ -79,15 +77,12 @@ const LayerUI = ({
|
||||
onLockToggle,
|
||||
onPenModeToggle,
|
||||
onInsertElements,
|
||||
zenModeEnabled,
|
||||
showExitZenModeBtn,
|
||||
showThemeBtn,
|
||||
toggleZenMode,
|
||||
isCollaborating,
|
||||
renderTopRightUI,
|
||||
renderCustomFooter,
|
||||
renderCustomStats,
|
||||
viewModeEnabled,
|
||||
libraryReturnUrl,
|
||||
UIOptions,
|
||||
focusContainer,
|
||||
@@ -171,7 +166,7 @@ const LayerUI = ({
|
||||
<Section
|
||||
heading="canvasActions"
|
||||
className={clsx("zen-mode-transition", {
|
||||
"transition-left": zenModeEnabled,
|
||||
"transition-left": appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{/* the zIndex ensures this menu has higher stacking order,
|
||||
@@ -192,7 +187,7 @@ const LayerUI = ({
|
||||
<Section
|
||||
heading="canvasActions"
|
||||
className={clsx("zen-mode-transition", {
|
||||
"transition-left": zenModeEnabled,
|
||||
"transition-left": appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{/* the zIndex ensures this menu has higher stacking order,
|
||||
@@ -232,7 +227,7 @@ const LayerUI = ({
|
||||
<Section
|
||||
heading="selectedShapeActions"
|
||||
className={clsx("zen-mode-transition", {
|
||||
"transition-left": zenModeEnabled,
|
||||
"transition-left": appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
<Island
|
||||
@@ -302,32 +297,34 @@ const LayerUI = ({
|
||||
<div className="App-menu App-menu_top">
|
||||
<Stack.Col
|
||||
gap={4}
|
||||
className={clsx({ "disable-pointerEvents": zenModeEnabled })}
|
||||
className={clsx({
|
||||
"disable-pointerEvents": appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{viewModeEnabled
|
||||
{appState.viewModeEnabled
|
||||
? renderViewModeCanvasActions()
|
||||
: renderCanvasActions()}
|
||||
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
||||
</Stack.Col>
|
||||
{!viewModeEnabled && (
|
||||
{!appState.viewModeEnabled && (
|
||||
<Section heading="shapes">
|
||||
{(heading) => (
|
||||
{(heading: React.ReactNode) => (
|
||||
<Stack.Col gap={4} align="start">
|
||||
<Stack.Row
|
||||
gap={1}
|
||||
className={clsx("App-toolbar-container", {
|
||||
"zen-mode": zenModeEnabled,
|
||||
"zen-mode": appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
<PenModeButton
|
||||
zenModeEnabled={zenModeEnabled}
|
||||
zenModeEnabled={appState.zenModeEnabled}
|
||||
checked={appState.penMode}
|
||||
onChange={onPenModeToggle}
|
||||
title={t("toolBar.penMode")}
|
||||
penDetected={appState.penDetected}
|
||||
/>
|
||||
<LockButton
|
||||
zenModeEnabled={zenModeEnabled}
|
||||
zenModeEnabled={appState.zenModeEnabled}
|
||||
checked={appState.activeTool.locked}
|
||||
onChange={() => onLockToggle()}
|
||||
title={t("toolBar.lock")}
|
||||
@@ -335,7 +332,7 @@ const LayerUI = ({
|
||||
<Island
|
||||
padding={1}
|
||||
className={clsx("App-toolbar", {
|
||||
"zen-mode": zenModeEnabled,
|
||||
"zen-mode": appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
<HintViewer
|
||||
@@ -371,7 +368,7 @@ const LayerUI = ({
|
||||
className={clsx(
|
||||
"layer-ui__wrapper__top-right zen-mode-transition",
|
||||
{
|
||||
"transition-right": zenModeEnabled,
|
||||
"transition-right": appState.zenModeEnabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
@@ -396,7 +393,8 @@ const LayerUI = ({
|
||||
className={clsx(
|
||||
"layer-ui__wrapper__footer-left zen-mode-transition",
|
||||
{
|
||||
"layer-ui__wrapper__footer-left--transition-left": zenModeEnabled,
|
||||
"layer-ui__wrapper__footer-left--transition-left":
|
||||
appState.zenModeEnabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
@@ -408,12 +406,12 @@ const LayerUI = ({
|
||||
zoom={appState.zoom}
|
||||
/>
|
||||
</Island>
|
||||
{!viewModeEnabled && (
|
||||
{!appState.viewModeEnabled && (
|
||||
<>
|
||||
<div
|
||||
className={clsx("undo-redo-buttons zen-mode-transition", {
|
||||
"layer-ui__wrapper__footer-left--transition-bottom":
|
||||
zenModeEnabled,
|
||||
appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{actionManager.renderAction("undo", { size: "small" })}
|
||||
@@ -423,20 +421,20 @@ const LayerUI = ({
|
||||
<div
|
||||
className={clsx("eraser-buttons zen-mode-transition", {
|
||||
"layer-ui__wrapper__footer-left--transition-left":
|
||||
zenModeEnabled,
|
||||
appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{actionManager.renderAction("eraser", { size: "small" })}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!viewModeEnabled &&
|
||||
{!appState.viewModeEnabled &&
|
||||
appState.multiElement &&
|
||||
device.isTouchScreen && (
|
||||
<div
|
||||
className={clsx("finalize-button zen-mode-transition", {
|
||||
"layer-ui__wrapper__footer-left--transition-left":
|
||||
zenModeEnabled,
|
||||
appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{actionManager.renderAction("finalize", { size: "small" })}
|
||||
@@ -450,7 +448,7 @@ const LayerUI = ({
|
||||
"layer-ui__wrapper__footer-center zen-mode-transition",
|
||||
{
|
||||
"layer-ui__wrapper__footer-left--transition-bottom":
|
||||
zenModeEnabled,
|
||||
appState.zenModeEnabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
@@ -460,7 +458,7 @@ const LayerUI = ({
|
||||
className={clsx(
|
||||
"layer-ui__wrapper__footer-right zen-mode-transition",
|
||||
{
|
||||
"transition-right disable-pointerEvents": zenModeEnabled,
|
||||
"transition-right disable-pointerEvents": appState.zenModeEnabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
@@ -470,7 +468,7 @@ const LayerUI = ({
|
||||
className={clsx("disable-zen-mode", {
|
||||
"disable-zen-mode--visible": showExitZenModeBtn,
|
||||
})}
|
||||
onClick={toggleZenMode}
|
||||
onClick={() => actionManager.executeAction(actionToggleZenMode)}
|
||||
>
|
||||
{t("buttons.exitZenMode")}
|
||||
</button>
|
||||
@@ -543,7 +541,6 @@ const LayerUI = ({
|
||||
canvas={canvas}
|
||||
isCollaborating={isCollaborating}
|
||||
renderCustomFooter={renderCustomFooter}
|
||||
viewModeEnabled={viewModeEnabled}
|
||||
showThemeBtn={showThemeBtn}
|
||||
onImageAction={onImageAction}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
|
@@ -224,7 +224,7 @@ export const LibraryMenu = ({
|
||||
}, [setPublishLibSuccess, publishLibSuccess]);
|
||||
|
||||
const onPublishLibSuccess = useCallback(
|
||||
(data, libraryItems: LibraryItems) => {
|
||||
(data: { url: string; authorName: string }, libraryItems: LibraryItems) => {
|
||||
setShowPublishLibraryDialog(false);
|
||||
setPublishLibSuccess({ url: data.url, authorName: data.authorName });
|
||||
const nextLibItems = libraryItems.slice();
|
||||
|
@@ -36,7 +36,6 @@ type MobileMenuProps = {
|
||||
isMobile: boolean,
|
||||
appState: AppState,
|
||||
) => JSX.Element | null;
|
||||
viewModeEnabled: boolean;
|
||||
showThemeBtn: boolean;
|
||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||
renderTopRightUI?: (
|
||||
@@ -60,7 +59,6 @@ export const MobileMenu = ({
|
||||
canvas,
|
||||
isCollaborating,
|
||||
renderCustomFooter,
|
||||
viewModeEnabled,
|
||||
showThemeBtn,
|
||||
onImageAction,
|
||||
renderTopRightUI,
|
||||
@@ -70,7 +68,7 @@ export const MobileMenu = ({
|
||||
return (
|
||||
<FixedSideContainer side="top" className="App-top-bar">
|
||||
<Section heading="shapes">
|
||||
{(heading) => (
|
||||
{(heading: React.ReactNode) => (
|
||||
<Stack.Col gap={4} align="center">
|
||||
<Stack.Row gap={1} className="App-toolbar-container">
|
||||
<Island padding={1} className="App-toolbar">
|
||||
@@ -125,7 +123,7 @@ export const MobileMenu = ({
|
||||
!appState.editingElement &&
|
||||
getSelectedElements(elements, appState).length === 0;
|
||||
|
||||
if (viewModeEnabled) {
|
||||
if (appState.viewModeEnabled) {
|
||||
return (
|
||||
<div className="App-toolbar-content">
|
||||
{actionManager.renderAction("toggleCanvasMenu")}
|
||||
@@ -151,7 +149,7 @@ export const MobileMenu = ({
|
||||
};
|
||||
|
||||
const renderCanvasActions = () => {
|
||||
if (viewModeEnabled) {
|
||||
if (appState.viewModeEnabled) {
|
||||
return (
|
||||
<>
|
||||
{renderJSONExportDialog()}
|
||||
@@ -185,7 +183,7 @@ export const MobileMenu = ({
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{!viewModeEnabled && renderToolbar()}
|
||||
{!appState.viewModeEnabled && renderToolbar()}
|
||||
{renderStats()}
|
||||
<div
|
||||
className="App-bottom-bar"
|
||||
@@ -216,7 +214,7 @@ export const MobileMenu = ({
|
||||
</div>
|
||||
</Section>
|
||||
) : appState.openMenu === "shape" &&
|
||||
!viewModeEnabled &&
|
||||
!appState.viewModeEnabled &&
|
||||
showSelectedShapeActions(appState, elements) ? (
|
||||
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
||||
<SelectedShapeActions
|
||||
@@ -229,18 +227,20 @@ export const MobileMenu = ({
|
||||
) : null}
|
||||
<footer className="App-toolbar">
|
||||
{renderAppToolbar()}
|
||||
{appState.scrolledOutside && !appState.openMenu && (
|
||||
<button
|
||||
className="scroll-back-to-content"
|
||||
onClick={() => {
|
||||
setAppState({
|
||||
...calculateScrollCenter(elements, appState, canvas),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("buttons.scrollBackToContent")}
|
||||
</button>
|
||||
)}
|
||||
{appState.scrolledOutside &&
|
||||
!appState.openMenu &&
|
||||
!appState.isLibraryOpen && (
|
||||
<button
|
||||
className="scroll-back-to-content"
|
||||
onClick={() => {
|
||||
setAppState({
|
||||
...calculateScrollCenter(elements, appState, canvas),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("buttons.scrollBackToContent")}
|
||||
</button>
|
||||
)}
|
||||
</footer>
|
||||
</Island>
|
||||
</div>
|
||||
|
@@ -8,7 +8,7 @@ import { useExcalidrawContainer, useDevice } from "./App";
|
||||
import { AppState } from "../types";
|
||||
import { THEME } from "../constants";
|
||||
|
||||
export const Modal = (props: {
|
||||
export const Modal: React.FC<{
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
maxWidth?: number;
|
||||
@@ -16,7 +16,7 @@ export const Modal = (props: {
|
||||
labelledBy: string;
|
||||
theme?: AppState["theme"];
|
||||
closeOnClickOutside?: boolean;
|
||||
}) => {
|
||||
}> = (props) => {
|
||||
const { theme = THEME.LIGHT, closeOnClickOutside = true } = props;
|
||||
const modalRoot = useBodyRoot(theme);
|
||||
|
||||
|
@@ -46,7 +46,7 @@ const ChartPreviewBtn = (props: {
|
||||
},
|
||||
null, // files
|
||||
);
|
||||
|
||||
previewNode.replaceChildren();
|
||||
previewNode.appendChild(svg);
|
||||
|
||||
if (props.selected) {
|
||||
@@ -55,7 +55,7 @@ const ChartPreviewBtn = (props: {
|
||||
})();
|
||||
|
||||
return () => {
|
||||
previewNode.removeChild(svg);
|
||||
previewNode.replaceChildren();
|
||||
};
|
||||
}, [props.spreadsheet, props.chartType, props.selected]);
|
||||
|
||||
|
@@ -2,5 +2,6 @@
|
||||
.popover {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
padding: 5px 0 5px;
|
||||
}
|
||||
}
|
||||
|
@@ -69,12 +69,26 @@ export const Popover = ({
|
||||
if (fitInViewport && popoverRef.current) {
|
||||
const element = popoverRef.current;
|
||||
const { x, y, width, height } = element.getBoundingClientRect();
|
||||
|
||||
//Position correctly when clicked on rightmost part or the bottom part of viewport
|
||||
if (x + width - offsetLeft > viewportWidth) {
|
||||
element.style.left = `${viewportWidth - width}px`;
|
||||
element.style.left = `${viewportWidth - width - 10}px`;
|
||||
}
|
||||
if (y + height - offsetTop > viewportHeight) {
|
||||
element.style.top = `${viewportHeight - height}px`;
|
||||
}
|
||||
|
||||
//Resize to fit viewport on smaller screens
|
||||
if (height >= viewportHeight) {
|
||||
element.style.height = `${viewportHeight - 20}px`;
|
||||
element.style.top = "10px";
|
||||
element.style.overflowY = "scroll";
|
||||
}
|
||||
if (width >= viewportWidth) {
|
||||
element.style.width = `${viewportWidth}px`;
|
||||
element.style.left = "0px";
|
||||
element.style.overflowX = "scroll";
|
||||
}
|
||||
}
|
||||
}, [fitInViewport, viewportWidth, viewportHeight, offsetLeft, offsetTop]);
|
||||
|
||||
|
@@ -2,12 +2,11 @@ import React from "react";
|
||||
import { t } from "../i18n";
|
||||
import { useExcalidrawContainer } from "./App";
|
||||
|
||||
interface SectionProps extends React.HTMLProps<HTMLElement> {
|
||||
export const Section: React.FC<{
|
||||
heading: string;
|
||||
children: React.ReactNode | ((header: React.ReactNode) => React.ReactNode);
|
||||
}
|
||||
|
||||
export const Section = ({ heading, children, ...props }: SectionProps) => {
|
||||
children?: React.ReactNode | ((heading: React.ReactNode) => React.ReactNode);
|
||||
className?: string;
|
||||
}> = ({ heading, children, ...props }) => {
|
||||
const { id } = useExcalidrawContainer();
|
||||
const header = (
|
||||
<h2 className="visually-hidden" id={`${id}-${heading}-title`}>
|
||||
|
@@ -7,13 +7,13 @@ const DEFAULT_TOAST_TIMEOUT = 5000;
|
||||
|
||||
export const Toast = ({
|
||||
message,
|
||||
clearToast,
|
||||
onClose,
|
||||
closable = false,
|
||||
// To prevent autoclose, pass duration as Infinity
|
||||
duration = DEFAULT_TOAST_TIMEOUT,
|
||||
}: {
|
||||
message: string;
|
||||
clearToast: () => void;
|
||||
onClose: () => void;
|
||||
closable?: boolean;
|
||||
duration?: number;
|
||||
}) => {
|
||||
@@ -23,8 +23,8 @@ export const Toast = ({
|
||||
if (!shouldAutoClose) {
|
||||
return;
|
||||
}
|
||||
timerRef.current = window.setTimeout(() => clearToast(), duration);
|
||||
}, [clearToast, duration, shouldAutoClose]);
|
||||
timerRef.current = window.setTimeout(() => onClose(), duration);
|
||||
}, [onClose, duration, shouldAutoClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldAutoClose) {
|
||||
@@ -50,7 +50,7 @@ export const Toast = ({
|
||||
icon={close}
|
||||
aria-label="close"
|
||||
type="icon"
|
||||
onClick={clearToast}
|
||||
onClick={onClose}
|
||||
className="close"
|
||||
/>
|
||||
)}
|
||||
|
@@ -212,16 +212,14 @@
|
||||
}
|
||||
}
|
||||
.ToolIcon.ToolIcon__library {
|
||||
top: 100px;
|
||||
top: calc(var(--sat) + 100px);
|
||||
}
|
||||
|
||||
.ToolIcon.ToolIcon__lock {
|
||||
margin-inline-end: 0;
|
||||
top: 60px;
|
||||
top: calc(var(--sat) + 60px);
|
||||
}
|
||||
.ToolIcon.ToolIcon__penMode {
|
||||
margin-inline-end: 0;
|
||||
top: 140px;
|
||||
top: calc(var(--sat) + 140px);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -32,7 +32,6 @@
|
||||
}
|
||||
|
||||
.ToolIcon.ToolIcon__lock {
|
||||
margin-inline-end: var(--space-factor);
|
||||
&.ToolIcon_type_floating {
|
||||
margin-left: 0.1rem;
|
||||
}
|
||||
|
@@ -67,13 +67,14 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
|
||||
};
|
||||
|
||||
const restoreElementWithProperties = <
|
||||
T extends ExcalidrawElement,
|
||||
K extends Pick<T, keyof Omit<Required<T>, keyof ExcalidrawElement>>,
|
||||
>(
|
||||
element: Required<T> & {
|
||||
T extends Required<Omit<ExcalidrawElement, "customData">> & {
|
||||
customData?: ExcalidrawElement["customData"];
|
||||
/** @deprecated */
|
||||
boundElementIds?: readonly ExcalidrawElement["id"][];
|
||||
},
|
||||
K extends Pick<T, keyof Omit<Required<T>, keyof ExcalidrawElement>>,
|
||||
>(
|
||||
element: T,
|
||||
extra: Pick<
|
||||
T,
|
||||
// This extra Pick<T, keyof K> ensure no excess properties are passed.
|
||||
@@ -115,6 +116,10 @@ const restoreElementWithProperties = <
|
||||
locked: element.locked ?? false,
|
||||
};
|
||||
|
||||
if ("customData" in element) {
|
||||
base.customData = element.customData;
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
...getNormalizedDimensions(base),
|
||||
|
@@ -18,6 +18,7 @@ import { rescalePoints } from "../points";
|
||||
|
||||
// x and y position of top left corner, x and y position of bottom right corner
|
||||
export type Bounds = readonly [number, number, number, number];
|
||||
type MaybeQuadraticSolution = [number | null, number | null] | false;
|
||||
|
||||
// If the element is created from right to left, the width is going to be negative
|
||||
// This set of functions retrieves the absolute position of the 4 points.
|
||||
@@ -68,11 +69,95 @@ export const getCurvePathOps = (shape: Drawable): Op[] => {
|
||||
return shape.sets[0].ops;
|
||||
};
|
||||
|
||||
// reference: https://eliot-jones.com/2019/12/cubic-bezier-curve-bounding-boxes
|
||||
const getBezierValueForT = (
|
||||
t: number,
|
||||
p0: number,
|
||||
p1: number,
|
||||
p2: number,
|
||||
p3: number,
|
||||
) => {
|
||||
const oneMinusT = 1 - t;
|
||||
return (
|
||||
Math.pow(oneMinusT, 3) * p0 +
|
||||
3 * Math.pow(oneMinusT, 2) * t * p1 +
|
||||
3 * oneMinusT * Math.pow(t, 2) * p2 +
|
||||
Math.pow(t, 3) * p3
|
||||
);
|
||||
};
|
||||
|
||||
const solveQuadratic = (
|
||||
p0: number,
|
||||
p1: number,
|
||||
p2: number,
|
||||
p3: number,
|
||||
): MaybeQuadraticSolution => {
|
||||
const i = p1 - p0;
|
||||
const j = p2 - p1;
|
||||
const k = p3 - p2;
|
||||
|
||||
const a = 3 * i - 6 * j + 3 * k;
|
||||
const b = 6 * j - 6 * i;
|
||||
const c = 3 * i;
|
||||
|
||||
const sqrtPart = b * b - 4 * a * c;
|
||||
const hasSolution = sqrtPart >= 0;
|
||||
|
||||
if (!hasSolution) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const t1 = (-b + Math.sqrt(sqrtPart)) / (2 * a);
|
||||
const t2 = (-b - Math.sqrt(sqrtPart)) / (2 * a);
|
||||
|
||||
let s1 = null;
|
||||
let s2 = null;
|
||||
|
||||
if (t1 >= 0 && t1 <= 1) {
|
||||
s1 = getBezierValueForT(t1, p0, p1, p2, p3);
|
||||
}
|
||||
|
||||
if (t2 >= 0 && t2 <= 1) {
|
||||
s2 = getBezierValueForT(t2, p0, p1, p2, p3);
|
||||
}
|
||||
|
||||
return [s1, s2];
|
||||
};
|
||||
|
||||
const getCubicBezierCurveBound = (
|
||||
p0: Point,
|
||||
p1: Point,
|
||||
p2: Point,
|
||||
p3: Point,
|
||||
): Bounds => {
|
||||
const solX = solveQuadratic(p0[0], p1[0], p2[0], p3[0]);
|
||||
const solY = solveQuadratic(p0[1], p1[1], p2[1], p3[1]);
|
||||
|
||||
let minX = Math.min(p0[0], p3[0]);
|
||||
let maxX = Math.max(p0[0], p3[0]);
|
||||
|
||||
if (solX) {
|
||||
const xs = solX.filter((x) => x !== null) as number[];
|
||||
minX = Math.min(minX, ...xs);
|
||||
maxX = Math.max(maxX, ...xs);
|
||||
}
|
||||
|
||||
let minY = Math.min(p0[1], p3[1]);
|
||||
let maxY = Math.max(p0[1], p3[1]);
|
||||
if (solY) {
|
||||
const ys = solY.filter((y) => y !== null) as number[];
|
||||
minY = Math.min(minY, ...ys);
|
||||
maxY = Math.max(maxY, ...ys);
|
||||
}
|
||||
return [minX, minY, maxX, maxY];
|
||||
};
|
||||
|
||||
const getMinMaxXYFromCurvePathOps = (
|
||||
ops: Op[],
|
||||
transformXY?: (x: number, y: number) => [number, number],
|
||||
): [number, number, number, number] => {
|
||||
let currentP: Point = [0, 0];
|
||||
|
||||
const { minX, minY, maxX, maxY } = ops.reduce(
|
||||
(limits, { op, data }) => {
|
||||
// There are only four operation types:
|
||||
@@ -83,38 +168,29 @@ const getMinMaxXYFromCurvePathOps = (
|
||||
// move operation does not draw anything; so, it always
|
||||
// returns false
|
||||
} else if (op === "bcurveTo") {
|
||||
// create points from bezier curve
|
||||
// bezier curve stores data as a flattened array of three positions
|
||||
// [x1, y1, x2, y2, x3, y3]
|
||||
const p1 = [data[0], data[1]] as Point;
|
||||
const p2 = [data[2], data[3]] as Point;
|
||||
const p3 = [data[4], data[5]] as Point;
|
||||
const _p1 = [data[0], data[1]] as Point;
|
||||
const _p2 = [data[2], data[3]] as Point;
|
||||
const _p3 = [data[4], data[5]] as Point;
|
||||
|
||||
const p0 = currentP;
|
||||
currentP = p3;
|
||||
const p1 = transformXY ? transformXY(..._p1) : _p1;
|
||||
const p2 = transformXY ? transformXY(..._p2) : _p2;
|
||||
const p3 = transformXY ? transformXY(..._p3) : _p3;
|
||||
|
||||
const equation = (t: number, idx: number) =>
|
||||
Math.pow(1 - t, 3) * p3[idx] +
|
||||
3 * t * Math.pow(1 - t, 2) * p2[idx] +
|
||||
3 * Math.pow(t, 2) * (1 - t) * p1[idx] +
|
||||
p0[idx] * Math.pow(t, 3);
|
||||
const p0 = transformXY ? transformXY(...currentP) : currentP;
|
||||
currentP = _p3;
|
||||
|
||||
let t = 0;
|
||||
while (t <= 1.0) {
|
||||
let x = equation(t, 0);
|
||||
let y = equation(t, 1);
|
||||
if (transformXY) {
|
||||
[x, y] = transformXY(x, y);
|
||||
}
|
||||
const [minX, minY, maxX, maxY] = getCubicBezierCurveBound(
|
||||
p0,
|
||||
p1,
|
||||
p2,
|
||||
p3,
|
||||
);
|
||||
|
||||
limits.minY = Math.min(limits.minY, y);
|
||||
limits.minX = Math.min(limits.minX, x);
|
||||
limits.minX = Math.min(limits.minX, minX);
|
||||
limits.minY = Math.min(limits.minY, minY);
|
||||
|
||||
limits.maxX = Math.max(limits.maxX, x);
|
||||
limits.maxY = Math.max(limits.maxY, y);
|
||||
|
||||
t += 0.1;
|
||||
}
|
||||
limits.maxX = Math.max(limits.maxX, maxX);
|
||||
limits.maxY = Math.max(limits.maxY, maxY);
|
||||
} else if (op === "lineTo") {
|
||||
// TODO: Implement this
|
||||
} else if (op === "qcurveTo") {
|
||||
@@ -124,7 +200,6 @@ const getMinMaxXYFromCurvePathOps = (
|
||||
},
|
||||
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
||||
);
|
||||
|
||||
return [minX, minY, maxX, maxY];
|
||||
};
|
||||
|
||||
@@ -420,6 +495,7 @@ export const getResizedElementAbsoluteCoords = (
|
||||
element: ExcalidrawElement,
|
||||
nextWidth: number,
|
||||
nextHeight: number,
|
||||
normalizePoints: boolean,
|
||||
): [number, number, number, number] => {
|
||||
if (!(isLinearElement(element) || isFreeDrawElement(element))) {
|
||||
return [
|
||||
@@ -433,7 +509,8 @@ export const getResizedElementAbsoluteCoords = (
|
||||
const points = rescalePoints(
|
||||
0,
|
||||
nextWidth,
|
||||
rescalePoints(1, nextHeight, element.points),
|
||||
rescalePoints(1, nextHeight, element.points, normalizePoints),
|
||||
normalizePoints,
|
||||
);
|
||||
|
||||
let bounds: [number, number, number, number];
|
||||
|
@@ -35,6 +35,7 @@ import { getShapeForElement } from "../renderer/renderElement";
|
||||
import { hasBoundTextElement, isImageElement } from "./typeChecks";
|
||||
import { isTextElement } from ".";
|
||||
import { isTransparent } from "../utils";
|
||||
import { shouldShowBoundingBox } from "./transformHandles";
|
||||
|
||||
const isElementDraggableFromInside = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
@@ -64,7 +65,10 @@ export const hitTest = (
|
||||
const threshold = 10 / appState.zoom.value;
|
||||
const point: Point = [x, y];
|
||||
|
||||
if (isElementSelected(appState, element)) {
|
||||
if (
|
||||
isElementSelected(appState, element) &&
|
||||
shouldShowBoundingBox([element], appState)
|
||||
) {
|
||||
return isPointHittingElementBoundingBox(element, point, threshold);
|
||||
}
|
||||
|
||||
|
@@ -105,15 +105,26 @@ export const dragNewElement = (
|
||||
true */
|
||||
widthAspectRatio?: number | null,
|
||||
) => {
|
||||
if (shouldMaintainAspectRatio) {
|
||||
if (shouldMaintainAspectRatio && draggingElement.type !== "selection") {
|
||||
if (widthAspectRatio) {
|
||||
height = width / widthAspectRatio;
|
||||
} else {
|
||||
({ width, height } = getPerfectElementSize(
|
||||
elementType,
|
||||
width,
|
||||
y < originY ? -height : height,
|
||||
));
|
||||
// Depending on where the cursor is at (x, y) relative to where the starting point is
|
||||
// (originX, originY), we use ONLY width or height to control size increase.
|
||||
// This allows the cursor to always "stick" to one of the sides of the bounding box.
|
||||
if (Math.abs(y - originY) > Math.abs(x - originX)) {
|
||||
({ width, height } = getPerfectElementSize(
|
||||
elementType,
|
||||
height,
|
||||
x < originX ? -width : width,
|
||||
));
|
||||
} else {
|
||||
({ width, height } = getPerfectElementSize(
|
||||
elementType,
|
||||
width,
|
||||
y < originY ? -height : height,
|
||||
));
|
||||
}
|
||||
|
||||
if (height < 0) {
|
||||
height = -height;
|
||||
|
@@ -53,6 +53,7 @@ export { textWysiwyg } from "./textWysiwyg";
|
||||
export { redrawTextBoundingBox } from "./textElement";
|
||||
export {
|
||||
getPerfectElementSize,
|
||||
getLockedLinearCursorAlignSize,
|
||||
isInvisiblySmallElement,
|
||||
resizePerfectLineForNWHandler,
|
||||
getNormalizedDimensions,
|
||||
|
@@ -5,8 +5,15 @@ import {
|
||||
PointBinding,
|
||||
ExcalidrawBindableElement,
|
||||
} from "./types";
|
||||
import { distance2d, rotate, isPathALoop, getGridPoint } from "../math";
|
||||
import { getElementAbsoluteCoords } from ".";
|
||||
import {
|
||||
distance2d,
|
||||
rotate,
|
||||
isPathALoop,
|
||||
getGridPoint,
|
||||
rotatePoint,
|
||||
centerPoint,
|
||||
} from "../math";
|
||||
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
||||
import { getElementPointsCoords } from "./bounds";
|
||||
import { Point, AppState } from "../types";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
@@ -20,26 +27,32 @@ import {
|
||||
} from "./binding";
|
||||
import { tupleToCoors } from "../utils";
|
||||
import { isBindingElement } from "./typeChecks";
|
||||
import { shouldRotateWithDiscreteAngle } from "../keys";
|
||||
|
||||
export class LinearElementEditor {
|
||||
public elementId: ExcalidrawElement["id"] & {
|
||||
public readonly elementId: ExcalidrawElement["id"] & {
|
||||
_brand: "excalidrawLinearElementId";
|
||||
};
|
||||
/** indices */
|
||||
public selectedPointsIndices: readonly number[] | null;
|
||||
public readonly selectedPointsIndices: readonly number[] | null;
|
||||
|
||||
public pointerDownState: Readonly<{
|
||||
public readonly pointerDownState: Readonly<{
|
||||
prevSelectedPointsIndices: readonly number[] | null;
|
||||
/** index */
|
||||
lastClickedPoint: number;
|
||||
}>;
|
||||
|
||||
/** whether you're dragging a point */
|
||||
public isDragging: boolean;
|
||||
public lastUncommittedPoint: Point | null;
|
||||
public pointerOffset: Readonly<{ x: number; y: number }>;
|
||||
public startBindingElement: ExcalidrawBindableElement | null | "keep";
|
||||
public endBindingElement: ExcalidrawBindableElement | null | "keep";
|
||||
public readonly isDragging: boolean;
|
||||
public readonly lastUncommittedPoint: Point | null;
|
||||
public readonly pointerOffset: Readonly<{ x: number; y: number }>;
|
||||
public readonly startBindingElement:
|
||||
| ExcalidrawBindableElement
|
||||
| null
|
||||
| "keep";
|
||||
public readonly endBindingElement: ExcalidrawBindableElement | null | "keep";
|
||||
public readonly hoverPointIndex: number;
|
||||
public readonly midPointHovered: boolean;
|
||||
|
||||
constructor(element: NonDeleted<ExcalidrawLinearElement>, scene: Scene) {
|
||||
this.elementId = element.id as string & {
|
||||
@@ -58,13 +71,15 @@ export class LinearElementEditor {
|
||||
prevSelectedPointsIndices: null,
|
||||
lastClickedPoint: -1,
|
||||
};
|
||||
this.hoverPointIndex = -1;
|
||||
this.midPointHovered = false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// static methods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static POINT_HANDLE_SIZE = 20;
|
||||
static POINT_HANDLE_SIZE = 10;
|
||||
|
||||
/**
|
||||
* @param id the `elementId` from the instance of this class (so that we can
|
||||
@@ -132,22 +147,20 @@ export class LinearElementEditor {
|
||||
|
||||
/** @returns whether point was dragged */
|
||||
static handlePointDragging(
|
||||
event: PointerEvent,
|
||||
appState: AppState,
|
||||
setState: React.Component<any, AppState>["setState"],
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
maybeSuggestBinding: (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
pointSceneCoords: { x: number; y: number }[],
|
||||
) => void,
|
||||
linearElementEditor: LinearElementEditor,
|
||||
): boolean {
|
||||
if (!appState.editingLinearElement) {
|
||||
if (!linearElementEditor) {
|
||||
return false;
|
||||
}
|
||||
const { editingLinearElement } = appState;
|
||||
const { selectedPointsIndices, elementId, isDragging } =
|
||||
editingLinearElement;
|
||||
|
||||
const { selectedPointsIndices, elementId } = linearElementEditor;
|
||||
const element = LinearElementEditor.getElement(elementId);
|
||||
if (!element) {
|
||||
return false;
|
||||
@@ -155,54 +168,71 @@ export class LinearElementEditor {
|
||||
|
||||
// point that's being dragged (out of all selected points)
|
||||
const draggingPoint = element.points[
|
||||
editingLinearElement.pointerDownState.lastClickedPoint
|
||||
linearElementEditor.pointerDownState.lastClickedPoint
|
||||
] as [number, number] | undefined;
|
||||
|
||||
if (selectedPointsIndices && draggingPoint) {
|
||||
if (isDragging === false) {
|
||||
setState({
|
||||
editingLinearElement: {
|
||||
...editingLinearElement,
|
||||
isDragging: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (
|
||||
shouldRotateWithDiscreteAngle(event) &&
|
||||
selectedPointsIndices.length === 1 &&
|
||||
element.points.length > 1
|
||||
) {
|
||||
const selectedIndex = selectedPointsIndices[0];
|
||||
const referencePoint =
|
||||
element.points[selectedIndex === 0 ? 1 : selectedIndex - 1];
|
||||
|
||||
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
||||
element,
|
||||
scenePointerX - editingLinearElement.pointerOffset.x,
|
||||
scenePointerY - editingLinearElement.pointerOffset.y,
|
||||
appState.gridSize,
|
||||
);
|
||||
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
||||
element,
|
||||
referencePoint,
|
||||
[scenePointerX, scenePointerY],
|
||||
appState.gridSize,
|
||||
);
|
||||
|
||||
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
|
||||
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
|
||||
|
||||
LinearElementEditor.movePoints(
|
||||
element,
|
||||
selectedPointsIndices.map((pointIndex) => {
|
||||
const newPointPosition =
|
||||
pointIndex ===
|
||||
editingLinearElement.pointerDownState.lastClickedPoint
|
||||
? LinearElementEditor.createPointAt(
|
||||
element,
|
||||
scenePointerX - editingLinearElement.pointerOffset.x,
|
||||
scenePointerY - editingLinearElement.pointerOffset.y,
|
||||
appState.gridSize,
|
||||
)
|
||||
: ([
|
||||
element.points[pointIndex][0] + deltaX,
|
||||
element.points[pointIndex][1] + deltaY,
|
||||
] as const);
|
||||
return {
|
||||
index: pointIndex,
|
||||
point: newPointPosition,
|
||||
LinearElementEditor.movePoints(element, [
|
||||
{
|
||||
index: selectedIndex,
|
||||
point: [width + referencePoint[0], height + referencePoint[1]],
|
||||
isDragging:
|
||||
selectedIndex ===
|
||||
linearElementEditor.pointerDownState.lastClickedPoint,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
||||
element,
|
||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
appState.gridSize,
|
||||
);
|
||||
|
||||
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
|
||||
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
|
||||
|
||||
LinearElementEditor.movePoints(
|
||||
element,
|
||||
selectedPointsIndices.map((pointIndex) => {
|
||||
const newPointPosition =
|
||||
pointIndex ===
|
||||
editingLinearElement.pointerDownState.lastClickedPoint,
|
||||
};
|
||||
}),
|
||||
);
|
||||
linearElementEditor.pointerDownState.lastClickedPoint
|
||||
? LinearElementEditor.createPointAt(
|
||||
element,
|
||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
appState.gridSize,
|
||||
)
|
||||
: ([
|
||||
element.points[pointIndex][0] + deltaX,
|
||||
element.points[pointIndex][1] + deltaY,
|
||||
] as const);
|
||||
return {
|
||||
index: pointIndex,
|
||||
point: newPointPosition,
|
||||
isDragging:
|
||||
pointIndex ===
|
||||
linearElementEditor.pointerDownState.lastClickedPoint,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// suggest bindings for first and last point if selected
|
||||
if (isBindingElement(element, false)) {
|
||||
@@ -256,10 +286,12 @@ export class LinearElementEditor {
|
||||
return editingLinearElement;
|
||||
}
|
||||
|
||||
const bindings: Partial<
|
||||
Pick<
|
||||
InstanceType<typeof LinearElementEditor>,
|
||||
"startBindingElement" | "endBindingElement"
|
||||
const bindings: Mutable<
|
||||
Partial<
|
||||
Pick<
|
||||
InstanceType<typeof LinearElementEditor>,
|
||||
"startBindingElement" | "endBindingElement"
|
||||
>
|
||||
>
|
||||
> = {};
|
||||
|
||||
@@ -327,34 +359,126 @@ export class LinearElementEditor {
|
||||
};
|
||||
}
|
||||
|
||||
static isHittingMidPoint = (
|
||||
linearElementEditor: LinearElementEditor,
|
||||
scenePointer: { x: number; y: number },
|
||||
appState: AppState,
|
||||
) => {
|
||||
const { elementId } = linearElementEditor;
|
||||
const element = LinearElementEditor.getElement(elementId);
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor(
|
||||
element,
|
||||
appState.zoom,
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
);
|
||||
if (clickedPointIndex >= 0) {
|
||||
return false;
|
||||
}
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(element);
|
||||
if (points.length >= 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const midPoint = LinearElementEditor.getMidPoint(linearElementEditor);
|
||||
if (midPoint) {
|
||||
const threshold =
|
||||
LinearElementEditor.POINT_HANDLE_SIZE / appState.zoom.value;
|
||||
const distance = distance2d(
|
||||
midPoint[0],
|
||||
midPoint[1],
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
);
|
||||
return distance <= threshold;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
static getMidPoint(linearElementEditor: LinearElementEditor) {
|
||||
const { elementId } = linearElementEditor;
|
||||
const element = LinearElementEditor.getElement(elementId);
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(element);
|
||||
|
||||
return centerPoint(points[0], points.at(-1)!);
|
||||
}
|
||||
|
||||
static handlePointerDown(
|
||||
event: React.PointerEvent<HTMLCanvasElement>,
|
||||
appState: AppState,
|
||||
setState: React.Component<any, AppState>["setState"],
|
||||
history: History,
|
||||
scenePointer: { x: number; y: number },
|
||||
linearElementEditor: LinearElementEditor,
|
||||
): {
|
||||
didAddPoint: boolean;
|
||||
hitElement: NonDeleted<ExcalidrawElement> | null;
|
||||
linearElementEditor: LinearElementEditor | null;
|
||||
isMidPoint: boolean;
|
||||
} {
|
||||
const ret: ReturnType<typeof LinearElementEditor["handlePointerDown"]> = {
|
||||
didAddPoint: false,
|
||||
hitElement: null,
|
||||
linearElementEditor: null,
|
||||
isMidPoint: false,
|
||||
};
|
||||
|
||||
if (!appState.editingLinearElement) {
|
||||
if (!linearElementEditor) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
const { elementId } = appState.editingLinearElement;
|
||||
const { elementId } = linearElementEditor;
|
||||
const element = LinearElementEditor.getElement(elementId);
|
||||
|
||||
if (!element) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (event.altKey) {
|
||||
if (appState.editingLinearElement.lastUncommittedPoint == null) {
|
||||
const hittingMidPoint = LinearElementEditor.isHittingMidPoint(
|
||||
linearElementEditor,
|
||||
scenePointer,
|
||||
appState,
|
||||
);
|
||||
if (
|
||||
LinearElementEditor.isHittingMidPoint(
|
||||
linearElementEditor,
|
||||
scenePointer,
|
||||
appState,
|
||||
)
|
||||
) {
|
||||
const midPoint = LinearElementEditor.getMidPoint(linearElementEditor);
|
||||
if (midPoint) {
|
||||
mutateElement(element, {
|
||||
points: [
|
||||
element.points[0],
|
||||
LinearElementEditor.createPointAt(
|
||||
element,
|
||||
midPoint[0],
|
||||
midPoint[1],
|
||||
appState.gridSize,
|
||||
),
|
||||
...element.points.slice(1),
|
||||
],
|
||||
});
|
||||
}
|
||||
ret.didAddPoint = true;
|
||||
ret.isMidPoint = true;
|
||||
ret.linearElementEditor = {
|
||||
...linearElementEditor,
|
||||
selectedPointsIndices: element.points[1],
|
||||
pointerDownState: {
|
||||
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
|
||||
lastClickedPoint: -1,
|
||||
},
|
||||
lastUncommittedPoint: null,
|
||||
};
|
||||
}
|
||||
if (event.altKey && appState.editingLinearElement) {
|
||||
if (linearElementEditor.lastUncommittedPoint == null) {
|
||||
mutateElement(element, {
|
||||
points: [
|
||||
...element.points,
|
||||
@@ -366,24 +490,23 @@ export class LinearElementEditor {
|
||||
),
|
||||
],
|
||||
});
|
||||
ret.didAddPoint = true;
|
||||
}
|
||||
history.resumeRecording();
|
||||
setState({
|
||||
editingLinearElement: {
|
||||
...appState.editingLinearElement,
|
||||
pointerDownState: {
|
||||
prevSelectedPointsIndices:
|
||||
appState.editingLinearElement.selectedPointsIndices,
|
||||
lastClickedPoint: -1,
|
||||
},
|
||||
selectedPointsIndices: [element.points.length - 1],
|
||||
lastUncommittedPoint: null,
|
||||
endBindingElement: getHoveredElementForBinding(
|
||||
scenePointer,
|
||||
Scene.getScene(element)!,
|
||||
),
|
||||
ret.linearElementEditor = {
|
||||
...linearElementEditor,
|
||||
pointerDownState: {
|
||||
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
|
||||
lastClickedPoint: -1,
|
||||
},
|
||||
});
|
||||
selectedPointsIndices: [element.points.length - 1],
|
||||
lastUncommittedPoint: null,
|
||||
endBindingElement: getHoveredElementForBinding(
|
||||
scenePointer,
|
||||
Scene.getScene(element)!,
|
||||
),
|
||||
};
|
||||
|
||||
ret.didAddPoint = true;
|
||||
return ret;
|
||||
}
|
||||
@@ -397,7 +520,7 @@ export class LinearElementEditor {
|
||||
|
||||
// if we clicked on a point, set the element as hitElement otherwise
|
||||
// it would get deselected if the point is outside the hitbox area
|
||||
if (clickedPointIndex > -1) {
|
||||
if (clickedPointIndex >= 0 || hittingMidPoint) {
|
||||
ret.hitElement = element;
|
||||
} else {
|
||||
// You might be wandering why we are storing the binding elements on
|
||||
@@ -405,8 +528,7 @@ export class LinearElementEditor {
|
||||
// from the end points of the `linearElement` - this is to allow disabling
|
||||
// binding (which needs to happen at the point the user finishes moving
|
||||
// the point).
|
||||
const { startBindingElement, endBindingElement } =
|
||||
appState.editingLinearElement;
|
||||
const { startBindingElement, endBindingElement } = linearElementEditor;
|
||||
if (isBindingEnabled(appState) && isBindingElement(element)) {
|
||||
bindOrUnbindLinearElement(
|
||||
element,
|
||||
@@ -432,33 +554,28 @@ export class LinearElementEditor {
|
||||
const nextSelectedPointsIndices =
|
||||
clickedPointIndex > -1 || event.shiftKey
|
||||
? event.shiftKey ||
|
||||
appState.editingLinearElement.selectedPointsIndices?.includes(
|
||||
clickedPointIndex,
|
||||
)
|
||||
linearElementEditor.selectedPointsIndices?.includes(clickedPointIndex)
|
||||
? normalizeSelectedPoints([
|
||||
...(appState.editingLinearElement.selectedPointsIndices || []),
|
||||
...(linearElementEditor.selectedPointsIndices || []),
|
||||
clickedPointIndex,
|
||||
])
|
||||
: [clickedPointIndex]
|
||||
: null;
|
||||
|
||||
setState({
|
||||
editingLinearElement: {
|
||||
...appState.editingLinearElement,
|
||||
pointerDownState: {
|
||||
prevSelectedPointsIndices:
|
||||
appState.editingLinearElement.selectedPointsIndices,
|
||||
lastClickedPoint: clickedPointIndex,
|
||||
},
|
||||
selectedPointsIndices: nextSelectedPointsIndices,
|
||||
pointerOffset: targetPoint
|
||||
? {
|
||||
x: scenePointer.x - targetPoint[0],
|
||||
y: scenePointer.y - targetPoint[1],
|
||||
}
|
||||
: { x: 0, y: 0 },
|
||||
ret.linearElementEditor = {
|
||||
...linearElementEditor,
|
||||
pointerDownState: {
|
||||
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
|
||||
lastClickedPoint: clickedPointIndex,
|
||||
},
|
||||
});
|
||||
selectedPointsIndices: nextSelectedPointsIndices,
|
||||
pointerOffset: targetPoint
|
||||
? {
|
||||
x: scenePointer.x - targetPoint[0],
|
||||
y: scenePointer.y - targetPoint[1],
|
||||
}
|
||||
: { x: 0, y: 0 },
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -466,13 +583,13 @@ export class LinearElementEditor {
|
||||
event: React.PointerEvent<HTMLCanvasElement>,
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
editingLinearElement: LinearElementEditor,
|
||||
linearElementEditor: LinearElementEditor,
|
||||
gridSize: number | null,
|
||||
): LinearElementEditor {
|
||||
const { elementId, lastUncommittedPoint } = editingLinearElement;
|
||||
const { elementId, lastUncommittedPoint } = linearElementEditor;
|
||||
const element = LinearElementEditor.getElement(elementId);
|
||||
if (!element) {
|
||||
return editingLinearElement;
|
||||
return linearElementEditor;
|
||||
}
|
||||
|
||||
const { points } = element;
|
||||
@@ -482,15 +599,33 @@ export class LinearElementEditor {
|
||||
if (lastPoint === lastUncommittedPoint) {
|
||||
LinearElementEditor.deletePoints(element, [points.length - 1]);
|
||||
}
|
||||
return { ...editingLinearElement, lastUncommittedPoint: null };
|
||||
return { ...linearElementEditor, lastUncommittedPoint: null };
|
||||
}
|
||||
|
||||
const newPoint = LinearElementEditor.createPointAt(
|
||||
element,
|
||||
scenePointerX - editingLinearElement.pointerOffset.x,
|
||||
scenePointerY - editingLinearElement.pointerOffset.y,
|
||||
gridSize,
|
||||
);
|
||||
let newPoint: Point;
|
||||
|
||||
if (shouldRotateWithDiscreteAngle(event) && points.length >= 2) {
|
||||
const lastCommittedPoint = points[points.length - 2];
|
||||
|
||||
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
||||
element,
|
||||
lastCommittedPoint,
|
||||
[scenePointerX, scenePointerY],
|
||||
gridSize,
|
||||
);
|
||||
|
||||
newPoint = [
|
||||
width + lastCommittedPoint[0],
|
||||
height + lastCommittedPoint[1],
|
||||
];
|
||||
} else {
|
||||
newPoint = LinearElementEditor.createPointAt(
|
||||
element,
|
||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
gridSize,
|
||||
);
|
||||
}
|
||||
|
||||
if (lastPoint === lastUncommittedPoint) {
|
||||
LinearElementEditor.movePoints(element, [
|
||||
@@ -504,7 +639,7 @@ export class LinearElementEditor {
|
||||
}
|
||||
|
||||
return {
|
||||
...editingLinearElement,
|
||||
...linearElementEditor,
|
||||
lastUncommittedPoint: element.points[element.points.length - 1],
|
||||
};
|
||||
}
|
||||
@@ -526,14 +661,14 @@ export class LinearElementEditor {
|
||||
/** scene coords */
|
||||
static getPointsGlobalCoordinates(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
) {
|
||||
): Point[] {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
return element.points.map((point) => {
|
||||
let { x, y } = element;
|
||||
[x, y] = rotate(x + point[0], y + point[1], cx, cy, element.angle);
|
||||
return [x, y];
|
||||
return [x, y] as const;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -577,7 +712,8 @@ export class LinearElementEditor {
|
||||
x: number,
|
||||
y: number,
|
||||
) {
|
||||
const pointHandles = this.getPointsGlobalCoordinates(element);
|
||||
const pointHandles =
|
||||
LinearElementEditor.getPointsGlobalCoordinates(element);
|
||||
let idx = pointHandles.length;
|
||||
// loop from right to left because points on the right are rendered over
|
||||
// points on the left, thus should take precedence when clicking, if they
|
||||
@@ -587,7 +723,7 @@ export class LinearElementEditor {
|
||||
if (
|
||||
distance2d(x, y, point[0], point[1]) * zoom.value <
|
||||
// +1px to account for outline stroke
|
||||
this.POINT_HANDLE_SIZE / 2 + 1
|
||||
LinearElementEditor.POINT_HANDLE_SIZE + 1
|
||||
) {
|
||||
return idx;
|
||||
}
|
||||
@@ -775,9 +911,9 @@ export class LinearElementEditor {
|
||||
|
||||
if (selectedOriginPoint) {
|
||||
offsetX =
|
||||
selectedOriginPoint.point[0] - points[selectedOriginPoint.index][0];
|
||||
selectedOriginPoint.point[0] + points[selectedOriginPoint.index][0];
|
||||
offsetY =
|
||||
selectedOriginPoint.point[1] - points[selectedOriginPoint.index][1];
|
||||
selectedOriginPoint.point[1] + points[selectedOriginPoint.index][1];
|
||||
}
|
||||
|
||||
const nextPoints = points.map((point, idx) => {
|
||||
@@ -840,6 +976,33 @@ export class LinearElementEditor {
|
||||
y: element.y + rotated[1],
|
||||
});
|
||||
}
|
||||
|
||||
private static _getShiftLockedDelta(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
referencePoint: Point,
|
||||
scenePointer: Point,
|
||||
gridSize: number | null,
|
||||
) {
|
||||
const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates(
|
||||
element,
|
||||
referencePoint,
|
||||
);
|
||||
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
scenePointer[0],
|
||||
scenePointer[1],
|
||||
gridSize,
|
||||
);
|
||||
|
||||
const { width, height } = getLockedLinearCursorAlignSize(
|
||||
referencePointCoords[0],
|
||||
referencePointCoords[1],
|
||||
gridX,
|
||||
gridY,
|
||||
);
|
||||
|
||||
return rotatePoint([width, height], [0, 0], -element.angle);
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeSelectedPoints = (
|
||||
|
@@ -198,6 +198,7 @@ const getAdjustedDimensions = (
|
||||
element,
|
||||
nextWidth,
|
||||
nextHeight,
|
||||
false,
|
||||
);
|
||||
const deltaX1 = (x1 - nextX1) / 2;
|
||||
const deltaY1 = (y1 - nextY1) / 2;
|
||||
|
@@ -18,6 +18,7 @@ import {
|
||||
getElementAbsoluteCoords,
|
||||
getCommonBounds,
|
||||
getResizedElementAbsoluteCoords,
|
||||
getCommonBoundingBox,
|
||||
} from "./bounds";
|
||||
import {
|
||||
isFreeDrawElement,
|
||||
@@ -137,8 +138,10 @@ export const transformElements = (
|
||||
transformHandleType === "se"
|
||||
) {
|
||||
resizeMultipleElements(
|
||||
pointerDownState,
|
||||
selectedElements,
|
||||
transformHandleType,
|
||||
shouldResizeFromCenter,
|
||||
pointerX,
|
||||
pointerY,
|
||||
);
|
||||
@@ -261,13 +264,15 @@ const rescalePointsInElement = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
width: number,
|
||||
height: number,
|
||||
normalizePoints: boolean,
|
||||
) =>
|
||||
isLinearElement(element) || isFreeDrawElement(element)
|
||||
? {
|
||||
points: rescalePoints(
|
||||
0,
|
||||
width,
|
||||
rescalePoints(1, height, element.points),
|
||||
rescalePoints(1, height, element.points, normalizePoints),
|
||||
normalizePoints,
|
||||
),
|
||||
}
|
||||
: {};
|
||||
@@ -371,6 +376,7 @@ const resizeSingleTextElement = (
|
||||
element,
|
||||
nextWidth,
|
||||
nextHeight,
|
||||
false,
|
||||
);
|
||||
const deltaX1 = (x1 - nextX1) / 2;
|
||||
const deltaY1 = (y1 - nextY1) / 2;
|
||||
@@ -412,6 +418,7 @@ export const resizeSingleElement = (
|
||||
stateAtResizeStart,
|
||||
stateAtResizeStart.width,
|
||||
stateAtResizeStart.height,
|
||||
true,
|
||||
);
|
||||
const startTopLeft: Point = [x1, y1];
|
||||
const startBottomRight: Point = [x2, y2];
|
||||
@@ -429,6 +436,7 @@ export const resizeSingleElement = (
|
||||
element,
|
||||
element.width,
|
||||
element.height,
|
||||
true,
|
||||
);
|
||||
|
||||
const boundsCurrentWidth = esx2 - esx1;
|
||||
@@ -522,6 +530,7 @@ export const resizeSingleElement = (
|
||||
stateAtResizeStart,
|
||||
eleNewWidth,
|
||||
eleNewHeight,
|
||||
true,
|
||||
);
|
||||
const newBoundsWidth = newBoundsX2 - newBoundsX1;
|
||||
const newBoundsHeight = newBoundsY2 - newBoundsY1;
|
||||
@@ -592,6 +601,7 @@ export const resizeSingleElement = (
|
||||
stateAtResizeStart,
|
||||
eleNewWidth,
|
||||
eleNewHeight,
|
||||
true,
|
||||
);
|
||||
// For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
|
||||
// So we need to readjust (x,y) to be where the first point should be
|
||||
@@ -637,146 +647,147 @@ export const resizeSingleElement = (
|
||||
};
|
||||
|
||||
const resizeMultipleElements = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
pointerDownState: PointerDownState,
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||
transformHandleType: "nw" | "ne" | "sw" | "se",
|
||||
shouldResizeFromCenter: boolean,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
) => {
|
||||
const [x1, y1, x2, y2] = getCommonBounds(elements);
|
||||
let scale: number;
|
||||
let getNextXY: (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
origCoords: readonly [number, number, number, number],
|
||||
finalCoords: readonly [number, number, number, number],
|
||||
) => { x: number; y: number };
|
||||
switch (transformHandleType) {
|
||||
case "se":
|
||||
scale = Math.max(
|
||||
(pointerX - x1) / (x2 - x1),
|
||||
(pointerY - y1) / (y2 - y1),
|
||||
);
|
||||
getNextXY = (element, [origX1, origY1], [finalX1, finalY1]) => {
|
||||
const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
|
||||
const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
|
||||
return { x, y };
|
||||
};
|
||||
break;
|
||||
case "nw":
|
||||
scale = Math.max(
|
||||
(x2 - pointerX) / (x2 - x1),
|
||||
(y2 - pointerY) / (y2 - y1),
|
||||
);
|
||||
getNextXY = (element, [, , origX2, origY2], [, , finalX2, finalY2]) => {
|
||||
const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
|
||||
const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
|
||||
return { x, y };
|
||||
};
|
||||
break;
|
||||
case "ne":
|
||||
scale = Math.max(
|
||||
(pointerX - x1) / (x2 - x1),
|
||||
(y2 - pointerY) / (y2 - y1),
|
||||
);
|
||||
getNextXY = (element, [origX1, , , origY2], [finalX1, , , finalY2]) => {
|
||||
const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
|
||||
const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
|
||||
return { x, y };
|
||||
};
|
||||
break;
|
||||
case "sw":
|
||||
scale = Math.max(
|
||||
(x2 - pointerX) / (x2 - x1),
|
||||
(pointerY - y1) / (y2 - y1),
|
||||
);
|
||||
getNextXY = (element, [, origY1, origX2], [, finalY1, finalX2]) => {
|
||||
const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
|
||||
const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
|
||||
return { x, y };
|
||||
};
|
||||
break;
|
||||
// map selected elements to the original elements. While it never should
|
||||
// happen that pointerDownState.originalElements won't contain the selected
|
||||
// elements during resize, this coupling isn't guaranteed, so to ensure
|
||||
// type safety we need to transform only those elements we filter.
|
||||
const targetElements = selectedElements.reduce(
|
||||
(
|
||||
acc: {
|
||||
/** element at resize start */
|
||||
orig: NonDeletedExcalidrawElement;
|
||||
/** latest element */
|
||||
latest: NonDeletedExcalidrawElement;
|
||||
}[],
|
||||
element,
|
||||
) => {
|
||||
const origElement = pointerDownState.originalElements.get(element.id);
|
||||
if (origElement) {
|
||||
acc.push({ orig: origElement, latest: element });
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(
|
||||
targetElements.map(({ orig }) => orig),
|
||||
);
|
||||
const direction = transformHandleType;
|
||||
|
||||
const mapDirectionsToAnchors: Record<typeof direction, Point> = {
|
||||
ne: [minX, maxY],
|
||||
se: [minX, minY],
|
||||
sw: [maxX, minY],
|
||||
nw: [maxX, maxY],
|
||||
};
|
||||
|
||||
// anchor point must be on the opposite side of the dragged selection handle
|
||||
// or be the center of the selection if alt is pressed
|
||||
const [anchorX, anchorY]: Point = shouldResizeFromCenter
|
||||
? [midX, midY]
|
||||
: mapDirectionsToAnchors[direction];
|
||||
|
||||
const mapDirectionsToPointerSides: Record<
|
||||
typeof direction,
|
||||
[x: boolean, y: boolean]
|
||||
> = {
|
||||
ne: [pointerX >= anchorX, pointerY <= anchorY],
|
||||
se: [pointerX >= anchorX, pointerY >= anchorY],
|
||||
sw: [pointerX <= anchorX, pointerY >= anchorY],
|
||||
nw: [pointerX <= anchorX, pointerY <= anchorY],
|
||||
};
|
||||
|
||||
// pointer side relative to anchor
|
||||
const [pointerSideX, pointerSideY] = mapDirectionsToPointerSides[
|
||||
direction
|
||||
].map((condition) => (condition ? 1 : -1));
|
||||
|
||||
// stop resizing if a pointer is on the other side of selection
|
||||
if (pointerSideX < 0 && pointerSideY < 0) {
|
||||
return;
|
||||
}
|
||||
if (scale > 0) {
|
||||
const updates = elements.reduce(
|
||||
(prev, element) => {
|
||||
if (!prev) {
|
||||
return prev;
|
||||
|
||||
const scale =
|
||||
Math.max(
|
||||
(pointerSideX * Math.abs(pointerX - anchorX)) / (maxX - minX),
|
||||
(pointerSideY * Math.abs(pointerY - anchorY)) / (maxY - minY),
|
||||
) * (shouldResizeFromCenter ? 2 : 1);
|
||||
|
||||
if (scale === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
targetElements.forEach((element) => {
|
||||
const width = element.orig.width * scale;
|
||||
const height = element.orig.height * scale;
|
||||
const x = anchorX + (element.orig.x - anchorX) * scale;
|
||||
const y = anchorY + (element.orig.y - anchorY) * scale;
|
||||
|
||||
// readjust points for linear & free draw elements
|
||||
const rescaledPoints = rescalePointsInElement(
|
||||
element.orig,
|
||||
width,
|
||||
height,
|
||||
false,
|
||||
);
|
||||
|
||||
const update: {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
points?: Point[];
|
||||
fontSize?: number;
|
||||
baseline?: number;
|
||||
} = {
|
||||
width,
|
||||
height,
|
||||
x,
|
||||
y,
|
||||
...rescaledPoints,
|
||||
};
|
||||
|
||||
let boundTextUpdates: { fontSize: number; baseline: number } | null = null;
|
||||
|
||||
const boundTextElement = getBoundTextElement(element.latest);
|
||||
|
||||
if (boundTextElement || isTextElement(element.orig)) {
|
||||
const optionalPadding = boundTextElement ? BOUND_TEXT_PADDING * 2 : 0;
|
||||
const textMeasurements = measureFontSizeFromWH(
|
||||
boundTextElement ?? (element.orig as ExcalidrawTextElement),
|
||||
width - optionalPadding,
|
||||
height - optionalPadding,
|
||||
);
|
||||
if (textMeasurements) {
|
||||
if (isTextElement(element.orig)) {
|
||||
update.fontSize = textMeasurements.size;
|
||||
update.baseline = textMeasurements.baseline;
|
||||
}
|
||||
const width = element.width * scale;
|
||||
const height = element.height * scale;
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
let font: { fontSize?: number; baseline?: number } = {};
|
||||
|
||||
if (boundTextElement) {
|
||||
const nextFont = measureFontSizeFromWH(
|
||||
boundTextElement,
|
||||
width - BOUND_TEXT_PADDING * 2,
|
||||
height - BOUND_TEXT_PADDING * 2,
|
||||
);
|
||||
|
||||
if (nextFont === null) {
|
||||
return null;
|
||||
}
|
||||
font = {
|
||||
fontSize: nextFont.size,
|
||||
baseline: nextFont.baseline,
|
||||
boundTextUpdates = {
|
||||
fontSize: textMeasurements.size,
|
||||
baseline: textMeasurements.baseline,
|
||||
};
|
||||
}
|
||||
|
||||
if (isTextElement(element)) {
|
||||
const nextFont = measureFontSizeFromWH(element, width, height);
|
||||
if (nextFont === null) {
|
||||
return null;
|
||||
}
|
||||
font = { fontSize: nextFont.size, baseline: nextFont.baseline };
|
||||
}
|
||||
const origCoords = getElementAbsoluteCoords(element);
|
||||
|
||||
const rescaledPoints = rescalePointsInElement(element, width, height);
|
||||
|
||||
updateBoundElements(element, {
|
||||
newSize: { width, height },
|
||||
simultaneouslyUpdated: elements,
|
||||
});
|
||||
|
||||
const finalCoords = getResizedElementAbsoluteCoords(
|
||||
{
|
||||
...element,
|
||||
...rescaledPoints,
|
||||
},
|
||||
width,
|
||||
height,
|
||||
);
|
||||
|
||||
const { x, y } = getNextXY(element, origCoords, finalCoords);
|
||||
return [...prev, { width, height, x, y, ...rescaledPoints, ...font }];
|
||||
},
|
||||
[] as
|
||||
| {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
points?: (readonly [number, number])[];
|
||||
fontSize?: number;
|
||||
baseline?: number;
|
||||
}[]
|
||||
| null,
|
||||
);
|
||||
if (updates) {
|
||||
elements.forEach((element, index) => {
|
||||
mutateElement(element, updates[index]);
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
|
||||
if (boundTextElement) {
|
||||
mutateElement(boundTextElement, {
|
||||
fontSize: updates[index].fontSize,
|
||||
baseline: updates[index].baseline,
|
||||
});
|
||||
handleBindTextResize(element, transformHandleType);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutateElement(element.latest, update);
|
||||
|
||||
if (boundTextElement && boundTextUpdates) {
|
||||
mutateElement(boundTextElement, boundTextUpdates);
|
||||
handleBindTextResize(element.latest, transformHandleType);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const rotateMultipleElements = (
|
||||
|
@@ -1,49 +1,51 @@
|
||||
import { getPerfectElementSize } from "./sizeHelpers";
|
||||
import * as constants from "../constants";
|
||||
|
||||
const EPSILON_DIGITS = 3;
|
||||
|
||||
describe("getPerfectElementSize", () => {
|
||||
it("should return height:0 if `elementType` is line and locked angle is 0", () => {
|
||||
const { height, width } = getPerfectElementSize("line", 149, 10);
|
||||
expect(width).toEqual(149);
|
||||
expect(height).toEqual(0);
|
||||
expect(width).toBeCloseTo(149, EPSILON_DIGITS);
|
||||
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
|
||||
});
|
||||
it("should return width:0 if `elementType` is line and locked angle is 90 deg (Math.PI/2)", () => {
|
||||
const { height, width } = getPerfectElementSize("line", 10, 140);
|
||||
expect(width).toEqual(0);
|
||||
expect(height).toEqual(140);
|
||||
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
||||
expect(height).toBeCloseTo(140, EPSILON_DIGITS);
|
||||
});
|
||||
it("should return height:0 if `elementType` is arrow and locked angle is 0", () => {
|
||||
const { height, width } = getPerfectElementSize("arrow", 200, 20);
|
||||
expect(width).toEqual(200);
|
||||
expect(height).toEqual(0);
|
||||
expect(width).toBeCloseTo(200, EPSILON_DIGITS);
|
||||
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
|
||||
});
|
||||
it("should return width:0 if `elementType` is arrow and locked angle is 90 deg (Math.PI/2)", () => {
|
||||
const { height, width } = getPerfectElementSize("arrow", 10, 100);
|
||||
expect(width).toEqual(0);
|
||||
expect(height).toEqual(100);
|
||||
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
||||
expect(height).toBeCloseTo(100, EPSILON_DIGITS);
|
||||
});
|
||||
it("should return adjust height to be width * tan(locked angle)", () => {
|
||||
const { height, width } = getPerfectElementSize("arrow", 120, 185);
|
||||
expect(width).toEqual(120);
|
||||
expect(height).toEqual(208);
|
||||
expect(width).toBeCloseTo(120, EPSILON_DIGITS);
|
||||
expect(height).toBeCloseTo(207.846, EPSILON_DIGITS);
|
||||
});
|
||||
it("should return height equals to width if locked angle is 45 deg", () => {
|
||||
const { height, width } = getPerfectElementSize("arrow", 135, 145);
|
||||
expect(width).toEqual(135);
|
||||
expect(height).toEqual(135);
|
||||
expect(width).toBeCloseTo(135, EPSILON_DIGITS);
|
||||
expect(height).toBeCloseTo(135, EPSILON_DIGITS);
|
||||
});
|
||||
it("should return height:0 and width:0 when width and height are 0", () => {
|
||||
const { height, width } = getPerfectElementSize("arrow", 0, 0);
|
||||
expect(width).toEqual(0);
|
||||
expect(height).toEqual(0);
|
||||
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
||||
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
|
||||
});
|
||||
|
||||
describe("should respond to SHIFT_LOCKING_ANGLE constant", () => {
|
||||
it("should have only 2 locking angles per section if SHIFT_LOCKING_ANGLE = 45 deg (Math.PI/4)", () => {
|
||||
(constants as any).SHIFT_LOCKING_ANGLE = Math.PI / 4;
|
||||
const { height, width } = getPerfectElementSize("arrow", 120, 185);
|
||||
expect(width).toEqual(120);
|
||||
expect(height).toEqual(120);
|
||||
expect(width).toBeCloseTo(120, EPSILON_DIGITS);
|
||||
expect(height).toBeCloseTo(120, EPSILON_DIGITS);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -37,9 +37,7 @@ export const getPerfectElementSize = (
|
||||
} else if (lockedAngle === Math.PI / 2) {
|
||||
width = 0;
|
||||
} else {
|
||||
height =
|
||||
Math.round(absWidth * Math.tan(lockedAngle)) * Math.sign(height) ||
|
||||
height;
|
||||
height = absWidth * Math.tan(lockedAngle) * Math.sign(height) || height;
|
||||
}
|
||||
} else if (elementType !== "selection") {
|
||||
height = absWidth * Math.sign(height);
|
||||
@@ -47,6 +45,46 @@ export const getPerfectElementSize = (
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
export const getLockedLinearCursorAlignSize = (
|
||||
originX: number,
|
||||
originY: number,
|
||||
x: number,
|
||||
y: number,
|
||||
) => {
|
||||
let width = x - originX;
|
||||
let height = y - originY;
|
||||
|
||||
const lockedAngle =
|
||||
Math.round(Math.atan(height / width) / SHIFT_LOCKING_ANGLE) *
|
||||
SHIFT_LOCKING_ANGLE;
|
||||
|
||||
if (lockedAngle === 0) {
|
||||
height = 0;
|
||||
} else if (lockedAngle === Math.PI / 2) {
|
||||
width = 0;
|
||||
} else {
|
||||
// locked angle line, y = mx + b => mx - y + b = 0
|
||||
const a1 = Math.tan(lockedAngle);
|
||||
const b1 = -1;
|
||||
const c1 = originY - a1 * originX;
|
||||
|
||||
// line through cursor, perpendicular to locked angle line
|
||||
const a2 = -1 / a1;
|
||||
const b2 = -1;
|
||||
const c2 = y - a2 * x;
|
||||
|
||||
// intersection of the two lines above
|
||||
const intersectX = (b1 * c2 - b2 * c1) / (a1 * b2 - a2 * b1);
|
||||
const intersectY = (c1 * a2 - c2 * a1) / (a1 * b2 - a2 * b1);
|
||||
|
||||
// delta
|
||||
width = intersectX - originX;
|
||||
height = intersectY - originY;
|
||||
}
|
||||
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
export const resizePerfectLineForNWHandler = (
|
||||
element: ExcalidrawElement,
|
||||
x: number,
|
||||
|
@@ -1,9 +1,15 @@
|
||||
import { ExcalidrawElement, PointerType } from "./types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
PointerType,
|
||||
} from "./types";
|
||||
|
||||
import { getElementAbsoluteCoords, Bounds } from "./bounds";
|
||||
import { rotate } from "../math";
|
||||
import { Zoom } from "../types";
|
||||
import { AppState, Zoom } from "../types";
|
||||
import { isTextElement } from ".";
|
||||
import { isLinearElement } from "./typeChecks";
|
||||
import { DEFAULT_SPACING } from "../renderer/renderScene";
|
||||
|
||||
export type TransformHandleDirection =
|
||||
| "n"
|
||||
@@ -59,8 +65,6 @@ const OMIT_SIDES_FOR_LINE_BACKSLASH = {
|
||||
s: true,
|
||||
n: true,
|
||||
w: true,
|
||||
ne: true,
|
||||
sw: true,
|
||||
};
|
||||
|
||||
const generateTransformHandle = (
|
||||
@@ -82,6 +86,7 @@ export const getTransformHandlesFromCoords = (
|
||||
zoom: Zoom,
|
||||
pointerType: PointerType,
|
||||
omitSides: { [T in TransformHandleType]?: boolean } = {},
|
||||
margin = 4,
|
||||
): TransformHandles => {
|
||||
const size = transformHandleSizes[pointerType];
|
||||
const handleWidth = size / zoom.value;
|
||||
@@ -94,9 +99,7 @@ export const getTransformHandlesFromCoords = (
|
||||
const height = y2 - y1;
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
|
||||
const dashedLineMargin = 4 / zoom.value;
|
||||
|
||||
const dashedLineMargin = margin / zoom.value;
|
||||
const centeringOffset = (size - 8) / (2 * zoom.value);
|
||||
|
||||
const transformHandles: TransformHandles = {
|
||||
@@ -230,11 +233,7 @@ export const getTransformHandles = (
|
||||
}
|
||||
|
||||
let omitSides: { [T in TransformHandleType]?: boolean } = {};
|
||||
if (
|
||||
element.type === "arrow" ||
|
||||
element.type === "line" ||
|
||||
element.type === "freedraw"
|
||||
) {
|
||||
if (element.type === "freedraw" || isLinearElement(element)) {
|
||||
if (element.points.length === 2) {
|
||||
// only check the last point because starting point is always (0,0)
|
||||
const [, p1] = element.points;
|
||||
@@ -253,12 +252,33 @@ export const getTransformHandles = (
|
||||
} else if (isTextElement(element)) {
|
||||
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
|
||||
}
|
||||
|
||||
const dashedLineMargin = isLinearElement(element)
|
||||
? DEFAULT_SPACING * 3
|
||||
: DEFAULT_SPACING;
|
||||
return getTransformHandlesFromCoords(
|
||||
getElementAbsoluteCoords(element),
|
||||
element.angle,
|
||||
zoom,
|
||||
pointerType,
|
||||
omitSides,
|
||||
dashedLineMargin,
|
||||
);
|
||||
};
|
||||
|
||||
export const shouldShowBoundingBox = (
|
||||
elements: NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => {
|
||||
if (appState.editingLinearElement) {
|
||||
return false;
|
||||
}
|
||||
if (elements.length > 1) {
|
||||
return true;
|
||||
}
|
||||
const element = elements[0];
|
||||
if (!isLinearElement(element)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return element.points.length > 2;
|
||||
};
|
||||
|
@@ -56,6 +56,7 @@ type _ExcalidrawElementBase = Readonly<{
|
||||
updated: number;
|
||||
link: string | null;
|
||||
locked: boolean;
|
||||
customData?: Record<string, any>;
|
||||
}>;
|
||||
|
||||
export type ExcalidrawSelectionElement = _ExcalidrawElementBase & {
|
||||
|
@@ -19,7 +19,7 @@ const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
|
||||
}, STORAGE_SIZE_TIMEOUT);
|
||||
|
||||
type Props = {
|
||||
setToastMessage: (message: string) => void;
|
||||
setToast: (message: string) => void;
|
||||
};
|
||||
const CustomStats = (props: Props) => {
|
||||
const [storageSizes, setStorageSizes] = useState<StorageSizes>({
|
||||
@@ -68,7 +68,7 @@ const CustomStats = (props: Props) => {
|
||||
onClick={async () => {
|
||||
try {
|
||||
await copyTextToSystemClipboard(getVersion());
|
||||
props.setToastMessage(t("toast.copyToClipboard"));
|
||||
props.setToast(t("toast.copyToClipboard"));
|
||||
} catch {}
|
||||
}}
|
||||
title={t("stats.versionCopy")}
|
||||
|
@@ -169,7 +169,8 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
|
||||
if (
|
||||
process.env.NODE_ENV === ENV.TEST ||
|
||||
process.env.NODE_ENV === ENV.DEVELOPMENT
|
||||
process.env.NODE_ENV === ENV.DEVELOPMENT ||
|
||||
process.env.REACT_APP_VERCEL_ENV === "preview"
|
||||
) {
|
||||
window.collab = window.collab || ({} as Window["collab"]);
|
||||
Object.defineProperties(window, {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import polyfill from "../polyfill";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { trackEvent } from "../analytics";
|
||||
@@ -83,6 +84,9 @@ import { jotaiStore, useAtomWithInitialValue } from "../jotai";
|
||||
import { reconcileElements } from "./collab/reconciliation";
|
||||
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
||||
|
||||
polyfill();
|
||||
window.EXCALIDRAW_THROTTLE_RENDER = true;
|
||||
|
||||
const isExcalidrawPlusSignedUser = document.cookie.includes(
|
||||
COOKIES.AUTH_STATE_COOKIE,
|
||||
);
|
||||
@@ -94,6 +98,7 @@ languageDetector.init({
|
||||
|
||||
const initializeScene = async (opts: {
|
||||
collabAPI: CollabAPI;
|
||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||
}): Promise<
|
||||
{ scene: ExcalidrawInitialDataState | null } & (
|
||||
| { isExternalScene: true; id: string; key: string }
|
||||
@@ -178,8 +183,28 @@ const initializeScene = async (opts: {
|
||||
}
|
||||
|
||||
if (roomLinkData) {
|
||||
const { excalidrawAPI } = opts;
|
||||
|
||||
const scene = await opts.collabAPI.startCollaboration(roomLinkData);
|
||||
|
||||
return {
|
||||
scene: await opts.collabAPI.startCollaboration(roomLinkData),
|
||||
// when collaborating, the state may have already been updated at this
|
||||
// point (we may have received updates from other clients), so reconcile
|
||||
// elements and appState with existing state
|
||||
scene: {
|
||||
...scene,
|
||||
appState: {
|
||||
...restoreAppState(scene?.appState, excalidrawAPI.getAppState()),
|
||||
// necessary if we're invoking from a hashchange handler which doesn't
|
||||
// go through App.initializeScene() that resets this flag
|
||||
isLoading: false,
|
||||
},
|
||||
elements: reconcileElements(
|
||||
scene?.elements || [],
|
||||
excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||
excalidrawAPI.getAppState(),
|
||||
),
|
||||
},
|
||||
isExternalScene: true,
|
||||
id: roomLinkData.roomId,
|
||||
key: roomLinkData.roomKey,
|
||||
@@ -333,23 +358,9 @@ const ExcalidrawWrapper = () => {
|
||||
}
|
||||
};
|
||||
|
||||
initializeScene({ collabAPI }).then(async (data) => {
|
||||
initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => {
|
||||
loadImages(data, /* isInitialLoad */ true);
|
||||
|
||||
initialStatePromiseRef.current.promise.resolve({
|
||||
...data.scene,
|
||||
// at this point the state may have already been updated (e.g. when
|
||||
// collaborating, we may have received updates from other clients)
|
||||
appState: restoreAppState(
|
||||
data.scene?.appState,
|
||||
excalidrawAPI.getAppState(),
|
||||
),
|
||||
elements: reconcileElements(
|
||||
data.scene?.elements || [],
|
||||
excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||
excalidrawAPI.getAppState(),
|
||||
),
|
||||
});
|
||||
initialStatePromiseRef.current.promise.resolve(data.scene);
|
||||
});
|
||||
|
||||
const onHashChange = async (event: HashChangeEvent) => {
|
||||
@@ -364,7 +375,7 @@ const ExcalidrawWrapper = () => {
|
||||
}
|
||||
excalidrawAPI.updateScene({ appState: { isLoading: true } });
|
||||
|
||||
initializeScene({ collabAPI }).then((data) => {
|
||||
initializeScene({ collabAPI, excalidrawAPI }).then((data) => {
|
||||
loadImages(data);
|
||||
if (data.scene) {
|
||||
excalidrawAPI.updateScene({
|
||||
@@ -658,7 +669,7 @@ const ExcalidrawWrapper = () => {
|
||||
const renderCustomStats = () => {
|
||||
return (
|
||||
<CustomStats
|
||||
setToastMessage={(message) => excalidrawAPI!.setToastMessage(message)}
|
||||
setToast={(message) => excalidrawAPI!.setToast({ message })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
4
src/global.d.ts
vendored
4
src/global.d.ts
vendored
@@ -14,7 +14,11 @@ interface Window {
|
||||
__EXCALIDRAW_SHA__: string | undefined;
|
||||
EXCALIDRAW_ASSET_PATH: string | undefined;
|
||||
EXCALIDRAW_EXPORT_SOURCE: string;
|
||||
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
|
||||
gtag: Function;
|
||||
logTime: (name: string, time?: number) => void;
|
||||
logTimeAverage: (name: string, time?: number) => void;
|
||||
DEBUG_LOG_TIMES: boolean;
|
||||
}
|
||||
|
||||
// https://github.com/facebook/create-react-app/blob/ddcb7d5/packages/react-scripts/lib/react-app.d.ts
|
||||
|
@@ -48,10 +48,13 @@ const allLanguages: Language[] = [
|
||||
{ code: "ru-RU", label: "Русский" },
|
||||
{ code: "sk-SK", label: "Slovenčina" },
|
||||
{ code: "sv-SE", label: "Svenska" },
|
||||
{ code: "sl-SI", label: "Slovenščina" },
|
||||
{ code: "tr-TR", label: "Türkçe" },
|
||||
{ code: "uk-UA", label: "Українська" },
|
||||
{ code: "zh-CN", label: "简体中文" },
|
||||
{ code: "zh-TW", label: "繁體中文" },
|
||||
{ code: "vi-VN", label: "Tiếng Việt" },
|
||||
{ code: "mr-IN", label: "मराठी" },
|
||||
].concat([defaultLang]);
|
||||
|
||||
export const languages: Language[] = allLanguages
|
||||
|
@@ -1,8 +1,14 @@
|
||||
import ReactDOM from "react-dom";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import ExcalidrawApp from "./excalidraw-app";
|
||||
|
||||
import "./excalidraw-app/pwa";
|
||||
import "./excalidraw-app/sentry";
|
||||
window.__EXCALIDRAW_SHA__ = process.env.REACT_APP_GIT_SHA;
|
||||
|
||||
ReactDOM.render(<ExcalidrawApp />, document.getElementById("root"));
|
||||
const rootElement = document.getElementById("root")!;
|
||||
const root = createRoot(rootElement);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<ExcalidrawApp />
|
||||
</StrictMode>,
|
||||
);
|
||||
|
@@ -80,5 +80,5 @@ export const shouldMaintainAspectRatio = (event: MouseEvent | KeyboardEvent) =>
|
||||
event.shiftKey;
|
||||
|
||||
export const shouldRotateWithDiscreteAngle = (
|
||||
event: MouseEvent | KeyboardEvent,
|
||||
event: MouseEvent | KeyboardEvent | React.PointerEvent<HTMLCanvasElement>,
|
||||
) => event.shiftKey;
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "إعادة تعيين اللوحة",
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Нулиране на платно",
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "",
|
||||
|
@@ -9,7 +9,7 @@
|
||||
"copy": "Copia",
|
||||
"copyAsPng": "Copia al porta-retalls com a PNG",
|
||||
"copyAsSvg": "Copia al porta-retalls com a SVG",
|
||||
"copyText": "",
|
||||
"copyText": "Copia al porta-retalls com a text",
|
||||
"bringForward": "Porta endavant",
|
||||
"sendToBack": "Envia enrere",
|
||||
"bringToFront": "Porta al davant",
|
||||
@@ -108,19 +108,25 @@
|
||||
"decreaseFontSize": "Redueix la mida de la lletra",
|
||||
"increaseFontSize": "Augmenta la mida de la lletra",
|
||||
"unbindText": "Desvincular el text",
|
||||
"bindText": "",
|
||||
"bindText": "Ajusta el text al contenidor",
|
||||
"link": {
|
||||
"edit": "Edita l'enllaç",
|
||||
"create": "Crea un enllaç",
|
||||
"label": "Enllaç"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "",
|
||||
"unlock": "",
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
"lock": "Bloca",
|
||||
"unlock": "Desbloca",
|
||||
"lockAll": "Bloca-ho tot",
|
||||
"unlockAll": "Desbloca-ho tot"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "Publicat",
|
||||
"sidebarLock": "Manté la barra lateral oberta"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Encara no s'hi han afegit elements...",
|
||||
"hint_emptyLibrary": "Trieu un element o un llenç per a afegir-lo aquí, o instal·leu una biblioteca del repositori públic, més avall.",
|
||||
"hint_emptyPrivateLibrary": "Trieu un element o un llenç per a afegir-lo aquí."
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Neteja el llenç",
|
||||
@@ -168,7 +174,7 @@
|
||||
"couldNotLoadInvalidFile": "No s'ha pogut carregar un fitxer no vàlid",
|
||||
"importBackendFailed": "Importació fallida.",
|
||||
"cannotExportEmptyCanvas": "No es pot exportar un llenç buit.",
|
||||
"couldNotCopyToClipboard": "",
|
||||
"couldNotCopyToClipboard": "No s'ha pogut copiar al porta-retalls.",
|
||||
"decryptFailed": "No s'ha pogut desencriptar.",
|
||||
"uploadedSecurly": "La càrrega s'ha assegurat amb xifratge punta a punta, cosa que significa que el servidor Excalidraw i tercers no poden llegir el contingut.",
|
||||
"loadSceneOverridePrompt": "Si carregas aquest dibuix extern, substituirá el que tens. Vols continuar?",
|
||||
@@ -189,8 +195,8 @@
|
||||
"fileTooBig": "El fitxer és massa gros. La mida màxima permesa és {{maxSize}}.",
|
||||
"svgImageInsertError": "No ha estat possible inserir la imatge SVG. Les marques SVG semblen invàlides.",
|
||||
"invalidSVGString": "SVG no vàlid.",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": ""
|
||||
"cannotResolveCollabServer": "No ha estat possible connectar amb el servidor collab. Si us plau recarregueu la pàgina i torneu a provar.",
|
||||
"importLibraryError": "No s'ha pogut carregar la biblioteca"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Selecció",
|
||||
@@ -206,7 +212,7 @@
|
||||
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar",
|
||||
"penMode": "Evita el zoom i accepta solament el dibuix lliure amb bolígraf",
|
||||
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
|
||||
"eraser": ""
|
||||
"eraser": "Esborrador"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Accions del llenç",
|
||||
@@ -232,7 +238,7 @@
|
||||
"publishLibrary": "Publiqueu la vostra pròpia llibreria",
|
||||
"bindTextToElement": "Premeu enter per a afegir-hi text",
|
||||
"deepBoxSelect": "Manteniu CtrlOrCmd per a selecció profunda, i per a evitar l'arrossegament",
|
||||
"eraserRevert": ""
|
||||
"eraserRevert": "Mantingueu premuda Alt per a revertir els elements seleccionats per a esborrar"
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "No es pot mostrar la previsualització",
|
||||
@@ -292,7 +298,7 @@
|
||||
"howto": "Seguiu les nostres guies",
|
||||
"or": "o",
|
||||
"preventBinding": "Prevenir vinculació de la fletxa",
|
||||
"tools": "",
|
||||
"tools": "Eines",
|
||||
"shortcuts": "Dreceres de teclat",
|
||||
"textFinish": "Finalitza l'edició (editor de text)",
|
||||
"textNewLine": "Afegeix una línia nova (editor de text)",
|
||||
@@ -300,7 +306,7 @@
|
||||
"view": "Visualització",
|
||||
"zoomToFit": "Zoom per veure tots els elements",
|
||||
"zoomToSelection": "Zoom per veure la selecció",
|
||||
"toggleElementLock": ""
|
||||
"toggleElementLock": "Blocar/desblocar la selecció"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": "Neteja el llenç"
|
||||
@@ -318,7 +324,7 @@
|
||||
"authorName": "Nom o usuari",
|
||||
"libraryName": "Nom de la vostra biblioteca",
|
||||
"libraryDesc": "Descripció de la biblioteca per a ajudar a la gent a entendre'n el funcionament",
|
||||
"githubHandle": "",
|
||||
"githubHandle": "Identificador de GitHub (opcional), per tal que pugueu editar la biblioteca una vegada enviada per a ser revisada",
|
||||
"twitterHandle": "Usuari de twitter (opcional), per tal que puguem donar-vos crèdit quan fem la promoció a Twitter",
|
||||
"website": "Enllaç al vostre lloc web personal o a qualsevol altre (opcional)"
|
||||
},
|
||||
@@ -343,7 +349,7 @@
|
||||
},
|
||||
"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",
|
||||
"republishWarning": ""
|
||||
"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": {
|
||||
"title": "Biblioteca enviada",
|
||||
|
@@ -9,7 +9,7 @@
|
||||
"copy": "Kopírovat",
|
||||
"copyAsPng": "Zkopírovat do schránky jako PNG",
|
||||
"copyAsSvg": "Zkopírovat do schránky jako SVG",
|
||||
"copyText": "",
|
||||
"copyText": "Zkopírovat do schránky jako text",
|
||||
"bringForward": "Přenést blíž",
|
||||
"sendToBack": "Přenést do pozadí",
|
||||
"bringToFront": "Přenést do popředí",
|
||||
@@ -36,14 +36,14 @@
|
||||
"arrowhead_arrow": "Šipka",
|
||||
"arrowhead_bar": "Kóta",
|
||||
"arrowhead_dot": "Tečka",
|
||||
"arrowhead_triangle": "",
|
||||
"arrowhead_triangle": "Trojúhelník",
|
||||
"fontSize": "Velikost písma",
|
||||
"fontFamily": "Písmo",
|
||||
"onlySelected": "Pouze vybrané",
|
||||
"withBackground": "",
|
||||
"exportEmbedScene": "",
|
||||
"exportEmbedScene_details": "",
|
||||
"addWatermark": "",
|
||||
"withBackground": "Pozadí",
|
||||
"exportEmbedScene": "Vložit scénu",
|
||||
"exportEmbedScene_details": "Data scény budou uložena do exportovaného souboru PNG/SVG tak, aby z něj mohla být scéna obnovena.\nZvýší se velikost exportovaného souboru.",
|
||||
"addWatermark": "Přidat \"Vyrobeno s Excalidraw\"",
|
||||
"handDrawn": "Od ruky",
|
||||
"normal": "Normální",
|
||||
"code": "Kód",
|
||||
@@ -51,10 +51,10 @@
|
||||
"medium": "Střední",
|
||||
"large": "Velké",
|
||||
"veryLarge": "Velmi velké",
|
||||
"solid": "",
|
||||
"solid": "Plný",
|
||||
"hachure": "",
|
||||
"crossHatch": "",
|
||||
"thin": "",
|
||||
"thin": "Tenký",
|
||||
"bold": "",
|
||||
"left": "",
|
||||
"center": "",
|
||||
@@ -68,7 +68,7 @@
|
||||
"canvasColors": "",
|
||||
"canvasBackground": "Pozadí plátna",
|
||||
"drawingCanvas": "",
|
||||
"layers": "",
|
||||
"layers": "Vrstvy",
|
||||
"actions": "",
|
||||
"language": "",
|
||||
"liveCollaboration": "",
|
||||
@@ -87,16 +87,16 @@
|
||||
"libraries": "",
|
||||
"loadingScene": "",
|
||||
"align": "",
|
||||
"alignTop": "",
|
||||
"alignBottom": "",
|
||||
"alignLeft": "",
|
||||
"alignRight": "",
|
||||
"alignTop": "Zarovnat nahoru",
|
||||
"alignBottom": "Zarovnat dolů",
|
||||
"alignLeft": "Zarovnat vlevo",
|
||||
"alignRight": "Zarovnejte vpravo",
|
||||
"centerVertically": "",
|
||||
"centerHorizontally": "",
|
||||
"distributeHorizontally": "",
|
||||
"distributeVertically": "",
|
||||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"flipHorizontal": "Převrátit vodorovně",
|
||||
"flipVertical": "Převrátit svisle",
|
||||
"viewMode": "Náhled",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": "Sdílet",
|
||||
@@ -120,7 +120,13 @@
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "",
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "",
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "Alle sperren",
|
||||
"unlockAll": "Alle entsperren"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "Veröffentlicht",
|
||||
"sidebarLock": "Seitenleiste offen lassen"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Noch keine Elemente hinzugefügt...",
|
||||
"hint_emptyLibrary": "Wähle ein Element auf der Zeichenfläche, um es hier hinzuzufügen. Oder installiere eine Bibliothek aus dem öffentlichen Verzeichnis.",
|
||||
"hint_emptyPrivateLibrary": "Wähle ein Element von der Zeichenfläche, um es hier hinzuzufügen."
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen",
|
||||
@@ -343,7 +349,7 @@
|
||||
},
|
||||
"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",
|
||||
"republishWarning": ""
|
||||
"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": {
|
||||
"title": "Bibliothek übermittelt",
|
||||
|
@@ -9,7 +9,7 @@
|
||||
"copy": "Αντιγραφή",
|
||||
"copyAsPng": "Αντιγραφή στο πρόχειρο ως PNG",
|
||||
"copyAsSvg": "Αντιγραφή στο πρόχειρο ως SVG",
|
||||
"copyText": "",
|
||||
"copyText": "Αντιγραφή στο πρόχειρο ως κείμενο",
|
||||
"bringForward": "Στο προσκήνιο",
|
||||
"sendToBack": "Ένα επίπεδο πίσω",
|
||||
"bringToFront": "Ένα επίπεδο μπροστά",
|
||||
@@ -41,7 +41,7 @@
|
||||
"fontFamily": "Γραμματοσειρά",
|
||||
"onlySelected": "Μόνο τα Επιλεγμένα",
|
||||
"withBackground": "Φόντο",
|
||||
"exportEmbedScene": "",
|
||||
"exportEmbedScene": "Ενσωμάτωση σκηνής",
|
||||
"exportEmbedScene_details": "Τα δεδομένα σκηνής θα αποθηκευτούν στο αρχείο PNG/SVG προς εξαγωγή ώστε η σκηνή να είναι δυνατό να αποκατασταθεί από αυτό.\nΘα αυξήσει το μέγεθος του αρχείου προς εξαγωγή.",
|
||||
"addWatermark": "Προσθήκη \"Φτιαγμένο με Excalidraw\"",
|
||||
"handDrawn": "Σχεδιασμένο στο χέρι",
|
||||
@@ -65,7 +65,7 @@
|
||||
"cartoonist": "Σκιτσογράφος",
|
||||
"fileTitle": "Όνομα αρχείου",
|
||||
"colorPicker": "Επιλογή Χρώματος",
|
||||
"canvasColors": "",
|
||||
"canvasColors": "Χρησιμοποείται στον καμβά",
|
||||
"canvasBackground": "Φόντο καμβά",
|
||||
"drawingCanvas": "Σχεδίαση καμβά",
|
||||
"layers": "Στρώματα",
|
||||
@@ -105,22 +105,28 @@
|
||||
"toggleTheme": "Εναλλαγή θέματος",
|
||||
"personalLib": "Προσωπική Βιβλιοθήκη",
|
||||
"excalidrawLib": "Βιβλιοθήκη Excalidraw",
|
||||
"decreaseFontSize": "",
|
||||
"increaseFontSize": "",
|
||||
"unbindText": "",
|
||||
"bindText": "",
|
||||
"decreaseFontSize": "Μείωση μεγέθους γραμματοσειράς",
|
||||
"increaseFontSize": "Αύξηση μεγέθους γραμματοσειράς",
|
||||
"unbindText": "Αποσύνδεση κειμένου",
|
||||
"bindText": "Δέσμευση κειμένου στο δοχείο",
|
||||
"link": {
|
||||
"edit": "",
|
||||
"create": "",
|
||||
"label": ""
|
||||
"edit": "Επεξεργασία συνδέσμου",
|
||||
"create": "Δημιουργία συνδέσμου",
|
||||
"label": "Σύνδεσμος"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "",
|
||||
"unlock": "",
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
"lock": "Κλείδωμα",
|
||||
"unlock": "Ξεκλείδωμα",
|
||||
"lockAll": "Κλείδωμα όλων",
|
||||
"unlockAll": "Ξεκλείδωμα όλων"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "Δημοσιευμένο",
|
||||
"sidebarLock": "Κρατήστε την πλαϊνή μπάρα ανοιχτή"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Δεν έχουν προστεθεί αντικείμενα ακόμη...",
|
||||
"hint_emptyLibrary": "Επιλέξτε ένα στοιχείο στον καμβά για να το προσθέσετε εδώ, ή εγκαταστήστε μια βιβλιοθήκη από το δημόσιο αποθετήριο, παρακάτω.",
|
||||
"hint_emptyPrivateLibrary": "Επιλέξτε ένα στοιχείο στον καμβά για να το προσθέσετε εδώ."
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Επαναφορά του καμβά",
|
||||
@@ -168,7 +174,7 @@
|
||||
"couldNotLoadInvalidFile": "Δεν μπόρεσε να ανοίξει εσφαλμένο αρχείο",
|
||||
"importBackendFailed": "Η εισαγωγή από το backend απέτυχε.",
|
||||
"cannotExportEmptyCanvas": "Δεν είναι δυνατή η εξαγωγή κενού καμβά.",
|
||||
"couldNotCopyToClipboard": "",
|
||||
"couldNotCopyToClipboard": "Αδυναμία αντιγραφής στο πρόχειρο.",
|
||||
"decryptFailed": "Δεν ήταν δυνατή η αποκρυπτογράφηση δεδομένων.",
|
||||
"uploadedSecurly": "Η μεταφόρτωση έχει εξασφαλιστεί με κρυπτογράφηση από άκρο σε άκρο, πράγμα που σημαίνει ότι ο διακομιστής Excalidraw και τρίτα μέρη δεν μπορούν να διαβάσουν το περιεχόμενο.",
|
||||
"loadSceneOverridePrompt": "Η φόρτωση εξωτερικού σχεδίου θα αντικαταστήσει το υπάρχον περιεχόμενο. Επιθυμείτε να συνεχίσετε;",
|
||||
@@ -176,21 +182,21 @@
|
||||
"errorAddingToLibrary": "Αδυναμία προσθήκης αντικειμένου στη βιβλιοθήκη",
|
||||
"errorRemovingFromLibrary": "Αδυναμία αφαίρεσης αντικειμένου από τη βιβλιοθήκη",
|
||||
"confirmAddLibrary": "Αυτό θα προσθέσει {{numShapes}} σχήμα(τα) στη βιβλιοθήκη σας. Είστε σίγουροι;",
|
||||
"imageDoesNotContainScene": "",
|
||||
"imageDoesNotContainScene": "Αυτή η εικόνα δεν φαίνεται να περιέχει δεδομένα σκηνής. Έχετε ενεργοποιήσει την ενσωμάτωση σκηνής κατά την εξαγωγή;",
|
||||
"cannotRestoreFromImage": "Η σκηνή δεν ήταν δυνατό να αποκατασταθεί από αυτό το αρχείο εικόνας",
|
||||
"invalidSceneUrl": "",
|
||||
"invalidSceneUrl": "Δεν ήταν δυνατή η εισαγωγή σκηνής από το URL που δώσατε. Είτε έχει λάθος μορφή, είτε δεν περιέχει έγκυρα δεδομένα JSON Excalidraw.",
|
||||
"resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"removeItemsFromsLibrary": "Διαγραφή {{count}} αντικειμένου(ων) από τη βιβλιοθήκη;",
|
||||
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη."
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Μη υποστηριζόμενος τύπος αρχείου.",
|
||||
"imageInsertError": "Αδυναμία εισαγωγής εικόνας. Προσπαθήστε ξανά αργότερα...",
|
||||
"fileTooBig": "Το αρχείο είναι πολύ μεγάλο. Το μέγιστο επιτρεπόμενο μέγεθος είναι {{maxSize}}.",
|
||||
"svgImageInsertError": "",
|
||||
"svgImageInsertError": "Αδυναμία εισαγωγής εικόνας SVG. Η σήμανση της SVG δεν φαίνεται έγκυρη.",
|
||||
"invalidSVGString": "Μη έγκυρο SVG.",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": ""
|
||||
"cannotResolveCollabServer": "Αδυναμία σύνδεσης με τον διακομιστή συνεργασίας. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά.",
|
||||
"importLibraryError": "Αδυναμία φόρτωσης βιβλιοθήκης"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Επιλογή",
|
||||
@@ -205,8 +211,8 @@
|
||||
"library": "Βιβλιοθήκη",
|
||||
"lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο",
|
||||
"penMode": "",
|
||||
"link": "",
|
||||
"eraser": ""
|
||||
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
|
||||
"eraser": "Γόμα"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Ενέργειες καμβά",
|
||||
@@ -223,16 +229,16 @@
|
||||
"linearElementMulti": "Κάνε κλικ στο τελευταίο σημείο ή πάτησε Escape ή Enter για να τελειώσεις",
|
||||
"lockAngle": "Μπορείτε να περιορίσετε τη γωνία κρατώντας πατημένο το SHIFT",
|
||||
"resize": "Μπορείς να περιορίσεις τις αναλογίες κρατώντας το SHIFT ενώ αλλάζεις μέγεθος,\nκράτησε πατημένο το ALT για αλλαγή μεγέθους από το κέντρο",
|
||||
"resizeImage": "",
|
||||
"resizeImage": "Μπορείτε να αλλάξετε το μέγεθος ελεύθερα κρατώντας πατημένο το SHIFT,\nκρατήστε πατημένο το ALT για να αλλάξετε το μέγεθος από το κέντρο",
|
||||
"rotate": "Μπορείς να περιορίσεις τις γωνίες κρατώντας πατημένο το πλήκτρο SHIFT κατά την περιστροφή",
|
||||
"lineEditor_info": "Διπλό-κλικ ή πιέστε Enter για να επεξεργαστείτε τα σημεία",
|
||||
"lineEditor_pointSelected": "",
|
||||
"lineEditor_nothingSelected": "",
|
||||
"placeImage": "",
|
||||
"lineEditor_pointSelected": "Πατήστε Διαγραφή για αφαίρεση σημείου(ων),\nCtrlOrCmd+D για αντιγραφή, ή σύρετε για μετακίνηση",
|
||||
"lineEditor_nothingSelected": "Επιλέξτε ένα σημείο για να επεξεργαστείτε (κρατήστε πατημένο το SHIFT για να επιλέξετε πολλαπλά),\nή κρατήστε πατημένο το Alt και κάντε κλικ για να προσθέσετε νέα σημεία",
|
||||
"placeImage": "Κάντε κλικ για να τοποθετήσετε την εικόνα ή κάντε κλικ και σύρετε για να ορίσετε το μέγεθός της χειροκίνητα",
|
||||
"publishLibrary": "Δημοσιεύστε τη δική σας βιβλιοθήκη",
|
||||
"bindTextToElement": "",
|
||||
"deepBoxSelect": "",
|
||||
"eraserRevert": ""
|
||||
"bindTextToElement": "Πατήστε Enter για προσθήκη κειμένου",
|
||||
"deepBoxSelect": "Κρατήστε πατημένο το CtrlOrCmd για να επιλέξετε βαθιά, και να αποτρέψετε τη μεταφορά",
|
||||
"eraserRevert": "Κρατήστε πατημένο το Alt για να επαναφέρετε τα στοιχεία που σημειώθηκαν για διαγραφή"
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης",
|
||||
@@ -260,39 +266,39 @@
|
||||
"desc_inProgressIntro": "Η ζωντανή συνεργασία με άλλους είναι σε ενεργή.",
|
||||
"desc_shareLink": "Μοιραστείτε τον σύνδεσμο με όποιον θέλετε να δουλέψετε μαζί:",
|
||||
"desc_exitSession": "Η διακοπή θα σας αποσυνδέσει από το δωμάτιο, αλλά θα μπορείτε να συνεχίσετε να δουλεύετε στον πίνακα, τοπικά. Σημειώσατε ότι αυτό δεν θα επηρεάσει τον πίνακα άλλων, και θα μπορούν ακόμα να συνεισφέρουν στην δική τους έκδοση.",
|
||||
"shareTitle": ""
|
||||
"shareTitle": "Συμμετάσχετε σε μια ζωντανή συνεδρία συνεργασίας για το Excalidraw"
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": "Σφάλμα"
|
||||
},
|
||||
"exportDialog": {
|
||||
"disk_title": "Αποθήκευση στο δίσκο",
|
||||
"disk_details": "",
|
||||
"disk_details": "Εξαγωγή δεδομένων σκηνής σε ένα αρχείο από το οποίο μπορείτε να εισάγετε αργότερα.",
|
||||
"disk_button": "Αποθήκευση σε αρχείο",
|
||||
"link_title": "Κοινόχρηστος σύνδεσμος",
|
||||
"link_details": "Εξαγωγή ως σύνδεσμο μόνο για ανάγνωση.",
|
||||
"link_button": "Εξαγωγή σε Σύνδεση",
|
||||
"excalidrawplus_description": "",
|
||||
"excalidrawplus_description": "Αποθηκεύστε τη σκηνή στο χώρο εργασίας σας Excalidraw+.",
|
||||
"excalidrawplus_button": "Εξαγωγή",
|
||||
"excalidrawplus_exportError": ""
|
||||
"excalidrawplus_exportError": "Δεν ήταν δυνατή η εξαγωγή στο Excalidraw+ αυτή τη στιγμή..."
|
||||
},
|
||||
"helpDialog": {
|
||||
"blog": "Διαβάστε το Blog μας",
|
||||
"click": "κλικ",
|
||||
"deepSelect": "",
|
||||
"deepBoxSelect": "",
|
||||
"deepSelect": "Βαθιά επιλογή",
|
||||
"deepBoxSelect": "Βαθιά επιλογή μέσα στο πλαίσιο και αποτροπή συρσίματος",
|
||||
"curvedArrow": "Κυρτό βέλος",
|
||||
"curvedLine": "Κυρτή γραμμή",
|
||||
"documentation": "Εγχειρίδιο",
|
||||
"doubleClick": "διπλό κλικ",
|
||||
"drag": "σύρε",
|
||||
"editor": "Επεξεργαστής",
|
||||
"editSelectedShape": "",
|
||||
"editSelectedShape": "Επεξεργασία επιλεγμένου σχήματος (κείμενο/βέλος/γραμμή)",
|
||||
"github": "Βρήκατε πρόβλημα; Υποβάλετε το",
|
||||
"howto": "Ακολουθήστε τους οδηγούς μας",
|
||||
"or": "ή",
|
||||
"preventBinding": "Αποτροπή δέσμευσης βέλων",
|
||||
"tools": "",
|
||||
"tools": "Εργαλεία",
|
||||
"shortcuts": "Συντομεύσεις πληκτρολογίου",
|
||||
"textFinish": "Ολοκλήρωση επεξεργασίας (επεξεργαστής κειμένου)",
|
||||
"textNewLine": "Προσθήκη νέας γραμμής (επεξεργαστής κειμένου)",
|
||||
@@ -300,54 +306,54 @@
|
||||
"view": "Προβολή",
|
||||
"zoomToFit": "Zoom ώστε να χωρέσουν όλα τα στοιχεία",
|
||||
"zoomToSelection": "Ζουμ στην επιλογή",
|
||||
"toggleElementLock": ""
|
||||
"toggleElementLock": "Κλείδωμα/Ξεκλείδωμα επιλογής"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": "Καθαρισμός καμβά"
|
||||
},
|
||||
"publishDialog": {
|
||||
"title": "",
|
||||
"itemName": "",
|
||||
"title": "Δημοσίευση βιβλιοθήκης",
|
||||
"itemName": "Όνομα αντικειμένου",
|
||||
"authorName": "Όνομα δημιουργού",
|
||||
"githubUsername": "GitHub username",
|
||||
"twitterUsername": "Twitter username",
|
||||
"libraryName": "Όνομα βιβλιοθήκης",
|
||||
"libraryDesc": "",
|
||||
"libraryDesc": "Περιγραφή βιβλιοθήκης",
|
||||
"website": "Ιστοσελίδα",
|
||||
"placeholder": {
|
||||
"authorName": "",
|
||||
"libraryName": "",
|
||||
"libraryDesc": "",
|
||||
"githubHandle": "",
|
||||
"twitterHandle": "",
|
||||
"website": ""
|
||||
"authorName": "Όνομα ή όνομα χρήστη",
|
||||
"libraryName": "Όνομα της βιβλιοθήκης σας",
|
||||
"libraryDesc": "Περιγραφή της βιβλιοθήκης σας ώστε να βοηθήσει το κοινό να κατανοήσει τη χρήση της",
|
||||
"githubHandle": "Όνομα χρήστη στο GitHub (προαιρετικό), ώστε να μπορείτε να επεξεργαστείτε τη βιβλιοθήκη αφού υποβληθεί για αξιολόγηση",
|
||||
"twitterHandle": "Όνομα χρήστη Twitter (προαιρετικό), ώστε να γνωρίζουμε σε ποιον/η να δώσουμε εύσημα κατά την προώθηση μέσω Twitter",
|
||||
"website": "Σύνδεσμος για την προσωπική σας ιστοσελίδα ή αλλού (προαιρετικό)"
|
||||
},
|
||||
"errors": {
|
||||
"required": "Απαιτείται",
|
||||
"website": "Εισάγετε μια έγκυρη διεύθυνση URL"
|
||||
},
|
||||
"noteDescription": {
|
||||
"pre": "",
|
||||
"link": "",
|
||||
"post": ""
|
||||
"pre": "Υποβάλετε τη βιβλιοθήκη σας για να συμπεριληφθεί στο ",
|
||||
"link": "δημόσιο αποθετήριο βιβλιοθήκης",
|
||||
"post": "ώστε να χρησιμοποιηθεί από άλλα άτομα στα σχέδιά τους."
|
||||
},
|
||||
"noteGuidelines": {
|
||||
"pre": "",
|
||||
"pre": "Η βιβλιοθήκη πρέπει πρώτα να εγκριθεί χειροκίνητα. Παρακαλώ διαβάστε τους ",
|
||||
"link": "οδηγίες",
|
||||
"post": ""
|
||||
"post": " πριν την υποβολή. Θα χρειαστείτε έναν λογαριασμό GitHub για την επικοινωνία και για να προβείτε σε αλλαγές εφ' όσον χρειαστεί, αλλά δεν είναι αυστηρή απαίτηση."
|
||||
},
|
||||
"noteLicense": {
|
||||
"pre": "",
|
||||
"link": "",
|
||||
"post": ""
|
||||
"pre": "Με την υποβολή, συμφωνείτε ότι η βιβλιοθήκη θα δημοσιευθεί υπό την ",
|
||||
"link": "Άδεια MIT, ",
|
||||
"post": "που εν συντομία σημαίνει ότι ο καθένας μπορεί να τα χρησιμοποιήσει χωρίς περιορισμούς."
|
||||
},
|
||||
"noteItems": "",
|
||||
"atleastOneLibItem": "",
|
||||
"republishWarning": ""
|
||||
"noteItems": "Κάθε αντικείμενο της βιβλιοθήκης πρέπει να έχει το δικό του όνομα ώστε να μπορεί να φιλτραριστεί. Θα συμπεριληφθούν τα ακόλουθα αντικείμενα βιβλιοθήκης:",
|
||||
"atleastOneLibItem": "Παρακαλώ επιλέξτε τουλάχιστον ένα αντικείμενο βιβλιοθήκης για να ξεκινήσετε",
|
||||
"republishWarning": "Σημείωση: μερικά από τα επιλεγμένα αντικέιμενα έχουν ήδη επισημανθεί ως δημοσιευμένα/υποβεβλημένα. Θα πρέπει να υποβάλετε αντικείμενα εκ νέου μόνο για να ενημερώσετε μία ήδη υπάρχουσα βιβλιοθήκη ή υποβολή."
|
||||
},
|
||||
"publishSuccessDialog": {
|
||||
"title": "",
|
||||
"content": "",
|
||||
"title": "Η βιβλιοθήκη υποβλήθηκε",
|
||||
"content": "Ευχαριστούμε {{authorName}}. Η βιβλιοθήκη σας έχει υποβληθεί για αξιολόγηση. Μπορείτε να παρακολουθείτε τη διαδικασία",
|
||||
"link": "εδώ"
|
||||
},
|
||||
"confirmDialog": {
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "Bloquear todo",
|
||||
"unlockAll": "Desbloquear todo"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "Publicado",
|
||||
"sidebarLock": "Mantener barra lateral abierta"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "No hay elementos añadidos todavía...",
|
||||
"hint_emptyLibrary": "Seleccione un elemento en el lienzo para añadirlo aquí, o instale una biblioteca del repositorio público, a continuación.",
|
||||
"hint_emptyPrivateLibrary": "Seleccione un elemento del lienzo para añadirlo aquí."
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Limpiar lienzo y reiniciar el color de fondo",
|
||||
@@ -343,7 +349,7 @@
|
||||
},
|
||||
"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",
|
||||
"republishWarning": ""
|
||||
"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": {
|
||||
"title": "Biblioteca enviada",
|
||||
|
@@ -115,12 +115,18 @@
|
||||
"label": "Esteka"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "",
|
||||
"unlock": "",
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
"lock": "Blokeatu",
|
||||
"unlock": "Desblokeatu",
|
||||
"lockAll": "Blokeatu guztiak",
|
||||
"unlockAll": "Desblokeatu guztiak"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "Argitaratua",
|
||||
"sidebarLock": "Mantendu alboko barra irekita"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Oraindik ez da elementurik gehitu...",
|
||||
"hint_emptyLibrary": "Hautatu oihaleko elementu bat hemen gehitzeko, edo instalatu liburutegi bat beheko biltegi publikotik.",
|
||||
"hint_emptyPrivateLibrary": "Hautatu oihaleko elementu bat hemen gehitzeko."
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Garbitu oihala",
|
||||
@@ -190,7 +196,7 @@
|
||||
"svgImageInsertError": "Ezin izan da SVG irudia txertatu. SVG markak baliogabea dirudi.",
|
||||
"invalidSVGString": "SVG baliogabea.",
|
||||
"cannotResolveCollabServer": "Ezin izan da elkarlaneko zerbitzarira konektatu. Mesedez, berriro kargatu orria eta saiatu berriro.",
|
||||
"importLibraryError": ""
|
||||
"importLibraryError": "Ezin izan da liburutegia kargatu"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Hautapena",
|
||||
@@ -300,7 +306,7 @@
|
||||
"view": "Bistaratu",
|
||||
"zoomToFit": "Egin zoom elementu guztiak ikusteko",
|
||||
"zoomToSelection": "Zooma hautapenera",
|
||||
"toggleElementLock": ""
|
||||
"toggleElementLock": "Blokeatu/desbloketatu hautapena"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": "Garbitu oihala"
|
||||
@@ -343,7 +349,7 @@
|
||||
},
|
||||
"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",
|
||||
"republishWarning": ""
|
||||
"republishWarning": "Oharra: hautatutako elementu batzuk dagoeneko argitaratuta/bidalita bezala markatuta daude. Elementuak berriro bidali behar dituzu lehendik dagoen liburutegi edo bidalketa eguneratzen duzunean."
|
||||
},
|
||||
"publishSuccessDialog": {
|
||||
"title": "Liburutegia bidali da",
|
||||
|
@@ -9,7 +9,7 @@
|
||||
"copy": "کپی",
|
||||
"copyAsPng": "کپی در حافطه موقت به صورت PNG",
|
||||
"copyAsSvg": "کپی در حافطه موقت به صورت SVG",
|
||||
"copyText": "",
|
||||
"copyText": "کپی در حافطه موقت به صورت متن",
|
||||
"bringForward": "جلو آوردن",
|
||||
"sendToBack": "پس فرستادن",
|
||||
"bringToFront": "جلو آوردن",
|
||||
@@ -36,12 +36,12 @@
|
||||
"arrowhead_arrow": "پیکان",
|
||||
"arrowhead_bar": "میله ای",
|
||||
"arrowhead_dot": "نقطه",
|
||||
"arrowhead_triangle": "",
|
||||
"arrowhead_triangle": "مثلث",
|
||||
"fontSize": "اندازه قلم",
|
||||
"fontFamily": "نوع قلم",
|
||||
"onlySelected": "فقط انتخاب شده ها",
|
||||
"withBackground": "پس زمینه",
|
||||
"exportEmbedScene": "",
|
||||
"exportEmbedScene": "تعبیه صحنه",
|
||||
"exportEmbedScene_details": "متحوای صحنه به فایل خروجی SVG/PNG اضافه خواهد شد برای بازیابی صحنه به آن اضافه خواهد شد.\nباعث افزایش حجم فایل خروجی میشود.",
|
||||
"addWatermark": "\"ساخته شده با Excalidraw\" را اضافه کن",
|
||||
"handDrawn": "دست نویس",
|
||||
@@ -65,13 +65,13 @@
|
||||
"cartoonist": "کارتونیست",
|
||||
"fileTitle": "نام فایل",
|
||||
"colorPicker": "انتخابگر رنگ",
|
||||
"canvasColors": "",
|
||||
"canvasColors": "رنگ های بوم",
|
||||
"canvasBackground": "بوم",
|
||||
"drawingCanvas": "بوم نقاشی",
|
||||
"layers": "لایه ها",
|
||||
"actions": "عملیات",
|
||||
"language": "زبان",
|
||||
"liveCollaboration": "",
|
||||
"liveCollaboration": "همکاری زنده",
|
||||
"duplicateSelection": "تکرار",
|
||||
"untitled": "بدون عنوان",
|
||||
"name": "نام",
|
||||
@@ -98,29 +98,35 @@
|
||||
"flipHorizontal": "چرخش افقی",
|
||||
"flipVertical": "چرخش عمودی",
|
||||
"viewMode": "حالت نمایش",
|
||||
"toggleExportColorScheme": "",
|
||||
"toggleExportColorScheme": "تغییر طرح خروجی رنگ",
|
||||
"share": "اشتراکگذاری",
|
||||
"showStroke": "نمایش انتخاب کننده رنگ حاشیه",
|
||||
"showBackground": "نمایش انتخاب کننده رنگ پس زمینه",
|
||||
"toggleTheme": "تغییر تم",
|
||||
"personalLib": "",
|
||||
"excalidrawLib": "",
|
||||
"decreaseFontSize": "",
|
||||
"increaseFontSize": "",
|
||||
"unbindText": "",
|
||||
"bindText": "",
|
||||
"personalLib": "کتابخانه شخصی",
|
||||
"excalidrawLib": "کتابخانه",
|
||||
"decreaseFontSize": "کاهش اندازه فونت",
|
||||
"increaseFontSize": "افزایش دادن اندازه فونت",
|
||||
"unbindText": "بازکردن نوشته",
|
||||
"bindText": "بستن نوشته",
|
||||
"link": {
|
||||
"edit": "",
|
||||
"create": "",
|
||||
"label": ""
|
||||
"edit": "ویرایش لینک",
|
||||
"create": "ایجاد پیوند",
|
||||
"label": "لینک"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "",
|
||||
"unlock": "",
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
"lock": "قفل",
|
||||
"unlock": "باز کردن",
|
||||
"lockAll": "قفل همه",
|
||||
"unlockAll": "باز کردن قفل همه"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "منتشر شده",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "پاکسازی بوم نقاشی",
|
||||
@@ -147,19 +153,19 @@
|
||||
"edit": "ویرایش",
|
||||
"undo": "بازگرد",
|
||||
"redo": "از سر",
|
||||
"resetLibrary": "",
|
||||
"resetLibrary": "تنظیم مجدد کتابخانه",
|
||||
"createNewRoom": "ایجاد یک اتاق جدید",
|
||||
"fullScreen": "تمامصفحه",
|
||||
"darkMode": "حالت تیره",
|
||||
"lightMode": "حالت روشن",
|
||||
"zenMode": "حالت ذن",
|
||||
"exitZenMode": "خروج از حالت تمرکز",
|
||||
"cancel": "",
|
||||
"clear": "",
|
||||
"remove": "",
|
||||
"publishLibrary": "",
|
||||
"submit": "",
|
||||
"confirm": ""
|
||||
"cancel": "لغو",
|
||||
"clear": "پاک کردن",
|
||||
"remove": "پاک کردن",
|
||||
"publishLibrary": "انتشار",
|
||||
"submit": "ارسال",
|
||||
"confirm": "تایید"
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "این کار کل صفحه را پاک میکند. آیا مطمئنید؟",
|
||||
@@ -168,33 +174,33 @@
|
||||
"couldNotLoadInvalidFile": "عدم توانایی در بازگذاری فایل نامعتبر",
|
||||
"importBackendFailed": "بارگیری از پشت صحنه با شکست مواجه شد.",
|
||||
"cannotExportEmptyCanvas": "بوم خالی قابل تبدیل نیست.",
|
||||
"couldNotCopyToClipboard": "",
|
||||
"couldNotCopyToClipboard": "به کلیپ بورد کپی نشد.",
|
||||
"decryptFailed": "رمزگشایی داده ها امکان پذیر نیست.",
|
||||
"uploadedSecurly": "آپلود با رمزگذاری دو طرفه انجام میشود، به این معنی که سرور Excalidraw و اشخاص ثالث نمی توانند مطالب شما را بخوانند.",
|
||||
"loadSceneOverridePrompt": "بارگزاری یک طرح خارجی محتوای فعلی رو از بین میبرد. آیا میخواهید ادامه دهید؟",
|
||||
"collabStopOverridePrompt": "",
|
||||
"collabStopOverridePrompt": "با توقف بوم نقاشی، نقشه قبلی و ذخیره شده محلی شما را بازنویسی می کند. مطمئنی؟\n\n(اگر می خواهید نقاشی محلی خود را حفظ کنید، به سادگی برگه مرورگر را ببندید.)",
|
||||
"errorAddingToLibrary": "مورد به کتابخانه اضافه نشد",
|
||||
"errorRemovingFromLibrary": "مورد از کتابخانه حذف نشد",
|
||||
"confirmAddLibrary": "{{numShapes}} از اشکال به کتابخانه شما اضافه خواهد شد. مطمئن هستید؟",
|
||||
"imageDoesNotContainScene": "",
|
||||
"imageDoesNotContainScene": "به نظر نمی رسد این تصویر حاوی داده های بوم نقاشی باشد. آیا جاسازی صحنه را در حین خروجی فعال کرده اید?",
|
||||
"cannotRestoreFromImage": "صحنه را نمی توان از این فایل تصویری بازیابی کرد",
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": ""
|
||||
"invalidSceneUrl": "بوم نقاشی از آدرس ارائه شده وارد نشد. این یا نادرست است، یا حاوی داده Excalidraw JSON معتبر نیست.",
|
||||
"resetLibrary": "ین کار کل صفحه را پاک میکند. آیا مطمئنید?",
|
||||
"removeItemsFromsLibrary": "حذف {{count}} آیتم(ها) از کتابخانه?",
|
||||
"invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است."
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "",
|
||||
"imageInsertError": "",
|
||||
"fileTooBig": "",
|
||||
"svgImageInsertError": "",
|
||||
"invalidSVGString": "",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": ""
|
||||
"unsupportedFileType": "نوع فایل پشتیبانی نشده.",
|
||||
"imageInsertError": "عکس ارسال نشد. بعداً دوباره تلاش کنید...",
|
||||
"fileTooBig": "فایل خیلی بزرگ است حداکثر اندازه مجاز {{maxSize}}.",
|
||||
"svgImageInsertError": "تصویر SVG وارد نشد. نشانه گذاری SVG نامعتبر به نظر می رسد.",
|
||||
"invalidSVGString": "SVG نادرست.",
|
||||
"cannotResolveCollabServer": "به سرور collab متصل نشد. لطفا صفحه را مجددا بارگذاری کنید و دوباره تلاش کنید.",
|
||||
"importLibraryError": "دادهها بارگذاری نشدند"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "گزینش",
|
||||
"image": "",
|
||||
"image": "وارد کردن تصویر",
|
||||
"rectangle": "مستطیل",
|
||||
"diamond": "لوزی",
|
||||
"ellipse": "بیضی",
|
||||
@@ -204,9 +210,9 @@
|
||||
"text": "متن",
|
||||
"library": "کتابخانه",
|
||||
"lock": "ابزار انتخاب شده را بعد از کشیدن نگه دار",
|
||||
"penMode": "",
|
||||
"link": "",
|
||||
"eraser": ""
|
||||
"penMode": "از زوم کوچک کردن جلوگیری کنید و ورودی آزاد را فقط از قلم بپذیرید",
|
||||
"link": "افزودن/بهروزرسانی پیوند برای شکل انتخابی",
|
||||
"eraser": "پاک کن"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "عملیات روی بوم",
|
||||
@@ -214,25 +220,25 @@
|
||||
"shapes": "شکلها"
|
||||
},
|
||||
"hints": {
|
||||
"canvasPanning": "",
|
||||
"canvasPanning": "برای حرکت دادن بوم، چرخ ماوس یا فاصله را در حین کشیدن نگه دارید",
|
||||
"linearElement": "برای چند نقطه کلیک و برای یک خط بکشید",
|
||||
"freeDraw": "کلیک کنید و بکشید و وقتی کار تمام شد رها کنید",
|
||||
"text": "نکته: با برنامه انتخاب شده شما میتوانید با دوبار کلیک کردن هرکجا میخواید متن اظاف کنید",
|
||||
"text_selected": "",
|
||||
"text_editing": "",
|
||||
"text_selected": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید",
|
||||
"text_editing": "Escape یا CtrlOrCmd+ENTER را فشار دهید تا ویرایش تمام شود",
|
||||
"linearElementMulti": "روی آخرین نقطه کلیک کنید یا کلید ESC را بزنید یا کلید Enter را بزنید برای اتمام کار",
|
||||
"lockAngle": "با نگه داشتن SHIFT هنگام چرخش می توانید زاویه ها را محدود کنید",
|
||||
"resize": "می توانید با نگه داشتن SHIFT در هنگام تغییر اندازه، نسبت ها را محدود کنید،ALT را برای تغییر اندازه از مرکز نگه دارید",
|
||||
"resizeImage": "",
|
||||
"resizeImage": "با نگه داشتن SHIFT می توانید آزادانه اندازه را تغییر دهید،\nبرای تغییر اندازه از مرکز، ALT را نگه دارید",
|
||||
"rotate": "با نگه داشتن SHIFT هنگام چرخش می توانید زاویه ها را محدود کنید",
|
||||
"lineEditor_info": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید",
|
||||
"lineEditor_pointSelected": "",
|
||||
"lineEditor_nothingSelected": "",
|
||||
"placeImage": "",
|
||||
"publishLibrary": "",
|
||||
"bindTextToElement": "",
|
||||
"deepBoxSelect": "",
|
||||
"eraserRevert": ""
|
||||
"lineEditor_pointSelected": "برای حذف نقطه Delete برای کپی زدن Ctrl یا Cmd+D را بزنید و یا برای جابجایی بکشید",
|
||||
"lineEditor_nothingSelected": "یک نقطه را برای ویرایش انتخاب کنید (SHIFT را برای انتخاب چندگانه نگه دارید)،\nیا Alt را نگه دارید و برای افزودن نقاط جدید کلیک کنید",
|
||||
"placeImage": "برای قرار دادن تصویر کلیک کنید، یا کلیک کنید و بکشید تا اندازه آن به صورت دستی تنظیم شود",
|
||||
"publishLibrary": "کتابخانه خود را منتشر کنید",
|
||||
"bindTextToElement": "برای افزودن اینتر را بزنید",
|
||||
"deepBoxSelect": "CtrlOrCmd را برای انتخاب عمیق و جلوگیری از کشیدن نگه دارید",
|
||||
"eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند"
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "پیش نمایش نشان داده نمی شود",
|
||||
@@ -260,27 +266,27 @@
|
||||
"desc_inProgressIntro": "جلسه همکاری آنلاین در حال انجام است.",
|
||||
"desc_shareLink": "این لینک را با هر کسی که می خواهید با او همکاری کنید به اشتراک بگذارید:",
|
||||
"desc_exitSession": "با پایان دادن جلسه، شما از اتاق حذف میکند، اما می توانید به صورت محلی کار خود را با بوم ادامه دهید. توجه داشته باشید که این مورد بر سایر افراد تأثیر نمی گذارد و همچنان می توانند در نسخه خود همکاری کنند.",
|
||||
"shareTitle": ""
|
||||
"shareTitle": "به یک جلسه همکاری زنده در Excalidraw بپیوندید"
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": "خطا"
|
||||
},
|
||||
"exportDialog": {
|
||||
"disk_title": "ذخیره در دیسک",
|
||||
"disk_details": "",
|
||||
"disk_details": "داده های صحنه را به فایلی که بعداً می توانید از آن وارد کنید صادر کنید.",
|
||||
"disk_button": "ذخیره در فایل",
|
||||
"link_title": "لینک قابل اشتراکگذاری",
|
||||
"link_details": "",
|
||||
"link_button": "",
|
||||
"excalidrawplus_description": "",
|
||||
"link_details": "خروجی به عنوان یک پیوند فقط خواندنی.",
|
||||
"link_button": "خروجی در فایل",
|
||||
"excalidrawplus_description": "صحنه را در فضای کاری Excalidraw+ خود ذخیره کنید.",
|
||||
"excalidrawplus_button": "خروجی گرفتن",
|
||||
"excalidrawplus_exportError": ""
|
||||
"excalidrawplus_exportError": "در حال حاضر نمیتوان به Excalidraw+ صادر کرد..."
|
||||
},
|
||||
"helpDialog": {
|
||||
"blog": "بلاگ ما را بخوانید",
|
||||
"click": "کلیک",
|
||||
"deepSelect": "",
|
||||
"deepBoxSelect": "",
|
||||
"deepSelect": "انتخاب عمیق",
|
||||
"deepBoxSelect": "انتخاب عمیق در کادر، و جلوگیری از کشیدن",
|
||||
"curvedArrow": "فلش خمیده",
|
||||
"curvedLine": "منحنی",
|
||||
"documentation": "مستندات",
|
||||
@@ -292,7 +298,7 @@
|
||||
"howto": "راهنمای ما را دنبال کنید",
|
||||
"or": "یا",
|
||||
"preventBinding": "مانع شدن از چسبیدن فلش ها",
|
||||
"tools": "",
|
||||
"tools": "ابزار",
|
||||
"shortcuts": "میانبرهای صفحه کلید",
|
||||
"textFinish": "پایان ویرایش (ویرایشگر متن)",
|
||||
"textNewLine": "افزودن خط جدید (ویرایشگر متن)",
|
||||
@@ -300,63 +306,63 @@
|
||||
"view": "مشاهده",
|
||||
"zoomToFit": "بزرگنمایی برای دیدن تمام آیتم ها",
|
||||
"zoomToSelection": "بزرگنمایی قسمت انتخاب شده",
|
||||
"toggleElementLock": ""
|
||||
"toggleElementLock": "قفل/بازکردن انتخاب شده ها"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": ""
|
||||
"title": "پاک کردن بوم"
|
||||
},
|
||||
"publishDialog": {
|
||||
"title": "",
|
||||
"itemName": "",
|
||||
"authorName": "",
|
||||
"githubUsername": "",
|
||||
"twitterUsername": "",
|
||||
"libraryName": "",
|
||||
"libraryDesc": "",
|
||||
"website": "",
|
||||
"title": "انتشار کتابخانه",
|
||||
"itemName": "نام آیتم",
|
||||
"authorName": "نام نویسنده",
|
||||
"githubUsername": "نام کاربری گیت هاب",
|
||||
"twitterUsername": "نام کاربری توییتر",
|
||||
"libraryName": "نام کتابخانه",
|
||||
"libraryDesc": "توضیحات کتابخانه",
|
||||
"website": "تارنما",
|
||||
"placeholder": {
|
||||
"authorName": "",
|
||||
"libraryName": "",
|
||||
"libraryDesc": "",
|
||||
"githubHandle": "",
|
||||
"twitterHandle": "",
|
||||
"website": ""
|
||||
"authorName": "نام یا نام کاربری شما",
|
||||
"libraryName": "اسم کتابخانه",
|
||||
"libraryDesc": "شرحی از کتابخانه شما برای کمک به مردم برای درک استفاده از آن",
|
||||
"githubHandle": "دسته GitHub (اختیاری)، بنابراین می توانید پس از ارسال برای بررسی، کتابخانه را ویرایش کنید",
|
||||
"twitterHandle": "نام کاربری توییتر (اختیاری)، بنابراین می دانیم هنگام تبلیغ در توییتر به چه کسی اعتبار دهیم",
|
||||
"website": "پیوند به وب سایت شخصی شما یا هر جای دیگر (اختیاری)"
|
||||
},
|
||||
"errors": {
|
||||
"required": "",
|
||||
"website": ""
|
||||
"required": "لازم",
|
||||
"website": "وارد کردن آدرس درست"
|
||||
},
|
||||
"noteDescription": {
|
||||
"pre": "",
|
||||
"link": "",
|
||||
"post": ""
|
||||
"pre": "کتابخانه خود را ارسال کنید تا در آن گنجانده شود ",
|
||||
"link": "مخزن کتابخانه عمومی",
|
||||
"post": "تا افراد دیگر در نقاشی های خود از آن استفاده کنند."
|
||||
},
|
||||
"noteGuidelines": {
|
||||
"pre": "",
|
||||
"link": "",
|
||||
"post": ""
|
||||
"pre": "کتابخانه باید ابتدا به صورت دستی تایید شود. لطفاً بخوانید ",
|
||||
"link": "دستورالعملها",
|
||||
"post": " قبل از ارسال برای برقراری ارتباط و ایجاد تغییرات در صورت درخواست، به یک حساب GitHub نیاز دارید، اما به شدت الزامی نیست."
|
||||
},
|
||||
"noteLicense": {
|
||||
"pre": "",
|
||||
"link": "",
|
||||
"post": ""
|
||||
"pre": "با ارسال، موافقت می کنید که کتابخانه تحت عنوان منتشر شود ",
|
||||
"link": "پروانهٔ MIT ",
|
||||
"post": "که به طور خلاصه به این معنی است که هر کسی می تواند بدون محدودیت از آنها استفاده کند."
|
||||
},
|
||||
"noteItems": "",
|
||||
"atleastOneLibItem": "",
|
||||
"republishWarning": ""
|
||||
"noteItems": "هر مورد کتابخانه باید نام خاص خود را داشته باشد تا قابل فیلتر باشد. اقلام کتابخانه زیر شامل خواهد شد:",
|
||||
"atleastOneLibItem": "لطفاً حداقل یک مورد از کتابخانه را برای شروع انتخاب کنید",
|
||||
"republishWarning": "توجه: برخی از موارد انتخاب شده به عنوان قبلاً منتشر شده/ارسال شده علامت گذاری شده اند. شما فقط باید هنگام بهروزرسانی یک کتابخانه موجود یا ارسال، موارد را دوباره ارسال کنید."
|
||||
},
|
||||
"publishSuccessDialog": {
|
||||
"title": "",
|
||||
"content": "",
|
||||
"link": ""
|
||||
"title": "کتابخانه ارسال شد",
|
||||
"content": "تشکر از شما {{authorName}}. کتابخانه شما برای بررسی ارسال شده است. می توانید وضعیت را پیگیری کنید",
|
||||
"link": "اینجا"
|
||||
},
|
||||
"confirmDialog": {
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromLib": ""
|
||||
"resetLibrary": "تنظیم مجدد کتابخانه",
|
||||
"removeItemsFromLib": "موارد انتخاب شده از موارد پسندیده حذف شوند"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند.",
|
||||
"link": ""
|
||||
"link": "پست وبلاگ در مورد رمزگذاری سرتاسر در Excalidraw"
|
||||
},
|
||||
"stats": {
|
||||
"angle": "زاویه",
|
||||
@@ -374,60 +380,60 @@
|
||||
"width": "عرض"
|
||||
},
|
||||
"toast": {
|
||||
"addedToLibrary": "",
|
||||
"addedToLibrary": "به مجموعه اضافه شد",
|
||||
"copyStyles": "کپی سبک.",
|
||||
"copyToClipboard": "در کلیپبورد کپی شد.",
|
||||
"copyToClipboardAsPng": "",
|
||||
"copyToClipboardAsPng": "کپی {{exportSelection}} در کلیپبورد به عنوان PNG\n({{exportColorScheme}})",
|
||||
"fileSaved": "فایل ذخیره شد.",
|
||||
"fileSavedToFilename": "ذخیره در {filename}",
|
||||
"canvas": "بوم",
|
||||
"selection": "انتخاب"
|
||||
},
|
||||
"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": ""
|
||||
"ffffff": "سفید",
|
||||
"f8f9fa": "خاکستری 0",
|
||||
"f1f3f5": "خاکستری 1",
|
||||
"fff5f5": "قرمز 0",
|
||||
"fff0f6": "صورتی 0",
|
||||
"f8f0fc": "انگوری 0",
|
||||
"f3f0ff": "بنفش 0",
|
||||
"edf2ff": "نیلی 0",
|
||||
"e7f5ff": "آبی 0",
|
||||
"e3fafc": "آبی نفتی 0",
|
||||
"e6fcf5": "آبی فیروزه ای 0",
|
||||
"ebfbee": "سبز 0",
|
||||
"f4fce3": "لیمویی 0",
|
||||
"fff9db": "زرد 0",
|
||||
"fff4e6": "نارنجی 0",
|
||||
"transparent": "شفاف",
|
||||
"ced4da": "خاکستری 4",
|
||||
"868e96": "خاکستری 6",
|
||||
"fa5252": "قرمز 6",
|
||||
"e64980": "صورتی 6",
|
||||
"be4bdb": "انگوری 6",
|
||||
"7950f2": "بنفش 6",
|
||||
"4c6ef5": "نیلی 6",
|
||||
"228be6": "آبی 6",
|
||||
"15aabf": "آبی نفتی 6",
|
||||
"12b886": "آبی فیروزه ای 6",
|
||||
"40c057": "سبز 6",
|
||||
"82c91e": "لیمویی 6",
|
||||
"fab005": "زرد 6",
|
||||
"fd7e14": "نارنجی 6",
|
||||
"000000": "سیاه",
|
||||
"343a40": "خاکستری 8",
|
||||
"495057": "خاکستری 7",
|
||||
"c92a2a": "قرمز 9",
|
||||
"a61e4d": "صورتی 9",
|
||||
"862e9c": "انگوری 9",
|
||||
"5f3dc4": "بنفش 9",
|
||||
"364fc7": "نیلی 9",
|
||||
"1864ab": "آبی 9",
|
||||
"0b7285": "آبی نفتی 9",
|
||||
"087f5b": "آبی فیروزه ای 9",
|
||||
"2b8a3e": "سبز 9",
|
||||
"5c940d": "لیمویی 9",
|
||||
"e67700": "زرد 9",
|
||||
"d9480f": "نارنجی 9"
|
||||
}
|
||||
}
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": "Julkaistu"
|
||||
"statusPublished": "Julkaistu",
|
||||
"sidebarLock": "Pidä sivupalkki avoinna"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Kirjastossa ei ole vielä yhtään kohdetta...",
|
||||
"hint_emptyLibrary": "Valitse lisättävä kohde piirtoalueelta, tai asenna alta julkinen kirjasto.",
|
||||
"hint_emptyPrivateLibrary": "Valitse lisättävä kohde piirtoalueelta."
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Tyhjennä piirtoalue",
|
||||
|
@@ -10,29 +10,29 @@
|
||||
"copyAsPng": "Copier dans le presse-papier en PNG",
|
||||
"copyAsSvg": "Copier dans le presse-papier en SVG",
|
||||
"copyText": "Copier dans le presse-papier en tant que texte",
|
||||
"bringForward": "Envoyer vers l'avant",
|
||||
"sendToBack": "Mettre en arrière-plan",
|
||||
"bringToFront": "Mettre au premier plan",
|
||||
"sendBackward": "Envoyer vers l'arrière",
|
||||
"bringForward": "Avancer d'un plan",
|
||||
"sendToBack": "Déplacer à l'arrière-plan",
|
||||
"bringToFront": "Placer au premier plan",
|
||||
"sendBackward": "Reculer d'un plan",
|
||||
"delete": "Supprimer",
|
||||
"copyStyles": "Copier les styles",
|
||||
"pasteStyles": "Coller les styles",
|
||||
"stroke": "Trait",
|
||||
"background": "Arrière-plan",
|
||||
"fill": "Remplissage",
|
||||
"strokeWidth": "Largeur du trait",
|
||||
"background": "Fond",
|
||||
"fill": "Motif du fond",
|
||||
"strokeWidth": "Épaisseur du trait",
|
||||
"strokeStyle": "Style du trait",
|
||||
"strokeStyle_solid": "Plein",
|
||||
"strokeStyle_solid": "Continu",
|
||||
"strokeStyle_dashed": "Tirets",
|
||||
"strokeStyle_dotted": "Pointillé",
|
||||
"strokeStyle_dotted": "Pointillés",
|
||||
"sloppiness": "Style de tracé",
|
||||
"opacity": "Opacité",
|
||||
"textAlign": "Alignement du texte",
|
||||
"edges": "Angles",
|
||||
"sharp": "Pointus",
|
||||
"round": "Arrondis",
|
||||
"arrowheads": "Extrémités de flèche",
|
||||
"arrowhead_none": "Aucune",
|
||||
"arrowheads": "Extrémités",
|
||||
"arrowhead_none": "Sans",
|
||||
"arrowhead_arrow": "Flèche",
|
||||
"arrowhead_bar": "Barre",
|
||||
"arrowhead_dot": "Point",
|
||||
@@ -43,23 +43,23 @@
|
||||
"withBackground": "Arrière-plan",
|
||||
"exportEmbedScene": "Intégrer la scène",
|
||||
"exportEmbedScene_details": "Les données de scène seront enregistrées dans le fichier PNG/SVG exporté, afin que la scène puisse être restaurée à partir de celui-ci.\nCela augmentera la taille du fichier exporté.",
|
||||
"addWatermark": "Ajouter \"Fait avec Excalidraw\"",
|
||||
"addWatermark": "Ajouter \"Réalisé avec Excalidraw\"",
|
||||
"handDrawn": "À la main",
|
||||
"normal": "Normale",
|
||||
"code": "Code",
|
||||
"small": "Petit",
|
||||
"medium": "Moyen",
|
||||
"large": "Grand",
|
||||
"veryLarge": "Très grand",
|
||||
"small": "Petite",
|
||||
"medium": "Moyenne",
|
||||
"large": "Grande",
|
||||
"veryLarge": "Très grande",
|
||||
"solid": "Solide",
|
||||
"hachure": "Hachure",
|
||||
"crossHatch": "Hachure croisée",
|
||||
"thin": "Fin",
|
||||
"bold": "Épais",
|
||||
"left": "Gauche",
|
||||
"center": "Centre",
|
||||
"right": "Droite",
|
||||
"extraBold": "Très épais",
|
||||
"hachure": "Hachures",
|
||||
"crossHatch": "Hachures croisées",
|
||||
"thin": "Fine",
|
||||
"bold": "Épaisse",
|
||||
"left": "À gauche",
|
||||
"center": "Au centre",
|
||||
"right": "À droite",
|
||||
"extraBold": "Très épaisse",
|
||||
"architect": "Architecte",
|
||||
"artist": "Artiste",
|
||||
"cartoonist": "Caricaturiste",
|
||||
@@ -68,7 +68,7 @@
|
||||
"canvasColors": "Utilisé sur la zone de dessin",
|
||||
"canvasBackground": "Arrière-plan du canevas",
|
||||
"drawingCanvas": "Zone de dessin",
|
||||
"layers": "Calques",
|
||||
"layers": "Disposition",
|
||||
"actions": "Actions",
|
||||
"language": "Langue",
|
||||
"liveCollaboration": "Collaboration en direct",
|
||||
@@ -86,41 +86,47 @@
|
||||
"libraryLoadingMessage": "Chargement de la bibliothèque…",
|
||||
"libraries": "Parcourir les bibliothèques",
|
||||
"loadingScene": "Chargement de la scène…",
|
||||
"align": "Aligner",
|
||||
"align": "Alignement",
|
||||
"alignTop": "Aligner en haut",
|
||||
"alignBottom": "Aligner en bas",
|
||||
"alignLeft": "Aligner à gauche",
|
||||
"alignRight": "Aligner à droite",
|
||||
"centerVertically": "Centrer verticalement",
|
||||
"centerHorizontally": "Centrer horizontalement",
|
||||
"distributeHorizontally": "Distribuer horizontalement",
|
||||
"distributeVertically": "Distribuer verticalement",
|
||||
"distributeHorizontally": "Répartir horizontalement",
|
||||
"distributeVertically": "Répartir verticalement",
|
||||
"flipHorizontal": "Retourner horizontalement",
|
||||
"flipVertical": "Retourner verticalement",
|
||||
"viewMode": "Mode présentation",
|
||||
"toggleExportColorScheme": "Activer/Désactiver l'export du thème de couleur",
|
||||
"share": "Partager",
|
||||
"showStroke": "Afficher le sélecteur de couleur de trait",
|
||||
"showBackground": "Afficher le sélecteur de couleur d'arrière-plan",
|
||||
"showBackground": "Afficher le sélecteur de couleur de fond",
|
||||
"toggleTheme": "Changer le thème",
|
||||
"personalLib": "Bibliothèque personnelle",
|
||||
"excalidrawLib": "Bibliothèque Excalidraw",
|
||||
"decreaseFontSize": "Réduire la taille de police",
|
||||
"decreaseFontSize": "Diminuer la taille de police",
|
||||
"increaseFontSize": "Augmenter la taille de police",
|
||||
"unbindText": "Délier le texte",
|
||||
"bindText": "Lier le texte au conteneur",
|
||||
"unbindText": "Dissocier le texte",
|
||||
"bindText": "Associer le texte au conteneur",
|
||||
"link": {
|
||||
"edit": "Modifier le lien",
|
||||
"create": "Créer un lien",
|
||||
"create": "Ajouter un lien",
|
||||
"label": "Lien"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "Verrouiller",
|
||||
"unlock": "Déverrouiller",
|
||||
"lockAll": "Tout verrouiller",
|
||||
"unlockAll": "Tout déverouiller"
|
||||
"unlockAll": "Tout déverrouiller"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "Publié",
|
||||
"sidebarLock": "Maintenir la barre latérale ouverte"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Aucun élément n'a encore été ajouté ...",
|
||||
"hint_emptyLibrary": "Sélectionnez un élément sur le canevas pour l'ajouter ici ou installez une bibliothèque depuis le dépôt public, ci-dessous.",
|
||||
"hint_emptyPrivateLibrary": "Sélectionnez un élément sur le canevas pour l'ajouter ici."
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Réinitialiser le canevas",
|
||||
@@ -164,9 +170,9 @@
|
||||
"alerts": {
|
||||
"clearReset": "L'intégralité du canevas va être effacée. Êtes-vous sûr ?",
|
||||
"couldNotCreateShareableLink": "Impossible de créer un lien de partage.",
|
||||
"couldNotCreateShareableLinkTooBig": "Impossible de créer un lien partageable : la scène est trop volumineuse",
|
||||
"couldNotCreateShareableLinkTooBig": "Impossible de créer un lien de partage : la scène est trop volumineuse",
|
||||
"couldNotLoadInvalidFile": "Impossible de charger un fichier invalide",
|
||||
"importBackendFailed": "L'importation depuis le backend a échoué.",
|
||||
"importBackendFailed": "L'importation depuis le serveur a échoué.",
|
||||
"cannotExportEmptyCanvas": "Impossible d'exporter un canevas vide.",
|
||||
"couldNotCopyToClipboard": "Impossible de copier dans le presse-papiers.",
|
||||
"decryptFailed": "Les données n'ont pas pu être déchiffrées.",
|
||||
@@ -222,7 +228,7 @@
|
||||
"text_editing": "Appuyez sur ÉCHAP ou Ctrl/Cmd+ENTRÉE pour terminer l'édition",
|
||||
"linearElementMulti": "Cliquez sur le dernier point ou appuyez sur Échap ou Entrée pour terminer",
|
||||
"lockAngle": "Vous pouvez restreindre l'angle en maintenant MAJ",
|
||||
"resize": "Vous pouvez conserver les proportions en maintenant la touche MAJ pendant le redimensionnement,\nmaintenez la touche ALT pour redimensionner par rapport au centre",
|
||||
"resize": "Vous pouvez conserver les proportions en maintenant la touche MAJ pendant le redimensionnement, maintenez la touche ALT pour redimensionner par rapport au centre",
|
||||
"resizeImage": "Vous pouvez redimensionner librement en maintenant SHIFT,\nmaintenez ALT pour redimensionner depuis le centre",
|
||||
"rotate": "Vous pouvez restreindre les angles en maintenant MAJ pendant la rotation",
|
||||
"lineEditor_info": "Double-cliquez ou appuyez sur Entrée pour éditer les points",
|
||||
@@ -231,7 +237,7 @@
|
||||
"placeImage": "Cliquez pour placer l'image, ou cliquez et faites glisser pour définir sa taille manuellement",
|
||||
"publishLibrary": "Publier votre propre bibliothèque",
|
||||
"bindTextToElement": "Appuyer sur Entrée pour ajouter du texte",
|
||||
"deepBoxSelect": "Maintenir CtrlOuCmd pour sélectionner dans les groupes, et empêcher le déplacement",
|
||||
"deepBoxSelect": "Maintenir Ctrl ou Cmd pour sélectionner dans les groupes et empêcher le déplacement",
|
||||
"eraserRevert": "Maintenez Alt enfoncé pour annuler les éléments marqués pour suppression"
|
||||
},
|
||||
"canvasError": {
|
||||
@@ -343,7 +349,7 @@
|
||||
},
|
||||
"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",
|
||||
"republishWarning": ""
|
||||
"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": {
|
||||
"title": "Bibliothèque soumise",
|
||||
|
439
src/locales/gl-ES.json
Normal file
439
src/locales/gl-ES.json
Normal file
@@ -0,0 +1,439 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "Paste",
|
||||
"pasteCharts": "Pegar gráficos",
|
||||
"selectAll": "Seleccionar todo",
|
||||
"multiSelect": "Engadir elemento á selección",
|
||||
"moveCanvas": "Mover o lenzo",
|
||||
"cut": "Cortar",
|
||||
"copy": "Copiar",
|
||||
"copyAsPng": "Copiar no portapapeis como PNG",
|
||||
"copyAsSvg": "Copiar no portapapeis como SVG",
|
||||
"copyText": "Copia no portapapeis como texto",
|
||||
"bringForward": "Traer cara adiante",
|
||||
"sendToBack": "Enviar cara atrás",
|
||||
"bringToFront": "Traer á fronte",
|
||||
"sendBackward": "Enviar ao fondo",
|
||||
"delete": "Borrar",
|
||||
"copyStyles": "Copiar estilo",
|
||||
"pasteStyles": "Pegar estilo",
|
||||
"stroke": "Trazo",
|
||||
"background": "Fondo",
|
||||
"fill": "Recheo",
|
||||
"strokeWidth": "Largo do trazo",
|
||||
"strokeStyle": "Estilo do trazo",
|
||||
"strokeStyle_solid": "Sólido",
|
||||
"strokeStyle_dashed": "Liña de trazos",
|
||||
"strokeStyle_dotted": "Liña de puntos",
|
||||
"sloppiness": "Estilo de trazo",
|
||||
"opacity": "Opacidade",
|
||||
"textAlign": "Aliñar texto",
|
||||
"edges": "Bordos",
|
||||
"sharp": "Agudo",
|
||||
"round": "Redondo",
|
||||
"arrowheads": "Puntas de frecha",
|
||||
"arrowhead_none": "Ningunha",
|
||||
"arrowhead_arrow": "Frecha",
|
||||
"arrowhead_bar": "Barra",
|
||||
"arrowhead_dot": "Punto",
|
||||
"arrowhead_triangle": "Triángulo",
|
||||
"fontSize": "Tamaño da fonte",
|
||||
"fontFamily": "Tipo de fonte",
|
||||
"onlySelected": "Só seleccionados",
|
||||
"withBackground": "Fondo",
|
||||
"exportEmbedScene": "Inserir escena",
|
||||
"exportEmbedScene_details": "",
|
||||
"addWatermark": "Engadir \"Feito con Excalidraw\"",
|
||||
"handDrawn": "Debuzado á man",
|
||||
"normal": "Normal",
|
||||
"code": "Código",
|
||||
"small": "Pequeno",
|
||||
"medium": "Mediano",
|
||||
"large": "Grande",
|
||||
"veryLarge": "Moi grande",
|
||||
"solid": "Sólido",
|
||||
"hachure": "Folleto",
|
||||
"crossHatch": "Raiado transversal",
|
||||
"thin": "Estreito",
|
||||
"bold": "Groso",
|
||||
"left": "Esquerda",
|
||||
"center": "Centrado",
|
||||
"right": "Dereita",
|
||||
"extraBold": "Moi groso",
|
||||
"architect": "Arquitecto",
|
||||
"artist": "Artista",
|
||||
"cartoonist": "Caricatura",
|
||||
"fileTitle": "Nome do arquivo",
|
||||
"colorPicker": "Selector de cor",
|
||||
"canvasColors": "Usado en lenzo",
|
||||
"canvasBackground": "Fondo do lenzo",
|
||||
"drawingCanvas": "Lenzo de debuxo",
|
||||
"layers": "Capas",
|
||||
"actions": "Accións",
|
||||
"language": "Idioma",
|
||||
"liveCollaboration": "Colaboración en directo",
|
||||
"duplicateSelection": "Duplicar",
|
||||
"untitled": "Sen título",
|
||||
"name": "Nome",
|
||||
"yourName": "O teu nome",
|
||||
"madeWithExcalidraw": "Feito con Excalidraw",
|
||||
"group": "Agrupar selección",
|
||||
"ungroup": "Desagrupar selección",
|
||||
"collaborators": "Colaboradores",
|
||||
"showGrid": "Mostrar cuadrícula",
|
||||
"addToLibrary": "Engadir á biblioteca",
|
||||
"removeFromLibrary": "Eliminar da biblioteca",
|
||||
"libraryLoadingMessage": "Cargando biblioteca…",
|
||||
"libraries": "Explorar bibliotecas",
|
||||
"loadingScene": "Cargando escena…",
|
||||
"align": "Aliñamento",
|
||||
"alignTop": "Aliñamento superior",
|
||||
"alignBottom": "Aliñamento inferior",
|
||||
"alignLeft": "Aliñar a esquerda",
|
||||
"alignRight": "Aliñar a dereita",
|
||||
"centerVertically": "Centrar verticalmente",
|
||||
"centerHorizontally": "Centrar horizontalmente",
|
||||
"distributeHorizontally": "Distribuír horizontalmente",
|
||||
"distributeVertically": "Distribuír verticalmente",
|
||||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "Modo de visualización",
|
||||
"toggleExportColorScheme": "Alternar esquema de cores de exportación",
|
||||
"share": "Compartir",
|
||||
"showStroke": "Mostrar selector de cores do trazo",
|
||||
"showBackground": "Mostrar selector de cores do fondo",
|
||||
"toggleTheme": "Alternar tema",
|
||||
"personalLib": "Biblioteca Persoal",
|
||||
"excalidrawLib": "Biblioteca Excalidraw",
|
||||
"decreaseFontSize": "Diminuír tamaño da fonte",
|
||||
"increaseFontSize": "Aumentar o tamaño da fonte",
|
||||
"unbindText": "Desvincular texto",
|
||||
"bindText": "Ligar o texto ao contedor",
|
||||
"link": {
|
||||
"edit": "Editar ligazón",
|
||||
"create": "Crear ligazón",
|
||||
"label": "Ligazón"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "Bloquear",
|
||||
"unlock": "Desbloquear",
|
||||
"lockAll": "Bloquear todo",
|
||||
"unlockAll": "Desbloquear todo"
|
||||
},
|
||||
"statusPublished": "Publicado",
|
||||
"sidebarLock": "Manter a barra lateral aberta"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Aínda non hai elementos engadidos...",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Limpar o lenzo",
|
||||
"exportJSON": "Exportar a arquivo",
|
||||
"exportImage": "Gardar como imaxe",
|
||||
"export": "Exportar",
|
||||
"exportToPng": "Exportar a PNG",
|
||||
"exportToSvg": "Exportar a SVG",
|
||||
"copyToClipboard": "Copiar ao portapapeis",
|
||||
"copyPngToClipboard": "Copiar PNG ao portapapeis",
|
||||
"scale": "Escala",
|
||||
"save": "Gardar no ficheiro actual",
|
||||
"saveAs": "Gardar como",
|
||||
"load": "Cargar",
|
||||
"getShareableLink": "Obter unha ligazón que se poida compartir",
|
||||
"close": "Pechar",
|
||||
"selectLanguage": "Seleccionar idioma",
|
||||
"scrollBackToContent": "Volver ao contido",
|
||||
"zoomIn": "Ampliar",
|
||||
"zoomOut": "Reducir",
|
||||
"resetZoom": "Reiniciar zoom",
|
||||
"menu": "Menú",
|
||||
"done": "Feito",
|
||||
"edit": "Editar",
|
||||
"undo": "Desfacer",
|
||||
"redo": "Refacer",
|
||||
"resetLibrary": "Reiniciar biblioteca",
|
||||
"createNewRoom": "Crear nova sala",
|
||||
"fullScreen": "Pantalla completa",
|
||||
"darkMode": "Modo escuro",
|
||||
"lightMode": "Modo claro",
|
||||
"zenMode": "Modo zen",
|
||||
"exitZenMode": "Saír do modo zen",
|
||||
"cancel": "Cancelar",
|
||||
"clear": "Limpar",
|
||||
"remove": "Eliminar",
|
||||
"publishLibrary": "Publicar",
|
||||
"submit": "Enviar",
|
||||
"confirm": "Confirmar"
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "Isto limpará todo o lenzo. Estás seguro?",
|
||||
"couldNotCreateShareableLink": "Non se puido crear unha ligazón para compartir.",
|
||||
"couldNotCreateShareableLinkTooBig": "Non se puido crear a ligazón para compartir: a escena é demasiado grande",
|
||||
"couldNotLoadInvalidFile": "Non se puido cargar o ficheiro non válido",
|
||||
"importBackendFailed": "",
|
||||
"cannotExportEmptyCanvas": "",
|
||||
"couldNotCopyToClipboard": "",
|
||||
"decryptFailed": "Non se poideron descifrar os datos.",
|
||||
"uploadedSecurly": "A carga foi asegurada con cifrado de extremo a extremo, o que significa que o servidor de Excalidraw e terceiros non poder ler o contenido.",
|
||||
"loadSceneOverridePrompt": "Si carga este debuxo externo, reemprazará o que ten. ¿Desexa continuar?",
|
||||
"collabStopOverridePrompt": "",
|
||||
"errorAddingToLibrary": "",
|
||||
"errorRemovingFromLibrary": "",
|
||||
"confirmAddLibrary": "",
|
||||
"imageDoesNotContainScene": "",
|
||||
"cannotRestoreFromImage": "",
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"invalidEncryptionKey": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "",
|
||||
"imageInsertError": "",
|
||||
"fileTooBig": "",
|
||||
"svgImageInsertError": "",
|
||||
"invalidSVGString": "",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": ""
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Selección",
|
||||
"image": "",
|
||||
"rectangle": "Rectángulo",
|
||||
"diamond": "Diamante",
|
||||
"ellipse": "Elipse",
|
||||
"arrow": "Frecha",
|
||||
"line": "Liña",
|
||||
"freedraw": "Debuxar",
|
||||
"text": "Texto",
|
||||
"library": "Biblioteca",
|
||||
"lock": "Manter a ferramenta seleccionada activa despois de debuxar",
|
||||
"penMode": "",
|
||||
"link": "",
|
||||
"eraser": ""
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Accións do lenzo",
|
||||
"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": ""
|
||||
},
|
||||
"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": "Erro"
|
||||
},
|
||||
"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": "",
|
||||
"editSelectedShape": "",
|
||||
"github": "",
|
||||
"howto": "",
|
||||
"or": "",
|
||||
"preventBinding": "",
|
||||
"tools": "",
|
||||
"shortcuts": "",
|
||||
"textFinish": "",
|
||||
"textNewLine": "",
|
||||
"title": "",
|
||||
"view": "",
|
||||
"zoomToFit": "",
|
||||
"zoomToSelection": "",
|
||||
"toggleElementLock": ""
|
||||
},
|
||||
"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": ""
|
||||
},
|
||||
"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": ""
|
||||
}
|
||||
}
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "לנעול הכל",
|
||||
"unlockAll": "שחרור הכול"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "אפס את הלוח",
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "सब ताले के अंदर रखे",
|
||||
"unlockAll": "सब ताले के बाहर निकाले"
|
||||
},
|
||||
"statusPublished": "प्रकाशित"
|
||||
"statusPublished": "प्रकाशित",
|
||||
"sidebarLock": "साइडबार खुला रखे."
|
||||
},
|
||||
"library": {
|
||||
"noItems": "अभी तक कोई आइटम जोडा नहीं गया.",
|
||||
"hint_emptyLibrary": "यहाँ जोड़ने के लिए पटल से एक वस्तु चुने, अथवा जन कोष से एक संग्रह नीचे स्थापित करें.",
|
||||
"hint_emptyPrivateLibrary": "यहाँ जोड़ने के लिए पटल से एक वस्तु चुने."
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "कैनवास रीसेट करें",
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Vászon törlése",
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "Kunci semua",
|
||||
"unlockAll": "Lepas semua"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "Telah terbit",
|
||||
"sidebarLock": "Biarkan sidebar tetap terbuka"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Belum ada item yang ditambahkan...",
|
||||
"hint_emptyLibrary": "Pilih item pada kanvas untuk menambahkan nya di sini, atau pasang pustaka dari gudang di bawah ini.",
|
||||
"hint_emptyPrivateLibrary": "Pilih item pada kanvas untuk menambahkan nya di sini."
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Setel Ulang Kanvas",
|
||||
@@ -190,7 +196,7 @@
|
||||
"svgImageInsertError": "Tidak dapat menyisipkan gambar SVG. Markup SVG sepertinya tidak valid.",
|
||||
"invalidSVGString": "SVG tidak valid.",
|
||||
"cannotResolveCollabServer": "Tidak dapat terhubung ke server kolab. Muat ulang laman dan coba lagi.",
|
||||
"importLibraryError": ""
|
||||
"importLibraryError": "Tidak dapat memuat pustaka"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Pilihan",
|
||||
@@ -343,7 +349,7 @@
|
||||
},
|
||||
"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",
|
||||
"republishWarning": ""
|
||||
"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": {
|
||||
"title": "Pustaka telah dikirm",
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "Blocca tutto",
|
||||
"unlockAll": "Sblocca tutto"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "Pubblicato",
|
||||
"sidebarLock": "Mantieni aperta la barra laterale"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Nessun elemento ancora aggiunto...",
|
||||
"hint_emptyLibrary": "Selezionare un elemento su tela per aggiungerlo qui, o installare una libreria dal repository pubblico, sotto.",
|
||||
"hint_emptyPrivateLibrary": "Selezionare un elemento su tela per aggiungerlo qui."
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Svuota la tela",
|
||||
@@ -343,7 +349,7 @@
|
||||
},
|
||||
"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",
|
||||
"republishWarning": ""
|
||||
"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": {
|
||||
"title": "Libreria inviata",
|
||||
|
@@ -9,7 +9,7 @@
|
||||
"copy": "コピー",
|
||||
"copyAsPng": "PNGとしてクリップボードへコピー",
|
||||
"copyAsSvg": "SVGとしてクリップボードへコピー",
|
||||
"copyText": "",
|
||||
"copyText": "テキストとしてクリップボードにコピー",
|
||||
"bringForward": "前面に移動",
|
||||
"sendToBack": "最背面に移動",
|
||||
"bringToFront": "最前面に移動",
|
||||
@@ -120,7 +120,13 @@
|
||||
"lockAll": "すべてロック",
|
||||
"unlockAll": "すべてのロックを解除"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "公開済み",
|
||||
"sidebarLock": "サイドバーを開いたままにする"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "まだアイテムが追加されていません…",
|
||||
"hint_emptyLibrary": "キャンバス上のアイテムを選択してここに追加するか、以下の公開リポジトリからライブラリをインストールしてください。",
|
||||
"hint_emptyPrivateLibrary": "キャンバス上のアイテムを選択すると、ここに追加されます。"
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "キャンバスのリセット",
|
||||
@@ -168,7 +174,7 @@
|
||||
"couldNotLoadInvalidFile": "無効なファイルを読み込めませんでした。",
|
||||
"importBackendFailed": "サーバーからの読み込みに失敗しました。",
|
||||
"cannotExportEmptyCanvas": "空のキャンバスはエクスポートできません。",
|
||||
"couldNotCopyToClipboard": "",
|
||||
"couldNotCopyToClipboard": "クリップボードにコピーできませんでした。",
|
||||
"decryptFailed": "データを復号できませんでした。",
|
||||
"uploadedSecurly": "データのアップロードはエンドツーエンド暗号化によって保護されています。Excalidrawサーバーと第三者はデータの内容を見ることができません。",
|
||||
"loadSceneOverridePrompt": "外部図面を読み込むと、既存のコンテンツが置き換わります。続行しますか?",
|
||||
@@ -190,7 +196,7 @@
|
||||
"svgImageInsertError": "SVGイメージを挿入できませんでした。SVGマークアップは無効に見えます。",
|
||||
"invalidSVGString": "無効なSVGです。",
|
||||
"cannotResolveCollabServer": "コラボレーションサーバに接続できませんでした。ページを再読み込みして、もう一度お試しください。",
|
||||
"importLibraryError": ""
|
||||
"importLibraryError": "ライブラリを読み込めませんでした。"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "選択",
|
||||
@@ -231,7 +237,7 @@
|
||||
"placeImage": "クリックして画像を配置するか、クリックしてドラッグしてサイズを手動で設定します",
|
||||
"publishLibrary": "自分のライブラリを公開",
|
||||
"bindTextToElement": "Enterを押してテキストを追加",
|
||||
"deepBoxSelect": "CtrlOrCmd を押し続けることでドラッグを抑止し、深い選択を行う",
|
||||
"deepBoxSelect": "CtrlOrCmd を押し続けることでドラッグを抑止し、深い選択を行います",
|
||||
"eraserRevert": "Alt を押し続けることで削除マークされた要素を元に戻す"
|
||||
},
|
||||
"canvasError": {
|
||||
@@ -300,7 +306,7 @@
|
||||
"view": "表示",
|
||||
"zoomToFit": "すべての要素が収まるようにズーム",
|
||||
"zoomToSelection": "選択要素にズーム",
|
||||
"toggleElementLock": ""
|
||||
"toggleElementLock": "選択したアイテムをロック/ロック解除"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": "キャンバスを消去"
|
||||
@@ -343,7 +349,7 @@
|
||||
},
|
||||
"noteItems": "各ライブラリ項目は、フィルタリングのために独自の名前を持つ必要があります。以下のライブラリアイテムが含まれます:",
|
||||
"atleastOneLibItem": "開始するには少なくとも1つのライブラリ項目を選択してください",
|
||||
"republishWarning": ""
|
||||
"republishWarning": "注意: 選択された項目の中には、すでに公開/投稿済みと表示されているものがあります。既存のライブラリや投稿を更新する場合のみ、アイテムを再投稿してください。"
|
||||
},
|
||||
"publishSuccessDialog": {
|
||||
"title": "ライブラリを送信しました",
|
||||
|
@@ -62,7 +62,7 @@
|
||||
"extraBold": "Azuran aṭas",
|
||||
"architect": "Amasdag",
|
||||
"artist": "Anaẓur",
|
||||
"cartoonist": "",
|
||||
"cartoonist": "Amefɣul",
|
||||
"fileTitle": "Isem n ufaylu",
|
||||
"colorPicker": "Amafran n yini",
|
||||
"canvasColors": "Yettwaseqdec di teɣzut n usuneɣ",
|
||||
@@ -107,20 +107,26 @@
|
||||
"excalidrawLib": "Tamkarḍit n Excalidraw",
|
||||
"decreaseFontSize": "Senqes tiddi n tsefsit",
|
||||
"increaseFontSize": "Sali tiddi n tsefsit",
|
||||
"unbindText": "",
|
||||
"bindText": "",
|
||||
"unbindText": "Serreḥ iweḍris",
|
||||
"bindText": "Arez aḍris s anagbar",
|
||||
"link": {
|
||||
"edit": "Ẓreg aseɣwen",
|
||||
"create": "Snulfu-d aseɣwen",
|
||||
"label": "Aseɣwen"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "",
|
||||
"unlock": "",
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
"lock": "Sekkeṛ",
|
||||
"unlock": "Serreḥ",
|
||||
"lockAll": "Sekkeṛ akk",
|
||||
"unlockAll": "Serreḥ akk"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "Yeffeɣ-d",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Ales awennez n teɣzut n usuneɣ",
|
||||
@@ -190,7 +196,7 @@
|
||||
"svgImageInsertError": "D awezɣi tugra n tugna SVG. Acraḍ SVG yettban-d d armeɣtu.",
|
||||
"invalidSVGString": "SVG armeɣtu.",
|
||||
"cannotResolveCollabServer": "Ulamek tuqqna s aqeddac n umyalel. Ma ulac uɣilif ales asali n usebter sakin eɛreḍ tikkelt-nniḍen.",
|
||||
"importLibraryError": ""
|
||||
"importLibraryError": "Ur d-ssalay ara tamkarḍit"
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Tafrayt",
|
||||
@@ -333,7 +339,7 @@
|
||||
},
|
||||
"noteGuidelines": {
|
||||
"pre": "Tamkarḍit teḥwaǧ ad tettwaqbel s ufus qbel. Ma ulac uɣilif ɣer ",
|
||||
"link": "",
|
||||
"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": {
|
||||
@@ -343,7 +349,7 @@
|
||||
},
|
||||
"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ḍ",
|
||||
"republishWarning": ""
|
||||
"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": {
|
||||
"title": "Tamkarḍit tettwazen",
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "",
|
||||
|
@@ -65,13 +65,13 @@
|
||||
"cartoonist": "만화가",
|
||||
"fileTitle": "파일 이름",
|
||||
"colorPicker": "색상 선택기",
|
||||
"canvasColors": "",
|
||||
"canvasColors": "캔버스에서 사용되었음",
|
||||
"canvasBackground": "캔버스 배경",
|
||||
"drawingCanvas": "캔버스 그리기",
|
||||
"layers": "레이어",
|
||||
"actions": "동작",
|
||||
"language": "언어",
|
||||
"liveCollaboration": "라이브 협력",
|
||||
"liveCollaboration": "실시간 협업",
|
||||
"duplicateSelection": "복제",
|
||||
"untitled": "제목 없음",
|
||||
"name": "이름",
|
||||
@@ -120,7 +120,13 @@
|
||||
"lockAll": "모두 잠금",
|
||||
"unlockAll": "모두 잠금 해제"
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "게시됨",
|
||||
"sidebarLock": "사이드바 유지"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "추가된 아이템 없음",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "캔버스 초기화",
|
||||
@@ -204,7 +210,7 @@
|
||||
"text": "텍스트",
|
||||
"library": "라이브러리",
|
||||
"lock": "선택된 도구 유지하기",
|
||||
"penMode": "",
|
||||
"penMode": "핀치 줌을 하지 않도록 하고 펜을 통해서 자유롭게 그리기",
|
||||
"link": "선택한 도형에 대해서 링크를 추가/업데이트",
|
||||
"eraser": "지우개"
|
||||
},
|
||||
@@ -231,7 +237,7 @@
|
||||
"placeImage": "클릭해서 이미지를 배치하거나, 클릭하고 드래그해서 사이즈를 조정하기",
|
||||
"publishLibrary": "당신만의 라이브러리를 게시하기",
|
||||
"bindTextToElement": "Enter 키를 눌러서 텍스트 추가하기",
|
||||
"deepBoxSelect": "",
|
||||
"deepBoxSelect": "CtrlOrCmd 키를 눌러서 깊게 선택하고, 드래그하지 않도록 하기",
|
||||
"eraserRevert": "Alt를 눌러서 삭제하도록 지정된 요소를 되돌리기"
|
||||
},
|
||||
"canvasError": {
|
||||
@@ -279,8 +285,8 @@
|
||||
"helpDialog": {
|
||||
"blog": "블로그 읽어보기",
|
||||
"click": "클릭",
|
||||
"deepSelect": "",
|
||||
"deepBoxSelect": "",
|
||||
"deepSelect": "깊게 선택",
|
||||
"deepBoxSelect": "영역을 깊게 선택하고, 드래그하지 않도록 하기",
|
||||
"curvedArrow": "곡선 화살표",
|
||||
"curvedLine": "곡선",
|
||||
"documentation": "설명서",
|
||||
@@ -343,7 +349,7 @@
|
||||
},
|
||||
"noteItems": "각각의 라이브러리는 분류할 수 있도록 고유한 이름을 가져야 합니다. 다음의 라이브러리 항목이 포함됩니다:",
|
||||
"atleastOneLibItem": "최소한 하나의 라이브러리를 선택해주세요",
|
||||
"republishWarning": ""
|
||||
"republishWarning": "참고: 선택된 항목의 일부는 이미 제출/게시되었습니다. 기존의 라이브러리나 제출물을 업데이트하는 경우에만 제출하세요."
|
||||
},
|
||||
"publishSuccessDialog": {
|
||||
"title": "라이브러리 제출됨",
|
||||
|
@@ -120,7 +120,13 @@
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
},
|
||||
"statusPublished": ""
|
||||
"statusPublished": "",
|
||||
"sidebarLock": ""
|
||||
},
|
||||
"library": {
|
||||
"noItems": "",
|
||||
"hint_emptyLibrary": "",
|
||||
"hint_emptyPrivateLibrary": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user