mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-10-24 00:14:25 +02:00
Compare commits
113 Commits
v0.12.0
...
zsviczian-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e68e11ff9f | ||
![]() |
b4750f4485 | ||
![]() |
4426275184 | ||
![]() |
2b4462c941 | ||
![]() |
43b13d8e3a | ||
![]() |
720f468f39 | ||
![]() |
33300d19f6 | ||
![]() |
5aed159991 | ||
![]() |
de1d221d1c | ||
![]() |
9a68dbffe2 | ||
![]() |
32d82219b1 | ||
![]() |
ba2c86fe1b | ||
![]() |
f1ae37c84b | ||
![]() |
ec350ba8b2 | ||
![]() |
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
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Checkout repository
|
||||||
- uses: docker/build-push-action@v2
|
uses: actions/checkout@v3
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: excalidraw/excalidraw
|
- name: Build and push
|
||||||
tag_with_ref: true
|
uses: docker/build-push-action@v3
|
||||||
tag_with_sha: true
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: excalidraw/excalidraw:latest
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,7 +19,6 @@ logs
|
|||||||
node_modules
|
node_modules
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
package-lock.json
|
package-lock.json
|
||||||
static
|
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
src/packages/excalidraw/types
|
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
20
package.json
20
package.json
@@ -23,17 +23,17 @@
|
|||||||
"@sentry/integrations": "6.2.5",
|
"@sentry/integrations": "6.2.5",
|
||||||
"@testing-library/jest-dom": "5.16.2",
|
"@testing-library/jest-dom": "5.16.2",
|
||||||
"@testing-library/react": "12.1.5",
|
"@testing-library/react": "12.1.5",
|
||||||
"@tldraw/vec": "1.4.3",
|
"@tldraw/vec": "1.7.1",
|
||||||
"@types/jest": "27.4.0",
|
"@types/jest": "27.4.0",
|
||||||
"@types/pica": "5.1.3",
|
"@types/pica": "5.1.3",
|
||||||
"@types/react": "17.0.39",
|
"@types/react": "18.0.15",
|
||||||
"@types/react-dom": "17.0.11",
|
"@types/react-dom": "18.0.6",
|
||||||
"@types/socket.io-client": "1.4.36",
|
"@types/socket.io-client": "1.4.36",
|
||||||
"browser-fs-access": "0.29.1",
|
"browser-fs-access": "0.29.1",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"fake-indexeddb": "3.1.7",
|
"fake-indexeddb": "3.1.7",
|
||||||
"firebase": "8.3.3",
|
"firebase": "8.3.3",
|
||||||
"i18next-browser-languagedetector": "6.1.2",
|
"i18next-browser-languagedetector": "6.1.4",
|
||||||
"idb-keyval": "6.0.3",
|
"idb-keyval": "6.0.3",
|
||||||
"image-blob-reduce": "3.0.1",
|
"image-blob-reduce": "3.0.1",
|
||||||
"jotai": "1.6.4",
|
"jotai": "1.6.4",
|
||||||
@@ -47,8 +47,8 @@
|
|||||||
"png-chunks-extract": "1.0.0",
|
"png-chunks-extract": "1.0.0",
|
||||||
"points-on-curve": "0.2.0",
|
"points-on-curve": "0.2.0",
|
||||||
"pwacompat": "2.0.17",
|
"pwacompat": "2.0.17",
|
||||||
"react": "17.0.2",
|
"react": "18.2.0",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "18.2.0",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"roughjs": "4.5.2",
|
"roughjs": "4.5.2",
|
||||||
"sass": "1.51.0",
|
"sass": "1.51.0",
|
||||||
@@ -59,11 +59,11 @@
|
|||||||
"@excalidraw/eslint-config": "1.0.0",
|
"@excalidraw/eslint-config": "1.0.0",
|
||||||
"@excalidraw/prettier-config": "1.0.2",
|
"@excalidraw/prettier-config": "1.0.2",
|
||||||
"@types/chai": "4.3.0",
|
"@types/chai": "4.3.0",
|
||||||
"@types/lodash.throttle": "4.1.6",
|
"@types/lodash.throttle": "4.1.7",
|
||||||
"@types/pako": "1.0.3",
|
"@types/pako": "1.0.3",
|
||||||
"@types/resize-observer-browser": "0.1.6",
|
"@types/resize-observer-browser": "0.1.7",
|
||||||
"chai": "4.3.6",
|
"chai": "4.3.6",
|
||||||
"dotenv": "10.0.0",
|
"dotenv": "16.0.1",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
"eslint-plugin-prettier": "3.3.1",
|
"eslint-plugin-prettier": "3.3.1",
|
||||||
"husky": "7.0.4",
|
"husky": "7.0.4",
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
"lint-staged": "12.3.7",
|
"lint-staged": "12.3.7",
|
||||||
"pepjs": "0.5.3",
|
"pepjs": "0.5.3",
|
||||||
"prettier": "2.6.2",
|
"prettier": "2.6.2",
|
||||||
"rewire": "5.0.0"
|
"rewire": "6.0.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@typescript-eslint/typescript-estree": "5.10.2"
|
"@typescript-eslint/typescript-estree": "5.10.2"
|
||||||
|
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",
|
"ru-RU": "en-ru",
|
||||||
"si-LK": "en-silk",
|
"si-LK": "en-silk",
|
||||||
"sk-SK": "en-sk",
|
"sk-SK": "en-sk",
|
||||||
|
"sl-SI": "en-sl",
|
||||||
"sv-SE": "en-sv",
|
"sv-SE": "en-sv",
|
||||||
"ta-IN": "en-ta",
|
"ta-IN": "en-ta",
|
||||||
"tr-TR": "en-tr",
|
"tr-TR": "en-tr",
|
||||||
@@ -47,6 +48,8 @@ const crowdinMap = {
|
|||||||
"lv-LV": "en-lv",
|
"lv-LV": "en-lv",
|
||||||
"cs-CZ": "en-cs",
|
"cs-CZ": "en-cs",
|
||||||
"kk-KZ": "en-kk",
|
"kk-KZ": "en-kk",
|
||||||
|
"vi-vn": "en-vi",
|
||||||
|
"mr-in": "en-mr",
|
||||||
};
|
};
|
||||||
|
|
||||||
const flags = {
|
const flags = {
|
||||||
@@ -86,6 +89,7 @@ const flags = {
|
|||||||
"ru-RU": "🇷🇺",
|
"ru-RU": "🇷🇺",
|
||||||
"si-LK": "🇱🇰",
|
"si-LK": "🇱🇰",
|
||||||
"sk-SK": "🇸🇰",
|
"sk-SK": "🇸🇰",
|
||||||
|
"sl-SI": "🇸🇮",
|
||||||
"sv-SE": "🇸🇪",
|
"sv-SE": "🇸🇪",
|
||||||
"ta-IN": "🇮🇳",
|
"ta-IN": "🇮🇳",
|
||||||
"tr-TR": "🇹🇷",
|
"tr-TR": "🇹🇷",
|
||||||
@@ -93,6 +97,9 @@ const flags = {
|
|||||||
"zh-CN": "🇨🇳",
|
"zh-CN": "🇨🇳",
|
||||||
"zh-HK": "🇭🇰",
|
"zh-HK": "🇭🇰",
|
||||||
"zh-TW": "🇹🇼",
|
"zh-TW": "🇹🇼",
|
||||||
|
"eu-ES": "🇪🇦",
|
||||||
|
"vi-VN": "🇻🇳",
|
||||||
|
"mr-IN": "🇮🇳",
|
||||||
};
|
};
|
||||||
|
|
||||||
const languages = {
|
const languages = {
|
||||||
@@ -133,6 +140,7 @@ const languages = {
|
|||||||
"ru-RU": "Русский",
|
"ru-RU": "Русский",
|
||||||
"si-LK": "සිංහල",
|
"si-LK": "සිංහල",
|
||||||
"sk-SK": "Slovenčina",
|
"sk-SK": "Slovenčina",
|
||||||
|
"sl-SI": "Slovenščina",
|
||||||
"sv-SE": "Svenska",
|
"sv-SE": "Svenska",
|
||||||
"ta-IN": "Tamil",
|
"ta-IN": "Tamil",
|
||||||
"tr-TR": "Türkçe",
|
"tr-TR": "Türkçe",
|
||||||
@@ -140,6 +148,8 @@ const languages = {
|
|||||||
"zh-CN": "简体中文",
|
"zh-CN": "简体中文",
|
||||||
"zh-HK": "繁體中文 (香港)",
|
"zh-HK": "繁體中文 (香港)",
|
||||||
"zh-TW": "繁體中文",
|
"zh-TW": "繁體中文",
|
||||||
|
"vi-VN": "Tiếng Việt",
|
||||||
|
"mr-IN": "मराठी",
|
||||||
};
|
};
|
||||||
|
|
||||||
const percentages = fs.readFileSync(
|
const percentages = fs.readFileSync(
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
// for development purposes we want to have the service-worker.js file
|
// 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
|
// accessible from the public folder. On build though, we need to compile it
|
||||||
// and CRA expects that file to be in src/ folder.
|
// and CRA expects that file to be in src/ folder.
|
||||||
const moveServiceWorkerScript = () => {
|
const moveServiceWorkerScript = () => {
|
||||||
const oldPath = "./public/service-worker.js";
|
const oldPath = path.resolve(__dirname, "../public/service-worker.js");
|
||||||
const newPath = "./src/service-worker.js";
|
const newPath = path.resolve(__dirname, "../src/service-worker.js");
|
||||||
|
|
||||||
fs.rename(oldPath, newPath, (error) => {
|
fs.rename(oldPath, newPath, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@@ -42,7 +42,7 @@ export const actionAddToLibrary = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
toastMessage: t("toast.addedToLibrary"),
|
toast: { message: t("toast.addedToLibrary") },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
@@ -107,14 +107,16 @@ export const actionCopyAsPng = register({
|
|||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
toastMessage: t("toast.copyToClipboardAsPng", {
|
toast: {
|
||||||
exportSelection: selectedElements.length
|
message: t("toast.copyToClipboardAsPng", {
|
||||||
? t("toast.selection")
|
exportSelection: selectedElements.length
|
||||||
: t("toast.canvas"),
|
? t("toast.selection")
|
||||||
exportColorScheme: appState.exportWithDarkMode
|
: t("toast.canvas"),
|
||||||
? t("buttons.darkMode")
|
exportColorScheme: appState.exportWithDarkMode
|
||||||
: t("buttons.lightMode"),
|
? t("buttons.darkMode")
|
||||||
}),
|
: t("buttons.lightMode"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
|
@@ -128,12 +128,15 @@ const duplicateElements = (
|
|||||||
{
|
{
|
||||||
...appState,
|
...appState,
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
selectedElementIds: newElements.reduce((acc, element) => {
|
selectedElementIds: newElements.reduce(
|
||||||
if (!isBoundToContainer(element)) {
|
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||||
acc[element.id] = true;
|
if (!isBoundToContainer(element)) {
|
||||||
}
|
acc[element.id] = true;
|
||||||
return acc;
|
}
|
||||||
}, {} as any),
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getNonDeletedElements(finalElements),
|
getNonDeletedElements(finalElements),
|
||||||
),
|
),
|
||||||
|
@@ -144,13 +144,15 @@ export const actionSaveToActiveFile = register({
|
|||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
fileHandle,
|
fileHandle,
|
||||||
toastMessage: fileHandleExists
|
toast: fileHandleExists
|
||||||
? fileHandle?.name
|
? {
|
||||||
? t("toast.fileSavedToFilename").replace(
|
message: fileHandle?.name
|
||||||
"{filename}",
|
? t("toast.fileSavedToFilename").replace(
|
||||||
`"${fileHandle.name}"`,
|
"{filename}",
|
||||||
)
|
`"${fileHandle.name}"`,
|
||||||
: t("toast.fileSaved")
|
)
|
||||||
|
: t("toast.fileSaved"),
|
||||||
|
}
|
||||||
: null,
|
: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -242,7 +244,7 @@ export const actionLoadScene = register({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
|
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
|
||||||
PanelComponent: ({ updateData, appState }) => (
|
PanelComponent: ({ updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={load}
|
icon={load}
|
||||||
|
@@ -13,7 +13,7 @@ import {
|
|||||||
maybeBindLinearElement,
|
maybeBindLinearElement,
|
||||||
bindOrUnbindLinearElement,
|
bindOrUnbindLinearElement,
|
||||||
} from "../element/binding";
|
} from "../element/binding";
|
||||||
import { isBindingElement } from "../element/typeChecks";
|
import { isBindingElement, isLinearElement } from "../element/typeChecks";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
|
|
||||||
export const actionFinalize = register({
|
export const actionFinalize = register({
|
||||||
@@ -181,6 +181,11 @@ export const actionFinalize = register({
|
|||||||
[multiPointElement.id]: true,
|
[multiPointElement.id]: true,
|
||||||
}
|
}
|
||||||
: appState.selectedElementIds,
|
: 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,
|
pendingImageElementId: null,
|
||||||
},
|
},
|
||||||
commitToHistory: appState.activeTool.type === "freedraw",
|
commitToHistory: appState.activeTool.type === "freedraw",
|
||||||
|
@@ -2,29 +2,43 @@ import { KEYS } from "../keys";
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { selectGroupsForSelectedElements } from "../groups";
|
import { selectGroupsForSelectedElements } from "../groups";
|
||||||
import { getNonDeletedElements, isTextElement } from "../element";
|
import { getNonDeletedElements, isTextElement } from "../element";
|
||||||
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
import { isLinearElement } from "../element/typeChecks";
|
||||||
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
|
||||||
export const actionSelectAll = register({
|
export const actionSelectAll = register({
|
||||||
name: "selectAll",
|
name: "selectAll",
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, value, app) => {
|
||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
return false;
|
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 {
|
return {
|
||||||
appState: selectGroupsForSelectedElements(
|
appState: selectGroupsForSelectedElements(
|
||||||
{
|
{
|
||||||
...appState,
|
...appState,
|
||||||
|
selectedLinearElement:
|
||||||
|
// single linear element selected
|
||||||
|
Object.keys(selectedElementIds).length === 1 &&
|
||||||
|
isLinearElement(elements[0])
|
||||||
|
? new LinearElementEditor(elements[0], app.scene)
|
||||||
|
: null,
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
selectedElementIds: elements.reduce((map, element) => {
|
selectedElementIds,
|
||||||
if (
|
|
||||||
!element.isDeleted &&
|
|
||||||
!(isTextElement(element) && element.containerId) &&
|
|
||||||
element.locked === false
|
|
||||||
) {
|
|
||||||
map[element.id] = true;
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}, {} as any),
|
|
||||||
},
|
},
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
),
|
),
|
||||||
|
@@ -36,7 +36,7 @@ export const actionCopyStyles = register({
|
|||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
toastMessage: t("toast.copyStyles"),
|
toast: { message: t("toast.copyStyles") },
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
|
@@ -17,16 +17,19 @@ export const actionToggleLock = register({
|
|||||||
|
|
||||||
const operation = getOperation(selectedElements);
|
const operation = getOperation(selectedElements);
|
||||||
const selectedElementsMap = arrayToMap(selectedElements);
|
const selectedElementsMap = arrayToMap(selectedElements);
|
||||||
|
const lock = operation === "lock";
|
||||||
return {
|
return {
|
||||||
elements: elements.map((element) => {
|
elements: elements.map((element) => {
|
||||||
if (!selectedElementsMap.has(element.id)) {
|
if (!selectedElementsMap.has(element.id)) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
return newElementWith(element, { locked: operation === "lock" });
|
return newElementWith(element, { locked: lock });
|
||||||
}),
|
}),
|
||||||
appState,
|
appState: {
|
||||||
|
...appState,
|
||||||
|
selectedLinearElement: lock ? null : appState.selectedLinearElement,
|
||||||
|
},
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@@ -147,6 +147,7 @@ export class ActionManager {
|
|||||||
) {
|
) {
|
||||||
const action = this.actions[name];
|
const action = this.actions[name];
|
||||||
const PanelComponent = action.PanelComponent!;
|
const PanelComponent = action.PanelComponent!;
|
||||||
|
PanelComponent.displayName = "PanelComponent";
|
||||||
const elements = this.getElementsIncludingDeleted();
|
const elements = this.getElementsIncludingDeleted();
|
||||||
const appState = this.getAppState();
|
const appState = this.getAppState();
|
||||||
const updateData = (formState?: any) => {
|
const updateData = (formState?: any) => {
|
||||||
|
@@ -81,7 +81,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
showStats: false,
|
showStats: false,
|
||||||
startBoundElement: null,
|
startBoundElement: null,
|
||||||
suggestedBindings: [],
|
suggestedBindings: [],
|
||||||
toastMessage: null,
|
toast: null,
|
||||||
viewBackgroundColor: oc.white,
|
viewBackgroundColor: oc.white,
|
||||||
zenModeEnabled: false,
|
zenModeEnabled: false,
|
||||||
zoom: {
|
zoom: {
|
||||||
@@ -90,6 +90,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
viewModeEnabled: false,
|
viewModeEnabled: false,
|
||||||
pendingImageElementId: null,
|
pendingImageElementId: null,
|
||||||
showHyperlinkPopup: false,
|
showHyperlinkPopup: false,
|
||||||
|
selectedLinearElement: null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -173,7 +174,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
showStats: { browser: true, export: false, server: false },
|
showStats: { browser: true, export: false, server: false },
|
||||||
startBoundElement: { browser: false, export: false, server: false },
|
startBoundElement: { browser: false, export: false, server: false },
|
||||||
suggestedBindings: { 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 },
|
viewBackgroundColor: { browser: true, export: true, server: true },
|
||||||
width: { browser: false, export: false, server: false },
|
width: { browser: false, export: false, server: false },
|
||||||
zenModeEnabled: { browser: true, 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 },
|
viewModeEnabled: { browser: false, export: false, server: false },
|
||||||
pendingImageElementId: { browser: false, export: false, server: false },
|
pendingImageElementId: { browser: false, export: false, server: false },
|
||||||
showHyperlinkPopup: { browser: false, export: false, server: false },
|
showHyperlinkPopup: { browser: false, export: false, server: false },
|
||||||
|
selectedLinearElement: { browser: true, export: false, server: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const _clearAppStateForStorage = <
|
const _clearAppStateForStorage = <
|
||||||
|
@@ -26,17 +26,17 @@ import { ToolButton } from "./ToolButton";
|
|||||||
import { hasStrokeColor } from "../scene/comparisons";
|
import { hasStrokeColor } from "../scene/comparisons";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks";
|
import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { actionToggleZenMode } from "../actions";
|
||||||
|
|
||||||
export const SelectedShapeActions = ({
|
export const SelectedShapeActions = ({
|
||||||
appState,
|
appState,
|
||||||
elements,
|
elements,
|
||||||
renderAction,
|
renderAction,
|
||||||
activeTool,
|
|
||||||
}: {
|
}: {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
renderAction: ActionManager["renderAction"];
|
renderAction: ActionManager["renderAction"];
|
||||||
activeTool: AppState["activeTool"]["type"];
|
|
||||||
}) => {
|
}) => {
|
||||||
const targetElements = getTargetElements(
|
const targetElements = getTargetElements(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
@@ -56,13 +56,13 @@ export const SelectedShapeActions = ({
|
|||||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||||
|
|
||||||
const showFillIcons =
|
const showFillIcons =
|
||||||
hasBackground(activeTool) ||
|
hasBackground(appState.activeTool.type) ||
|
||||||
targetElements.some(
|
targetElements.some(
|
||||||
(element) =>
|
(element) =>
|
||||||
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
||||||
);
|
);
|
||||||
const showChangeBackgroundIcons =
|
const showChangeBackgroundIcons =
|
||||||
hasBackground(activeTool) ||
|
hasBackground(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => hasBackground(element.type));
|
targetElements.some((element) => hasBackground(element.type));
|
||||||
|
|
||||||
const showLinkIcon =
|
const showLinkIcon =
|
||||||
@@ -79,23 +79,23 @@ export const SelectedShapeActions = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panelColumn">
|
<div className="panelColumn">
|
||||||
{((hasStrokeColor(activeTool) &&
|
{((hasStrokeColor(appState.activeTool.type) &&
|
||||||
activeTool !== "image" &&
|
appState.activeTool.type !== "image" &&
|
||||||
commonSelectedType !== "image") ||
|
commonSelectedType !== "image") ||
|
||||||
targetElements.some((element) => hasStrokeColor(element.type))) &&
|
targetElements.some((element) => hasStrokeColor(element.type))) &&
|
||||||
renderAction("changeStrokeColor")}
|
renderAction("changeStrokeColor")}
|
||||||
{showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
|
{showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
|
||||||
{showFillIcons && renderAction("changeFillStyle")}
|
{showFillIcons && renderAction("changeFillStyle")}
|
||||||
|
|
||||||
{(hasStrokeWidth(activeTool) ||
|
{(hasStrokeWidth(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => hasStrokeWidth(element.type))) &&
|
targetElements.some((element) => hasStrokeWidth(element.type))) &&
|
||||||
renderAction("changeStrokeWidth")}
|
renderAction("changeStrokeWidth")}
|
||||||
|
|
||||||
{(activeTool === "freedraw" ||
|
{(appState.activeTool.type === "freedraw" ||
|
||||||
targetElements.some((element) => element.type === "freedraw")) &&
|
targetElements.some((element) => element.type === "freedraw")) &&
|
||||||
renderAction("changeStrokeShape")}
|
renderAction("changeStrokeShape")}
|
||||||
|
|
||||||
{(hasStrokeStyle(activeTool) ||
|
{(hasStrokeStyle(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => hasStrokeStyle(element.type))) && (
|
targetElements.some((element) => hasStrokeStyle(element.type))) && (
|
||||||
<>
|
<>
|
||||||
{renderAction("changeStrokeStyle")}
|
{renderAction("changeStrokeStyle")}
|
||||||
@@ -103,12 +103,12 @@ export const SelectedShapeActions = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(canChangeSharpness(activeTool) ||
|
{(canChangeSharpness(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => canChangeSharpness(element.type))) && (
|
targetElements.some((element) => canChangeSharpness(element.type))) && (
|
||||||
<>{renderAction("changeSharpness")}</>
|
<>{renderAction("changeSharpness")}</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(hasText(activeTool) ||
|
{(hasText(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => hasText(element.type))) && (
|
targetElements.some((element) => hasText(element.type))) && (
|
||||||
<>
|
<>
|
||||||
{renderAction("changeFontSize")}
|
{renderAction("changeFontSize")}
|
||||||
@@ -123,7 +123,7 @@ export const SelectedShapeActions = ({
|
|||||||
(element) =>
|
(element) =>
|
||||||
hasBoundTextElement(element) || isBoundToContainer(element),
|
hasBoundTextElement(element) || isBoundToContainer(element),
|
||||||
) && renderAction("changeVerticalAlign")}
|
) && renderAction("changeVerticalAlign")}
|
||||||
{(canHaveArrowheads(activeTool) ||
|
{(canHaveArrowheads(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => canHaveArrowheads(element.type))) && (
|
targetElements.some((element) => canHaveArrowheads(element.type))) && (
|
||||||
<>{renderAction("changeArrowhead")}</>
|
<>{renderAction("changeArrowhead")}</>
|
||||||
)}
|
)}
|
||||||
@@ -271,3 +271,45 @@ export const ZoomActions = ({
|
|||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const UndoRedoActions = ({
|
||||||
|
renderAction,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
renderAction: ActionManager["renderAction"];
|
||||||
|
className?: string;
|
||||||
|
}) => (
|
||||||
|
<div className={`undo-redo-buttons ${className}`}>
|
||||||
|
{renderAction("undo", { size: "small" })}
|
||||||
|
{renderAction("redo", { size: "small" })}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ExitZenModeAction = ({
|
||||||
|
executeAction,
|
||||||
|
showExitZenModeBtn,
|
||||||
|
}: {
|
||||||
|
executeAction: ActionManager["executeAction"];
|
||||||
|
showExitZenModeBtn: boolean;
|
||||||
|
}) => (
|
||||||
|
<button
|
||||||
|
className={clsx("disable-zen-mode", {
|
||||||
|
"disable-zen-mode--visible": showExitZenModeBtn,
|
||||||
|
})}
|
||||||
|
onClick={() => executeAction(actionToggleZenMode)}
|
||||||
|
>
|
||||||
|
{t("buttons.exitZenMode")}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FinalizeAction = ({
|
||||||
|
renderAction,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
renderAction: ActionManager["renderAction"];
|
||||||
|
className?: string;
|
||||||
|
}) => (
|
||||||
|
<div className={`finalize-button ${className}`}>
|
||||||
|
{renderAction("finalize", { size: "small" })}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import "./Card.scss";
|
|||||||
|
|
||||||
export const Card: React.FC<{
|
export const Card: React.FC<{
|
||||||
color: keyof OpenColor | "primary";
|
color: keyof OpenColor | "primary";
|
||||||
|
children?: React.ReactNode;
|
||||||
}> = ({ children, color }) => {
|
}> = ({ children, color }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@@ -8,6 +8,7 @@ export const CheckboxItem: React.FC<{
|
|||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange: (checked: boolean, event: React.MouseEvent) => void;
|
onChange: (checked: boolean, event: React.MouseEvent) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
}> = ({ children, checked, onChange, className }) => {
|
}> = ({ children, checked, onChange, className }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@@ -343,6 +343,8 @@ const ColorInput = React.forwardRef(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColorInput.displayName = "ColorInput";
|
||||||
|
|
||||||
export const ColorPicker = ({
|
export const ColorPicker = ({
|
||||||
type,
|
type,
|
||||||
color,
|
color,
|
||||||
|
@@ -85,6 +85,7 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
<button
|
<button
|
||||||
className="Modal__close"
|
className="Modal__close"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
title={t("buttons.close")}
|
||||||
aria-label={t("buttons.close")}
|
aria-label={t("buttons.close")}
|
||||||
>
|
>
|
||||||
{useDevice().isMobile ? back : close}
|
{useDevice().isMobile ? back : close}
|
||||||
|
99
src/components/Footer.tsx
Normal file
99
src/components/Footer.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { ActionManager } from "../actions/manager";
|
||||||
|
import { AppState, ExcalidrawProps } from "../types";
|
||||||
|
import {
|
||||||
|
ExitZenModeAction,
|
||||||
|
FinalizeAction,
|
||||||
|
UndoRedoActions,
|
||||||
|
ZoomActions,
|
||||||
|
} from "./Actions";
|
||||||
|
import { useDevice } from "./App";
|
||||||
|
import { Island } from "./Island";
|
||||||
|
import { Section } from "./Section";
|
||||||
|
import Stack from "./Stack";
|
||||||
|
|
||||||
|
const Footer = ({
|
||||||
|
appState,
|
||||||
|
actionManager,
|
||||||
|
renderCustomFooter,
|
||||||
|
showExitZenModeBtn,
|
||||||
|
}: {
|
||||||
|
appState: AppState;
|
||||||
|
actionManager: ActionManager;
|
||||||
|
renderCustomFooter?: ExcalidrawProps["renderFooter"];
|
||||||
|
showExitZenModeBtn: boolean;
|
||||||
|
}) => {
|
||||||
|
const device = useDevice();
|
||||||
|
const showFinalize =
|
||||||
|
!appState.viewModeEnabled && appState.multiElement && device.isTouchScreen;
|
||||||
|
return (
|
||||||
|
<footer
|
||||||
|
role="contentinfo"
|
||||||
|
className="layer-ui__wrapper__footer App-menu App-menu_bottom"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={clsx("layer-ui__wrapper__footer-left zen-mode-transition", {
|
||||||
|
"layer-ui__wrapper__footer-left--transition-left":
|
||||||
|
appState.zenModeEnabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Stack.Col gap={2}>
|
||||||
|
<Section heading="canvasActions">
|
||||||
|
<Island padding={1}>
|
||||||
|
<ZoomActions
|
||||||
|
renderAction={actionManager.renderAction}
|
||||||
|
zoom={appState.zoom}
|
||||||
|
/>
|
||||||
|
</Island>
|
||||||
|
{!appState.viewModeEnabled && (
|
||||||
|
<>
|
||||||
|
<UndoRedoActions
|
||||||
|
renderAction={actionManager.renderAction}
|
||||||
|
className={clsx("zen-mode-transition", {
|
||||||
|
"layer-ui__wrapper__footer-left--transition-bottom":
|
||||||
|
appState.zenModeEnabled,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={clsx("eraser-buttons zen-mode-transition", {
|
||||||
|
"layer-ui__wrapper__footer-left--transition-left":
|
||||||
|
appState.zenModeEnabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{actionManager.renderAction("eraser", { size: "small" })}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{showFinalize && (
|
||||||
|
<FinalizeAction
|
||||||
|
renderAction={actionManager.renderAction}
|
||||||
|
className={clsx("zen-mode-transition", {
|
||||||
|
"layer-ui__wrapper__footer-left--transition-left":
|
||||||
|
appState.zenModeEnabled,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Section>
|
||||||
|
</Stack.Col>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"layer-ui__wrapper__footer-center zen-mode-transition",
|
||||||
|
{
|
||||||
|
"layer-ui__wrapper__footer-left--transition-bottom":
|
||||||
|
appState.zenModeEnabled,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{renderCustomFooter?.(false, appState)}
|
||||||
|
</div>
|
||||||
|
<ExitZenModeAction
|
||||||
|
executeAction={actionManager.executeAction}
|
||||||
|
showExitZenModeBtn={showExitZenModeBtn}
|
||||||
|
/>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
@@ -58,6 +58,7 @@ const ExportButton: React.FC<{
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
title: string;
|
title: string;
|
||||||
shade?: number;
|
shade?: number;
|
||||||
|
children?: React.ReactNode;
|
||||||
}> = ({ children, title, onClick, color, shade = 6 }) => {
|
}> = ({ children, title, onClick, color, shade = 6 }) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -170,7 +171,9 @@ const ImageExportModal = ({
|
|||||||
<Stack.Row gap={2}>
|
<Stack.Row gap={2}>
|
||||||
{actionManager.renderAction("changeExportScale")}
|
{actionManager.renderAction("changeExportScale")}
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
<p style={{ marginLeft: "1em", userSelect: "none" }}>Scale</p>
|
<p style={{ marginLeft: "1em", userSelect: "none" }}>
|
||||||
|
{t("buttons.scale")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@@ -10,7 +10,7 @@ import { calculateScrollCenter, getSelectedElements } from "../scene";
|
|||||||
import { ExportType } from "../scene/types";
|
import { ExportType } from "../scene/types";
|
||||||
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
|
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
|
||||||
import { muteFSAbortError } from "../utils";
|
import { muteFSAbortError } from "../utils";
|
||||||
import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
|
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
||||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||||
import CollabButton from "./CollabButton";
|
import CollabButton from "./CollabButton";
|
||||||
import { ErrorDialog } from "./ErrorDialog";
|
import { ErrorDialog } from "./ErrorDialog";
|
||||||
@@ -39,6 +39,7 @@ import { trackEvent } from "../analytics";
|
|||||||
import { useDevice } from "../components/App";
|
import { useDevice } from "../components/App";
|
||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
import { actionToggleStats } from "../actions/actionToggleStats";
|
import { actionToggleStats } from "../actions/actionToggleStats";
|
||||||
|
import Footer from "./Footer";
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@@ -51,16 +52,13 @@ interface LayerUIProps {
|
|||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
onPenModeToggle: () => void;
|
onPenModeToggle: () => void;
|
||||||
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||||
zenModeEnabled: boolean;
|
|
||||||
showExitZenModeBtn: boolean;
|
showExitZenModeBtn: boolean;
|
||||||
showThemeBtn: boolean;
|
showThemeBtn: boolean;
|
||||||
toggleZenMode: () => void;
|
|
||||||
langCode: Language["code"];
|
langCode: Language["code"];
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
||||||
renderCustomFooter?: ExcalidrawProps["renderFooter"];
|
renderCustomFooter?: ExcalidrawProps["renderFooter"];
|
||||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||||
viewModeEnabled: boolean;
|
|
||||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
UIOptions: AppProps["UIOptions"];
|
UIOptions: AppProps["UIOptions"];
|
||||||
focusContainer: () => void;
|
focusContainer: () => void;
|
||||||
@@ -73,21 +71,18 @@ const LayerUI = ({
|
|||||||
appState,
|
appState,
|
||||||
files,
|
files,
|
||||||
setAppState,
|
setAppState,
|
||||||
canvas,
|
|
||||||
elements,
|
elements,
|
||||||
|
canvas,
|
||||||
onCollabButtonClick,
|
onCollabButtonClick,
|
||||||
onLockToggle,
|
onLockToggle,
|
||||||
onPenModeToggle,
|
onPenModeToggle,
|
||||||
onInsertElements,
|
onInsertElements,
|
||||||
zenModeEnabled,
|
|
||||||
showExitZenModeBtn,
|
showExitZenModeBtn,
|
||||||
showThemeBtn,
|
showThemeBtn,
|
||||||
toggleZenMode,
|
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
renderCustomFooter,
|
renderCustomFooter,
|
||||||
renderCustomStats,
|
renderCustomStats,
|
||||||
viewModeEnabled,
|
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
UIOptions,
|
UIOptions,
|
||||||
focusContainer,
|
focusContainer,
|
||||||
@@ -171,7 +166,7 @@ const LayerUI = ({
|
|||||||
<Section
|
<Section
|
||||||
heading="canvasActions"
|
heading="canvasActions"
|
||||||
className={clsx("zen-mode-transition", {
|
className={clsx("zen-mode-transition", {
|
||||||
"transition-left": zenModeEnabled,
|
"transition-left": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* the zIndex ensures this menu has higher stacking order,
|
{/* the zIndex ensures this menu has higher stacking order,
|
||||||
@@ -192,7 +187,7 @@ const LayerUI = ({
|
|||||||
<Section
|
<Section
|
||||||
heading="canvasActions"
|
heading="canvasActions"
|
||||||
className={clsx("zen-mode-transition", {
|
className={clsx("zen-mode-transition", {
|
||||||
"transition-left": zenModeEnabled,
|
"transition-left": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* the zIndex ensures this menu has higher stacking order,
|
{/* the zIndex ensures this menu has higher stacking order,
|
||||||
@@ -215,8 +210,8 @@ const LayerUI = ({
|
|||||||
)}
|
)}
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
<BackgroundPickerAndDarkModeToggle
|
<BackgroundPickerAndDarkModeToggle
|
||||||
actionManager={actionManager}
|
|
||||||
appState={appState}
|
appState={appState}
|
||||||
|
actionManager={actionManager}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
showThemeBtn={showThemeBtn}
|
showThemeBtn={showThemeBtn}
|
||||||
/>
|
/>
|
||||||
@@ -232,7 +227,7 @@ const LayerUI = ({
|
|||||||
<Section
|
<Section
|
||||||
heading="selectedShapeActions"
|
heading="selectedShapeActions"
|
||||||
className={clsx("zen-mode-transition", {
|
className={clsx("zen-mode-transition", {
|
||||||
"transition-left": zenModeEnabled,
|
"transition-left": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Island
|
<Island
|
||||||
@@ -249,7 +244,6 @@ const LayerUI = ({
|
|||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
renderAction={actionManager.renderAction}
|
renderAction={actionManager.renderAction}
|
||||||
activeTool={appState.activeTool.type}
|
|
||||||
/>
|
/>
|
||||||
</Island>
|
</Island>
|
||||||
</Section>
|
</Section>
|
||||||
@@ -284,7 +278,6 @@ const LayerUI = ({
|
|||||||
libraryReturnUrl={libraryReturnUrl}
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
focusContainer={focusContainer}
|
focusContainer={focusContainer}
|
||||||
library={library}
|
library={library}
|
||||||
theme={appState.theme}
|
|
||||||
files={files}
|
files={files}
|
||||||
id={id}
|
id={id}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
@@ -302,32 +295,34 @@ const LayerUI = ({
|
|||||||
<div className="App-menu App-menu_top">
|
<div className="App-menu App-menu_top">
|
||||||
<Stack.Col
|
<Stack.Col
|
||||||
gap={4}
|
gap={4}
|
||||||
className={clsx({ "disable-pointerEvents": zenModeEnabled })}
|
className={clsx({
|
||||||
|
"disable-pointerEvents": appState.zenModeEnabled,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{viewModeEnabled
|
{appState.viewModeEnabled
|
||||||
? renderViewModeCanvasActions()
|
? renderViewModeCanvasActions()
|
||||||
: renderCanvasActions()}
|
: renderCanvasActions()}
|
||||||
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
{!viewModeEnabled && (
|
{!appState.viewModeEnabled && (
|
||||||
<Section heading="shapes">
|
<Section heading="shapes">
|
||||||
{(heading) => (
|
{(heading: React.ReactNode) => (
|
||||||
<Stack.Col gap={4} align="start">
|
<Stack.Col gap={4} align="start">
|
||||||
<Stack.Row
|
<Stack.Row
|
||||||
gap={1}
|
gap={1}
|
||||||
className={clsx("App-toolbar-container", {
|
className={clsx("App-toolbar-container", {
|
||||||
"zen-mode": zenModeEnabled,
|
"zen-mode": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<PenModeButton
|
<PenModeButton
|
||||||
zenModeEnabled={zenModeEnabled}
|
zenModeEnabled={appState.zenModeEnabled}
|
||||||
checked={appState.penMode}
|
checked={appState.penMode}
|
||||||
onChange={onPenModeToggle}
|
onChange={onPenModeToggle}
|
||||||
title={t("toolBar.penMode")}
|
title={t("toolBar.penMode")}
|
||||||
penDetected={appState.penDetected}
|
penDetected={appState.penDetected}
|
||||||
/>
|
/>
|
||||||
<LockButton
|
<LockButton
|
||||||
zenModeEnabled={zenModeEnabled}
|
zenModeEnabled={appState.zenModeEnabled}
|
||||||
checked={appState.activeTool.locked}
|
checked={appState.activeTool.locked}
|
||||||
onChange={() => onLockToggle()}
|
onChange={() => onLockToggle()}
|
||||||
title={t("toolBar.lock")}
|
title={t("toolBar.lock")}
|
||||||
@@ -335,7 +330,7 @@ const LayerUI = ({
|
|||||||
<Island
|
<Island
|
||||||
padding={1}
|
padding={1}
|
||||||
className={clsx("App-toolbar", {
|
className={clsx("App-toolbar", {
|
||||||
"zen-mode": zenModeEnabled,
|
"zen-mode": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<HintViewer
|
<HintViewer
|
||||||
@@ -371,7 +366,7 @@ const LayerUI = ({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"layer-ui__wrapper__top-right zen-mode-transition",
|
"layer-ui__wrapper__top-right zen-mode-transition",
|
||||||
{
|
{
|
||||||
"transition-right": zenModeEnabled,
|
"transition-right": appState.zenModeEnabled,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -386,99 +381,7 @@ const LayerUI = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderBottomAppMenu = () => {
|
return (
|
||||||
return (
|
|
||||||
<footer
|
|
||||||
role="contentinfo"
|
|
||||||
className="layer-ui__wrapper__footer App-menu App-menu_bottom"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
"layer-ui__wrapper__footer-left zen-mode-transition",
|
|
||||||
{
|
|
||||||
"layer-ui__wrapper__footer-left--transition-left": zenModeEnabled,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Stack.Col gap={2}>
|
|
||||||
<Section heading="canvasActions">
|
|
||||||
<Island padding={1}>
|
|
||||||
<ZoomActions
|
|
||||||
renderAction={actionManager.renderAction}
|
|
||||||
zoom={appState.zoom}
|
|
||||||
/>
|
|
||||||
</Island>
|
|
||||||
{!viewModeEnabled && (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={clsx("undo-redo-buttons zen-mode-transition", {
|
|
||||||
"layer-ui__wrapper__footer-left--transition-bottom":
|
|
||||||
zenModeEnabled,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{actionManager.renderAction("undo", { size: "small" })}
|
|
||||||
{actionManager.renderAction("redo", { size: "small" })}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={clsx("eraser-buttons zen-mode-transition", {
|
|
||||||
"layer-ui__wrapper__footer-left--transition-left":
|
|
||||||
zenModeEnabled,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{actionManager.renderAction("eraser", { size: "small" })}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!viewModeEnabled &&
|
|
||||||
appState.multiElement &&
|
|
||||||
device.isTouchScreen && (
|
|
||||||
<div
|
|
||||||
className={clsx("finalize-button zen-mode-transition", {
|
|
||||||
"layer-ui__wrapper__footer-left--transition-left":
|
|
||||||
zenModeEnabled,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{actionManager.renderAction("finalize", { size: "small" })}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Section>
|
|
||||||
</Stack.Col>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
"layer-ui__wrapper__footer-center zen-mode-transition",
|
|
||||||
{
|
|
||||||
"layer-ui__wrapper__footer-left--transition-bottom":
|
|
||||||
zenModeEnabled,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{renderCustomFooter?.(false, appState)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
"layer-ui__wrapper__footer-right zen-mode-transition",
|
|
||||||
{
|
|
||||||
"transition-right disable-pointerEvents": zenModeEnabled,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{actionManager.renderAction("toggleShortcuts")}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className={clsx("disable-zen-mode", {
|
|
||||||
"disable-zen-mode--visible": showExitZenModeBtn,
|
|
||||||
})}
|
|
||||||
onClick={toggleZenMode}
|
|
||||||
>
|
|
||||||
{t("buttons.exitZenMode")}
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const dialogs = (
|
|
||||||
<>
|
<>
|
||||||
{appState.isLoading && <LoadingMessage delay={250} />}
|
{appState.isLoading && <LoadingMessage delay={250} />}
|
||||||
{appState.errorMessage && (
|
{appState.errorMessage && (
|
||||||
@@ -506,85 +409,77 @@ const LayerUI = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
{device.isMobile && (
|
||||||
);
|
<MobileMenu
|
||||||
|
appState={appState}
|
||||||
|
elements={elements}
|
||||||
|
actionManager={actionManager}
|
||||||
|
libraryMenu={libraryMenu}
|
||||||
|
renderJSONExportDialog={renderJSONExportDialog}
|
||||||
|
renderImageExportDialog={renderImageExportDialog}
|
||||||
|
setAppState={setAppState}
|
||||||
|
onCollabButtonClick={onCollabButtonClick}
|
||||||
|
onLockToggle={() => onLockToggle()}
|
||||||
|
onPenModeToggle={onPenModeToggle}
|
||||||
|
canvas={canvas}
|
||||||
|
isCollaborating={isCollaborating}
|
||||||
|
renderCustomFooter={renderCustomFooter}
|
||||||
|
showThemeBtn={showThemeBtn}
|
||||||
|
onImageAction={onImageAction}
|
||||||
|
renderTopRightUI={renderTopRightUI}
|
||||||
|
renderCustomStats={renderCustomStats}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
const renderStats = () => {
|
{!device.isMobile && (
|
||||||
if (!appState.showStats) {
|
<div
|
||||||
return null;
|
className={clsx("layer-ui__wrapper", {
|
||||||
}
|
"disable-pointerEvents":
|
||||||
return (
|
appState.draggingElement ||
|
||||||
<Stats
|
appState.resizingElement ||
|
||||||
appState={appState}
|
(appState.editingElement &&
|
||||||
setAppState={setAppState}
|
!isTextElement(appState.editingElement)),
|
||||||
elements={elements}
|
})}
|
||||||
onClose={() => {
|
style={
|
||||||
actionManager.executeAction(actionToggleStats);
|
appState.isLibraryOpen &&
|
||||||
}}
|
appState.isLibraryMenuDocked &&
|
||||||
renderCustomStats={renderCustomStats}
|
device.canDeviceFitSidebar
|
||||||
/>
|
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
|
||||||
);
|
: {}
|
||||||
};
|
}
|
||||||
|
>
|
||||||
return device.isMobile ? (
|
{renderFixedSideContainer()}
|
||||||
<>
|
<Footer
|
||||||
{dialogs}
|
appState={appState}
|
||||||
<MobileMenu
|
actionManager={actionManager}
|
||||||
appState={appState}
|
renderCustomFooter={renderCustomFooter}
|
||||||
elements={elements}
|
showExitZenModeBtn={showExitZenModeBtn}
|
||||||
actionManager={actionManager}
|
/>
|
||||||
libraryMenu={libraryMenu}
|
{appState.showStats && (
|
||||||
renderJSONExportDialog={renderJSONExportDialog}
|
<Stats
|
||||||
renderImageExportDialog={renderImageExportDialog}
|
appState={appState}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
onCollabButtonClick={onCollabButtonClick}
|
elements={elements}
|
||||||
onLockToggle={() => onLockToggle()}
|
onClose={() => {
|
||||||
onPenModeToggle={onPenModeToggle}
|
actionManager.executeAction(actionToggleStats);
|
||||||
canvas={canvas}
|
}}
|
||||||
isCollaborating={isCollaborating}
|
renderCustomStats={renderCustomStats}
|
||||||
renderCustomFooter={renderCustomFooter}
|
/>
|
||||||
viewModeEnabled={viewModeEnabled}
|
)}
|
||||||
showThemeBtn={showThemeBtn}
|
{appState.scrolledOutside && (
|
||||||
onImageAction={onImageAction}
|
<button
|
||||||
renderTopRightUI={renderTopRightUI}
|
className="scroll-back-to-content"
|
||||||
renderStats={renderStats}
|
onClick={() => {
|
||||||
/>
|
setAppState({
|
||||||
</>
|
...calculateScrollCenter(elements, appState, canvas),
|
||||||
) : (
|
});
|
||||||
<>
|
}}
|
||||||
<div
|
>
|
||||||
className={clsx("layer-ui__wrapper", {
|
{t("buttons.scrollBackToContent")}
|
||||||
"disable-pointerEvents":
|
</button>
|
||||||
appState.draggingElement ||
|
)}
|
||||||
appState.resizingElement ||
|
</div>
|
||||||
(appState.editingElement &&
|
)}
|
||||||
!isTextElement(appState.editingElement)),
|
|
||||||
})}
|
|
||||||
style={
|
|
||||||
appState.isLibraryOpen &&
|
|
||||||
appState.isLibraryMenuDocked &&
|
|
||||||
device.canDeviceFitSidebar
|
|
||||||
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{dialogs}
|
|
||||||
{renderFixedSideContainer()}
|
|
||||||
{renderBottomAppMenu()}
|
|
||||||
{renderStats()}
|
|
||||||
{appState.scrolledOutside && (
|
|
||||||
<button
|
|
||||||
className="scroll-back-to-content"
|
|
||||||
onClick={() => {
|
|
||||||
setAppState({
|
|
||||||
...calculateScrollCenter(elements, appState, canvas),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("buttons.scrollBackToContent")}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{appState.isLibraryOpen && (
|
{appState.isLibraryOpen && (
|
||||||
<div className="layer-ui__sidebar">{libraryMenu}</div>
|
<div className="layer-ui__sidebar">{libraryMenu}</div>
|
||||||
)}
|
)}
|
||||||
|
@@ -80,7 +80,6 @@ export const LibraryMenu = ({
|
|||||||
onInsertLibraryItems,
|
onInsertLibraryItems,
|
||||||
pendingElements,
|
pendingElements,
|
||||||
onAddToLibrary,
|
onAddToLibrary,
|
||||||
theme,
|
|
||||||
setAppState,
|
setAppState,
|
||||||
files,
|
files,
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
@@ -93,7 +92,6 @@ export const LibraryMenu = ({
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onInsertLibraryItems: (libraryItems: LibraryItems) => void;
|
onInsertLibraryItems: (libraryItems: LibraryItems) => void;
|
||||||
onAddToLibrary: () => void;
|
onAddToLibrary: () => void;
|
||||||
theme: AppState["theme"];
|
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
@@ -105,7 +103,6 @@ export const LibraryMenu = ({
|
|||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
|
|
||||||
useOnClickOutside(
|
useOnClickOutside(
|
||||||
ref,
|
ref,
|
||||||
useCallback(
|
useCallback(
|
||||||
@@ -224,7 +221,7 @@ export const LibraryMenu = ({
|
|||||||
}, [setPublishLibSuccess, publishLibSuccess]);
|
}, [setPublishLibSuccess, publishLibSuccess]);
|
||||||
|
|
||||||
const onPublishLibSuccess = useCallback(
|
const onPublishLibSuccess = useCallback(
|
||||||
(data, libraryItems: LibraryItems) => {
|
(data: { url: string; authorName: string }, libraryItems: LibraryItems) => {
|
||||||
setShowPublishLibraryDialog(false);
|
setShowPublishLibraryDialog(false);
|
||||||
setPublishLibSuccess({ url: data.url, authorName: data.authorName });
|
setPublishLibSuccess({ url: data.url, authorName: data.authorName });
|
||||||
const nextLibItems = libraryItems.slice();
|
const nextLibItems = libraryItems.slice();
|
||||||
@@ -290,7 +287,7 @@ export const LibraryMenu = ({
|
|||||||
appState={appState}
|
appState={appState}
|
||||||
libraryReturnUrl={libraryReturnUrl}
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
library={library}
|
library={library}
|
||||||
theme={theme}
|
theme={appState.theme}
|
||||||
files={files}
|
files={files}
|
||||||
id={id}
|
id={id}
|
||||||
selectedItems={selectedItems}
|
selectedItems={selectedItems}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { AppState } from "../types";
|
import { AppState, ExcalidrawProps } from "../types";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
@@ -18,6 +18,8 @@ import { UserList } from "./UserList";
|
|||||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||||
import { LibraryButton } from "./LibraryButton";
|
import { LibraryButton } from "./LibraryButton";
|
||||||
import { PenModeButton } from "./PenModeButton";
|
import { PenModeButton } from "./PenModeButton";
|
||||||
|
import { Stats } from "./Stats";
|
||||||
|
import { actionToggleStats } from "../actions";
|
||||||
|
|
||||||
type MobileMenuProps = {
|
type MobileMenuProps = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
@@ -36,14 +38,13 @@ type MobileMenuProps = {
|
|||||||
isMobile: boolean,
|
isMobile: boolean,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => JSX.Element | null;
|
) => JSX.Element | null;
|
||||||
viewModeEnabled: boolean;
|
|
||||||
showThemeBtn: boolean;
|
showThemeBtn: boolean;
|
||||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||||
renderTopRightUI?: (
|
renderTopRightUI?: (
|
||||||
isMobile: boolean,
|
isMobile: boolean,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => JSX.Element | null;
|
) => JSX.Element | null;
|
||||||
renderStats: () => JSX.Element | null;
|
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MobileMenu = ({
|
export const MobileMenu = ({
|
||||||
@@ -60,17 +61,16 @@ export const MobileMenu = ({
|
|||||||
canvas,
|
canvas,
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
renderCustomFooter,
|
renderCustomFooter,
|
||||||
viewModeEnabled,
|
|
||||||
showThemeBtn,
|
showThemeBtn,
|
||||||
onImageAction,
|
onImageAction,
|
||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
renderStats,
|
renderCustomStats,
|
||||||
}: MobileMenuProps) => {
|
}: MobileMenuProps) => {
|
||||||
const renderToolbar = () => {
|
const renderToolbar = () => {
|
||||||
return (
|
return (
|
||||||
<FixedSideContainer side="top" className="App-top-bar">
|
<FixedSideContainer side="top" className="App-top-bar">
|
||||||
<Section heading="shapes">
|
<Section heading="shapes">
|
||||||
{(heading) => (
|
{(heading: React.ReactNode) => (
|
||||||
<Stack.Col gap={4} align="center">
|
<Stack.Col gap={4} align="center">
|
||||||
<Stack.Row gap={1} className="App-toolbar-container">
|
<Stack.Row gap={1} className="App-toolbar-container">
|
||||||
<Island padding={1} className="App-toolbar">
|
<Island padding={1} className="App-toolbar">
|
||||||
@@ -125,7 +125,7 @@ export const MobileMenu = ({
|
|||||||
!appState.editingElement &&
|
!appState.editingElement &&
|
||||||
getSelectedElements(elements, appState).length === 0;
|
getSelectedElements(elements, appState).length === 0;
|
||||||
|
|
||||||
if (viewModeEnabled) {
|
if (appState.viewModeEnabled) {
|
||||||
return (
|
return (
|
||||||
<div className="App-toolbar-content">
|
<div className="App-toolbar-content">
|
||||||
{actionManager.renderAction("toggleCanvasMenu")}
|
{actionManager.renderAction("toggleCanvasMenu")}
|
||||||
@@ -151,7 +151,7 @@ export const MobileMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderCanvasActions = () => {
|
const renderCanvasActions = () => {
|
||||||
if (viewModeEnabled) {
|
if (appState.viewModeEnabled) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderJSONExportDialog()}
|
{renderJSONExportDialog()}
|
||||||
@@ -185,8 +185,18 @@ export const MobileMenu = ({
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!viewModeEnabled && renderToolbar()}
|
{!appState.viewModeEnabled && renderToolbar()}
|
||||||
{renderStats()}
|
{!appState.openMenu && appState.showStats && (
|
||||||
|
<Stats
|
||||||
|
appState={appState}
|
||||||
|
setAppState={setAppState}
|
||||||
|
elements={elements}
|
||||||
|
onClose={() => {
|
||||||
|
actionManager.executeAction(actionToggleStats);
|
||||||
|
}}
|
||||||
|
renderCustomStats={renderCustomStats}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className="App-bottom-bar"
|
className="App-bottom-bar"
|
||||||
style={{
|
style={{
|
||||||
@@ -216,31 +226,32 @@ export const MobileMenu = ({
|
|||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
) : appState.openMenu === "shape" &&
|
) : appState.openMenu === "shape" &&
|
||||||
!viewModeEnabled &&
|
!appState.viewModeEnabled &&
|
||||||
showSelectedShapeActions(appState, elements) ? (
|
showSelectedShapeActions(appState, elements) ? (
|
||||||
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
||||||
<SelectedShapeActions
|
<SelectedShapeActions
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
renderAction={actionManager.renderAction}
|
renderAction={actionManager.renderAction}
|
||||||
activeTool={appState.activeTool.type}
|
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
) : null}
|
) : null}
|
||||||
<footer className="App-toolbar">
|
<footer className="App-toolbar">
|
||||||
{renderAppToolbar()}
|
{renderAppToolbar()}
|
||||||
{appState.scrolledOutside && !appState.openMenu && (
|
{appState.scrolledOutside &&
|
||||||
<button
|
!appState.openMenu &&
|
||||||
className="scroll-back-to-content"
|
!appState.isLibraryOpen && (
|
||||||
onClick={() => {
|
<button
|
||||||
setAppState({
|
className="scroll-back-to-content"
|
||||||
...calculateScrollCenter(elements, appState, canvas),
|
onClick={() => {
|
||||||
});
|
setAppState({
|
||||||
}}
|
...calculateScrollCenter(elements, appState, canvas),
|
||||||
>
|
});
|
||||||
{t("buttons.scrollBackToContent")}
|
}}
|
||||||
</button>
|
>
|
||||||
)}
|
{t("buttons.scrollBackToContent")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</footer>
|
</footer>
|
||||||
</Island>
|
</Island>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -8,7 +8,7 @@ import { useExcalidrawContainer, useDevice } from "./App";
|
|||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { THEME } from "../constants";
|
import { THEME } from "../constants";
|
||||||
|
|
||||||
export const Modal = (props: {
|
export const Modal: React.FC<{
|
||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
@@ -16,7 +16,7 @@ export const Modal = (props: {
|
|||||||
labelledBy: string;
|
labelledBy: string;
|
||||||
theme?: AppState["theme"];
|
theme?: AppState["theme"];
|
||||||
closeOnClickOutside?: boolean;
|
closeOnClickOutside?: boolean;
|
||||||
}) => {
|
}> = (props) => {
|
||||||
const { theme = THEME.LIGHT, closeOnClickOutside = true } = props;
|
const { theme = THEME.LIGHT, closeOnClickOutside = true } = props;
|
||||||
const modalRoot = useBodyRoot(theme);
|
const modalRoot = useBodyRoot(theme);
|
||||||
|
|
||||||
|
@@ -46,7 +46,7 @@ const ChartPreviewBtn = (props: {
|
|||||||
},
|
},
|
||||||
null, // files
|
null, // files
|
||||||
);
|
);
|
||||||
|
previewNode.replaceChildren();
|
||||||
previewNode.appendChild(svg);
|
previewNode.appendChild(svg);
|
||||||
|
|
||||||
if (props.selected) {
|
if (props.selected) {
|
||||||
@@ -55,7 +55,7 @@ const ChartPreviewBtn = (props: {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
previewNode.removeChild(svg);
|
previewNode.replaceChildren();
|
||||||
};
|
};
|
||||||
}, [props.spreadsheet, props.chartType, props.selected]);
|
}, [props.spreadsheet, props.chartType, props.selected]);
|
||||||
|
|
||||||
|
@@ -2,5 +2,6 @@
|
|||||||
.popover {
|
.popover {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
padding: 5px 0 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -69,12 +69,26 @@ export const Popover = ({
|
|||||||
if (fitInViewport && popoverRef.current) {
|
if (fitInViewport && popoverRef.current) {
|
||||||
const element = popoverRef.current;
|
const element = popoverRef.current;
|
||||||
const { x, y, width, height } = element.getBoundingClientRect();
|
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) {
|
if (x + width - offsetLeft > viewportWidth) {
|
||||||
element.style.left = `${viewportWidth - width}px`;
|
element.style.left = `${viewportWidth - width - 10}px`;
|
||||||
}
|
}
|
||||||
if (y + height - offsetTop > viewportHeight) {
|
if (y + height - offsetTop > viewportHeight) {
|
||||||
element.style.top = `${viewportHeight - height}px`;
|
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]);
|
}, [fitInViewport, viewportWidth, viewportHeight, offsetLeft, offsetTop]);
|
||||||
|
|
||||||
|
@@ -2,12 +2,11 @@ import React from "react";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useExcalidrawContainer } from "./App";
|
import { useExcalidrawContainer } from "./App";
|
||||||
|
|
||||||
interface SectionProps extends React.HTMLProps<HTMLElement> {
|
export const Section: React.FC<{
|
||||||
heading: string;
|
heading: string;
|
||||||
children: React.ReactNode | ((header: React.ReactNode) => React.ReactNode);
|
children?: React.ReactNode | ((heading: React.ReactNode) => React.ReactNode);
|
||||||
}
|
className?: string;
|
||||||
|
}> = ({ heading, children, ...props }) => {
|
||||||
export const Section = ({ heading, children, ...props }: SectionProps) => {
|
|
||||||
const { id } = useExcalidrawContainer();
|
const { id } = useExcalidrawContainer();
|
||||||
const header = (
|
const header = (
|
||||||
<h2 className="visually-hidden" id={`${id}-${heading}-title`}>
|
<h2 className="visually-hidden" id={`${id}-${heading}-title`}>
|
||||||
|
@@ -2,7 +2,6 @@ import React from "react";
|
|||||||
import { getCommonBounds } from "../element/bounds";
|
import { getCommonBounds } from "../element/bounds";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useDevice } from "../components/App";
|
|
||||||
import { getTargetElements } from "../scene";
|
import { getTargetElements } from "../scene";
|
||||||
import { AppState, ExcalidrawProps } from "../types";
|
import { AppState, ExcalidrawProps } from "../types";
|
||||||
import { close } from "./icons";
|
import { close } from "./icons";
|
||||||
@@ -16,13 +15,10 @@ export const Stats = (props: {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
renderCustomStats: ExcalidrawProps["renderCustomStats"];
|
renderCustomStats: ExcalidrawProps["renderCustomStats"];
|
||||||
}) => {
|
}) => {
|
||||||
const device = useDevice();
|
|
||||||
const boundingBox = getCommonBounds(props.elements);
|
const boundingBox = getCommonBounds(props.elements);
|
||||||
const selectedElements = getTargetElements(props.elements, props.appState);
|
const selectedElements = getTargetElements(props.elements, props.appState);
|
||||||
const selectedBoundingBox = getCommonBounds(selectedElements);
|
const selectedBoundingBox = getCommonBounds(selectedElements);
|
||||||
if (device.isMobile && props.appState.openMenu) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div className="Stats">
|
<div className="Stats">
|
||||||
<Island padding={2}>
|
<Island padding={2}>
|
||||||
|
@@ -7,13 +7,13 @@ const DEFAULT_TOAST_TIMEOUT = 5000;
|
|||||||
|
|
||||||
export const Toast = ({
|
export const Toast = ({
|
||||||
message,
|
message,
|
||||||
clearToast,
|
onClose,
|
||||||
closable = false,
|
closable = false,
|
||||||
// To prevent autoclose, pass duration as Infinity
|
// To prevent autoclose, pass duration as Infinity
|
||||||
duration = DEFAULT_TOAST_TIMEOUT,
|
duration = DEFAULT_TOAST_TIMEOUT,
|
||||||
}: {
|
}: {
|
||||||
message: string;
|
message: string;
|
||||||
clearToast: () => void;
|
onClose: () => void;
|
||||||
closable?: boolean;
|
closable?: boolean;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -23,8 +23,8 @@ export const Toast = ({
|
|||||||
if (!shouldAutoClose) {
|
if (!shouldAutoClose) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
timerRef.current = window.setTimeout(() => clearToast(), duration);
|
timerRef.current = window.setTimeout(() => onClose(), duration);
|
||||||
}, [clearToast, duration, shouldAutoClose]);
|
}, [onClose, duration, shouldAutoClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldAutoClose) {
|
if (!shouldAutoClose) {
|
||||||
@@ -50,7 +50,7 @@ export const Toast = ({
|
|||||||
icon={close}
|
icon={close}
|
||||||
aria-label="close"
|
aria-label="close"
|
||||||
type="icon"
|
type="icon"
|
||||||
onClick={clearToast}
|
onClick={onClose}
|
||||||
className="close"
|
className="close"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@@ -187,3 +187,5 @@ ToolButton.defaultProps = {
|
|||||||
className: "",
|
className: "",
|
||||||
size: "medium",
|
size: "medium",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ToolButton.displayName = "ToolButton";
|
||||||
|
@@ -212,16 +212,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ToolIcon.ToolIcon__library {
|
.ToolIcon.ToolIcon__library {
|
||||||
top: 100px;
|
top: calc(var(--sat) + 100px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ToolIcon.ToolIcon__lock {
|
.ToolIcon.ToolIcon__lock {
|
||||||
margin-inline-end: 0;
|
top: calc(var(--sat) + 60px);
|
||||||
top: 60px;
|
|
||||||
}
|
}
|
||||||
.ToolIcon.ToolIcon__penMode {
|
.ToolIcon.ToolIcon__penMode {
|
||||||
margin-inline-end: 0;
|
top: calc(var(--sat) + 140px);
|
||||||
top: 140px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -32,7 +32,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ToolIcon.ToolIcon__lock {
|
.ToolIcon.ToolIcon__lock {
|
||||||
margin-inline-end: var(--space-factor);
|
|
||||||
&.ToolIcon_type_floating {
|
&.ToolIcon_type_floating {
|
||||||
margin-left: 0.1rem;
|
margin-left: 0.1rem;
|
||||||
}
|
}
|
||||||
|
@@ -67,13 +67,14 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const restoreElementWithProperties = <
|
const restoreElementWithProperties = <
|
||||||
T extends ExcalidrawElement,
|
T extends Required<Omit<ExcalidrawElement, "customData">> & {
|
||||||
K extends Pick<T, keyof Omit<Required<T>, keyof ExcalidrawElement>>,
|
customData?: ExcalidrawElement["customData"];
|
||||||
>(
|
|
||||||
element: Required<T> & {
|
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
boundElementIds?: readonly ExcalidrawElement["id"][];
|
boundElementIds?: readonly ExcalidrawElement["id"][];
|
||||||
},
|
},
|
||||||
|
K extends Pick<T, keyof Omit<Required<T>, keyof ExcalidrawElement>>,
|
||||||
|
>(
|
||||||
|
element: T,
|
||||||
extra: Pick<
|
extra: Pick<
|
||||||
T,
|
T,
|
||||||
// This extra Pick<T, keyof K> ensure no excess properties are passed.
|
// This extra Pick<T, keyof K> ensure no excess properties are passed.
|
||||||
@@ -115,6 +116,10 @@ const restoreElementWithProperties = <
|
|||||||
locked: element.locked ?? false,
|
locked: element.locked ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ("customData" in element) {
|
||||||
|
base.customData = element.customData;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
...getNormalizedDimensions(base),
|
...getNormalizedDimensions(base),
|
||||||
|
@@ -32,6 +32,7 @@ import { getElementAbsoluteCoords } from "./";
|
|||||||
|
|
||||||
import "./Hyperlink.scss";
|
import "./Hyperlink.scss";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
|
import { useExcalidrawAppState } from "../components/App";
|
||||||
|
|
||||||
const CONTAINER_WIDTH = 320;
|
const CONTAINER_WIDTH = 320;
|
||||||
const SPACE_BOTTOM = 85;
|
const SPACE_BOTTOM = 85;
|
||||||
@@ -48,15 +49,15 @@ let IS_HYPERLINK_TOOLTIP_VISIBLE = false;
|
|||||||
|
|
||||||
export const Hyperlink = ({
|
export const Hyperlink = ({
|
||||||
element,
|
element,
|
||||||
appState,
|
|
||||||
setAppState,
|
setAppState,
|
||||||
onLinkOpen,
|
onLinkOpen,
|
||||||
}: {
|
}: {
|
||||||
element: NonDeletedExcalidrawElement;
|
element: NonDeletedExcalidrawElement;
|
||||||
appState: AppState;
|
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
onLinkOpen: ExcalidrawProps["onLinkOpen"];
|
onLinkOpen: ExcalidrawProps["onLinkOpen"];
|
||||||
}) => {
|
}) => {
|
||||||
|
const appState = useExcalidrawAppState();
|
||||||
|
|
||||||
const linkVal = element.link || "";
|
const linkVal = element.link || "";
|
||||||
|
|
||||||
const [inputVal, setInputVal] = useState(linkVal);
|
const [inputVal, setInputVal] = useState(linkVal);
|
||||||
|
@@ -18,6 +18,7 @@ import { rescalePoints } from "../points";
|
|||||||
|
|
||||||
// x and y position of top left corner, x and y position of bottom right corner
|
// x and y position of top left corner, x and y position of bottom right corner
|
||||||
export type Bounds = readonly [number, number, number, number];
|
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
|
// 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.
|
// This set of functions retrieves the absolute position of the 4 points.
|
||||||
@@ -68,11 +69,102 @@ export const getCurvePathOps = (shape: Drawable): Op[] => {
|
|||||||
return shape.sets[0].ops;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let s1 = null;
|
||||||
|
let s2 = null;
|
||||||
|
|
||||||
|
let t1 = Infinity;
|
||||||
|
let t2 = Infinity;
|
||||||
|
|
||||||
|
if (a === 0) {
|
||||||
|
t1 = t2 = -c / b;
|
||||||
|
} else {
|
||||||
|
t1 = (-b + Math.sqrt(sqrtPart)) / (2 * a);
|
||||||
|
t2 = (-b - Math.sqrt(sqrtPart)) / (2 * a);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = (
|
const getMinMaxXYFromCurvePathOps = (
|
||||||
ops: Op[],
|
ops: Op[],
|
||||||
transformXY?: (x: number, y: number) => [number, number],
|
transformXY?: (x: number, y: number) => [number, number],
|
||||||
): [number, number, number, number] => {
|
): [number, number, number, number] => {
|
||||||
let currentP: Point = [0, 0];
|
let currentP: Point = [0, 0];
|
||||||
|
|
||||||
const { minX, minY, maxX, maxY } = ops.reduce(
|
const { minX, minY, maxX, maxY } = ops.reduce(
|
||||||
(limits, { op, data }) => {
|
(limits, { op, data }) => {
|
||||||
// There are only four operation types:
|
// There are only four operation types:
|
||||||
@@ -83,38 +175,29 @@ const getMinMaxXYFromCurvePathOps = (
|
|||||||
// move operation does not draw anything; so, it always
|
// move operation does not draw anything; so, it always
|
||||||
// returns false
|
// returns false
|
||||||
} else if (op === "bcurveTo") {
|
} else if (op === "bcurveTo") {
|
||||||
// create points from bezier curve
|
const _p1 = [data[0], data[1]] as Point;
|
||||||
// bezier curve stores data as a flattened array of three positions
|
const _p2 = [data[2], data[3]] as Point;
|
||||||
// [x1, y1, x2, y2, x3, y3]
|
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;
|
const p1 = transformXY ? transformXY(..._p1) : _p1;
|
||||||
currentP = p3;
|
const p2 = transformXY ? transformXY(..._p2) : _p2;
|
||||||
|
const p3 = transformXY ? transformXY(..._p3) : _p3;
|
||||||
|
|
||||||
const equation = (t: number, idx: number) =>
|
const p0 = transformXY ? transformXY(...currentP) : currentP;
|
||||||
Math.pow(1 - t, 3) * p3[idx] +
|
currentP = _p3;
|
||||||
3 * t * Math.pow(1 - t, 2) * p2[idx] +
|
|
||||||
3 * Math.pow(t, 2) * (1 - t) * p1[idx] +
|
|
||||||
p0[idx] * Math.pow(t, 3);
|
|
||||||
|
|
||||||
let t = 0;
|
const [minX, minY, maxX, maxY] = getCubicBezierCurveBound(
|
||||||
while (t <= 1.0) {
|
p0,
|
||||||
let x = equation(t, 0);
|
p1,
|
||||||
let y = equation(t, 1);
|
p2,
|
||||||
if (transformXY) {
|
p3,
|
||||||
[x, y] = transformXY(x, y);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
limits.minY = Math.min(limits.minY, y);
|
limits.minX = Math.min(limits.minX, minX);
|
||||||
limits.minX = Math.min(limits.minX, x);
|
limits.minY = Math.min(limits.minY, minY);
|
||||||
|
|
||||||
limits.maxX = Math.max(limits.maxX, x);
|
limits.maxX = Math.max(limits.maxX, maxX);
|
||||||
limits.maxY = Math.max(limits.maxY, y);
|
limits.maxY = Math.max(limits.maxY, maxY);
|
||||||
|
|
||||||
t += 0.1;
|
|
||||||
}
|
|
||||||
} else if (op === "lineTo") {
|
} else if (op === "lineTo") {
|
||||||
// TODO: Implement this
|
// TODO: Implement this
|
||||||
} else if (op === "qcurveTo") {
|
} else if (op === "qcurveTo") {
|
||||||
@@ -124,7 +207,6 @@ const getMinMaxXYFromCurvePathOps = (
|
|||||||
},
|
},
|
||||||
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
||||||
);
|
);
|
||||||
|
|
||||||
return [minX, minY, maxX, maxY];
|
return [minX, minY, maxX, maxY];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -420,6 +502,7 @@ export const getResizedElementAbsoluteCoords = (
|
|||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
nextWidth: number,
|
nextWidth: number,
|
||||||
nextHeight: number,
|
nextHeight: number,
|
||||||
|
normalizePoints: boolean,
|
||||||
): [number, number, number, number] => {
|
): [number, number, number, number] => {
|
||||||
if (!(isLinearElement(element) || isFreeDrawElement(element))) {
|
if (!(isLinearElement(element) || isFreeDrawElement(element))) {
|
||||||
return [
|
return [
|
||||||
@@ -433,7 +516,8 @@ export const getResizedElementAbsoluteCoords = (
|
|||||||
const points = rescalePoints(
|
const points = rescalePoints(
|
||||||
0,
|
0,
|
||||||
nextWidth,
|
nextWidth,
|
||||||
rescalePoints(1, nextHeight, element.points),
|
rescalePoints(1, nextHeight, element.points, normalizePoints),
|
||||||
|
normalizePoints,
|
||||||
);
|
);
|
||||||
|
|
||||||
let bounds: [number, number, number, number];
|
let bounds: [number, number, number, number];
|
||||||
|
@@ -35,6 +35,7 @@ import { getShapeForElement } from "../renderer/renderElement";
|
|||||||
import { hasBoundTextElement, isImageElement } from "./typeChecks";
|
import { hasBoundTextElement, isImageElement } from "./typeChecks";
|
||||||
import { isTextElement } from ".";
|
import { isTextElement } from ".";
|
||||||
import { isTransparent } from "../utils";
|
import { isTransparent } from "../utils";
|
||||||
|
import { shouldShowBoundingBox } from "./transformHandles";
|
||||||
|
|
||||||
const isElementDraggableFromInside = (
|
const isElementDraggableFromInside = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
@@ -64,7 +65,10 @@ export const hitTest = (
|
|||||||
const threshold = 10 / appState.zoom.value;
|
const threshold = 10 / appState.zoom.value;
|
||||||
const point: Point = [x, y];
|
const point: Point = [x, y];
|
||||||
|
|
||||||
if (isElementSelected(appState, element)) {
|
if (
|
||||||
|
isElementSelected(appState, element) &&
|
||||||
|
shouldShowBoundingBox([element], appState)
|
||||||
|
) {
|
||||||
return isPointHittingElementBoundingBox(element, point, threshold);
|
return isPointHittingElementBoundingBox(element, point, threshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -105,15 +105,26 @@ export const dragNewElement = (
|
|||||||
true */
|
true */
|
||||||
widthAspectRatio?: number | null,
|
widthAspectRatio?: number | null,
|
||||||
) => {
|
) => {
|
||||||
if (shouldMaintainAspectRatio) {
|
if (shouldMaintainAspectRatio && draggingElement.type !== "selection") {
|
||||||
if (widthAspectRatio) {
|
if (widthAspectRatio) {
|
||||||
height = width / widthAspectRatio;
|
height = width / widthAspectRatio;
|
||||||
} else {
|
} else {
|
||||||
({ width, height } = getPerfectElementSize(
|
// Depending on where the cursor is at (x, y) relative to where the starting point is
|
||||||
elementType,
|
// (originX, originY), we use ONLY width or height to control size increase.
|
||||||
width,
|
// This allows the cursor to always "stick" to one of the sides of the bounding box.
|
||||||
y < originY ? -height : height,
|
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) {
|
if (height < 0) {
|
||||||
height = -height;
|
height = -height;
|
||||||
|
@@ -53,6 +53,7 @@ export { textWysiwyg } from "./textWysiwyg";
|
|||||||
export { redrawTextBoundingBox } from "./textElement";
|
export { redrawTextBoundingBox } from "./textElement";
|
||||||
export {
|
export {
|
||||||
getPerfectElementSize,
|
getPerfectElementSize,
|
||||||
|
getLockedLinearCursorAlignSize,
|
||||||
isInvisiblySmallElement,
|
isInvisiblySmallElement,
|
||||||
resizePerfectLineForNWHandler,
|
resizePerfectLineForNWHandler,
|
||||||
getNormalizedDimensions,
|
getNormalizedDimensions,
|
||||||
|
@@ -5,8 +5,15 @@ import {
|
|||||||
PointBinding,
|
PointBinding,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { distance2d, rotate, isPathALoop, getGridPoint } from "../math";
|
import {
|
||||||
import { getElementAbsoluteCoords } from ".";
|
distance2d,
|
||||||
|
rotate,
|
||||||
|
isPathALoop,
|
||||||
|
getGridPoint,
|
||||||
|
rotatePoint,
|
||||||
|
centerPoint,
|
||||||
|
} from "../math";
|
||||||
|
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
||||||
import { getElementPointsCoords } from "./bounds";
|
import { getElementPointsCoords } from "./bounds";
|
||||||
import { Point, AppState } from "../types";
|
import { Point, AppState } from "../types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
@@ -20,26 +27,32 @@ import {
|
|||||||
} from "./binding";
|
} from "./binding";
|
||||||
import { tupleToCoors } from "../utils";
|
import { tupleToCoors } from "../utils";
|
||||||
import { isBindingElement } from "./typeChecks";
|
import { isBindingElement } from "./typeChecks";
|
||||||
|
import { shouldRotateWithDiscreteAngle } from "../keys";
|
||||||
|
|
||||||
export class LinearElementEditor {
|
export class LinearElementEditor {
|
||||||
public elementId: ExcalidrawElement["id"] & {
|
public readonly elementId: ExcalidrawElement["id"] & {
|
||||||
_brand: "excalidrawLinearElementId";
|
_brand: "excalidrawLinearElementId";
|
||||||
};
|
};
|
||||||
/** indices */
|
/** indices */
|
||||||
public selectedPointsIndices: readonly number[] | null;
|
public readonly selectedPointsIndices: readonly number[] | null;
|
||||||
|
|
||||||
public pointerDownState: Readonly<{
|
public readonly pointerDownState: Readonly<{
|
||||||
prevSelectedPointsIndices: readonly number[] | null;
|
prevSelectedPointsIndices: readonly number[] | null;
|
||||||
/** index */
|
/** index */
|
||||||
lastClickedPoint: number;
|
lastClickedPoint: number;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
/** whether you're dragging a point */
|
/** whether you're dragging a point */
|
||||||
public isDragging: boolean;
|
public readonly isDragging: boolean;
|
||||||
public lastUncommittedPoint: Point | null;
|
public readonly lastUncommittedPoint: Point | null;
|
||||||
public pointerOffset: Readonly<{ x: number; y: number }>;
|
public readonly pointerOffset: Readonly<{ x: number; y: number }>;
|
||||||
public startBindingElement: ExcalidrawBindableElement | null | "keep";
|
public readonly startBindingElement:
|
||||||
public endBindingElement: ExcalidrawBindableElement | null | "keep";
|
| ExcalidrawBindableElement
|
||||||
|
| null
|
||||||
|
| "keep";
|
||||||
|
public readonly endBindingElement: ExcalidrawBindableElement | null | "keep";
|
||||||
|
public readonly hoverPointIndex: number;
|
||||||
|
public readonly midPointHovered: boolean;
|
||||||
|
|
||||||
constructor(element: NonDeleted<ExcalidrawLinearElement>, scene: Scene) {
|
constructor(element: NonDeleted<ExcalidrawLinearElement>, scene: Scene) {
|
||||||
this.elementId = element.id as string & {
|
this.elementId = element.id as string & {
|
||||||
@@ -58,13 +71,15 @@ export class LinearElementEditor {
|
|||||||
prevSelectedPointsIndices: null,
|
prevSelectedPointsIndices: null,
|
||||||
lastClickedPoint: -1,
|
lastClickedPoint: -1,
|
||||||
};
|
};
|
||||||
|
this.hoverPointIndex = -1;
|
||||||
|
this.midPointHovered = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// static methods
|
// 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
|
* @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 */
|
/** @returns whether point was dragged */
|
||||||
static handlePointDragging(
|
static handlePointDragging(
|
||||||
|
event: PointerEvent,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
setState: React.Component<any, AppState>["setState"],
|
|
||||||
scenePointerX: number,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
maybeSuggestBinding: (
|
maybeSuggestBinding: (
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
pointSceneCoords: { x: number; y: number }[],
|
pointSceneCoords: { x: number; y: number }[],
|
||||||
) => void,
|
) => void,
|
||||||
|
linearElementEditor: LinearElementEditor,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!appState.editingLinearElement) {
|
if (!linearElementEditor) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const { editingLinearElement } = appState;
|
const { selectedPointsIndices, elementId } = linearElementEditor;
|
||||||
const { selectedPointsIndices, elementId, isDragging } =
|
|
||||||
editingLinearElement;
|
|
||||||
|
|
||||||
const element = LinearElementEditor.getElement(elementId);
|
const element = LinearElementEditor.getElement(elementId);
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return false;
|
return false;
|
||||||
@@ -155,54 +168,71 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
// point that's being dragged (out of all selected points)
|
// point that's being dragged (out of all selected points)
|
||||||
const draggingPoint = element.points[
|
const draggingPoint = element.points[
|
||||||
editingLinearElement.pointerDownState.lastClickedPoint
|
linearElementEditor.pointerDownState.lastClickedPoint
|
||||||
] as [number, number] | undefined;
|
] as [number, number] | undefined;
|
||||||
|
|
||||||
if (selectedPointsIndices && draggingPoint) {
|
if (selectedPointsIndices && draggingPoint) {
|
||||||
if (isDragging === false) {
|
if (
|
||||||
setState({
|
shouldRotateWithDiscreteAngle(event) &&
|
||||||
editingLinearElement: {
|
selectedPointsIndices.length === 1 &&
|
||||||
...editingLinearElement,
|
element.points.length > 1
|
||||||
isDragging: true,
|
) {
|
||||||
},
|
const selectedIndex = selectedPointsIndices[0];
|
||||||
});
|
const referencePoint =
|
||||||
}
|
element.points[selectedIndex === 0 ? 1 : selectedIndex - 1];
|
||||||
|
|
||||||
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
||||||
element,
|
element,
|
||||||
scenePointerX - editingLinearElement.pointerOffset.x,
|
referencePoint,
|
||||||
scenePointerY - editingLinearElement.pointerOffset.y,
|
[scenePointerX, scenePointerY],
|
||||||
appState.gridSize,
|
appState.gridSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
|
LinearElementEditor.movePoints(element, [
|
||||||
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
|
{
|
||||||
|
index: selectedIndex,
|
||||||
LinearElementEditor.movePoints(
|
point: [width + referencePoint[0], height + referencePoint[1]],
|
||||||
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,
|
|
||||||
isDragging:
|
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 ===
|
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
|
// suggest bindings for first and last point if selected
|
||||||
if (isBindingElement(element, false)) {
|
if (isBindingElement(element, false)) {
|
||||||
@@ -256,10 +286,12 @@ export class LinearElementEditor {
|
|||||||
return editingLinearElement;
|
return editingLinearElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bindings: Partial<
|
const bindings: Mutable<
|
||||||
Pick<
|
Partial<
|
||||||
InstanceType<typeof LinearElementEditor>,
|
Pick<
|
||||||
"startBindingElement" | "endBindingElement"
|
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(
|
static handlePointerDown(
|
||||||
event: React.PointerEvent<HTMLCanvasElement>,
|
event: React.PointerEvent<HTMLCanvasElement>,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
setState: React.Component<any, AppState>["setState"],
|
|
||||||
history: History,
|
history: History,
|
||||||
scenePointer: { x: number; y: number },
|
scenePointer: { x: number; y: number },
|
||||||
|
linearElementEditor: LinearElementEditor,
|
||||||
): {
|
): {
|
||||||
didAddPoint: boolean;
|
didAddPoint: boolean;
|
||||||
hitElement: NonDeleted<ExcalidrawElement> | null;
|
hitElement: NonDeleted<ExcalidrawElement> | null;
|
||||||
|
linearElementEditor: LinearElementEditor | null;
|
||||||
|
isMidPoint: boolean;
|
||||||
} {
|
} {
|
||||||
const ret: ReturnType<typeof LinearElementEditor["handlePointerDown"]> = {
|
const ret: ReturnType<typeof LinearElementEditor["handlePointerDown"]> = {
|
||||||
didAddPoint: false,
|
didAddPoint: false,
|
||||||
hitElement: null,
|
hitElement: null,
|
||||||
|
linearElementEditor: null,
|
||||||
|
isMidPoint: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!appState.editingLinearElement) {
|
if (!linearElementEditor) {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { elementId } = appState.editingLinearElement;
|
const { elementId } = linearElementEditor;
|
||||||
const element = LinearElementEditor.getElement(elementId);
|
const element = LinearElementEditor.getElement(elementId);
|
||||||
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
const hittingMidPoint = LinearElementEditor.isHittingMidPoint(
|
||||||
if (event.altKey) {
|
linearElementEditor,
|
||||||
if (appState.editingLinearElement.lastUncommittedPoint == null) {
|
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, {
|
mutateElement(element, {
|
||||||
points: [
|
points: [
|
||||||
...element.points,
|
...element.points,
|
||||||
@@ -366,24 +490,23 @@ export class LinearElementEditor {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
ret.didAddPoint = true;
|
||||||
}
|
}
|
||||||
history.resumeRecording();
|
history.resumeRecording();
|
||||||
setState({
|
ret.linearElementEditor = {
|
||||||
editingLinearElement: {
|
...linearElementEditor,
|
||||||
...appState.editingLinearElement,
|
pointerDownState: {
|
||||||
pointerDownState: {
|
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
|
||||||
prevSelectedPointsIndices:
|
lastClickedPoint: -1,
|
||||||
appState.editingLinearElement.selectedPointsIndices,
|
|
||||||
lastClickedPoint: -1,
|
|
||||||
},
|
|
||||||
selectedPointsIndices: [element.points.length - 1],
|
|
||||||
lastUncommittedPoint: null,
|
|
||||||
endBindingElement: getHoveredElementForBinding(
|
|
||||||
scenePointer,
|
|
||||||
Scene.getScene(element)!,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
});
|
selectedPointsIndices: [element.points.length - 1],
|
||||||
|
lastUncommittedPoint: null,
|
||||||
|
endBindingElement: getHoveredElementForBinding(
|
||||||
|
scenePointer,
|
||||||
|
Scene.getScene(element)!,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
ret.didAddPoint = true;
|
ret.didAddPoint = true;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -397,7 +520,7 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
// if we clicked on a point, set the element as hitElement otherwise
|
// if we clicked on a point, set the element as hitElement otherwise
|
||||||
// it would get deselected if the point is outside the hitbox area
|
// it would get deselected if the point is outside the hitbox area
|
||||||
if (clickedPointIndex > -1) {
|
if (clickedPointIndex >= 0 || hittingMidPoint) {
|
||||||
ret.hitElement = element;
|
ret.hitElement = element;
|
||||||
} else {
|
} else {
|
||||||
// You might be wandering why we are storing the binding elements on
|
// 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
|
// from the end points of the `linearElement` - this is to allow disabling
|
||||||
// binding (which needs to happen at the point the user finishes moving
|
// binding (which needs to happen at the point the user finishes moving
|
||||||
// the point).
|
// the point).
|
||||||
const { startBindingElement, endBindingElement } =
|
const { startBindingElement, endBindingElement } = linearElementEditor;
|
||||||
appState.editingLinearElement;
|
|
||||||
if (isBindingEnabled(appState) && isBindingElement(element)) {
|
if (isBindingEnabled(appState) && isBindingElement(element)) {
|
||||||
bindOrUnbindLinearElement(
|
bindOrUnbindLinearElement(
|
||||||
element,
|
element,
|
||||||
@@ -432,33 +554,28 @@ export class LinearElementEditor {
|
|||||||
const nextSelectedPointsIndices =
|
const nextSelectedPointsIndices =
|
||||||
clickedPointIndex > -1 || event.shiftKey
|
clickedPointIndex > -1 || event.shiftKey
|
||||||
? event.shiftKey ||
|
? event.shiftKey ||
|
||||||
appState.editingLinearElement.selectedPointsIndices?.includes(
|
linearElementEditor.selectedPointsIndices?.includes(clickedPointIndex)
|
||||||
clickedPointIndex,
|
|
||||||
)
|
|
||||||
? normalizeSelectedPoints([
|
? normalizeSelectedPoints([
|
||||||
...(appState.editingLinearElement.selectedPointsIndices || []),
|
...(linearElementEditor.selectedPointsIndices || []),
|
||||||
clickedPointIndex,
|
clickedPointIndex,
|
||||||
])
|
])
|
||||||
: [clickedPointIndex]
|
: [clickedPointIndex]
|
||||||
: null;
|
: null;
|
||||||
|
ret.linearElementEditor = {
|
||||||
setState({
|
...linearElementEditor,
|
||||||
editingLinearElement: {
|
pointerDownState: {
|
||||||
...appState.editingLinearElement,
|
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
|
||||||
pointerDownState: {
|
lastClickedPoint: clickedPointIndex,
|
||||||
prevSelectedPointsIndices:
|
|
||||||
appState.editingLinearElement.selectedPointsIndices,
|
|
||||||
lastClickedPoint: clickedPointIndex,
|
|
||||||
},
|
|
||||||
selectedPointsIndices: nextSelectedPointsIndices,
|
|
||||||
pointerOffset: targetPoint
|
|
||||||
? {
|
|
||||||
x: scenePointer.x - targetPoint[0],
|
|
||||||
y: scenePointer.y - targetPoint[1],
|
|
||||||
}
|
|
||||||
: { x: 0, y: 0 },
|
|
||||||
},
|
},
|
||||||
});
|
selectedPointsIndices: nextSelectedPointsIndices,
|
||||||
|
pointerOffset: targetPoint
|
||||||
|
? {
|
||||||
|
x: scenePointer.x - targetPoint[0],
|
||||||
|
y: scenePointer.y - targetPoint[1],
|
||||||
|
}
|
||||||
|
: { x: 0, y: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,13 +583,13 @@ export class LinearElementEditor {
|
|||||||
event: React.PointerEvent<HTMLCanvasElement>,
|
event: React.PointerEvent<HTMLCanvasElement>,
|
||||||
scenePointerX: number,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
editingLinearElement: LinearElementEditor,
|
linearElementEditor: LinearElementEditor,
|
||||||
gridSize: number | null,
|
gridSize: number | null,
|
||||||
): LinearElementEditor {
|
): LinearElementEditor {
|
||||||
const { elementId, lastUncommittedPoint } = editingLinearElement;
|
const { elementId, lastUncommittedPoint } = linearElementEditor;
|
||||||
const element = LinearElementEditor.getElement(elementId);
|
const element = LinearElementEditor.getElement(elementId);
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return editingLinearElement;
|
return linearElementEditor;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { points } = element;
|
const { points } = element;
|
||||||
@@ -482,15 +599,33 @@ export class LinearElementEditor {
|
|||||||
if (lastPoint === lastUncommittedPoint) {
|
if (lastPoint === lastUncommittedPoint) {
|
||||||
LinearElementEditor.deletePoints(element, [points.length - 1]);
|
LinearElementEditor.deletePoints(element, [points.length - 1]);
|
||||||
}
|
}
|
||||||
return { ...editingLinearElement, lastUncommittedPoint: null };
|
return { ...linearElementEditor, lastUncommittedPoint: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPoint = LinearElementEditor.createPointAt(
|
let newPoint: Point;
|
||||||
element,
|
|
||||||
scenePointerX - editingLinearElement.pointerOffset.x,
|
if (shouldRotateWithDiscreteAngle(event) && points.length >= 2) {
|
||||||
scenePointerY - editingLinearElement.pointerOffset.y,
|
const lastCommittedPoint = points[points.length - 2];
|
||||||
gridSize,
|
|
||||||
);
|
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) {
|
if (lastPoint === lastUncommittedPoint) {
|
||||||
LinearElementEditor.movePoints(element, [
|
LinearElementEditor.movePoints(element, [
|
||||||
@@ -504,7 +639,7 @@ export class LinearElementEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...editingLinearElement,
|
...linearElementEditor,
|
||||||
lastUncommittedPoint: element.points[element.points.length - 1],
|
lastUncommittedPoint: element.points[element.points.length - 1],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -526,14 +661,14 @@ export class LinearElementEditor {
|
|||||||
/** scene coords */
|
/** scene coords */
|
||||||
static getPointsGlobalCoordinates(
|
static getPointsGlobalCoordinates(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
) {
|
): Point[] {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
return element.points.map((point) => {
|
return element.points.map((point) => {
|
||||||
let { x, y } = element;
|
let { x, y } = element;
|
||||||
[x, y] = rotate(x + point[0], y + point[1], cx, cy, element.angle);
|
[x, y] = rotate(x + point[0], y + point[1], cx, cy, element.angle);
|
||||||
return [x, y];
|
return [x, y] as const;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,7 +686,9 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
const point = element.points[index];
|
const point = element.points[index];
|
||||||
const { x, y } = element;
|
const { x, y } = element;
|
||||||
return rotate(x + point[0], y + point[1], cx, cy, element.angle);
|
return point
|
||||||
|
? rotate(x + point[0], y + point[1], cx, cy, element.angle)
|
||||||
|
: rotate(x, y, cx, cy, element.angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
static pointFromAbsoluteCoords(
|
static pointFromAbsoluteCoords(
|
||||||
@@ -577,7 +714,8 @@ export class LinearElementEditor {
|
|||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
) {
|
) {
|
||||||
const pointHandles = this.getPointsGlobalCoordinates(element);
|
const pointHandles =
|
||||||
|
LinearElementEditor.getPointsGlobalCoordinates(element);
|
||||||
let idx = pointHandles.length;
|
let idx = pointHandles.length;
|
||||||
// loop from right to left because points on the right are rendered over
|
// 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
|
// points on the left, thus should take precedence when clicking, if they
|
||||||
@@ -587,7 +725,7 @@ export class LinearElementEditor {
|
|||||||
if (
|
if (
|
||||||
distance2d(x, y, point[0], point[1]) * zoom.value <
|
distance2d(x, y, point[0], point[1]) * zoom.value <
|
||||||
// +1px to account for outline stroke
|
// +1px to account for outline stroke
|
||||||
this.POINT_HANDLE_SIZE / 2 + 1
|
LinearElementEditor.POINT_HANDLE_SIZE + 1
|
||||||
) {
|
) {
|
||||||
return idx;
|
return idx;
|
||||||
}
|
}
|
||||||
@@ -775,9 +913,9 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
if (selectedOriginPoint) {
|
if (selectedOriginPoint) {
|
||||||
offsetX =
|
offsetX =
|
||||||
selectedOriginPoint.point[0] - points[selectedOriginPoint.index][0];
|
selectedOriginPoint.point[0] + points[selectedOriginPoint.index][0];
|
||||||
offsetY =
|
offsetY =
|
||||||
selectedOriginPoint.point[1] - points[selectedOriginPoint.index][1];
|
selectedOriginPoint.point[1] + points[selectedOriginPoint.index][1];
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextPoints = points.map((point, idx) => {
|
const nextPoints = points.map((point, idx) => {
|
||||||
@@ -840,6 +978,33 @@ export class LinearElementEditor {
|
|||||||
y: element.y + rotated[1],
|
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 = (
|
const normalizeSelectedPoints = (
|
||||||
|
@@ -198,6 +198,7 @@ const getAdjustedDimensions = (
|
|||||||
element,
|
element,
|
||||||
nextWidth,
|
nextWidth,
|
||||||
nextHeight,
|
nextHeight,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
const deltaX1 = (x1 - nextX1) / 2;
|
const deltaX1 = (x1 - nextX1) / 2;
|
||||||
const deltaY1 = (y1 - nextY1) / 2;
|
const deltaY1 = (y1 - nextY1) / 2;
|
||||||
|
@@ -18,6 +18,7 @@ import {
|
|||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getResizedElementAbsoluteCoords,
|
getResizedElementAbsoluteCoords,
|
||||||
|
getCommonBoundingBox,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
import {
|
import {
|
||||||
isFreeDrawElement,
|
isFreeDrawElement,
|
||||||
@@ -137,8 +138,10 @@ export const transformElements = (
|
|||||||
transformHandleType === "se"
|
transformHandleType === "se"
|
||||||
) {
|
) {
|
||||||
resizeMultipleElements(
|
resizeMultipleElements(
|
||||||
|
pointerDownState,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
|
shouldResizeFromCenter,
|
||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
);
|
);
|
||||||
@@ -261,13 +264,15 @@ const rescalePointsInElement = (
|
|||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
|
normalizePoints: boolean,
|
||||||
) =>
|
) =>
|
||||||
isLinearElement(element) || isFreeDrawElement(element)
|
isLinearElement(element) || isFreeDrawElement(element)
|
||||||
? {
|
? {
|
||||||
points: rescalePoints(
|
points: rescalePoints(
|
||||||
0,
|
0,
|
||||||
width,
|
width,
|
||||||
rescalePoints(1, height, element.points),
|
rescalePoints(1, height, element.points, normalizePoints),
|
||||||
|
normalizePoints,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
@@ -371,6 +376,7 @@ const resizeSingleTextElement = (
|
|||||||
element,
|
element,
|
||||||
nextWidth,
|
nextWidth,
|
||||||
nextHeight,
|
nextHeight,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
const deltaX1 = (x1 - nextX1) / 2;
|
const deltaX1 = (x1 - nextX1) / 2;
|
||||||
const deltaY1 = (y1 - nextY1) / 2;
|
const deltaY1 = (y1 - nextY1) / 2;
|
||||||
@@ -412,6 +418,7 @@ export const resizeSingleElement = (
|
|||||||
stateAtResizeStart,
|
stateAtResizeStart,
|
||||||
stateAtResizeStart.width,
|
stateAtResizeStart.width,
|
||||||
stateAtResizeStart.height,
|
stateAtResizeStart.height,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
const startTopLeft: Point = [x1, y1];
|
const startTopLeft: Point = [x1, y1];
|
||||||
const startBottomRight: Point = [x2, y2];
|
const startBottomRight: Point = [x2, y2];
|
||||||
@@ -429,6 +436,7 @@ export const resizeSingleElement = (
|
|||||||
element,
|
element,
|
||||||
element.width,
|
element.width,
|
||||||
element.height,
|
element.height,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
const boundsCurrentWidth = esx2 - esx1;
|
const boundsCurrentWidth = esx2 - esx1;
|
||||||
@@ -522,6 +530,7 @@ export const resizeSingleElement = (
|
|||||||
stateAtResizeStart,
|
stateAtResizeStart,
|
||||||
eleNewWidth,
|
eleNewWidth,
|
||||||
eleNewHeight,
|
eleNewHeight,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
const newBoundsWidth = newBoundsX2 - newBoundsX1;
|
const newBoundsWidth = newBoundsX2 - newBoundsX1;
|
||||||
const newBoundsHeight = newBoundsY2 - newBoundsY1;
|
const newBoundsHeight = newBoundsY2 - newBoundsY1;
|
||||||
@@ -592,6 +601,7 @@ export const resizeSingleElement = (
|
|||||||
stateAtResizeStart,
|
stateAtResizeStart,
|
||||||
eleNewWidth,
|
eleNewWidth,
|
||||||
eleNewHeight,
|
eleNewHeight,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
// For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
|
// 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
|
// So we need to readjust (x,y) to be where the first point should be
|
||||||
@@ -637,146 +647,147 @@ export const resizeSingleElement = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resizeMultipleElements = (
|
const resizeMultipleElements = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
pointerDownState: PointerDownState,
|
||||||
|
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
transformHandleType: "nw" | "ne" | "sw" | "se",
|
transformHandleType: "nw" | "ne" | "sw" | "se",
|
||||||
|
shouldResizeFromCenter: boolean,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(elements);
|
// map selected elements to the original elements. While it never should
|
||||||
let scale: number;
|
// happen that pointerDownState.originalElements won't contain the selected
|
||||||
let getNextXY: (
|
// elements during resize, this coupling isn't guaranteed, so to ensure
|
||||||
element: NonDeletedExcalidrawElement,
|
// type safety we need to transform only those elements we filter.
|
||||||
origCoords: readonly [number, number, number, number],
|
const targetElements = selectedElements.reduce(
|
||||||
finalCoords: readonly [number, number, number, number],
|
(
|
||||||
) => { x: number; y: number };
|
acc: {
|
||||||
switch (transformHandleType) {
|
/** element at resize start */
|
||||||
case "se":
|
orig: NonDeletedExcalidrawElement;
|
||||||
scale = Math.max(
|
/** latest element */
|
||||||
(pointerX - x1) / (x2 - x1),
|
latest: NonDeletedExcalidrawElement;
|
||||||
(pointerY - y1) / (y2 - y1),
|
}[],
|
||||||
);
|
element,
|
||||||
getNextXY = (element, [origX1, origY1], [finalX1, finalY1]) => {
|
) => {
|
||||||
const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
|
const origElement = pointerDownState.originalElements.get(element.id);
|
||||||
const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
|
if (origElement) {
|
||||||
return { x, y };
|
acc.push({ orig: origElement, latest: element });
|
||||||
};
|
}
|
||||||
break;
|
return acc;
|
||||||
case "nw":
|
},
|
||||||
scale = Math.max(
|
[],
|
||||||
(x2 - pointerX) / (x2 - x1),
|
);
|
||||||
(y2 - pointerY) / (y2 - y1),
|
|
||||||
);
|
const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(
|
||||||
getNextXY = (element, [, , origX2, origY2], [, , finalX2, finalY2]) => {
|
targetElements.map(({ orig }) => orig),
|
||||||
const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
|
);
|
||||||
const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
|
const direction = transformHandleType;
|
||||||
return { x, y };
|
|
||||||
};
|
const mapDirectionsToAnchors: Record<typeof direction, Point> = {
|
||||||
break;
|
ne: [minX, maxY],
|
||||||
case "ne":
|
se: [minX, minY],
|
||||||
scale = Math.max(
|
sw: [maxX, minY],
|
||||||
(pointerX - x1) / (x2 - x1),
|
nw: [maxX, maxY],
|
||||||
(y2 - pointerY) / (y2 - y1),
|
};
|
||||||
);
|
|
||||||
getNextXY = (element, [origX1, , , origY2], [finalX1, , , finalY2]) => {
|
// anchor point must be on the opposite side of the dragged selection handle
|
||||||
const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
|
// or be the center of the selection if alt is pressed
|
||||||
const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
|
const [anchorX, anchorY]: Point = shouldResizeFromCenter
|
||||||
return { x, y };
|
? [midX, midY]
|
||||||
};
|
: mapDirectionsToAnchors[direction];
|
||||||
break;
|
|
||||||
case "sw":
|
const mapDirectionsToPointerSides: Record<
|
||||||
scale = Math.max(
|
typeof direction,
|
||||||
(x2 - pointerX) / (x2 - x1),
|
[x: boolean, y: boolean]
|
||||||
(pointerY - y1) / (y2 - y1),
|
> = {
|
||||||
);
|
ne: [pointerX >= anchorX, pointerY <= anchorY],
|
||||||
getNextXY = (element, [, origY1, origX2], [, finalY1, finalX2]) => {
|
se: [pointerX >= anchorX, pointerY >= anchorY],
|
||||||
const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
|
sw: [pointerX <= anchorX, pointerY >= anchorY],
|
||||||
const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
|
nw: [pointerX <= anchorX, pointerY <= anchorY],
|
||||||
return { x, y };
|
};
|
||||||
};
|
|
||||||
break;
|
// 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(
|
const scale =
|
||||||
(prev, element) => {
|
Math.max(
|
||||||
if (!prev) {
|
(pointerSideX * Math.abs(pointerX - anchorX)) / (maxX - minX),
|
||||||
return prev;
|
(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) {
|
if (boundTextElement) {
|
||||||
const nextFont = measureFontSizeFromWH(
|
boundTextUpdates = {
|
||||||
boundTextElement,
|
fontSize: textMeasurements.size,
|
||||||
width - BOUND_TEXT_PADDING * 2,
|
baseline: textMeasurements.baseline,
|
||||||
height - BOUND_TEXT_PADDING * 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (nextFont === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
font = {
|
|
||||||
fontSize: nextFont.size,
|
|
||||||
baseline: nextFont.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 = (
|
const rotateMultipleElements = (
|
||||||
|
@@ -1,49 +1,51 @@
|
|||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import * as constants from "../constants";
|
import * as constants from "../constants";
|
||||||
|
|
||||||
|
const EPSILON_DIGITS = 3;
|
||||||
|
|
||||||
describe("getPerfectElementSize", () => {
|
describe("getPerfectElementSize", () => {
|
||||||
it("should return height:0 if `elementType` is line and locked angle is 0", () => {
|
it("should return height:0 if `elementType` is line and locked angle is 0", () => {
|
||||||
const { height, width } = getPerfectElementSize("line", 149, 10);
|
const { height, width } = getPerfectElementSize("line", 149, 10);
|
||||||
expect(width).toEqual(149);
|
expect(width).toBeCloseTo(149, EPSILON_DIGITS);
|
||||||
expect(height).toEqual(0);
|
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
it("should return width:0 if `elementType` is line and locked angle is 90 deg (Math.PI/2)", () => {
|
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);
|
const { height, width } = getPerfectElementSize("line", 10, 140);
|
||||||
expect(width).toEqual(0);
|
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
||||||
expect(height).toEqual(140);
|
expect(height).toBeCloseTo(140, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
it("should return height:0 if `elementType` is arrow and locked angle is 0", () => {
|
it("should return height:0 if `elementType` is arrow and locked angle is 0", () => {
|
||||||
const { height, width } = getPerfectElementSize("arrow", 200, 20);
|
const { height, width } = getPerfectElementSize("arrow", 200, 20);
|
||||||
expect(width).toEqual(200);
|
expect(width).toBeCloseTo(200, EPSILON_DIGITS);
|
||||||
expect(height).toEqual(0);
|
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
it("should return width:0 if `elementType` is arrow and locked angle is 90 deg (Math.PI/2)", () => {
|
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);
|
const { height, width } = getPerfectElementSize("arrow", 10, 100);
|
||||||
expect(width).toEqual(0);
|
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
||||||
expect(height).toEqual(100);
|
expect(height).toBeCloseTo(100, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
it("should return adjust height to be width * tan(locked angle)", () => {
|
it("should return adjust height to be width * tan(locked angle)", () => {
|
||||||
const { height, width } = getPerfectElementSize("arrow", 120, 185);
|
const { height, width } = getPerfectElementSize("arrow", 120, 185);
|
||||||
expect(width).toEqual(120);
|
expect(width).toBeCloseTo(120, EPSILON_DIGITS);
|
||||||
expect(height).toEqual(208);
|
expect(height).toBeCloseTo(207.846, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
it("should return height equals to width if locked angle is 45 deg", () => {
|
it("should return height equals to width if locked angle is 45 deg", () => {
|
||||||
const { height, width } = getPerfectElementSize("arrow", 135, 145);
|
const { height, width } = getPerfectElementSize("arrow", 135, 145);
|
||||||
expect(width).toEqual(135);
|
expect(width).toBeCloseTo(135, EPSILON_DIGITS);
|
||||||
expect(height).toEqual(135);
|
expect(height).toBeCloseTo(135, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
it("should return height:0 and width:0 when width and height are 0", () => {
|
it("should return height:0 and width:0 when width and height are 0", () => {
|
||||||
const { height, width } = getPerfectElementSize("arrow", 0, 0);
|
const { height, width } = getPerfectElementSize("arrow", 0, 0);
|
||||||
expect(width).toEqual(0);
|
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
||||||
expect(height).toEqual(0);
|
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("should respond to SHIFT_LOCKING_ANGLE constant", () => {
|
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)", () => {
|
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;
|
(constants as any).SHIFT_LOCKING_ANGLE = Math.PI / 4;
|
||||||
const { height, width } = getPerfectElementSize("arrow", 120, 185);
|
const { height, width } = getPerfectElementSize("arrow", 120, 185);
|
||||||
expect(width).toEqual(120);
|
expect(width).toBeCloseTo(120, EPSILON_DIGITS);
|
||||||
expect(height).toEqual(120);
|
expect(height).toBeCloseTo(120, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -37,9 +37,7 @@ export const getPerfectElementSize = (
|
|||||||
} else if (lockedAngle === Math.PI / 2) {
|
} else if (lockedAngle === Math.PI / 2) {
|
||||||
width = 0;
|
width = 0;
|
||||||
} else {
|
} else {
|
||||||
height =
|
height = absWidth * Math.tan(lockedAngle) * Math.sign(height) || height;
|
||||||
Math.round(absWidth * Math.tan(lockedAngle)) * Math.sign(height) ||
|
|
||||||
height;
|
|
||||||
}
|
}
|
||||||
} else if (elementType !== "selection") {
|
} else if (elementType !== "selection") {
|
||||||
height = absWidth * Math.sign(height);
|
height = absWidth * Math.sign(height);
|
||||||
@@ -47,6 +45,46 @@ export const getPerfectElementSize = (
|
|||||||
return { width, height };
|
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 = (
|
export const resizePerfectLineForNWHandler = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
x: number,
|
x: number,
|
||||||
|
@@ -1,9 +1,15 @@
|
|||||||
import { ExcalidrawElement, PointerType } from "./types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
PointerType,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
import { getElementAbsoluteCoords, Bounds } from "./bounds";
|
import { getElementAbsoluteCoords, Bounds } from "./bounds";
|
||||||
import { rotate } from "../math";
|
import { rotate } from "../math";
|
||||||
import { Zoom } from "../types";
|
import { AppState, Zoom } from "../types";
|
||||||
import { isTextElement } from ".";
|
import { isTextElement } from ".";
|
||||||
|
import { isLinearElement } from "./typeChecks";
|
||||||
|
import { DEFAULT_SPACING } from "../renderer/renderScene";
|
||||||
|
|
||||||
export type TransformHandleDirection =
|
export type TransformHandleDirection =
|
||||||
| "n"
|
| "n"
|
||||||
@@ -59,8 +65,6 @@ const OMIT_SIDES_FOR_LINE_BACKSLASH = {
|
|||||||
s: true,
|
s: true,
|
||||||
n: true,
|
n: true,
|
||||||
w: true,
|
w: true,
|
||||||
ne: true,
|
|
||||||
sw: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateTransformHandle = (
|
const generateTransformHandle = (
|
||||||
@@ -82,6 +86,7 @@ export const getTransformHandlesFromCoords = (
|
|||||||
zoom: Zoom,
|
zoom: Zoom,
|
||||||
pointerType: PointerType,
|
pointerType: PointerType,
|
||||||
omitSides: { [T in TransformHandleType]?: boolean } = {},
|
omitSides: { [T in TransformHandleType]?: boolean } = {},
|
||||||
|
margin = 4,
|
||||||
): TransformHandles => {
|
): TransformHandles => {
|
||||||
const size = transformHandleSizes[pointerType];
|
const size = transformHandleSizes[pointerType];
|
||||||
const handleWidth = size / zoom.value;
|
const handleWidth = size / zoom.value;
|
||||||
@@ -94,9 +99,7 @@ export const getTransformHandlesFromCoords = (
|
|||||||
const height = y2 - y1;
|
const height = y2 - y1;
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
|
const dashedLineMargin = margin / zoom.value;
|
||||||
const dashedLineMargin = 4 / zoom.value;
|
|
||||||
|
|
||||||
const centeringOffset = (size - 8) / (2 * zoom.value);
|
const centeringOffset = (size - 8) / (2 * zoom.value);
|
||||||
|
|
||||||
const transformHandles: TransformHandles = {
|
const transformHandles: TransformHandles = {
|
||||||
@@ -230,11 +233,7 @@ export const getTransformHandles = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
let omitSides: { [T in TransformHandleType]?: boolean } = {};
|
let omitSides: { [T in TransformHandleType]?: boolean } = {};
|
||||||
if (
|
if (element.type === "freedraw" || isLinearElement(element)) {
|
||||||
element.type === "arrow" ||
|
|
||||||
element.type === "line" ||
|
|
||||||
element.type === "freedraw"
|
|
||||||
) {
|
|
||||||
if (element.points.length === 2) {
|
if (element.points.length === 2) {
|
||||||
// only check the last point because starting point is always (0,0)
|
// only check the last point because starting point is always (0,0)
|
||||||
const [, p1] = element.points;
|
const [, p1] = element.points;
|
||||||
@@ -253,12 +252,33 @@ export const getTransformHandles = (
|
|||||||
} else if (isTextElement(element)) {
|
} else if (isTextElement(element)) {
|
||||||
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
|
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
|
||||||
}
|
}
|
||||||
|
const dashedLineMargin = isLinearElement(element)
|
||||||
|
? DEFAULT_SPACING * 3
|
||||||
|
: DEFAULT_SPACING;
|
||||||
return getTransformHandlesFromCoords(
|
return getTransformHandlesFromCoords(
|
||||||
getElementAbsoluteCoords(element),
|
getElementAbsoluteCoords(element),
|
||||||
element.angle,
|
element.angle,
|
||||||
zoom,
|
zoom,
|
||||||
pointerType,
|
pointerType,
|
||||||
omitSides,
|
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;
|
updated: number;
|
||||||
link: string | null;
|
link: string | null;
|
||||||
locked: boolean;
|
locked: boolean;
|
||||||
|
customData?: Record<string, any>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type ExcalidrawSelectionElement = _ExcalidrawElementBase & {
|
export type ExcalidrawSelectionElement = _ExcalidrawElementBase & {
|
||||||
|
@@ -7,6 +7,8 @@ import {
|
|||||||
import { DEFAULT_VERSION } from "../constants";
|
import { DEFAULT_VERSION } from "../constants";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { copyTextToSystemClipboard } from "../clipboard";
|
import { copyTextToSystemClipboard } from "../clipboard";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
type StorageSizes = { scene: number; total: number };
|
type StorageSizes = { scene: number; total: number };
|
||||||
|
|
||||||
const STORAGE_SIZE_TIMEOUT = 500;
|
const STORAGE_SIZE_TIMEOUT = 500;
|
||||||
@@ -19,7 +21,9 @@ const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
|
|||||||
}, STORAGE_SIZE_TIMEOUT);
|
}, STORAGE_SIZE_TIMEOUT);
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setToastMessage: (message: string) => void;
|
setToast: (message: string) => void;
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
appState: AppState;
|
||||||
};
|
};
|
||||||
const CustomStats = (props: Props) => {
|
const CustomStats = (props: Props) => {
|
||||||
const [storageSizes, setStorageSizes] = useState<StorageSizes>({
|
const [storageSizes, setStorageSizes] = useState<StorageSizes>({
|
||||||
@@ -31,7 +35,7 @@ const CustomStats = (props: Props) => {
|
|||||||
getStorageSizes((sizes) => {
|
getStorageSizes((sizes) => {
|
||||||
setStorageSizes(sizes);
|
setStorageSizes(sizes);
|
||||||
});
|
});
|
||||||
});
|
}, [props.elements, props.appState]);
|
||||||
useEffect(() => () => getStorageSizes.cancel(), []);
|
useEffect(() => () => getStorageSizes.cancel(), []);
|
||||||
|
|
||||||
const version = getVersion();
|
const version = getVersion();
|
||||||
@@ -68,7 +72,7 @@ const CustomStats = (props: Props) => {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await copyTextToSystemClipboard(getVersion());
|
await copyTextToSystemClipboard(getVersion());
|
||||||
props.setToastMessage(t("toast.copyToClipboard"));
|
props.setToast(t("toast.copyToClipboard"));
|
||||||
} catch {}
|
} catch {}
|
||||||
}}
|
}}
|
||||||
title={t("stats.versionCopy")}
|
title={t("stats.versionCopy")}
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import polyfill from "../polyfill";
|
||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
@@ -83,6 +84,9 @@ import { jotaiStore, useAtomWithInitialValue } from "../jotai";
|
|||||||
import { reconcileElements } from "./collab/reconciliation";
|
import { reconcileElements } from "./collab/reconciliation";
|
||||||
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
||||||
|
|
||||||
|
polyfill();
|
||||||
|
window.EXCALIDRAW_THROTTLE_RENDER = true;
|
||||||
|
|
||||||
const isExcalidrawPlusSignedUser = document.cookie.includes(
|
const isExcalidrawPlusSignedUser = document.cookie.includes(
|
||||||
COOKIES.AUTH_STATE_COOKIE,
|
COOKIES.AUTH_STATE_COOKIE,
|
||||||
);
|
);
|
||||||
@@ -94,6 +98,7 @@ languageDetector.init({
|
|||||||
|
|
||||||
const initializeScene = async (opts: {
|
const initializeScene = async (opts: {
|
||||||
collabAPI: CollabAPI;
|
collabAPI: CollabAPI;
|
||||||
|
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||||
}): Promise<
|
}): Promise<
|
||||||
{ scene: ExcalidrawInitialDataState | null } & (
|
{ scene: ExcalidrawInitialDataState | null } & (
|
||||||
| { isExternalScene: true; id: string; key: string }
|
| { isExternalScene: true; id: string; key: string }
|
||||||
@@ -178,8 +183,28 @@ const initializeScene = async (opts: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (roomLinkData) {
|
if (roomLinkData) {
|
||||||
|
const { excalidrawAPI } = opts;
|
||||||
|
|
||||||
|
const scene = await opts.collabAPI.startCollaboration(roomLinkData);
|
||||||
|
|
||||||
return {
|
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,
|
isExternalScene: true,
|
||||||
id: roomLinkData.roomId,
|
id: roomLinkData.roomId,
|
||||||
key: roomLinkData.roomKey,
|
key: roomLinkData.roomKey,
|
||||||
@@ -333,23 +358,9 @@ const ExcalidrawWrapper = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
initializeScene({ collabAPI }).then(async (data) => {
|
initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => {
|
||||||
loadImages(data, /* isInitialLoad */ true);
|
loadImages(data, /* isInitialLoad */ true);
|
||||||
|
initialStatePromiseRef.current.promise.resolve(data.scene);
|
||||||
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(),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const onHashChange = async (event: HashChangeEvent) => {
|
const onHashChange = async (event: HashChangeEvent) => {
|
||||||
@@ -364,7 +375,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
}
|
}
|
||||||
excalidrawAPI.updateScene({ appState: { isLoading: true } });
|
excalidrawAPI.updateScene({ appState: { isLoading: true } });
|
||||||
|
|
||||||
initializeScene({ collabAPI }).then((data) => {
|
initializeScene({ collabAPI, excalidrawAPI }).then((data) => {
|
||||||
loadImages(data);
|
loadImages(data);
|
||||||
if (data.scene) {
|
if (data.scene) {
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
@@ -655,10 +666,15 @@ const ExcalidrawWrapper = () => {
|
|||||||
[langCode],
|
[langCode],
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderCustomStats = () => {
|
const renderCustomStats = (
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
) => {
|
||||||
return (
|
return (
|
||||||
<CustomStats
|
<CustomStats
|
||||||
setToastMessage={(message) => excalidrawAPI!.setToastMessage(message)}
|
setToast={(message) => excalidrawAPI!.setToast({ message })}
|
||||||
|
appState={appState}
|
||||||
|
elements={elements}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
1
src/global.d.ts
vendored
1
src/global.d.ts
vendored
@@ -14,6 +14,7 @@ interface Window {
|
|||||||
__EXCALIDRAW_SHA__: string | undefined;
|
__EXCALIDRAW_SHA__: string | undefined;
|
||||||
EXCALIDRAW_ASSET_PATH: string | undefined;
|
EXCALIDRAW_ASSET_PATH: string | undefined;
|
||||||
EXCALIDRAW_EXPORT_SOURCE: string;
|
EXCALIDRAW_EXPORT_SOURCE: string;
|
||||||
|
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
|
||||||
gtag: Function;
|
gtag: Function;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -48,10 +48,13 @@ const allLanguages: Language[] = [
|
|||||||
{ code: "ru-RU", label: "Русский" },
|
{ code: "ru-RU", label: "Русский" },
|
||||||
{ code: "sk-SK", label: "Slovenčina" },
|
{ code: "sk-SK", label: "Slovenčina" },
|
||||||
{ code: "sv-SE", label: "Svenska" },
|
{ code: "sv-SE", label: "Svenska" },
|
||||||
|
{ code: "sl-SI", label: "Slovenščina" },
|
||||||
{ code: "tr-TR", label: "Türkçe" },
|
{ code: "tr-TR", label: "Türkçe" },
|
||||||
{ code: "uk-UA", label: "Українська" },
|
{ code: "uk-UA", label: "Українська" },
|
||||||
{ code: "zh-CN", label: "简体中文" },
|
{ code: "zh-CN", label: "简体中文" },
|
||||||
{ code: "zh-TW", label: "繁體中文" },
|
{ code: "zh-TW", label: "繁體中文" },
|
||||||
|
{ code: "vi-VN", label: "Tiếng Việt" },
|
||||||
|
{ code: "mr-IN", label: "मराठी" },
|
||||||
].concat([defaultLang]);
|
].concat([defaultLang]);
|
||||||
|
|
||||||
export const languages: Language[] = allLanguages
|
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 ExcalidrawApp from "./excalidraw-app";
|
||||||
|
|
||||||
import "./excalidraw-app/pwa";
|
import "./excalidraw-app/pwa";
|
||||||
import "./excalidraw-app/sentry";
|
import "./excalidraw-app/sentry";
|
||||||
window.__EXCALIDRAW_SHA__ = process.env.REACT_APP_GIT_SHA;
|
window.__EXCALIDRAW_SHA__ = process.env.REACT_APP_GIT_SHA;
|
||||||
|
const rootElement = document.getElementById("root")!;
|
||||||
ReactDOM.render(<ExcalidrawApp />, document.getElementById("root"));
|
const root = createRoot(rootElement);
|
||||||
|
root.render(
|
||||||
|
<StrictMode>
|
||||||
|
<ExcalidrawApp />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
|
@@ -80,5 +80,5 @@ export const shouldMaintainAspectRatio = (event: MouseEvent | KeyboardEvent) =>
|
|||||||
event.shiftKey;
|
event.shiftKey;
|
||||||
|
|
||||||
export const shouldRotateWithDiscreteAngle = (
|
export const shouldRotateWithDiscreteAngle = (
|
||||||
event: MouseEvent | KeyboardEvent,
|
event: MouseEvent | KeyboardEvent | React.PointerEvent<HTMLCanvasElement>,
|
||||||
) => event.shiftKey;
|
) => event.shiftKey;
|
||||||
|
@@ -120,7 +120,13 @@
|
|||||||
"lockAll": "",
|
"lockAll": "",
|
||||||
"unlockAll": ""
|
"unlockAll": ""
|
||||||
},
|
},
|
||||||
"statusPublished": ""
|
"statusPublished": "",
|
||||||
|
"sidebarLock": ""
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"noItems": "",
|
||||||
|
"hint_emptyLibrary": "",
|
||||||
|
"hint_emptyPrivateLibrary": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "إعادة تعيين اللوحة",
|
"clearReset": "إعادة تعيين اللوحة",
|
||||||
|
@@ -120,7 +120,13 @@
|
|||||||
"lockAll": "",
|
"lockAll": "",
|
||||||
"unlockAll": ""
|
"unlockAll": ""
|
||||||
},
|
},
|
||||||
"statusPublished": ""
|
"statusPublished": "",
|
||||||
|
"sidebarLock": ""
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"noItems": "",
|
||||||
|
"hint_emptyLibrary": "",
|
||||||
|
"hint_emptyPrivateLibrary": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Нулиране на платно",
|
"clearReset": "Нулиране на платно",
|
||||||
|
@@ -120,7 +120,13 @@
|
|||||||
"lockAll": "",
|
"lockAll": "",
|
||||||
"unlockAll": ""
|
"unlockAll": ""
|
||||||
},
|
},
|
||||||
"statusPublished": ""
|
"statusPublished": "",
|
||||||
|
"sidebarLock": ""
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"noItems": "",
|
||||||
|
"hint_emptyLibrary": "",
|
||||||
|
"hint_emptyPrivateLibrary": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "",
|
"clearReset": "",
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
"copy": "Copia",
|
"copy": "Copia",
|
||||||
"copyAsPng": "Copia al porta-retalls com a PNG",
|
"copyAsPng": "Copia al porta-retalls com a PNG",
|
||||||
"copyAsSvg": "Copia al porta-retalls com a SVG",
|
"copyAsSvg": "Copia al porta-retalls com a SVG",
|
||||||
"copyText": "",
|
"copyText": "Copia al porta-retalls com a text",
|
||||||
"bringForward": "Porta endavant",
|
"bringForward": "Porta endavant",
|
||||||
"sendToBack": "Envia enrere",
|
"sendToBack": "Envia enrere",
|
||||||
"bringToFront": "Porta al davant",
|
"bringToFront": "Porta al davant",
|
||||||
@@ -108,19 +108,25 @@
|
|||||||
"decreaseFontSize": "Redueix la mida de la lletra",
|
"decreaseFontSize": "Redueix la mida de la lletra",
|
||||||
"increaseFontSize": "Augmenta la mida de la lletra",
|
"increaseFontSize": "Augmenta la mida de la lletra",
|
||||||
"unbindText": "Desvincular el text",
|
"unbindText": "Desvincular el text",
|
||||||
"bindText": "",
|
"bindText": "Ajusta el text al contenidor",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "Edita l'enllaç",
|
"edit": "Edita l'enllaç",
|
||||||
"create": "Crea un enllaç",
|
"create": "Crea un enllaç",
|
||||||
"label": "Enllaç"
|
"label": "Enllaç"
|
||||||
},
|
},
|
||||||
"elementLock": {
|
"elementLock": {
|
||||||
"lock": "",
|
"lock": "Bloca",
|
||||||
"unlock": "",
|
"unlock": "Desbloca",
|
||||||
"lockAll": "",
|
"lockAll": "Bloca-ho tot",
|
||||||
"unlockAll": ""
|
"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": {
|
"buttons": {
|
||||||
"clearReset": "Neteja el llenç",
|
"clearReset": "Neteja el llenç",
|
||||||
@@ -168,7 +174,7 @@
|
|||||||
"couldNotLoadInvalidFile": "No s'ha pogut carregar un fitxer no vàlid",
|
"couldNotLoadInvalidFile": "No s'ha pogut carregar un fitxer no vàlid",
|
||||||
"importBackendFailed": "Importació fallida.",
|
"importBackendFailed": "Importació fallida.",
|
||||||
"cannotExportEmptyCanvas": "No es pot exportar un llenç buit.",
|
"cannotExportEmptyCanvas": "No es pot exportar un llenç buit.",
|
||||||
"couldNotCopyToClipboard": "",
|
"couldNotCopyToClipboard": "No s'ha pogut copiar al porta-retalls.",
|
||||||
"decryptFailed": "No s'ha pogut desencriptar.",
|
"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.",
|
"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?",
|
"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}}.",
|
"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.",
|
"svgImageInsertError": "No ha estat possible inserir la imatge SVG. Les marques SVG semblen invàlides.",
|
||||||
"invalidSVGString": "SVG no vàlid.",
|
"invalidSVGString": "SVG no vàlid.",
|
||||||
"cannotResolveCollabServer": "",
|
"cannotResolveCollabServer": "No ha estat possible connectar amb el servidor collab. Si us plau recarregueu la pàgina i torneu a provar.",
|
||||||
"importLibraryError": ""
|
"importLibraryError": "No s'ha pogut carregar la biblioteca"
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selecció",
|
"selection": "Selecció",
|
||||||
@@ -206,7 +212,7 @@
|
|||||||
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar",
|
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar",
|
||||||
"penMode": "Evita el zoom i accepta solament el dibuix lliure amb bolígraf",
|
"penMode": "Evita el zoom i accepta solament el dibuix lliure amb bolígraf",
|
||||||
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
|
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
|
||||||
"eraser": ""
|
"eraser": "Esborrador"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Accions del llenç",
|
"canvasActions": "Accions del llenç",
|
||||||
@@ -232,7 +238,7 @@
|
|||||||
"publishLibrary": "Publiqueu la vostra pròpia llibreria",
|
"publishLibrary": "Publiqueu la vostra pròpia llibreria",
|
||||||
"bindTextToElement": "Premeu enter per a afegir-hi text",
|
"bindTextToElement": "Premeu enter per a afegir-hi text",
|
||||||
"deepBoxSelect": "Manteniu CtrlOrCmd per a selecció profunda, i per a evitar l'arrossegament",
|
"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": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "No es pot mostrar la previsualització",
|
"cannotShowPreview": "No es pot mostrar la previsualització",
|
||||||
@@ -292,7 +298,7 @@
|
|||||||
"howto": "Seguiu les nostres guies",
|
"howto": "Seguiu les nostres guies",
|
||||||
"or": "o",
|
"or": "o",
|
||||||
"preventBinding": "Prevenir vinculació de la fletxa",
|
"preventBinding": "Prevenir vinculació de la fletxa",
|
||||||
"tools": "",
|
"tools": "Eines",
|
||||||
"shortcuts": "Dreceres de teclat",
|
"shortcuts": "Dreceres de teclat",
|
||||||
"textFinish": "Finalitza l'edició (editor de text)",
|
"textFinish": "Finalitza l'edició (editor de text)",
|
||||||
"textNewLine": "Afegeix una línia nova (editor de text)",
|
"textNewLine": "Afegeix una línia nova (editor de text)",
|
||||||
@@ -300,7 +306,7 @@
|
|||||||
"view": "Visualització",
|
"view": "Visualització",
|
||||||
"zoomToFit": "Zoom per veure tots els elements",
|
"zoomToFit": "Zoom per veure tots els elements",
|
||||||
"zoomToSelection": "Zoom per veure la selecció",
|
"zoomToSelection": "Zoom per veure la selecció",
|
||||||
"toggleElementLock": ""
|
"toggleElementLock": "Blocar/desblocar la selecció"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": "Neteja el llenç"
|
"title": "Neteja el llenç"
|
||||||
@@ -318,7 +324,7 @@
|
|||||||
"authorName": "Nom o usuari",
|
"authorName": "Nom o usuari",
|
||||||
"libraryName": "Nom de la vostra biblioteca",
|
"libraryName": "Nom de la vostra biblioteca",
|
||||||
"libraryDesc": "Descripció de la biblioteca per a ajudar a la gent a entendre'n el funcionament",
|
"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",
|
"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)"
|
"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:",
|
"noteItems": "Cada element de la biblioteca ha de tenir el seu propi nom per tal que sigui filtrable. S'hi inclouran els elements següents:",
|
||||||
"atleastOneLibItem": "Si us plau, seleccioneu si més no un element de la biblioteca per a començar",
|
"atleastOneLibItem": "Si us plau, seleccioneu si més no un element de la biblioteca per a començar",
|
||||||
"republishWarning": ""
|
"republishWarning": "Nota: alguns dels elements seleccionats s'han marcat com a publicats/enviats. Només hauríeu de reenviar elements quan actualitzeu una biblioteca existent."
|
||||||
},
|
},
|
||||||
"publishSuccessDialog": {
|
"publishSuccessDialog": {
|
||||||
"title": "Biblioteca enviada",
|
"title": "Biblioteca enviada",
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
"copy": "Kopírovat",
|
"copy": "Kopírovat",
|
||||||
"copyAsPng": "Zkopírovat do schránky jako PNG",
|
"copyAsPng": "Zkopírovat do schránky jako PNG",
|
||||||
"copyAsSvg": "Zkopírovat do schránky jako SVG",
|
"copyAsSvg": "Zkopírovat do schránky jako SVG",
|
||||||
"copyText": "",
|
"copyText": "Zkopírovat do schránky jako text",
|
||||||
"bringForward": "Přenést blíž",
|
"bringForward": "Přenést blíž",
|
||||||
"sendToBack": "Přenést do pozadí",
|
"sendToBack": "Přenést do pozadí",
|
||||||
"bringToFront": "Přenést do popředí",
|
"bringToFront": "Přenést do popředí",
|
||||||
@@ -36,14 +36,14 @@
|
|||||||
"arrowhead_arrow": "Šipka",
|
"arrowhead_arrow": "Šipka",
|
||||||
"arrowhead_bar": "Kóta",
|
"arrowhead_bar": "Kóta",
|
||||||
"arrowhead_dot": "Tečka",
|
"arrowhead_dot": "Tečka",
|
||||||
"arrowhead_triangle": "",
|
"arrowhead_triangle": "Trojúhelník",
|
||||||
"fontSize": "Velikost písma",
|
"fontSize": "Velikost písma",
|
||||||
"fontFamily": "Písmo",
|
"fontFamily": "Písmo",
|
||||||
"onlySelected": "Pouze vybrané",
|
"onlySelected": "Pouze vybrané",
|
||||||
"withBackground": "",
|
"withBackground": "Pozadí",
|
||||||
"exportEmbedScene": "",
|
"exportEmbedScene": "Vložit scénu",
|
||||||
"exportEmbedScene_details": "",
|
"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": "",
|
"addWatermark": "Přidat \"Vyrobeno s Excalidraw\"",
|
||||||
"handDrawn": "Od ruky",
|
"handDrawn": "Od ruky",
|
||||||
"normal": "Normální",
|
"normal": "Normální",
|
||||||
"code": "Kód",
|
"code": "Kód",
|
||||||
@@ -51,10 +51,10 @@
|
|||||||
"medium": "Střední",
|
"medium": "Střední",
|
||||||
"large": "Velké",
|
"large": "Velké",
|
||||||
"veryLarge": "Velmi velké",
|
"veryLarge": "Velmi velké",
|
||||||
"solid": "",
|
"solid": "Plný",
|
||||||
"hachure": "",
|
"hachure": "",
|
||||||
"crossHatch": "",
|
"crossHatch": "",
|
||||||
"thin": "",
|
"thin": "Tenký",
|
||||||
"bold": "",
|
"bold": "",
|
||||||
"left": "",
|
"left": "",
|
||||||
"center": "",
|
"center": "",
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
"canvasColors": "",
|
"canvasColors": "",
|
||||||
"canvasBackground": "Pozadí plátna",
|
"canvasBackground": "Pozadí plátna",
|
||||||
"drawingCanvas": "",
|
"drawingCanvas": "",
|
||||||
"layers": "",
|
"layers": "Vrstvy",
|
||||||
"actions": "",
|
"actions": "",
|
||||||
"language": "",
|
"language": "",
|
||||||
"liveCollaboration": "",
|
"liveCollaboration": "",
|
||||||
@@ -87,16 +87,16 @@
|
|||||||
"libraries": "",
|
"libraries": "",
|
||||||
"loadingScene": "",
|
"loadingScene": "",
|
||||||
"align": "",
|
"align": "",
|
||||||
"alignTop": "",
|
"alignTop": "Zarovnat nahoru",
|
||||||
"alignBottom": "",
|
"alignBottom": "Zarovnat dolů",
|
||||||
"alignLeft": "",
|
"alignLeft": "Zarovnat vlevo",
|
||||||
"alignRight": "",
|
"alignRight": "Zarovnejte vpravo",
|
||||||
"centerVertically": "",
|
"centerVertically": "",
|
||||||
"centerHorizontally": "",
|
"centerHorizontally": "",
|
||||||
"distributeHorizontally": "",
|
"distributeHorizontally": "",
|
||||||
"distributeVertically": "",
|
"distributeVertically": "",
|
||||||
"flipHorizontal": "",
|
"flipHorizontal": "Převrátit vodorovně",
|
||||||
"flipVertical": "",
|
"flipVertical": "Převrátit svisle",
|
||||||
"viewMode": "Náhled",
|
"viewMode": "Náhled",
|
||||||
"toggleExportColorScheme": "",
|
"toggleExportColorScheme": "",
|
||||||
"share": "Sdílet",
|
"share": "Sdílet",
|
||||||
@@ -120,7 +120,13 @@
|
|||||||
"lockAll": "",
|
"lockAll": "",
|
||||||
"unlockAll": ""
|
"unlockAll": ""
|
||||||
},
|
},
|
||||||
"statusPublished": ""
|
"statusPublished": "",
|
||||||
|
"sidebarLock": ""
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"noItems": "",
|
||||||
|
"hint_emptyLibrary": "",
|
||||||
|
"hint_emptyPrivateLibrary": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "",
|
"clearReset": "",
|
||||||
|
@@ -120,7 +120,13 @@
|
|||||||
"lockAll": "",
|
"lockAll": "",
|
||||||
"unlockAll": ""
|
"unlockAll": ""
|
||||||
},
|
},
|
||||||
"statusPublished": ""
|
"statusPublished": "",
|
||||||
|
"sidebarLock": ""
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"noItems": "",
|
||||||
|
"hint_emptyLibrary": "",
|
||||||
|
"hint_emptyPrivateLibrary": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "",
|
"clearReset": "",
|
||||||
|
@@ -120,7 +120,13 @@
|
|||||||
"lockAll": "Alle sperren",
|
"lockAll": "Alle sperren",
|
||||||
"unlockAll": "Alle entsperren"
|
"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": {
|
"buttons": {
|
||||||
"clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen",
|
"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:",
|
"noteItems": "Jedes Bibliothekselement muss einen eigenen Namen haben, damit es gefiltert werden kann. Die folgenden Bibliothekselemente werden hinzugefügt:",
|
||||||
"atleastOneLibItem": "Bitte wähle mindestens ein Bibliothekselement aus, um zu beginnen",
|
"atleastOneLibItem": "Bitte wähle mindestens ein Bibliothekselement aus, um zu beginnen",
|
||||||
"republishWarning": ""
|
"republishWarning": "Hinweis: Einige der ausgewählten Elemente sind bereits als veröffentlicht/eingereicht markiert. Du solltest Elemente nur erneut einreichen, wenn Du eine existierende Bibliothek oder Einreichung aktualisierst."
|
||||||
},
|
},
|
||||||
"publishSuccessDialog": {
|
"publishSuccessDialog": {
|
||||||
"title": "Bibliothek übermittelt",
|
"title": "Bibliothek übermittelt",
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
"copy": "Αντιγραφή",
|
"copy": "Αντιγραφή",
|
||||||
"copyAsPng": "Αντιγραφή στο πρόχειρο ως PNG",
|
"copyAsPng": "Αντιγραφή στο πρόχειρο ως PNG",
|
||||||
"copyAsSvg": "Αντιγραφή στο πρόχειρο ως SVG",
|
"copyAsSvg": "Αντιγραφή στο πρόχειρο ως SVG",
|
||||||
"copyText": "",
|
"copyText": "Αντιγραφή στο πρόχειρο ως κείμενο",
|
||||||
"bringForward": "Στο προσκήνιο",
|
"bringForward": "Στο προσκήνιο",
|
||||||
"sendToBack": "Ένα επίπεδο πίσω",
|
"sendToBack": "Ένα επίπεδο πίσω",
|
||||||
"bringToFront": "Ένα επίπεδο μπροστά",
|
"bringToFront": "Ένα επίπεδο μπροστά",
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
"fontFamily": "Γραμματοσειρά",
|
"fontFamily": "Γραμματοσειρά",
|
||||||
"onlySelected": "Μόνο τα Επιλεγμένα",
|
"onlySelected": "Μόνο τα Επιλεγμένα",
|
||||||
"withBackground": "Φόντο",
|
"withBackground": "Φόντο",
|
||||||
"exportEmbedScene": "",
|
"exportEmbedScene": "Ενσωμάτωση σκηνής",
|
||||||
"exportEmbedScene_details": "Τα δεδομένα σκηνής θα αποθηκευτούν στο αρχείο PNG/SVG προς εξαγωγή ώστε η σκηνή να είναι δυνατό να αποκατασταθεί από αυτό.\nΘα αυξήσει το μέγεθος του αρχείου προς εξαγωγή.",
|
"exportEmbedScene_details": "Τα δεδομένα σκηνής θα αποθηκευτούν στο αρχείο PNG/SVG προς εξαγωγή ώστε η σκηνή να είναι δυνατό να αποκατασταθεί από αυτό.\nΘα αυξήσει το μέγεθος του αρχείου προς εξαγωγή.",
|
||||||
"addWatermark": "Προσθήκη \"Φτιαγμένο με Excalidraw\"",
|
"addWatermark": "Προσθήκη \"Φτιαγμένο με Excalidraw\"",
|
||||||
"handDrawn": "Σχεδιασμένο στο χέρι",
|
"handDrawn": "Σχεδιασμένο στο χέρι",
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
"cartoonist": "Σκιτσογράφος",
|
"cartoonist": "Σκιτσογράφος",
|
||||||
"fileTitle": "Όνομα αρχείου",
|
"fileTitle": "Όνομα αρχείου",
|
||||||
"colorPicker": "Επιλογή Χρώματος",
|
"colorPicker": "Επιλογή Χρώματος",
|
||||||
"canvasColors": "",
|
"canvasColors": "Χρησιμοποείται στον καμβά",
|
||||||
"canvasBackground": "Φόντο καμβά",
|
"canvasBackground": "Φόντο καμβά",
|
||||||
"drawingCanvas": "Σχεδίαση καμβά",
|
"drawingCanvas": "Σχεδίαση καμβά",
|
||||||
"layers": "Στρώματα",
|
"layers": "Στρώματα",
|
||||||
@@ -105,22 +105,28 @@
|
|||||||
"toggleTheme": "Εναλλαγή θέματος",
|
"toggleTheme": "Εναλλαγή θέματος",
|
||||||
"personalLib": "Προσωπική Βιβλιοθήκη",
|
"personalLib": "Προσωπική Βιβλιοθήκη",
|
||||||
"excalidrawLib": "Βιβλιοθήκη Excalidraw",
|
"excalidrawLib": "Βιβλιοθήκη Excalidraw",
|
||||||
"decreaseFontSize": "",
|
"decreaseFontSize": "Μείωση μεγέθους γραμματοσειράς",
|
||||||
"increaseFontSize": "",
|
"increaseFontSize": "Αύξηση μεγέθους γραμματοσειράς",
|
||||||
"unbindText": "",
|
"unbindText": "Αποσύνδεση κειμένου",
|
||||||
"bindText": "",
|
"bindText": "Δέσμευση κειμένου στο δοχείο",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "",
|
"edit": "Επεξεργασία συνδέσμου",
|
||||||
"create": "",
|
"create": "Δημιουργία συνδέσμου",
|
||||||
"label": ""
|
"label": "Σύνδεσμος"
|
||||||
},
|
},
|
||||||
"elementLock": {
|
"elementLock": {
|
||||||
"lock": "",
|
"lock": "Κλείδωμα",
|
||||||
"unlock": "",
|
"unlock": "Ξεκλείδωμα",
|
||||||
"lockAll": "",
|
"lockAll": "Κλείδωμα όλων",
|
||||||
"unlockAll": ""
|
"unlockAll": "Ξεκλείδωμα όλων"
|
||||||
},
|
},
|
||||||
"statusPublished": ""
|
"statusPublished": "Δημοσιευμένο",
|
||||||
|
"sidebarLock": "Κρατήστε την πλαϊνή μπάρα ανοιχτή"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"noItems": "Δεν έχουν προστεθεί αντικείμενα ακόμη...",
|
||||||
|
"hint_emptyLibrary": "Επιλέξτε ένα στοιχείο στον καμβά για να το προσθέσετε εδώ, ή εγκαταστήστε μια βιβλιοθήκη από το δημόσιο αποθετήριο, παρακάτω.",
|
||||||
|
"hint_emptyPrivateLibrary": "Επιλέξτε ένα στοιχείο στον καμβά για να το προσθέσετε εδώ."
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Επαναφορά του καμβά",
|
"clearReset": "Επαναφορά του καμβά",
|
||||||
@@ -168,7 +174,7 @@
|
|||||||
"couldNotLoadInvalidFile": "Δεν μπόρεσε να ανοίξει εσφαλμένο αρχείο",
|
"couldNotLoadInvalidFile": "Δεν μπόρεσε να ανοίξει εσφαλμένο αρχείο",
|
||||||
"importBackendFailed": "Η εισαγωγή από το backend απέτυχε.",
|
"importBackendFailed": "Η εισαγωγή από το backend απέτυχε.",
|
||||||
"cannotExportEmptyCanvas": "Δεν είναι δυνατή η εξαγωγή κενού καμβά.",
|
"cannotExportEmptyCanvas": "Δεν είναι δυνατή η εξαγωγή κενού καμβά.",
|
||||||
"couldNotCopyToClipboard": "",
|
"couldNotCopyToClipboard": "Αδυναμία αντιγραφής στο πρόχειρο.",
|
||||||
"decryptFailed": "Δεν ήταν δυνατή η αποκρυπτογράφηση δεδομένων.",
|
"decryptFailed": "Δεν ήταν δυνατή η αποκρυπτογράφηση δεδομένων.",
|
||||||
"uploadedSecurly": "Η μεταφόρτωση έχει εξασφαλιστεί με κρυπτογράφηση από άκρο σε άκρο, πράγμα που σημαίνει ότι ο διακομιστής Excalidraw και τρίτα μέρη δεν μπορούν να διαβάσουν το περιεχόμενο.",
|
"uploadedSecurly": "Η μεταφόρτωση έχει εξασφαλιστεί με κρυπτογράφηση από άκρο σε άκρο, πράγμα που σημαίνει ότι ο διακομιστής Excalidraw και τρίτα μέρη δεν μπορούν να διαβάσουν το περιεχόμενο.",
|
||||||
"loadSceneOverridePrompt": "Η φόρτωση εξωτερικού σχεδίου θα αντικαταστήσει το υπάρχον περιεχόμενο. Επιθυμείτε να συνεχίσετε;",
|
"loadSceneOverridePrompt": "Η φόρτωση εξωτερικού σχεδίου θα αντικαταστήσει το υπάρχον περιεχόμενο. Επιθυμείτε να συνεχίσετε;",
|
||||||
@@ -176,21 +182,21 @@
|
|||||||
"errorAddingToLibrary": "Αδυναμία προσθήκης αντικειμένου στη βιβλιοθήκη",
|
"errorAddingToLibrary": "Αδυναμία προσθήκης αντικειμένου στη βιβλιοθήκη",
|
||||||
"errorRemovingFromLibrary": "Αδυναμία αφαίρεσης αντικειμένου από τη βιβλιοθήκη",
|
"errorRemovingFromLibrary": "Αδυναμία αφαίρεσης αντικειμένου από τη βιβλιοθήκη",
|
||||||
"confirmAddLibrary": "Αυτό θα προσθέσει {{numShapes}} σχήμα(τα) στη βιβλιοθήκη σας. Είστε σίγουροι;",
|
"confirmAddLibrary": "Αυτό θα προσθέσει {{numShapes}} σχήμα(τα) στη βιβλιοθήκη σας. Είστε σίγουροι;",
|
||||||
"imageDoesNotContainScene": "",
|
"imageDoesNotContainScene": "Αυτή η εικόνα δεν φαίνεται να περιέχει δεδομένα σκηνής. Έχετε ενεργοποιήσει την ενσωμάτωση σκηνής κατά την εξαγωγή;",
|
||||||
"cannotRestoreFromImage": "Η σκηνή δεν ήταν δυνατό να αποκατασταθεί από αυτό το αρχείο εικόνας",
|
"cannotRestoreFromImage": "Η σκηνή δεν ήταν δυνατό να αποκατασταθεί από αυτό το αρχείο εικόνας",
|
||||||
"invalidSceneUrl": "",
|
"invalidSceneUrl": "Δεν ήταν δυνατή η εισαγωγή σκηνής από το URL που δώσατε. Είτε έχει λάθος μορφή, είτε δεν περιέχει έγκυρα δεδομένα JSON Excalidraw.",
|
||||||
"resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;",
|
"resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;",
|
||||||
"removeItemsFromsLibrary": "",
|
"removeItemsFromsLibrary": "Διαγραφή {{count}} αντικειμένου(ων) από τη βιβλιοθήκη;",
|
||||||
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη."
|
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη."
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Μη υποστηριζόμενος τύπος αρχείου.",
|
"unsupportedFileType": "Μη υποστηριζόμενος τύπος αρχείου.",
|
||||||
"imageInsertError": "Αδυναμία εισαγωγής εικόνας. Προσπαθήστε ξανά αργότερα...",
|
"imageInsertError": "Αδυναμία εισαγωγής εικόνας. Προσπαθήστε ξανά αργότερα...",
|
||||||
"fileTooBig": "Το αρχείο είναι πολύ μεγάλο. Το μέγιστο επιτρεπόμενο μέγεθος είναι {{maxSize}}.",
|
"fileTooBig": "Το αρχείο είναι πολύ μεγάλο. Το μέγιστο επιτρεπόμενο μέγεθος είναι {{maxSize}}.",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "Αδυναμία εισαγωγής εικόνας SVG. Η σήμανση της SVG δεν φαίνεται έγκυρη.",
|
||||||
"invalidSVGString": "Μη έγκυρο SVG.",
|
"invalidSVGString": "Μη έγκυρο SVG.",
|
||||||
"cannotResolveCollabServer": "",
|
"cannotResolveCollabServer": "Αδυναμία σύνδεσης με τον διακομιστή συνεργασίας. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά.",
|
||||||
"importLibraryError": ""
|
"importLibraryError": "Αδυναμία φόρτωσης βιβλιοθήκης"
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Επιλογή",
|
"selection": "Επιλογή",
|
||||||
@@ -205,8 +211,8 @@
|
|||||||
"library": "Βιβλιοθήκη",
|
"library": "Βιβλιοθήκη",
|
||||||
"lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο",
|
"lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο",
|
||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
|
||||||
"eraser": ""
|
"eraser": "Γόμα"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Ενέργειες καμβά",
|
"canvasActions": "Ενέργειες καμβά",
|
||||||
@@ -223,16 +229,16 @@
|
|||||||
"linearElementMulti": "Κάνε κλικ στο τελευταίο σημείο ή πάτησε Escape ή Enter για να τελειώσεις",
|
"linearElementMulti": "Κάνε κλικ στο τελευταίο σημείο ή πάτησε Escape ή Enter για να τελειώσεις",
|
||||||
"lockAngle": "Μπορείτε να περιορίσετε τη γωνία κρατώντας πατημένο το SHIFT",
|
"lockAngle": "Μπορείτε να περιορίσετε τη γωνία κρατώντας πατημένο το SHIFT",
|
||||||
"resize": "Μπορείς να περιορίσεις τις αναλογίες κρατώντας το SHIFT ενώ αλλάζεις μέγεθος,\nκράτησε πατημένο το ALT για αλλαγή μεγέθους από το κέντρο",
|
"resize": "Μπορείς να περιορίσεις τις αναλογίες κρατώντας το SHIFT ενώ αλλάζεις μέγεθος,\nκράτησε πατημένο το ALT για αλλαγή μεγέθους από το κέντρο",
|
||||||
"resizeImage": "",
|
"resizeImage": "Μπορείτε να αλλάξετε το μέγεθος ελεύθερα κρατώντας πατημένο το SHIFT,\nκρατήστε πατημένο το ALT για να αλλάξετε το μέγεθος από το κέντρο",
|
||||||
"rotate": "Μπορείς να περιορίσεις τις γωνίες κρατώντας πατημένο το πλήκτρο SHIFT κατά την περιστροφή",
|
"rotate": "Μπορείς να περιορίσεις τις γωνίες κρατώντας πατημένο το πλήκτρο SHIFT κατά την περιστροφή",
|
||||||
"lineEditor_info": "Διπλό-κλικ ή πιέστε Enter για να επεξεργαστείτε τα σημεία",
|
"lineEditor_info": "Διπλό-κλικ ή πιέστε Enter για να επεξεργαστείτε τα σημεία",
|
||||||
"lineEditor_pointSelected": "",
|
"lineEditor_pointSelected": "Πατήστε Διαγραφή για αφαίρεση σημείου(ων),\nCtrlOrCmd+D για αντιγραφή, ή σύρετε για μετακίνηση",
|
||||||
"lineEditor_nothingSelected": "",
|
"lineEditor_nothingSelected": "Επιλέξτε ένα σημείο για να επεξεργαστείτε (κρατήστε πατημένο το SHIFT για να επιλέξετε πολλαπλά),\nή κρατήστε πατημένο το Alt και κάντε κλικ για να προσθέσετε νέα σημεία",
|
||||||
"placeImage": "",
|
"placeImage": "Κάντε κλικ για να τοποθετήσετε την εικόνα ή κάντε κλικ και σύρετε για να ορίσετε το μέγεθός της χειροκίνητα",
|
||||||
"publishLibrary": "Δημοσιεύστε τη δική σας βιβλιοθήκη",
|
"publishLibrary": "Δημοσιεύστε τη δική σας βιβλιοθήκη",
|
||||||
"bindTextToElement": "",
|
"bindTextToElement": "Πατήστε Enter για προσθήκη κειμένου",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "Κρατήστε πατημένο το CtrlOrCmd για να επιλέξετε βαθιά, και να αποτρέψετε τη μεταφορά",
|
||||||
"eraserRevert": ""
|
"eraserRevert": "Κρατήστε πατημένο το Alt για να επαναφέρετε τα στοιχεία που σημειώθηκαν για διαγραφή"
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης",
|
"cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης",
|
||||||
@@ -260,39 +266,39 @@
|
|||||||
"desc_inProgressIntro": "Η ζωντανή συνεργασία με άλλους είναι σε ενεργή.",
|
"desc_inProgressIntro": "Η ζωντανή συνεργασία με άλλους είναι σε ενεργή.",
|
||||||
"desc_shareLink": "Μοιραστείτε τον σύνδεσμο με όποιον θέλετε να δουλέψετε μαζί:",
|
"desc_shareLink": "Μοιραστείτε τον σύνδεσμο με όποιον θέλετε να δουλέψετε μαζί:",
|
||||||
"desc_exitSession": "Η διακοπή θα σας αποσυνδέσει από το δωμάτιο, αλλά θα μπορείτε να συνεχίσετε να δουλεύετε στον πίνακα, τοπικά. Σημειώσατε ότι αυτό δεν θα επηρεάσει τον πίνακα άλλων, και θα μπορούν ακόμα να συνεισφέρουν στην δική τους έκδοση.",
|
"desc_exitSession": "Η διακοπή θα σας αποσυνδέσει από το δωμάτιο, αλλά θα μπορείτε να συνεχίσετε να δουλεύετε στον πίνακα, τοπικά. Σημειώσατε ότι αυτό δεν θα επηρεάσει τον πίνακα άλλων, και θα μπορούν ακόμα να συνεισφέρουν στην δική τους έκδοση.",
|
||||||
"shareTitle": ""
|
"shareTitle": "Συμμετάσχετε σε μια ζωντανή συνεδρία συνεργασίας για το Excalidraw"
|
||||||
},
|
},
|
||||||
"errorDialog": {
|
"errorDialog": {
|
||||||
"title": "Σφάλμα"
|
"title": "Σφάλμα"
|
||||||
},
|
},
|
||||||
"exportDialog": {
|
"exportDialog": {
|
||||||
"disk_title": "Αποθήκευση στο δίσκο",
|
"disk_title": "Αποθήκευση στο δίσκο",
|
||||||
"disk_details": "",
|
"disk_details": "Εξαγωγή δεδομένων σκηνής σε ένα αρχείο από το οποίο μπορείτε να εισάγετε αργότερα.",
|
||||||
"disk_button": "Αποθήκευση σε αρχείο",
|
"disk_button": "Αποθήκευση σε αρχείο",
|
||||||
"link_title": "Κοινόχρηστος σύνδεσμος",
|
"link_title": "Κοινόχρηστος σύνδεσμος",
|
||||||
"link_details": "Εξαγωγή ως σύνδεσμο μόνο για ανάγνωση.",
|
"link_details": "Εξαγωγή ως σύνδεσμο μόνο για ανάγνωση.",
|
||||||
"link_button": "Εξαγωγή σε Σύνδεση",
|
"link_button": "Εξαγωγή σε Σύνδεση",
|
||||||
"excalidrawplus_description": "",
|
"excalidrawplus_description": "Αποθηκεύστε τη σκηνή στο χώρο εργασίας σας Excalidraw+.",
|
||||||
"excalidrawplus_button": "Εξαγωγή",
|
"excalidrawplus_button": "Εξαγωγή",
|
||||||
"excalidrawplus_exportError": ""
|
"excalidrawplus_exportError": "Δεν ήταν δυνατή η εξαγωγή στο Excalidraw+ αυτή τη στιγμή..."
|
||||||
},
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "Διαβάστε το Blog μας",
|
"blog": "Διαβάστε το Blog μας",
|
||||||
"click": "κλικ",
|
"click": "κλικ",
|
||||||
"deepSelect": "",
|
"deepSelect": "Βαθιά επιλογή",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "Βαθιά επιλογή μέσα στο πλαίσιο και αποτροπή συρσίματος",
|
||||||
"curvedArrow": "Κυρτό βέλος",
|
"curvedArrow": "Κυρτό βέλος",
|
||||||
"curvedLine": "Κυρτή γραμμή",
|
"curvedLine": "Κυρτή γραμμή",
|
||||||
"documentation": "Εγχειρίδιο",
|
"documentation": "Εγχειρίδιο",
|
||||||
"doubleClick": "διπλό κλικ",
|
"doubleClick": "διπλό κλικ",
|
||||||
"drag": "σύρε",
|
"drag": "σύρε",
|
||||||
"editor": "Επεξεργαστής",
|
"editor": "Επεξεργαστής",
|
||||||
"editSelectedShape": "",
|
"editSelectedShape": "Επεξεργασία επιλεγμένου σχήματος (κείμενο/βέλος/γραμμή)",
|
||||||
"github": "Βρήκατε πρόβλημα; Υποβάλετε το",
|
"github": "Βρήκατε πρόβλημα; Υποβάλετε το",
|
||||||
"howto": "Ακολουθήστε τους οδηγούς μας",
|
"howto": "Ακολουθήστε τους οδηγούς μας",
|
||||||
"or": "ή",
|
"or": "ή",
|
||||||
"preventBinding": "Αποτροπή δέσμευσης βέλων",
|
"preventBinding": "Αποτροπή δέσμευσης βέλων",
|
||||||
"tools": "",
|
"tools": "Εργαλεία",
|
||||||
"shortcuts": "Συντομεύσεις πληκτρολογίου",
|
"shortcuts": "Συντομεύσεις πληκτρολογίου",
|
||||||
"textFinish": "Ολοκλήρωση επεξεργασίας (επεξεργαστής κειμένου)",
|
"textFinish": "Ολοκλήρωση επεξεργασίας (επεξεργαστής κειμένου)",
|
||||||
"textNewLine": "Προσθήκη νέας γραμμής (επεξεργαστής κειμένου)",
|
"textNewLine": "Προσθήκη νέας γραμμής (επεξεργαστής κειμένου)",
|
||||||
@@ -300,54 +306,54 @@
|
|||||||
"view": "Προβολή",
|
"view": "Προβολή",
|
||||||
"zoomToFit": "Zoom ώστε να χωρέσουν όλα τα στοιχεία",
|
"zoomToFit": "Zoom ώστε να χωρέσουν όλα τα στοιχεία",
|
||||||
"zoomToSelection": "Ζουμ στην επιλογή",
|
"zoomToSelection": "Ζουμ στην επιλογή",
|
||||||
"toggleElementLock": ""
|
"toggleElementLock": "Κλείδωμα/Ξεκλείδωμα επιλογής"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": "Καθαρισμός καμβά"
|
"title": "Καθαρισμός καμβά"
|
||||||
},
|
},
|
||||||
"publishDialog": {
|
"publishDialog": {
|
||||||
"title": "",
|
"title": "Δημοσίευση βιβλιοθήκης",
|
||||||
"itemName": "",
|
"itemName": "Όνομα αντικειμένου",
|
||||||
"authorName": "Όνομα δημιουργού",
|
"authorName": "Όνομα δημιουργού",
|
||||||
"githubUsername": "GitHub username",
|
"githubUsername": "GitHub username",
|
||||||
"twitterUsername": "Twitter username",
|
"twitterUsername": "Twitter username",
|
||||||
"libraryName": "Όνομα βιβλιοθήκης",
|
"libraryName": "Όνομα βιβλιοθήκης",
|
||||||
"libraryDesc": "",
|
"libraryDesc": "Περιγραφή βιβλιοθήκης",
|
||||||
"website": "Ιστοσελίδα",
|
"website": "Ιστοσελίδα",
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"authorName": "",
|
"authorName": "Όνομα ή όνομα χρήστη",
|
||||||
"libraryName": "",
|
"libraryName": "Όνομα της βιβλιοθήκης σας",
|
||||||
"libraryDesc": "",
|
"libraryDesc": "Περιγραφή της βιβλιοθήκης σας ώστε να βοηθήσει το κοινό να κατανοήσει τη χρήση της",
|
||||||
"githubHandle": "",
|
"githubHandle": "Όνομα χρήστη στο GitHub (προαιρετικό), ώστε να μπορείτε να επεξεργαστείτε τη βιβλιοθήκη αφού υποβληθεί για αξιολόγηση",
|
||||||
"twitterHandle": "",
|
"twitterHandle": "Όνομα χρήστη Twitter (προαιρετικό), ώστε να γνωρίζουμε σε ποιον/η να δώσουμε εύσημα κατά την προώθηση μέσω Twitter",
|
||||||
"website": ""
|
"website": "Σύνδεσμος για την προσωπική σας ιστοσελίδα ή αλλού (προαιρετικό)"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"required": "Απαιτείται",
|
"required": "Απαιτείται",
|
||||||
"website": "Εισάγετε μια έγκυρη διεύθυνση URL"
|
"website": "Εισάγετε μια έγκυρη διεύθυνση URL"
|
||||||
},
|
},
|
||||||
"noteDescription": {
|
"noteDescription": {
|
||||||
"pre": "",
|
"pre": "Υποβάλετε τη βιβλιοθήκη σας για να συμπεριληφθεί στο ",
|
||||||
"link": "",
|
"link": "δημόσιο αποθετήριο βιβλιοθήκης",
|
||||||
"post": ""
|
"post": "ώστε να χρησιμοποιηθεί από άλλα άτομα στα σχέδιά τους."
|
||||||
},
|
},
|
||||||
"noteGuidelines": {
|
"noteGuidelines": {
|
||||||
"pre": "",
|
"pre": "Η βιβλιοθήκη πρέπει πρώτα να εγκριθεί χειροκίνητα. Παρακαλώ διαβάστε τους ",
|
||||||
"link": "οδηγίες",
|
"link": "οδηγίες",
|
||||||
"post": ""
|
"post": " πριν την υποβολή. Θα χρειαστείτε έναν λογαριασμό GitHub για την επικοινωνία και για να προβείτε σε αλλαγές εφ' όσον χρειαστεί, αλλά δεν είναι αυστηρή απαίτηση."
|
||||||
},
|
},
|
||||||
"noteLicense": {
|
"noteLicense": {
|
||||||
"pre": "",
|
"pre": "Με την υποβολή, συμφωνείτε ότι η βιβλιοθήκη θα δημοσιευθεί υπό την ",
|
||||||
"link": "",
|
"link": "Άδεια MIT, ",
|
||||||
"post": ""
|
"post": "που εν συντομία σημαίνει ότι ο καθένας μπορεί να τα χρησιμοποιήσει χωρίς περιορισμούς."
|
||||||
},
|
},
|
||||||
"noteItems": "",
|
"noteItems": "Κάθε αντικείμενο της βιβλιοθήκης πρέπει να έχει το δικό του όνομα ώστε να μπορεί να φιλτραριστεί. Θα συμπεριληφθούν τα ακόλουθα αντικείμενα βιβλιοθήκης:",
|
||||||
"atleastOneLibItem": "",
|
"atleastOneLibItem": "Παρακαλώ επιλέξτε τουλάχιστον ένα αντικείμενο βιβλιοθήκης για να ξεκινήσετε",
|
||||||
"republishWarning": ""
|
"republishWarning": "Σημείωση: μερικά από τα επιλεγμένα αντικέιμενα έχουν ήδη επισημανθεί ως δημοσιευμένα/υποβεβλημένα. Θα πρέπει να υποβάλετε αντικείμενα εκ νέου μόνο για να ενημερώσετε μία ήδη υπάρχουσα βιβλιοθήκη ή υποβολή."
|
||||||
},
|
},
|
||||||
"publishSuccessDialog": {
|
"publishSuccessDialog": {
|
||||||
"title": "",
|
"title": "Η βιβλιοθήκη υποβλήθηκε",
|
||||||
"content": "",
|
"content": "Ευχαριστούμε {{authorName}}. Η βιβλιοθήκη σας έχει υποβληθεί για αξιολόγηση. Μπορείτε να παρακολουθείτε τη διαδικασία",
|
||||||
"link": "εδώ"
|
"link": "εδώ"
|
||||||
},
|
},
|
||||||
"confirmDialog": {
|
"confirmDialog": {
|
||||||
|
@@ -120,7 +120,13 @@
|
|||||||
"lockAll": "Bloquear todo",
|
"lockAll": "Bloquear todo",
|
||||||
"unlockAll": "Desbloquear 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": {
|
"buttons": {
|
||||||
"clearReset": "Limpiar lienzo y reiniciar el color de fondo",
|
"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:",
|
"noteItems": "Cada elemento de la biblioteca debe tener su propio nombre para que sea filtrable. Los siguientes elementos de la biblioteca serán incluidos:",
|
||||||
"atleastOneLibItem": "Por favor, seleccione al menos un elemento de la biblioteca para empezar",
|
"atleastOneLibItem": "Por favor, seleccione al menos un elemento de la biblioteca para empezar",
|
||||||
"republishWarning": ""
|
"republishWarning": "Nota: algunos de los elementos seleccionados están marcados como ya publicados/enviados. Sólo debería volver a enviar elementos cuando se actualice una biblioteca o envío."
|
||||||
},
|
},
|
||||||
"publishSuccessDialog": {
|
"publishSuccessDialog": {
|
||||||
"title": "Biblioteca enviada",
|
"title": "Biblioteca enviada",
|
||||||
|
@@ -115,12 +115,18 @@
|
|||||||
"label": "Esteka"
|
"label": "Esteka"
|
||||||
},
|
},
|
||||||
"elementLock": {
|
"elementLock": {
|
||||||
"lock": "",
|
"lock": "Blokeatu",
|
||||||
"unlock": "",
|
"unlock": "Desblokeatu",
|
||||||
"lockAll": "",
|
"lockAll": "Blokeatu guztiak",
|
||||||
"unlockAll": ""
|
"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": {
|
"buttons": {
|
||||||
"clearReset": "Garbitu oihala",
|
"clearReset": "Garbitu oihala",
|
||||||
@@ -190,7 +196,7 @@
|
|||||||
"svgImageInsertError": "Ezin izan da SVG irudia txertatu. SVG markak baliogabea dirudi.",
|
"svgImageInsertError": "Ezin izan da SVG irudia txertatu. SVG markak baliogabea dirudi.",
|
||||||
"invalidSVGString": "SVG baliogabea.",
|
"invalidSVGString": "SVG baliogabea.",
|
||||||
"cannotResolveCollabServer": "Ezin izan da elkarlaneko zerbitzarira konektatu. Mesedez, berriro kargatu orria eta saiatu berriro.",
|
"cannotResolveCollabServer": "Ezin izan da elkarlaneko zerbitzarira konektatu. Mesedez, berriro kargatu orria eta saiatu berriro.",
|
||||||
"importLibraryError": ""
|
"importLibraryError": "Ezin izan da liburutegia kargatu"
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Hautapena",
|
"selection": "Hautapena",
|
||||||
@@ -300,7 +306,7 @@
|
|||||||
"view": "Bistaratu",
|
"view": "Bistaratu",
|
||||||
"zoomToFit": "Egin zoom elementu guztiak ikusteko",
|
"zoomToFit": "Egin zoom elementu guztiak ikusteko",
|
||||||
"zoomToSelection": "Zooma hautapenera",
|
"zoomToSelection": "Zooma hautapenera",
|
||||||
"toggleElementLock": ""
|
"toggleElementLock": "Blokeatu/desbloketatu hautapena"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": "Garbitu oihala"
|
"title": "Garbitu oihala"
|
||||||
@@ -343,7 +349,7 @@
|
|||||||
},
|
},
|
||||||
"noteItems": "Liburutegiko elementu bakoitzak bere izena eduki behar du iragazi ahal izateko. Liburutegiko hurrengo elementuak barne daude:",
|
"noteItems": "Liburutegiko elementu bakoitzak bere izena eduki behar du iragazi ahal izateko. Liburutegiko hurrengo elementuak barne daude:",
|
||||||
"atleastOneLibItem": "Hautatu gutxienez liburutegiko elementu bat gutxienez hasi ahal izateko",
|
"atleastOneLibItem": "Hautatu gutxienez liburutegiko elementu bat gutxienez hasi ahal izateko",
|
||||||
"republishWarning": ""
|
"republishWarning": "Oharra: hautatutako elementu batzuk dagoeneko argitaratuta/bidalita bezala markatuta daude. Elementuak berriro bidali behar dituzu lehendik dagoen liburutegi edo bidalketa eguneratzen duzunean."
|
||||||
},
|
},
|
||||||
"publishSuccessDialog": {
|
"publishSuccessDialog": {
|
||||||
"title": "Liburutegia bidali da",
|
"title": "Liburutegia bidali da",
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
"copy": "کپی",
|
"copy": "کپی",
|
||||||
"copyAsPng": "کپی در حافطه موقت به صورت PNG",
|
"copyAsPng": "کپی در حافطه موقت به صورت PNG",
|
||||||
"copyAsSvg": "کپی در حافطه موقت به صورت SVG",
|
"copyAsSvg": "کپی در حافطه موقت به صورت SVG",
|
||||||
"copyText": "",
|
"copyText": "کپی در حافطه موقت به صورت متن",
|
||||||
"bringForward": "جلو آوردن",
|
"bringForward": "جلو آوردن",
|
||||||
"sendToBack": "پس فرستادن",
|
"sendToBack": "پس فرستادن",
|
||||||
"bringToFront": "جلو آوردن",
|
"bringToFront": "جلو آوردن",
|
||||||
@@ -36,12 +36,12 @@
|
|||||||
"arrowhead_arrow": "پیکان",
|
"arrowhead_arrow": "پیکان",
|
||||||
"arrowhead_bar": "میله ای",
|
"arrowhead_bar": "میله ای",
|
||||||
"arrowhead_dot": "نقطه",
|
"arrowhead_dot": "نقطه",
|
||||||
"arrowhead_triangle": "",
|
"arrowhead_triangle": "مثلث",
|
||||||
"fontSize": "اندازه قلم",
|
"fontSize": "اندازه قلم",
|
||||||
"fontFamily": "نوع قلم",
|
"fontFamily": "نوع قلم",
|
||||||
"onlySelected": "فقط انتخاب شده ها",
|
"onlySelected": "فقط انتخاب شده ها",
|
||||||
"withBackground": "پس زمینه",
|
"withBackground": "پس زمینه",
|
||||||
"exportEmbedScene": "",
|
"exportEmbedScene": "تعبیه صحنه",
|
||||||
"exportEmbedScene_details": "متحوای صحنه به فایل خروجی SVG/PNG اضافه خواهد شد برای بازیابی صحنه به آن اضافه خواهد شد.\nباعث افزایش حجم فایل خروجی میشود.",
|
"exportEmbedScene_details": "متحوای صحنه به فایل خروجی SVG/PNG اضافه خواهد شد برای بازیابی صحنه به آن اضافه خواهد شد.\nباعث افزایش حجم فایل خروجی میشود.",
|
||||||
"addWatermark": "\"ساخته شده با Excalidraw\" را اضافه کن",
|
"addWatermark": "\"ساخته شده با Excalidraw\" را اضافه کن",
|
||||||
"handDrawn": "دست نویس",
|
"handDrawn": "دست نویس",
|
||||||
@@ -65,13 +65,13 @@
|
|||||||
"cartoonist": "کارتونیست",
|
"cartoonist": "کارتونیست",
|
||||||
"fileTitle": "نام فایل",
|
"fileTitle": "نام فایل",
|
||||||
"colorPicker": "انتخابگر رنگ",
|
"colorPicker": "انتخابگر رنگ",
|
||||||
"canvasColors": "",
|
"canvasColors": "رنگ های بوم",
|
||||||
"canvasBackground": "بوم",
|
"canvasBackground": "بوم",
|
||||||
"drawingCanvas": "بوم نقاشی",
|
"drawingCanvas": "بوم نقاشی",
|
||||||
"layers": "لایه ها",
|
"layers": "لایه ها",
|
||||||
"actions": "عملیات",
|
"actions": "عملیات",
|
||||||
"language": "زبان",
|
"language": "زبان",
|
||||||
"liveCollaboration": "",
|
"liveCollaboration": "همکاری زنده",
|
||||||
"duplicateSelection": "تکرار",
|
"duplicateSelection": "تکرار",
|
||||||
"untitled": "بدون عنوان",
|
"untitled": "بدون عنوان",
|
||||||
"name": "نام",
|
"name": "نام",
|
||||||
@@ -98,29 +98,35 @@
|
|||||||
"flipHorizontal": "چرخش افقی",
|
"flipHorizontal": "چرخش افقی",
|
||||||
"flipVertical": "چرخش عمودی",
|
"flipVertical": "چرخش عمودی",
|
||||||
"viewMode": "حالت نمایش",
|
"viewMode": "حالت نمایش",
|
||||||
"toggleExportColorScheme": "",
|
"toggleExportColorScheme": "تغییر طرح خروجی رنگ",
|
||||||
"share": "اشتراکگذاری",
|
"share": "اشتراکگذاری",
|
||||||
"showStroke": "نمایش انتخاب کننده رنگ حاشیه",
|
"showStroke": "نمایش انتخاب کننده رنگ حاشیه",
|
||||||
"showBackground": "نمایش انتخاب کننده رنگ پس زمینه",
|
"showBackground": "نمایش انتخاب کننده رنگ پس زمینه",
|
||||||
"toggleTheme": "تغییر تم",
|
"toggleTheme": "تغییر تم",
|
||||||
"personalLib": "",
|
"personalLib": "کتابخانه شخصی",
|
||||||
"excalidrawLib": "",
|
"excalidrawLib": "کتابخانه",
|
||||||
"decreaseFontSize": "",
|
"decreaseFontSize": "کاهش اندازه فونت",
|
||||||
"increaseFontSize": "",
|
"increaseFontSize": "افزایش دادن اندازه فونت",
|
||||||
"unbindText": "",
|
"unbindText": "بازکردن نوشته",
|
||||||
"bindText": "",
|
"bindText": "بستن نوشته",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "",
|
"edit": "ویرایش لینک",
|
||||||
"create": "",
|
"create": "ایجاد پیوند",
|
||||||
"label": ""
|
"label": "لینک"
|
||||||
},
|
},
|
||||||
"elementLock": {
|
"elementLock": {
|
||||||
"lock": "",
|
"lock": "قفل",
|
||||||
"unlock": "",
|
"unlock": "باز کردن",
|
||||||
"lockAll": "",
|
"lockAll": "قفل همه",
|
||||||
"unlockAll": ""
|
"unlockAll": "باز کردن قفل همه"
|
||||||
},
|
},
|
||||||
"statusPublished": ""
|
"statusPublished": "منتشر شده",
|
||||||
|
"sidebarLock": ""
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"noItems": "",
|
||||||
|
"hint_emptyLibrary": "",
|
||||||
|
"hint_emptyPrivateLibrary": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "پاکسازی بوم نقاشی",
|
"clearReset": "پاکسازی بوم نقاشی",
|
||||||
@@ -147,19 +153,19 @@
|
|||||||
"edit": "ویرایش",
|
"edit": "ویرایش",
|
||||||
"undo": "بازگرد",
|
"undo": "بازگرد",
|
||||||
"redo": "از سر",
|
"redo": "از سر",
|
||||||
"resetLibrary": "",
|
"resetLibrary": "تنظیم مجدد کتابخانه",
|
||||||
"createNewRoom": "ایجاد یک اتاق جدید",
|
"createNewRoom": "ایجاد یک اتاق جدید",
|
||||||
"fullScreen": "تمامصفحه",
|
"fullScreen": "تمامصفحه",
|
||||||
"darkMode": "حالت تیره",
|
"darkMode": "حالت تیره",
|
||||||
"lightMode": "حالت روشن",
|
"lightMode": "حالت روشن",
|
||||||
"zenMode": "حالت ذن",
|
"zenMode": "حالت ذن",
|
||||||
"exitZenMode": "خروج از حالت تمرکز",
|
"exitZenMode": "خروج از حالت تمرکز",
|
||||||
"cancel": "",
|
"cancel": "لغو",
|
||||||
"clear": "",
|
"clear": "پاک کردن",
|
||||||
"remove": "",
|
"remove": "پاک کردن",
|
||||||
"publishLibrary": "",
|
"publishLibrary": "انتشار",
|
||||||
"submit": "",
|
"submit": "ارسال",
|
||||||
"confirm": ""
|
"confirm": "تایید"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "این کار کل صفحه را پاک میکند. آیا مطمئنید؟",
|
"clearReset": "این کار کل صفحه را پاک میکند. آیا مطمئنید؟",
|
||||||
@@ -168,33 +174,33 @@
|
|||||||
"couldNotLoadInvalidFile": "عدم توانایی در بازگذاری فایل نامعتبر",
|
"couldNotLoadInvalidFile": "عدم توانایی در بازگذاری فایل نامعتبر",
|
||||||
"importBackendFailed": "بارگیری از پشت صحنه با شکست مواجه شد.",
|
"importBackendFailed": "بارگیری از پشت صحنه با شکست مواجه شد.",
|
||||||
"cannotExportEmptyCanvas": "بوم خالی قابل تبدیل نیست.",
|
"cannotExportEmptyCanvas": "بوم خالی قابل تبدیل نیست.",
|
||||||
"couldNotCopyToClipboard": "",
|
"couldNotCopyToClipboard": "به کلیپ بورد کپی نشد.",
|
||||||
"decryptFailed": "رمزگشایی داده ها امکان پذیر نیست.",
|
"decryptFailed": "رمزگشایی داده ها امکان پذیر نیست.",
|
||||||
"uploadedSecurly": "آپلود با رمزگذاری دو طرفه انجام میشود، به این معنی که سرور Excalidraw و اشخاص ثالث نمی توانند مطالب شما را بخوانند.",
|
"uploadedSecurly": "آپلود با رمزگذاری دو طرفه انجام میشود، به این معنی که سرور Excalidraw و اشخاص ثالث نمی توانند مطالب شما را بخوانند.",
|
||||||
"loadSceneOverridePrompt": "بارگزاری یک طرح خارجی محتوای فعلی رو از بین میبرد. آیا میخواهید ادامه دهید؟",
|
"loadSceneOverridePrompt": "بارگزاری یک طرح خارجی محتوای فعلی رو از بین میبرد. آیا میخواهید ادامه دهید؟",
|
||||||
"collabStopOverridePrompt": "",
|
"collabStopOverridePrompt": "با توقف بوم نقاشی، نقشه قبلی و ذخیره شده محلی شما را بازنویسی می کند. مطمئنی؟\n\n(اگر می خواهید نقاشی محلی خود را حفظ کنید، به سادگی برگه مرورگر را ببندید.)",
|
||||||
"errorAddingToLibrary": "مورد به کتابخانه اضافه نشد",
|
"errorAddingToLibrary": "مورد به کتابخانه اضافه نشد",
|
||||||
"errorRemovingFromLibrary": "مورد از کتابخانه حذف نشد",
|
"errorRemovingFromLibrary": "مورد از کتابخانه حذف نشد",
|
||||||
"confirmAddLibrary": "{{numShapes}} از اشکال به کتابخانه شما اضافه خواهد شد. مطمئن هستید؟",
|
"confirmAddLibrary": "{{numShapes}} از اشکال به کتابخانه شما اضافه خواهد شد. مطمئن هستید؟",
|
||||||
"imageDoesNotContainScene": "",
|
"imageDoesNotContainScene": "به نظر نمی رسد این تصویر حاوی داده های بوم نقاشی باشد. آیا جاسازی صحنه را در حین خروجی فعال کرده اید?",
|
||||||
"cannotRestoreFromImage": "صحنه را نمی توان از این فایل تصویری بازیابی کرد",
|
"cannotRestoreFromImage": "صحنه را نمی توان از این فایل تصویری بازیابی کرد",
|
||||||
"invalidSceneUrl": "",
|
"invalidSceneUrl": "بوم نقاشی از آدرس ارائه شده وارد نشد. این یا نادرست است، یا حاوی داده Excalidraw JSON معتبر نیست.",
|
||||||
"resetLibrary": "",
|
"resetLibrary": "ین کار کل صفحه را پاک میکند. آیا مطمئنید?",
|
||||||
"removeItemsFromsLibrary": "",
|
"removeItemsFromsLibrary": "حذف {{count}} آیتم(ها) از کتابخانه?",
|
||||||
"invalidEncryptionKey": ""
|
"invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است."
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "",
|
"unsupportedFileType": "نوع فایل پشتیبانی نشده.",
|
||||||
"imageInsertError": "",
|
"imageInsertError": "عکس ارسال نشد. بعداً دوباره تلاش کنید...",
|
||||||
"fileTooBig": "",
|
"fileTooBig": "فایل خیلی بزرگ است حداکثر اندازه مجاز {{maxSize}}.",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "تصویر SVG وارد نشد. نشانه گذاری SVG نامعتبر به نظر می رسد.",
|
||||||
"invalidSVGString": "",
|
"invalidSVGString": "SVG نادرست.",
|
||||||
"cannotResolveCollabServer": "",
|
"cannotResolveCollabServer": "به سرور collab متصل نشد. لطفا صفحه را مجددا بارگذاری کنید و دوباره تلاش کنید.",
|
||||||
"importLibraryError": ""
|
"importLibraryError": "دادهها بارگذاری نشدند"
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "گزینش",
|
"selection": "گزینش",
|
||||||
"image": "",
|
"image": "وارد کردن تصویر",
|
||||||
"rectangle": "مستطیل",
|
"rectangle": "مستطیل",
|
||||||
"diamond": "لوزی",
|
"diamond": "لوزی",
|
||||||
"ellipse": "بیضی",
|
"ellipse": "بیضی",
|
||||||
@@ -204,9 +210,9 @@
|
|||||||
"text": "متن",
|
"text": "متن",
|
||||||
"library": "کتابخانه",
|
"library": "کتابخانه",
|
||||||
"lock": "ابزار انتخاب شده را بعد از کشیدن نگه دار",
|
"lock": "ابزار انتخاب شده را بعد از کشیدن نگه دار",
|
||||||
"penMode": "",
|
"penMode": "از زوم کوچک کردن جلوگیری کنید و ورودی آزاد را فقط از قلم بپذیرید",
|
||||||
"link": "",
|
"link": "افزودن/بهروزرسانی پیوند برای شکل انتخابی",
|
||||||
"eraser": ""
|
"eraser": "پاک کن"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "عملیات روی بوم",
|
"canvasActions": "عملیات روی بوم",
|
||||||
@@ -214,25 +220,25 @@
|
|||||||
"shapes": "شکلها"
|
"shapes": "شکلها"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "",
|
"canvasPanning": "برای حرکت دادن بوم، چرخ ماوس یا فاصله را در حین کشیدن نگه دارید",
|
||||||
"linearElement": "برای چند نقطه کلیک و برای یک خط بکشید",
|
"linearElement": "برای چند نقطه کلیک و برای یک خط بکشید",
|
||||||
"freeDraw": "کلیک کنید و بکشید و وقتی کار تمام شد رها کنید",
|
"freeDraw": "کلیک کنید و بکشید و وقتی کار تمام شد رها کنید",
|
||||||
"text": "نکته: با برنامه انتخاب شده شما میتوانید با دوبار کلیک کردن هرکجا میخواید متن اظاف کنید",
|
"text": "نکته: با برنامه انتخاب شده شما میتوانید با دوبار کلیک کردن هرکجا میخواید متن اظاف کنید",
|
||||||
"text_selected": "",
|
"text_selected": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید",
|
||||||
"text_editing": "",
|
"text_editing": "Escape یا CtrlOrCmd+ENTER را فشار دهید تا ویرایش تمام شود",
|
||||||
"linearElementMulti": "روی آخرین نقطه کلیک کنید یا کلید ESC را بزنید یا کلید Enter را بزنید برای اتمام کار",
|
"linearElementMulti": "روی آخرین نقطه کلیک کنید یا کلید ESC را بزنید یا کلید Enter را بزنید برای اتمام کار",
|
||||||
"lockAngle": "با نگه داشتن SHIFT هنگام چرخش می توانید زاویه ها را محدود کنید",
|
"lockAngle": "با نگه داشتن SHIFT هنگام چرخش می توانید زاویه ها را محدود کنید",
|
||||||
"resize": "می توانید با نگه داشتن SHIFT در هنگام تغییر اندازه، نسبت ها را محدود کنید،ALT را برای تغییر اندازه از مرکز نگه دارید",
|
"resize": "می توانید با نگه داشتن SHIFT در هنگام تغییر اندازه، نسبت ها را محدود کنید،ALT را برای تغییر اندازه از مرکز نگه دارید",
|
||||||
"resizeImage": "",
|
"resizeImage": "با نگه داشتن SHIFT می توانید آزادانه اندازه را تغییر دهید،\nبرای تغییر اندازه از مرکز، ALT را نگه دارید",
|
||||||
"rotate": "با نگه داشتن SHIFT هنگام چرخش می توانید زاویه ها را محدود کنید",
|
"rotate": "با نگه داشتن SHIFT هنگام چرخش می توانید زاویه ها را محدود کنید",
|
||||||
"lineEditor_info": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید",
|
"lineEditor_info": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید",
|
||||||
"lineEditor_pointSelected": "",
|
"lineEditor_pointSelected": "برای حذف نقطه Delete برای کپی زدن Ctrl یا Cmd+D را بزنید و یا برای جابجایی بکشید",
|
||||||
"lineEditor_nothingSelected": "",
|
"lineEditor_nothingSelected": "یک نقطه را برای ویرایش انتخاب کنید (SHIFT را برای انتخاب چندگانه نگه دارید)،\nیا Alt را نگه دارید و برای افزودن نقاط جدید کلیک کنید",
|
||||||
"placeImage": "",
|
"placeImage": "برای قرار دادن تصویر کلیک کنید، یا کلیک کنید و بکشید تا اندازه آن به صورت دستی تنظیم شود",
|
||||||
"publishLibrary": "",
|
"publishLibrary": "کتابخانه خود را منتشر کنید",
|
||||||
"bindTextToElement": "",
|
"bindTextToElement": "برای افزودن اینتر را بزنید",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "CtrlOrCmd را برای انتخاب عمیق و جلوگیری از کشیدن نگه دارید",
|
||||||
"eraserRevert": ""
|
"eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند"
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "پیش نمایش نشان داده نمی شود",
|
"cannotShowPreview": "پیش نمایش نشان داده نمی شود",
|
||||||
@@ -260,27 +266,27 @@
|
|||||||
"desc_inProgressIntro": "جلسه همکاری آنلاین در حال انجام است.",
|
"desc_inProgressIntro": "جلسه همکاری آنلاین در حال انجام است.",
|
||||||
"desc_shareLink": "این لینک را با هر کسی که می خواهید با او همکاری کنید به اشتراک بگذارید:",
|
"desc_shareLink": "این لینک را با هر کسی که می خواهید با او همکاری کنید به اشتراک بگذارید:",
|
||||||
"desc_exitSession": "با پایان دادن جلسه، شما از اتاق حذف میکند، اما می توانید به صورت محلی کار خود را با بوم ادامه دهید. توجه داشته باشید که این مورد بر سایر افراد تأثیر نمی گذارد و همچنان می توانند در نسخه خود همکاری کنند.",
|
"desc_exitSession": "با پایان دادن جلسه، شما از اتاق حذف میکند، اما می توانید به صورت محلی کار خود را با بوم ادامه دهید. توجه داشته باشید که این مورد بر سایر افراد تأثیر نمی گذارد و همچنان می توانند در نسخه خود همکاری کنند.",
|
||||||
"shareTitle": ""
|
"shareTitle": "به یک جلسه همکاری زنده در Excalidraw بپیوندید"
|
||||||
},
|
},
|
||||||
"errorDialog": {
|
"errorDialog": {
|
||||||
"title": "خطا"
|
"title": "خطا"
|
||||||
},
|
},
|
||||||
"exportDialog": {
|
"exportDialog": {
|
||||||
"disk_title": "ذخیره در دیسک",
|
"disk_title": "ذخیره در دیسک",
|
||||||
"disk_details": "",
|
"disk_details": "داده های صحنه را به فایلی که بعداً می توانید از آن وارد کنید صادر کنید.",
|
||||||
"disk_button": "ذخیره در فایل",
|
"disk_button": "ذخیره در فایل",
|
||||||
"link_title": "لینک قابل اشتراکگذاری",
|
"link_title": "لینک قابل اشتراکگذاری",
|
||||||
"link_details": "",
|
"link_details": "خروجی به عنوان یک پیوند فقط خواندنی.",
|
||||||
"link_button": "",
|
"link_button": "خروجی در فایل",
|
||||||
"excalidrawplus_description": "",
|
"excalidrawplus_description": "صحنه را در فضای کاری Excalidraw+ خود ذخیره کنید.",
|
||||||
"excalidrawplus_button": "خروجی گرفتن",
|
"excalidrawplus_button": "خروجی گرفتن",
|
||||||
"excalidrawplus_exportError": ""
|
"excalidrawplus_exportError": "در حال حاضر نمیتوان به Excalidraw+ صادر کرد..."
|
||||||
},
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "بلاگ ما را بخوانید",
|
"blog": "بلاگ ما را بخوانید",
|
||||||
"click": "کلیک",
|
"click": "کلیک",
|
||||||
"deepSelect": "",
|
"deepSelect": "انتخاب عمیق",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "انتخاب عمیق در کادر، و جلوگیری از کشیدن",
|
||||||
"curvedArrow": "فلش خمیده",
|
"curvedArrow": "فلش خمیده",
|
||||||
"curvedLine": "منحنی",
|
"curvedLine": "منحنی",
|
||||||
"documentation": "مستندات",
|
"documentation": "مستندات",
|
||||||
@@ -292,7 +298,7 @@
|
|||||||
"howto": "راهنمای ما را دنبال کنید",
|
"howto": "راهنمای ما را دنبال کنید",
|
||||||
"or": "یا",
|
"or": "یا",
|
||||||
"preventBinding": "مانع شدن از چسبیدن فلش ها",
|
"preventBinding": "مانع شدن از چسبیدن فلش ها",
|
||||||
"tools": "",
|
"tools": "ابزار",
|
||||||
"shortcuts": "میانبرهای صفحه کلید",
|
"shortcuts": "میانبرهای صفحه کلید",
|
||||||
"textFinish": "پایان ویرایش (ویرایشگر متن)",
|
"textFinish": "پایان ویرایش (ویرایشگر متن)",
|
||||||
"textNewLine": "افزودن خط جدید (ویرایشگر متن)",
|
"textNewLine": "افزودن خط جدید (ویرایشگر متن)",
|
||||||
@@ -300,63 +306,63 @@
|
|||||||
"view": "مشاهده",
|
"view": "مشاهده",
|
||||||
"zoomToFit": "بزرگنمایی برای دیدن تمام آیتم ها",
|
"zoomToFit": "بزرگنمایی برای دیدن تمام آیتم ها",
|
||||||
"zoomToSelection": "بزرگنمایی قسمت انتخاب شده",
|
"zoomToSelection": "بزرگنمایی قسمت انتخاب شده",
|
||||||
"toggleElementLock": ""
|
"toggleElementLock": "قفل/بازکردن انتخاب شده ها"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": ""
|
"title": "پاک کردن بوم"
|
||||||
},
|
},
|
||||||
"publishDialog": {
|
"publishDialog": {
|
||||||
"title": "",
|
"title": "انتشار کتابخانه",
|
||||||
"itemName": "",
|
"itemName": "نام آیتم",
|
||||||
"authorName": "",
|
"authorName": "نام نویسنده",
|
||||||
"githubUsername": "",
|
"githubUsername": "نام کاربری گیت هاب",
|
||||||
"twitterUsername": "",
|
"twitterUsername": "نام کاربری توییتر",
|
||||||
"libraryName": "",
|
"libraryName": "نام کتابخانه",
|
||||||
"libraryDesc": "",
|
"libraryDesc": "توضیحات کتابخانه",
|
||||||
"website": "",
|
"website": "تارنما",
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"authorName": "",
|
"authorName": "نام یا نام کاربری شما",
|
||||||
"libraryName": "",
|
"libraryName": "اسم کتابخانه",
|
||||||
"libraryDesc": "",
|
"libraryDesc": "شرحی از کتابخانه شما برای کمک به مردم برای درک استفاده از آن",
|
||||||
"githubHandle": "",
|
"githubHandle": "دسته GitHub (اختیاری)، بنابراین می توانید پس از ارسال برای بررسی، کتابخانه را ویرایش کنید",
|
||||||
"twitterHandle": "",
|
"twitterHandle": "نام کاربری توییتر (اختیاری)، بنابراین می دانیم هنگام تبلیغ در توییتر به چه کسی اعتبار دهیم",
|
||||||
"website": ""
|
"website": "پیوند به وب سایت شخصی شما یا هر جای دیگر (اختیاری)"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"required": "",
|
"required": "لازم",
|
||||||
"website": ""
|
"website": "وارد کردن آدرس درست"
|
||||||
},
|
},
|
||||||
"noteDescription": {
|
"noteDescription": {
|
||||||
"pre": "",
|
"pre": "کتابخانه خود را ارسال کنید تا در آن گنجانده شود ",
|
||||||
"link": "",
|
"link": "مخزن کتابخانه عمومی",
|
||||||
"post": ""
|
"post": "تا افراد دیگر در نقاشی های خود از آن استفاده کنند."
|
||||||
},
|
},
|
||||||
"noteGuidelines": {
|
"noteGuidelines": {
|
||||||
"pre": "",
|
"pre": "کتابخانه باید ابتدا به صورت دستی تایید شود. لطفاً بخوانید ",
|
||||||
"link": "",
|
"link": "دستورالعملها",
|
||||||
"post": ""
|
"post": " قبل از ارسال برای برقراری ارتباط و ایجاد تغییرات در صورت درخواست، به یک حساب GitHub نیاز دارید، اما به شدت الزامی نیست."
|
||||||
},
|
},
|
||||||
"noteLicense": {
|
"noteLicense": {
|
||||||
"pre": "",
|
"pre": "با ارسال، موافقت می کنید که کتابخانه تحت عنوان منتشر شود ",
|
||||||
"link": "",
|
"link": "پروانهٔ MIT ",
|
||||||
"post": ""
|
"post": "که به طور خلاصه به این معنی است که هر کسی می تواند بدون محدودیت از آنها استفاده کند."
|
||||||
},
|
},
|
||||||
"noteItems": "",
|
"noteItems": "هر مورد کتابخانه باید نام خاص خود را داشته باشد تا قابل فیلتر باشد. اقلام کتابخانه زیر شامل خواهد شد:",
|
||||||
"atleastOneLibItem": "",
|
"atleastOneLibItem": "لطفاً حداقل یک مورد از کتابخانه را برای شروع انتخاب کنید",
|
||||||
"republishWarning": ""
|
"republishWarning": "توجه: برخی از موارد انتخاب شده به عنوان قبلاً منتشر شده/ارسال شده علامت گذاری شده اند. شما فقط باید هنگام بهروزرسانی یک کتابخانه موجود یا ارسال، موارد را دوباره ارسال کنید."
|
||||||
},
|
},
|
||||||
"publishSuccessDialog": {
|
"publishSuccessDialog": {
|
||||||
"title": "",
|
"title": "کتابخانه ارسال شد",
|
||||||
"content": "",
|
"content": "تشکر از شما {{authorName}}. کتابخانه شما برای بررسی ارسال شده است. می توانید وضعیت را پیگیری کنید",
|
||||||
"link": ""
|
"link": "اینجا"
|
||||||
},
|
},
|
||||||
"confirmDialog": {
|
"confirmDialog": {
|
||||||
"resetLibrary": "",
|
"resetLibrary": "تنظیم مجدد کتابخانه",
|
||||||
"removeItemsFromLib": ""
|
"removeItemsFromLib": "موارد انتخاب شده از موارد پسندیده حذف شوند"
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند.",
|
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند.",
|
||||||
"link": ""
|
"link": "پست وبلاگ در مورد رمزگذاری سرتاسر در Excalidraw"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"angle": "زاویه",
|
"angle": "زاویه",
|
||||||
@@ -374,60 +380,60 @@
|
|||||||
"width": "عرض"
|
"width": "عرض"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"addedToLibrary": "",
|
"addedToLibrary": "به مجموعه اضافه شد",
|
||||||
"copyStyles": "کپی سبک.",
|
"copyStyles": "کپی سبک.",
|
||||||
"copyToClipboard": "در کلیپبورد کپی شد.",
|
"copyToClipboard": "در کلیپبورد کپی شد.",
|
||||||
"copyToClipboardAsPng": "",
|
"copyToClipboardAsPng": "کپی {{exportSelection}} در کلیپبورد به عنوان PNG\n({{exportColorScheme}})",
|
||||||
"fileSaved": "فایل ذخیره شد.",
|
"fileSaved": "فایل ذخیره شد.",
|
||||||
"fileSavedToFilename": "ذخیره در {filename}",
|
"fileSavedToFilename": "ذخیره در {filename}",
|
||||||
"canvas": "بوم",
|
"canvas": "بوم",
|
||||||
"selection": "انتخاب"
|
"selection": "انتخاب"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"ffffff": "",
|
"ffffff": "سفید",
|
||||||
"f8f9fa": "",
|
"f8f9fa": "خاکستری 0",
|
||||||
"f1f3f5": "",
|
"f1f3f5": "خاکستری 1",
|
||||||
"fff5f5": "",
|
"fff5f5": "قرمز 0",
|
||||||
"fff0f6": "",
|
"fff0f6": "صورتی 0",
|
||||||
"f8f0fc": "",
|
"f8f0fc": "انگوری 0",
|
||||||
"f3f0ff": "",
|
"f3f0ff": "بنفش 0",
|
||||||
"edf2ff": "",
|
"edf2ff": "نیلی 0",
|
||||||
"e7f5ff": "",
|
"e7f5ff": "آبی 0",
|
||||||
"e3fafc": "",
|
"e3fafc": "آبی نفتی 0",
|
||||||
"e6fcf5": "",
|
"e6fcf5": "آبی فیروزه ای 0",
|
||||||
"ebfbee": "",
|
"ebfbee": "سبز 0",
|
||||||
"f4fce3": "",
|
"f4fce3": "لیمویی 0",
|
||||||
"fff9db": "",
|
"fff9db": "زرد 0",
|
||||||
"fff4e6": "",
|
"fff4e6": "نارنجی 0",
|
||||||
"transparent": "",
|
"transparent": "شفاف",
|
||||||
"ced4da": "",
|
"ced4da": "خاکستری 4",
|
||||||
"868e96": "",
|
"868e96": "خاکستری 6",
|
||||||
"fa5252": "",
|
"fa5252": "قرمز 6",
|
||||||
"e64980": "",
|
"e64980": "صورتی 6",
|
||||||
"be4bdb": "",
|
"be4bdb": "انگوری 6",
|
||||||
"7950f2": "",
|
"7950f2": "بنفش 6",
|
||||||
"4c6ef5": "",
|
"4c6ef5": "نیلی 6",
|
||||||
"228be6": "",
|
"228be6": "آبی 6",
|
||||||
"15aabf": "",
|
"15aabf": "آبی نفتی 6",
|
||||||
"12b886": "",
|
"12b886": "آبی فیروزه ای 6",
|
||||||
"40c057": "",
|
"40c057": "سبز 6",
|
||||||
"82c91e": "",
|
"82c91e": "لیمویی 6",
|
||||||
"fab005": "",
|
"fab005": "زرد 6",
|
||||||
"fd7e14": "",
|
"fd7e14": "نارنجی 6",
|
||||||
"000000": "",
|
"000000": "سیاه",
|
||||||
"343a40": "",
|
"343a40": "خاکستری 8",
|
||||||
"495057": "",
|
"495057": "خاکستری 7",
|
||||||
"c92a2a": "",
|
"c92a2a": "قرمز 9",
|
||||||
"a61e4d": "",
|
"a61e4d": "صورتی 9",
|
||||||
"862e9c": "",
|
"862e9c": "انگوری 9",
|
||||||
"5f3dc4": "",
|
"5f3dc4": "بنفش 9",
|
||||||
"364fc7": "",
|
"364fc7": "نیلی 9",
|
||||||
"1864ab": "",
|
"1864ab": "آبی 9",
|
||||||
"0b7285": "",
|
"0b7285": "آبی نفتی 9",
|
||||||
"087f5b": "",
|
"087f5b": "آبی فیروزه ای 9",
|
||||||
"2b8a3e": "",
|
"2b8a3e": "سبز 9",
|
||||||
"5c940d": "",
|
"5c940d": "لیمویی 9",
|
||||||
"e67700": "",
|
"e67700": "زرد 9",
|
||||||
"d9480f": ""
|
"d9480f": "نارنجی 9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -120,7 +120,13 @@
|
|||||||
"lockAll": "",
|
"lockAll": "",
|
||||||
"unlockAll": ""
|
"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": {
|
"buttons": {
|
||||||
"clearReset": "Tyhjennä piirtoalue",
|
"clearReset": "Tyhjennä piirtoalue",
|
||||||
|
@@ -10,29 +10,29 @@
|
|||||||
"copyAsPng": "Copier dans le presse-papier en PNG",
|
"copyAsPng": "Copier dans le presse-papier en PNG",
|
||||||
"copyAsSvg": "Copier dans le presse-papier en SVG",
|
"copyAsSvg": "Copier dans le presse-papier en SVG",
|
||||||
"copyText": "Copier dans le presse-papier en tant que texte",
|
"copyText": "Copier dans le presse-papier en tant que texte",
|
||||||
"bringForward": "Envoyer vers l'avant",
|
"bringForward": "Avancer d'un plan",
|
||||||
"sendToBack": "Mettre en arrière-plan",
|
"sendToBack": "Déplacer à l'arrière-plan",
|
||||||
"bringToFront": "Mettre au premier plan",
|
"bringToFront": "Placer au premier plan",
|
||||||
"sendBackward": "Envoyer vers l'arrière",
|
"sendBackward": "Reculer d'un plan",
|
||||||
"delete": "Supprimer",
|
"delete": "Supprimer",
|
||||||
"copyStyles": "Copier les styles",
|
"copyStyles": "Copier les styles",
|
||||||
"pasteStyles": "Coller les styles",
|
"pasteStyles": "Coller les styles",
|
||||||
"stroke": "Trait",
|
"stroke": "Trait",
|
||||||
"background": "Arrière-plan",
|
"background": "Fond",
|
||||||
"fill": "Remplissage",
|
"fill": "Motif du fond",
|
||||||
"strokeWidth": "Largeur du trait",
|
"strokeWidth": "Épaisseur du trait",
|
||||||
"strokeStyle": "Style du trait",
|
"strokeStyle": "Style du trait",
|
||||||
"strokeStyle_solid": "Plein",
|
"strokeStyle_solid": "Continu",
|
||||||
"strokeStyle_dashed": "Tirets",
|
"strokeStyle_dashed": "Tirets",
|
||||||
"strokeStyle_dotted": "Pointillé",
|
"strokeStyle_dotted": "Pointillés",
|
||||||
"sloppiness": "Style de tracé",
|
"sloppiness": "Style de tracé",
|
||||||
"opacity": "Opacité",
|
"opacity": "Opacité",
|
||||||
"textAlign": "Alignement du texte",
|
"textAlign": "Alignement du texte",
|
||||||
"edges": "Angles",
|
"edges": "Angles",
|
||||||
"sharp": "Pointus",
|
"sharp": "Pointus",
|
||||||
"round": "Arrondis",
|
"round": "Arrondis",
|
||||||
"arrowheads": "Extrémités de flèche",
|
"arrowheads": "Extrémités",
|
||||||
"arrowhead_none": "Aucune",
|
"arrowhead_none": "Sans",
|
||||||
"arrowhead_arrow": "Flèche",
|
"arrowhead_arrow": "Flèche",
|
||||||
"arrowhead_bar": "Barre",
|
"arrowhead_bar": "Barre",
|
||||||
"arrowhead_dot": "Point",
|
"arrowhead_dot": "Point",
|
||||||
@@ -43,23 +43,23 @@
|
|||||||
"withBackground": "Arrière-plan",
|
"withBackground": "Arrière-plan",
|
||||||
"exportEmbedScene": "Intégrer la scène",
|
"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é.",
|
"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",
|
"handDrawn": "À la main",
|
||||||
"normal": "Normale",
|
"normal": "Normale",
|
||||||
"code": "Code",
|
"code": "Code",
|
||||||
"small": "Petit",
|
"small": "Petite",
|
||||||
"medium": "Moyen",
|
"medium": "Moyenne",
|
||||||
"large": "Grand",
|
"large": "Grande",
|
||||||
"veryLarge": "Très grand",
|
"veryLarge": "Très grande",
|
||||||
"solid": "Solide",
|
"solid": "Solide",
|
||||||
"hachure": "Hachure",
|
"hachure": "Hachures",
|
||||||
"crossHatch": "Hachure croisée",
|
"crossHatch": "Hachures croisées",
|
||||||
"thin": "Fin",
|
"thin": "Fine",
|
||||||
"bold": "Épais",
|
"bold": "Épaisse",
|
||||||
"left": "Gauche",
|
"left": "À gauche",
|
||||||
"center": "Centre",
|
"center": "Au centre",
|
||||||
"right": "Droite",
|
"right": "À droite",
|
||||||
"extraBold": "Très épais",
|
"extraBold": "Très épaisse",
|
||||||
"architect": "Architecte",
|
"architect": "Architecte",
|
||||||
"artist": "Artiste",
|
"artist": "Artiste",
|
||||||
"cartoonist": "Caricaturiste",
|
"cartoonist": "Caricaturiste",
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
"canvasColors": "Utilisé sur la zone de dessin",
|
"canvasColors": "Utilisé sur la zone de dessin",
|
||||||
"canvasBackground": "Arrière-plan du canevas",
|
"canvasBackground": "Arrière-plan du canevas",
|
||||||
"drawingCanvas": "Zone de dessin",
|
"drawingCanvas": "Zone de dessin",
|
||||||
"layers": "Calques",
|
"layers": "Disposition",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
"liveCollaboration": "Collaboration en direct",
|
"liveCollaboration": "Collaboration en direct",
|
||||||
@@ -86,41 +86,47 @@
|
|||||||
"libraryLoadingMessage": "Chargement de la bibliothèque…",
|
"libraryLoadingMessage": "Chargement de la bibliothèque…",
|
||||||
"libraries": "Parcourir les bibliothèques",
|
"libraries": "Parcourir les bibliothèques",
|
||||||
"loadingScene": "Chargement de la scène…",
|
"loadingScene": "Chargement de la scène…",
|
||||||
"align": "Aligner",
|
"align": "Alignement",
|
||||||
"alignTop": "Aligner en haut",
|
"alignTop": "Aligner en haut",
|
||||||
"alignBottom": "Aligner en bas",
|
"alignBottom": "Aligner en bas",
|
||||||
"alignLeft": "Aligner à gauche",
|
"alignLeft": "Aligner à gauche",
|
||||||
"alignRight": "Aligner à droite",
|
"alignRight": "Aligner à droite",
|
||||||
"centerVertically": "Centrer verticalement",
|
"centerVertically": "Centrer verticalement",
|
||||||
"centerHorizontally": "Centrer horizontalement",
|
"centerHorizontally": "Centrer horizontalement",
|
||||||
"distributeHorizontally": "Distribuer horizontalement",
|
"distributeHorizontally": "Répartir horizontalement",
|
||||||
"distributeVertically": "Distribuer verticalement",
|
"distributeVertically": "Répartir verticalement",
|
||||||
"flipHorizontal": "Retourner horizontalement",
|
"flipHorizontal": "Retourner horizontalement",
|
||||||
"flipVertical": "Retourner verticalement",
|
"flipVertical": "Retourner verticalement",
|
||||||
"viewMode": "Mode présentation",
|
"viewMode": "Mode présentation",
|
||||||
"toggleExportColorScheme": "Activer/Désactiver l'export du thème de couleur",
|
"toggleExportColorScheme": "Activer/Désactiver l'export du thème de couleur",
|
||||||
"share": "Partager",
|
"share": "Partager",
|
||||||
"showStroke": "Afficher le sélecteur de couleur de trait",
|
"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",
|
"toggleTheme": "Changer le thème",
|
||||||
"personalLib": "Bibliothèque personnelle",
|
"personalLib": "Bibliothèque personnelle",
|
||||||
"excalidrawLib": "Bibliothèque Excalidraw",
|
"excalidrawLib": "Bibliothèque Excalidraw",
|
||||||
"decreaseFontSize": "Réduire la taille de police",
|
"decreaseFontSize": "Diminuer la taille de police",
|
||||||
"increaseFontSize": "Augmenter la taille de police",
|
"increaseFontSize": "Augmenter la taille de police",
|
||||||
"unbindText": "Délier le texte",
|
"unbindText": "Dissocier le texte",
|
||||||
"bindText": "Lier le texte au conteneur",
|
"bindText": "Associer le texte au conteneur",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "Modifier le lien",
|
"edit": "Modifier le lien",
|
||||||
"create": "Créer un lien",
|
"create": "Ajouter un lien",
|
||||||
"label": "Lien"
|
"label": "Lien"
|
||||||
},
|
},
|
||||||
"elementLock": {
|
"elementLock": {
|
||||||
"lock": "Verrouiller",
|
"lock": "Verrouiller",
|
||||||
"unlock": "Déverrouiller",
|
"unlock": "Déverrouiller",
|
||||||
"lockAll": "Tout 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": {
|
"buttons": {
|
||||||
"clearReset": "Réinitialiser le canevas",
|
"clearReset": "Réinitialiser le canevas",
|
||||||
@@ -164,9 +170,9 @@
|
|||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "L'intégralité du canevas va être effacée. Êtes-vous sûr ?",
|
"clearReset": "L'intégralité du canevas va être effacée. Êtes-vous sûr ?",
|
||||||
"couldNotCreateShareableLink": "Impossible de créer un lien de partage.",
|
"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",
|
"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.",
|
"cannotExportEmptyCanvas": "Impossible d'exporter un canevas vide.",
|
||||||
"couldNotCopyToClipboard": "Impossible de copier dans le presse-papiers.",
|
"couldNotCopyToClipboard": "Impossible de copier dans le presse-papiers.",
|
||||||
"decryptFailed": "Les données n'ont pas pu être déchiffrées.",
|
"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",
|
"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",
|
"linearElementMulti": "Cliquez sur le dernier point ou appuyez sur Échap ou Entrée pour terminer",
|
||||||
"lockAngle": "Vous pouvez restreindre l'angle en maintenant MAJ",
|
"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",
|
"resizeImage": "Vous pouvez redimensionner librement en maintenant SHIFT,\nmaintenez ALT pour redimensionner depuis le centre",
|
||||||
"rotate": "Vous pouvez restreindre les angles en maintenant MAJ pendant la rotation",
|
"rotate": "Vous pouvez restreindre les angles en maintenant MAJ pendant la rotation",
|
||||||
"lineEditor_info": "Double-cliquez ou appuyez sur Entrée pour éditer les points",
|
"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",
|
"placeImage": "Cliquez pour placer l'image, ou cliquez et faites glisser pour définir sa taille manuellement",
|
||||||
"publishLibrary": "Publier votre propre bibliothèque",
|
"publishLibrary": "Publier votre propre bibliothèque",
|
||||||
"bindTextToElement": "Appuyer sur Entrée pour ajouter du texte",
|
"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"
|
"eraserRevert": "Maintenez Alt enfoncé pour annuler les éléments marqués pour suppression"
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"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 :",
|
"noteItems": "Chaque élément de la bibliothèque doit avoir son propre nom afin qu'il soit filtrable. Les éléments de bibliothèque suivants seront inclus :",
|
||||||
"atleastOneLibItem": "Veuillez sélectionner au moins un élément de bibliothèque pour commencer",
|
"atleastOneLibItem": "Veuillez sélectionner au moins un élément de bibliothèque pour commencer",
|
||||||
"republishWarning": ""
|
"republishWarning": "Remarque : certains des éléments sélectionnés sont marqués comme étant déjà publiés/soumis. Vous devez uniquement resoumettre des éléments lors de la mise à jour d'une bibliothèque ou d'une soumission existante."
|
||||||
},
|
},
|
||||||
"publishSuccessDialog": {
|
"publishSuccessDialog": {
|
||||||
"title": "Bibliothèque soumise",
|
"title": "Bibliothèque soumise",
|
||||||
|
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": "לנעול הכל",
|
"lockAll": "לנעול הכל",
|
||||||
"unlockAll": "שחרור הכול"
|
"unlockAll": "שחרור הכול"
|
||||||
},
|
},
|
||||||
"statusPublished": ""
|
"statusPublished": "",
|
||||||
|
"sidebarLock": ""
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"noItems": "",
|
||||||
|
"hint_emptyLibrary": "",
|
||||||
|
"hint_emptyPrivateLibrary": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "אפס את הלוח",
|
"clearReset": "אפס את הלוח",
|
||||||
|
@@ -120,7 +120,13 @@
|
|||||||
"lockAll": "सब ताले के अंदर रखे",
|
"lockAll": "सब ताले के अंदर रखे",
|
||||||
"unlockAll": "सब ताले के बाहर निकाले"
|
"unlockAll": "सब ताले के बाहर निकाले"
|
||||||
},
|
},
|
||||||
"statusPublished": "प्रकाशित"
|
"statusPublished": "प्रकाशित",
|
||||||
|
"sidebarLock": "साइडबार खुला रखे."
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"noItems": "अभी तक कोई आइटम जोडा नहीं गया.",
|
||||||
|
"hint_emptyLibrary": "यहाँ जोड़ने के लिए पटल से एक वस्तु चुने, अथवा जन कोष से एक संग्रह नीचे स्थापित करें.",
|
||||||
|
"hint_emptyPrivateLibrary": "यहाँ जोड़ने के लिए पटल से एक वस्तु चुने."
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "कैनवास रीसेट करें",
|
"clearReset": "कैनवास रीसेट करें",
|
||||||
|
@@ -120,7 +120,13 @@
|
|||||||
"lockAll": "",
|
"lockAll": "",
|
||||||
"unlockAll": ""
|
"unlockAll": ""
|
||||||
},
|
},
|
||||||
"statusPublished": ""
|
"statusPublished": "",
|
||||||
|
"sidebarLock": ""
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"noItems": "",
|
||||||
|
"hint_emptyLibrary": "",
|
||||||
|
"hint_emptyPrivateLibrary": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Vászon törlése",
|
"clearReset": "Vászon törlése",
|
||||||
|
@@ -120,7 +120,13 @@
|
|||||||
"lockAll": "Kunci semua",
|
"lockAll": "Kunci semua",
|
||||||
"unlockAll": "Lepas 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": {
|
"buttons": {
|
||||||
"clearReset": "Setel Ulang Kanvas",
|
"clearReset": "Setel Ulang Kanvas",
|
||||||
@@ -190,7 +196,7 @@
|
|||||||
"svgImageInsertError": "Tidak dapat menyisipkan gambar SVG. Markup SVG sepertinya tidak valid.",
|
"svgImageInsertError": "Tidak dapat menyisipkan gambar SVG. Markup SVG sepertinya tidak valid.",
|
||||||
"invalidSVGString": "SVG tidak valid.",
|
"invalidSVGString": "SVG tidak valid.",
|
||||||
"cannotResolveCollabServer": "Tidak dapat terhubung ke server kolab. Muat ulang laman dan coba lagi.",
|
"cannotResolveCollabServer": "Tidak dapat terhubung ke server kolab. Muat ulang laman dan coba lagi.",
|
||||||
"importLibraryError": ""
|
"importLibraryError": "Tidak dapat memuat pustaka"
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Pilihan",
|
"selection": "Pilihan",
|
||||||
@@ -343,7 +349,7 @@
|
|||||||
},
|
},
|
||||||
"noteItems": "Setiap item pustaka harus memiliki nama, sehingga bisa disortir. Item pustaka di bawah ini akan dimasukan:",
|
"noteItems": "Setiap item pustaka harus memiliki nama, sehingga bisa disortir. Item pustaka di bawah ini akan dimasukan:",
|
||||||
"atleastOneLibItem": "Pilih setidaknya satu item pustaka untuk mulai",
|
"atleastOneLibItem": "Pilih setidaknya satu item pustaka untuk mulai",
|
||||||
"republishWarning": ""
|
"republishWarning": "Catatan: beberapa item yang dipilih telah ditandai sebagai sudah dipublikasikan/diserahkan. Anda hanya dapat menyerahkan kembali item-item ketika memperbarui pustaka atau pengumpulan."
|
||||||
},
|
},
|
||||||
"publishSuccessDialog": {
|
"publishSuccessDialog": {
|
||||||
"title": "Pustaka telah dikirm",
|
"title": "Pustaka telah dikirm",
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user