From f9b7cfd8aa5235aa08a19c15d1c9d0d954a5d94d Mon Sep 17 00:00:00 2001 From: David Luzar Date: Sat, 27 Aug 2022 23:02:17 +0200 Subject: [PATCH 001/276] fix: reintroduce help dialog button (#5631) --- src/components/Footer.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 297a2124d..420aeb6a8 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -88,6 +88,13 @@ const Footer = ({ > {renderCustomFooter?.(false, appState)} +
+ {actionManager.renderAction("toggleShortcuts")} +
Date: Mon, 29 Aug 2022 15:44:10 +0530 Subject: [PATCH 002/276] fix: don't render library menu twice for mobile (#5636) --- src/components/LayerUI.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 2db1786bd..ba08bf7c0 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -478,11 +478,11 @@ const LayerUI = ({ {t("buttons.scrollBackToContent")} )} + {appState.isLibraryOpen && ( +
{libraryMenu}
+ )} )} - {appState.isLibraryOpen && ( -
{libraryMenu}
- )} ); }; From 59a1d192d268f403c3d946c52f28960a0045c357 Mon Sep 17 00:00:00 2001 From: Ives van Hoorne Date: Mon, 29 Aug 2022 15:22:04 +0200 Subject: [PATCH 003/276] chore: update CodeSandbox links and add a config (#5624) * chore: update CodeSandbox links and add a config * Update tasks.json --- .codesandbox/tasks.json | 43 +++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 .codesandbox/tasks.json diff --git a/.codesandbox/tasks.json b/.codesandbox/tasks.json new file mode 100644 index 000000000..360636c4c --- /dev/null +++ b/.codesandbox/tasks.json @@ -0,0 +1,43 @@ +{ + // These tasks will run in order when initializing your CodeSandbox project. + "setupTasks": [ + { + "name": "Install Dependencies", + "command": "yarn install" + } + ], + + // These tasks can be run from CodeSandbox. Running one will open a log in the app. + "tasks": { + "build": { + "name": "Build", + "command": "yarn build", + "runAtStart": false + }, + "fix": { + "name": "Fix", + "command": "yarn fix", + "runAtStart": false + }, + "prettier": { + "name": "Prettify", + "command": "yarn prettier", + "runAtStart": false + }, + "start": { + "name": "Start Excalidraw", + "command": "yarn start", + "runAtStart": true + }, + "test": { + "name": "Run Tests", + "command": "yarn test", + "runAtStart": false + }, + "install-deps": { + "name": "Install Dependencies", + "command": "yarn install", + "restartOn": { "files": ["yarn.lock"] } + } + } +} diff --git a/README.md b/README.md index 51f706382..45fec41ba 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Try out [`@excalidraw/excalidraw`](https://www.npmjs.com/package/@excalidraw/exc ### Code Sandbox -- Go to https://codesandbox.io/s/github/excalidraw/excalidraw +- Go to https://codesandbox.io/p/github/excalidraw/excalidraw - You may need to sign in with GitHub and reload the page - You can start coding instantly, and even send PRs from there! From 553b493f37c198a5556d3f7f00914c21eabec8bb Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Mon, 29 Aug 2022 19:26:03 +0530 Subject: [PATCH 004/276] fix: library actions inside the sidebar (#5638) --- src/components/LayerUI.tsx | 92 +++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index ba08bf7c0..d52c68d44 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -432,56 +432,58 @@ const LayerUI = ({ )} {!device.isMobile && ( -
- {renderFixedSideContainer()} -
- {appState.showStats && ( - +
+ {renderFixedSideContainer()} +
{ - actionManager.executeAction(actionToggleStats); - }} - renderCustomStats={renderCustomStats} + actionManager={actionManager} + renderCustomFooter={renderCustomFooter} + showExitZenModeBtn={showExitZenModeBtn} /> - )} - {appState.scrolledOutside && ( - - )} + {appState.showStats && ( + { + actionManager.executeAction(actionToggleStats); + }} + renderCustomStats={renderCustomStats} + /> + )} + {appState.scrolledOutside && ( + + )} +
{appState.isLibraryOpen && (
{libraryMenu}
)} -
+ )} ); From da4fa91ffc2bf9b1b74c9fec8164348273a9cad2 Mon Sep 17 00:00:00 2001 From: DanielJGeiger <1852529+DanielJGeiger@users.noreply.github.com> Date: Tue, 30 Aug 2022 02:07:18 -0500 Subject: [PATCH 005/276] chore: Dedupe webpack configs. (#5449) * chore: Dedupe package dependencies and webpack configs. * Fully dedupe `src/packages` via symlinks * Merge https://github.com/excalidraw/excalidraw into dedupe-package-deps-configs * fix: Link `tsc` so `build:example` works in @excalidraw/excalidraw * @excalidraw/plugins: Revert the `yarn.lock` deduping. * Drop yarn commands from the root `package.json`. * Remove more unneeded `package.json` additions. * One more change to drop in `package.json` files. * Deduping: Move even more into common webpack configs. * renaming Co-authored-by: Aakansha Doshi --- src/packages/common.webpack.dev.config.js | 88 +++++++++++++ src/packages/common.webpack.prod.config.js | 119 ++++++++++++++++++ src/packages/excalidraw/webpack.dev.config.js | 89 +------------ .../excalidraw/webpack.prod.config.js | 112 +---------------- src/packages/utils/webpack.prod.config.js | 49 +------- 5 files changed, 223 insertions(+), 234 deletions(-) create mode 100644 src/packages/common.webpack.dev.config.js create mode 100644 src/packages/common.webpack.prod.config.js diff --git a/src/packages/common.webpack.dev.config.js b/src/packages/common.webpack.dev.config.js new file mode 100644 index 000000000..0ee82688c --- /dev/null +++ b/src/packages/common.webpack.dev.config.js @@ -0,0 +1,88 @@ +const path = require("path"); +const autoprefixer = require("autoprefixer"); +const webpack = require("webpack"); +const { parseEnvVariables } = require(path.resolve(global.__childdir, "./env")); + +module.exports = { + mode: "development", + devtool: false, + output: { + libraryTarget: "umd", + filename: "[name].js", + publicPath: "", + }, + resolve: { + extensions: [".js", ".ts", ".tsx", ".css", ".scss"], + }, + module: { + rules: [ + { + test: /\.(sa|sc|c)ss$/, + exclude: /node_modules/, + use: [ + "style-loader", + { loader: "css-loader" }, + { + loader: "postcss-loader", + options: { + postcssOptions: { + plugins: [autoprefixer()], + }, + }, + }, + "sass-loader", + ], + }, + { + test: /\.(ts|tsx|js|jsx|mjs)$/, + exclude: /node_modules\/(?!browser-fs-access)/, + use: [ + { + loader: "ts-loader", + options: { + transpileOnly: true, + configFile: path.resolve(__dirname, "./tsconfig.dev.json"), + }, + }, + ], + }, + { + test: /\.(woff|woff2|eot|ttf|otf)$/, + type: "asset/resource", + }, + ], + }, + optimization: { + splitChunks: { + chunks: "async", + cacheGroups: { + vendors: { + test: /[\\/]node_modules[\\/]/, + name: "vendor", + }, + }, + }, + }, + plugins: [ + new webpack.EvalSourceMapDevToolPlugin({ exclude: /vendor/ }), + new webpack.DefinePlugin({ + "process.env": parseEnvVariables( + path.resolve(__dirname, "../../.env.development"), + ), + }), + ], + externals: { + react: { + root: "React", + commonjs2: "react", + commonjs: "react", + amd: "react", + }, + "react-dom": { + root: "ReactDOM", + commonjs2: "react-dom", + commonjs: "react-dom", + amd: "react-dom", + }, + }, +}; diff --git a/src/packages/common.webpack.prod.config.js b/src/packages/common.webpack.prod.config.js new file mode 100644 index 000000000..f0cca725d --- /dev/null +++ b/src/packages/common.webpack.prod.config.js @@ -0,0 +1,119 @@ +const path = require("path"); +const autoprefixer = require("autoprefixer"); +const webpack = require("webpack"); +const BundleAnalyzerPlugin = require(path.resolve( + path.join(global.__childdir, "node_modules"), + "webpack-bundle-analyzer", +)).BundleAnalyzerPlugin; +const TerserPlugin = require("terser-webpack-plugin"); +const { parseEnvVariables } = + "__noenv" in global ? {} : require(path.resolve(global.__childdir, "./env")); + +module.exports = { + mode: "production", + output: { + libraryTarget: "umd", + filename: "[name].js", + publicPath: "", + }, + resolve: { + extensions: [".js", ".ts", ".tsx", ".css", ".scss"], + }, + module: { + rules: [ + { + test: /\.(sa|sc|c)ss$/, + exclude: /node_modules/, + use: [ + "style-loader", + { + loader: "css-loader", + }, + { + loader: "postcss-loader", + options: { + postcssOptions: { + plugins: [autoprefixer()], + }, + }, + }, + "sass-loader", + ], + }, + { + test: /\.(ts|tsx|js|jsx|mjs)$/, + exclude: /node_modules\/(?!browser-fs-access)/, + use: [ + { + loader: "ts-loader", + options: { + transpileOnly: true, + configFile: path.resolve(__dirname, "./tsconfig.prod.json"), + }, + }, + { + loader: "babel-loader", + options: { + presets: [ + "@babel/preset-env", + ["@babel/preset-react", { runtime: "automatic" }], + "@babel/preset-typescript", + ], + plugins: [ + "transform-class-properties", + "@babel/plugin-transform-runtime", + ], + }, + }, + ], + }, + { + test: /\.(woff|woff2|eot|ttf|otf)$/, + type: "asset/resource", + }, + ], + }, + optimization: { + minimize: true, + minimizer: [ + new TerserPlugin({ + test: /\.js($|\?)/i, + }), + ], + splitChunks: { + chunks: "async", + cacheGroups: { + vendors: { + test: /[\\/]node_modules[\\/]/, + name: "vendor", + }, + }, + }, + }, + plugins: [ + ...(process.env.ANALYZER === "true" ? [new BundleAnalyzerPlugin()] : []), + ...("__noenv" in global + ? [] + : [ + new webpack.DefinePlugin({ + "process.env": parseEnvVariables( + path.resolve(__dirname, "../../.env.production"), + ), + }), + ]), + ], + externals: { + react: { + root: "React", + commonjs2: "react", + commonjs: "react", + amd: "react", + }, + "react-dom": { + root: "ReactDOM", + commonjs2: "react-dom", + commonjs: "react-dom", + amd: "react-dom", + }, + }, +}; diff --git a/src/packages/excalidraw/webpack.dev.config.js b/src/packages/excalidraw/webpack.dev.config.js index 9e8180f5f..5b535186f 100644 --- a/src/packages/excalidraw/webpack.dev.config.js +++ b/src/packages/excalidraw/webpack.dev.config.js @@ -1,97 +1,18 @@ +global.__childdir = __dirname; const path = require("path"); -const webpack = require("webpack"); -const autoprefixer = require("autoprefixer"); -const { parseEnvVariables } = require("./env"); +const { merge } = require("webpack-merge"); +const commonConfig = require("../common.webpack.dev.config"); const outputDir = process.env.EXAMPLE === "true" ? "example/public" : "dist"; -module.exports = { - mode: "development", - devtool: false, +const config = { entry: { "excalidraw.development": "./entry.js", }, output: { path: path.resolve(__dirname, outputDir), library: "ExcalidrawLib", - libraryTarget: "umd", - filename: "[name].js", chunkFilename: "excalidraw-assets-dev/[name]-[contenthash].js", assetModuleFilename: "excalidraw-assets-dev/[name][ext]", - - publicPath: "", - }, - resolve: { - extensions: [".js", ".ts", ".tsx", ".css", ".scss"], - }, - module: { - rules: [ - { - test: /\.(sa|sc|c)ss$/, - exclude: /node_modules/, - use: [ - "style-loader", - { loader: "css-loader" }, - { - loader: "postcss-loader", - options: { - postcssOptions: { - plugins: [autoprefixer()], - }, - }, - }, - "sass-loader", - ], - }, - { - test: /\.(ts|tsx|js|jsx|mjs)$/, - exclude: /node_modules\/(?!browser-fs-access)/, - use: [ - { - loader: "ts-loader", - options: { - transpileOnly: true, - configFile: path.resolve(__dirname, "../tsconfig.dev.json"), - }, - }, - ], - }, - { - test: /\.(woff|woff2|eot|ttf|otf)$/, - type: "asset/resource", - }, - ], - }, - optimization: { - splitChunks: { - chunks: "async", - cacheGroups: { - vendors: { - test: /[\\/]node_modules[\\/]/, - name: "vendor", - }, - }, - }, - }, - plugins: [ - new webpack.EvalSourceMapDevToolPlugin({ exclude: /vendor/ }), - new webpack.DefinePlugin({ - "process.env": parseEnvVariables( - path.resolve(__dirname, "../../../.env.development"), - ), - }), - ], - externals: { - react: { - root: "React", - commonjs2: "react", - commonjs: "react", - amd: "react", - }, - "react-dom": { - root: "ReactDOM", - commonjs2: "react-dom", - commonjs: "react-dom", - amd: "react-dom", - }, }, }; +module.exports = merge(commonConfig, config); diff --git a/src/packages/excalidraw/webpack.prod.config.js b/src/packages/excalidraw/webpack.prod.config.js index 0450d36fa..593287076 100644 --- a/src/packages/excalidraw/webpack.prod.config.js +++ b/src/packages/excalidraw/webpack.prod.config.js @@ -1,119 +1,17 @@ +global.__childdir = __dirname; const path = require("path"); -const TerserPlugin = require("terser-webpack-plugin"); -const BundleAnalyzerPlugin = - require("webpack-bundle-analyzer").BundleAnalyzerPlugin; -const autoprefixer = require("autoprefixer"); -const webpack = require("webpack"); -const { parseEnvVariables } = require("./env"); +const { merge } = require("webpack-merge"); +const commonConfig = require("../common.webpack.prod.config"); -module.exports = { - mode: "production", +const config = { entry: { "excalidraw.production.min": "./entry.js", }, output: { path: path.resolve(__dirname, "dist"), library: "ExcalidrawLib", - libraryTarget: "umd", - filename: "[name].js", chunkFilename: "excalidraw-assets/[name]-[contenthash].js", assetModuleFilename: "excalidraw-assets/[name][ext]", - publicPath: "", - }, - resolve: { - extensions: [".js", ".ts", ".tsx", ".css", ".scss"], - }, - module: { - rules: [ - { - test: /\.(sa|sc|c)ss$/, - exclude: /node_modules/, - use: [ - "style-loader", - { - loader: "css-loader", - }, - { - loader: "postcss-loader", - options: { - postcssOptions: { - plugins: [autoprefixer()], - }, - }, - }, - "sass-loader", - ], - }, - { - test: /\.(ts|tsx|js|jsx|mjs)$/, - exclude: /node_modules\/(?!browser-fs-access)/, - use: [ - { - loader: "ts-loader", - options: { - transpileOnly: true, - configFile: path.resolve(__dirname, "../tsconfig.prod.json"), - }, - }, - { - loader: "babel-loader", - options: { - presets: [ - "@babel/preset-env", - ["@babel/preset-react", { runtime: "automatic" }], - "@babel/preset-typescript", - ], - plugins: [ - "transform-class-properties", - "@babel/plugin-transform-runtime", - ], - }, - }, - ], - }, - { - test: /\.(woff|woff2|eot|ttf|otf)$/, - type: "asset/resource", - }, - ], - }, - optimization: { - minimize: true, - minimizer: [ - new TerserPlugin({ - test: /\.js($|\?)/i, - }), - ], - splitChunks: { - chunks: "async", - cacheGroups: { - vendors: { - test: /[\\/]node_modules[\\/]/, - name: "vendor", - }, - }, - }, - }, - plugins: [ - ...(process.env.ANALYZER === "true" ? [new BundleAnalyzerPlugin()] : []), - new webpack.DefinePlugin({ - "process.env": parseEnvVariables( - path.resolve(__dirname, "../../../.env.production"), - ), - }), - ], - externals: { - react: { - root: "React", - commonjs2: "react", - commonjs: "react", - amd: "react", - }, - "react-dom": { - root: "ReactDOM", - commonjs2: "react-dom", - commonjs: "react-dom", - amd: "react-dom", - }, }, }; +module.exports = merge(commonConfig, config); diff --git a/src/packages/utils/webpack.prod.config.js b/src/packages/utils/webpack.prod.config.js index 7238ad029..58a6bbf08 100644 --- a/src/packages/utils/webpack.prod.config.js +++ b/src/packages/utils/webpack.prod.config.js @@ -1,60 +1,23 @@ +global.__childdir = __dirname; +global.__noenv = true; const webpack = require("webpack"); const path = require("path"); -const BundleAnalyzerPlugin = - require("webpack-bundle-analyzer").BundleAnalyzerPlugin; +const { merge } = require("webpack-merge"); +const commonConfig = require("../common.webpack.prod.config"); -module.exports = { - mode: "production", +const config = { entry: { "excalidraw-utils.min": "./index.js" }, output: { path: path.resolve(__dirname, "dist"), - filename: "[name].js", library: "ExcalidrawUtils", - libraryTarget: "umd", - }, - resolve: { - extensions: [".tsx", ".ts", ".js", ".css", ".scss"], }, optimization: { runtimeChunk: false, }, - module: { - rules: [ - { - test: /\.(sa|sc|c)ss$/, - exclude: /node_modules/, - use: ["style-loader", { loader: "css-loader" }, "sass-loader"], - }, - { - test: /\.(ts|tsx|js)$/, - use: [ - { - loader: "ts-loader", - options: { - transpileOnly: true, - configFile: path.resolve(__dirname, "../tsconfig.prod.json"), - }, - }, - { - loader: "babel-loader", - - options: { - presets: [ - "@babel/preset-env", - ["@babel/preset-react", { runtime: "automatic" }], - "@babel/preset-typescript", - ], - plugins: [["@babel/plugin-transform-runtime"]], - }, - }, - ], - }, - ], - }, plugins: [ new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1, }), - ...(process.env.ANALYZER === "true" ? [new BundleAnalyzerPlugin()] : []), ], }; +module.exports = merge(commonConfig, config); From 836120c14b648567cad2bbc6fccc5c0775a639c7 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Tue, 30 Aug 2022 09:18:24 +0200 Subject: [PATCH 006/276] feat: added exportPadding to PNG (blob) export in @excalidraw/utils (#5626) * added exportPadding * Update README.md * Update CHANGELOG.md --- src/packages/excalidraw/CHANGELOG.md | 1 + src/packages/excalidraw/README.md | 8 ++++++-- src/packages/utils.ts | 8 ++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 9bd7d6d5f..4fab8e8d4 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -18,6 +18,7 @@ Please add the latest change on the top under the correct section. #### Features - Added support for storing [`customData`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#storing-custom-data-to-excalidraw-elements) on Excalidraw elements [#5592]. +- Added `exportPadding?: number;` to [exportToCanvas](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exporttocanvas) and [exportToBlob](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exporttoblob). The default value of the padding is 10. #### Breaking Changes diff --git a/src/packages/excalidraw/README.md b/src/packages/excalidraw/README.md index 5c9a73595..1d09fb5b0 100644 --- a/src/packages/excalidraw/README.md +++ b/src/packages/excalidraw/README.md @@ -929,7 +929,8 @@ This function normalizes library items elements, adding missing values when need elements, appState getDimensions, - files + files, + exportPadding?: number; }: ExportOpts @@ -940,6 +941,7 @@ This function normalizes library items elements, adding missing values when need | getDimensions | `(width: number, height: number) => { width: number, height: number, scale?: number }` | undefined | A function which returns the `width`, `height`, and optionally `scale` (defaults `1`), with which canvas is to be exported. | | maxWidthOrHeight | `number` | undefined | The maximum width or height of the exported image. If provided, `getDimensions` is ignored. | | files | [BinaryFiles](The [`BinaryFiles`](<[BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64)>) | undefined | The files added to the scene. | +| exportPadding | number | 10 | The padding to be added on canvas | **How to use** @@ -957,7 +959,8 @@ This function returns the canvas with the exported elements, appState and dimens exportToBlob( opts: ExportOpts & { mimeType?: string, - quality?: number; + quality?: number, + exportPadding?: number; }) @@ -966,6 +969,7 @@ exportToBlob( | opts | | | This param is passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exportToCanvas) | | mimeType | string | "image/png" | Indicates the image format | | quality | number | 0.92 | A value between 0 and 1 indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg`/`image/webp` MIME types. | +| exportPadding | number | 10 | The padding to be added on canvas | **How to use** diff --git a/src/packages/utils.ts b/src/packages/utils.ts index 4d9a8af17..d81995080 100644 --- a/src/packages/utils.ts +++ b/src/packages/utils.ts @@ -35,7 +35,10 @@ export const exportToCanvas = ({ files, maxWidthOrHeight, getDimensions, -}: ExportOpts) => { + exportPadding, +}: ExportOpts & { + exportPadding?: number; +}) => { const { elements: restoredElements, appState: restoredAppState } = restore( { elements, appState }, null, @@ -46,7 +49,7 @@ export const exportToCanvas = ({ getNonDeletedElements(restoredElements), { ...restoredAppState, offsetTop: 0, offsetLeft: 0, width: 0, height: 0 }, files || {}, - { exportBackground, viewBackgroundColor }, + { exportBackground, exportPadding, viewBackgroundColor }, (width: number, height: number) => { const canvas = document.createElement("canvas"); @@ -87,6 +90,7 @@ export const exportToBlob = async ( opts: ExportOpts & { mimeType?: string; quality?: number; + exportPadding?: number; }, ): Promise => { let { mimeType = MIME_TYPES.png, quality } = opts; From a271e42af1931ad0be26d2c9ff8b322562fb7a4b Mon Sep 17 00:00:00 2001 From: Excalidraw Bot <77840495+excalibot@users.noreply.github.com> Date: Thu, 1 Sep 2022 10:06:54 +0200 Subject: [PATCH 007/276] chore: Update translations from Crowdin (#5596) * New translations en.json (Vietnamese) * Auto commit: Calculate translation coverage * New translations en.json (Lithuanian) * Auto commit: Calculate translation coverage * New translations en.json (Lithuanian) * Auto commit: Calculate translation coverage * New translations en.json (Bengali) * Auto commit: Calculate translation coverage * New translations en.json (Bengali) * Auto commit: Calculate translation coverage --- src/locales/bn-BD.json | 100 +++++++++++++++++------------------ src/locales/lt-LT.json | 10 ++-- src/locales/percentages.json | 6 +-- src/locales/vi-VN.json | 22 ++++---- 4 files changed, 69 insertions(+), 69 deletions(-) diff --git a/src/locales/bn-BD.json b/src/locales/bn-BD.json index 11993ef9b..c34b2d357 100644 --- a/src/locales/bn-BD.json +++ b/src/locales/bn-BD.json @@ -1,55 +1,55 @@ { "labels": { - "paste": "", - "pasteCharts": "", - "selectAll": "", - "multiSelect": "", - "moveCanvas": "", - "cut": "", - "copy": "", - "copyAsPng": "", - "copyAsSvg": "", - "copyText": "", - "bringForward": "", - "sendToBack": "", - "bringToFront": "", - "sendBackward": "", - "delete": "", - "copyStyles": "", - "pasteStyles": "", - "stroke": "", - "background": "", - "fill": "", - "strokeWidth": "", - "strokeStyle": "", - "strokeStyle_solid": "", - "strokeStyle_dashed": "", - "strokeStyle_dotted": "", - "sloppiness": "", - "opacity": "", - "textAlign": "", - "edges": "", - "sharp": "", - "round": "", - "arrowheads": "", - "arrowhead_none": "", - "arrowhead_arrow": "", - "arrowhead_bar": "", - "arrowhead_dot": "", - "arrowhead_triangle": "", - "fontSize": "", - "fontFamily": "", - "onlySelected": "", - "withBackground": "", - "exportEmbedScene": "", - "exportEmbedScene_details": "", + "paste": "পেস্ট করুন", + "pasteCharts": "চার্টগুলো পেস্ট করুন", + "selectAll": "সব সিলেক্ট করুন", + "multiSelect": "সিলেকশনে এলিমেন্ট এ্যাড করুন", + "moveCanvas": "ক্যানভাস মুভ করুন", + "cut": "কাট করুন", + "copy": "কপি করুন", + "copyAsPng": "PNG হিসেবে ক্লিপবোর্ডে কপি করুন", + "copyAsSvg": "SVG হিসেবে ক্লিপবোর্ডে কপি করুন", + "copyText": "টেক্সট হিসেবে ক্লিপবোর্ডে কপি করুন", + "bringForward": "সামনে আনুন", + "sendToBack": "একদম পেছনে পাঠান", + "bringToFront": "একদম সামনে আনুন", + "sendBackward": "পেছনে পাঠান", + "delete": "ডিলিট করুন", + "copyStyles": "স্টাইলগুলো কপি করুন", + "pasteStyles": "স্টাইলগুলো পেস্ট করুন", + "stroke": "স্ট্রোক", + "background": "ব্যাকগ্রাউন্ড", + "fill": "ফিল", + "strokeWidth": "স্ট্রোকের পুরুত্ব", + "strokeStyle": "স্ট্রোকের স্টাইল", + "strokeStyle_solid": "সলিড", + "strokeStyle_dashed": "কাটা-কাটা", + "strokeStyle_dotted": "ফোটা-ফোটা", + "sloppiness": "স্ট্রোকের ধরণ", + "opacity": "অস্বচ্ছতা", + "textAlign": "লেখার দিক", + "edges": "কোণা", + "sharp": "তীক্ষ্ণ", + "round": "গোলাকার", + "arrowheads": "তীরের মাথা", + "arrowhead_none": "কিছু না", + "arrowhead_arrow": "তীর", + "arrowhead_bar": "বার", + "arrowhead_dot": "ডট", + "arrowhead_triangle": "ত্রিভুজ", + "fontSize": "ফন্ট সাইজ", + "fontFamily": "ফন্ট ফ্যামিলি", + "onlySelected": "শুধুমাত্র সিলেক্টেডগুলো", + "withBackground": "ব্যাকগ্রাউন্ড", + "exportEmbedScene": "সিন এম্বেড করুন", + "exportEmbedScene_details": "সিনের ডেটা এক্সপোর্টকৃত PNG/SVG ফাইলের মধ্যে সেভ করা হবে যাতে করে পরবর্তী সময়ে আপনি এডিট করতে পারেন । তবে এতে ফাইলের সাইজ বাড়বে ।.", "addWatermark": "", - "handDrawn": "", - "normal": "", - "code": "", - "small": "", - "medium": "", - "large": "", + "handDrawn": "হাতে আঁকা", + "normal": "স্বাভাবিক", + "code": "কোড", + "small": "ছোট", + "medium": "মধ্যবর্তী", + "large": "বড়", "veryLarge": "", "solid": "", "hachure": "", @@ -99,7 +99,7 @@ "flipVertical": "", "viewMode": "", "toggleExportColorScheme": "", - "share": "", + "share": "শেয়ার করুন", "showStroke": "", "showBackground": "", "toggleTheme": "", diff --git a/src/locales/lt-LT.json b/src/locales/lt-LT.json index cc75b9747..c7afc65eb 100644 --- a/src/locales/lt-LT.json +++ b/src/locales/lt-LT.json @@ -110,13 +110,13 @@ "unbindText": "", "bindText": "", "link": { - "edit": "", - "create": "", - "label": "" + "edit": "Redeguoti nuorodą", + "create": "Sukurti nuorodą", + "label": "Nuoroda" }, "elementLock": { - "lock": "", - "unlock": "", + "lock": "Užrakinti", + "unlock": "Atrakinti", "lockAll": "", "unlockAll": "" }, diff --git a/src/locales/percentages.json b/src/locales/percentages.json index 5f078434f..bdb04fcac 100644 --- a/src/locales/percentages.json +++ b/src/locales/percentages.json @@ -1,7 +1,7 @@ { "ar-SA": 91, "bg-BG": 58, - "bn-BD": 0, + "bn-BD": 13, "ca-ES": 99, "cs-CZ": 27, "da-DK": 34, @@ -23,7 +23,7 @@ "kab-KAB": 95, "kk-KZ": 22, "ko-KR": 99, - "lt-LT": 22, + "lt-LT": 24, "lv-LV": 100, "mr-IN": 100, "my-MM": 44, @@ -44,7 +44,7 @@ "ta-IN": 98, "tr-TR": 99, "uk-UA": 100, - "vi-VN": 13, + "vi-VN": 16, "zh-CN": 100, "zh-HK": 27, "zh-TW": 100 diff --git a/src/locales/vi-VN.json b/src/locales/vi-VN.json index ba3b2ecff..0aa55fa3a 100644 --- a/src/locales/vi-VN.json +++ b/src/locales/vi-VN.json @@ -1,10 +1,10 @@ { "labels": { "paste": "Dán", - "pasteCharts": "", + "pasteCharts": "Dán biểu đồ", "selectAll": "Chọn tất cả", - "multiSelect": "", - "moveCanvas": "", + "multiSelect": "Thêm mới vào Select", + "moveCanvas": "Di chuyển Canvas", "cut": "Cắt", "copy": "Sao chép", "copyAsPng": "Sao chép vào bộ nhớ tạm dưới dạng PNG", @@ -43,22 +43,22 @@ "withBackground": "Nền", "exportEmbedScene": "", "exportEmbedScene_details": "", - "addWatermark": "", + "addWatermark": "Làm với Excalidraw\"", "handDrawn": "", "normal": "Bình thường", "code": "Mã", "small": "Nhỏ", "medium": "Vừa", "large": "Lớn", - "veryLarge": "", - "solid": "", + "veryLarge": "Rất lớn", + "solid": "Đặc", "hachure": "", "crossHatch": "", - "thin": "", - "bold": "", - "left": "", - "center": "", - "right": "", + "thin": "Mỏng", + "bold": "In đậm", + "left": "Trái", + "center": "Giữa", + "right": "Phải", "extraBold": "", "architect": "", "artist": "", From b3052f017889e146869c0a48eda377795692737b Mon Sep 17 00:00:00 2001 From: Abdullah Adeel <64294045+mabdullahadeel@users.noreply.github.com> Date: Thu, 1 Sep 2022 15:11:44 +0500 Subject: [PATCH 008/276] fix: #5622 - prevent session theme reset during collaboration (#5640) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✅ fixed #5622 - prevent session theme reset - ❌ prevent newly initialized state to override theme preferences. - 🔧 fix for #5622 * refactor Co-authored-by: Aakansha Doshi --- src/excalidraw-app/index.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index 896c96d98..ebe6ed8eb 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -194,7 +194,13 @@ const initializeScene = async (opts: { scene: { ...scene, appState: { - ...restoreAppState(scene?.appState, excalidrawAPI.getAppState()), + ...restoreAppState( + { + ...scene?.appState, + theme: localDataState?.appState?.theme || scene?.appState?.theme, + }, + excalidrawAPI.getAppState(), + ), // necessary if we're invoking from a hashchange handler which doesn't // go through App.initializeScene() that resets this flag isLoading: false, From cd61f3111684244133ae02d42109e8f29eaf6cc9 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Mon, 5 Sep 2022 12:30:47 +0200 Subject: [PATCH 009/276] fix: buttons jump around on the mobile menu (#5658) Update MobileMenu.tsx --- src/components/MobileMenu.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/MobileMenu.tsx b/src/components/MobileMenu.tsx index f6e28f441..a20547546 100644 --- a/src/components/MobileMenu.tsx +++ b/src/components/MobileMenu.tsx @@ -121,7 +121,6 @@ export const MobileMenu = ({ const renderAppToolbar = () => { // Render eraser conditionally in mobile const showEraser = - !appState.viewModeEnabled && !appState.editingElement && getSelectedElements(elements, appState).length === 0; @@ -140,11 +139,11 @@ export const MobileMenu = ({ {actionManager.renderAction("undo")} {actionManager.renderAction("redo")} - {showEraser && actionManager.renderAction("eraser")} - - {actionManager.renderAction( - appState.multiElement ? "finalize" : "duplicateSelection", - )} + {showEraser + ? actionManager.renderAction("eraser") + : actionManager.renderAction( + appState.multiElement ? "finalize" : "duplicateSelection", + )} {actionManager.renderAction("deleteSelectedElements")} ); From 933c6a2237163ebf438bb1d9b387449d8e7e6216 Mon Sep 17 00:00:00 2001 From: Igor Berlenko Date: Wed, 7 Sep 2022 18:38:04 +0800 Subject: [PATCH 010/276] build: add missing dependencies: pica, lodash (#5656) * add missing dependencies: pica, lodash * remove lodash & fix yarn.lock * first * second Co-authored-by: dwelle --- package.json | 1 + src/components/LibraryMenuItems.tsx | 3 +-- src/excalidraw-app/collab/Portal.tsx | 2 +- yarn.lock | 11 +++++++++++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 436e0de23..f5189072a 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "open-color": "1.9.1", "pako": "1.0.11", "perfect-freehand": "1.0.16", + "pica": "7.1.1", "png-chunk-text": "1.0.0", "png-chunks-encode": "1.0.0", "png-chunks-extract": "1.0.0", diff --git a/src/components/LibraryMenuItems.tsx b/src/components/LibraryMenuItems.tsx index 01223baed..b39003cda 100644 --- a/src/components/LibraryMenuItems.tsx +++ b/src/components/LibraryMenuItems.tsx @@ -1,4 +1,3 @@ -import { chunk } from "lodash"; import React, { useCallback, useState } from "react"; import { saveLibraryAsJSON, serializeLibraryAsJSON } from "../data/json"; import Library from "../data/library"; @@ -11,7 +10,7 @@ import { LibraryItem, LibraryItems, } from "../types"; -import { arrayToMap, muteFSAbortError } from "../utils"; +import { arrayToMap, chunk, muteFSAbortError } from "../utils"; import { useDevice } from "./App"; import ConfirmDialog from "./ConfirmDialog"; import { close, exportToFileIcon, load, publishIcon, trash } from "./icons"; diff --git a/src/excalidraw-app/collab/Portal.tsx b/src/excalidraw-app/collab/Portal.tsx index 95e0e7aa4..2d073c86a 100644 --- a/src/excalidraw-app/collab/Portal.tsx +++ b/src/excalidraw-app/collab/Portal.tsx @@ -14,7 +14,7 @@ import { } from "../app_constants"; import { UserIdleState } from "../../types"; import { trackEvent } from "../../analytics"; -import { throttle } from "lodash"; +import throttle from "lodash.throttle"; import { newElementWith } from "../../element/mutateElement"; import { BroadcastedExcalidrawElement } from "./reconciliation"; import { encryptData } from "../../data/encryption"; diff --git a/yarn.lock b/yarn.lock index 64e577c58..98fb485d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9032,6 +9032,17 @@ performance-now@^2.1.0: resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= +pica@7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/pica/-/pica-7.1.1.tgz#c68b42f5cfa6cc26eaec5cfa10cc0a5299ef3b7a" + integrity sha512-WY73tMvNzXWEld2LicT9Y260L43isrZ85tPuqRyvtkljSDLmnNFQmZICt4xUJMVulmcc6L9O7jbBrtx3DOz/YQ== + dependencies: + glur "^1.1.2" + inherits "^2.0.3" + multimath "^2.0.0" + object-assign "^4.1.1" + webworkify "^1.5.0" + pica@^7.1.0: version "7.1.0" resolved "https://registry.npmjs.org/pica/-/pica-7.1.0.tgz" From 59ec1c6cee2e8150f70fea13464af5715ee2b9b8 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Fri, 9 Sep 2022 13:53:38 +0200 Subject: [PATCH 011/276] fix: zen-mode exit button not working (#5682) --- src/components/Actions.tsx | 6 +++--- src/components/Footer.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Actions.tsx b/src/components/Actions.tsx index 8cd1d2abd..1ba40ba08 100644 --- a/src/components/Actions.tsx +++ b/src/components/Actions.tsx @@ -286,17 +286,17 @@ export const UndoRedoActions = ({ ); export const ExitZenModeAction = ({ - executeAction, + actionManager, showExitZenModeBtn, }: { - executeAction: ActionManager["executeAction"]; + actionManager: ActionManager; showExitZenModeBtn: boolean; }) => ( diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 420aeb6a8..0fbcd73d9 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -96,7 +96,7 @@ const Footer = ({ {actionManager.renderAction("toggleShortcuts")} From 7922ce129e569f0abed54158deba15506415af15 Mon Sep 17 00:00:00 2001 From: Ikko Ashimine Date: Mon, 12 Sep 2022 06:50:51 +0900 Subject: [PATCH 012/276] chore: fix typo in blob.ts (#5664) Co-authored-by: David Luzar --- src/data/blob.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/blob.ts b/src/data/blob.ts index ceecd9345..b5ca1dcad 100644 --- a/src/data/blob.ts +++ b/src/data/blob.ts @@ -356,7 +356,7 @@ export const getFileHandle = async ( }; /** - * attemps to detect if a buffer is a valid image by checking its leading bytes + * attempts to detect if a buffer is a valid image by checking its leading bytes */ const getActualMimeTypeFromImage = (buffer: ArrayBuffer) => { let mimeType: ValueOf> | null = @@ -396,7 +396,7 @@ export const createFile = ( }); }; -/** attemps to detect correct mimeType if none is set, or if an image +/** attempts to detect correct mimeType if none is set, or if an image * has an incorrect extension. * Note: doesn't handle missing .excalidraw/.excalidrawlib extension */ export const normalizeFile = async (file: File) => { From 898789b9792ea9bbd4dc26b7a62874b6052b0a58 Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Mon, 12 Sep 2022 13:49:22 +0800 Subject: [PATCH 013/276] chore: update lib menu click outside callback comment (#5687) --- src/components/LibraryMenu.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/LibraryMenu.tsx b/src/components/LibraryMenu.tsx index c9fc75703..ebd609763 100644 --- a/src/components/LibraryMenu.tsx +++ b/src/components/LibraryMenu.tsx @@ -107,7 +107,8 @@ export const LibraryMenu = ({ ref, useCallback( (event) => { - // If click on the library icon, do nothing. + // If click on the library icon, do nothing so that LibraryButton + // can toggle library menu if ((event.target as Element).closest(".ToolIcon__library")) { return; } From 5c17751662e67b8b078eee3ab522b1e3b83d73f9 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Tue, 13 Sep 2022 16:29:56 +0530 Subject: [PATCH 014/276] fix: Move to release notes for v0.9.0 and after (#5686) * fix: Move to release notes for v0.9.0 and after * fix --- src/packages/excalidraw/CHANGELOG.md | 856 +-------------------------- 1 file changed, 4 insertions(+), 852 deletions(-) diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 4fab8e8d4..d239d25fa 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -26,869 +26,21 @@ Please add the latest change on the top under the correct section. ## 0.12.0 (2022-07-07) -### Excalidraw API - -#### Features - -- [`loadLibraryFromBlob`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#loadLibraryFromBlob) now takes an additional parameter `defaultStatus` which sets the default status of library item if not present, defaults to `unpublished` [#5067](https://github.com/excalidraw/excalidraw/pull/5067). - -- Add [`UIOptions.dockedSidebarBreakpoint`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#dockedSidebarBreakpoint) to customize at which point to break from the docked sidebar [#5274](https://github.com/excalidraw/excalidraw/pull/5274). - -- Added support for supplying user `id` in the Collaborator object (see `collaborators` in [`updateScene()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateScene)), which will be used to deduplicate users when rendering collaborator avatar list. Cursors will still be rendered for every user. [#5309](https://github.com/excalidraw/excalidraw/pull/5309) - -- Export API to [set](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#setCursor) and [reset](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#resetCursor) mouse cursor on the canvas [#5215](https://github.com/excalidraw/excalidraw/pull/5215). - -- Export [`sceneCoordsToViewportCoords`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPointerDown) and [`viewportCoordsToSceneCoords`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPointerDown) utilities [#5187](https://github.com/excalidraw/excalidraw/pull/5187). - -- Added [`useHandleLibrary`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#useHandleLibrary) hook to automatically handle importing of libraries when `#addLibrary` URL hash key is present, and potentially for initializing library as well [#5115](https://github.com/excalidraw/excalidraw/pull/5115). - - Also added [`parseLibraryTokensFromUrl`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#parseLibraryTokensFromUrl) to help in manually importing library from URL if desired. - - ##### BREAKING CHANGE - - - Libraries are no longer automatically initialized from URL when `#addLibrary` hash key is present. Host apps now need to handle this themselves with the help of either of the above APIs (`useHandleLibrary` is recommended). - -- Added [`updateLibrary`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateLibrary) API to update (replace/merge) the library [#5115](https://github.com/excalidraw/excalidraw/pull/5115). - - ##### BREAKING CHANGE - - - `updateScene` API no longer supports passing `libraryItems`. Instead, use the `updateLibrary` API. - -- Add support for integrating custom elements [#5164](https://github.com/excalidraw/excalidraw/pull/5164). - - - Add [`onPointerDown`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPointerDown) callback which gets triggered on pointer down events. - - Add [`onScrollChange`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onScrollChange) callback which gets triggered when scrolling the canvas. - - Add API [`setActiveTool`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#setActiveTool) which host can call to set the active tool. - -- Exported [`loadSceneOrLibraryFromBlob`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#loadSceneOrLibraryFromBlob) function [#5057](https://github.com/excalidraw/excalidraw/pull/5057). -- Export [`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L92) supported by Excalidraw [#5135](https://github.com/excalidraw/excalidraw/pull/5135). -- Support [`avatarUrl`](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L50) for collaborators. Now onwards host can pass `avatarUrl` to render the customized avatar for collaborators [#5114](https://github.com/excalidraw/excalidraw/pull/5114), renamed in [#5177](https://github.com/excalidraw/excalidraw/pull/5177). -- Support `libraryItems` argument in `initialData.libraryItems` and `updateScene({ libraryItems })` to be a Promise resolving to `LibraryItems`, and support functional update of `libraryItems` in [`updateScene({ libraryItems })`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateScene). [#5101](https://github.com/excalidraw/excalidraw/pull/5101). -- Expose util [`mergeLibraryItems`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#mergeLibraryItems) [#5101](https://github.com/excalidraw/excalidraw/pull/5101). -- Expose util [`exportToClipboard`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportToClipboard) which allows to copy the scene contents to clipboard as `svg`, `png` or `json` [#5103](https://github.com/excalidraw/excalidraw/pull/5103). -- Expose `window.EXCALIDRAW_EXPORT_SOURCE` which you can use to overwrite the `source` field in exported data [#5095](https://github.com/excalidraw/excalidraw/pull/5095). -- The `exportToBlob` utility now supports the `exportEmbedScene` option when generating a png image [#5047](https://github.com/excalidraw/excalidraw/pull/5047). -- Exported [`restoreLibraryItems`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreLibraryItems) API [#4995](https://github.com/excalidraw/excalidraw/pull/4995). - -#### Fixes - -- Allow returning `null ` in [`renderFooter`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#renderFooter) prop [#5282](https://github.com/excalidraw/excalidraw/pull/5282). - -- Transpile `browser-fs-access` dependency so that its `for await` syntax doesn't force `es2018` requirement onto dependent projects [#5041](https://github.com/excalidraw/excalidraw/pull/5041). - -- Use `window.EXCALIDRAW_ASSET_PATH` for fonts when exporting to svg [#5065](https://github.com/excalidraw/excalidraw/pull/5065). -- Library menu now properly rerenders if open when library is updated using `updateScene({ libraryItems })` [#4995](https://github.com/excalidraw/excalidraw/pull/4995). - -#### Refactor - -- Rename `appState.elementLocked` to `appState.activeTool.locked` [#4983](https://github.com/excalidraw/excalidraw/pull/4983). -- Expose [`serializeLibraryAsJSON`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#serializeLibraryAsJSON) helper that we use when saving Excalidraw Library to a file. - -##### BREAKING CHANGE - -You will need to pass `activeTool.locked` instead of `elementType` from now onwards in `appState`. - -- Rename `appState.elementType` to [`appState.activeTool`](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L80) which is now an object [#4698](https://github.com/excalidraw/excalidraw/pull/4968). - -##### BREAKING CHANGE - -You will need to pass `activeTool` instead of `elementType` from now onwards in `appState` - -### Build - -- Use only named exports [#5045](https://github.com/excalidraw/excalidraw/pull/5045). - -#### BREAKING CHANGE - -You will need to import the named export from now onwards to use the component - -Using bundler :point_down: - -```js -import { Excalidraw } from "@excalidraw/excalidraw"; -``` - -In Browser :point_down: - -```js -React.createElement(ExcalidrawLib.Excalidraw, opts); -``` - -## Excalidraw Library +Check out the [release notes](https://github.com/excalidraw/excalidraw/releases/tag/v0.12.0) ) **_This section lists the updates made to the excalidraw library and will not affect the integration._** -### Features - -- Throttle scene rendering to animation framerate [#5422](https://github.com/excalidraw/excalidraw/pull/5422) - -- Make toast closable and allow custom duration [#5308](https://github.com/excalidraw/excalidraw/pull/5308) - -- Collab component state handling rewrite & fixes [#5046](https://github.com/excalidraw/excalidraw/pull/5046) - -- Support debugging PWA in dev [#4853](https://github.com/excalidraw/excalidraw/pull/4853) - -- Redirect vscode.excalidraw.com to vscode marketplace [#5285](https://github.com/excalidraw/excalidraw/pull/5285) - -- Go-to-excalidrawplus button [#5202](https://github.com/excalidraw/excalidraw/pull/5202) - -- Autoredirect to Excalidraw+ if special cookie is present [#5183](https://github.com/excalidraw/excalidraw/pull/5183) - -- Support resubmitting published library items [#5174](https://github.com/excalidraw/excalidraw/pull/5174) - -- Support adding multiple library items on canvas [#5116](https://github.com/excalidraw/excalidraw/pull/5116) - -- Support customType in activeTool [#5144](https://github.com/excalidraw/excalidraw/pull/5144) - -- Stop event propagation when key handled [#5091](https://github.com/excalidraw/excalidraw/pull/5091) - -- Rewrite library state management & related refactor [#5067](https://github.com/excalidraw/excalidraw/pull/5067) - -- Delay initial loading message & tweak design [#5049](https://github.com/excalidraw/excalidraw/pull/5049) - -- Reconcile when saving to firebase [#4991](https://github.com/excalidraw/excalidraw/pull/4991) - -- Hide trash button during collaboration [#5037](https://github.com/excalidraw/excalidraw/pull/5037) - -- Refactor local persistence & fix race condition on SW reload [#5032](https://github.com/excalidraw/excalidraw/pull/5032) - -- Element locking [#4964](https://github.com/excalidraw/excalidraw/pull/4964) - -- Copy to clipboard all text nodes as text [#5013](https://github.com/excalidraw/excalidraw/pull/5013) - -- Create and expose serializeLibraryAsJSON [#5009](https://github.com/excalidraw/excalidraw/pull/5009) - -- Hide penMode button on reload if not enabled [#4992](https://github.com/excalidraw/excalidraw/pull/4992) - -- Eraser toggle to switch back to the previous tool [#4981](https://github.com/excalidraw/excalidraw/pull/4981) - -- Save penDetected and penMode, and detect pen already on ToolButton click [#4955](https://github.com/excalidraw/excalidraw/pull/4955) - -- Support binding text to container via context menu [#4935](https://github.com/excalidraw/excalidraw/pull/4935) - -- Map shortcut O to ellipse and Add eraser shortcut E [#4930](https://github.com/excalidraw/excalidraw/pull/4930) - -- Update eraser cursor [#4922](https://github.com/excalidraw/excalidraw/pull/4922) - -- Add Eraser 🎉 [#4887](https://github.com/excalidraw/excalidraw/pull/4887) - -- Added optional REACT_APP_WS_SERVER_URL for forks usecases [#4889](https://github.com/excalidraw/excalidraw/pull/4889) - -- Rewrite collab server connecting [#4881](https://github.com/excalidraw/excalidraw/pull/4881) - -- Support vertical text align for bound containers [#4852](https://github.com/excalidraw/excalidraw/pull/4852) - -- Support custom colors 🎉 [#4843](https://github.com/excalidraw/excalidraw/pull/4843) - -- Support Links in Exported SVG [#4791](https://github.com/excalidraw/excalidraw/pull/4791) - -- Scale font size when bound text containers resized with shift pressed [#4828](https://github.com/excalidraw/excalidraw/pull/4828) - -### Fixes - -- Autorelease job name [#5412](https://github.com/excalidraw/excalidraw/pull/5412) - -- Action name for autorelease [#5411](https://github.com/excalidraw/excalidraw/pull/5411) - -- Typecast file to fix the build [#5410](https://github.com/excalidraw/excalidraw/pull/5410) - -- File handle not persisted when importing excalidraw files [#5372](https://github.com/excalidraw/excalidraw/pull/5372) - -- Library not scrollable when no published items installed [#5352](https://github.com/excalidraw/excalidraw/pull/5352) - -- Focus traps inside popovers [#5317](https://github.com/excalidraw/excalidraw/pull/5317) - -- Unable to use cmd/ctrl-delete/backspace in inputs [#5348](https://github.com/excalidraw/excalidraw/pull/5348) - -- Delay loading until language imported [#5344](https://github.com/excalidraw/excalidraw/pull/5344) - -- Command to trigger release [#5347](https://github.com/excalidraw/excalidraw/pull/5347) - -- Remove unnecessary options passed to language detector [#5336](https://github.com/excalidraw/excalidraw/pull/5336) - -- Stale `appState.pendingImageElement` [#5322](https://github.com/excalidraw/excalidraw/pull/5322) - -- Non-letter shortcuts being swallowed by color picker [#5316](https://github.com/excalidraw/excalidraw/pull/5316) - -- Bind text to correct container when nested [#5307](https://github.com/excalidraw/excalidraw/pull/5307) - -- Copy bound text style when copying element having bound text [#5305](https://github.com/excalidraw/excalidraw/pull/5305) - -- Copy arrow head when using copy styles [#5303](https://github.com/excalidraw/excalidraw/pull/5303) - -- Unsafely accessing draggingElement [#5216](https://github.com/excalidraw/excalidraw/pull/5216) - -- Library load button does not work [#5205](https://github.com/excalidraw/excalidraw/pull/5205) - -- Do not deselect when not zooming using touchscreen pinch [#5181](https://github.com/excalidraw/excalidraw/pull/5181) - -- Wheel zoom normalization [#5165](https://github.com/excalidraw/excalidraw/pull/5165) - -- Hide sidebar when `custom` tool active [#5179](https://github.com/excalidraw/excalidraw/pull/5179) - -- Don't save deleted ExcalidrawElements to Firebase [#5108](https://github.com/excalidraw/excalidraw/pull/5108) - -- Eraser removed deleted elements [#5155](https://github.com/excalidraw/excalidraw/pull/5155) - -- Handle `ColorPicker` parentSelector being undefined [#5152](https://github.com/excalidraw/excalidraw/pull/5152) - -- Library multiselect not accounting for published state [#5132](https://github.com/excalidraw/excalidraw/pull/5132) - -- Chart display fix [#5154](https://github.com/excalidraw/excalidraw/pull/5154) - -- Update opacity of bound text when opacity of container updated [#5142](https://github.com/excalidraw/excalidraw/pull/5142) - -- Jumping of text when typing single line in bound text [#5139](https://github.com/excalidraw/excalidraw/pull/5139) - -- Remove opacity scroll wheel interaction [#5111](https://github.com/excalidraw/excalidraw/pull/5111) - -- Propagate keydown events from excalidraw-wysiwyg inputs [#5099](https://github.com/excalidraw/excalidraw/pull/5099) - -- Don't bind text to container if double clicked else instead of center [#5105](https://github.com/excalidraw/excalidraw/pull/5105) - -- ToolIcon height not using rem [#5092](https://github.com/excalidraw/excalidraw/pull/5092) - -- Excalidraw named export type [#5078](https://github.com/excalidraw/excalidraw/pull/5078) - -- BoundElementIds when arrows bound to elements are deleted [#5077](https://github.com/excalidraw/excalidraw/pull/5077) - -- Don't merge libraryItems on updateScene [#5076](https://github.com/excalidraw/excalidraw/pull/5076) - -- SVG metadata extraction regex on multiline elements [#5074](https://github.com/excalidraw/excalidraw/pull/5074) - -- Eraser cursor showing on theme change when not using eraser [#4990](https://github.com/excalidraw/excalidraw/pull/4990) - -- Update `storage.rules` [#5020](https://github.com/excalidraw/excalidraw/pull/5020) - -- Add image button not working on iPad [#5038](https://github.com/excalidraw/excalidraw/pull/5038) - -- Ensure svg image dimensions are always set [#5044](https://github.com/excalidraw/excalidraw/pull/5044) - -- Pinch zoom in view mode [#5001](https://github.com/excalidraw/excalidraw/pull/5001) - -- Select whole group on righclick & few lock-related fixes [#5022](https://github.com/excalidraw/excalidraw/pull/5022) - -- Export serializeLibraryAsJSON from the package [#5017](https://github.com/excalidraw/excalidraw/pull/5017) - -- Support copying PNG to clipboard on Safari [#3746](https://github.com/excalidraw/excalidraw/pull/3746) - -- More copyText fixes [#5016](https://github.com/excalidraw/excalidraw/pull/5016) - -- Copy to clipboard all text nodes as text [#5014](https://github.com/excalidraw/excalidraw/pull/5014) - -- Update cursorButton once freedraw is released [#4996](https://github.com/excalidraw/excalidraw/pull/4996) - -- Decouple actionFinalize and actionErase [#4984](https://github.com/excalidraw/excalidraw/pull/4984) - -- Using stale state when switching tools [#4989](https://github.com/excalidraw/excalidraw/pull/4989) - -- UpdateWysiwygStyle updatedElement is undefined TypeError [#4980](https://github.com/excalidraw/excalidraw/pull/4980) - -- Adding check for link length to prevent early return [#4982](https://github.com/excalidraw/excalidraw/pull/4982) - -- Show link icon for bound text containers [#4960](https://github.com/excalidraw/excalidraw/pull/4960) - -- Cancel erase elements on pointer up if eraser is not active on pointer up [#4956](https://github.com/excalidraw/excalidraw/pull/4956) - -- Restore original opacities when alt pressed while erasing [#4954](https://github.com/excalidraw/excalidraw/pull/4954) - -- Don't bind text to container if already present [#4946](https://github.com/excalidraw/excalidraw/pull/4946) - -- Erase all elements which are hit with single point click [#4934](https://github.com/excalidraw/excalidraw/pull/4934) - -- Add multiElement-edit finalize action to Desktop (currently only visible in Mobile view) [#4764](https://github.com/excalidraw/excalidraw/pull/4764) - -- Hide eraser in view mode in desktop [#4929](https://github.com/excalidraw/excalidraw/pull/4929) - -- Undo when erasing elements by clicking [#4921](https://github.com/excalidraw/excalidraw/pull/4921) - -- Undo when erasing [#4900](https://github.com/excalidraw/excalidraw/pull/4900) - -- Incorrectly erasing on mobile [#4899](https://github.com/excalidraw/excalidraw/pull/4899) - -- Don't crash on drop highlighted text onto canvas [#4890](https://github.com/excalidraw/excalidraw/pull/4890) - -- Paste styles shortcut [#4886](https://github.com/excalidraw/excalidraw/pull/4886) - -- Freedraw element's background fill color missing from SVG when exporting with package API exportToSvg() [#4871](https://github.com/excalidraw/excalidraw/pull/4871) - -- Improve pointer syncing performance [#4883](https://github.com/excalidraw/excalidraw/pull/4883) - -- Collab room initialization [#4882](https://github.com/excalidraw/excalidraw/pull/4882) - -- Ensure verticalAlign properties not shown when no element selected [#4860](https://github.com/excalidraw/excalidraw/pull/4860) - -- Binding text to non-bindable containers and not always preferring selection [#4655](https://github.com/excalidraw/excalidraw/pull/4655) - -- Don't show align icons for single bound container element [#4846](https://github.com/excalidraw/excalidraw/pull/4846) - -- Redraw text bounding box when pasting styles [#4845](https://github.com/excalidraw/excalidraw/pull/4845) - -- Restore cursor position after bound text container value updated [#4836](https://github.com/excalidraw/excalidraw/pull/4836) - -- Support resizing multiple bound text containers [#4824](https://github.com/excalidraw/excalidraw/pull/4824) - -- Also check overflowY: overlay in detectScroll [#4806](https://github.com/excalidraw/excalidraw/pull/4806) - -- Stuck resizing when resizing bound text container very fast beyond threshold [#4804](https://github.com/excalidraw/excalidraw/pull/4804) - -### Refactor - -- Don't pass array to handleBindTextResize [#4826](https://github.com/excalidraw/excalidraw/pull/4826) - -### Build - -- Extract all i18n files into locales folder [#5419](https://github.com/excalidraw/excalidraw/pull/5419) - -- Automate release step fully [#5414](https://github.com/excalidraw/excalidraw/pull/5414) - -- Use next and preview tags instead of separate packages for next and preview release [#5346](https://github.com/excalidraw/excalidraw/pull/5346) - -- Support runtime React Jsx in @excalidraw/utils [#4866](https://github.com/excalidraw/excalidraw/pull/4866) - -- Release @excalidraw/utils 0.1.1 [#4862](https://github.com/excalidraw/excalidraw/pull/4862) - -- Remove build packages workflow [#4835](https://github.com/excalidraw/excalidraw/pull/4835) - ---- - ## 0.11.0 (2022-02-17) -## Excalidraw API - -### Features - -- Add `onLinkOpen` prop which will be triggered when clicked on element hyperlink if present [#4694](https://github.com/excalidraw/excalidraw/pull/4694). -- Support updating library using [`updateScene`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateScene) API [#4546](https://github.com/excalidraw/excalidraw/pull/4546). - -- Introduced primary colors to the app. The colors can be overridden. Check [readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#customizing-styles) on how to do so [#4387](https://github.com/excalidraw/excalidraw/pull/4387). - -- [`exportToBlob`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportToBlob) now automatically sets `appState.exportBackground` to `true` if exporting to `image/jpeg` MIME type (to ensure that alpha channel is not compressed to black color) [#4342](https://github.com/excalidraw/excalidraw/pull/4342). - - #### BREAKING CHANGE - - Remove `getElementMap` util [#4306](https://github.com/excalidraw/excalidraw/pull/4306). - -- Changes to [`exportToCanvas`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportToCanvas) util function [#4321](https://github.com/excalidraw/excalidraw/pull/4321): - - - Add `maxWidthOrHeight?: number` attribute. - - `scale` returned from `getDimensions()` is now optional (default to `1`). - -- Image support added for host [PR](https://github.com/excalidraw/excalidraw/pull/4011) - - General notes: - - - File data are encoded as DataURLs (base64) for portability reasons. - - [ExcalidrawAPI](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onLibraryChange): - - - added `getFiles()` to get current `BinaryFiles` (`Record`). It may contain files that aren't referenced by any element, so if you're persisting the files to a storage, you should compare them against stored elements. - - Excalidraw app props: - - - added `generateIdForFile(file: File)` optional prop so you can generate your own ids for added files. - - `onChange(elements, appState, files)` prop callback is now passed `BinaryFiles` as third argument. - - `onPaste(data, event)` data prop should contain `data.files` (`BinaryFiles`) if the elements pasted are referencing new files. - - `initialData` object now supports additional `files` (`BinaryFiles`) attribute. - - Other notes: - - - `.excalidraw` files may now contain top-level `files` key in format of `Record` when exporting any (image) elements. - - Changes were made to various export utilities exported from the package so that they take `files`, you can refer to the docs for the same. - -- Export [`isLinearElement`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#isLinearElement) and [`getNonDeletedElements`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#getNonDeletedElements) [#4072](https://github.com/excalidraw/excalidraw/pull/4072). - -- Support [`renderTopRightUI`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#renderTopRightUI) in mobile UI [#4065](https://github.com/excalidraw/excalidraw/pull/4065). - -- Export `THEME` constant from the package so host can use this when passing the theme [#4055](https://github.com/excalidraw/excalidraw/pull/4055). - - #### BREAKING CHANGE - - The `Appearance` type is now removed and renamed to `Theme` so `Theme` type needs to be used. - -### Fixes - -- Reset `unmounted` state on the component once component mounts to fix the mounting/unmounting repeatedly when used with `useEffect` [#4682](https://github.com/excalidraw/excalidraw/pull/4682). -- Panning the canvas using `mousewheel-drag` and `space-drag` now prevents the browser from scrolling the container/page [#4489](https://github.com/excalidraw/excalidraw/pull/4489). -- Scope drag and drop events to Excalidraw container to prevent overriding host application drag and drop events [#4445](https://github.com/excalidraw/excalidraw/pull/4445). - -### Build - -- Release preview package [@excalidraw/excalidraw-preview](https://www.npmjs.com/package/@excalidraw/excalidraw-preview) when triggered via comment - -``` - @excalibot trigger release -``` - -[#4750](https://github.com/excalidraw/excalidraw/pull/4750). - -- Added an example to test and develop the package [locally](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#Development) using `yarn start` [#4488](https://github.com/excalidraw/excalidraw/pull/4488) - -- Remove `file-loader` so font assets are not duplicated by webpack and use webpack asset modules for font generation [#4380](https://github.com/excalidraw/excalidraw/pull/4380). - -- We're now compiling to `es2017` target. Notably, `async/await` is not compiled down to generators. [#4341](https://github.com/excalidraw/excalidraw/pull/4341). - ---- - -## Excalidraw Library - -**_This section lists the updates made to the excalidraw library and will not affect the integration._** - -### Features - -- Show group/group and link action in mobile [#4795](https://github.com/excalidraw/excalidraw/pull/4795) - -- Support background fill for freedraw shapes [#4610](https://github.com/excalidraw/excalidraw/pull/4610) - -- Keep selected tool on canvas reset [#4728](https://github.com/excalidraw/excalidraw/pull/4728) - -- Make whole element clickable in view mode when it has hyperlink [#4735](https://github.com/excalidraw/excalidraw/pull/4735) - -- Allow any precision when zooming [#4730](https://github.com/excalidraw/excalidraw/pull/4730) - -- Throttle `pointermove` events per framerate [#4727](https://github.com/excalidraw/excalidraw/pull/4727) - -- Support hyperlinks 🔥 [#4620](https://github.com/excalidraw/excalidraw/pull/4620) - -- Added penMode for palm rejection [#4657](https://github.com/excalidraw/excalidraw/pull/4657) - -- Support unbinding bound text [#4686](https://github.com/excalidraw/excalidraw/pull/4686) - -- Sync local storage state across tabs when out of sync [#4545](https://github.com/excalidraw/excalidraw/pull/4545) - -- Support contextMenuLabel to be of function type to support dynamic labels [#4654](https://github.com/excalidraw/excalidraw/pull/4654) - -- Support decreasing/increasing `fontSize` via keyboard [#4553](https://github.com/excalidraw/excalidraw/pull/4553) - -- Link to new LP for excalidraw plus [#4549](https://github.com/excalidraw/excalidraw/pull/4549) - -- Update stroke color of bounded text along with container [#4541](https://github.com/excalidraw/excalidraw/pull/4541) - -- Hints and shortcuts help around deep selection [#4502](https://github.com/excalidraw/excalidraw/pull/4502) - -- Support updating text properties by clicking on container [#4499](https://github.com/excalidraw/excalidraw/pull/4499) - -- Bind text to shapes when pressing enter and support sticky notes 🎉 [#4343](https://github.com/excalidraw/excalidraw/pull/4343) - -- Change `boundElementIds` → `boundElements` [#4404](https://github.com/excalidraw/excalidraw/pull/4404) - -- Support selecting multiple points when editing line [#4373](https://github.com/excalidraw/excalidraw/pull/4373) - -- Horizontally center toolbar menu [commit link](https://github.com/excalidraw/excalidraw/commit/9b8ee3cacfec239617c357693cf2a3ca9972d2cb) - -- Add support for rounded corners in diamond [#4369](https://github.com/excalidraw/excalidraw/pull/4369) - -- Allow zooming up to 3000% [#4358](https://github.com/excalidraw/excalidraw/pull/4358) - -- Stop discarding precision when rendering [#4357](https://github.com/excalidraw/excalidraw/pull/4357) - -- Support Image binding [#4347](https://github.com/excalidraw/excalidraw/pull/4347) - -- Add `element.updated` [#4070](https://github.com/excalidraw/excalidraw/pull/4070) - -- Compress shareLink data when uploading to json server [#4225](https://github.com/excalidraw/excalidraw/pull/4225) - -- Supply `version` param when installing libraries [#4305](https://github.com/excalidraw/excalidraw/pull/4305) - -- Log FS abortError to console [#4279](https://github.com/excalidraw/excalidraw/pull/4279) - -- Add validation for website and remove validation for library item name [#4269](https://github.com/excalidraw/excalidraw/pull/4269) - -- Allow publishing libraries from UI [#4115](https://github.com/excalidraw/excalidraw/pull/4115) - -- Create confirm dialog to use instead of window.confirm [#4256](https://github.com/excalidraw/excalidraw/pull/4256) - -- Allow letters in IDs for storing files in backend [#4224](https://github.com/excalidraw/excalidraw/pull/4224) - -- Remove support for V1 unencrypted backend [#4189](https://github.com/excalidraw/excalidraw/pull/4189) - -- Use separate backend for local storage [#4187](https://github.com/excalidraw/excalidraw/pull/4187) - -- Add hint around canvas panning [#4159](https://github.com/excalidraw/excalidraw/pull/4159) - -- Stop using production services for development [#4113](https://github.com/excalidraw/excalidraw/pull/4113) - -- Add triangle arrowhead [#4024](https://github.com/excalidraw/excalidraw/pull/4024) - -- Add rewrite to webex landing page [#4102](https://github.com/excalidraw/excalidraw/pull/4102) - -- Switch collab server [#4092](https://github.com/excalidraw/excalidraw/pull/4092) - -- Use dialog component for clear canvas instead of window confirm [#4075](https://github.com/excalidraw/excalidraw/pull/4075) - -### Fixes - -- Rename --color-primary-chubb to --color-primary-contrast-offset and fallback to primary color if not present [#4803](https://github.com/excalidraw/excalidraw/pull/4803) - -- Add commits directly pushed to master in changelog [#4798](https://github.com/excalidraw/excalidraw/pull/4798) - -- Don't bump element version when adding files data [#4794](https://github.com/excalidraw/excalidraw/pull/4794) - -- Mobile link click [#4742](https://github.com/excalidraw/excalidraw/pull/4742) - -- ContextMenu timer & pointers not correctly reset on iOS [#4765](https://github.com/excalidraw/excalidraw/pull/4765) - -- Use absolute coords when rendering link popover [#4753](https://github.com/excalidraw/excalidraw/pull/4753) - -- Changing font size when text is not selected or edited [#4751](https://github.com/excalidraw/excalidraw/pull/4751) - -- Disable contextmenu on non-secondary `pen` events or `touch` [#4675](https://github.com/excalidraw/excalidraw/pull/4675) - -- Mobile context menu won't show on long press [#4741](https://github.com/excalidraw/excalidraw/pull/4741) - -- Do not open links twice [#4738](https://github.com/excalidraw/excalidraw/pull/4738) - -- Make link icon clickable in mobile [#4736](https://github.com/excalidraw/excalidraw/pull/4736) - -- Apple Pen missing strokes [#4705](https://github.com/excalidraw/excalidraw/pull/4705) - -- Freedraw slow movement jittery lines [#4726](https://github.com/excalidraw/excalidraw/pull/4726) - -- Disable three finger pinch zoom in penMode [#4725](https://github.com/excalidraw/excalidraw/pull/4725) - -- Remove click listener for opening popup [#4700](https://github.com/excalidraw/excalidraw/pull/4700) - -- Link popup position not accounting for offsets [#4695](https://github.com/excalidraw/excalidraw/pull/4695) - -- PenMode darkmode style [#4692](https://github.com/excalidraw/excalidraw/pull/4692) - -- Typing `_+` in wysiwyg not working [#4681](https://github.com/excalidraw/excalidraw/pull/4681) - -- Keyboard-zooming in wysiwyg should zoom canvas [#4676](https://github.com/excalidraw/excalidraw/pull/4676) - -- SceneCoordsToViewportCoords, jumping text when there is an offset [#4413](https://github.com/excalidraw/excalidraw/pull/4413) (#4630) - -- Right-click object menu displays partially off-screen [#4572](https://github.com/excalidraw/excalidraw/pull/4572) (#4631) - -- Support collaboration in bound text [#4573](https://github.com/excalidraw/excalidraw/pull/4573) - -- Cmd/ctrl native browser behavior blocked in inputs [#4589](https://github.com/excalidraw/excalidraw/pull/4589) - -- Use cached width when calculating min width during resize [#4585](https://github.com/excalidraw/excalidraw/pull/4585) - -- Support collaboration in bounded text [#4580](https://github.com/excalidraw/excalidraw/pull/4580) - -- Port for collab server and update docs [#4569](https://github.com/excalidraw/excalidraw/pull/4569) - -- Don't mutate the bounded text if not updated when submitted [#4543](https://github.com/excalidraw/excalidraw/pull/4543) - -- Prevent canvas drag while editing text [#4552](https://github.com/excalidraw/excalidraw/pull/4552) - -- Support shift+P for freedraw [#4550](https://github.com/excalidraw/excalidraw/pull/4550) - -- Prefer spreadsheet data over image [#4533](https://github.com/excalidraw/excalidraw/pull/4533) - -- Show text properties button states correctly for bounded text [#4542](https://github.com/excalidraw/excalidraw/pull/4542) - -- Rotate bounded text when container is rotated before typing [#4535](https://github.com/excalidraw/excalidraw/pull/4535) - -- Undo should work when selecting bounded textr [#4537](https://github.com/excalidraw/excalidraw/pull/4537) - -- Reduce padding to 5px for bounded text [#4530](https://github.com/excalidraw/excalidraw/pull/4530) - -- Bound text doesn't inherit container [#4521](https://github.com/excalidraw/excalidraw/pull/4521) - -- Text wrapping with grid [#4505](https://github.com/excalidraw/excalidraw/pull/4505) (#4506) - -- Check if process is defined before using so it works in browser [#4497](https://github.com/excalidraw/excalidraw/pull/4497) - -- Pending review fixes for sticky notes [#4493](https://github.com/excalidraw/excalidraw/pull/4493) - -- Pasted elements except binded text once paste action is complete [#4472](https://github.com/excalidraw/excalidraw/pull/4472) - -- Don't select binded text when ungrouping [#4470](https://github.com/excalidraw/excalidraw/pull/4470) - -- Set height correctly when text properties updated while editing in container until first submit [#4469](https://github.com/excalidraw/excalidraw/pull/4469) - -- Align and distribute binded text in container and cleanup [#4468](https://github.com/excalidraw/excalidraw/pull/4468) - -- Move binded text when moving container using keyboard [#4466](https://github.com/excalidraw/excalidraw/pull/4466) - -- Support dragging binded text in container selected in a group [#4462](https://github.com/excalidraw/excalidraw/pull/4462) - -- Vertically align single line when deleting text in bounded container [#4460](https://github.com/excalidraw/excalidraw/pull/4460) - -- Update height correctly when updating text properties in binded text [#4459](https://github.com/excalidraw/excalidraw/pull/4459) - -- Align library item previews to center [#4447](https://github.com/excalidraw/excalidraw/pull/4447) - -- Vertically center align text when text deleted [#4457](https://github.com/excalidraw/excalidraw/pull/4457) - -- Vertically center the first line as user starts typing in container [#4454](https://github.com/excalidraw/excalidraw/pull/4454) - -- Switch cursor to center of container when adding text when dimensions are too small [#4452](https://github.com/excalidraw/excalidraw/pull/4452) - -- Vertically center align the bounded text correctly when zoomed [#4444](https://github.com/excalidraw/excalidraw/pull/4444) - -- Support updating stroke color for text by typing in color picker input [#4415](https://github.com/excalidraw/excalidraw/pull/4415) - -- Bound text not atomic with container when changing z-index [#4414](https://github.com/excalidraw/excalidraw/pull/4414) - -- Update viewport coords correctly when editing text [#4416](https://github.com/excalidraw/excalidraw/pull/4416) - -- Use word-break break-word only and update text editor height only when binded to container [#4410](https://github.com/excalidraw/excalidraw/pull/4410) - -- Husky not able to execute pre-commit on windows [#4370](https://github.com/excalidraw/excalidraw/pull/4370) - -- Make firebase config parsing not fail on undefined env [#4381](https://github.com/excalidraw/excalidraw/pull/4381) - -- Adding to library via contextmenu when no image is selected [#4356](https://github.com/excalidraw/excalidraw/pull/4356) - -- Export scale quality regression [#4316](https://github.com/excalidraw/excalidraw/pull/4316) - -- Remove `100%` height from tooltip container to fix layout issues [#3980](https://github.com/excalidraw/excalidraw/pull/3980) - -- Inline ENV variables when building excalidraw package [#4311](https://github.com/excalidraw/excalidraw/pull/4311) - -- SVG export in dark mode with embedded bitmap image [#4285](https://github.com/excalidraw/excalidraw/pull/4285) - -- New FS API not working on Linux [#4280](https://github.com/excalidraw/excalidraw/pull/4280) - -- Url -> URL for consistency [#4277](https://github.com/excalidraw/excalidraw/pull/4277) - -- Prevent adding images to library via contextMenu [#4264](https://github.com/excalidraw/excalidraw/pull/4264) - -- Account for libraries v2 when prompting [#4263](https://github.com/excalidraw/excalidraw/pull/4263) - -- Skia rendering issues [#4200](https://github.com/excalidraw/excalidraw/pull/4200) - -- Ellipse roughness when `0` [#4194](https://github.com/excalidraw/excalidraw/pull/4194) - -- Proper string for invalid SVG [#4191](https://github.com/excalidraw/excalidraw/pull/4191) - -- Images not initialized correctly [#4157](https://github.com/excalidraw/excalidraw/pull/4157) - -- Image-related fixes [#4147](https://github.com/excalidraw/excalidraw/pull/4147) - -- Rewrite collab element reconciliation to fix z-index issues [#4076](https://github.com/excalidraw/excalidraw/pull/4076) - -- Redirect excalidraw.com/about to for-webex.excalidraw.com [#4104](https://github.com/excalidraw/excalidraw/pull/4104) - -- Redirect to webex LP instead of rewrite to fix SW [#4103](https://github.com/excalidraw/excalidraw/pull/4103) - -- Clear image/shape cache of affected elements when adding files [#4089](https://github.com/excalidraw/excalidraw/pull/4089) - -- Clear `LibraryUnit` DOM on unmount [#4084](https://github.com/excalidraw/excalidraw/pull/4084) - -- Pasting images on firefox [#4085](https://github.com/excalidraw/excalidraw/pull/4085) - -### Refactor - -- Simplify zoom by removing `zoom.translation` [#4477](https://github.com/excalidraw/excalidraw/pull/4477) - -- Deduplicate encryption helpers [#4146](https://github.com/excalidraw/excalidraw/pull/4146) - -### Performance - -- Cache approx line height in textwysiwg [#4651](https://github.com/excalidraw/excalidraw/pull/4651) - -### Build - -- Rename release command to 'release package' [#4783](https://github.com/excalidraw/excalidraw/pull/4783) - -- Deploy excalidraw package example [#4762](https://github.com/excalidraw/excalidraw/pull/4762) - -- Allow package.json changes when autoreleasing next [#4068](https://github.com/excalidraw/excalidraw/pull/4068) - ---- +Check out the [release notes](https://github.com/excalidraw/excalidraw/releases/tag/v0.11.0) ## 0.10.0 (2021-10-13) -## Excalidraw API - -### Fixes - -- Don't show save file to disk button in export dialog when `saveFileToDisk` passed as `false` in [`UIOptions.canvasActions.export`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportOpts) [#4073](https://github.com/excalidraw/excalidraw/pull/4073). - -- [`onPaste`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPaste) prop should return false to prevent the native excalidraw paste action [#3974](https://github.com/excalidraw/excalidraw/pull/3974). - - #### BREAKING CHANGE - - - Earlier the paste action was prevented when the prop [`onPaste`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPaste) returned true, but now it should return false to prevent the paste action. This was done to make it semantically more correct and intuitive. - -### Build - -- Enable jsx transform in webpack [#4049](https://github.com/excalidraw/excalidraw/pull/4049) - -### Docs - -- Correct exportToBackend in README to onExportToBackend [#3952](https://github.com/excalidraw/excalidraw/pull/3952) - -## Excalidraw Library - -**_This section lists the updates made to the excalidraw library and will not affect the integration._** - -### Features - -- Improve freedraw shape [#3984](https://github.com/excalidraw/excalidraw/pull/3984) - -- Make color ARIA labels better [#3871](https://github.com/excalidraw/excalidraw/pull/3871) - -- Add origin trial tokens [#3853](https://github.com/excalidraw/excalidraw/pull/3853) - -- Re-order zoom buttons [#3837](https://github.com/excalidraw/excalidraw/pull/3837) - -- Add undo/redo buttons & tweak footer [#3832](https://github.com/excalidraw/excalidraw/pull/3832) - -- Resave to png/svg with metadata if you loaded your scene from a png/svg file [#3645](https://github.com/excalidraw/excalidraw/pull/3645) - -### Fixes - -- Abstract and fix legacy fs [#4032](https://github.com/excalidraw/excalidraw/pull/4032) - -- Context menu positioning [#4025](https://github.com/excalidraw/excalidraw/pull/4025) - -- Added alert for bad encryption key [#3998](https://github.com/excalidraw/excalidraw/pull/3998) - -- OnPaste should return false to prevent paste action [#3974](https://github.com/excalidraw/excalidraw/pull/3974) - -- Help-icon now visible on Safari [#3939](https://github.com/excalidraw/excalidraw/pull/3939) - -- Permanent zoom mode [#3931](https://github.com/excalidraw/excalidraw/pull/3931) - -- Undo/redo buttons gap in Safari [#3836](https://github.com/excalidraw/excalidraw/pull/3836) - -- Prevent gradual canvas misalignment [#3833](https://github.com/excalidraw/excalidraw/pull/3833) - -- Color picker shortcuts not working when elements selected [#3817](https://github.com/excalidraw/excalidraw/pull/3817) - ---- +Check out the [release notes](https://github.com/excalidraw/excalidraw/releases/tag/v0.10.0) ## 0.9.0 (2021-07-10) -## Excalidraw API - -### Features - -- [`restore(data, localAppState, localElements)`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restore) and [`restoreElements(elements, localElements)`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreElements) now take `localElements` argument which will be used to ensure existing elements' versions are used and incremented. This fixes an issue where importing the same file would resolve to elements with older versions, potentially causing issues when reconciling [#3797](https://github.com/excalidraw/excalidraw/pull/3797). - - #### BREAKING CHANGE - - - `localElements` argument is mandatory (can be `null`/`undefined`) if using TypeScript. - -- Support `appState.exportEmbedScene` attribute in [`exportToSvg`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportToSvg) which allows to embed the scene data [#3777](https://github.com/excalidraw/excalidraw/pull/3777). - - #### BREAKING CHANGE - - - The attribute `metadata` is now removed as `metadata` was only used to embed scene data which is now supported with the `appState.exportEmbedScene` attribute. - - [`exportToSvg`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportToSvg) now resolves to a promise which resolves to `svg` of the exported drawing. - -- Expose [`loadLibraryFromBlob`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#loadLibraryFromBlobY), [`loadFromBlob`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#loadFromBlob), and [`getFreeDrawSvgPath`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#getFreeDrawSvgPath) [#3764](https://github.com/excalidraw/excalidraw/pull/3764). - -- Expose [`FONT_FAMILY`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#FONT_FAMILY) so that consumer can use when passing `initialData.appState.currentItemFontFamily` [#3710](https://github.com/excalidraw/excalidraw/pull/3710). - -- Added prop [`autoFocus`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#autoFocus) to focus the excalidraw component on page load when enabled, defaults to false [#3691](https://github.com/excalidraw/excalidraw/pull/3691). - - Note: Earlier Excalidraw component was focused by default on page load, you need to enable `autoFocus` prop to retain the same behaviour. - -- Added prop `UIOptions.canvasActions.export.renderCustomUI` to support Custom UI rendering inside export dialog [#3666](https://github.com/excalidraw/excalidraw/pull/3666). -- Added prop `UIOptions.canvasActions.saveAsImage` to show/hide the **Save as image** button in the canvas actions. Defaults to `true` hence the **Save as Image** button is rendered [#3662](https://github.com/excalidraw/excalidraw/pull/3662). - -- Export dialog can be customised with [`UiOptions.canvasActions.export`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportOpts) [#3658](https://github.com/excalidraw/excalidraw/pull/3658). - - Also, [`UIOptions`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#UIOptions) is now memoized to avoid unnecessary rerenders. - - #### BREAKING CHANGE - - - `UIOptions.canvasActions.saveAsScene` is now renamed to `UiOptions.canvasActions.export.saveFileToDisk`. Defaults to `true` hence the **save file to disk** button is rendered inside the export dialog. - - `exportToBackend` is now renamed to `UIOptions.canvasActions.export.exportToBackend`. If this prop is not passed, the **shareable-link** button will not be rendered, same as before. - -### Fixes - -- Use excalidraw Id in elements so every element has unique id [#3696](https://github.com/excalidraw/excalidraw/pull/3696). - -### Refactor - -- #### BREAKING CHANGE - - Rename `UIOptions.canvasActions.saveScene` to `UIOptions.canvasActions.saveToActiveFile`[#3657](https://github.com/excalidraw/excalidraw/pull/3657). - - Removed `shouldAddWatermark: boolean` attribute from options for [export](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#export-utilities) APIs [#3639](https://github.com/excalidraw/excalidraw/pull/3639). - - Removed `appState.shouldAddWatermark` so in case you were passing `shouldAddWatermark` in [initialData.AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) it will not work anymore. - -## Excalidraw Library - -**_This section lists the updates made to the excalidraw library and will not affect the integration._** - -### Features - -- Switch to selection tool on library item insert [#3773](https://github.com/excalidraw/excalidraw/pull/3773) - -- Show active file name when saving to current file [#3733](https://github.com/excalidraw/excalidraw/pull/3733) - -- Add hint around text editing [#3708](https://github.com/excalidraw/excalidraw/pull/3708) - -- Change library icon to be more clear [#3583](https://github.com/excalidraw/excalidraw/pull/3583) - -- Pass current `theme` when installing libraries [#3701](https://github.com/excalidraw/excalidraw/pull/3701) - -- Update virgil font [#3692](https://github.com/excalidraw/excalidraw/pull/3692) - -- Support exporting json to excalidraw plus [#3678](https://github.com/excalidraw/excalidraw/pull/3678) - -- Save exportScale in AppState [#3580](https://github.com/excalidraw/excalidraw/pull/3580) - -- Add shortcuts for stroke and background color picker [#3318](https://github.com/excalidraw/excalidraw/pull/3318) - -- Exporting redesign [#3613](https://github.com/excalidraw/excalidraw/pull/3613) - -- Auto-position tooltip and support overflowing container [#3631](https://github.com/excalidraw/excalidraw/pull/3631) - -- Auto release @excalidraw/excalidraw-next on every change [#3614](https://github.com/excalidraw/excalidraw/pull/3614) - -- Allow inner-drag-selecting with cmd/ctrl [#3603](https://github.com/excalidraw/excalidraw/pull/3603) - -### Fixes - -- view mode cursor adjustments [#3809](https://github.com/excalidraw/excalidraw/pull/3809). - -- Pass next release to updatePackageVersion & replace ## unreleased with new version [#3806](https://github.com/excalidraw/excalidraw/pull/3806) - -- Include deleted elements when passing to restore [#3802](https://github.com/excalidraw/excalidraw/pull/3802) - -- Import React before using jsx [#3804](https://github.com/excalidraw/excalidraw/pull/3804) - -- Ensure `s` and `g` shortcuts work on no selection [#3800](https://github.com/excalidraw/excalidraw/pull/3800) - -- Keep binding for attached arrows after changing text [#3754](https://github.com/excalidraw/excalidraw/pull/3754) - -- Deselect elements on viewMode toggle [#3741](https://github.com/excalidraw/excalidraw/pull/3741) - -- Allow pointer events for disable zen mode button [#3743](https://github.com/excalidraw/excalidraw/pull/3743) - -- Use rgba instead of shorthand alpha [#3688](https://github.com/excalidraw/excalidraw/pull/3688) - -- Color pickers not opening on mobile [#3676](https://github.com/excalidraw/excalidraw/pull/3676) - -- On contextMenu, use selected element regardless of z-index [#3668](https://github.com/excalidraw/excalidraw/pull/3668) - -- SelectedGroupIds not being stored in history [#3630](https://github.com/excalidraw/excalidraw/pull/3630) - -- Overscroll on touch devices [#3663](https://github.com/excalidraw/excalidraw/pull/3663) - -- Small UI issues around image export dialog [#3642](https://github.com/excalidraw/excalidraw/pull/3642) - -- Normalize linear element points on restore [#3633](https://github.com/excalidraw/excalidraw/pull/3633) - -- Disable pointer-events on footer-center container [#3629](https://github.com/excalidraw/excalidraw/pull/3629) - -### Refactor - -- Delete React SyntheticEvent persist [#3700](https://github.com/excalidraw/excalidraw/pull/3700) - -- Code clean up [#3681](https://github.com/excalidraw/excalidraw/pull/3681) - -### Performance - -- Improve arrow head sizing [#3480](https://github.com/excalidraw/excalidraw/pull/3480) - -### Build - -- Add release script to update relevant files and commit for next release [#3805](https://github.com/excalidraw/excalidraw/pull/3805) - -- Add script to update changelog before a stable release [#3784](https://github.com/excalidraw/excalidraw/pull/3784) - -- Add script to update readme before stable release [#3781](https://github.com/excalidraw/excalidraw/pull/3781) - ---- +Check out the [release notes](https://github.com/excalidraw/excalidraw/releases/tag/v0.9.0) ## 0.8.0 (2021-05-15) From 6a6b9c90a788b67c0208049abc67172a6637196e Mon Sep 17 00:00:00 2001 From: David Luzar Date: Tue, 13 Sep 2022 21:19:57 +0200 Subject: [PATCH 015/276] fix: revert webpack deduping to fix `@next` runtime (#5695) Revert "chore: Dedupe webpack configs. (#5449)" This reverts commit da4fa91ffc2bf9b1b74c9fec8164348273a9cad2. --- src/packages/common.webpack.dev.config.js | 88 ------------- src/packages/common.webpack.prod.config.js | 119 ------------------ src/packages/excalidraw/webpack.dev.config.js | 89 ++++++++++++- .../excalidraw/webpack.prod.config.js | 112 ++++++++++++++++- src/packages/utils/webpack.prod.config.js | 49 +++++++- 5 files changed, 234 insertions(+), 223 deletions(-) delete mode 100644 src/packages/common.webpack.dev.config.js delete mode 100644 src/packages/common.webpack.prod.config.js diff --git a/src/packages/common.webpack.dev.config.js b/src/packages/common.webpack.dev.config.js deleted file mode 100644 index 0ee82688c..000000000 --- a/src/packages/common.webpack.dev.config.js +++ /dev/null @@ -1,88 +0,0 @@ -const path = require("path"); -const autoprefixer = require("autoprefixer"); -const webpack = require("webpack"); -const { parseEnvVariables } = require(path.resolve(global.__childdir, "./env")); - -module.exports = { - mode: "development", - devtool: false, - output: { - libraryTarget: "umd", - filename: "[name].js", - publicPath: "", - }, - resolve: { - extensions: [".js", ".ts", ".tsx", ".css", ".scss"], - }, - module: { - rules: [ - { - test: /\.(sa|sc|c)ss$/, - exclude: /node_modules/, - use: [ - "style-loader", - { loader: "css-loader" }, - { - loader: "postcss-loader", - options: { - postcssOptions: { - plugins: [autoprefixer()], - }, - }, - }, - "sass-loader", - ], - }, - { - test: /\.(ts|tsx|js|jsx|mjs)$/, - exclude: /node_modules\/(?!browser-fs-access)/, - use: [ - { - loader: "ts-loader", - options: { - transpileOnly: true, - configFile: path.resolve(__dirname, "./tsconfig.dev.json"), - }, - }, - ], - }, - { - test: /\.(woff|woff2|eot|ttf|otf)$/, - type: "asset/resource", - }, - ], - }, - optimization: { - splitChunks: { - chunks: "async", - cacheGroups: { - vendors: { - test: /[\\/]node_modules[\\/]/, - name: "vendor", - }, - }, - }, - }, - plugins: [ - new webpack.EvalSourceMapDevToolPlugin({ exclude: /vendor/ }), - new webpack.DefinePlugin({ - "process.env": parseEnvVariables( - path.resolve(__dirname, "../../.env.development"), - ), - }), - ], - externals: { - react: { - root: "React", - commonjs2: "react", - commonjs: "react", - amd: "react", - }, - "react-dom": { - root: "ReactDOM", - commonjs2: "react-dom", - commonjs: "react-dom", - amd: "react-dom", - }, - }, -}; diff --git a/src/packages/common.webpack.prod.config.js b/src/packages/common.webpack.prod.config.js deleted file mode 100644 index f0cca725d..000000000 --- a/src/packages/common.webpack.prod.config.js +++ /dev/null @@ -1,119 +0,0 @@ -const path = require("path"); -const autoprefixer = require("autoprefixer"); -const webpack = require("webpack"); -const BundleAnalyzerPlugin = require(path.resolve( - path.join(global.__childdir, "node_modules"), - "webpack-bundle-analyzer", -)).BundleAnalyzerPlugin; -const TerserPlugin = require("terser-webpack-plugin"); -const { parseEnvVariables } = - "__noenv" in global ? {} : require(path.resolve(global.__childdir, "./env")); - -module.exports = { - mode: "production", - output: { - libraryTarget: "umd", - filename: "[name].js", - publicPath: "", - }, - resolve: { - extensions: [".js", ".ts", ".tsx", ".css", ".scss"], - }, - module: { - rules: [ - { - test: /\.(sa|sc|c)ss$/, - exclude: /node_modules/, - use: [ - "style-loader", - { - loader: "css-loader", - }, - { - loader: "postcss-loader", - options: { - postcssOptions: { - plugins: [autoprefixer()], - }, - }, - }, - "sass-loader", - ], - }, - { - test: /\.(ts|tsx|js|jsx|mjs)$/, - exclude: /node_modules\/(?!browser-fs-access)/, - use: [ - { - loader: "ts-loader", - options: { - transpileOnly: true, - configFile: path.resolve(__dirname, "./tsconfig.prod.json"), - }, - }, - { - loader: "babel-loader", - options: { - presets: [ - "@babel/preset-env", - ["@babel/preset-react", { runtime: "automatic" }], - "@babel/preset-typescript", - ], - plugins: [ - "transform-class-properties", - "@babel/plugin-transform-runtime", - ], - }, - }, - ], - }, - { - test: /\.(woff|woff2|eot|ttf|otf)$/, - type: "asset/resource", - }, - ], - }, - optimization: { - minimize: true, - minimizer: [ - new TerserPlugin({ - test: /\.js($|\?)/i, - }), - ], - splitChunks: { - chunks: "async", - cacheGroups: { - vendors: { - test: /[\\/]node_modules[\\/]/, - name: "vendor", - }, - }, - }, - }, - plugins: [ - ...(process.env.ANALYZER === "true" ? [new BundleAnalyzerPlugin()] : []), - ...("__noenv" in global - ? [] - : [ - new webpack.DefinePlugin({ - "process.env": parseEnvVariables( - path.resolve(__dirname, "../../.env.production"), - ), - }), - ]), - ], - externals: { - react: { - root: "React", - commonjs2: "react", - commonjs: "react", - amd: "react", - }, - "react-dom": { - root: "ReactDOM", - commonjs2: "react-dom", - commonjs: "react-dom", - amd: "react-dom", - }, - }, -}; diff --git a/src/packages/excalidraw/webpack.dev.config.js b/src/packages/excalidraw/webpack.dev.config.js index 5b535186f..9e8180f5f 100644 --- a/src/packages/excalidraw/webpack.dev.config.js +++ b/src/packages/excalidraw/webpack.dev.config.js @@ -1,18 +1,97 @@ -global.__childdir = __dirname; const path = require("path"); -const { merge } = require("webpack-merge"); -const commonConfig = require("../common.webpack.dev.config"); +const webpack = require("webpack"); +const autoprefixer = require("autoprefixer"); +const { parseEnvVariables } = require("./env"); const outputDir = process.env.EXAMPLE === "true" ? "example/public" : "dist"; -const config = { +module.exports = { + mode: "development", + devtool: false, entry: { "excalidraw.development": "./entry.js", }, output: { path: path.resolve(__dirname, outputDir), library: "ExcalidrawLib", + libraryTarget: "umd", + filename: "[name].js", chunkFilename: "excalidraw-assets-dev/[name]-[contenthash].js", assetModuleFilename: "excalidraw-assets-dev/[name][ext]", + + publicPath: "", + }, + resolve: { + extensions: [".js", ".ts", ".tsx", ".css", ".scss"], + }, + module: { + rules: [ + { + test: /\.(sa|sc|c)ss$/, + exclude: /node_modules/, + use: [ + "style-loader", + { loader: "css-loader" }, + { + loader: "postcss-loader", + options: { + postcssOptions: { + plugins: [autoprefixer()], + }, + }, + }, + "sass-loader", + ], + }, + { + test: /\.(ts|tsx|js|jsx|mjs)$/, + exclude: /node_modules\/(?!browser-fs-access)/, + use: [ + { + loader: "ts-loader", + options: { + transpileOnly: true, + configFile: path.resolve(__dirname, "../tsconfig.dev.json"), + }, + }, + ], + }, + { + test: /\.(woff|woff2|eot|ttf|otf)$/, + type: "asset/resource", + }, + ], + }, + optimization: { + splitChunks: { + chunks: "async", + cacheGroups: { + vendors: { + test: /[\\/]node_modules[\\/]/, + name: "vendor", + }, + }, + }, + }, + plugins: [ + new webpack.EvalSourceMapDevToolPlugin({ exclude: /vendor/ }), + new webpack.DefinePlugin({ + "process.env": parseEnvVariables( + path.resolve(__dirname, "../../../.env.development"), + ), + }), + ], + externals: { + react: { + root: "React", + commonjs2: "react", + commonjs: "react", + amd: "react", + }, + "react-dom": { + root: "ReactDOM", + commonjs2: "react-dom", + commonjs: "react-dom", + amd: "react-dom", + }, }, }; -module.exports = merge(commonConfig, config); diff --git a/src/packages/excalidraw/webpack.prod.config.js b/src/packages/excalidraw/webpack.prod.config.js index 593287076..0450d36fa 100644 --- a/src/packages/excalidraw/webpack.prod.config.js +++ b/src/packages/excalidraw/webpack.prod.config.js @@ -1,17 +1,119 @@ -global.__childdir = __dirname; const path = require("path"); -const { merge } = require("webpack-merge"); -const commonConfig = require("../common.webpack.prod.config"); +const TerserPlugin = require("terser-webpack-plugin"); +const BundleAnalyzerPlugin = + require("webpack-bundle-analyzer").BundleAnalyzerPlugin; +const autoprefixer = require("autoprefixer"); +const webpack = require("webpack"); +const { parseEnvVariables } = require("./env"); -const config = { +module.exports = { + mode: "production", entry: { "excalidraw.production.min": "./entry.js", }, output: { path: path.resolve(__dirname, "dist"), library: "ExcalidrawLib", + libraryTarget: "umd", + filename: "[name].js", chunkFilename: "excalidraw-assets/[name]-[contenthash].js", assetModuleFilename: "excalidraw-assets/[name][ext]", + publicPath: "", + }, + resolve: { + extensions: [".js", ".ts", ".tsx", ".css", ".scss"], + }, + module: { + rules: [ + { + test: /\.(sa|sc|c)ss$/, + exclude: /node_modules/, + use: [ + "style-loader", + { + loader: "css-loader", + }, + { + loader: "postcss-loader", + options: { + postcssOptions: { + plugins: [autoprefixer()], + }, + }, + }, + "sass-loader", + ], + }, + { + test: /\.(ts|tsx|js|jsx|mjs)$/, + exclude: /node_modules\/(?!browser-fs-access)/, + use: [ + { + loader: "ts-loader", + options: { + transpileOnly: true, + configFile: path.resolve(__dirname, "../tsconfig.prod.json"), + }, + }, + { + loader: "babel-loader", + options: { + presets: [ + "@babel/preset-env", + ["@babel/preset-react", { runtime: "automatic" }], + "@babel/preset-typescript", + ], + plugins: [ + "transform-class-properties", + "@babel/plugin-transform-runtime", + ], + }, + }, + ], + }, + { + test: /\.(woff|woff2|eot|ttf|otf)$/, + type: "asset/resource", + }, + ], + }, + optimization: { + minimize: true, + minimizer: [ + new TerserPlugin({ + test: /\.js($|\?)/i, + }), + ], + splitChunks: { + chunks: "async", + cacheGroups: { + vendors: { + test: /[\\/]node_modules[\\/]/, + name: "vendor", + }, + }, + }, + }, + plugins: [ + ...(process.env.ANALYZER === "true" ? [new BundleAnalyzerPlugin()] : []), + new webpack.DefinePlugin({ + "process.env": parseEnvVariables( + path.resolve(__dirname, "../../../.env.production"), + ), + }), + ], + externals: { + react: { + root: "React", + commonjs2: "react", + commonjs: "react", + amd: "react", + }, + "react-dom": { + root: "ReactDOM", + commonjs2: "react-dom", + commonjs: "react-dom", + amd: "react-dom", + }, }, }; -module.exports = merge(commonConfig, config); diff --git a/src/packages/utils/webpack.prod.config.js b/src/packages/utils/webpack.prod.config.js index 58a6bbf08..7238ad029 100644 --- a/src/packages/utils/webpack.prod.config.js +++ b/src/packages/utils/webpack.prod.config.js @@ -1,23 +1,60 @@ -global.__childdir = __dirname; -global.__noenv = true; const webpack = require("webpack"); const path = require("path"); -const { merge } = require("webpack-merge"); -const commonConfig = require("../common.webpack.prod.config"); +const BundleAnalyzerPlugin = + require("webpack-bundle-analyzer").BundleAnalyzerPlugin; -const config = { +module.exports = { + mode: "production", entry: { "excalidraw-utils.min": "./index.js" }, output: { path: path.resolve(__dirname, "dist"), + filename: "[name].js", library: "ExcalidrawUtils", + libraryTarget: "umd", + }, + resolve: { + extensions: [".tsx", ".ts", ".js", ".css", ".scss"], }, optimization: { runtimeChunk: false, }, + module: { + rules: [ + { + test: /\.(sa|sc|c)ss$/, + exclude: /node_modules/, + use: ["style-loader", { loader: "css-loader" }, "sass-loader"], + }, + { + test: /\.(ts|tsx|js)$/, + use: [ + { + loader: "ts-loader", + options: { + transpileOnly: true, + configFile: path.resolve(__dirname, "../tsconfig.prod.json"), + }, + }, + { + loader: "babel-loader", + + options: { + presets: [ + "@babel/preset-env", + ["@babel/preset-react", { runtime: "automatic" }], + "@babel/preset-typescript", + ], + plugins: [["@babel/plugin-transform-runtime"]], + }, + }, + ], + }, + ], + }, plugins: [ new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1, }), + ...(process.env.ANALYZER === "true" ? [new BundleAnalyzerPlugin()] : []), ], }; -module.exports = merge(commonConfig, config); From c5869979c828bac1d2d3ceb810d4e20de8af7062 Mon Sep 17 00:00:00 2001 From: Seunghyun oh Date: Wed, 14 Sep 2022 15:45:35 +0900 Subject: [PATCH 016/276] chore: fix typo in clipboard alert (#5693) chore: fix typo --- src/packages/excalidraw/example/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/excalidraw/example/App.tsx b/src/packages/excalidraw/example/App.tsx index 272120c1a..5d97bb484 100644 --- a/src/packages/excalidraw/example/App.tsx +++ b/src/packages/excalidraw/example/App.tsx @@ -282,7 +282,7 @@ export default function App() { files: excalidrawAPI?.getFiles(), type, }); - window.alert(`Copied to clipboard as ${type} sucessfully`); + window.alert(`Copied to clipboard as ${type} successfully`); }; const [pointerData, setPointerData] = useState<{ From 0d1058a596145894b703fe3078b574f83bfbb312 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Wed, 14 Sep 2022 19:55:54 +0530 Subject: [PATCH 017/276] feat: support segment midpoints in line editor (#5641) * feat: support segment midpoints in line editor * fix tests * midpoints working in bezier curve * midpoint working with non zero roughness * calculate beizer curve control points for points >2 * unnecessary rerender * don't show phantom points inside editor for short segments * don't show phantom points for small curves * improve the algo for plotting midpoints on bezier curve by taking arc lengths and doing binary search * fix tests finally * fix naming * cache editor midpoints * clear midpoint cache when undo * fix caching * calculate index properly when not all segments have midpoints * make sure correct element version is fetched from cache * chore * fix * direct comparison for equal points * create arePointsEqual util * upate name * don't update cache except inside getter * don't compute midpoints outside editor unless 2pointer lines * update cache to object and burst when Zoom updated as well * early return if midpoints not present outside editor * don't early return * cleanup * Add specs * fix --- src/components/App.tsx | 37 ++- src/element/linearElementEditor.ts | 299 ++++++++++++++---- src/math.ts | 166 +++++++++- src/renderer/renderScene.ts | 63 ++-- .../linearElementEditor.test.tsx.snap | 60 ++++ .../regressionTests.test.tsx.snap | 8 +- src/tests/linearElementEditor.test.tsx | 146 +++++++++ 7 files changed, 666 insertions(+), 113 deletions(-) create mode 100644 src/tests/__snapshots__/linearElementEditor.test.tsx.snap create mode 100644 src/tests/linearElementEditor.test.tsx diff --git a/src/components/App.tsx b/src/components/App.tsx index 91a6cbe7f..0ecbe27e4 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -2718,18 +2718,23 @@ class App extends React.Component { event, scenePointerX, scenePointerY, - this.state.editingLinearElement, - this.state.gridSize, + this.state, ); - if (editingLinearElement !== this.state.editingLinearElement) { + + if ( + editingLinearElement && + editingLinearElement !== this.state.editingLinearElement + ) { // Since we are reading from previous state which is not possible with // automatic batching in React 18 hence using flush sync to synchronously // update the state. Check https://github.com/excalidraw/excalidraw/pull/5508 for more details. flushSync(() => { - this.setState({ editingLinearElement }); + this.setState({ + editingLinearElement, + }); }); } - if (editingLinearElement.lastUncommittedPoint != null) { + if (editingLinearElement?.lastUncommittedPoint != null) { this.maybeSuggestBindingAtCursor(scenePointer); } else { this.setState({ suggestedBindings: [] }); @@ -3058,7 +3063,7 @@ class App extends React.Component { } if (this.state.selectedLinearElement) { let hoverPointIndex = -1; - let midPointHovered = false; + let segmentMidPointHoveredCoords = null; if ( isHittingElementNotConsideringBoundingBox(element, this.state, [ scenePointerX, @@ -3071,13 +3076,14 @@ class App extends React.Component { scenePointerX, scenePointerY, ); - midPointHovered = LinearElementEditor.isHittingMidPoint( - linearElementEditor, - { x: scenePointerX, y: scenePointerY }, - this.state, - ); + segmentMidPointHoveredCoords = + LinearElementEditor.getSegmentMidpointHitCoords( + linearElementEditor, + { x: scenePointerX, y: scenePointerY }, + this.state, + ); - if (hoverPointIndex >= 0 || midPointHovered) { + if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) { setCursor(this.canvas, CURSOR_TYPE.POINTER); } else { setCursor(this.canvas, CURSOR_TYPE.MOVE); @@ -3106,12 +3112,15 @@ class App extends React.Component { } if ( - this.state.selectedLinearElement.midPointHovered !== midPointHovered + !LinearElementEditor.arePointsEqual( + this.state.selectedLinearElement.segmentMidPointHoveredCoords, + segmentMidPointHoveredCoords, + ) ) { this.setState({ selectedLinearElement: { ...this.state.selectedLinearElement, - midPointHovered, + segmentMidPointHoveredCoords, }, }); } diff --git a/src/element/linearElementEditor.ts b/src/element/linearElementEditor.ts index 48972a823..df9157486 100644 --- a/src/element/linearElementEditor.ts +++ b/src/element/linearElementEditor.ts @@ -12,6 +12,11 @@ import { getGridPoint, rotatePoint, centerPoint, + getControlPointsForBezierCurve, + getBezierXY, + getBezierCurveLength, + mapIntervalToBezierT, + arePointsEqual, } from "../math"; import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from "."; import { getElementPointsCoords } from "./bounds"; @@ -29,6 +34,12 @@ import { tupleToCoors } from "../utils"; import { isBindingElement } from "./typeChecks"; import { shouldRotateWithDiscreteAngle } from "../keys"; +const editorMidPointsCache: { + version: number | null; + points: (Point | null)[]; + zoom: number | null; +} = { version: null, points: [], zoom: null }; + export class LinearElementEditor { public readonly elementId: ExcalidrawElement["id"] & { _brand: "excalidrawLinearElementId"; @@ -52,7 +63,7 @@ export class LinearElementEditor { | "keep"; public readonly endBindingElement: ExcalidrawBindableElement | null | "keep"; public readonly hoverPointIndex: number; - public readonly midPointHovered: boolean; + public readonly segmentMidPointHoveredCoords: Point | null; constructor(element: NonDeleted, scene: Scene) { this.elementId = element.id as string & { @@ -72,7 +83,7 @@ export class LinearElementEditor { lastClickedPoint: -1, }; this.hoverPointIndex = -1; - this.midPointHovered = false; + this.segmentMidPointHoveredCoords = null; } // --------------------------------------------------------------------------- @@ -80,7 +91,6 @@ export class LinearElementEditor { // --------------------------------------------------------------------------- static POINT_HANDLE_SIZE = 10; - /** * @param id the `elementId` from the instance of this class (so that we can * statically guarantee this method returns an ExcalidrawLinearElement) @@ -359,7 +369,60 @@ export class LinearElementEditor { }; } - static isHittingMidPoint = ( + static getEditorMidPoints = ( + element: NonDeleted, + appState: AppState, + ): typeof editorMidPointsCache["points"] => { + // Since its not needed outside editor unless 2 pointer lines + if (!appState.editingLinearElement && element.points.length > 2) { + return []; + } + if ( + editorMidPointsCache.version === element.version && + editorMidPointsCache.zoom === appState.zoom.value + ) { + return editorMidPointsCache.points; + } + LinearElementEditor.updateEditorMidPointsCache(element, appState); + return editorMidPointsCache.points!; + }; + + static updateEditorMidPointsCache = ( + element: NonDeleted, + appState: AppState, + ) => { + const points = LinearElementEditor.getPointsGlobalCoordinates(element); + + let index = 0; + const midpoints: (Point | null)[] = []; + while (index < points.length - 1) { + if ( + LinearElementEditor.isSegmentTooShort( + element, + element.points[index], + element.points[index + 1], + appState.zoom, + ) + ) { + midpoints.push(null); + index++; + continue; + } + const segmentMidPoint = LinearElementEditor.getSegmentMidPoint( + element, + points[index], + points[index + 1], + index + 1, + ); + midpoints.push(segmentMidPoint); + index++; + } + editorMidPointsCache.points = midpoints; + editorMidPointsCache.version = element.version; + editorMidPointsCache.zoom = appState.zoom.value; + }; + + static getSegmentMidpointHitCoords = ( linearElementEditor: LinearElementEditor, scenePointer: { x: number; y: number }, appState: AppState, @@ -367,7 +430,7 @@ export class LinearElementEditor { const { elementId } = linearElementEditor; const element = LinearElementEditor.getElement(elementId); if (!element) { - return false; + return null; } const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor( element, @@ -376,37 +439,125 @@ export class LinearElementEditor { 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); + if (points.length >= 3 && !appState.editingLinearElement) { + return null; + } - return centerPoint(points[0], points.at(-1)!); + const threshold = + LinearElementEditor.POINT_HANDLE_SIZE / appState.zoom.value; + + const existingSegmentMidpointHitCoords = + linearElementEditor.segmentMidPointHoveredCoords; + if (existingSegmentMidpointHitCoords) { + const distance = distance2d( + existingSegmentMidpointHitCoords[0], + existingSegmentMidpointHitCoords[1], + scenePointer.x, + scenePointer.y, + ); + if (distance <= threshold) { + return existingSegmentMidpointHitCoords; + } + } + let index = 0; + const midPoints: typeof editorMidPointsCache["points"] = + LinearElementEditor.getEditorMidPoints(element, appState); + while (index < midPoints.length) { + if (midPoints[index] !== null) { + const distance = distance2d( + midPoints[index]![0], + midPoints[index]![1], + scenePointer.x, + scenePointer.y, + ); + if (distance <= threshold) { + return midPoints[index]; + } + } + + index++; + } + return null; + }; + + static isSegmentTooShort( + element: NonDeleted, + startPoint: Point, + endPoint: Point, + zoom: AppState["zoom"], + ) { + let distance = distance2d( + startPoint[0], + startPoint[1], + endPoint[0], + endPoint[1], + ); + if (element.points.length > 2 && element.strokeSharpness === "round") { + distance = getBezierCurveLength(element, endPoint); + } + + return distance * zoom.value < LinearElementEditor.POINT_HANDLE_SIZE * 4; + } + + static getSegmentMidPoint( + element: NonDeleted, + startPoint: Point, + endPoint: Point, + endPointIndex: number, + ) { + let segmentMidPoint = centerPoint(startPoint, endPoint); + if (element.points.length > 2 && element.strokeSharpness === "round") { + const controlPoints = getControlPointsForBezierCurve( + element, + element.points[endPointIndex], + ); + if (controlPoints) { + const t = mapIntervalToBezierT( + element, + element.points[endPointIndex], + 0.5, + ); + + const [tx, ty] = getBezierXY( + controlPoints[0], + controlPoints[1], + controlPoints[2], + controlPoints[3], + t, + ); + segmentMidPoint = LinearElementEditor.getPointGlobalCoordinates( + element, + [tx, ty], + ); + } + } + + return segmentMidPoint; + } + + static getSegmentMidPointIndex( + linearElementEditor: LinearElementEditor, + appState: AppState, + midPoint: Point, + ) { + const element = LinearElementEditor.getElement( + linearElementEditor.elementId, + ); + if (!element) { + return -1; + } + const midPoints = LinearElementEditor.getEditorMidPoints(element, appState); + let index = 0; + while (index < midPoints.length - 1) { + if (LinearElementEditor.arePointsEqual(midPoint, midPoints[index])) { + return index + 1; + } + index++; + } + return -1; } static handlePointerDown( @@ -438,33 +589,32 @@ export class LinearElementEditor { if (!element) { return ret; } - const hittingMidPoint = LinearElementEditor.isHittingMidPoint( + const segmentMidPoint = LinearElementEditor.getSegmentMidpointHitCoords( linearElementEditor, scenePointer, appState, ); - if ( - LinearElementEditor.isHittingMidPoint( + if (segmentMidPoint) { + const index = LinearElementEditor.getSegmentMidPointIndex( 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), - ], - }); - } + segmentMidPoint, + ); + const newMidPoint = LinearElementEditor.createPointAt( + element, + segmentMidPoint[0], + segmentMidPoint[1], + appState.gridSize, + ); + const points = [ + ...element.points.slice(0, index), + newMidPoint, + ...element.points.slice(index), + ]; + mutateElement(element, { + points, + }); + ret.didAddPoint = true; ret.isMidPoint = true; ret.linearElementEditor = { @@ -520,7 +670,7 @@ export class LinearElementEditor { // if we clicked on a point, set the element as hitElement otherwise // it would get deselected if the point is outside the hitbox area - if (clickedPointIndex >= 0 || hittingMidPoint) { + if (clickedPointIndex >= 0 || segmentMidPoint) { ret.hitElement = element; } else { // You might be wandering why we are storing the binding elements on @@ -579,17 +729,29 @@ export class LinearElementEditor { return ret; } + static arePointsEqual(point1: Point | null, point2: Point | null) { + if (!point1 && !point2) { + return true; + } + if (!point1 || !point2) { + return false; + } + return arePointsEqual(point1, point2); + } + static handlePointerMove( event: React.PointerEvent, scenePointerX: number, scenePointerY: number, - linearElementEditor: LinearElementEditor, - gridSize: number | null, - ): LinearElementEditor { - const { elementId, lastUncommittedPoint } = linearElementEditor; + appState: AppState, + ): LinearElementEditor | null { + if (!appState.editingLinearElement) { + return null; + } + const { elementId, lastUncommittedPoint } = appState.editingLinearElement; const element = LinearElementEditor.getElement(elementId); if (!element) { - return linearElementEditor; + return appState.editingLinearElement; } const { points } = element; @@ -599,7 +761,10 @@ export class LinearElementEditor { if (lastPoint === lastUncommittedPoint) { LinearElementEditor.deletePoints(element, [points.length - 1]); } - return { ...linearElementEditor, lastUncommittedPoint: null }; + return { + ...appState.editingLinearElement, + lastUncommittedPoint: null, + }; } let newPoint: Point; @@ -611,7 +776,7 @@ export class LinearElementEditor { element, lastCommittedPoint, [scenePointerX, scenePointerY], - gridSize, + appState.gridSize, ); newPoint = [ @@ -621,9 +786,9 @@ export class LinearElementEditor { } else { newPoint = LinearElementEditor.createPointAt( element, - scenePointerX - linearElementEditor.pointerOffset.x, - scenePointerY - linearElementEditor.pointerOffset.y, - gridSize, + scenePointerX - appState.editingLinearElement.pointerOffset.x, + scenePointerY - appState.editingLinearElement.pointerOffset.y, + appState.gridSize, ); } @@ -635,11 +800,10 @@ export class LinearElementEditor { }, ]); } else { - LinearElementEditor.addPoints(element, [{ point: newPoint }]); + LinearElementEditor.addPoints(element, appState, [{ point: newPoint }]); } - return { - ...linearElementEditor, + ...appState.editingLinearElement, lastUncommittedPoint: element.points[element.points.length - 1], }; } @@ -884,6 +1048,7 @@ export class LinearElementEditor { static addPoints( element: NonDeleted, + appState: AppState, targetPoints: { point: Point }[], ) { const offsetX = 0; diff --git a/src/math.ts b/src/math.ts index 1f2ffc4f5..dd1a73b56 100644 --- a/src/math.ts +++ b/src/math.ts @@ -1,6 +1,8 @@ import { NormalizedZoomValue, Point, Zoom } from "./types"; import { LINE_CONFIRM_THRESHOLD } from "./constants"; -import { ExcalidrawLinearElement } from "./element/types"; +import { ExcalidrawLinearElement, NonDeleted } from "./element/types"; +import { getShapeForElement } from "./renderer/renderElement"; +import { getCurvePathOps } from "./element/bounds"; export const rotate = ( x1: number, @@ -263,3 +265,165 @@ export const getGridPoint = ( } return [x, y]; }; + +export const getControlPointsForBezierCurve = ( + element: NonDeleted, + endPoint: Point, +) => { + const shape = getShapeForElement(element as ExcalidrawLinearElement); + if (!shape) { + return null; + } + + const ops = getCurvePathOps(shape[0]); + let currentP: Mutable = [0, 0]; + let index = 0; + let minDistance = Infinity; + let controlPoints: Mutable[] | null = null; + + while (index < ops.length) { + const { op, data } = ops[index]; + if (op === "move") { + currentP = data as unknown as Mutable; + } + if (op === "bcurveTo") { + const p0 = currentP; + const p1 = [data[0], data[1]] as Mutable; + const p2 = [data[2], data[3]] as Mutable; + const p3 = [data[4], data[5]] as Mutable; + const distance = distance2d(p3[0], p3[1], endPoint[0], endPoint[1]); + if (distance < minDistance) { + minDistance = distance; + controlPoints = [p0, p1, p2, p3]; + } + currentP = p3; + } + index++; + } + + return controlPoints; +}; + +export const getBezierXY = ( + p0: Point, + p1: Point, + p2: Point, + p3: Point, + t: number, +) => { + const equation = (t: number, idx: number) => + Math.pow(1 - t, 3) * p3[idx] + + 3 * t * Math.pow(1 - t, 2) * p2[idx] + + 3 * Math.pow(t, 2) * (1 - t) * p1[idx] + + p0[idx] * Math.pow(t, 3); + const tx = equation(t, 0); + const ty = equation(t, 1); + return [tx, ty]; +}; + +export const getPointsInBezierCurve = ( + element: NonDeleted, + endPoint: Point, +) => { + const controlPoints: Mutable[] = getControlPointsForBezierCurve( + element, + endPoint, + )!; + if (!controlPoints) { + return []; + } + const pointsOnCurve: Mutable[] = []; + let t = 1; + // Take 20 points on curve for better accuracy + while (t > 0) { + const point = getBezierXY( + controlPoints[0], + controlPoints[1], + controlPoints[2], + controlPoints[3], + t, + ); + pointsOnCurve.push([point[0], point[1]]); + t -= 0.05; + } + if (pointsOnCurve.length) { + if (arePointsEqual(pointsOnCurve.at(-1)!, endPoint)) { + pointsOnCurve.push([endPoint[0], endPoint[1]]); + } + } + return pointsOnCurve; +}; + +export const getBezierCurveArcLengths = ( + element: NonDeleted, + endPoint: Point, +) => { + const arcLengths: number[] = []; + arcLengths[0] = 0; + const points = getPointsInBezierCurve(element, endPoint); + let index = 0; + let distance = 0; + while (index < points.length - 1) { + const segmentDistance = distance2d( + points[index][0], + points[index][1], + points[index + 1][0], + points[index + 1][1], + ); + distance += segmentDistance; + arcLengths.push(distance); + index++; + } + + return arcLengths; +}; + +export const getBezierCurveLength = ( + element: NonDeleted, + endPoint: Point, +) => { + const arcLengths = getBezierCurveArcLengths(element, endPoint); + return arcLengths.at(-1) as number; +}; + +// This maps interval to actual interval t on the curve so that when t = 0.5, its actually the point at 50% of the length +export const mapIntervalToBezierT = ( + element: NonDeleted, + endPoint: Point, + interval: number, // The interval between 0 to 1 for which you want to find the point on the curve, +) => { + const arcLengths = getBezierCurveArcLengths(element, endPoint); + const pointsCount = arcLengths.length - 1; + const curveLength = arcLengths.at(-1) as number; + const targetLength = interval * curveLength; + let low = 0; + let high = pointsCount; + let index = 0; + // Doing a binary search to find the largest length that is less than the target length + while (low < high) { + index = Math.floor(low + (high - low) / 2); + if (arcLengths[index] < targetLength) { + low = index + 1; + } else { + high = index; + } + } + if (arcLengths[index] > targetLength) { + index--; + } + if (arcLengths[index] === targetLength) { + return index / pointsCount; + } + + return ( + 1 - + (index + + (targetLength - arcLengths[index]) / + (arcLengths[index + 1] - arcLengths[index])) / + pointsCount + ); +}; + +export const arePointsEqual = (p1: Point, p2: Point) => { + return p1[0] === p2[0] && p1[1] === p2[1]; +}; diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index 8ac1e1ab7..d4c6e19de 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -197,12 +197,7 @@ const renderLinearPointHandles = ( context.translate(renderConfig.scrollX, renderConfig.scrollY); context.lineWidth = 1 / renderConfig.zoom.value; const points = LinearElementEditor.getPointsGlobalCoordinates(element); - const centerPoint = LinearElementEditor.getMidPoint( - appState.selectedLinearElement, - ); - if (!centerPoint) { - return; - } + const { POINT_HANDLE_SIZE } = LinearElementEditor; const radius = appState.editingLinearElement ? POINT_HANDLE_SIZE @@ -221,11 +216,20 @@ const renderLinearPointHandles = ( ); }); - if (points.length < 3) { - if (appState.selectedLinearElement.midPointHovered) { - const centerPoint = LinearElementEditor.getMidPoint( - appState.selectedLinearElement, - )!; + //Rendering segment mid points + const midPoints = LinearElementEditor.getEditorMidPoints( + element, + appState, + ).filter((midPoint) => midPoint !== null) as Point[]; + + midPoints.forEach((segmentMidPoint) => { + if ( + appState?.selectedLinearElement?.segmentMidPointHoveredCoords && + LinearElementEditor.arePointsEqual( + segmentMidPoint, + appState.selectedLinearElement.segmentMidPointHoveredCoords, + ) + ) { // The order of renderingSingleLinearPoint and highLight points is different // inside vs outside editor as hover states are different, // in editor when hovered the original point is not visible as hover state fully covers it whereas outside the @@ -235,34 +239,34 @@ const renderLinearPointHandles = ( context, appState, renderConfig, - centerPoint, + segmentMidPoint, radius, false, ); - highlightPoint(centerPoint, context, renderConfig); + highlightPoint(segmentMidPoint, context, renderConfig); } else { - highlightPoint(centerPoint, context, renderConfig); + highlightPoint(segmentMidPoint, context, renderConfig); renderSingleLinearPoint( context, appState, renderConfig, - centerPoint, + segmentMidPoint, radius, false, ); } - } else { + } else if (appState.editingLinearElement || points.length === 2) { renderSingleLinearPoint( context, appState, renderConfig, - centerPoint, + segmentMidPoint, POINT_HANDLE_SIZE / 2, false, true, ); } - } + }); context.restore(); }; @@ -403,6 +407,20 @@ export const _renderScene = ({ visibleElements.forEach((element) => { try { renderElement(element, rc, context, renderConfig); + // Getting the element using LinearElementEditor during collab mismatches version - being one head of visible elements due to + // ShapeCache returns empty hence making sure that we get the + // correct element from visible elements + if (appState.editingLinearElement?.elementId === element.id) { + if (element) { + renderLinearPointHandles( + context, + appState, + renderConfig, + element as NonDeleted, + ); + } + } + if (!isExporting) { renderLinkIcon(element, context, appState); } @@ -411,15 +429,6 @@ export const _renderScene = ({ } }); - if (appState.editingLinearElement) { - const element = LinearElementEditor.getElement( - appState.editingLinearElement.elementId, - ); - if (element) { - renderLinearPointHandles(context, appState, renderConfig, element); - } - } - // Paint selection element if (appState.selectionElement) { try { diff --git a/src/tests/__snapshots__/linearElementEditor.test.tsx.snap b/src/tests/__snapshots__/linearElementEditor.test.tsx.snap new file mode 100644 index 000000000..945d8197a --- /dev/null +++ b/src/tests/__snapshots__/linearElementEditor.test.tsx.snap @@ -0,0 +1,60 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` Test Linear Elements Inside editor should allow dragging line from midpoint in 2 pointer lines 1`] = ` +Array [ + Array [ + 0, + 0, + ], + Array [ + 70, + 50, + ], + Array [ + 40, + 0, + ], +] +`; + +exports[` Test Linear Elements Inside editor should allow dragging lines from midpoints in between segments 1`] = ` +Array [ + Array [ + 0, + 0, + ], + Array [ + 85, + 75, + ], + Array [ + 70, + 50, + ], + Array [ + 105, + 75, + ], + Array [ + 40, + 0, + ], +] +`; + +exports[` Test Linear Elements should allow dragging line from midpoint in 2 pointer lines outside editor 1`] = ` +Array [ + Array [ + 0, + 0, + ], + Array [ + 70, + 50, + ], + Array [ + 40, + 0, + ], +] +`; diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap index 3f023df45..1a30d90a4 100644 --- a/src/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap @@ -10982,7 +10982,6 @@ Object { "hoverPointIndex": -1, "isDragging": false, "lastUncommittedPoint": null, - "midPointHovered": false, "pointerDownState": Object { "lastClickedPoint": -1, "prevSelectedPointsIndices": null, @@ -10991,6 +10990,7 @@ Object { "x": 0, "y": 0, }, + "segmentMidPointHoveredCoords": null, "selectedPointsIndices": null, "startBindingElement": "keep", }, @@ -11208,7 +11208,6 @@ Object { "hoverPointIndex": -1, "isDragging": false, "lastUncommittedPoint": null, - "midPointHovered": false, "pointerDownState": Object { "lastClickedPoint": -1, "prevSelectedPointsIndices": null, @@ -11217,6 +11216,7 @@ Object { "x": 0, "y": 0, }, + "segmentMidPointHoveredCoords": null, "selectedPointsIndices": null, "startBindingElement": "keep", }, @@ -11661,7 +11661,6 @@ Object { "hoverPointIndex": -1, "isDragging": false, "lastUncommittedPoint": null, - "midPointHovered": false, "pointerDownState": Object { "lastClickedPoint": -1, "prevSelectedPointsIndices": null, @@ -11670,6 +11669,7 @@ Object { "x": 0, "y": 0, }, + "segmentMidPointHoveredCoords": null, "selectedPointsIndices": null, "startBindingElement": "keep", }, @@ -12066,7 +12066,6 @@ Object { "hoverPointIndex": -1, "isDragging": false, "lastUncommittedPoint": null, - "midPointHovered": false, "pointerDownState": Object { "lastClickedPoint": -1, "prevSelectedPointsIndices": null, @@ -12075,6 +12074,7 @@ Object { "x": 0, "y": 0, }, + "segmentMidPointHoveredCoords": null, "selectedPointsIndices": null, "startBindingElement": "keep", }, diff --git a/src/tests/linearElementEditor.test.tsx b/src/tests/linearElementEditor.test.tsx new file mode 100644 index 000000000..c697fa2cb --- /dev/null +++ b/src/tests/linearElementEditor.test.tsx @@ -0,0 +1,146 @@ +import ReactDOM from "react-dom"; +import { ExcalidrawLinearElement } from "../element/types"; +import ExcalidrawApp from "../excalidraw-app"; +import { centerPoint } from "../math"; +import { reseed } from "../random"; +import * as Renderer from "../renderer/renderScene"; +import { Keyboard } from "./helpers/ui"; +import { screen } from "./test-utils"; + +import { render, fireEvent } from "./test-utils"; +import { Point } from "../types"; +import { KEYS } from "../keys"; +import { LinearElementEditor } from "../element/linearElementEditor"; + +const renderScene = jest.spyOn(Renderer, "renderScene"); + +const { h } = window; + +describe(" Test Linear Elements", () => { + let getByToolName: (...args: string[]) => HTMLElement; + let container: HTMLElement; + let canvas: HTMLCanvasElement; + + beforeEach(async () => { + // Unmount ReactDOM from root + ReactDOM.unmountComponentAtNode(document.getElementById("root")!); + localStorage.clear(); + renderScene.mockClear(); + reseed(7); + const comp = await render(); + getByToolName = comp.getByToolName; + container = comp.container; + canvas = container.querySelector("canvas")!; + }); + + const p1: Point = [20, 20]; + const p2: Point = [60, 20]; + const midpoint = centerPoint(p1, p2); + + const createTwoPointerLinearElement = ( + type: ExcalidrawLinearElement["type"], + edge: "Sharp" | "Round" = "Sharp", + roughness: "Architect" | "Cartoonist" | "Artist" = "Architect", + ) => { + const tool = getByToolName(type); + fireEvent.click(tool); + fireEvent.click(screen.getByTitle(edge)); + fireEvent.click(screen.getByTitle(roughness)); + fireEvent.pointerDown(canvas, { clientX: p1[0], clientY: p1[1] }); + fireEvent.pointerMove(canvas, { clientX: p2[0], clientY: p2[1] }); + fireEvent.pointerUp(canvas, { clientX: p2[0], clientY: p2[1] }); + }; + + const createThreePointerLinearElement = ( + type: ExcalidrawLinearElement["type"], + edge: "Sharp" | "Round" = "Sharp", + ) => { + createTwoPointerLinearElement("line"); + // Extending line via midpoint + fireEvent.pointerDown(canvas, { + clientX: midpoint[0], + clientY: midpoint[1], + }); + fireEvent.pointerMove(canvas, { + clientX: midpoint[0] + 50, + clientY: midpoint[1] + 50, + }); + fireEvent.pointerUp(canvas, { + clientX: midpoint[0] + 50, + clientY: midpoint[1] + 50, + }); + }; + + const dragLinearElementFromPoint = (point: Point) => { + fireEvent.pointerDown(canvas, { + clientX: point[0], + clientY: point[1], + }); + fireEvent.pointerMove(canvas, { + clientX: point[0] + 50, + clientY: point[1] + 50, + }); + fireEvent.pointerUp(canvas, { + clientX: point[0] + 50, + clientY: point[1] + 50, + }); + }; + + it("should allow dragging line from midpoint in 2 pointer lines outside editor", async () => { + createTwoPointerLinearElement("line"); + const line = h.elements[0] as ExcalidrawLinearElement; + + expect(renderScene).toHaveBeenCalledTimes(10); + expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2); + + // drag line from midpoint + dragLinearElementFromPoint(midpoint); + expect(renderScene).toHaveBeenCalledTimes(13); + expect(line.points.length).toEqual(3); + expect(line.points).toMatchSnapshot(); + }); + + describe("Inside editor", () => { + it("should allow dragging line from midpoint in 2 pointer lines", async () => { + createTwoPointerLinearElement("line"); + const line = h.elements[0] as ExcalidrawLinearElement; + + fireEvent.click(canvas, { clientX: p1[0], clientY: p1[1] }); + + Keyboard.keyPress(KEYS.ENTER); + expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); + + // drag line from midpoint + dragLinearElementFromPoint(midpoint); + expect(line.points.length).toEqual(3); + expect(line.points).toMatchSnapshot(); + }); + + it("should allow dragging lines from midpoints in between segments", async () => { + createThreePointerLinearElement("line"); + + const line = h.elements[0] as ExcalidrawLinearElement; + expect(line.points.length).toEqual(3); + fireEvent.click(canvas, { clientX: p1[0], clientY: p1[1] }); + + Keyboard.keyPress(KEYS.ENTER); + expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); + + let points = LinearElementEditor.getPointsGlobalCoordinates(line); + const firstSegmentMidpoint = centerPoint(points[0], points[1]); + // drag line via first segment midpoint + dragLinearElementFromPoint(firstSegmentMidpoint); + expect(line.points.length).toEqual(4); + + // drag line from last segment midpoint + points = LinearElementEditor.getPointsGlobalCoordinates(line); + const lastSegmentMidpoint = centerPoint(points.at(-2)!, points.at(-1)!); + dragLinearElementFromPoint(lastSegmentMidpoint); + expect(line.points.length).toEqual(5); + + expect( + (h.elements[0] as ExcalidrawLinearElement).points, + ).toMatchSnapshot(); + }); + }); +}); From 5390617c0180772c028ac24cdd24c8a72e2937e8 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Thu, 15 Sep 2022 19:31:55 +0530 Subject: [PATCH 018/276] test: add more specs for line editor segment midpoints (#5698) * tests: add more specs for line editor segment midpoints * use API to create elements * Add specs for checking midpoint hidden when points too close --- .../linearElementEditor.test.tsx.snap | 60 -- src/tests/linearElementEditor.test.tsx | 543 ++++++++++++++++-- 2 files changed, 484 insertions(+), 119 deletions(-) delete mode 100644 src/tests/__snapshots__/linearElementEditor.test.tsx.snap diff --git a/src/tests/__snapshots__/linearElementEditor.test.tsx.snap b/src/tests/__snapshots__/linearElementEditor.test.tsx.snap deleted file mode 100644 index 945d8197a..000000000 --- a/src/tests/__snapshots__/linearElementEditor.test.tsx.snap +++ /dev/null @@ -1,60 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Test Linear Elements Inside editor should allow dragging line from midpoint in 2 pointer lines 1`] = ` -Array [ - Array [ - 0, - 0, - ], - Array [ - 70, - 50, - ], - Array [ - 40, - 0, - ], -] -`; - -exports[` Test Linear Elements Inside editor should allow dragging lines from midpoints in between segments 1`] = ` -Array [ - Array [ - 0, - 0, - ], - Array [ - 85, - 75, - ], - Array [ - 70, - 50, - ], - Array [ - 105, - 75, - ], - Array [ - 40, - 0, - ], -] -`; - -exports[` Test Linear Elements should allow dragging line from midpoint in 2 pointer lines outside editor 1`] = ` -Array [ - Array [ - 0, - 0, - ], - Array [ - 70, - 50, - ], - Array [ - 40, - 0, - ], -] -`; diff --git a/src/tests/linearElementEditor.test.tsx b/src/tests/linearElementEditor.test.tsx index c697fa2cb..dccd6f87b 100644 --- a/src/tests/linearElementEditor.test.tsx +++ b/src/tests/linearElementEditor.test.tsx @@ -4,10 +4,9 @@ import ExcalidrawApp from "../excalidraw-app"; import { centerPoint } from "../math"; import { reseed } from "../random"; import * as Renderer from "../renderer/renderScene"; -import { Keyboard } from "./helpers/ui"; -import { screen } from "./test-utils"; - -import { render, fireEvent } from "./test-utils"; +import { Keyboard, Pointer } from "./helpers/ui"; +import { screen, render, fireEvent } from "./test-utils"; +import { API } from "../tests/helpers/api"; import { Point } from "../types"; import { KEYS } from "../keys"; import { LinearElementEditor } from "../element/linearElementEditor"; @@ -17,7 +16,6 @@ const renderScene = jest.spyOn(Renderer, "renderScene"); const { h } = window; describe(" Test Linear Elements", () => { - let getByToolName: (...args: string[]) => HTMLElement; let container: HTMLElement; let canvas: HTMLCanvasElement; @@ -28,119 +26,546 @@ describe(" Test Linear Elements", () => { renderScene.mockClear(); reseed(7); const comp = await render(); - getByToolName = comp.getByToolName; container = comp.container; canvas = container.querySelector("canvas")!; + canvas.width = 1000; + canvas.height = 1000; }); const p1: Point = [20, 20]; const p2: Point = [60, 20]; const midpoint = centerPoint(p1, p2); + const delta = 50; + const mouse = new Pointer("mouse"); const createTwoPointerLinearElement = ( type: ExcalidrawLinearElement["type"], - edge: "Sharp" | "Round" = "Sharp", - roughness: "Architect" | "Cartoonist" | "Artist" = "Architect", + strokeSharpness: ExcalidrawLinearElement["strokeSharpness"] = "sharp", + roughness: ExcalidrawLinearElement["roughness"] = 0, ) => { - const tool = getByToolName(type); - fireEvent.click(tool); - fireEvent.click(screen.getByTitle(edge)); - fireEvent.click(screen.getByTitle(roughness)); - fireEvent.pointerDown(canvas, { clientX: p1[0], clientY: p1[1] }); - fireEvent.pointerMove(canvas, { clientX: p2[0], clientY: p2[1] }); - fireEvent.pointerUp(canvas, { clientX: p2[0], clientY: p2[1] }); + h.elements = [ + API.createElement({ + x: p1[0], + y: p1[1], + width: p2[0] - p1[0], + height: 0, + type, + roughness, + points: [ + [0, 0], + [p2[0] - p1[0], p2[1] - p1[1]], + ], + strokeSharpness, + }), + ]; + + mouse.clickAt(p1[0], p1[1]); }; const createThreePointerLinearElement = ( type: ExcalidrawLinearElement["type"], - edge: "Sharp" | "Round" = "Sharp", + strokeSharpness: ExcalidrawLinearElement["strokeSharpness"] = "sharp", + roughness: ExcalidrawLinearElement["roughness"] = 0, ) => { - createTwoPointerLinearElement("line"); - // Extending line via midpoint + //dragging line from midpoint + const p3 = [midpoint[0] + delta - p1[0], midpoint[1] + delta - p1[1]]; + h.elements = [ + API.createElement({ + x: p1[0], + y: p1[1], + width: p3[0] - p1[0], + height: 0, + type, + roughness, + points: [ + [0, 0], + [p3[0], p3[1]], + [p2[0] - p1[0], p2[1] - p1[1]], + ], + strokeSharpness, + }), + ]; + mouse.clickAt(p1[0], p1[1]); + }; + + const enterLineEditingMode = (line: ExcalidrawLinearElement) => { + mouse.clickAt(p1[0], p1[1]); + Keyboard.keyPress(KEYS.ENTER); + expect(h.state.editingLinearElement?.elementId).toEqual(line.id); + }; + + const drag = (startPoint: Point, endPoint: Point) => { fireEvent.pointerDown(canvas, { - clientX: midpoint[0], - clientY: midpoint[1], + clientX: startPoint[0], + clientY: startPoint[1], }); fireEvent.pointerMove(canvas, { - clientX: midpoint[0] + 50, - clientY: midpoint[1] + 50, + clientX: endPoint[0], + clientY: endPoint[1], }); fireEvent.pointerUp(canvas, { - clientX: midpoint[0] + 50, - clientY: midpoint[1] + 50, + clientX: endPoint[0], + clientY: endPoint[1], }); }; - const dragLinearElementFromPoint = (point: Point) => { + const deletePoint = (point: Point) => { fireEvent.pointerDown(canvas, { clientX: point[0], clientY: point[1], }); - fireEvent.pointerMove(canvas, { - clientX: point[0] + 50, - clientY: point[1] + 50, - }); fireEvent.pointerUp(canvas, { - clientX: point[0] + 50, - clientY: point[1] + 50, + clientX: point[0], + clientY: point[1], }); + Keyboard.keyPress(KEYS.DELETE); }; it("should allow dragging line from midpoint in 2 pointer lines outside editor", async () => { createTwoPointerLinearElement("line"); const line = h.elements[0] as ExcalidrawLinearElement; - expect(renderScene).toHaveBeenCalledTimes(10); + expect(renderScene).toHaveBeenCalledTimes(6); expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2); // drag line from midpoint - dragLinearElementFromPoint(midpoint); - expect(renderScene).toHaveBeenCalledTimes(13); + drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]); + expect(renderScene).toHaveBeenCalledTimes(9); expect(line.points.length).toEqual(3); - expect(line.points).toMatchSnapshot(); + expect(line.points).toMatchInlineSnapshot(` + Array [ + Array [ + 0, + 0, + ], + Array [ + 70, + 50, + ], + Array [ + 40, + 0, + ], + ] + `); }); describe("Inside editor", () => { it("should allow dragging line from midpoint in 2 pointer lines", async () => { createTwoPointerLinearElement("line"); + const line = h.elements[0] as ExcalidrawLinearElement; - - fireEvent.click(canvas, { clientX: p1[0], clientY: p1[1] }); - - Keyboard.keyPress(KEYS.ENTER); - expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); + enterLineEditingMode(line); // drag line from midpoint - dragLinearElementFromPoint(midpoint); + drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]); + expect(renderScene).toHaveBeenCalledTimes(13); + expect(line.points.length).toEqual(3); - expect(line.points).toMatchSnapshot(); + expect(line.points).toMatchInlineSnapshot(` + Array [ + Array [ + 0, + 0, + ], + Array [ + 70, + 50, + ], + Array [ + 40, + 0, + ], + ] + `); }); - it("should allow dragging lines from midpoints in between segments", async () => { + it("should update the midpoints when element sharpness changed", async () => { createThreePointerLinearElement("line"); const line = h.elements[0] as ExcalidrawLinearElement; expect(line.points.length).toEqual(3); - fireEvent.click(canvas, { clientX: p1[0], clientY: p1[1] }); - Keyboard.keyPress(KEYS.ENTER); - expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); + enterLineEditingMode(line); - let points = LinearElementEditor.getPointsGlobalCoordinates(line); - const firstSegmentMidpoint = centerPoint(points[0], points[1]); - // drag line via first segment midpoint - dragLinearElementFromPoint(firstSegmentMidpoint); - expect(line.points.length).toEqual(4); + const midPointsWithSharpEdge = LinearElementEditor.getEditorMidPoints( + line, + h.state, + ); - // drag line from last segment midpoint - points = LinearElementEditor.getPointsGlobalCoordinates(line); - const lastSegmentMidpoint = centerPoint(points.at(-2)!, points.at(-1)!); - dragLinearElementFromPoint(lastSegmentMidpoint); - expect(line.points.length).toEqual(5); + // update sharpness + fireEvent.click(screen.getByTitle("Round")); - expect( - (h.elements[0] as ExcalidrawLinearElement).points, - ).toMatchSnapshot(); + expect(renderScene).toHaveBeenCalledTimes(11); + const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints( + h.elements[0] as ExcalidrawLinearElement, + h.state, + ); + expect(midPointsWithRoundEdge[0]).not.toEqual(midPointsWithSharpEdge[0]); + expect(midPointsWithRoundEdge[1]).not.toEqual(midPointsWithSharpEdge[1]); + + expect(midPointsWithRoundEdge).toMatchInlineSnapshot(` + Array [ + Array [ + 55.9697848965255, + 47.442326230998205, + ], + Array [ + 76.08587175006699, + 43.294165939653226, + ], + ] + `); + }); + + it("should update all the midpoints when element position changed", async () => { + createThreePointerLinearElement("line", "round"); + + const line = h.elements[0] as ExcalidrawLinearElement; + expect(line.points.length).toEqual(3); + enterLineEditingMode(line); + + const points = LinearElementEditor.getPointsGlobalCoordinates(line); + expect([line.x, line.y]).toEqual(points[0]); + + const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + + const startPoint = centerPoint(points[0], midPoints[0] as Point); + const deltaX = 50; + const deltaY = 20; + const endPoint: Point = [startPoint[0] + deltaX, startPoint[1] + deltaY]; + + // Move the element + drag(startPoint, endPoint); + + expect(renderScene).toHaveBeenCalledTimes(14); + expect([line.x, line.y]).toEqual([ + points[0][0] + deltaX, + points[0][1] + deltaY, + ]); + + const newMidPoints = LinearElementEditor.getEditorMidPoints( + line, + h.state, + ); + expect(midPoints[0]).not.toEqual(newMidPoints[0]); + expect(midPoints[1]).not.toEqual(newMidPoints[1]); + expect(newMidPoints).toMatchInlineSnapshot(` + Array [ + Array [ + 105.96978489652551, + 67.4423262309982, + ], + Array [ + 126.08587175006699, + 63.294165939653226, + ], + ] + `); + }); + + describe("When edges are sharp", () => { + // This is the expected midpoint for line with sharp edge + // hence hardcoding it so if later some bug is introduced + // this will fail and we can fix it + const firstSegmentMidpoint: Point = [55, 45]; + const lastSegmentMidpoint: Point = [75, 40]; + + let line: ExcalidrawLinearElement; + + beforeEach(() => { + createThreePointerLinearElement("line"); + line = h.elements[0] as ExcalidrawLinearElement; + expect(line.points.length).toEqual(3); + + enterLineEditingMode(line); + }); + + it("should allow dragging lines from midpoints in between segments", async () => { + // drag line via first segment midpoint + drag(firstSegmentMidpoint, [ + firstSegmentMidpoint[0] + delta, + firstSegmentMidpoint[1] + delta, + ]); + expect(line.points.length).toEqual(4); + + // drag line from last segment midpoint + drag(lastSegmentMidpoint, [ + lastSegmentMidpoint[0] + delta, + lastSegmentMidpoint[1] + delta, + ]); + + expect(renderScene).toHaveBeenCalledTimes(18); + expect(line.points.length).toEqual(5); + + expect((h.elements[0] as ExcalidrawLinearElement).points) + .toMatchInlineSnapshot(` + Array [ + Array [ + 0, + 0, + ], + Array [ + 85, + 75, + ], + Array [ + 70, + 50, + ], + Array [ + 105, + 75, + ], + Array [ + 40, + 0, + ], + ] + `); + }); + + it("should update only the first segment midpoint when its point is dragged", async () => { + const points = LinearElementEditor.getPointsGlobalCoordinates(line); + const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + + const hitCoords: Point = [points[0][0], points[0][1]]; + + // Drag from first point + drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]); + + expect(renderScene).toHaveBeenCalledTimes(14); + + const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line); + expect([newPoints[0][0], newPoints[0][1]]).toEqual([ + points[0][0] - delta, + points[0][1] - delta, + ]); + + const newMidPoints = LinearElementEditor.getEditorMidPoints( + line, + h.state, + ); + + expect(midPoints[0]).not.toEqual(newMidPoints[0]); + expect(midPoints[1]).toEqual(newMidPoints[1]); + }); + + it("should hide midpoints in the segment when points moved close", async () => { + const points = LinearElementEditor.getPointsGlobalCoordinates(line); + const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + + const hitCoords: Point = [points[0][0], points[0][1]]; + + // Drag from first point + drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]); + + expect(renderScene).toHaveBeenCalledTimes(14); + + const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line); + expect([newPoints[0][0], newPoints[0][1]]).toEqual([ + points[0][0] + delta, + points[0][1] + delta, + ]); + + const newMidPoints = LinearElementEditor.getEditorMidPoints( + line, + h.state, + ); + // This midpoint is hidden since the points are too close + expect(newMidPoints[0]).toBeNull(); + expect(midPoints[1]).toEqual(newMidPoints[1]); + }); + + it("should remove the midpoint when one of the points in the segment is deleted", async () => { + const line = h.elements[0] as ExcalidrawLinearElement; + enterLineEditingMode(line); + const points = LinearElementEditor.getPointsGlobalCoordinates(line); + + // dragging line from last segment midpoint + drag(lastSegmentMidpoint, [ + lastSegmentMidpoint[0] + 50, + lastSegmentMidpoint[1] + 50, + ]); + expect(line.points.length).toEqual(4); + + const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + + // delete 3rd point + deletePoint(points[2]); + expect(line.points.length).toEqual(3); + expect(renderScene).toHaveBeenCalledTimes(19); + + const newMidPoints = LinearElementEditor.getEditorMidPoints( + line, + h.state, + ); + expect(newMidPoints.length).toEqual(2); + expect(midPoints[0]).toEqual(newMidPoints[0]); + expect(midPoints[1]).toEqual(newMidPoints[1]); + }); + }); + + describe("When edges are round", () => { + // This is the expected midpoint for line with round edge + // hence hardcoding it so if later some bug is introduced + // this will fail and we can fix it + const firstSegmentMidpoint: Point = [ + 55.9697848965255, 47.442326230998205, + ]; + const lastSegmentMidpoint: Point = [ + 76.08587175006699, 43.294165939653226, + ]; + let line: ExcalidrawLinearElement; + + beforeEach(() => { + createThreePointerLinearElement("line", "round"); + line = h.elements[0] as ExcalidrawLinearElement; + expect(line.points.length).toEqual(3); + + enterLineEditingMode(line); + }); + + it("should allow dragging lines from midpoints in between segments", async () => { + // drag line from first segment midpoint + drag(firstSegmentMidpoint, [ + firstSegmentMidpoint[0] + delta, + firstSegmentMidpoint[1] + delta, + ]); + expect(line.points.length).toEqual(4); + + // drag line from last segment midpoint + drag(lastSegmentMidpoint, [ + lastSegmentMidpoint[0] + delta, + lastSegmentMidpoint[1] + delta, + ]); + expect(renderScene).toHaveBeenCalledTimes(18); + + expect(line.points.length).toEqual(5); + + expect((h.elements[0] as ExcalidrawLinearElement).points) + .toMatchInlineSnapshot(` + Array [ + Array [ + 0, + 0, + ], + Array [ + 85.96978489652551, + 77.4423262309982, + ], + Array [ + 70, + 50, + ], + Array [ + 104.58050066266131, + 74.24758482724201, + ], + Array [ + 40, + 0, + ], + ] + `); + }); + + it("should update all the midpoints when its point is dragged", async () => { + const points = LinearElementEditor.getPointsGlobalCoordinates(line); + const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + + const hitCoords: Point = [points[0][0], points[0][1]]; + + // Drag from first point + drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]); + + const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line); + expect([newPoints[0][0], newPoints[0][1]]).toEqual([ + points[0][0] - delta, + points[0][1] - delta, + ]); + + const newMidPoints = LinearElementEditor.getEditorMidPoints( + line, + h.state, + ); + + expect(midPoints[0]).not.toEqual(newMidPoints[0]); + expect(midPoints[1]).not.toEqual(newMidPoints[1]); + expect(newMidPoints).toMatchInlineSnapshot(` + Array [ + Array [ + 31.884084517616053, + 23.13275505472383, + ], + Array [ + 77.74792546875662, + 44.57840982272327, + ], + ] + `); + }); + + it("should hide midpoints in the segment when points moved close", async () => { + const points = LinearElementEditor.getPointsGlobalCoordinates(line); + const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + + const hitCoords: Point = [points[0][0], points[0][1]]; + + // Drag from first point + drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]); + + expect(renderScene).toHaveBeenCalledTimes(14); + + const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line); + expect([newPoints[0][0], newPoints[0][1]]).toEqual([ + points[0][0] + delta, + points[0][1] + delta, + ]); + + const newMidPoints = LinearElementEditor.getEditorMidPoints( + line, + h.state, + ); + // This mid point is hidden due to point being too close + expect(newMidPoints[0]).toBeNull(); + expect(newMidPoints[1]).not.toEqual(midPoints[1]); + }); + + it("should update all the midpoints when a point is deleted", async () => { + drag(lastSegmentMidpoint, [ + lastSegmentMidpoint[0] + delta, + lastSegmentMidpoint[1] + delta, + ]); + expect(line.points.length).toEqual(4); + + const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + const points = LinearElementEditor.getPointsGlobalCoordinates(line); + + // delete 3rd point + deletePoint(points[2]); + expect(line.points.length).toEqual(3); + + const newMidPoints = LinearElementEditor.getEditorMidPoints( + line, + h.state, + ); + expect(newMidPoints.length).toEqual(2); + expect(midPoints[0]).not.toEqual(newMidPoints[0]); + expect(midPoints[1]).not.toEqual(newMidPoints[1]); + expect(newMidPoints).toMatchInlineSnapshot(` + Array [ + Array [ + 55.9697848965255, + 47.442326230998205, + ], + Array [ + 76.08587175006699, + 43.294165939653226, + ], + ] + `); + }); }); }); }); From ec4b3d913e27537f9ad39bfcd2785729f5bc8432 Mon Sep 17 00:00:00 2001 From: Seunghyun oh Date: Fri, 16 Sep 2022 04:58:07 +0900 Subject: [PATCH 019/276] fix: remove no longer used code related to collab room loading (#5699) Co-authored-by: dwelle --- src/excalidraw-app/app_constants.ts | 1 - src/excalidraw-app/collab/Collab.tsx | 13 ------------- 2 files changed, 14 deletions(-) diff --git a/src/excalidraw-app/app_constants.ts b/src/excalidraw-app/app_constants.ts index cbead7683..e56f7b24f 100644 --- a/src/excalidraw-app/app_constants.ts +++ b/src/excalidraw-app/app_constants.ts @@ -33,7 +33,6 @@ export const STORAGE_KEYS = { LOCAL_STORAGE_ELEMENTS: "excalidraw", LOCAL_STORAGE_APP_STATE: "excalidraw-state", LOCAL_STORAGE_COLLAB: "excalidraw-collab", - LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG: "collabLinkForceLoadFlag", LOCAL_STORAGE_LIBRARY: "excalidraw-library", VERSION_DATA_STATE: "version-dataState", VERSION_FILES: "version-files", diff --git a/src/excalidraw-app/collab/Collab.tsx b/src/excalidraw-app/collab/Collab.tsx index 7112671c4..363de9498 100644 --- a/src/excalidraw-app/collab/Collab.tsx +++ b/src/excalidraw-app/collab/Collab.tsx @@ -25,7 +25,6 @@ import { INITIAL_SCENE_UPDATE_TIMEOUT, LOAD_IMAGES_TIMEOUT, WS_SCENE_EVENT_TYPES, - STORAGE_KEYS, SYNC_FULL_SCENE_INTERVAL_MS, } from "../app_constants"; import { @@ -225,18 +224,6 @@ class Collab extends PureComponent { preventUnload(event); } - - if (this.isCollaborating || this.portal.roomId) { - try { - localStorage?.setItem( - STORAGE_KEYS.LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG, - JSON.stringify({ - timestamp: Date.now(), - room: this.portal.roomId, - }), - ); - } catch {} - } }); saveCollabRoomToFirebase = async ( From 7eaf47c9d41a33a6230d8c3a16b5087fc720dcfb Mon Sep 17 00:00:00 2001 From: Abdullah Adeel <64294045+mabdullahadeel@users.noreply.github.com> Date: Fri, 16 Sep 2022 18:59:03 +0500 Subject: [PATCH 020/276] =?UTF-8?q?fix:=20default=20light=20theme=20splash?= =?UTF-8?q?=20=F0=9F=94=A7=20(#5660)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dwelle Co-authored-by: Aakansha Doshi --- src/actions/manager.tsx | 1 - src/components/App.tsx | 15 ++++----- .../BackgroundPickerAndDarkModeToggle.tsx | 10 +----- src/components/InitializeApp.tsx | 4 ++- src/components/LayerUI.tsx | 10 +----- src/components/LoadingMessage.tsx | 14 ++++++-- src/components/MobileMenu.tsx | 11 +------ src/constants.ts | 2 +- src/css/app.scss | 7 ++++ src/excalidraw-app/app_constants.ts | 1 + src/excalidraw-app/index.tsx | 18 ++++++++++ src/packages/excalidraw/CHANGELOG.md | 2 ++ src/packages/excalidraw/README.md | 6 ++-- src/packages/excalidraw/index.tsx | 9 ++++- src/tests/packages/excalidraw.test.tsx | 33 +++++++++++++++---- src/types.ts | 6 +++- 16 files changed, 97 insertions(+), 52 deletions(-) diff --git a/src/actions/manager.tsx b/src/actions/manager.tsx index 3e3533732..e2655e506 100644 --- a/src/actions/manager.tsx +++ b/src/actions/manager.tsx @@ -137,7 +137,6 @@ export class ActionManager { */ renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => { const canvasActions = this.app.props.UIOptions.canvasActions; - if ( this.actions[name] && "PanelComponent" in this.actions[name] && diff --git a/src/components/App.tsx b/src/components/App.tsx index 0ecbe27e4..2e8d8cb3d 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -552,10 +552,6 @@ class App extends React.Component { typeof this.props?.zenModeEnabled === "undefined" && this.state.zenModeEnabled } - showThemeBtn={ - typeof this.props?.theme === "undefined" && - this.props.UIOptions.canvasActions.theme - } libraryReturnUrl={this.props.libraryReturnUrl} UIOptions={this.props.UIOptions} focusContainer={this.focusContainer} @@ -645,7 +641,8 @@ class App extends React.Component { let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false; let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false; let gridSize = actionResult?.appState?.gridSize || null; - let theme = actionResult?.appState?.theme || THEME.LIGHT; + const theme = + actionResult?.appState?.theme || this.props.theme || THEME.LIGHT; let name = actionResult?.appState?.name ?? this.state.name; if (typeof this.props.viewModeEnabled !== "undefined") { viewModeEnabled = this.props.viewModeEnabled; @@ -659,10 +656,6 @@ class App extends React.Component { gridSize = this.props.gridModeEnabled ? GRID_SIZE : null; } - if (typeof this.props.theme !== "undefined") { - theme = this.props.theme; - } - if (typeof this.props.name !== "undefined") { name = this.props.name; } @@ -755,6 +748,9 @@ class App extends React.Component { ); } + if (this.props.theme) { + this.setState({ theme: this.props.theme }); + } if (!this.state.isLoading) { this.setState({ isLoading: true }); } @@ -784,6 +780,7 @@ class App extends React.Component { const scene = restore(initialData, null, null); scene.appState = { ...scene.appState, + theme: this.props.theme || scene.appState.theme, // we're falling back to current (pre-init) state when deciding // whether to open the library, to handle a case where we // update the state outside of initialData (e.g. when loading the app diff --git a/src/components/BackgroundPickerAndDarkModeToggle.tsx b/src/components/BackgroundPickerAndDarkModeToggle.tsx index 07c1c060e..603a99a92 100644 --- a/src/components/BackgroundPickerAndDarkModeToggle.tsx +++ b/src/components/BackgroundPickerAndDarkModeToggle.tsx @@ -1,20 +1,12 @@ -import React from "react"; import { ActionManager } from "../actions/manager"; -import { AppState } from "../types"; export const BackgroundPickerAndDarkModeToggle = ({ - appState, - setAppState, actionManager, - showThemeBtn, }: { actionManager: ActionManager; - appState: AppState; - setAppState: React.Component["setState"]; - showThemeBtn: boolean; }) => (
{actionManager.renderAction("changeViewBackgroundColor")} - {showThemeBtn && actionManager.renderAction("toggleTheme")} + {actionManager.renderAction("toggleTheme")}
); diff --git a/src/components/InitializeApp.tsx b/src/components/InitializeApp.tsx index c1941512a..af4961fa1 100644 --- a/src/components/InitializeApp.tsx +++ b/src/components/InitializeApp.tsx @@ -2,10 +2,12 @@ import React, { useEffect, useState } from "react"; import { LoadingMessage } from "./LoadingMessage"; import { defaultLang, Language, languages, setLanguage } from "../i18n"; +import { Theme } from "../element/types"; interface Props { langCode: Language["code"]; children: React.ReactElement; + theme?: Theme; } export const InitializeApp = (props: Props) => { @@ -21,5 +23,5 @@ export const InitializeApp = (props: Props) => { updateLang(); }, [props.langCode]); - return loading ? : props.children; + return loading ? : props.children; }; diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index d52c68d44..691558217 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -53,7 +53,6 @@ interface LayerUIProps { onPenModeToggle: () => void; onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void; showExitZenModeBtn: boolean; - showThemeBtn: boolean; langCode: Language["code"]; isCollaborating: boolean; renderTopRightUI?: ExcalidrawProps["renderTopRightUI"]; @@ -78,7 +77,6 @@ const LayerUI = ({ onPenModeToggle, onInsertElements, showExitZenModeBtn, - showThemeBtn, isCollaborating, renderTopRightUI, renderCustomFooter, @@ -209,12 +207,7 @@ const LayerUI = ({ /> )} - + {appState.fileHandle && ( <>{actionManager.renderAction("saveToActiveFile")} )} @@ -424,7 +417,6 @@ const LayerUI = ({ canvas={canvas} isCollaborating={isCollaborating} renderCustomFooter={renderCustomFooter} - showThemeBtn={showThemeBtn} onImageAction={onImageAction} renderTopRightUI={renderTopRightUI} renderCustomStats={renderCustomStats} diff --git a/src/components/LoadingMessage.tsx b/src/components/LoadingMessage.tsx index 83fe80400..124396151 100644 --- a/src/components/LoadingMessage.tsx +++ b/src/components/LoadingMessage.tsx @@ -1,8 +1,14 @@ import { t } from "../i18n"; import { useState, useEffect } from "react"; import Spinner from "./Spinner"; +import clsx from "clsx"; +import { THEME } from "../constants"; +import { Theme } from "../element/types"; -export const LoadingMessage: React.FC<{ delay?: number }> = ({ delay }) => { +export const LoadingMessage: React.FC<{ delay?: number; theme?: Theme }> = ({ + delay, + theme, +}) => { const [isWaiting, setIsWaiting] = useState(!!delay); useEffect(() => { @@ -20,7 +26,11 @@ export const LoadingMessage: React.FC<{ delay?: number }> = ({ delay }) => { } return ( -
+
diff --git a/src/components/MobileMenu.tsx b/src/components/MobileMenu.tsx index a20547546..1e1fdb575 100644 --- a/src/components/MobileMenu.tsx +++ b/src/components/MobileMenu.tsx @@ -38,7 +38,6 @@ type MobileMenuProps = { isMobile: boolean, appState: AppState, ) => JSX.Element | null; - showThemeBtn: boolean; onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void; renderTopRightUI?: ( isMobile: boolean, @@ -61,7 +60,6 @@ export const MobileMenu = ({ canvas, isCollaborating, renderCustomFooter, - showThemeBtn, onImageAction, renderTopRightUI, renderCustomStats, @@ -171,14 +169,7 @@ export const MobileMenu = ({ onClick={onCollabButtonClick} /> )} - { - - } + {} ); }; diff --git a/src/constants.ts b/src/constants.ts index b1638f39f..57496c4db 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -149,7 +149,7 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = { export: { saveFileToDisk: true }, loadScene: true, saveToActiveFile: true, - theme: true, + toggleTheme: null, saveAsImage: true, }, }; diff --git a/src/css/app.scss b/src/css/app.scss index a8c93074c..9d7fc8195 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -1,3 +1,5 @@ +@import "open-color/open-color.scss"; + .visually-hidden { position: absolute !important; height: 1px; @@ -30,3 +32,8 @@ font-size: 0.8em; } } + +.LoadingMessage--dark { + background-color: $oc-black; + color: $oc-white; +} diff --git a/src/excalidraw-app/app_constants.ts b/src/excalidraw-app/app_constants.ts index e56f7b24f..55047ddf0 100644 --- a/src/excalidraw-app/app_constants.ts +++ b/src/excalidraw-app/app_constants.ts @@ -34,6 +34,7 @@ export const STORAGE_KEYS = { LOCAL_STORAGE_APP_STATE: "excalidraw-state", LOCAL_STORAGE_COLLAB: "excalidraw-collab", LOCAL_STORAGE_LIBRARY: "excalidraw-library", + LOCAL_STORAGE_THEME: "excalidraw-theme", VERSION_DATA_STATE: "version-dataState", VERSION_FILES: "version-files", } as const; diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index ebe6ed8eb..25f758f30 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -9,6 +9,7 @@ import { APP_NAME, COOKIES, EVENT, + THEME, TITLE_TIMEOUT, VERSION_TIMEOUT, } from "../constants"; @@ -17,6 +18,7 @@ import { ExcalidrawElement, FileId, NonDeletedExcalidrawElement, + Theme, } from "../element/types"; import { useCallbackRefState } from "../hooks/useCallbackRefState"; import { t } from "../i18n"; @@ -512,6 +514,18 @@ const ExcalidrawWrapper = () => { languageDetector.cacheUserLanguage(langCode); }, [langCode]); + const [theme, setTheme] = useState( + () => + localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_THEME) || + // FIXME migration from old LS scheme. Can be removed later. #5660 + importFromLocalStorage().appState?.theme || + THEME.LIGHT, + ); + + useEffect(() => { + localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_THEME, theme); + }, [theme]); + const onChange = ( elements: readonly ExcalidrawElement[], appState: AppState, @@ -521,6 +535,8 @@ const ExcalidrawWrapper = () => { collabAPI.syncElements(elements); } + setTheme(appState.theme); + // this check is redundant, but since this is a hot path, it's best // not to evaludate the nested expression every time if (!LocalData.isSavePaused()) { @@ -710,6 +726,7 @@ const ExcalidrawWrapper = () => { onPointerUpdate={collabAPI?.onPointerUpdate} UIOptions={{ canvasActions: { + toggleTheme: true, export: { onExportToBackend, renderCustomUI: (elements, appState, files) => { @@ -739,6 +756,7 @@ const ExcalidrawWrapper = () => { handleKeyboardGlobally={true} onLibraryChange={onLibraryChange} autoFocus={true} + theme={theme} /> {excalidrawAPI && } {errorMessage && ( diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index d239d25fa..4f886ba53 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -17,11 +17,13 @@ Please add the latest change on the top under the correct section. #### Features +- Support [theme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#theme) to be semi-controlled [#5660](https://github.com/excalidraw/excalidraw/pull/5660). - Added support for storing [`customData`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#storing-custom-data-to-excalidraw-elements) on Excalidraw elements [#5592]. - Added `exportPadding?: number;` to [exportToCanvas](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exporttocanvas) and [exportToBlob](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exporttoblob). The default value of the padding is 10. #### Breaking Changes +- `props.UIOptions.canvasActions.theme` is now renamed to `props.UIOptions.canvasActions.toggleTheme` [#5660](https://github.com/excalidraw/excalidraw/pull/5660). - `setToastMessage` API is now renamed to `setToast` API and the function signature is also updated [#5427](https://github.com/excalidraw/excalidraw/pull/5427). You can also pass `duration` and `closable` attributes along with `message`. ## 0.12.0 (2022-07-07) diff --git a/src/packages/excalidraw/README.md b/src/packages/excalidraw/README.md index 1d09fb5b0..5a1882242 100644 --- a/src/packages/excalidraw/README.md +++ b/src/packages/excalidraw/README.md @@ -637,7 +637,9 @@ If supplied, this URL will be used when user tries to install a library from [li #### `theme` -This prop controls Excalidraw's theme. When supplied, the value takes precedence over `intialData.appState.theme`, the theme will be fully controlled by the host app, and users won't be able to toggle it from within the app. You can use [`THEME`](#THEME-1) to specify the theme. +This prop controls Excalidraw's theme. When supplied, the value takes precedence over `intialData.appState.theme`, the theme will be fully controlled by the host app, and users won't be able to toggle it from within the app unless `UIOptions.canvasActions.toggleTheme` is set to `true`, in which case the `theme` prop will control Excalidraw's default theme with ability to allow theme switching (you must take care of updating the `theme` prop when you detect a change to `appState.theme` from the [onChange](#onChange) callback). + +You can use [`THEME`](#THEME-1) to specify the theme. #### `name` @@ -660,7 +662,7 @@ This prop can be used to customise UI of Excalidraw. Currently we support custom | `export` | false | [exportOpts](#exportOpts) |
{ saveFileToDisk: true }
| This prop allows to customize the UI inside the export dialog. By default it shows the "saveFileToDisk". If this prop is `false` the export button will not be rendered. For more details visit [`exportOpts`](#exportOpts). | | `loadScene` | boolean | true | Implies whether to show `Load button` | | `saveToActiveFile` | boolean | true | Implies whether to show `Save button` to save to current file | -| `theme` | boolean | true | Implies whether to show `Theme toggle` | +| `toggleTheme` | boolean | null | null | Implies whether to show `Theme toggle`. When defined as `boolean`, takes precedence over [`props.theme`](#theme) to show `Theme toggle` | | `saveAsImage` | boolean | true | Implies whether to show `Save as image button` | ##### `dockedSidebarBreakpoint` diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index c7b6f37ba..3a978b44f 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -56,6 +56,13 @@ const ExcalidrawBase = (props: ExcalidrawProps) => { DEFAULT_UI_OPTIONS.canvasActions.export.saveFileToDisk; } + if ( + UIOptions.canvasActions.toggleTheme === null && + typeof theme === "undefined" + ) { + UIOptions.canvasActions.toggleTheme = true; + } + useEffect(() => { // Block pinch-zooming on iOS outside of the content area const handleTouchMove = (event: TouchEvent) => { @@ -75,7 +82,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => { }, []); return ( - + jotaiStore} scope={jotaiScope}> ", () => { }); describe("Test theme prop", () => { - it('should show the dark mode toggle when the theme prop is "undefined"', async () => { + it("should show the theme toggle by default", async () => { const { container } = await render(); expect(h.state.theme).toBe(THEME.LIGHT); - const darkModeToggle = queryByTestId(container, "toggle-dark-mode"); - expect(darkModeToggle).toBeTruthy(); }); - it('should not show the dark mode toggle when the theme prop is not "undefined"', async () => { + it("should not show theme toggle when the theme prop is defined", async () => { const { container } = await render(); expect(h.state.theme).toBe(THEME.DARK); - expect(queryByTestId(container, "toggle-dark-mode")).toBe(null); }); + + it("should show theme mode toggle when `UIOptions.canvasActions.toggleTheme` is true", async () => { + const { container } = await render( + , + ); + expect(h.state.theme).toBe(THEME.DARK); + const darkModeToggle = queryByTestId(container, "toggle-dark-mode"); + expect(darkModeToggle).toBeTruthy(); + }); + + it("should not show theme toggle when `UIOptions.canvasActions.toggleTheme` is false", async () => { + const { container } = await render( + , + ); + expect(h.state.theme).toBe(THEME.DARK); + const darkModeToggle = queryByTestId(container, "toggle-dark-mode"); + expect(darkModeToggle).toBeFalsy(); + }); }); describe("Test name prop", () => { @@ -214,7 +235,7 @@ describe("", () => { it("should hide the theme toggle when theme is false", async () => { const { container } = await render( - , + , ); expect(queryByTestId(container, "toggle-dark-mode")).toBeNull(); diff --git a/src/types.ts b/src/types.ts index db6647f17..0cfed3af6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -344,13 +344,17 @@ export type ExportOpts = { ) => JSX.Element; }; +// NOTE at the moment, if action name coressponds to canvasAction prop, its +// truthiness value will determine whether the action is rendered or not +// (see manager renderAction). We also override canvasAction values in +// excalidraw package index.tsx. type CanvasActions = { changeViewBackgroundColor?: boolean; clearCanvas?: boolean; export?: false | ExportOpts; loadScene?: boolean; saveToActiveFile?: boolean; - theme?: boolean; + toggleTheme?: boolean | null; saveAsImage?: boolean; }; From 9cccac1458c8ff6b79452305cbbed7032522f18b Mon Sep 17 00:00:00 2001 From: David Luzar Date: Fri, 16 Sep 2022 17:12:24 +0200 Subject: [PATCH 021/276] feat: further reduce darkmode init flash (#5701) * feat: further reduce darkmode init flash * fix lint * tweak doc * colocate code --- public/index.html | 23 +++++++++++++++++++++-- src/css/app.scss | 4 ++-- src/excalidraw-app/index.tsx | 3 +++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/public/index.html b/public/index.html index 0c576a292..4a4602b70 100644 --- a/public/index.html +++ b/public/index.html @@ -52,6 +52,25 @@ content="Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them." /> + + + + + + - <% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %> + <% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true' && + process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %> +``` + +For production use :point_down: + +```js + +``` + +You will need to make sure `react`, `react-dom` is available as shown in the below example. For prod please use the production versions of `react`, `react-dom`. + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + + + + +```html + + + + Excalidraw in browser + + + + + + + + +
+

Excalidraw Embed Example

+
+
+ + + +``` + +
+ + +```js showLineNumbers +const App = () => { + return React.createElement( + React.Fragment, + null, + React.createElement( + "div", + { + style: { height: "500px" }, + }, + React.createElement(ExcalidrawLib.Excalidraw), + ), + ); +}; + +const excalidrawWrapper = document.getElementById("app"); +const root = ReactDOM.createRoot(excalidrawWrapper); +root.render(React.createElement(App)); +``` + + +
diff --git a/dev-docs/docs/assets/nerd-stats.png b/dev-docs/docs/assets/nerd-stats.png new file mode 100644 index 0000000000000000000000000000000000000000..0c1a525e4ed716a50b591109f8ce384bd4822a0a GIT binary patch literal 84676 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�E!BPC|Oq0!-R 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

7ZCAP#fm2nuJ_*I@8ZK`A3er6 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 To keep `master` branch pointing to remote repository and make pull requests from branches on your fork. To do this, run: +> +> ```bash +> git remote add upstream https://github.com/excalidraw/excalidraw.git +> git fetch upstream +> git branch --set-upstream-to=upstream/master master +> ``` + +### Option 2 - CodeSandbox + +1. Go to https://codesandbox.io/s/github/excalidraw/excalidraw +1. Connect your GitHub account +1. Go to Git tab on left side +1. Tap on `Fork Sandbox` +1. Write your code +1. Commit and PR automatically + +## Pull Request Guidelines + +Don't worry if you get any of the below wrong, or if you don't know how. We'll gladly help out. + +### Title + +Make sure the title starts with a semantic prefix: + +- **feat**: A new feature +- **fix**: A bug fix +- **docs**: Documentation only changes +- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) +- **refactor**: A code change that neither fixes a bug nor adds a feature +- **perf**: A code change that improves performance +- **test**: Adding missing tests or correcting existing tests +- **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) +- **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) +- **chore**: Other changes that don't modify src or test files +- **revert**: Reverts a previous commit + +### Changelog + +Add a brief description of your pull request to the changelog located here: [changelog](https://github.com/excalidraw/excalidraw/blob/master/CHANGELOG.md) + +Notes: + +- Make sure to prepend to the section corresponding with the semantic prefix you selected in the title +- Link to your pull request - this will require updating the CHANGELOG _after_ creating the pull request + +### Testing + +Once you submit your pull request it will automatically be tested. Be sure to check the results of the test and fix any issues that arise. + +It's also a good idea to consider if your change should include additional tests. This is highly recommended for new features or bug-fixes. For example, it's good practice to create a test for each bug you fix which ensures that we don't regress the code in the future. + +Finally - always manually test your changes using the convenient staging environment deployed for each pull request. As much as local development attempts to replicate production, there can still be subtle differences in behavior. For larger features consider testing your change in multiple browsers as well. + + +## Translating + +To translate Excalidraw into other languages, please visit [our Crowdin page](https://crowdin.com/project/excalidraw). To add a new language, [open an issue](https://github.com/excalidraw/excalidraw/issues/new) so we can get things set up on our end first. + +Translations will be available on the app if they exceed a certain threshold of completion (currently **85%**). diff --git a/dev-docs/docs/introduction/development.mdx b/dev-docs/docs/introduction/development.mdx new file mode 100644 index 000000000..63c1875e5 --- /dev/null +++ b/dev-docs/docs/introduction/development.mdx @@ -0,0 +1,102 @@ +# Development + +## Code Sandbox + +- Go to https://codesandbox.io/p/github/excalidraw/excalidraw + - You may need to sign in with GitHub and reload the page +- You can start coding instantly, and even send PRs from there! + +## Local Installation + +These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. + +### Requirements + +- [Node.js](https://nodejs.org/en/) +- [Yarn](https://yarnpkg.com/getting-started/install) (v1 or v2.4.2+) +- [Git](https://git-scm.com/downloads) + +### Clone the repo + +```bash +git clone https://github.com/excalidraw/excalidraw.git +``` + +### Install the dependencies + +```bash +yarn +``` + +### Start the server + +```bash +yarn start +``` + +Now you can open [http://localhost:3000](http://localhost:3000) and start coding in your favorite code editor. + +## Collaboration + +For collaboration, you will need to set up [collab server](https://github.com/excalidraw/excalidraw-room) in local. + +## Commands + +### Install the dependencies + +```bash +yarn +``` + +### Run the project + +```bash +yarn start +``` + +### Reformat all files with Prettier + +```bash +yarn fix +``` + +### Run tests + +```bash +yarn test +``` + +### Update test snapshots + +```bash +yarn test:update +``` + +### Test for formatting with Prettier + +```bash +yarn test:code +``` + +### Docker Compose + +You can use docker-compose to work on Excalidraw locally if you don't want to setup a Node.js env. + +```bash +docker-compose up --build -d +``` + +## Self-hosting + +We publish a Docker image with the Excalidraw client at [excalidraw/excalidraw](https://hub.docker.com/r/excalidraw/excalidraw). You can use it to self-host your own client under your own domain, on Kubernetes, AWS ECS, etc. + +```bash +docker build -t excalidraw/excalidraw . +docker run --rm -dit --name excalidraw -p 5000:80 excalidraw/excalidraw:latest +``` + +The Docker image is free of analytics and other tracking libraries. + +**At the moment, self-hosting your own instance doesn't support sharing or collaboration features.** + +We are working towards providing a full-fledged solution for self-hosting your own Excalidraw. diff --git a/dev-docs/docs/introduction/get-started.mdx b/dev-docs/docs/introduction/get-started.mdx new file mode 100644 index 000000000..f122c9a77 --- /dev/null +++ b/dev-docs/docs/introduction/get-started.mdx @@ -0,0 +1,16 @@ +--- +title: Introduction +slug: ../ +--- + +## Try now + +Go to [excalidraw.com](https://excalidraw.com) to start sketching. + +## How are these docs structured + +These docs are focused on developers, and structured in the following way: + +- [Introduction](/docs/) — development setup and introduction. +- [@excalidraw/excalidraw](/docs/@excalidraw/excalidraw/installation) — docs for the npm package to help you integrate Excalidraw into your own app. +- Editor — IN PROGRESS. Docs describing the internals of the Excalidraw editor to help in contributing to the codebase. diff --git a/dev-docs/docs/package/overview.md b/dev-docs/docs/package/overview.md deleted file mode 100644 index b411dcf29..000000000 --- a/dev-docs/docs/package/overview.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -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). diff --git a/dev-docs/docusaurus.config.js b/dev-docs/docusaurus.config.js index c1f9952df..1c69e6dcb 100644 --- a/dev-docs/docusaurus.config.js +++ b/dev-docs/docusaurus.config.js @@ -1,15 +1,12 @@ // @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", + url: "https://docs.excalidraw.com", baseUrl: "/", onBrokenLinks: "throw", onBrokenMarkdownLinks: "warn", @@ -36,7 +33,10 @@ const config = { editUrl: "https://github.com/excalidraw/docs/tree/master/", }, theme: { - customCss: require.resolve("./src/css/custom.css"), + customCss: [ + require.resolve("./src/css/custom.scss"), + require.resolve("../src/packages/excalidraw/example/App.scss"), + ], }, }), ], @@ -45,18 +45,20 @@ const config = { themeConfig: /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ ({ + colorMode: { + respectPrefersColorScheme: true, + }, navbar: { - title: "Excalidraw Docs", + title: "Excalidraw", logo: { alt: "Excalidraw Logo", src: "img/logo.svg", }, items: [ { - type: "doc", - docId: "get-started", + to: "/docs", position: "left", - label: "Get started", + label: "Docs", }, { to: "https://blog.excalidraw.com", @@ -78,7 +80,7 @@ const config = { items: [ { label: "Get Started", - to: "/docs/get-started", + to: "/docs", }, ], }, @@ -93,6 +95,10 @@ const config = { label: "Twitter", href: "https://twitter.com/excalidraw", }, + { + label: "Linkedin", + href: "https://www.linkedin.com/company/excalidraw", + }, ], }, { @@ -109,13 +115,23 @@ const config = { ], }, ], - copyright: `Made with ❤️ Built with Docusaurus`, + copyright: `Copyright © 2023 Excalidraw community. Built with Docusaurus ❤️`, }, prism: { - theme: lightCodeTheme, - darkTheme: darkCodeTheme, + theme: require("prism-react-renderer/themes/dracula"), + }, + image: "img/og-image.png", + docs: { + sidebar: { + hideable: true, + }, + }, + tableOfContents: { + maxHeadingLevel: 4, }, }), + themes: ["@docusaurus/theme-live-codeblock"], + plugins: ["docusaurus-plugin-sass"], }; module.exports = config; diff --git a/dev-docs/package.json b/dev-docs/package.json index 9f9d4a760..dd3c45872 100644 --- a/dev-docs/package.json +++ b/dev-docs/package.json @@ -15,13 +15,17 @@ "typecheck": "tsc" }, "dependencies": { - "@docusaurus/core": "2.0.0-rc.1", - "@docusaurus/preset-classic": "2.0.0-rc.1", + "@docusaurus/core": "2.2.0", + "@docusaurus/preset-classic": "2.2.0", + "@docusaurus/theme-live-codeblock": "2.2.0", + "@excalidraw/excalidraw": "0.14.2", "@mdx-js/react": "^1.6.22", "clsx": "^1.2.1", + "docusaurus-plugin-sass": "0.2.3", "prism-react-renderer": "^1.3.5", "react": "^17.0.2", - "react-dom": "^17.0.2" + "react-dom": "^17.0.2", + "sass": "1.57.1" }, "devDependencies": { "@docusaurus/module-type-aliases": "2.0.0-rc.1", diff --git a/dev-docs/sidebars.js b/dev-docs/sidebars.js index 966215066..00879c12f 100644 --- a/dev-docs/sidebars.js +++ b/dev-docs/sidebars.js @@ -13,19 +13,86 @@ /** @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: [ + docs: [ { - type: 'category', - label: 'Tutorial', - items: ['hello'], + type: "category", + label: "Introduction", + link: { + type: "doc", + id: "introduction/get-started", + }, + items: ["introduction/development", "introduction/contributing"], + }, + + { + type: "category", + label: "@excalidraw/excalidraw", + collapsed: false, + items: [ + "@excalidraw/excalidraw/installation", + "@excalidraw/excalidraw/integration", + "@excalidraw/excalidraw/customizing-styles", + { + type: "category", + label: "API", + link: { + type: "doc", + id: "@excalidraw/excalidraw/api/api-intro", + }, + items: [ + { + type: "category", + label: "Props", + link: { + type: "doc", + id: "@excalidraw/excalidraw/api/props/props", + }, + items: [ + "@excalidraw/excalidraw/api/props/initialdata", + "@excalidraw/excalidraw/api/props/ref", + "@excalidraw/excalidraw/api/props/render-props", + "@excalidraw/excalidraw/api/props/ui-options", + ], + }, + { + type: "category", + label: "Children Components", + link: { + type: "doc", + id: "@excalidraw/excalidraw/api/children-components/children-components-intro", + }, + items: [ + "@excalidraw/excalidraw/api/children-components/main-menu", + "@excalidraw/excalidraw/api/children-components/welcome-screen", + "@excalidraw/excalidraw/api/children-components/footer", + "@excalidraw/excalidraw/api/children-components/live-collaboration-trigger", + ], + }, + { + type: "category", + label: "Utils", + link: { + type: "doc", + id: "@excalidraw/excalidraw/api/utils/utils-intro", + }, + items: [ + "@excalidraw/excalidraw/api/utils/export", + "@excalidraw/excalidraw/api/utils/restore", + ], + }, + { + type: "category", + label: "Constants", + link: { type: "doc", id: "@excalidraw/excalidraw/api/constants" }, + items: [], + }, + ], + }, + "@excalidraw/excalidraw/faq", + "@excalidraw/excalidraw/development", + ], }, ], - */ }; module.exports = sidebars; diff --git a/dev-docs/src/components/Highlight.js b/dev-docs/src/components/Highlight.js new file mode 100644 index 000000000..6ef30412b --- /dev/null +++ b/dev-docs/src/components/Highlight.js @@ -0,0 +1,15 @@ +import React from "react"; +export default function Highlight({ children }) { + return ( + + {children} + + ); +} diff --git a/dev-docs/src/css/custom.css b/dev-docs/src/css/custom.scss similarity index 54% rename from dev-docs/src/css/custom.css rename to dev-docs/src/css/custom.scss index bbdc13c56..93c7f90ab 100644 --- a/dev-docs/src/css/custom.css +++ b/dev-docs/src/css/custom.scss @@ -14,11 +14,13 @@ --ifm-color-primary-lighter: #5b57d1; --ifm-color-primary-lightest: #5b57d1; --ifm-code-font-size: 95%; + + scrollbar-gutter: stable; } /* For readability concerns, you should choose a lighter palette in dark mode. */ [data-theme="dark"] { - --ifm-color-primary: #5650f0; + --ifm-color-primary: #8784e3; --ifm-color-primary-dark: #4b46d8; --ifm-color-primary-darker: #4b46d8; --ifm-color-primary-darkest: #3e39be; @@ -41,3 +43,59 @@ [data-theme="dark"] .navbar__logo { filter: invert(93%) hue-rotate(180deg); } + +pre a { + color: #5dccff; + + &:hover { + color: #8fd3f3; + } +} + +.custom-button { + height: 40px; + max-width: 200px; + margin: 10px 0; + padding: 5px; + background: #70b1ec; + color: white; + font-weight: bold; + border: none; +} + +.custom-styles .excalidraw { + --color-primary: #fcc6d9; + --color-primary-darker: #f783ac; + --color-primary-darkest: #e64980; + --color-primary-light: #f2a9c4; +} + +.custom-styles .excalidraw.theme--dark { + --color-primary: #d494aa; + --color-primary-darker: #d64c7e; + --color-primary-darkest: #e86e99; + --color-primary-light: #dcbec9; +} + +/* The global css conflicts with Excal css hence overriding */ + +.excalidraw .context-menu-item__shortcut { + background-color: transparent; + border: none; + box-shadow: none; + padding: 0; +} +.excalidraw .Stats table td, +.excalidraw .Stats table th, +.excalidraw .Stats table tr { + border: none; + background: none; + padding: 0; +} +.excalidraw .Stats .close { + padding: 0; +} + +.excalidraw .Stats table { + display: table; +} diff --git a/dev-docs/src/initialData.js b/dev-docs/src/initialData.js new file mode 100644 index 000000000..d0a9b2127 --- /dev/null +++ b/dev-docs/src/initialData.js @@ -0,0 +1,1230 @@ +export const libraryItems = [ + [ + { + type: "line", + version: 1699, + versionNonce: 1813275999, + isDeleted: false, + id: "1OMHrnYMU3LJ3w3IaXU_R", + fillStyle: "hachure", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: 209.72304760646858, + y: 338.83587294718825, + strokeColor: "#881fa3", + backgroundColor: "#be4bdb", + width: 116.42036295658873, + height: 103.65107323746608, + seed: 1445523839, + groupIds: [], + strokeSharpness: "sharp", + boundElementIds: [], + startBinding: null, + endBinding: null, + points: [ + [-92.28090097254909, 7.105427357601002e-15], + [-154.72281841151394, 19.199290805487394], + [-155.45758928571422, 79.43840749607878], + [-99.89923520113778, 103.6510732374661], + [-40.317783799181804, 79.1587107641305], + [-39.037226329125524, 21.285677238400705], + [-92.28090097254909, 7.105427357601002e-15], + ], + lastCommittedPoint: null, + startArrowhead: null, + endArrowhead: null, + }, + ], + [ + { + type: "line", + version: 3901, + versionNonce: 540959103, + isDeleted: false, + id: "b-rwW8s76ztV_uTu1SHq1", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -249.48446738689245, + y: 374.851387389359, + strokeColor: "#0a11d3", + backgroundColor: "#228be6", + width: 88.21658171083376, + height: 113.8575037534261, + seed: 1513238033, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "round", + boundElementIds: [], + startBinding: null, + endBinding: null, + points: [ + [-0.22814350714115691, -43.414939319563715], + [0.06274947619197979, 42.63794490105306], + [-0.21453039840335475, 52.43469208825097], + [4.315205554872581, 56.66774540453215], + [20.089784992984285, 60.25027917349701], + [46.7532926683984, 61.365826671969444], + [72.22851104292477, 59.584691681394986], + [85.76368213524371, 55.325139565662596], + [87.67263486434864, 51.7342924478499], + [87.94074036468018, 43.84700272879395], + [87.73030872197806, -36.195582644606276], + [87.2559282533682, -43.758132174307036], + [81.5915337527493, -47.984890854524416], + [69.66352776578219, -50.4328058257654], + [42.481213744224995, -52.49167708145666], + [20.68789182864576, -51.26396751574663], + [3.5475921483286084, -47.099726468136254], + [-0.2758413461535838, -43.46664538034193], + [-0.22814350714115691, -43.414939319563715], + ], + lastCommittedPoint: null, + startArrowhead: null, + endArrowhead: null, + }, + { + type: "line", + version: 1635, + versionNonce: 1383184881, + isDeleted: false, + id: "3CMZYj34FwjhgPB7jUC3f", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -249.02524930453623, + y: 398.8804363713438, + strokeColor: "#0a11d3", + backgroundColor: "transparent", + width: 88.30808627974527, + height: 9.797916664247975, + seed: 683951089, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "round", + boundElementIds: [], + startBinding: null, + endBinding: null, + points: [ + [0, -2.1538602707609424], + [2.326538897826852, 1.751753055375216], + [12.359939318521995, 5.028526743934819], + [25.710950037209347, 7.012921076245119], + [46.6269757640547, 7.193749997581346], + [71.03526003420632, 5.930375670950649], + [85.2899738827162, 1.3342483900732343], + [88.30808627974527, -2.6041666666666288], + ], + lastCommittedPoint: null, + startArrowhead: null, + endArrowhead: null, + }, + { + type: "line", + version: 1722, + versionNonce: 303290783, + isDeleted: false, + id: "DX3fUhBWtlJwYyrBDhebG", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -250.11899081659772, + y: 365.80628180927204, + strokeColor: "#0a11d3", + backgroundColor: "transparent", + width: 88.30808627974527, + height: 9.797916664247975, + seed: 1817746897, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "round", + boundElementIds: [], + startBinding: null, + endBinding: null, + points: [ + [0, -2.1538602707609424], + [2.326538897826852, 1.751753055375216], + [12.359939318521995, 5.028526743934819], + [25.710950037209347, 7.012921076245119], + [46.6269757640547, 7.193749997581346], + [71.03526003420632, 5.930375670950649], + [85.2899738827162, 1.3342483900732343], + [88.30808627974527, -2.6041666666666288], + ], + lastCommittedPoint: null, + startArrowhead: null, + endArrowhead: null, + }, + { + type: "ellipse", + version: 4738, + versionNonce: 753357777, + isDeleted: false, + id: "a-Snvp2FgqDYqSLylF44S", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -251.23981350275943, + y: 323.4117518426986, + strokeColor: "#0a11d3", + backgroundColor: "#fff", + width: 87.65074610854188, + height: 17.72670397681366, + seed: 1409727409, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "sharp", + boundElementIds: ["bxuMGTzXLn7H-uBCptINx"], + }, + { + type: "ellipse", + version: 109, + versionNonce: 1992641983, + isDeleted: false, + id: "7-6c-JFuB2yGoNQRgb2WM", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -179.73008120217884, + y: 347.98755471983213, + strokeColor: "#0a11d3", + backgroundColor: "#fff", + width: 12.846057046979809, + height: 13.941904362416096, + seed: 1073094033, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "sharp", + boundElementIds: [], + }, + { + type: "ellipse", + version: 158, + versionNonce: 1028567473, + isDeleted: false, + id: "150XitJtlKDhTPRCyzv56", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -179.73008120217884, + y: 378.5900085788926, + strokeColor: "#0a11d3", + backgroundColor: "#fff", + width: 12.846057046979809, + height: 13.941904362416096, + seed: 526271345, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "sharp", + boundElementIds: [], + }, + { + type: "ellipse", + version: 212, + versionNonce: 158547423, + isDeleted: false, + id: "cmwAR3NBl1VqvSorrQN2W", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -179.73008120217884, + y: 411.8508097533892, + strokeColor: "#0a11d3", + backgroundColor: "#fff", + width: 12.846057046979809, + height: 13.941904362416096, + seed: 243707217, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "sharp", + boundElementIds: [], + }, + ], + [ + { + type: "diamond", + version: 659, + versionNonce: 1294871039, + isDeleted: false, + id: "aDDArXRjZugwyEawdhCeZ", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -109.55894395256101, + y: 381.22641397493356, + strokeColor: "#c92a2a", + backgroundColor: "#fd8888", + width: 112.64736525303451, + height: 36.77344700318558, + seed: 511870335, + groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], + strokeSharpness: "sharp", + boundElementIds: [], + }, + { + type: "diamond", + version: 700, + versionNonce: 60864881, + isDeleted: false, + id: "Hzx8zkeyDs3YicO2Tdv6G", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -109.55894395256101, + y: 372.354634046675, + strokeColor: "#c92a2a", + backgroundColor: "#fd8888", + width: 112.64736525303451, + height: 36.77344700318558, + seed: 1283079231, + groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], + strokeSharpness: "sharp", + boundElementIds: [], + }, + { + type: "diamond", + version: 780, + versionNonce: 251040287, + isDeleted: false, + id: "PNzYhT295VNCT5EXmqvmw", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -109.55894395256101, + y: 359.72407445196296, + strokeColor: "#c92a2a", + backgroundColor: "#fd8888", + width: 112.64736525303451, + height: 36.77344700318558, + seed: 996251633, + groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], + strokeSharpness: "sharp", + boundElementIds: [], + }, + { + type: "diamond", + version: 822, + versionNonce: 1862951761, + isDeleted: false, + id: "jiMMAhQF3__7bF-obgXc0", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -109.55894395256101, + y: 347.1924021546656, + strokeColor: "#c92a2a", + backgroundColor: "#fd8888", + width: 112.64736525303451, + height: 36.77344700318558, + seed: 1764842481, + groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], + strokeSharpness: "sharp", + boundElementIds: [], + }, + ], + [ + { + type: "line", + version: 4766, + versionNonce: 2003030321, + isDeleted: false, + id: "BXfdLRoPYZ9MIumzzoA9-", + fillStyle: "hachure", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 1.5707963267948957, + x: -471.6208001976387, + y: 520.7681448415112, + strokeColor: "#087f5b", + backgroundColor: "#40c057", + width: 52.317507746132115, + height: 154.56722543646003, + seed: 1424381745, + groupIds: ["HSrtfEf-CssQTf160Fb6R"], + strokeSharpness: "round", + boundElementIds: [], + startBinding: null, + endBinding: null, + points: [ + [-0.24755378372925183, -40.169554027464216], + [-0.07503751055611152, 76.6515171914404], + [-0.23948042713317108, 89.95108885873196], + [2.446913573036335, 95.69766931810295], + [11.802146636255692, 100.56113713047068], + [27.615140546177496, 102.07554835500338], + [42.72341054254274, 99.65756899883291], + [50.75054563137204, 93.87501510096598], + [51.88266441510958, 89.00026150397161], + [52.04166639997853, 78.29287333983132], + [51.916868330459295, -30.36891819848148], + [51.635533423123285, -40.63545540065934], + [48.27622163143906, -46.37349057843314], + [41.202227904674494, -49.69665692879073], + [25.081551986374073, -52.49167708145666], + [12.15685839679867, -50.825000270901], + [1.9916746648394732, -45.171835889467935], + [-0.2758413461535838, -40.23974757720194], + [-0.24755378372925183, -40.169554027464216], + ], + lastCommittedPoint: null, + startArrowhead: null, + endArrowhead: null, + }, + { + type: "line", + version: 2405, + versionNonce: 2120341087, + isDeleted: false, + id: "TYsYe2VvJ60T_yKa3kyOw", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 1.5707963267948957, + x: -496.3957643857249, + y: 541.7241190920508, + strokeColor: "#087f5b", + backgroundColor: "transparent", + width: 50.7174766392476, + height: 12.698053371678215, + seed: 726657713, + groupIds: ["HSrtfEf-CssQTf160Fb6R"], + strokeSharpness: "round", + boundElementIds: [], + startBinding: null, + endBinding: null, + points: [ + [0, -2.0205717204386002], + [1.3361877396713384, 3.0410845646550486], + [7.098613049589299, 7.287767671898479], + [14.766422451441104, 9.859533283467512], + [26.779003528407447, 10.093886705011586], + [40.79727342221974, 8.456559589697127], + [48.98410145879092, 2.500000505196364], + [50.7174766392476, -2.6041666666666288], + ], + lastCommittedPoint: null, + startArrowhead: null, + endArrowhead: null, + }, + { + type: "line", + version: 2538, + versionNonce: 1913946897, + isDeleted: false, + id: "VIuxhGjvYUBniitomEkKm", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 1.5707963267948957, + x: -450.969983237283, + y: 542.1789894334747, + strokeColor: "#087f5b", + backgroundColor: "transparent", + width: 50.57247907260371, + height: 10.178760037658167, + seed: 1977326481, + groupIds: ["HSrtfEf-CssQTf160Fb6R"], + strokeSharpness: "round", + boundElementIds: [], + startBinding: null, + endBinding: null, + points: [ + [0, -2.136356936862347], + [1.332367676378171, 1.9210669226078037], + [7.078318632616268, 5.325208253515953], + [14.724206326638113, 7.386735659885842], + [26.70244431044034, 7.574593370991538], + [40.68063699304561, 6.262111896696538], + [48.84405948536458, 1.4873339211608216], + [50.57247907260371, -2.6041666666666288], + ], + lastCommittedPoint: null, + startArrowhead: null, + endArrowhead: null, + }, + { + type: "ellipse", + version: 5503, + versionNonce: 1236644479, + isDeleted: false, + id: "1acGiqpJjntE3sr1JVnBP", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 1.5707963267948957, + x: -404.36521010516793, + y: 534.1894365757241, + strokeColor: "#087f5b", + backgroundColor: "#fff", + width: 51.27812853552538, + height: 22.797152568995934, + seed: 1774660383, + groupIds: ["HSrtfEf-CssQTf160Fb6R"], + strokeSharpness: "sharp", + boundElementIds: ["bxuMGTzXLn7H-uBCptINx"], + }, + ], + [ + { + type: "rectangle", + version: 4270, + versionNonce: 309922463, + isDeleted: false, + id: "SqGRpNqls7OV1QB2Eq-0m", + fillStyle: "solid", + strokeWidth: 2, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -393.3000561423187, + y: 338.9742643666818, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 70.67858069123133, + height: 107.25081879410921, + seed: 371096063, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "sharp", + boundElementIds: [ + "CFu0B4Mw_1wC1Hbgx8Fs0", + "XIl_NhaFtRO00pX5Pq6VU", + "EndiSTFlx1AT7vcBVjgve", + ], + }, + { + type: "rectangle", + version: 4319, + versionNonce: 896119505, + isDeleted: false, + id: "fayss6b_GPh6LK1x4iX-q", + fillStyle: "solid", + strokeWidth: 2, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -400.8474891780329, + y: 331.95417508096745, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 70.67858069123133, + height: 107.25081879410921, + seed: 685932433, + groupIds: ["0RJwA-yKP5dqk5oMiSeot", "9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "sharp", + boundElementIds: [ + "CFu0B4Mw_1wC1Hbgx8Fs0", + "XIl_NhaFtRO00pX5Pq6VU", + "EndiSTFlx1AT7vcBVjgve", + ], + }, + { + type: "rectangle", + version: 4417, + versionNonce: 1968987839, + isDeleted: false, + id: "HgAnv2rwYhUpLiJiZAXv-", + fillStyle: "solid", + strokeWidth: 2, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -410.24257846374826, + y: 323.7002688309677, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 70.67858069123133, + height: 107.25081879410921, + seed: 58634943, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "sharp", + boundElementIds: [ + "CFu0B4Mw_1wC1Hbgx8Fs0", + "XIl_NhaFtRO00pX5Pq6VU", + "EndiSTFlx1AT7vcBVjgve", + ], + }, + { + type: "draw", + version: 3541, + versionNonce: 1680683185, + isDeleted: false, + id: "12aO-Bs9HdALZN_-tuQTr", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -398.2561518768373, + y: 371.84603609547054, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 46.57983585730082, + height: 3.249953844290203, + seed: 1673003743, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "round", + boundElementIds: [], + points: [ + [0, 0.6014697828497827], + [40.42449133807562, 0.7588628355182573], + [46.57983585730082, -2.491091008771946], + ], + lastCommittedPoint: null, + startArrowhead: null, + endArrowhead: null, + }, + { + type: "draw", + version: 3567, + versionNonce: 620768991, + isDeleted: false, + id: "Ck_Y0EVPh_fsY0qoRnGiD", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -396.400899638823, + y: 340.9822185794818, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 45.567415680676426, + height: 2.8032978840147194, + seed: 1821527807, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "round", + boundElementIds: [], + points: [ + [0, 0], + [16.832548902953302, -2.8032978840147194], + [45.567415680676426, -0.3275477042019195], + ], + lastCommittedPoint: null, + startArrowhead: null, + endArrowhead: null, + }, + { + type: "draw", + version: 3592, + versionNonce: 1300624017, + isDeleted: false, + id: "a_7IZapEuD918VW1P8Ss_", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -396.4774991551924, + y: 408.37659284983897, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 48.33668263438425, + height: 4.280657518731036, + seed: 1485707039, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "round", + boundElementIds: [], + points: [ + [0, 0], + [26.41225578429045, -0.2552319773002338], + [37.62000339651456, 2.3153712935189787], + [48.33668263438425, -1.9652862252120569], + ], + lastCommittedPoint: null, + startArrowhead: null, + endArrowhead: null, + }, + { + type: "draw", + version: 3629, + versionNonce: 737475327, + isDeleted: false, + id: "8io6FVNdFOLsQ266W8Lni", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -399.6615463367227, + y: 419.61974125811776, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 54.40694982784246, + height: 2.9096445412231735, + seed: 1042012991, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "round", + boundElementIds: [], + points: [ + [0, 0], + [10.166093050596771, -1.166642430373031], + [16.130660965377448, -0.8422655250909383], + [46.26079588567538, 0.6125567455206506], + [54.40694982784246, -2.297087795702523], + ], + lastCommittedPoint: null, + startArrowhead: null, + endArrowhead: null, + }, + { + type: "draw", + version: 3594, + versionNonce: 1982560369, + isDeleted: false, + id: "LJI5kY6tg7UFAjPV3fKL-", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -399.3767034411569, + y: 356.042820132743, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 46.92865289294453, + height: 2.4757501798128, + seed: 295443295, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "round", + boundElementIds: [], + points: [ + [0, 0], + [18.193786115221407, -0.5912874140789839], + [46.92865289294453, 1.884462765733816], + ], + lastCommittedPoint: null, + startArrowhead: null, + endArrowhead: null, + }, + { + type: "draw", + version: 3609, + versionNonce: 1857766175, + isDeleted: false, + id: "zCrZOHW-q8YWKLw6ltKxX", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -399.26921524500654, + y: 390.5261491685826, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 46.92865289294453, + height: 2.4757501798128, + seed: 1734301567, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "round", + boundElementIds: [], + points: [ + [0, 0], + [8.093938105125233, 1.4279702913643746], + [18.193786115221407, -0.5912874140789839], + [46.92865289294453, 1.884462765733816], + ], + lastCommittedPoint: null, + startArrowhead: null, + endArrowhead: null, + }, + ], + [ + { + type: "rectangle", + version: 676, + versionNonce: 1841530687, + isDeleted: false, + id: "XOD3vRhtoLWoxC9wF9Sk8", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -593.9896997899341, + y: 343.9798351106279, + strokeColor: "#000000", + backgroundColor: "transparent", + width: 127.88383573213892, + height: 76.53703389977764, + seed: 106569279, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "sharp", + boundElementIds: [], + }, + { + type: "line", + version: 462, + versionNonce: 1737150513, + isDeleted: false, + id: "WBkTga1PjKzYK-tcGjnjZ", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -595.0652975408293, + y: 354.6963695028721, + strokeColor: "#000000", + backgroundColor: "transparent", + width: 128.84193229844433, + height: 0, + seed: 73916127, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "round", + boundElementIds: [], + startBinding: null, + endBinding: null, + points: [ + [0, 0], + [128.84193229844433, 0], + ], + lastCommittedPoint: null, + startArrowhead: null, + endArrowhead: null, + }, + { + type: "ellipse", + version: 282, + versionNonce: 1198409567, + isDeleted: false, + id: "FHX0ZsIzUUfYPJqrZ8Lso", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 0, + opacity: 100, + angle: 0, + x: -589.5016643209792, + y: 348.2514049106367, + strokeColor: "#000000", + backgroundColor: "#fa5252", + width: 5.001953125, + height: 5.001953125, + seed: 387857791, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "sharp", + boundElementIds: [], + }, + { + type: "ellipse", + version: 327, + versionNonce: 1661182481, + isDeleted: false, + id: "ugVRR0f_uDOjrllO10yAs", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 0, + opacity: 100, + angle: 0, + x: -579.2389690084792, + y: 348.2514049106367, + strokeColor: "#000000", + backgroundColor: "#fab005", + width: 5.001953125, + height: 5.001953125, + seed: 1486370207, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "sharp", + boundElementIds: [], + }, + { + type: "ellipse", + version: 385, + versionNonce: 2047607679, + isDeleted: false, + id: "SBzNA0Sn-ou4QGxotj0SB", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 0, + opacity: 100, + angle: 0, + x: -568.525552542133, + y: 348.7021260644829, + strokeColor: "#000000", + backgroundColor: "#40c057", + width: 5.001953125, + height: 5.001953125, + seed: 610150847, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "sharp", + boundElementIds: [], + }, + { + type: "ellipse", + version: 664, + versionNonce: 2135373809, + isDeleted: false, + id: "VKcfbELTVlyJ90m0bGsj0", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 90, + angle: 0, + x: -552.4984915525058, + y: 364.75449494249875, + strokeColor: "#000000", + backgroundColor: "#04aaf7", + width: 42.72020253937572, + height: 42.72020253937572, + seed: 144280593, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "sharp", + boundElementIds: [], + }, + { + type: "draw", + version: 1281, + versionNonce: 1708997535, + isDeleted: false, + id: "zWrJVrKnkF5K8iXNxi9Aa", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 0, + opacity: 100, + angle: 0, + x: -530.327851842306, + y: 378.9357912947449, + strokeColor: "#087f5b", + backgroundColor: "#40c057", + width: 28.226201983883442, + height: 24.44112284281997, + seed: 29167967, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "round", + boundElementIds: [], + startBinding: null, + endBinding: null, + points: [ + [4.907524351775825, 2.043055712211473], + [3.0769604829149455, 1.6284171290602836], + [-2.66472604008681, -4.228569719133945], + [-6.450168189798415, -2.304577297733668], + [-7.704241049212052, 4.416384506147983], + [-6.361372181234263, 8.783101300254884], + [-12.516984713388897, 10.9291595737194], + [-12.295677738198286, 15.686226498407976], + [-7.473371426945252, 15.393030178104425], + [-3.787654025313423, 11.5207568827343], + [1.2873793872375165, 19.910682356036197], + [4.492232250183542, 20.212553123686025], + [1.1302787567009416, 6.843494873631317], + [6.294108177816019, 6.390688722156585], + [8.070028349098962, 7.910451897221202], + [14.143675334886687, 7.910451897221202], + [15.709217270494545, 2.6780252579576427], + [9.128749989671498, 3.1533849725326517], + [10.393751588600717, -3.7167773257046695], + [7.380151667177483, -3.30213874255348], + [4.669824267311791, 1.1200945145694894], + [4.907524351775825, 2.043055712211473], + ], + lastCommittedPoint: null, + startArrowhead: null, + endArrowhead: null, + }, + { + type: "line", + version: 701, + versionNonce: 1583157713, + isDeleted: false, + id: "LX6kTl9A8K36ld2MEV4tI", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 90, + angle: 0, + x: -551.4394290784783, + y: 385.71736850567976, + strokeColor: "#000000", + backgroundColor: "#99bcff", + width: 42.095115772272244, + height: 0, + seed: 1443027377, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "round", + boundElementIds: [], + startBinding: null, + endBinding: null, + points: [ + [0, 0], + [42.095115772272244, 0], + ], + lastCommittedPoint: null, + startArrowhead: null, + endArrowhead: null, + }, + { + type: "line", + version: 2908, + versionNonce: 252866495, + isDeleted: false, + id: "SHmV_QtcwxIE-peI_QOX1", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 0, + opacity: 90, + angle: 0, + x: -546.3441000487039, + y: 372.6245229061568, + strokeColor: "#000000", + backgroundColor: "#99bcff", + width: 29.31860660384862, + height: 5.711199931375845, + seed: 244310513, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "round", + boundElementIds: [], + startBinding: null, + endBinding: null, + points: [ + [0, -2.341683327443203], + [0.7724193963150375, -0.06510358900749044], + [4.103544916365185, 1.84492589414448], + [8.536129150893453, 3.0016281808630056], + [15.480325949120388, 3.1070332647092163], + [23.583965316012858, 2.3706131055211244], + [28.316582284417855, -0.3084668090492442], + [29.31860660384862, -2.6041666666666288], + ], + lastCommittedPoint: null, + startArrowhead: null, + endArrowhead: null, + }, + { + type: "ellipse", + version: 725, + versionNonce: 1969008561, + isDeleted: false, + id: "PKRg6SqIetkWIgRqBAnDY", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 90, + angle: 0, + x: -538.2701841247845, + y: 363.37196531290607, + strokeColor: "#000000", + backgroundColor: "transparent", + width: 15.528434353116108, + height: 44.82230388130942, + seed: 683572113, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "sharp", + boundElementIds: [], + }, + { + type: "line", + version: 3113, + versionNonce: 533471199, + isDeleted: false, + id: "HrelUAgvfxi_4v8MyL_iT", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 0, + opacity: 90, + angle: 0, + x: -544.828148539078, + y: 402.0199316371545, + strokeColor: "#000000", + backgroundColor: "#99bcff", + width: 29.31860660384862, + height: 5.896061363392446, + seed: 318798801, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "round", + boundElementIds: [], + startBinding: null, + endBinding: null, + points: [ + [0, 0], + [4.103544916365185, -4.322122351104391], + [8.536129150893453, -5.516265043290966], + [15.480325949120388, -5.625081903117008], + [23.583965316012858, -4.8648251269605955], + [28.316582284417855, -2.0990281379671547], + [29.31860660384862, 0.2709794602754383], + ], + lastCommittedPoint: null, + startArrowhead: null, + endArrowhead: null, + }, + ], + [ + { + type: "rectangle", + version: 685, + versionNonce: 706399231, + isDeleted: false, + id: "dba8s5bDYEnF20oGn2a8b", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -715.1043446306466, + y: 330.4231266309418, + strokeColor: "#000000", + backgroundColor: "#ced4da", + width: 70.81644178885557, + height: 108.30428902193904, + seed: 1914896753, + groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], + strokeSharpness: "sharp", + boundElementIds: [], + }, + { + type: "rectangle", + version: 835, + versionNonce: 851916657, + isDeleted: false, + id: "3HxCT4mFZF-jJ6m9pyOCt", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -706.996640540555, + y: 338.68030798133873, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 55.801163535143246, + height: 82.83278895375764, + seed: 1306468145, + groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], + strokeSharpness: "sharp", + boundElementIds: [], + }, + { + type: "ellipse", + version: 881, + versionNonce: 704574495, + isDeleted: false, + id: "xX9mcMHy_0Bn-D0UAMyCc", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -684.8099707762028, + y: 425.0579911039235, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 11.427824006438863, + height: 11.427824006438863, + seed: 93422161, + groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], + strokeSharpness: "sharp", + boundElementIds: [], + }, + { + type: "rectangle", + version: 528, + versionNonce: 816914769, + isDeleted: false, + id: "h60d2h6UPYkopTlW_XEs4", + fillStyle: "cross-hatch", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -698.7169501405845, + y: 349.2244646574789, + strokeColor: "#000000", + backgroundColor: "#fab005", + width: 39.2417827352022, + height: 19.889460471185775, + seed: 11646495, + groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], + strokeSharpness: "sharp", + boundElementIds: [], + }, + { + type: "rectangle", + version: 570, + versionNonce: 1198069823, + isDeleted: false, + id: "bZbx28BjXM33JV1UezMcH", + fillStyle: "cross-hatch", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -698.7169501405845, + y: 384.7822247024333, + strokeColor: "#000000", + backgroundColor: "#fab005", + width: 39.2417827352022, + height: 19.889460471185775, + seed: 291717649, + groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], + strokeSharpness: "sharp", + boundElementIds: [], + }, + ], +]; + +const initialData = { + libraryItems, +}; +export default initialData; diff --git a/dev-docs/src/pages/index.js b/dev-docs/src/pages/index.js deleted file mode 100644 index 9437c6c71..000000000 --- a/dev-docs/src/pages/index.js +++ /dev/null @@ -1,42 +0,0 @@ -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 ( -

-
-

{siteConfig.title}

-

{siteConfig.tagline}

-
- - Get started - -
-
-
- ); -} - -export default function Home() { - const { siteConfig } = useDocusaurusContext(); - return ( - - -
- -
-
- ); -} diff --git a/dev-docs/src/pages/index.tsx b/dev-docs/src/pages/index.tsx index 9437c6c71..f35c2e4a7 100644 --- a/dev-docs/src/pages/index.tsx +++ b/dev-docs/src/pages/index.tsx @@ -14,10 +14,7 @@ function HomepageHeader() {

{siteConfig.title}

{siteConfig.tagline}

- + Get started
@@ -27,12 +24,8 @@ function HomepageHeader() { } export default function Home() { - const { siteConfig } = useDocusaurusContext(); return ( - +
diff --git a/dev-docs/src/theme/MDXComponents.js b/dev-docs/src/theme/MDXComponents.js new file mode 100644 index 000000000..35234301f --- /dev/null +++ b/dev-docs/src/theme/MDXComponents.js @@ -0,0 +1,11 @@ +// Import the original mapper +import MDXComponents from "@theme-original/MDXComponents"; +import Highlight from "@site/src/components/Highlight"; + +export default { + // Re-use the default mapping + ...MDXComponents, + // Map the "highlight" tag to our component! + // `Highlight` will receive all props that were passed to `highlight` in MDX + highlight: Highlight, +}; diff --git a/dev-docs/src/theme/ReactLiveScope/index.js b/dev-docs/src/theme/ReactLiveScope/index.js new file mode 100644 index 000000000..a282ad6f0 --- /dev/null +++ b/dev-docs/src/theme/ReactLiveScope/index.js @@ -0,0 +1,29 @@ +import React from "react"; +import ExecutionEnvironment from "@docusaurus/ExecutionEnvironment"; +import initialData from "@site/src/initialData"; +import { useColorMode } from "@docusaurus/theme-common"; + +let ExcalidrawComp = {}; +if (ExecutionEnvironment.canUseDOM) { + ExcalidrawComp = require("@excalidraw/excalidraw"); +} +const Excalidraw = React.forwardRef((props, ref) => { + const { colorMode } = useColorMode(); + return ; +}); +// Add react-live imports you need here +const ExcalidrawScope = { + React, + ...React, + Excalidraw, + Footer: ExcalidrawComp.Footer, + useDevice: ExcalidrawComp.useDevice, + MainMenu: ExcalidrawComp.MainMenu, + WelcomeScreen: ExcalidrawComp.WelcomeScreen, + LiveCollaborationTrigger: ExcalidrawComp.LiveCollaborationTrigger, + Sidebar: ExcalidrawComp.Sidebar, + exportToCanvas: ExcalidrawComp.exportToCanvas, + initialData, +}; + +export default ExcalidrawScope; diff --git a/dev-docs/static/img/doremon.png b/dev-docs/static/img/doremon.png new file mode 100644 index 0000000000000000000000000000000000000000..36208a4665fb89e44247292e92a1143a52cdf38c GIT binary patch literal 201946 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

>rqP?iYp>X2@Ky7x^}8x-)S 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

O1!W$4;+)P9SasnI-`V8^ZFleat5n zYd0JZy(?EnNYKfRQzE3n`5=-Wn}8ongWo{f;5O0{YLi4V$yW4!o1prk-{3^Ei->F`@a9JAT-Nf?%nnq1^fjSZfy$ToJ&~Ia3=N_rhb>!?`WTY zm8tgS#~CVFlAu8qw-IsmlpeqxG3jP>UTD_+V^!aH!q^qjcN0Zg3x1Z0D$DP`BALRP z&g4V@f>f2;0F`DF_`-ZuB;K7Wbw0PQ(f+@|HUTxcoAGtWbT+@b2!Hd}f6eLwc2UL< z*5cM!j^b>M_$DEc`TJQRH&2gL;$RPve(1?uWgl2TEH)RasX%wM%SuClZR-!;{aCHu zM+Y^Wyz=MYEwOobryf_m){yvujLEB%&RrW#y?A5^Pb0hEqx`Wc@3C`qj2Mc=A5fJc-8%@R zM8mZ1v+=5D{J(F5N)=MOW-H**dwXlV`)%royY%+9Zq)Sy;&Q%jw84E#glGa&V*c%D z;X0nZfGGNR=TQb#BwnNvVi*y{2XRgy=#>!W7OcaOkiWMznmm2=lCvTFec>!8lhgG; zUXOV7YS_C)PJ1p@>)Tz|wD*q!fEci}bnb7{E*Rvxn*K>oGaJi1gkd1>`*cy_Rkxv) zrzXlZv_o2kHmbF7=q8HO{C?AtT5p zjxo`QV1e&5?9U$u0i9eoZ1ULnjD-{jn^F(M;GDM%=97^p79@7)l)?_+1{nc>m-m@-kxhY+gV}qLP^H-He)y`@PB%G*0^-Vd#SO6 z@nYFCKJsvCKTwtKs}~)aF_HlVczJ4O<=6^WgK+N5_jMR4^$)&VSRrP_41X4jSOcGs zooc)|*L|XbCu9ZI_Aj`+%*Q-76(JxNqI$Xs#Xzf%>x9`A@MrbgBrMWN8TQif{@vfE zY{HlCn=HqX!TK$WSA?wd$ELC5SOsku7HgV9{J*gy6#YjY*TD-2oGo;Y-V{qzA%gMK zLRYBz1R0wUS4_bYmJg=*2Bz6;&t&@AExkn;ScChHvic&r@P)p)V~pv1I~I|z3X zi3oTXa>!AMLQVuBXQq@` zD)Z9Qt!p)DvX>U;sVBR%x@{tlmfO)rg~w>gZ40SR zThXb;ClcN=K`ElM;&i1rkG203adB*J&8SS}SQn~@p& z=S1~sF-L27x*8>gT$W#rPf%p996jYF?qdHOZ15Fe7bC`qqnJ);Mm1_d!LS^> zx5@ny!(EeT6Mkq{3uBd8Xtb?xrAHP(-3ho3N}N#{{Y(-awQR>X8-f_NFwzXP*48a=ilRqTp&JYHT^yQd`G%|4KVrD$z4bY;ilRH zIo@l@3EV>lkz(ng2Q&UORK(T7)*q!OVBS~@O_(02y8Q+)wMB???V5BHpi37mgXM;h z@w95R_isvYWR=}ZXct!9!-lGZe_T2X`80lPJRGe{S-z*cEMUN@d5`&Zds1|X-Jamz z`1~h7=W-KK1QHFJ%ZghOhq?>lSpyl6B4$8`9>VSMkaNqDaF8me#AE7STUV`!oSU3*upssHoku)UESV@m>jRKc}pO<_(E1)sFR zV>yy8U4SwvekVKn52EZjWJfCkq%czv)&alT$E-gb9v{vB_vYJEa2!7kJIS8GbiCO( zfqlR9zS129Db}zI-9dTZyKib}xDJ+iU+ptmgZigr$}T3Co9OmkTlKnLT7UbX7^Ktl zE_zcPXIJ#qdE26i?iy=CW1RAa@WZQlX#S#AUH6kt$zG6ZiI0v~i}TP>V^5z5RBK*_ zQ^zqx60Q0ERy=t7qfDStfxA>n{r<3>$+<>8XI!ckrG)Eiuze${fN@f1nGK8v#epC$ zp+k;px+j*-}N%1e~*mqwvU(D;zpQ2p-UdOz{3}#=KVeUlI*r{a>SR2g2fm>I z_=s58ncntljz|Qw6W(?*tsHh;ME*2ur~6ED?OY_RkS~HSy(ngs8-%#xzR@jQf#}$@ z8EWE(wJCwP?zVmJlMfOZBs&i+ibn`Jw6UxhIS?VZW&X%@l8Pm`C1pG!;qU0A2r0SL zD-_wb`BPzaQsSW~T$>B4S6rD|#FGj&lfU2Y^fNk*XH%*eP4v`wGS*r*YLQ|ZBf1RY zd#s&)!mJJxX_MaP$hZoh(v23CE~rZg-wXhp;XlNYPQKg@7(`dp_Ph$9TxfRaH~aVT z2RKg~I2zQ8;VGA`GgJr3GsLIP?7G4=l`)ZmxA}Pep{FbY@VpnRbWdC-xhgr9Jp5w= zTP@+zS@enf?6q1##^0_0CIeHT4KM0D>pmFR95&1gv7@IC!@&mIj ztd*M~{MC>5KUKjK4qu44k#oJaqmA%Y>9RN;aQrInr0tPJ?>9pi%uo#T9OB;{+}4Uu zSuq|qVwnu&qs>6J#<0`_myeI)QH}A3>W9h3p*TbKTl<4p-8c#o-{lKaKvQ8E%%ZY6>+8^S>9>|u;YN6MJ6`Umn5y;n60_zVRUZYc#a zDdpXRD1$yY7w&eWnv~BAgs!xv6e6!F?-8p$VJk|xi=wbZLpfw!KR8(~O>GijQL^tR zeN`3MsJNA({Qj|ovm=Q!y?tWc7d5=wKZZZuPD_`?wS)myDAMn4XPf%jZ>a)YaX>-& z86_Aq<0kUlvVB6zlCw1P_(_rHoR=@J1%N`?I67)stD`wU9TfKLX5^*Tl7I(xhOjC7 zGnRrHH}6QYk&F>|*PJ9gf2S$IX1;!O*IhtOH*AIccX#Cbv!%Ddlg7cYUqWK~{B6?j zpbqLP=an=e7?N*$cS6|NNx5;^b8~7LqJaUmk%=+=iZUNiiTrqR4FsM!TkyboO;JVB z+*)`8{=OL2jrtm1tWrOZaqxaoO?*I7OxW*pGYQjL365?r8p%JXy)LZg#DjfEpd`hz zmyj^!>X;0A`Vg(D1bHZn{^QLIgO&etXoquR?fq9x+L``!a^$$ejbJ*g%ar!gVgjnw3)iU7so48YqPd?+z z<4x#4{uF^5rXYQYpj^>K}2srT0l8i2>wrptq z$iE>CMY=5A;rBpKv5^r4v&8*dL7EaAS~lQ8C1p1*WK{=v|D-3a8v!KE@9q3t`MX@# z_>`WD5F_QjVZ_9+!KMFfz`jza()zIq_ZRyC_AZX@zPaM6qR=fXmumxO!DMA9-#VCp zA@csuVZiHfpJ)vZC5bvn(*IZL{-lq1_TClMwCiZQt{FHT_0fqIfr%E@c>b$?#({cv z@)UtHzb)BE?mT+Yw8%ShD9z&V>~ZeaLL?FK^h%TJ|l-!{3>f~iXqi43xd>C85-`;>;;m)4x@C`6s(fmCq&k(F19!0kvjig#s7V} z2BW;zgFcu1QcwM6xbUv7$PQ#>v9?V&GV^*6&hz$jH z$4H-$#w>ph>~cg64L`&_KKh*bp;j>lZ=5SM<90OoO0-c4e~8iYL5~aiLnPa&6qcb5&4uHTMZ_j6zs2TalET1^ zvE)kjThz2GWt_XctpNdZ8HXU#+i-NMaFacwB)CxKSkUP^1Xb`BOXF_aQ}*)X~*{Eqcc>WNkz`Z?`uK)nbJ z1eUN!?0Lexi$ONokh`h#>!UGqW<4Ntm&W}ij_j2GPVDe<=My_U*{UeMC48k5ZphCC zKAD;pQZ?T!!VK97Zggw(0HLiQoUw7~8xu5^L+bqIuie5+7k|rk#aqE|KE&*K^1mWL_~41X zOBfS$#n#4<9i%K7qBHcRr!1sk*e(V))L}Cu{U$Rex_2ek(pFW$9F%4GNiK&yFsB@{4Z$6Na@n%s6bMXqu_bBIz)Vfpzpu-2yt;t&?_Pk} z{H2e;j)`eL?SGGJCCIATrEf+p1O;vez@1FOaMf8_ZdY9kr8RKVh{4i4iEw>>CdkHA zZ^szx_J8UJs{hZw#_)MOnNH$8K8L4Qc+>rj$3XUg*!Jj;5sgw zWet!qWh!!{Pxs{a<5kJ|k8dqd5C1LtK~X`@{(XVbtna%I2c@TQj2lA?k9<(sV5)df zQQ=^=_I`KFp)Y8YUw$JE7%c7={&H2W(b`NjW04nB;((f2K&IB+ycC{pa%AN;g_AWG}!tK?L-Y~LX^fW^S&g&VpO$}+wI10_!fmHtf@~*>JIq+ah7m`fc*wrCjA|ML5)tjH%*4v2U-wrD)^@wVm_ozO zT}=tAIhby~*PCd|RVEdRQRVB@kNzQ0kfk-Uvw^RH79=lbZz@6&5wY{627YTjmZ&VK z^ri@Y_b%w2oTQk>+Y8izZK-nJ)TFn(!*6S$qzGA+b@+<4YA^J`zBc&`TJbiGeyg0> zm+^O;UDG*+2c<(Lp978;w2ispC9A(I`sue+Z8lii3F8FteyW^E||Jd{PBM2^i8-pp_^Z1u6PuxKMa)VIg$9 z;2GaNFfMfcbg7>u&UkXFn*IhzQ3GrTV_N+UV_Xl-zK#kqBwWt%Sd{UJZ1?;HnK$wl zvkj0NT*kPwTcLG#AOq^*@7|9jUE5U{hkvuRMQPZEnP%RwQ+|v`kkOs-+ao@)?7pOS zVk^VSrI(ZA8|YplChaA=dyPF_+X$2XX$#x(WMxkJL(?dNG!Dk z*jR`4O8G$gs!-YWQaw$Fh0ylj1x^HKi4HIpzhF*jrWmOxICzME;7FBNAauud@cFc2 z67zk#oy5vWPEKPYD{~&#QmkCG#O}H6BV$B)8S#Y<&H?j4xn1;^ZtrY7{sHR0bu*GhlhuV6I~pmv+6Ik))H91+Lug! zW0#F@`St;k5%3>Ber$q>U(9GB1UDX$Z{6X&qTN;!tpY)R&S#eE zus+6~V_TM@2KDDbCUk`p|G-7_&FdX&>;%XiK!bCYxNJwyi*#Tav&6)+a+byg6BW)f z*wyb|JZKI%tt*9YiXNy1p=#$|VMT?PXD|sW4vMcRQ>VzfgWNxWZKFTFX_nv53r`Q*5(s*{tGypMcSi0p#ji4VWRs>xDpv&^8o-=w}}R)LT5U5P+-?0i=1p zYv0}uN79Lix<7A(Ij?fbbd38gT||87*|El&O{jFJ`)zJY8aiX995e+p7x%x+2Np-M z&tOX%(pJj`9;)q4a7`r>ah3m@m?78Ah_l!zDXtYuJYu)BMv48HN*LWVxW;#XjcIy1 z8W`DVM$Y!1J^+kG7NMyc<1c<0 zs9o4Hkxs?jJFFF`_7;BUl5twNY{;uDltdAK8N-)S?Fi9W$q(Td3aXs;>{4a*A>9zh zmK@NT;Ddp8;M4Nc(0nC)|Bu{5B2|s4AB)abNJEQ22$0G38?ja6 z-V&KxoU1-yT}$_Itn{|=`OZ|w&6b$6858@S287CAP)^ZbfFy22Nx&Xx_hjBajhf9K zu=9mi{IkZszRfb>|49kKL7a*Vq04?V8Er;7ZO5QyJhO{kp2$$wV|+ISIfG!7spr=} zp7Vv1t@&lOwUK~ymkeH7$;Y;eb(^ABBU|P3T(PBR^t5*ALH642O|(GAjdr8YMkf7} zmpMbmmcD{3#xBFxQs4};G+NV)nbxw z*6x7KA1LOmk>#DCMNFv{R}5NZ=^nkJ6)H)YPE|-;HvZJ zk$0ZK=WKR&=Aok^1-1~Ihf{}a9393~NRcr*{UBP`foGj0Mib3BmhXj*mZwMJ)c&-$ z6Xnv~UWdc4Py2ng_?b00L$+Xc$V0ZmT_r#WEXIgKNm&E^yj9UyS{P;$-2x&NtaxGH z!*-&QQk{Z!l`9ft)d|;Sx?Gb#7%D#cVisHEM9c^+#yX$^d#5oEW^9groHGA=!dQ0< z(R0r-(EdU$O7?I1=~$oYpx|5+HfO&Q=Nf#R57FyzV+8Oppg}ELpEKYJNTb4}n^=Ku z&n%a8B1@- zo{g191P+|-W*VXE152nE;~Mcc!hjjq9e*mHBuezKEVwL8U%W(zP~nA?8w3oPxzl&^U;cYsm6zo5<WHZ<|LJ;A0tOF=AGDC_Qkk^-#Slnoc4CGp^Ir9*_Z|sm3mUn)atdAa!Wh_@LZnn6JUwhpeSR@~xZJ0}&MvtS5W>sm z#~0{`{6oraHD9FecqAthMfsm(E|Yf0WfI|FHc_BxD5lvczbOzM5RMoyu+6y;|0 z4#>5kO_wno5&HusyH=#*(axwqL z+|HM=bWJD$ws#MINa4jT)W4um;Zh^>oPe$0_OI_ZAUh9 z510T0+i3d#x+K|f?Ha^~iM!e#w?s`%O&75tNiI%SC1?PJr4m9vtQGkJ)s|=2;UyVeP`|Vv(IMfL4NXeZFCgW$62{EYx!N%N-JO zAVUq)fsK^+wU4rXE)(3M2Mm4Kj(ZG?ziK<_C(BuLw`Cgm*Bz}L9t=8hT;pL z98n>}Axp)NZgp?_Ctf~E@(|t-6mnfq(OOu{o_?z#YQRj@SMZ*d$rjwZiSm$4ieZGe z@0?>V_gGx@Acp3)Y$*Hi`~Xy+li4wB^~iJ%fpkk~F$rw$6(3!ohE_k|i{i!Bcd@J4 zFWkZ%Bry4lU76JsAQ$K6^R{X{L@A(xu`x21V(h{+4BE~w!yZM{TBEPJ2Mo{FCIDAU z_si{S6))}*JR#)&9$-|sUIqr)0{A!3K(c<{R%Ih~k(w6L7AnZ#rfATwc|+h+_uLN+ z93R_knpZNX%r}+z`cXf$|2g~1fMpT}&M&$fz@{+(7#b-jJt?e+S5e-)=U@pFC@idJ z#fHK*Das-lj~c)S_bOh_q_;W!ow1UA8WXAc=s79Gc**C)IkY7i*HE}_u<%&om>#yfO z3zb_FoYN@>?eK9>2d#k@A~f2^jGE5u*{<%7KFu#hTHgUWl))z!RF`B$kM9F#I+4rV z)BASzyw=NK$?($F?49aIF?8Slg~MoM^(i~Yw3ZR@x<_wVF3xRbbp|sh~D;0 zVbE$}14+`*{&a&{`%EH?=Rv5>6wN}DlpFZ^Fc%>YL5ntcz3aE%%;>P7BL*YFc>_Az zzfgitvOi0FLtMwLeVnnEnjdLxJr!gqP`YD@;kQ8#=a2iEgT zOzdfxVfv4wc`%9Jbh~)($ltwjiO)Q0c?Dwm6@E>im4^T7_%@U@h4}Q*X@Cg{Y6EZR zpX1;KH!g^6u3b=y&8Y2GS%|pR`k`dYHMZ?+`N6c{sr~9VaB1kb`m>A8nde}KlM5q+ z@9yrt#t*ubTp|O;BAP2K zl0ol^xikzKu*zwSQI6+8^&*rPf6actYM6n&t5tV8FB;zO38X4C$|n5o@)PIKcXmB# zAmiv(?aTlv7}5|*-%`<`XfG`KnaiHD@Y^KuqjBY-(&idgcUmJY!amkpT@A=J4^yaV zTI6YDvmg#aBm|X>Vszn-bjWrNh3LUTXN0Rma_bWP?m`MYYydO$FdeKe8e~U5=ycC* zw`lRA3(+T~{y(8xnlCo><4FA?x>moN<%ch%tYAXkF2n z{@2s`5r(rjQ>N4k&=BHhOeY^_OV!E4=ak)ZeJCP_lkN#7&3mfhQN`;PAzI` z8`3)DR(%%EW!t65iA)VGHX)o4&5Bo(?i z*gL_h39r3$gWgS&U1)}m5C}vV?sGF$w(9@=Ujv<1{YBRAXa)YG6v0hj%IHBqhPdOU z%EG&;`qpB`u({0^c!xNNZC9n8vTz+vtBD|B57Acj@&~R*_O!V*%Em?D8-T`dsN(ok zVz*ca^17W-HQ;Mdtiax{=VI|<{8A<>=imW`YMim~c_3{m>kp3kdq(u;nV!K^*wAf& z)tmUY-Kh|9i&PY@LIfmeRzxgqveijmOa4%U?JchYM~eD!drhjo{6qJ6-vn|!odMp^ zV(1fkWH}5-*DCw1IXWRn0o)E7@OV_(403UCPH7zdtNuc^#j9=PO`By%J*4$9`7Xel ziO;hIVZxsF`tXJ}BKT);)Lj@9k%%duIiA}Fj&Y(-=aG>b-z`9pwodtK8Dlq{{$x3O z@!wbm2FKVGv&D43ssHTRG;87?0K9c{Gyc>T0~wmBT!wJRvi9j+_9E$hOM4Pl8dRDr zjmy{q67t8XR8Yoo|3nk*U%(z^T?BS6LBa1}c>!&XhD!H;MR~b*4_1?zpu3q+ygYSc zq{`rxveb@-?PZw#MrS(c@2J<5>h@)%i=C>MS87)-@hmN&uQ+!eOOhhfd#H7({QX&x zIt>rC3Af1GzkFfMse?z<~Q@TJ6{cn7zZL1II`O4vBXC;>|>HKpzUET z?~mfc9cE;INl;NDjWSfjTHkE|x7q=RAC==w3;xmvj1*u8$u0eUar$HX9*57 z182ck6RT`f!EJaakPlDF*!cJsppeq=LTl#Pbx@cuSZB&XoZs6FAhZ7`mcb|FK2rw2yS0ecxur;f;@7=POsq$wG!LaR|@?xA`GK$|8V*WH152kf${3^zO zVh@fZDDK4tvkw#_zKdtnJ1G40ND&@+P6HjqcqcbOx$^;cyGoxc2q0lk2DcL>Do$uh zS++7uO}IUfvRrmoSRD*rHEuV_IsIOIDQM}E^?5-f=0SbE_&RxGN$WP0rUn*kJ z;^(wr&38HqH8Hg}IpbTi?R4<|Re9ixNgS>K|Yd^aNtO~F$kFvB7WmK^4A@+E~3A#7~N^&9wiuhHVQuI|Pm@8!K znSxM6zA+H_k4HcL{wn*_65oS&f2NAy`V0OW$ax;<(WMVm%>6ev)Sz(p_h#0cCn&tUwJ-nHG#HVFZH+C=y1l zRp9w$K2tNBTXKfJ0fSj^kWgb=F)FEp1HZP9e8F4pO`{I%|MDKyg(G@K!?{u}OAvn| zhKV+%vcT9LtE*>|$(qXro2s7Yr|YhUZID@JcB0P?R{H3)*NyEu52)x#$7!9_j`gR;~PQq|u0^97<{Ua-ZY02pLH##uRb=RuKdn_Ub}3nUwWhf z7e8QS4FusCzX}O_Jo+%L;Fi{s;M+hSzATI_$E>DLyd1id8$fA3p2f`m_X8S58vl2+ z91%|fK>CYD$V{j$XX5*?`N|D%GA8GXl$XH=B4&`R;5W|gTEQ*61oTwb6_szSk}a;F z2-E8jkc??t+C3rtb~jz(zIE>xRh71)!*N;geF-%C^U~W}uXRcNWGrBLzasE_2uzHX zj8v1ywXK^q@afTY=pf8scvY0bCqLv^2{$s$b+I}FU?kxI!LRz^DfI`lYZLO$&sl|p3SSOo^&rxj};_r%2COOR>?-)>;G=o2*>J` zkXXn9wY+j`>ry3Pt;=W#lsV#Y!*x{Xn7~(A#cQjL&es0sapYv)`gY3WyI2TPPu+xw z9OxJt$m~BhF9N9wk+^$_TLCRDShlciP@aLeDZSY~7-nkxohh;7pFT)!kutVORj9|% zpQsuCvSMptiXH0SD)D@tX7?0m_xs@J<*8hX8)#gfGh7j~Z;`Kp_SZW)t?+=rGF0rk zE6LOUVcvv!Z1WOX-oih=DqTttJA4Y$4DX5pe)uJ{q*Y@qRn>U7y+}HK{mg1^m+ie@ zT0aN%`o@(R?6Mw%wB84a3N0$RF%6uQvt%iKKRyV5j7&eUfnZNbBNxKxykif5azY)t z9(k+2^>9`*v9`Zl5EX;jb)~6bitN*@jB2B(d)t$Nta}P0u`wn&^7w<#FFKa!N zjL+ot!N^Ue8hy*Ib^o67SLA#N#{C-SMd-oxx-9-$^2d8|zpy3R2>T15QyB)}+jzOz z&KAz0`S`U9HG)kVTleV?vTW(D#@Fu?9zWg1NZ8e##sUH$K>-CHTK|nv<%yR6_CVD z3+R<*l>z1pk8(r|k2r9vAF+BiRKW9cs0Jf&l2jcm8cO{E5$9RWSdzKk6-r|b`f2I3 z$%3f%!6hr1sj{F#I?YEm5EphzY^Pe4>uVvte<}4@3D|)-ta-#YzACPZ0@!z{G?5AW z-^`Hza7v%gFbui*VIk<4MhfzMnA?yCYN)Q;AS?GXP|>@-FR~&jV%^oetgQKrR2iXk zfCMa_gul-#UU7Qv!TlYUeys@Jpc>4Y%m2 z2%is0T=w3eJpDeJ(X&Jvhh15Ub$WfjCcD}T<$Ym4=G!dvBE4eH@#H8l+MRB~g;`kY zzut~W&#wmV%0h_z#thf+Kf+$TGwsyOY)Hjsyx>{3&0YcZPnrz5TitTqfVbvT7-+me zDrOEtwGzE!#XZM4U6)HrC597oi6%$bhgbIF!D2DVr_7a69gK!kBtrw$4_ic53uiL4 zc~E_{te&6S8dDcW!;8^V+S+*4Awz2ggn@^8Tbd&=I(nZ9v@sl^miumAG!ao zZW9+P1lKjizI!gi+q(>|%IA04?gr|Tb1t73tE`T>QlZyfK#oz0rm;2aft%{0Z)QKi zK3;)@9s`ynD)6@I+d78U&(lsu%$h4yH@Wr1nC3<&TgJX>AAA7_FKNdt~ZM!BP&cYNO&dS<~E=^cJ zAJt&7=KMwAmI4cS8>C}z0#2}DiF8rFqCuFpR7%e)P>jDOs27b6|7`YPwqB77{47H= zCfTUX@&O0eB*Vy}|6wu1rKnpKIX>9#y_$qo;*vN?l;oTZ%1*-!Z}RpmZ*kO#)tq$X ziQt>KYV-tLpP3z7E1m0l7<4q(M9!?U4+tUPjWVE!F@wjJ|fP|PMCe2Pn zg@aFfUno{kCEx{7fu4mw5NG=+jn= zr<`XnH5GD~0o_}tm~k$B{1AV=HiW*ld~&=X#n*cEE?UaM(w#}~#)~8MSZnPR6XA<8 z3zN?4)UMx3Cs3$gK=iMxdhr+d4uwbtcvH9v3YlobF-YM6`vmI)Fz0o>%CE1KoC8#2 zIFD67K7OhnSrxbyYgv!Y6Z0RCz_LFAuew8Tmr@g5zMw?qVy6*uJGVyO{|T_2D(=}6 zj1^2xGYjYZ+%W$L$p$h|FFpd+Dk=tuK8N~g&0Mrh1A{pzBBmh>omkw@uLB23XCtc8 zuczfszh55qEXeP6wVRZmy`%f=@?q2V$IC`x@Os?`-7bC*`fdSO^m~B*Q3<@?oQ75^ ztIo|96OhwO2S!wFZF`t%@j&p6(zcob!TU?q79-e#*M*KTLs``@@4}wbd~Nc|>Bx)J zg5UqrkzJt-+L=uxP%YEwXF3`x9Gb7uxLMC)K83bVaa!22s}T`@s6v+PJONInr%hdt z6>UKJoK(A|x$hRodY+tYhnA!5@J(2qI>vZk#m}Ilv)|tNgxDFn^yc{+EV#*>6FqP2 z5#=+MtEh{|O~cW{_OcN7#+oW!qF_D)Ol?MoArICpz&Z62!nvd!;-b2yNty~9a%TQo zUXPb$hmE}RWnvH88kOS2PnaJ|;X+BoVHjT@U^LNv*J)sJ;MTM0#nfHA@fwwn?l=Am zohpC&-OpLofGYxa5_%vwa%xRhP*QU*yt$>-Btp=pUQiVmNn>hONe+>!-&F=_esG*t znYKfUWZoA_uqSg`BA^jbjV9&i>eCjWQtr1+J(-&m@!kZ5|I^~X1AZ4;qS=O0m{XnnP-x3lyNS=s69HRYUFZSI41%>Nhwo{* z^7|+lbK--cByt9Fw7x-9($*#=_dV?yRUUCSU@`R4IoKy*XbSq&AH&f(M2(V?lJf2J zm9in)jxAx3qCz^#GruOoQ(*y`R%mc#YIMuORR@6>>9d9dC6n)mxmERNx99t0r6m1 z8HG=pGd#c?-GqqKL;g;b8H8xDP#G#wCEkLckoT^bmDKo(^}A0Y4Z{Foy_m^)e$Nq` zCuhg{7pM7{80|pUqU_1P+zhGUqy{JX|HgUYp;a^qY>XIt&2@M6R81P^k56B2DHorx z1P?j+Nm{;RgBK~XDGPYT229-18^5Cbfr|LuA($hiQ_qIzBtT8=_s;#)2Dw^}^6n|< zT00-k<-%u9R=c`*7W>D6o75psE7I|z516i_UtoD-dsqQ?XQdfI@9Xcp<%yK|3diDR zlfefP4?@2w^=%7TaV5U{eqkg{!LKQ_pHgvCm_;o^(7YMozVxSa-E#d66h6J5*j5ac zGsg~0yfph(9`?)!)ZO9$2QI-_$MmX?QJhtqw(Fc?shAf5JI{ih5lfz%an7tfHW47C zVml8eotX;f1Maog4R84c!LLq zA9UJr19Cp6KrkwNA(m%8Yg98Lik@)cpC?INBq&W0m-vf;CCh`n5gC==U)-0P?{j(0 zbVg_+KY5lE)fswpvoQ%7$80Np>L%1Y@$NdJP%iaNtro9~j+imqY}R+%?SAhI67ZrM zfb8b}oc}k=mF5rf>=$ShcgA|k>%s1Lbof9#t`Kc@*w%GyI0a>%z7lfdtY~Ef@l-5o z4LuN{Ix4+ppks3oD0lqvqD@Q1$Ec}o>t_Nc;cY^eoD1pxW6JA+?#`rr(y-5XKbfkX zHsO0np0vXB`V~5#yUx!OSOb*dmSI>P9$yaXK*!jPZ9IIlvuY=e?wEcND zEXLQ?8M*pkTLbhO85I4us^T640zJK{T96tAh<3zYOW#A_bbJz)iFYKc<*kkGiTR3!Z52ZsvmVDjKhmC zD?h0v7L+CTiqD=LUUGL^MP9c^&6qy(HlaH8Euj%)+emHj>4o6{An7;{0cw|RV?@S@ z_%3vJ;7wYWw&C_Sx(WNUC)sqxZvul002()_p(yp)AI>nrR5sU9Xq9>7`1qXR_Q*C08H2_8 zSF=oyZqdH}eWyyA)*Sp$lIUGL#u<_^cw&PZG0HpZwnWNBsB}0P;tbAK z<(RDdP+RKIM?Z7)Gwtd~B(yd%r($NB-iXVlhaFQ9IG%pJz9(F_4_8|E4D*S^MK?ctBag zdwo5E3UmVcx|>le3?^<*aIdA4tZ_)aJ;rhz_aw@z&A0 zX0hyfHKUEbXkVgB65-bNCPw)ise%9P7Di>@k>zwSWe^6u%?G37P=RKTW6qn0>8R5a z7oVkX_L}bEkdHV&76TAq9IGMkxiZH*24&${x;`4ExHpD|}0kv|eC}jVwc#BLC&qQ^X zDg{TXlG(3kSlhQxz!YYws19na7Hhs2#I<9Vn{UN5VZHtSAnu=4j9+YkT$ z!Dg%uYnrE`60LkIoD5AUae;D@lfr8jW`S(+$ydd8I+F&yKYXb)pF zG_3)gI1Koo*LH}=&9#TlF^XCnjGE% zHTmp`l+u%NTc9133X^fnGQ8#^&xSBO4FJ2u(*w+U@|>BSL^DJboSEqrdfTra!!kk5 z-GjH|K+5z@8S|~q@#drdi3?y_C=bLjNt!RDE@moqkR?FWSUS~`Zn8NC6Krr$x>AL@ zH0-3WC0{jfHTQ&P@uPBjnM{|d=NK3=w9kcr! zO#$D6ezg|snU7{G1t^}C1%>f^?(SqOi!IAn%-@ZH?Q394RQkV@2;b2-0D!fgjIYOGXd+opFJOb+C^@x zy2cv<4Jbrs!b={X7!|UHj7TAiceVM4+ii<|(H#HUu@W0e&AE3{j*zqMJtq6keujZe zh|p<+oy6$IRC%zFcII?@v>)OQoo_|gY7lpUS96g+j{6qZ?- zm8g-DbFqEML<6`WGQMuh}{Zy8p!(iO~CIoa$Gi>+9GCn zu!Dbp{QL`ut?AZet559>dvH37EMmE6Q%4k1WIRz^WCG2ZtTQVqw|E7#@!m|Qg?UR8!3Z6u@2jB#348wg$v%aK`h1%WlYSd;FOtD{Yc zQmD7sDyj8{GNNKOD&GE_#$ z^w~JLX*6#RI9WeEKHH?33QK$DFCu1R(Uiv!VF*)9&Mo{r6wF0yB9}+*JV<}htHW;@ z$u#;V{$wyrRSTF}{UUmQ0AJIONNVE18|`W-f705{E)1xZO-4lzI1~d-m^B)Y(P@Ho zz4Muyk@}y3NEs5)kHbL@bl`AWPcv0yh82|kz*lh@CR+<%z&-W-MR%jRDcRg^PGYOC zgHA7AnX9UTHI6kH8V?Dip76RbZBPZW5_hbM!z=PmasVG`-pJ->@}Rf> zg)N{ZhXXDWOGL%FbO?S;iX1~>_U6BzClM~yt6<1EES~uw7Ye*XZ3vSAwnteiNFR-z zo!uFSBO0{ywm;BRnp1vgo=Yo|R^$)bkG+ufs=>I*P(miQTcpYn08T}leW;U8=IA5opDnud zXm8R`RER=xIlk%9XNb@jtc1;o9p9%dsmQb$fbg}BEabyqJZy4TuaMp=+8EQ8_u%K! zN6&wkU!)dzyJ*)$vE+Y$6nhxO?FNBs@{x1g3U-|!+6@x@`ZaT_X=8IZ64RG2!z=c z{6c+1%*QhNc`<6!+uj_l4b8;Wg1-Fg_oZcZU=N!|phCXvEk_`DyILrSbji2$Lwd45 z$LE;Y!fkzgFObVB7{UkTXSV2G0`%hu1NtbCUP((OI*>&|AV$kPg5%09x#3LLXFy-7 zJ~Du0WBfjuj1q&RM^c)|70u<30Wv8%?d@HcIHkTQaU?pL!#X(FPGsu^cF3G}G{!%b zM}6HEOtt)G2#Ank-(fj3j1o65ksevye$|0@yU@<&@_IsZ8FWNK@68nzF@tLdJMg$v z``>5!zet2GC$Csz=1i0k2`tbkdVrH5SHvk$0&C#hF zKbzy>2`kJ3DOABqrq-7bLA=NvQzqgMb zKE~8vrXl}xGs8=;ihUFJWgXS`b=2zI_Jao>h>jX0bMqWAY3&i8p&Nb0PH1g;hHw$v zlv}GUt~|^No&}QN(KXq_!s#ot+gK3!E>Dh)$fWWBaHqmcX^0rgbs#RGuuh+HJ@1G< zLjcI;Njp9k{~sLQx^SLR>IedgQW*&9p_T!*<_tzi7thCVj`YmmGh3#GUGAO*Wy{xs z+m2EX_L_GNd^4Xy2ZD_$iMzI7eTAf!5bSYzQq2UYq;Rp_WZakaG{s$XQQ@r%_V4r! zIlM_jzHc zvX&NY1ILt0Sc|r=JuMFoeYCHq;D3Vmr5!VnigiPMXQ}v|=`!QR-m|Z+!Tyr3@*V3j zt@E7Ww}LFaO zh^KrLYr|rpbvOUI95Qj{=2)_bGWgNK{22cy9>)tXNw`C%hD-tS;wOIPFeFLDt$HQA2O$7pnVr#&-n-aX=>L) zST?p0$1R?yn6_q-m+h@(zB&c61=CbNG?vdbV}gH&oVT18I0-ej;QbX~x_F9LHnmgL zK}Ih}>dhr%gm$%B)GahO_T-!_dM|xH&h-IhwygO0=|bAx(2(=Lc_S=XX8SBh0>f6a zW+b(eQkKG*qa>&#kVwb~O)hosYc8nZ%t5g?>>D}s_kP8@fi56&G0J_>NRt|eZ$^81 zj$3d0pNdGg!G^&yE7_Nh!lhbc5xu1D*FT>JFo&_iv8g~D-5-X#)7vq@FWZ;!>Y2nw z`U&yZ-B#wKXw=#F1dsn1)Ua)O*M~I)S6iWhI|n38-PL0#x`Wl1IH@iHqN+mRnD~2c+?{GB}iWen_v4dbyJkt0aC~YdlvjoS@ZtLpKw-U^MeOe&n zLQz|PcJlEN44HXZk{nNakn3g)Y~Q|h5 zc|W0LarpXA^XwmDArSP9{i=zzjikm=o|Ri6~UdC@Yk;MjVQ@7&6VBt&uR2_Mh7d`!ygtWKNQTDTbHp%qbx#2s#lF4tqC^%xPwCe##Tm$YDi zN8sdA(aHuIHdAfjX0Tij-vJK0&3Ptl56@$HYwBcnMY$h4FndulJQ|_@ec|$&uP?Xe z=Ep74jgCr3Ha_hwwYcn!QuKjOym-NUzA_w2O0E!TjA-ul*12Ohz${va7?4IFe zBM5f)LvJ7?KK^0)d5gW{e3;b{K)}>!2+`df0mOV;*!XJHZ!`I8sDi{_Dr~}D3oYcTJ|##Oubu%>h{vi`toDFRTC5 z3wS$r48dN)Mor;Sr;y3N^Pdae7%CIIxLLkaDA{wdA-|l?Yb1NdWq5Iur9!U|Bbl?g022jWa`Pu-e; z($xLj1zbsqDFYsy+&gRZ@l0SiLolBs9J%}GuwWLihSAX=?;ysezcA@%S@hM zz0iDD9A16@Ic|EJ#gc2e0g{+RUGy6##va>kIo3?vJ13v*OJ7^Mae{#gW0VQw20AF> zDQJod#}HyeTNy{!D1i0L1}^E06&f?l!vBWm>G}3YXiGhUdCaGYh9o)++ur|zE`bPY zB8wQkdMke=I7WRnD(fY)LVEf+uomQE_$3FiZD4oO0M3O=%F>&VI)j}qnDNkx=D_h$ zV@T7oyNQBS+Lv+J_EB-T%GD8xRi0nfrOEz_q!WWE~+0Hh~8 zt%YExX+f7g2&{M0HZ-#;fReeUH2|PwqA*5m=-X2nQGf-DE&BYxjZfai@c!qwM^1UU zawhw;0PG1WM_;ceTg zS=ZiYnSCHSTaPKd;0Xtj#4*yJ-6PW2uqEE}z3ofCCJbBYB>X5u=?}=4m42e#>cD=l zz@misHrH&!l60xEEKF$!+V93Az3wshk<*yvntT%pX}hRGPIE-S=wD~PMpfsJ%;@j7 z_Zu$s!domn{p}zOS!v?nQO~mPh!%^lds+_N9L}nB4D^gxF=%t;Sm_FuZq7E_h%r0Z z16_l6W^21o;_hoeI&8D_fpod!nsY58ACzd=Cr=2Bua>EvPLZ3pGi=rXYKz738qi$S z8b;WtngQ1IcT(Rb5MPDjrRnLR`lRrQefN%JtRCpC-PYKqYd{BSL{Mi*HkD<*kpUah z?srIRMdHpL5IH6Ot-&s{+?GgCR@1AKr?Cv3*#dLDFB}<-X5>jXm`A`xegr`#&(l!C zy{qRd$R7zqk2rWn9se%T@+D0^e;2Ap{p)+B z+t&Wy?X#K2o87U+5l!mLs`aPP?3!T|Rs`)eA@cD|efN2@7nn{pW{lOZq8eVgtdHDc zb>(Gq%z>@Jw7l>QK3=aS%aQAFHoAD5{^l(*M}{$FB^NL+qYXvTO*PQ!AgcGT`g@CD zP*k=M#PAwH|h_=2swV&M{))dDcoqb?~!8JC|y|J}C`{@%F@2{jD-)mm-oUh?}TMUV046?sVWQnj(?xF;$yC2rc7l3-!HJ~lSjN6i;L(n9zGuzBaIfDe8rST8 z?AX?K3~zL-!i}{bs)s~a?Qy5W@;-hC0?Ao+y7`2z|HUrcDu83jP@$hhYIY&syCT0^ z7Ff4Vbh~q+@IiEs}tF4YNyGRY#uFtB5CT5-N7 z@ z>gi_­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 zk308F

View Example - -```js -import React, { useEffect, useState, useRef } from "react"; -import { Excalidraw } from "@excalidraw/excalidraw"; -import InitialData from "./initialData"; - -import "./styles.scss"; - -export default function App() { - const excalidrawRef = useRef(null); - - const [viewModeEnabled, setViewModeEnabled] = useState(false); - const [zenModeEnabled, setZenModeEnabled] = useState(false); - const [gridModeEnabled, setGridModeEnabled] = useState(false); - - const updateScene = () => { - const sceneData = { - elements: [ - { - type: "rectangle", - version: 141, - versionNonce: 361174001, - isDeleted: false, - id: "oDVXy8D6rom3H1-LLH2-f", - fillStyle: "hachure", - strokeWidth: 1, - strokeStyle: "solid", - roughness: 1, - opacity: 100, - angle: 0, - x: 100.50390625, - y: 93.67578125, - strokeColor: "#c92a2a", - backgroundColor: "transparent", - width: 186.47265625, - height: 141.9765625, - seed: 1968410350, - groupIds: [], - }, - ], - appState: { - viewBackgroundColor: "#edf2ff", - }, - }; - excalidrawRef.current.updateScene(sceneData); - }; - - return ( -
-

Excalidraw Example

-
- - - - - -
-
- - console.log("Elements :", elements, "State : ", state) - } - onPointerUpdate={(payload) => console.log(payload)} - viewModeEnabled={viewModeEnabled} - zenModeEnabled={zenModeEnabled} - gridModeEnabled={gridModeEnabled} - /> -
-
- ); -} -``` - -To view the full example visit :point_down: - -[![Edit excalidraw](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/excalidraw-ehlz3?fontsize=14&hidenavigation=1&theme=dark) - -
- -Since Excalidraw doesn't support server side rendering yet, you should render the component once the host is mounted. - -```js -import { useState, useEffect } from "react"; -export default function IndexPage() { - const [Comp, setComp] = useState(null); - useEffect(() => { - import("@excalidraw/excalidraw").then((comp) => setComp(comp.default)); - }, []); - return <>{Comp && }; -} -``` - -The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm) - -#### In Browser - -To use it in a browser directly: - -For development use :point_down: - -```js - -``` - -For production use :point_down: - -```js - -``` - -You will need to make sure `react`, `react-dom` is available as shown in the below example. For prod please use the production versions of `react`, `react-dom`. - -
View Example - -```html - - - - Excalidraw in browser - - - - - - - - -
-

Excalidraw Embed Example

-
-
- - - -``` - -```js -/*eslint-disable */ -import "./styles.css"; -import InitialData from "./initialData"; - -const App = () => { - const excalidrawRef = React.useRef(null); - - const [viewModeEnabled, setViewModeEnabled] = React.useState(false); - const [zenModeEnabled, setZenModeEnabled] = React.useState(false); - const [gridModeEnabled, setGridModeEnabled] = React.useState(false); - - const updateScene = () => { - const sceneData = { - elements: [ - { - type: "rectangle", - version: 141, - versionNonce: 361174001, - isDeleted: false, - id: "oDVXy8D6rom3H1-LLH2-f", - fillStyle: "hachure", - strokeWidth: 1, - strokeStyle: "solid", - roughness: 1, - opacity: 100, - angle: 0, - x: 100.50390625, - y: 93.67578125, - strokeColor: "#c92a2a", - backgroundColor: "transparent", - width: 186.47265625, - height: 141.9765625, - seed: 1968410350, - groupIds: [], - }, - ], - appState: { - viewBackgroundColor: "#edf2ff", - }, - }; - excalidrawRef.current.updateScene(sceneData); - }; - - return React.createElement( - React.Fragment, - null, - React.createElement( - "div", - { className: "button-wrapper" }, - React.createElement( - "button", - { - className: "update-scene", - onClick: updateScene, - }, - "Update Scene", - ), - React.createElement( - "button", - { - className: "reset-scene", - onClick: () => excalidrawRef.current.resetScene(), - }, - "Reset Scene", - ), - React.createElement( - "label", - null, - React.createElement("input", { - type: "checkbox", - checked: viewModeEnabled, - onChange: () => setViewModeEnabled(!viewModeEnabled), - }), - "View mode", - ), - React.createElement( - "label", - null, - React.createElement("input", { - type: "checkbox", - checked: zenModeEnabled, - onChange: () => setZenModeEnabled(!zenModeEnabled), - }), - "Zen mode", - ), - React.createElement( - "label", - null, - React.createElement("input", { - type: "checkbox", - checked: gridModeEnabled, - onChange: () => setGridModeEnabled(!gridModeEnabled), - }), - "Grid mode", - ), - ), - React.createElement( - "div", - { - className: "excalidraw-wrapper", - ref: excalidrawWrapperRef, - }, - React.createElement(ExcalidrawLib.Excalidraw, { - initialData: InitialData, - onChange: (elements, state) => - console.log("Elements :", elements, "State : ", state), - onPointerUpdate: (payload) => console.log(payload), - viewModeEnabled: viewModeEnabled, - zenModeEnabled: zenModeEnabled, - gridModeEnabled: gridModeEnabled, - }), - ), - ); -}; - -const excalidrawWrapper = document.getElementById("app"); - -ReactDOM.render(React.createElement(App), excalidrawWrapper); -``` - -To view the full example visit :point_down: - -[![Edit excalidraw-in-browser](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/excalidraw-in-browser-tlqom?fontsize=14&hidenavigation=1&theme=dark) - -
- -### Customizing styles - -Excalidraw is using CSS variables to style certain components. To override them, you should set your own on the `.excalidraw` and `.excalidraw.theme--dark` (for dark mode variables) selectors. - -Make sure the selector has higher specificity, e.g. by prefixing it with your app's selector: - -```css -.your-app .excalidraw { - --color-primary: red; -} -.your-app .excalidraw.theme--dark { - --color-primary: pink; -} -``` - -Most notably, you can customize the primary colors, by overriding these variables: - -- `--color-primary` -- `--color-primary-darker` -- `--color-primary-darkest` -- `--color-primary-light` -- `--color-primary-contrast-offset` — a slightly darker (in light mode), or lighter (in dark mode) `--color-primary` color to fix contrast issues (see [Chubb illusion](https://en.wikipedia.org/wiki/Chubb_illusion)). It will fall back to `--color-primary` if not present. - -For a complete list of variables, check [theme.scss](https://github.com/excalidraw/excalidraw/blob/master/src/css/theme.scss), though most of them will not make sense to override. - -### Does this package support collaboration? - -No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx). - -### Component API - -#### Footer - -Earlier we were using `renderFooter` prop to render custom footer which was removed in [#5970](https://github.com/excalidraw/excalidraw/pull/5970). Now you can pass a `Footer` component instead to render the custom UI for footer. - -You will need to import the `Footer` component from the package and wrap your component with the Footer component. The `Footer` should a valid React Node. - -**Usage** - -```js -import { Footer } from "@excalidraw/excalidraw"; - -const CustomFooter = () => ; -const App = () => { - return ( - -
- -
-
- ); -}; -``` - -Footer is only rendered in the desktop view. - -In the mobile view you can render it inside the [MainMenu](#mainmenu) (later we will expose other ways to customize the UI). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component. - -```js -import { useDevice, Footer } from "@excalidraw/excalidraw"; - -const MobileFooter = () => { - const device = useDevice(); - if (device.isMobile) { - return ( -
- -
- ); - } - return null; -}; - -const App = () => { - - - window.alert("Item1")}> - Item1 - - window.alert("Item2")}> - Item2 - - - - ; -}; -``` - -You can visit the [example](https://ehlz3.csb.app/) for working demo. - -#### MainMenu - -By default Excalidraw will render the `MainMenu` with default options. If you want to customise the `MainMenu`, you can pass the `MainMenu` component with the list options. You can visit [codesandbox example](https://ehlz3.csb.app/) for a working demo. - -**Usage** - -```js -import { MainMenu } from "@excalidraw/excalidraw"; -const App = () => { - - - window.alert("Item1")}> - Item1 - - window.alert("Item2")}> - Item2 - - - ; -}; -``` - -**MainMenu** - -This is the `MainMenu` component which you need to import to render the menu with custom options. - -**MainMenu.Item** - -Use this component to render a menu item. - -| Prop | Type | Required | Default | Description | -| --- | --- | --- | --- | --- | -| `onSelect` | `Function` | Yes | `undefined` | The handler is triggered when the item is selected. | -| `children` | `React.ReactNode` | Yes | `undefined` | The content of the menu item | -| `icon` | `JSX.Element` | No | `undefined` | The icon used in the menu item | -| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) | - -**MainMenu.ItemLink** - -To render an external link in a menu item, you can use this component. - -**Usage** - -```js -import { MainMenu } from "@excalidraw/excalidraw"; -const App = () => ( - - - Google - - Excalidraw - - - ; -); -``` - -| Prop | Type | Required | Default | Description | -| --- | --- | --- | --- | --- | -| `href` | `string` | Yes | `undefined` | The `href` attribute to be added to the `anchor` element. | -| `children` | `React.ReactNode` | Yes | `undefined` | The content of the menu item | -| `icon` | `JSX.Element` | No | `undefined` | The icon used in the menu item | -| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) | - -**MainMenu.ItemCustom** - -To render a custom item, you can use `MainMenu.ItemCustom`. - -**Usage** - -```js -import { MainMenu } from "@excalidraw/excalidraw"; -const App = () => ( - - - - - - - ; -); -``` - -| Prop | Type | Required | Default | Description | -| --- | --- | --- | --- | --- | -| `children` | `React.ReactNode` | Yes | `undefined` | The content of the menu item | - -**MainMenu.DefaultItems** - -For the items which are shown in the menu in [excalidraw.com](https://excalidraw.com), you can use `MainMenu.DefaultItems` - -```js -import { MainMenu } from "@excalidraw/excalidraw"; -const App = () => ( - - - - - window.alert("Item1")}> Item1 - window.alert("Item2")}> Item 2 - - -) -``` - -Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/src/components/mainMenu/DefaultItems.tsx) of the default items. - -**MainMenu.Group** - -To Group item in the main menu, you can use `MainMenu.Group` - -```js -import { MainMenu } from "@excalidraw/excalidraw"; -const App = () => ( - - - - - - - - window.alert("Item1")}> Item1 - window.alert("Item2")}> Item 2 - - - -) -``` - -| Prop | Type | Required | Default | Description | -| --- | --- | --- | --- | --- | -| `children ` | `React.ReactNode` | Yes | `undefined` | The content of the `MainMenu.Group` | - -### WelcomeScreen - -When the canvas is empty, Excalidraw shows a welcome "splash" screen with a logo, a few quick action items, and hints explaining what some of the UI buttons do. You can customize the welcome screen by rendering the `WelcomeScreen` component inside your Excalidraw instance. - -You can also disable the welcome screen altogether by setting `UIOptions.welcomeScreen` to `false`. - -**Usage** - -```jsx -import { WelcomScreen } from "@excalidraw/excalidraw"; -const App = () => ( - - - - - Your data are autosaved to the cloud. - - - console.log("clicked!")} - > - Click me! - - - Excalidraw GitHub - - - - - - -); -``` - -To disable the WelcomeScreen: - -```jsx -import { WelcomScreen } from "@excalidraw/excalidraw"; -const App = () => ; -``` - -**WelcomeScreen** - -If you render the `` component, you are responsible for rendering the content. - -There are 2 main parts: 1) welcome screen center component, and 2) welcome screen hints. - -![WelcomeScreen overview](./welcome-screen-overview.png) - -**WelcomeScreen.Center** - -This is the center piece of the welcome screen, containing the logo, heading, and menu. All three sub-components are optional, and you can render whatever you wish into the center component. - -**WelcomeScreen.Center.Logo** - -By default renders the Excalidraw logo and name. Supply `children` to customize. - -**WelcomeScreen.Center.Heading** - -Supply `children` to change the default message. - -**WelcomeScreen.Center.Menu** - -Wrapper component for the menu items. You can build your menu using the `` and `` components, render your own, or render one of the default menu items. - -The default menu items are: - -- `` - opens the help dialog. -- `` - open the load file dialog. -- `` - intended to open the live collaboration dialog. Works similarly to [``](#LiveCollaborationTrigger) and you must supply `onSelect()` handler to integrate with your collaboration implementation. - -**Usage** - -```jsx -import { WelcomScreen } from "@excalidraw/excalidraw"; -const App = () => ( - - - - - console.log("clicked!")} - > - Click me! - - - Excalidraw GitHub - - - - - - -); -``` - -**WelcomeScreen.Center.MenuItem** - -Use this component to render a menu item. - -| Prop | Type | Required | Default | Description | -| --- | --- | --- | --- | --- | -| `onSelect` | `Function` | Yes | | The handler is triggered when the item is selected. | -| `children` | `React.ReactNode` | Yes | | The content of the menu item | -| `icon` | `JSX.Element` | No | | The icon used in the menu item | -| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) | - -**WelcomeScreen.Center.MenuItemLink** - -To render an external link in a menu item, you can use this component. - -| Prop | Type | Required | Default | Description | -| --- | --- | --- | --- | --- | -| `href` | `string` | Yes | | The `href` attribute to be added to the `anchor` element. | -| `children` | `React.ReactNode` | Yes | | The content of the menu item | -| `icon` | `JSX.Element` | No | | The icon used in the menu item | -| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) | - -**WelcomeScreen.Hints** - -These subcomponents render the UI hints. Text of each hint can be customized by supplying `children`. - -**WelcomeScreen.Hints.Menu** - -Hint for the main menu. Supply `children` to customize the hint text. - -**WelcomeScreen.Hints.Toolbar** - -Hint for the toolbar. Supply `children` to customize the hint text. - -**WelcomeScreen.Hints.Help** - -Hint for the help dialog. Supply `children` to customize the hint text. - -### LiveCollaborationTrigger - -If you implement live collaboration support and want to expose the same UI button as on excalidraw.com, you can render the `` component using the [renderTopRightUI](#rendertoprightui) prop. You'll need to supply `onSelect()` to handle opening of your collaboration dialog, but the button will display current `appState.collaborators` count for you. - -| Prop | Type | Required | Default | Description | -| --- | --- | --- | --- | --- | -| `onSelect` | `() => any` | Yes | | Handler called when the user click on the button | -| `isCollaborating` | `boolean` | Yes | false | Whether live collaboration session is in effect. Modifies button style. | - -**Usage** - -```jsx -import { LiveCollaborationTrigger } from "@excalidraw/excalidraw"; -const App = () => ( - { - if (isMobile) { - return null; - } - return ( - setCollabDialogShown(true)} - /> - ); - }} - /> -); -``` - -### Props - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| [`onChange`](#onChange) | Function | | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw elements and the current app state. | -| [`initialData`](#initialData) | {elements?:
ExcalidrawElement[], appState?: AppState } | null | The initial data with which app loads. | -| [`ref`](#ref) | [`createRef`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs) | [`useRef`](https://reactjs.org/docs/hooks-reference.html#useref) | [`callbackRef`](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs) | { current: { readyPromise: resolvablePromise } } | | Ref to be passed to Excalidraw | -| [`isCollaborating`](#isCollaborating) | `boolean` | | This implies if the app is in collaboration mode | -| [`onPointerUpdate`](#onPointerUpdate) | Function | | Callback triggered when mouse pointer is updated. | -| [`langCode`](#langCode) | string | `en` | Language code string | -| [`renderTopRightUI`](#renderTopRightUI) | Function | | Function that renders custom UI in top right corner | -| [`renderCustomStats`](#renderCustomStats) | Function | | Function that can be used to render custom stats on the stats dialog. | -| [`renderSIdebar`](#renderSIdebar) | Function | | Render function that renders custom sidebar. | -| [`viewModeEnabled`](#viewModeEnabled) | boolean | | This implies if the app is in view mode. | -| [`zenModeEnabled`](#zenModeEnabled) | boolean | | This implies if the zen mode is enabled | -| [`gridModeEnabled`](#gridModeEnabled) | boolean | | This implies if the grid mode is enabled | -| [`libraryReturnUrl`](#libraryReturnUrl) | string | | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to | -| [`theme`](#theme) | [THEME.LIGHT](#THEME-1) | [THEME.DARK](#THEME-1) | [THEME.LIGHT](#THEME-1) | The theme of the Excalidraw component | -| [`name`](#name) | string | | Name of the drawing | -| [`UIOptions`](#UIOptions) | { canvasActions: CanvasActions } | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L129) | To customise UI options. Currently we support customising [`canvas actions`](#canvasActions) | -| [`onPaste`](#onPaste) | (data: ClipboardData, event: ClipboardEvent | null) => boolean | | Callback to be triggered if passed when the something is pasted in to the scene | -| [`detectScroll`](#detectScroll) | boolean | true | Indicates whether to update the offsets when nearest ancestor is scrolled. | -| [`handleKeyboardGlobally`](#handleKeyboardGlobally) | boolean | false | Indicates whether to bind the keyboard events to document. | -| [`onLibraryChange`](#onLibraryChange) | (items: LibraryItems) => void | Promise<any> | | The callback if supplied is triggered when the library is updated and receives the library items. | -| [`autoFocus`](#autoFocus) | boolean | false | Implies whether to focus the Excalidraw component on page load | -| [`generateIdForFile`](#generateIdForFile) | `(file: File) => string | Promise` | Allows you to override `id` generation for files added on canvas | -| [`onLinkOpen`](#onLinkOpen) | (element: NonDeletedExcalidrawElement, event: CustomEvent) | | This prop if passed will be triggered when link of an element is clicked. | -| [`onPointerDown`](#onPointerDown) | (activeTool: AppState["activeTool"], pointerDownState: PointerDownState) => void | | This prop if passed gets triggered on pointer down evenets | -| [`onScrollChange`](#onScrollChange) | (scrollX: number, scrollY: number) | | This prop if passed gets triggered when scrolling the canvas. | - -### Dimensions of Excalidraw - -Excalidraw takes `100%` of `width` and `height` of the containing block so make sure the container in which you render Excalidraw has non zero dimensions. - -#### `onChange` - -Every time component updates, this callback if passed will get triggered and has the below signature. - -```js -(excalidrawElements, appState, files) => void; -``` - -1.`excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106) in the scene. - -2.`appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79) of the scene. - -3. `files`: The [`BinaryFiles`]([BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) which are added to the scene. - -Here you can try saving the data to your backend or local storage for example. - -#### `initialData` - -This helps to load Excalidraw with `initialData`. It must be an object or a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) which resolves to an object containing the below optional fields. - -| Name | Type | Description | -| --- | --- | --- | -| `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106) | The elements with which Excalidraw should be mounted. | -| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79) | The App state with which Excalidraw should be mounted. | -| `scrollToContent` | boolean | This attribute implies whether to scroll to the nearest element to center once Excalidraw is mounted. By default, it will not scroll the nearest element to the center. Make sure you pass `initialData.appState.scrollX` and `initialData.appState.scrollY` when `scrollToContent` is false so that scroll positions are retained | -| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)> | This library items with which Excalidraw should be mounted. | -| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) | The files added to the scene. | - -```json -{ - "elements": [ - { - "type": "rectangle", - "version": 141, - "versionNonce": 361174001, - "isDeleted": false, - "id": "oDVXy8D6rom3H1-LLH2-f", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 100.50390625, - "y": 93.67578125, - "strokeColor": "#000000", - "backgroundColor": "transparent", - "width": 186.47265625, - "height": 141.9765625, - "seed": 1968410350, - "groupIds": [] - } - ], - "appState": { "zenModeEnabled": true, "viewBackgroundColor": "#AFEEEE" } -} -``` - -You might want to use this when you want to load excalidraw with some initial elements and app state. - -#### Storing custom data on Excalidraw elements - -Beyond attributes that Excalidraw elements already support, you can store custom data on each element in a `customData` object. The type of the attribute is [`Record`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L59) and is optional. - -You can use this to add any extra information you need to keep track of. - -You can add `customData` to elements when passing them as `initialData`, or using [`updateScene`](#updateScene)/[`updateLibrary`](#updateLibrary) afterwards. - -#### `ref` - -You can pass a `ref` when you want to access some excalidraw APIs. We expose the below APIs: - -| API | Signature | Usage | -| --- | --- | --- | -| ready | `boolean` | This is set to true once Excalidraw is rendered | -| readyPromise | [resolvablePromise](https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317) | This promise will be resolved with the api once excalidraw has rendered. This will be helpful when you want do some action on the host app once this promise resolves. For this to work you will have to pass ref as shown [here](#readyPromise) | -| [updateScene](#updateScene) | (scene: sceneData) => void | updates the scene with the sceneData | -| [updateLibrary](#updateLibrary) | (opts) => Promise<LibraryItems> | updates the scene with the sceneData | -| [addFiles](#addFiles) | (files: BinaryFileData) => void | add files data to the appState | -| resetScene | `({ resetLoadingState: boolean }) => void` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. | -| getSceneElementsIncludingDeleted | () => ExcalidrawElement[] | Returns all the elements including the deleted in the scene | -| getSceneElements | () => ExcalidrawElement[] | Returns all the elements excluding the deleted in the scene | -| getAppState | () => AppState | Returns current appState | -| history | `{ clear: () => void }` | This is the history API. `history.clear()` will clear the history | -| scrollToContent | (target?: ExcalidrawElement | ExcalidrawElement[]) => void | Scroll the nearest element out of the elements supplied to the center. Defaults to the elements on the scene. | -| refresh | `() => void` | Updates the offsets for the Excalidraw component so that the coordinates are computed correctly (for example the cursor position). You don't have to call this when the position is changed on page scroll or when the excalidraw container resizes (we handle that ourselves). For any other cases if the position of excalidraw is updated (example due to scroll on parent container and not page scroll) you should call this API. | -| [importLibrary](#importlibrary) | (url: string, token?: string) => void | Imports library from given URL | -| [setToast](#setToast) | ({ message: string, closable?:boolean, duration?:number } | null) => void | This API can be used to show the toast with custom message. | -| [id](#id) | string | Unique ID for the excalidraw component. | -| [getFiles](#getFiles) | () => files | This API can be used to get the files present in the scene. It may contain files that aren't referenced by any element, so if you're persisting the files to a storage, you should compare them against stored elements. | -| [setActiveTool](#setActiveTool) | (tool: { type: typeof SHAPES [number]["value"]| "eraser" } | { type: "custom"; customType: string }) => void | This API can be used to set the active tool | -| [setCursor](#setCursor) | (cursor: string) => void | This API can be used to set customise the mouse cursor on the canvas | -| [resetCursor](#resetCursor) | () => void | This API can be used to reset to default mouse cursor on the canvas | -| [toggleMenu](#toggleMenu) | (type: string, force?: boolean) => boolean | Toggles specific menus on/off | - -#### `readyPromise` - -
-const excalidrawRef = { current: { readyPromise: resolvablePromise}}
-
- -Since plain object is passed as a `ref`, the `readyPromise` is resolved as soon as the component is mounted. Most of the time you will not need this unless you have a specific use case where you can't pass the `ref` in the react way and want to do some action on the host when this promise resolves. You can check the [example](https://codesandbox.io/s/eexcalidraw-resolvable-promise-d0qg3?file=/src/App.js) for the usage. - -### `updateScene` - -
-(scene: sceneData) => void
-
- -You can use this function to update the scene with the sceneData. It accepts the below attributes. - -| Name | Type | Description | -| --- | --- | --- | -| `elements` | [`ImportedDataState["elements"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L17) | The `elements` to be updated in the scene | -| `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L18) | The `appState` to be updated in the scene. | -| `collaborators` |
MapCollaborator>
| The list of collaborators to be updated in the scene. | -| `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. | -| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)> | ((currentItems: [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)>) => [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)>) | The `libraryItems` to be update in the scene. | - -### `updateLibrary` - -
-(opts: {
-  libraryItems: LibraryItemsSource;
-  merge?: boolean;
-  prompt?: boolean;
-  openLibraryMenu?: boolean;
-  defaultStatus?: "unpublished" | "published";
-}) => Promise<LibraryItems>
-
- -You can use this function to update the library. It accepts the below attributes. - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `libraryItems` | | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L224) | The `libraryItems` to be replaced/merged with current library | -| `merge` | boolean | `false` | Whether to merge with existing library items. | -| `prompt` | boolean | `false` | Whether to prompt user for confirmation. | -| `openLibraryMenu` | boolean | `false` | Whether to open the library menu before importing. | -| `defaultStatus` | "unpublished" | "published" | `"unpublished"` | Default library item's `status` if not present. | - -### `addFiles` - -
(files: BinaryFileData) => void 
- -Adds supplied files data to the `appState.files` cache on top of existing files present in the cache. - -#### `isCollaborating` - -This prop indicates if the app is in collaboration mode. - -#### `onPointerUpdate` - -This callback is triggered when mouse pointer is updated. - -```js -({ x, y }, button, pointersMap}) => void; -``` - -1.`{x, y}`: Pointer coordinates - -2.`button`: The position of the button. This will be one of `["down", "up"]` - -3.`pointersMap`: [`pointers map`](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L131) of the scene - -```js -(exportedElements, appState, canvas) => void -``` - -1. `exportedElements`: An array of [non deleted elements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L87) which needs to be exported. -2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79) of the scene. -3. `canvas`: The `HTMLCanvasElement` of the scene. - -#### `langCode` - -Determines the language of the UI. It should be one of the [available language codes](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L14). Defaults to `en` (English). We also export default language and supported languages which you can import as shown below. - -```js -import { defaultLang, languages } from "@excalidraw/excalidraw"; -``` - -| name | type | -| --- | --- | -| defaultLang | string | -| languages | [Language[]](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L8) | - -#### `renderTopRightUI` - -
-(isMobile: boolean, appState: AppState) => JSX | null
-
- -A function returning JSX to render custom UI in the top right corner of the app. - -#### `renderCustomStats` - -A function that can be used to render custom stats (returns JSX) in the nerd stats dialog. For example you can use this prop to render the size of the elements in the storage. - -#### `renderSidebar` - -
-() => JSX | null
-
- -Optional function that can render custom sidebar. This sidebar is the same that the library menu sidebar is using, and can be used for any purposes your app needs. The render function should return a `` instance — a component that is exported from the Excalidraw package. It accepts `children` which can be any content you like to render inside. - -The `` component takes these props (all are optional except `children`): - -| name | type | description | -| --- | --- | --- | -| className | string | -| children |
React.ReactNode
| Content you want to render inside the sidebar. | -| onClose |
() => void
| Invoked when the component is closed (by user, or the editor). No need to act on this event, as the editor manages the sidebar open state on its own. | -| onDock |
(isDocked: boolean) => void
| Invoked when the user toggles the dock button. | -| docked | boolean | Indicates whether the sidebar is docked. By default, the sidebar is undocked. If passed, the docking becomes controlled, and you are responsible for updating the `docked` state by listening on `onDock` callback. See [here](#dockedSidebarBreakpoint) for more info docking. | -| dockable | boolean | Indicates whether the sidebar can be docked by user (=the dock button is shown). If `false`, you can still dock programmatically by passing `docked=true` | - -The sidebar will always include a header with close/dock buttons (when applicable). - -You can also add custom content to the header, by rendering `` as a child of the `` component. Note that the custom header will still include the default buttons. - -The `` component takes these props children (all are optional): - -| name | type | description | -| --- | --- | --- | -| className | string | -| children |
React.ReactNode
| Content you want to render inside the sidebar header, sibling of the header buttons. | - -For example code, see the example [`App.tsx`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/example/App.tsx#L524) file. - -#### `viewModeEnabled` - -This prop indicates whether the app is in `view mode`. When supplied, the value takes precedence over `intialData.appState.viewModeEnabled`, the `view mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app. - -#### `zenModeEnabled` - -This prop indicates whether the app is in `zen mode`. When supplied, the value takes precedence over `intialData.appState.zenModeEnabled`, the `zen mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app. - -#### `gridModeEnabled` - -This prop indicates whether the shows the grid. When supplied, the value takes precedence over `intialData.appState.gridModeEnabled`, the grid will be fully controlled by the host app, and users won't be able to toggle it from within the app. - -#### `libraryReturnUrl` - -If supplied, this URL will be used when user tries to install a library from [libraries.excalidraw.com](https://libraries.excalidraw.com). Defaults to `window.location.origin + window.location.pathname`. To install the libraries in the same tab from which it was opened, you need to set `window.name` (to any alphanumeric string) — if it's not set it will open in a new tab. - -#### `theme` - -This prop controls Excalidraw's theme. When supplied, the value takes precedence over `intialData.appState.theme`, the theme will be fully controlled by the host app, and users won't be able to toggle it from within the app unless `UIOptions.canvasActions.toggleTheme` is set to `true`, in which case the `theme` prop will control Excalidraw's default theme with ability to allow theme switching (you must take care of updating the `theme` prop when you detect a change to `appState.theme` from the [onChange](#onChange) callback). - -You can use [`THEME`](#THEME-1) to specify the theme. - -#### `name` - -This prop sets the name of the drawing which will be used when exporting the drawing. When supplied, the value takes precedence over `intialData.appState.name`, the `name` will be fully controlled by host app and the users won't be able to edit from within Excalidraw. - -#### `UIOptions` - -This prop can be used to customise UI of Excalidraw. Currently we support customising [`canvasActions`](#canvasActions) and [`dockedSidebarBreakpoint`](dockedSidebarBreakpoint). It accepts the below parameters - -
-{ canvasActions:  CanvasActions }
-
- -##### canvasActions - -| Attribute | Type | Default | Description | -| --- | --- | --- | --- | -| `changeViewBackgroundColor` | boolean | true | Implies whether to show `Background color picker` | -| `clearCanvas` | boolean | true | Implies whether to show `Clear canvas button` | -| `export` | false | [exportOpts](#exportOpts) |
{ saveFileToDisk: true }
| This prop allows to customize the UI inside the export dialog. By default it shows the "saveFileToDisk". If this prop is `false` the export button will not be rendered. For more details visit [`exportOpts`](#exportOpts). | -| `loadScene` | boolean | true | Implies whether to show `Load button` | -| `saveToActiveFile` | boolean | true | Implies whether to show `Save button` to save to current file | -| `toggleTheme` | boolean | null | null | Implies whether to show `Theme toggle`. When defined as `boolean`, takes precedence over [`props.theme`](#theme) to show `Theme toggle` | -| `saveAsImage` | boolean | true | Implies whether to show `Save as image button` | - -##### `dockedSidebarBreakpoint` - -This prop indicates at what point should we break to a docked, permanent sidebar. If not passed it defaults to [`MQ_RIGHT_SIDEBAR_MAX_WIDTH_PORTRAIT`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L167). If the `width` of the `excalidraw` container exceeds `dockedSidebarBreakpoint`, the sidebar will be dockable. If user choses to dock the sidebar, it will push the right part of the UI towards the left, making space for the sidebar as shown below. - -![image](https://user-images.githubusercontent.com/11256141/174664866-c698c3fa-197b-43ff-956c-d79852c7b326.png) - -#### `exportOpts` - -The below attributes can be set in `UIOptions.canvasActions.export` to customize the export dialog. If `UIOptions.canvasActions.export` is `false` the export button will not be rendered. - -| Attribute | Type | Default | Description | -| --- | --- | --- | --- | -| `saveFileToDisk` | boolean | true | Implies if save file to disk button should be shown | -| `onExportToBackend` |
 (exportedElements: readonly NonDeletedExcalidrawElement[],appState: AppState,canvas: HTMLCanvasElement | null) => void 
| | This callback is triggered when the shareable-link button is clicked in the export dialog. The link button will only be shown if this callback is passed. | -| `renderCustomUI` |
 (exportedElements: readonly NonDeletedExcalidrawElement[],appState: AppState,canvas: HTMLCanvasElement | null) => void 
| | This callback should be supplied if you want to render custom UI in the export dialog. | - -#### `onPaste` - -This callback is triggered if passed when something is pasted into the scene. You can use this callback in case you want to do something additional when the paste event occurs. - -
-(data: ClipboardData, event: ClipboardEvent | null) => boolean
-
- -This callback must return a `boolean` value or a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) which resolves to a boolean value. - -In case you want to prevent the excalidraw paste action you must return `false`, it will stop the native excalidraw clipboard management flow (nothing will be pasted into the scene). - -#### `importLibrary` - -Imports library from given URL. You should call this on `hashchange`, passing the `addLibrary` value if you detect it as shown below. Optionally pass a CSRF `token` to skip prompting during installation (retrievable via `token` key from the url coming from [https://libraries.excalidraw.com](https://libraries.excalidraw.com/)). - -```js -useEffect(() => { - const onHashChange = () => { - const hash = new URLSearchParams(window.location.hash.slice(1)); - const libraryUrl = hash.get("addLibrary"); - if (libraryUrl) { - excalidrawRef.current.importLibrary(libraryUrl, hash.get("token")); - } - }; - window.addEventListener("hashchange", onHashChange, false); - return () => { - window.removeEventListener("hashchange", onHashChange); - }; -}, []); -``` - -Try out the [Demo](#Demo) to see it in action. - -#### `setToast` - -This API can be used to show the toast with custom message. - -
-({ message: string,
- closable?:boolean,
- duration?:number } | null) => void
-
- -| Attribute | type | Description | -| --- | --- | --- | -| message | string | The message to be shown on the toast. | -| closable | boolean | Indicates whether to show the closable button on toast to dismiss the toast. | -| duration | number | Determines the duration after which the toast should auto dismiss. To prevent autodimiss you can pass `Infinity`. | - -To dismiss an existing toast you can simple pass `null` - -```js -setToast(null); -``` - -#### `setActiveTool` - -This API has the below signature. It sets the `tool` passed in param as the active tool. - -
-(tool: { type: typeof SHAPES[number]["value"] | "eraser" } | { type: "custom"; customType: string }) => void
-
- -#### `setCursor` - -This API can be used to customise the mouse cursor on the canvas and has the below signature. It sets the mouse cursor to the cursor passed in param. - -
-(cursor: string) => void
-
- -#### `toggleMenu` - -
-(type: "library" | "customSidebar", force?: boolean) => boolean
-
- -This API can be used to toggle a specific menu (currently only the sidebars), and returns whether the menu was toggled on or off. If the `force` flag passed, it will force the menu to be toggled either on/off based on the boolean passed. - -This API is especially useful when you render a custom sidebar using [`renderSidebar`](#renderSidebar) prop, and you want to toggle it from your app based on a user action. - -#### `resetCursor` - -This API can be used to reset to default mouse cursor. - -#### `detectScroll` - -Indicates whether Excalidraw should listen for `scroll` event on the nearest scrollable container in the DOM tree and recompute the coordinates (e.g. to correctly handle the cursor) when the component's position changes. You can disable this when you either know this doesn't affect your app or you want to take care of it yourself (calling the [`refresh()`](#ref) method). - -#### `handleKeyboardGlobally` - -Indicates whether to bind keyboard events to `document`. Disabled by default, meaning the keyboard events are bound to the Excalidraw component. This allows for multiple Excalidraw components to live on the same page, and ensures that Excalidraw keyboard handling doesn't collide with your app's (or the browser) when the component isn't focused. - -Enable this if you want Excalidraw to handle keyboard even if the component isn't focused (e.g. a user is interacting with the navbar, sidebar, or similar). - -#### `onLibraryChange` - -This callback if supplied will get triggered when the library is updated and has the below signature. - -
-(items: LibraryItems) => void | Promise
-
- -It is invoked with empty items when user clears the library. You can use this callback when you want to do something additional when library is updated for example persisting it to local storage. - -#### `id` - -The unique id of the excalidraw component. This can be used to identify the excalidraw component, for example importing the library items to the excalidraw component from where it was initiated when you have multiple excalidraw components rendered on the same page as shown in [multiple excalidraw demo](https://codesandbox.io/s/multiple-excalidraw-k1xx5). - -#### `autoFocus` - -This prop implies whether to focus the Excalidraw component on page load. Defaults to false. - -#### `generateIdForFile` - -Allows you to override `id` generation for files added on canvas (images). By default, an SHA-1 digest of the file is used. - -``` -(file: File) => string | Promise -``` - -#### `onLinkOpen` - -This prop if passed will be triggered when clicked on link. To handle the redirect yourself (such as when using your own router for internal links), you must call `event.preventDefault()`. - -``` -(element: ExcalidrawElement, event: CustomEvent<{ nativeEvent: MouseEvent }>) => void -``` - -Example: - -```ts -const history = useHistory(); - -// open internal links using the app's router, but opens external links in -// a new tab/window -const onLinkOpen: ExcalidrawProps["onLinkOpen"] = useCallback( - (element, event) => { - const link = element.link; - const { nativeEvent } = event.detail; - const isNewTab = nativeEvent.ctrlKey || nativeEvent.metaKey; - const isNewWindow = nativeEvent.shiftKey; - const isInternalLink = - link.startsWith("/") || link.includes(window.location.origin); - if (isInternalLink && !isNewTab && !isNewWindow) { - history.push(link.replace(window.location.origin, "")); - // signal that we're handling the redirect ourselves - event.preventDefault(); - } - }, - [history], -); -``` - -#### `onPointerDown` - -This prop if passed will be triggered on pointer down events and has the below signature. - -
-(activeTool:  AppState["activeTool"], pointerDownState: PointerDownState) => void
-
- -#### `onScrollChange` - -This prop if passed will be triggered when canvas is scrolled and has the below signature. - -```ts -(scrollX: number, scrollY: number) => void -``` - -### Restore utilities - -#### `restoreAppState` - -**_Signature_** - -
-restoreAppState(appState: ImportedDataState["appState"], localAppState: Partial<AppState> | null): AppState
-
- -**_How to use_** - -```js -import { restoreAppState } from "@excalidraw/excalidraw"; -``` - -This function will make sure all the keys have appropriate values in [appState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79) and if any key is missing, it will be set to default value. - -When `localAppState` is supplied, it's used in place of values that are missing (`undefined`) in `appState` instead of defaults. Use this as a way to not override user's defaults if you persist them. Required: supply `null`/`undefined` if not applicable. - -#### `restoreElements` - -**_Signature_** - -
-restoreElements(
-  elements: ImportedDataState["elements"],
-  localElements: ExcalidrawElement[] | null | undefined): ExcalidrawElement[],
-  refreshDimensions: boolean
-)
-
- -**_How to use_** - -```js -import { restoreElements } from "@excalidraw/excalidraw"; -``` - -This function will make sure all properties of element is correctly set and if any attribute is missing, it will be set to default value. - -When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. Use this when you import elements which may already be present in the scene to ensure that you do not disregard the newly imported elements if you're using element version to detect the updates. - -Parameter `refreshDimensions` indicates whether we should also recalculate text element dimensions. Defaults to `false`. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration. - -#### `restore` - -**_Signature_** - -
-restoreElements(
-  data: ImportedDataState,
-  localAppState: Partial<AppState> | null | undefined,
-  localElements: ExcalidrawElement[] | null | undefined): DataState
-)
-
- -See [`restoreAppState()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreAppState) about `localAppState`, and [`restoreElements()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreElements) about `localElements`. - -**_How to use_** - -```js -import { restore } from "@excalidraw/excalidraw"; -``` - -This function makes sure elements and state is set to appropriate values and set to default value if not present. It is a combination of [restoreElements](#restoreElements) and [restoreAppState](#restoreAppState). - -#### `restoreLibraryItems` - -**_Signature_** - -
-restoreLibraryItems(libraryItems: ImportedDataState["libraryItems"], defaultStatus: "published" | "unpublished")
-
- -**_How to use_** - -```js -import { restoreLibraryItems } from "@excalidraw/excalidraw"; - -restoreLibraryItems(libraryItems, "unpublished"); -``` - -This function normalizes library items elements, adding missing values when needed. - -### Export utilities - -#### `exportToCanvas` - -**_Signature_** - -
exportToCanvas({
-  elements,
-  appState
-  getDimensions,
-  files,
-  exportPadding?: number;
-}: ExportOpts
-
- -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types) | | The elements to be exported to canvas | -| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L12) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L11) | The app state of the scene | -| getDimensions | `(width: number, height: number) => { width: number, height: number, scale?: number }` | undefined | A function which returns the `width`, `height`, and optionally `scale` (defaults `1`), with which canvas is to be exported. | -| maxWidthOrHeight | `number` | undefined | The maximum width or height of the exported image. If provided, `getDimensions` is ignored. | -| files | [BinaryFiles](The [`BinaryFiles`](<[BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64)>) | undefined | The files added to the scene. | -| exportPadding | number | 10 | The padding to be added on canvas | - -**How to use** - -```js -import { exportToCanvas } from "@excalidraw/excalidraw"; -``` - -This function returns the canvas with the exported elements, appState and dimensions. - -#### `exportToBlob` - -**_Signature_** - -
-exportToBlob(
-  opts: ExportOpts & {
-  mimeType?: string,
-  quality?: number,
-  exportPadding?: number;
-})
-
- -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| opts | | | This param is passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exportToCanvas) | -| mimeType | string | "image/png" | Indicates the image format | -| quality | number | 0.92 | A value between 0 and 1 indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg`/`image/webp` MIME types. | -| exportPadding | number | 10 | The padding to be added on canvas | - -**How to use** - -```js -import { exportToBlob } from "@excalidraw/excalidraw"; -``` - -Returns a promise which resolves with a [blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob). It internally uses [canvas.ToBlob](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob). - -#### `exportToSvg` - -**_Signature_** - -
-exportToSvg({
-  elements: ExcalidrawElement[],
-  appState: AppState,
-  exportPadding?: number,
-  metadata?: string,
-  files?: BinaryFiles
-})
-
- -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106) | | The elements to exported as svg | -| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L11) | The app state of the scene | -| exportPadding | number | 10 | The padding to be added on canvas | -| files | [BinaryFiles](The [`BinaryFiles`](<[BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64)>) | undefined | The files added to the scene. | - -This function returns a promise which resolves to svg of the exported drawing. - -#### `exportToClipboard` - -**_Signature_** - -
-exportToClipboard(
-  opts: ExportOpts & {
-  mimeType?: string,
-  quality?: number;
-  type: 'png' | 'svg' |'json'
-})
-
- -| Name | Type | Default | Description | -| --- | --- | --- | --- | --- | --- | -| opts | | | This param is same as the params passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exportToCanvas). | -| mimeType | string | "image/png" | Indicates the image format, this will be used when exporting as `png`. | -| quality | number | 0.92 | A value between 0 and 1 indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg`/`image/webp` MIME types. This will be used when exporting as `png`. | -| type | 'png' | 'svg' | 'json' | | This determines the format to which the scene data should be exported. | - -**How to use** - -```js -import { exportToClipboard } from "@excalidraw/excalidraw"; -``` - -Copies the scene data in the specified format (determined by `type`) to clipboard. - -##### Additional attributes of appState for `export\*` APIs - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| exportBackground | boolean | true | Indicates whether background should be exported | -| viewBackgroundColor | string | #fff | The default background color | -| exportWithDarkMode | boolean | false | Indicates whether to export with dark mode | -| exportEmbedScene | boolean | false | Indicates whether scene data should be embedded in svg/png. This will increase the image size. | - -### Extra API's - -#### `serializeAsJSON` - -**_Signature_** - -
-serializeAsJSON({
-  elements: ExcalidrawElement[],
-  appState: AppState,
-}): string
-
- -Takes the scene elements and state and returns a JSON string. Deleted `elements`as well as most properties from `AppState` are removed from the resulting JSON. (see [`serializeAsJSON()`](https://github.com/excalidraw/excalidraw/blob/master/src/data/json.ts#L16) source for details). - -If you want to overwrite the source field in the JSON string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value. - -#### `serializeLibraryAsJSON` - -**_Signature_** - -
-serializeLibraryAsJSON({
-  libraryItems: LibraryItems[],
-
- -Takes the library items and returns a JSON string. - -If you want to overwrite the source field in the JSON string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value. - -#### `getSceneVersion` - -**How to use** - -
-import { getSceneVersion } from "@excalidraw/excalidraw";
-getSceneVersion(elements:  ExcalidrawElement[])
-
- -This function returns the current scene version. - -#### `isInvisiblySmallElement` - -**_Signature_** - -
-isInvisiblySmallElement(element:  ExcalidrawElement): boolean
-
- -**How to use** - -```js -import { isInvisiblySmallElement } from "@excalidraw/excalidraw"; -``` - -Returns `true` if element is invisibly small (e.g. width & height are zero). - -#### `loadLibraryFromBlob` - -```js -import { loadLibraryFromBlob } from "@excalidraw/excalidraw"; -``` - -**_Signature_** - -
-loadLibraryFromBlob(blob: Blob, defaultStatus: "published" | "unpublished")
-
- -This function loads the library from the blob. Additonally takes `defaultStatus` param which sets the default status for library item if not present, defaults to `unpublished`. - -#### `loadFromBlob` - -**How to use** - -```js -import { loadFromBlob } from "@excalidraw/excalidraw"; - -const scene = await loadFromBlob(file, null, null); -excalidrawAPI.updateScene(scene); -``` - -**Signature** - -
-loadFromBlob(
-  blob: Blob,
-  localAppState: AppState | null,
-  localElements: ExcalidrawElement[] | null,
-  fileHandle?: FileSystemHandle | null
-) => Promise<RestoredDataState>
-
- -This function loads the scene data from the blob (or file). If you pass `localAppState`, `localAppState` value will be preferred over the `appState` derived from `blob`. Throws if blob doesn't contain valid scene data. - -#### `loadSceneOrLibraryFromBlob` - -**How to use** - -```js -import { loadSceneOrLibraryFromBlob, MIME_TYPES } from "@excalidraw/excalidraw"; - -const contents = await loadSceneOrLibraryFromBlob(file, null, null); -if (contents.type === MIME_TYPES.excalidraw) { - excalidrawAPI.updateScene(contents.data); -} else if (contents.type === MIME_TYPES.excalidrawlib) { - excalidrawAPI.updateLibrary(contents.data); -} -``` - -**Signature** - -
-loadSceneOrLibraryFromBlob(
-  blob: Blob,
-  localAppState: AppState | null,
-  localElements: ExcalidrawElement[] | null,
-  fileHandle?: FileSystemHandle | null
-) => Promise<{ type: string, data: RestoredDataState | ImportedLibraryState}>
-
- -This function loads either scene or library data from the supplied blob. If the blob contains scene data, and you pass `localAppState`, `localAppState` value will be preferred over the `appState` derived from `blob`. Throws if blob doesn't contain neither valid scene data or library data. - -#### `getFreeDrawSvgPath` - -**How to use** - -```js -import { getFreeDrawSvgPath } from "@excalidraw/excalidraw"; -``` - -**Signature** - -
-getFreeDrawSvgPath(element: ExcalidrawFreeDrawElement
-
- -This function returns the free draw svg path for the element. - -#### `isLinearElement` - -**How to use** - -```js -import { isLinearElement } from "@excalidraw/excalidraw"; -``` - -**Signature** - -
-isLinearElement(elementType?: ExcalidrawElement): boolean
-
- -This function returns true if the element is linear type (`arrow` |`line`) else returns false. - -#### `getNonDeletedElements` - -**How to use** - -```js -import { getNonDeletedElements } from "@excalidraw/excalidraw"; -``` - -**Signature** - -
-getNonDeletedElements(elements:  readonly ExcalidrawElement[]): as readonly NonDeletedExcalidrawElement[]
-
- -This function returns an array of deleted elements. - -#### `mergeLibraryItems` - -```js -import { mergeLibraryItems } from "@excalidraw/excalidraw"; -``` - -**_Signature_** - -
-mergeLibraryItems(localItems: LibraryItems, otherItems: LibraryItems) => LibraryItems
-
- -This function merges two `LibraryItems` arrays, where unique items from `otherItems` are sorted first in the returned array. - -#### `parseLibraryTokensFromUrl` - -**How to use** - -```js -import { parseLibraryTokensFromUrl } from "@excalidraw/excalidraw"; -``` - -**Signature** - -
-parseLibraryTokensFromUrl(): {
-    libraryUrl: string;
-    idToken: string | null;
-} | null
-
- -Parses library parameters from URL if present (expects the `#addLibrary` hash key), and returns an object with the `libraryUrl` and `idToken`. Returns `null` if `#addLibrary` hash key not found. - -#### `useHandleLibrary` - -**How to use** - -```js -import { useHandleLibrary } from "@excalidraw/excalidraw"; - -export const App = () => { - // ... - useHandleLibrary({ excalidrawAPI }); -}; -``` - -**Signature** - -
-useHandleLibrary(opts: {
-  excalidrawAPI: ExcalidrawAPI,
-  getInitialLibraryItems?: () => LibraryItemsSource
-});
-
- -A hook that automatically imports library from url if `#addLibrary` hash key exists on initial load, or when it changes during the editing session (e.g. when a user installs a new library), and handles initial library load if `getInitialLibraryItems` getter is supplied. - -In the future, we will be adding support for handling library persistence to browser storage (or elsewhere). - -#### `sceneCoordsToViewportCoords` - -```js -import { sceneCoordsToViewportCoords } from "@excalidraw/excalidraw"; -``` - -**_Signature_** - -
-sceneCoordsToViewportCoords({sceneX: number, sceneY: number}, appState: AppState): {x: number, y: number}
-
- -This function returns equivalent viewport coords for the provided scene coords in params. - -#### `viewportCoordsToSceneCoords` - -```js -import { viewportCoordsToSceneCoords } from "@excalidraw/excalidraw"; -``` - -**_Signature_** - -
-viewportCoordsToSceneCoords({clientX: number, clientY: number}, appState: AppState): {x: number, y: number}
-
- -This function returns equivalent scene coords for the provided viewport coords in params. - -#### useDevice - -This hook can be used to check the type of device which is being used. It can only be used inside the `children` of `Excalidraw` component - -```js -import { useDevice, Footer } from "@excalidraw/excalidraw"; - -const MobileFooter = () => { - const device = useDevice(); - if (device.isMobile) { - return ( -
- -
- ); - } - return null; - -}; -const App = () => { - - - Item1 - Item 2 - - - -} - -``` - -The `device` has the following `attributes` - -| Name | Type | Description | -| --- | --- | --- | -| `isSmScreen` | `boolean` | Set to `true` when the device small screen is small (Width < `640px` ) | -| `isMobile` | `boolean` | Set to `true` when the device is `mobile` | -| `isTouchScreen` | `boolean` | Set to `true` for `touch` devices | -| `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` | - -### Exported constants - -#### `FONT_FAMILY` - -**How to use** - -```js -import { FONT_FAMILY } from "@excalidraw/excalidraw"; -``` - -`FONT_FAMILY` contains all the font families used in `Excalidraw` as explained below - -| Font Family | Description | -| ----------- | -------------------- | -| Virgil | The handwritten font | -| Helvetica | The Normal Font | -| Cascadia | The Code Font | - -Defaults to `FONT_FAMILY.Virgil` unless passed in `initialData.appState.currentItemFontFamily`. - -#### `THEME` - -**How to use** - -```js -import { THEME } from "@excalidraw/excalidraw"; -``` - -`THEME` contains all the themes supported by `Excalidraw` as explained below - -| Theme | Description | -| ----- | --------------- | -| LIGHT | The light theme | -| DARK | The Dark theme | - -Defaults to `THEME.LIGHT` unless passed in `initialData.appState.theme` - -### `MIME_TYPES` - -**How to use ** - -```js -import { MIME_TYPES } from "@excalidraw/excalidraw"; -``` - -[`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L92) contains all the mime types supported by `Excalidraw`. - -## Need help? - -Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). - -### Development - -#### Install the dependencies - -```bash -yarn -``` - -#### Start the server - -```bash -yarn start -``` - -[http://localhost:3001](http://localhost:3001) will open in your default browser. - -The example is same as the [codesandbox example](https://ehlz3.csb.app/) - -#### Create a test release - -You can create a test release by posting the below comment in your pull request - -``` -@excalibot trigger release -``` - -Once the version is released `@excalibot` will post a comment with the release version. - -#### Creating a production release - -To release the next stable version follow the below steps - -``` -yarn prerelease version -``` - -You need to pass the `version` for which you want to create the release. This will make the changes needed before making the release like updating `package.json`, `changelog` and more. - -The next step is to run the `release` script - -``` -yarn release -``` - -This will publish the package. - -Right now there are two steps to create a production release but once this works fine these scripts will be combined and more automation will be done. +Head over to the [docs](https://docs.excalidraw.com/docs/package/api) diff --git a/src/packages/excalidraw/example/App.scss b/src/packages/excalidraw/example/App.scss index a2a70da24..7f37540d8 100644 --- a/src/packages/excalidraw/example/App.scss +++ b/src/packages/excalidraw/example/App.scss @@ -66,11 +66,18 @@ button.custom-element { width: 2rem; height: 2rem; - margin: 0 8px; } .custom-footer, .custom-element { padding: 0.1rem; + margin: 0 8px; + } + .layer-ui__wrapper__footer.App-menu_bottom { + align-items: stretch; + } + // till its merged in OSS + .App-toolbar-container .mobile-misc-tools-container { + position: absolute; } } diff --git a/src/packages/excalidraw/example/App.tsx b/src/packages/excalidraw/example/App.tsx index 21f91ecd4..e394d1a12 100644 --- a/src/packages/excalidraw/example/App.tsx +++ b/src/packages/excalidraw/example/App.tsx @@ -71,6 +71,7 @@ const { restoreElements, Sidebar, Footer, + WelcomeScreen, MainMenu, LiveCollaborationTrigger, } = window.ExcalidrawLib; @@ -704,6 +705,7 @@ export default function App() { )} + {renderMenu()} {Object.keys(commentIcons || []).length > 0 && renderCommentIcons()} diff --git a/src/packages/utils/README.md b/src/packages/utils/README.md index 033378541..a6e4eabc2 100644 --- a/src/packages/utils/README.md +++ b/src/packages/utils/README.md @@ -28,7 +28,7 @@ Export an Excalidraw diagram to a [SVGElement](https://developer.mozilla.org/en- ## Usage -Excalidraw utils is published as a UMD (Universal Module Definition). If you are using a Web bundler (for instance, Webpack), you can import it as an ES6 module: +Excalidraw utils is published as a UMD (Universal Module Definition). If you are using a module bundler (for instance, Webpack), you can import it as an ES6 module: ```js import { exportToSvg, exportToBlob } from "@excalidraw/utils"; From eb9eeefc63d1702629e2515b510fbf2efb8c076a Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Wed, 1 Feb 2023 20:27:31 +0530 Subject: [PATCH 159/276] fix: edit link in docs (#6182) --- dev-docs/docusaurus.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dev-docs/docusaurus.config.js b/dev-docs/docusaurus.config.js index 1c69e6dcb..e9d72f3db 100644 --- a/dev-docs/docusaurus.config.js +++ b/dev-docs/docusaurus.config.js @@ -30,7 +30,8 @@ const config = { docs: { sidebarPath: require.resolve("./sidebars.js"), // Please change this to your repo. - editUrl: "https://github.com/excalidraw/docs/tree/master/", + editUrl: + "https://github.com/excalidraw/excalidraw/tree/master/dev-docs/", }, theme: { customCss: [ From d8a4ca6911b79ac6a72fd934863dcc3aa9e8219e Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Wed, 1 Feb 2023 21:09:23 +0530 Subject: [PATCH 160/276] docs: show last updated time and author (#6183) docs:show last updated time and author --- dev-docs/docusaurus.config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev-docs/docusaurus.config.js b/dev-docs/docusaurus.config.js index e9d72f3db..e24901f2e 100644 --- a/dev-docs/docusaurus.config.js +++ b/dev-docs/docusaurus.config.js @@ -32,6 +32,8 @@ const config = { // Please change this to your repo. editUrl: "https://github.com/excalidraw/excalidraw/tree/master/dev-docs/", + showLastUpdateAuthor: true, + showLastUpdateTime: true, }, theme: { customCss: [ From 5a0334f37f354af0bd9562190f5210cfa8dd092c Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Thu, 2 Feb 2023 12:58:45 +0530 Subject: [PATCH 161/276] fix: hide welcome screen on mobile once user interacts (#6185) * fix: hide welcome screen on mobile once started drawing * Add specs --- src/components/LayerUI.tsx | 1 + src/components/MobileMenu.tsx | 4 +- src/tests/MobileMenu.test.tsx | 45 ++++ .../__snapshots__/MobileMenu.test.tsx.snap | 240 ++++++++++++++++++ 4 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 src/tests/MobileMenu.test.tsx create mode 100644 src/tests/__snapshots__/MobileMenu.test.tsx.snap diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 161512001..b7d765a8a 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -409,6 +409,7 @@ const LayerUI = ({ renderCustomStats={renderCustomStats} renderSidebars={renderSidebars} device={device} + renderWelcomeScreen={renderWelcomeScreen} /> )} diff --git a/src/components/MobileMenu.tsx b/src/components/MobileMenu.tsx index 42a5bda40..75e08867c 100644 --- a/src/components/MobileMenu.tsx +++ b/src/components/MobileMenu.tsx @@ -41,6 +41,7 @@ type MobileMenuProps = { renderCustomStats?: ExcalidrawProps["renderCustomStats"]; renderSidebars: () => JSX.Element | null; device: Device; + renderWelcomeScreen: boolean; }; export const MobileMenu = ({ @@ -57,12 +58,13 @@ export const MobileMenu = ({ renderCustomStats, renderSidebars, device, + renderWelcomeScreen, }: MobileMenuProps) => { const { welcomeScreenCenterTunnel, mainMenuTunnel } = useTunnels(); const renderToolbar = () => { return ( - + {renderWelcomeScreen && }
{(heading: React.ReactNode) => ( diff --git a/src/tests/MobileMenu.test.tsx b/src/tests/MobileMenu.test.tsx new file mode 100644 index 000000000..41d5d0169 --- /dev/null +++ b/src/tests/MobileMenu.test.tsx @@ -0,0 +1,45 @@ +import ExcalidrawApp from "../excalidraw-app"; +import { + mockBoundingClientRect, + render, + restoreOriginalGetBoundingClientRect, +} from "./test-utils"; + +import { UI } from "./helpers/ui"; + +describe("Test MobileMenu", () => { + const { h } = window; + const dimensions = { height: 400, width: 800 }; + + beforeEach(async () => { + await render(); + //@ts-ignore + h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!); + }); + + beforeAll(() => { + mockBoundingClientRect(dimensions); + }); + + afterAll(() => { + restoreOriginalGetBoundingClientRect(); + }); + + it("should set device correctly", () => { + expect(h.app.device).toMatchInlineSnapshot(` + Object { + "canDeviceFitSidebar": false, + "isMobile": true, + "isSmScreen": false, + "isTouchScreen": false, + } + `); + }); + + it("should initialize with welcome screen and hide once user interacts", async () => { + expect(document.querySelector(".welcome-screen-center")).toMatchSnapshot(); + + UI.clickTool("rectangle"); + expect(document.querySelector(".welcome-screen-center")).toBeNull(); + }); +}); diff --git a/src/tests/__snapshots__/MobileMenu.test.tsx.snap b/src/tests/__snapshots__/MobileMenu.test.tsx.snap new file mode 100644 index 000000000..f3e55fd99 --- /dev/null +++ b/src/tests/__snapshots__/MobileMenu.test.tsx.snap @@ -0,0 +1,240 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test MobileMenu should initialize with welcome screen and hide once user interacts 1`] = ` +
+ +
+ All your data is saved locally in your browser. +
+
+ + + + +
+ +
+
+ Try Excalidraw Plus! +
+
+
+
+`; From a9c5bdb8785be82273761070a270f82cebbe2d44 Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Thu, 2 Feb 2023 16:23:39 +0800 Subject: [PATCH 162/276] fix: sort bound text elements to fix text duplication z-index error (#5130) * fix: sort bound text elements to fix text duplication z-index error * improve & sort groups & add tests * fix backtracking and discontiguous groups --------- Co-authored-by: dwelle --- src/actions/actionDuplicateSelection.tsx | 121 +++++- src/element/sortElements.test.ts | 402 ++++++++++++++++++++ src/element/sortElements.ts | 123 ++++++ src/element/textElement.ts | 14 +- src/excalidraw-app/collab/reconciliation.ts | 52 +-- src/tests/zindex.test.tsx | 159 +++++--- src/utils.ts | 8 + 7 files changed, 761 insertions(+), 118 deletions(-) create mode 100644 src/element/sortElements.test.ts create mode 100644 src/element/sortElements.ts diff --git a/src/actions/actionDuplicateSelection.tsx b/src/actions/actionDuplicateSelection.tsx index fcff03f51..e3e5fb85a 100644 --- a/src/actions/actionDuplicateSelection.tsx +++ b/src/actions/actionDuplicateSelection.tsx @@ -16,8 +16,12 @@ import { AppState } from "../types"; import { fixBindingsAfterDuplication } from "../element/binding"; import { ActionResult } from "./types"; import { GRID_SIZE } from "../constants"; -import { bindTextToShapeAfterDuplication } from "../element/textElement"; +import { + bindTextToShapeAfterDuplication, + getBoundTextElement, +} from "../element/textElement"; import { isBoundToContainer } from "../element/typeChecks"; +import { normalizeElementOrder } from "../element/sortElements"; import { DuplicateIcon } from "../components/icons"; export const actionDuplicateSelection = register({ @@ -64,6 +68,11 @@ const duplicateElements = ( elements: readonly ExcalidrawElement[], appState: AppState, ): Partial => { + // --------------------------------------------------------------------------- + + // step (1) + + const sortedElements = normalizeElementOrder(elements); const groupIdMap = new Map(); const newElements: ExcalidrawElement[] = []; const oldElements: ExcalidrawElement[] = []; @@ -85,42 +94,112 @@ const duplicateElements = ( return newElement; }; - const finalElements: ExcalidrawElement[] = []; - - let index = 0; const selectedElementIds = arrayToMap( - getSelectedElements(elements, appState, true), + getSelectedElements(sortedElements, appState, true), ); - while (index < elements.length) { - const element = elements[index]; + + // Ids of elements that have already been processed so we don't push them + // into the array twice if we end up backtracking when retrieving + // discontiguous group of elements (can happen due to a bug, or in edge + // cases such as a group containing deleted elements which were not selected). + // + // This is not enough to prevent duplicates, so we do a second loop afterwards + // to remove them. + // + // For convenience we mark even the newly created ones even though we don't + // loop over them. + const processedIds = new Map(); + + const markAsProcessed = (elements: ExcalidrawElement[]) => { + for (const element of elements) { + processedIds.set(element.id, true); + } + return elements; + }; + + const elementsWithClones: ExcalidrawElement[] = []; + + let index = -1; + + while (++index < sortedElements.length) { + const element = sortedElements[index]; + + if (processedIds.get(element.id)) { + continue; + } + + const boundTextElement = getBoundTextElement(element); if (selectedElementIds.get(element.id)) { - if (element.groupIds.length) { + // if a group or a container/bound-text, duplicate atomically + if (element.groupIds.length || boundTextElement) { const groupId = getSelectedGroupForElement(appState, element); - // if group selected, duplicate it atomically if (groupId) { - const groupElements = getElementsInGroup(elements, groupId); - finalElements.push( - ...groupElements, - ...groupElements.map((element) => - duplicateAndOffsetElement(element), - ), + const groupElements = getElementsInGroup(sortedElements, groupId); + elementsWithClones.push( + ...markAsProcessed([ + ...groupElements, + ...groupElements.map((element) => + duplicateAndOffsetElement(element), + ), + ]), + ); + continue; + } + if (boundTextElement) { + elementsWithClones.push( + ...markAsProcessed([ + element, + boundTextElement, + duplicateAndOffsetElement(element), + duplicateAndOffsetElement(boundTextElement), + ]), ); - index = index + groupElements.length; continue; } } - finalElements.push(element, duplicateAndOffsetElement(element)); + elementsWithClones.push( + ...markAsProcessed([element, duplicateAndOffsetElement(element)]), + ); } else { - finalElements.push(element); + elementsWithClones.push(...markAsProcessed([element])); } - index++; } + + // step (2) + + // second pass to remove duplicates. We loop from the end as it's likelier + // that the last elements are in the correct order (contiguous or otherwise). + // Thus we need to reverse as the last step (3). + + const finalElementsReversed: ExcalidrawElement[] = []; + + const finalElementIds = new Map(); + index = elementsWithClones.length; + + while (--index >= 0) { + const element = elementsWithClones[index]; + if (!finalElementIds.get(element.id)) { + finalElementIds.set(element.id, true); + finalElementsReversed.push(element); + } + } + + // step (3) + + const finalElements = finalElementsReversed.reverse(); + + // --------------------------------------------------------------------------- + bindTextToShapeAfterDuplication( - finalElements, + elementsWithClones, + oldElements, + oldIdToDuplicatedId, + ); + fixBindingsAfterDuplication( + elementsWithClones, oldElements, oldIdToDuplicatedId, ); - fixBindingsAfterDuplication(finalElements, oldElements, oldIdToDuplicatedId); return { elements: finalElements, diff --git a/src/element/sortElements.test.ts b/src/element/sortElements.test.ts new file mode 100644 index 000000000..35cf560ef --- /dev/null +++ b/src/element/sortElements.test.ts @@ -0,0 +1,402 @@ +import { API } from "../tests/helpers/api"; +import { mutateElement } from "./mutateElement"; +import { normalizeElementOrder } from "./sortElements"; +import { ExcalidrawElement } from "./types"; + +const assertOrder = ( + elements: readonly ExcalidrawElement[], + expectedOrder: string[], +) => { + const actualOrder = elements.map((element) => element.id); + expect(actualOrder).toEqual(expectedOrder); +}; + +describe("normalizeElementsOrder", () => { + it("sort bound-text elements", () => { + const container = API.createElement({ + id: "container", + type: "rectangle", + }); + const boundText = API.createElement({ + id: "boundText", + type: "text", + containerId: container.id, + }); + const otherElement = API.createElement({ + id: "otherElement", + type: "rectangle", + boundElements: [], + }); + const otherElement2 = API.createElement({ + id: "otherElement2", + type: "rectangle", + boundElements: [], + }); + + mutateElement(container, { + boundElements: [{ type: "text", id: boundText.id }], + }); + + assertOrder(normalizeElementOrder([container, boundText]), [ + "container", + "boundText", + ]); + assertOrder(normalizeElementOrder([boundText, container]), [ + "container", + "boundText", + ]); + assertOrder( + normalizeElementOrder([ + boundText, + container, + otherElement, + otherElement2, + ]), + ["container", "boundText", "otherElement", "otherElement2"], + ); + assertOrder(normalizeElementOrder([container, otherElement, boundText]), [ + "container", + "boundText", + "otherElement", + ]); + assertOrder( + normalizeElementOrder([ + container, + otherElement, + otherElement2, + boundText, + ]), + ["container", "boundText", "otherElement", "otherElement2"], + ); + + assertOrder( + normalizeElementOrder([ + boundText, + otherElement, + container, + otherElement2, + ]), + ["otherElement", "container", "boundText", "otherElement2"], + ); + + // noop + assertOrder( + normalizeElementOrder([ + otherElement, + container, + boundText, + otherElement2, + ]), + ["otherElement", "container", "boundText", "otherElement2"], + ); + + // text has existing containerId, but container doesn't list is + // as a boundElement + assertOrder( + normalizeElementOrder([ + API.createElement({ + id: "boundText", + type: "text", + containerId: "container", + }), + API.createElement({ + id: "container", + type: "rectangle", + }), + ]), + ["boundText", "container"], + ); + assertOrder( + normalizeElementOrder([ + API.createElement({ + id: "boundText", + type: "text", + containerId: "container", + }), + ]), + ["boundText"], + ); + assertOrder( + normalizeElementOrder([ + API.createElement({ + id: "container", + type: "rectangle", + boundElements: [], + }), + ]), + ["container"], + ); + assertOrder( + normalizeElementOrder([ + API.createElement({ + id: "container", + type: "rectangle", + boundElements: [{ id: "x", type: "text" }], + }), + ]), + ["container"], + ); + assertOrder( + normalizeElementOrder([ + API.createElement({ + id: "arrow", + type: "arrow", + }), + API.createElement({ + id: "container", + type: "rectangle", + boundElements: [{ id: "arrow", type: "arrow" }], + }), + ]), + ["arrow", "container"], + ); + }); + + it("normalize group order", () => { + assertOrder( + normalizeElementOrder([ + API.createElement({ + id: "A_rect1", + type: "rectangle", + groupIds: ["A"], + }), + API.createElement({ + id: "rect2", + type: "rectangle", + }), + API.createElement({ + id: "rect3", + type: "rectangle", + }), + API.createElement({ + id: "A_rect4", + type: "rectangle", + groupIds: ["A"], + }), + API.createElement({ + id: "A_rect5", + type: "rectangle", + groupIds: ["A"], + }), + API.createElement({ + id: "rect6", + type: "rectangle", + }), + API.createElement({ + id: "A_rect7", + type: "rectangle", + groupIds: ["A"], + }), + ]), + ["A_rect1", "A_rect4", "A_rect5", "A_rect7", "rect2", "rect3", "rect6"], + ); + assertOrder( + normalizeElementOrder([ + API.createElement({ + id: "A_rect1", + type: "rectangle", + groupIds: ["A"], + }), + API.createElement({ + id: "rect2", + type: "rectangle", + }), + API.createElement({ + id: "B_rect3", + type: "rectangle", + groupIds: ["B"], + }), + API.createElement({ + id: "A_rect4", + type: "rectangle", + groupIds: ["A"], + }), + API.createElement({ + id: "B_rect5", + type: "rectangle", + groupIds: ["B"], + }), + API.createElement({ + id: "rect6", + type: "rectangle", + }), + API.createElement({ + id: "A_rect7", + type: "rectangle", + groupIds: ["A"], + }), + ]), + ["A_rect1", "A_rect4", "A_rect7", "rect2", "B_rect3", "B_rect5", "rect6"], + ); + // nested groups + assertOrder( + normalizeElementOrder([ + API.createElement({ + id: "A_rect1", + type: "rectangle", + groupIds: ["A"], + }), + API.createElement({ + id: "BA_rect2", + type: "rectangle", + groupIds: ["B", "A"], + }), + ]), + ["A_rect1", "BA_rect2"], + ); + assertOrder( + normalizeElementOrder([ + API.createElement({ + id: "BA_rect1", + type: "rectangle", + groupIds: ["B", "A"], + }), + API.createElement({ + id: "A_rect2", + type: "rectangle", + groupIds: ["A"], + }), + ]), + ["BA_rect1", "A_rect2"], + ); + assertOrder( + normalizeElementOrder([ + API.createElement({ + id: "BA_rect1", + type: "rectangle", + groupIds: ["B", "A"], + }), + API.createElement({ + id: "A_rect2", + type: "rectangle", + groupIds: ["A"], + }), + API.createElement({ + id: "CBA_rect3", + type: "rectangle", + groupIds: ["C", "B", "A"], + }), + API.createElement({ + id: "rect4", + type: "rectangle", + }), + API.createElement({ + id: "A_rect5", + type: "rectangle", + groupIds: ["A"], + }), + API.createElement({ + id: "BA_rect5", + type: "rectangle", + groupIds: ["B", "A"], + }), + API.createElement({ + id: "BA_rect6", + type: "rectangle", + groupIds: ["B", "A"], + }), + API.createElement({ + id: "CBA_rect7", + type: "rectangle", + groupIds: ["C", "B", "A"], + }), + API.createElement({ + id: "X_rect8", + type: "rectangle", + groupIds: ["X"], + }), + API.createElement({ + id: "rect9", + type: "rectangle", + }), + API.createElement({ + id: "YX_rect10", + type: "rectangle", + groupIds: ["Y", "X"], + }), + API.createElement({ + id: "X_rect11", + type: "rectangle", + groupIds: ["X"], + }), + ]), + [ + "BA_rect1", + "BA_rect5", + "BA_rect6", + "A_rect2", + "A_rect5", + "CBA_rect3", + "CBA_rect7", + "rect4", + "X_rect8", + "X_rect11", + "YX_rect10", + "rect9", + ], + ); + }); + + // TODO + it.skip("normalize boundElements array", () => { + const container = API.createElement({ + id: "container", + type: "rectangle", + boundElements: [], + }); + const boundText = API.createElement({ + id: "boundText", + type: "text", + containerId: container.id, + }); + + mutateElement(container, { + boundElements: [ + { type: "text", id: boundText.id }, + { type: "text", id: "xxx" }, + ], + }); + + expect(normalizeElementOrder([container, boundText])).toEqual([ + expect.objectContaining({ + id: container.id, + }), + expect.objectContaining({ id: boundText.id }), + ]); + }); + + // should take around <100ms for 10K iterations (@dwelle's PC 22-05-25) + it.skip("normalizeElementsOrder() perf", () => { + const makeElements = (iterations: number) => { + const elements: ExcalidrawElement[] = []; + while (iterations--) { + const container = API.createElement({ + type: "rectangle", + boundElements: [], + groupIds: ["B", "A"], + }); + const boundText = API.createElement({ + type: "text", + containerId: container.id, + groupIds: ["A"], + }); + const otherElement = API.createElement({ + type: "rectangle", + boundElements: [], + groupIds: ["C", "A"], + }); + mutateElement(container, { + boundElements: [{ type: "text", id: boundText.id }], + }); + + elements.push(boundText, otherElement, container); + } + return elements; + }; + + const elements = makeElements(10000); + const t0 = Date.now(); + normalizeElementOrder(elements); + console.info(`${Date.now() - t0}ms`); + }); +}); diff --git a/src/element/sortElements.ts b/src/element/sortElements.ts new file mode 100644 index 000000000..3c91fc040 --- /dev/null +++ b/src/element/sortElements.ts @@ -0,0 +1,123 @@ +import { arrayToMapWithIndex } from "../utils"; +import { ExcalidrawElement } from "./types"; + +const normalizeGroupElementOrder = (elements: readonly ExcalidrawElement[]) => { + const origElements: ExcalidrawElement[] = elements.slice(); + const sortedElements = new Set(); + + const orderInnerGroups = ( + elements: readonly ExcalidrawElement[], + ): ExcalidrawElement[] => { + const firstGroupSig = elements[0]?.groupIds?.join(""); + const aGroup: ExcalidrawElement[] = [elements[0]]; + const bGroup: ExcalidrawElement[] = []; + for (const element of elements.slice(1)) { + if (element.groupIds?.join("") === firstGroupSig) { + aGroup.push(element); + } else { + bGroup.push(element); + } + } + return bGroup.length ? [...aGroup, ...orderInnerGroups(bGroup)] : aGroup; + }; + + const groupHandledElements = new Map(); + + origElements.forEach((element, idx) => { + if (groupHandledElements.has(element.id)) { + return; + } + if (element.groupIds?.length) { + const topGroup = element.groupIds[element.groupIds.length - 1]; + const groupElements = origElements.slice(idx).filter((element) => { + const ret = element?.groupIds?.some((id) => id === topGroup); + if (ret) { + groupHandledElements.set(element!.id, true); + } + return ret; + }); + + for (const elem of orderInnerGroups(groupElements)) { + sortedElements.add(elem); + } + } else { + sortedElements.add(element); + } + }); + + // if there's a bug which resulted in losing some of the elements, return + // original instead as that's better than losing data + if (sortedElements.size !== elements.length) { + console.error("normalizeGroupElementOrder: lost some elements... bailing!"); + return elements; + } + + return [...sortedElements]; +}; + +/** + * In theory, when we have text elements bound to a container, they + * should be right after the container element in the elements array. + * However, this is not guaranteed due to old and potential future bugs. + * + * This function sorts containers and their bound texts together. It prefers + * original z-index of container (i.e. it moves bound text elements after + * containers). + */ +const normalizeBoundElementsOrder = ( + elements: readonly ExcalidrawElement[], +) => { + const elementsMap = arrayToMapWithIndex(elements); + + const origElements: (ExcalidrawElement | null)[] = elements.slice(); + const sortedElements = new Set(); + + origElements.forEach((element, idx) => { + if (!element) { + return; + } + if (element.boundElements?.length) { + sortedElements.add(element); + origElements[idx] = null; + element.boundElements.forEach((boundElement) => { + const child = elementsMap.get(boundElement.id); + if (child && boundElement.type === "text") { + sortedElements.add(child[0]); + origElements[child[1]] = null; + } + }); + } else if (element.type === "text" && element.containerId) { + const parent = elementsMap.get(element.containerId); + if (!parent?.[0].boundElements?.find((x) => x.id === element.id)) { + sortedElements.add(element); + origElements[idx] = null; + + // if element has a container and container lists it, skip this element + // as it'll be taken care of by the container + } + } else { + sortedElements.add(element); + origElements[idx] = null; + } + }); + + // if there's a bug which resulted in losing some of the elements, return + // original instead as that's better than losing data + if (sortedElements.size !== elements.length) { + console.error( + "normalizeBoundElementsOrder: lost some elements... bailing!", + ); + return elements; + } + + return [...sortedElements]; +}; + +export const normalizeElementOrder = ( + elements: readonly ExcalidrawElement[], +) => { + // console.time(); + const ret = normalizeBoundElementsOrder(normalizeGroupElementOrder(elements)); + // console.timeEnd(); + return ret; +}; diff --git a/src/element/textElement.ts b/src/element/textElement.ts index 033b3db0e..c726c1c3f 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -127,10 +127,16 @@ export const bindTextToShapeAfterDuplication = ( const newContainer = sceneElementMap.get(newElementId); if (newContainer) { mutateElement(newContainer, { - boundElements: (newContainer.boundElements || []).concat({ - type: "text", - id: newTextElementId, - }), + boundElements: (element.boundElements || []) + .filter( + (boundElement) => + boundElement.id !== newTextElementId && + boundElement.id !== boundTextElementId, + ) + .concat({ + type: "text", + id: newTextElementId, + }), }); } const newTextElement = sceneElementMap.get(newTextElementId); diff --git a/src/excalidraw-app/collab/reconciliation.ts b/src/excalidraw-app/collab/reconciliation.ts index dee9f7390..76b6f052a 100644 --- a/src/excalidraw-app/collab/reconciliation.ts +++ b/src/excalidraw-app/collab/reconciliation.ts @@ -1,6 +1,7 @@ import { PRECEDING_ELEMENT_KEY } from "../../constants"; import { ExcalidrawElement } from "../../element/types"; import { AppState } from "../../types"; +import { arrayToMapWithIndex } from "../../utils"; export type ReconciledElements = readonly ExcalidrawElement[] & { _brand: "reconciledElements"; @@ -33,30 +34,13 @@ const shouldDiscardRemoteElement = ( return false; }; -const getElementsMapWithIndex = ( - elements: readonly T[], -) => - elements.reduce( - ( - acc: { - [key: string]: [element: T, index: number] | undefined; - }, - element: T, - idx, - ) => { - acc[element.id] = [element, idx]; - return acc; - }, - {}, - ); - export const reconcileElements = ( localElements: readonly ExcalidrawElement[], remoteElements: readonly BroadcastedExcalidrawElement[], localAppState: AppState, ): ReconciledElements => { const localElementsData = - getElementsMapWithIndex(localElements); + arrayToMapWithIndex(localElements); const reconciledElements: ExcalidrawElement[] = localElements.slice(); @@ -69,7 +53,7 @@ export const reconcileElements = ( for (const remoteElement of remoteElements) { remoteElementIdx++; - const local = localElementsData[remoteElement.id]; + const local = localElementsData.get(remoteElement.id); if (shouldDiscardRemoteElement(localAppState, local?.[0], remoteElement)) { if (remoteElement[PRECEDING_ELEMENT_KEY]) { @@ -105,21 +89,21 @@ export const reconcileElements = ( offset++; if (cursor === 0) { reconciledElements.unshift(remoteElement); - localElementsData[remoteElement.id] = [ + localElementsData.set(remoteElement.id, [ remoteElement, cursor - offset, - ]; + ]); } else { reconciledElements.splice(cursor + 1, 0, remoteElement); - localElementsData[remoteElement.id] = [ + localElementsData.set(remoteElement.id, [ remoteElement, cursor + 1 - offset, - ]; + ]); cursor++; } } else { - let idx = localElementsData[parent] - ? localElementsData[parent]![1] + let idx = localElementsData.has(parent) + ? localElementsData.get(parent)![1] : null; if (idx != null) { idx += offset; @@ -127,38 +111,38 @@ export const reconcileElements = ( if (idx != null && idx >= cursor) { reconciledElements.splice(idx + 1, 0, remoteElement); offset++; - localElementsData[remoteElement.id] = [ + localElementsData.set(remoteElement.id, [ remoteElement, idx + 1 - offset, - ]; + ]); cursor = idx + 1; } else if (idx != null) { reconciledElements.splice(cursor + 1, 0, remoteElement); offset++; - localElementsData[remoteElement.id] = [ + localElementsData.set(remoteElement.id, [ remoteElement, cursor + 1 - offset, - ]; + ]); cursor++; } else { reconciledElements.push(remoteElement); - localElementsData[remoteElement.id] = [ + localElementsData.set(remoteElement.id, [ remoteElement, reconciledElements.length - 1 - offset, - ]; + ]); } } // no parent z-index information, local element exists → replace in place } else if (local) { reconciledElements[local[1]] = remoteElement; - localElementsData[remoteElement.id] = [remoteElement, local[1]]; + localElementsData.set(remoteElement.id, [remoteElement, local[1]]); // otherwise push to the end } else { reconciledElements.push(remoteElement); - localElementsData[remoteElement.id] = [ + localElementsData.set(remoteElement.id, [ remoteElement, reconciledElements.length - 1 - offset, - ]; + ]); } } diff --git a/src/tests/zindex.test.tsx b/src/tests/zindex.test.tsx index 402e0a2d9..a59af77cf 100644 --- a/src/tests/zindex.test.tsx +++ b/src/tests/zindex.test.tsx @@ -11,6 +11,7 @@ import { } from "../actions"; import { AppState } from "../types"; import { API } from "./helpers/api"; +import { selectGroupsForSelectedElements } from "../groups"; // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); @@ -34,6 +35,7 @@ const populateElements = ( height?: number; containerId?: string; }[], + appState?: Partial, ) => { const selectedElementIds: any = {}; @@ -84,6 +86,11 @@ const populateElements = ( }); h.setState({ + ...selectGroupsForSelectedElements( + { ...h.state, ...appState, selectedElementIds }, + h.elements, + ), + ...appState, selectedElementIds, }); @@ -111,11 +118,7 @@ const assertZindex = ({ appState?: Partial; operations: [Actions, string[]][]; }) => { - const selectedElementIds = populateElements(elements); - - h.setState({ - editingGroupId: appState?.editingGroupId || null, - }); + const selectedElementIds = populateElements(elements, appState); operations.forEach(([action, expected]) => { h.app.actionManager.executeAction(action); @@ -884,9 +887,6 @@ describe("z-index manipulation", () => { { id: "A", groupIds: ["g1"], isSelected: true }, { id: "B", groupIds: ["g1"], isSelected: true }, ]); - h.setState({ - selectedGroupIds: { g1: true }, - }); h.app.actionManager.executeAction(actionDuplicateSelection); expect(h.elements).toMatchObject([ { id: "A" }, @@ -908,9 +908,6 @@ describe("z-index manipulation", () => { { id: "B", groupIds: ["g1"], isSelected: true }, { id: "C" }, ]); - h.setState({ - selectedGroupIds: { g1: true }, - }); h.app.actionManager.executeAction(actionDuplicateSelection); expect(h.elements).toMatchObject([ { id: "A" }, @@ -933,9 +930,6 @@ describe("z-index manipulation", () => { { id: "B", groupIds: ["g1"], isSelected: true }, { id: "C", isSelected: true }, ]); - h.setState({ - selectedGroupIds: { g1: true }, - }); h.app.actionManager.executeAction(actionDuplicateSelection); expect(h.elements.map((element) => element.id)).toEqual([ "A", @@ -952,9 +946,6 @@ describe("z-index manipulation", () => { { id: "C", groupIds: ["g2"], isSelected: true }, { id: "D", groupIds: ["g2"], isSelected: true }, ]); - h.setState({ - selectedGroupIds: { g1: true, g2: true }, - }); h.app.actionManager.executeAction(actionDuplicateSelection); expect(h.elements.map((element) => element.id)).toEqual([ "A", @@ -967,14 +958,16 @@ describe("z-index manipulation", () => { "D_copy", ]); - populateElements([ - { id: "A", groupIds: ["g1", "g2"], isSelected: true }, - { id: "B", groupIds: ["g1", "g2"], isSelected: true }, - { id: "C", groupIds: ["g2"], isSelected: true }, - ]); - h.setState({ - selectedGroupIds: { g1: true }, - }); + populateElements( + [ + { id: "A", groupIds: ["g1", "g2"], isSelected: true }, + { id: "B", groupIds: ["g1", "g2"], isSelected: true }, + { id: "C", groupIds: ["g2"], isSelected: true }, + ], + { + selectedGroupIds: { g1: true }, + }, + ); h.app.actionManager.executeAction(actionDuplicateSelection); expect(h.elements.map((element) => element.id)).toEqual([ "A", @@ -985,14 +978,16 @@ describe("z-index manipulation", () => { "C_copy", ]); - populateElements([ - { id: "A", groupIds: ["g1", "g2"], isSelected: true }, - { id: "B", groupIds: ["g1", "g2"], isSelected: true }, - { id: "C", groupIds: ["g2"], isSelected: true }, - ]); - h.setState({ - selectedGroupIds: { g2: true }, - }); + populateElements( + [ + { id: "A", groupIds: ["g1", "g2"], isSelected: true }, + { id: "B", groupIds: ["g1", "g2"], isSelected: true }, + { id: "C", groupIds: ["g2"], isSelected: true }, + ], + { + selectedGroupIds: { g2: true }, + }, + ); h.app.actionManager.executeAction(actionDuplicateSelection); expect(h.elements.map((element) => element.id)).toEqual([ "A", @@ -1003,17 +998,19 @@ describe("z-index manipulation", () => { "C_copy", ]); - populateElements([ - { id: "A", groupIds: ["g1", "g2"], isSelected: true }, - { id: "B", groupIds: ["g1", "g2"], isSelected: true }, - { id: "C", groupIds: ["g2"], isSelected: true }, - { id: "D", groupIds: ["g3", "g4"], isSelected: true }, - { id: "E", groupIds: ["g3", "g4"], isSelected: true }, - { id: "F", groupIds: ["g4"], isSelected: true }, - ]); - h.setState({ - selectedGroupIds: { g2: true, g4: true }, - }); + populateElements( + [ + { id: "A", groupIds: ["g1", "g2"], isSelected: true }, + { id: "B", groupIds: ["g1", "g2"], isSelected: true }, + { id: "C", groupIds: ["g2"], isSelected: true }, + { id: "D", groupIds: ["g3", "g4"], isSelected: true }, + { id: "E", groupIds: ["g3", "g4"], isSelected: true }, + { id: "F", groupIds: ["g4"], isSelected: true }, + ], + { + selectedGroupIds: { g2: true, g4: true }, + }, + ); h.app.actionManager.executeAction(actionDuplicateSelection); expect(h.elements.map((element) => element.id)).toEqual([ "A", @@ -1030,11 +1027,14 @@ describe("z-index manipulation", () => { "F_copy", ]); - populateElements([ - { id: "A", groupIds: ["g1", "g2"], isSelected: true }, - { id: "B", groupIds: ["g1", "g2"] }, - { id: "C", groupIds: ["g2"] }, - ]); + populateElements( + [ + { id: "A", groupIds: ["g1", "g2"], isSelected: true }, + { id: "B", groupIds: ["g1", "g2"] }, + { id: "C", groupIds: ["g2"] }, + ], + { editingGroupId: "g1" }, + ); h.app.actionManager.executeAction(actionDuplicateSelection); expect(h.elements.map((element) => element.id)).toEqual([ "A", @@ -1043,11 +1043,14 @@ describe("z-index manipulation", () => { "C", ]); - populateElements([ - { id: "A", groupIds: ["g1", "g2"] }, - { id: "B", groupIds: ["g1", "g2"], isSelected: true }, - { id: "C", groupIds: ["g2"] }, - ]); + populateElements( + [ + { id: "A", groupIds: ["g1", "g2"] }, + { id: "B", groupIds: ["g1", "g2"], isSelected: true }, + { id: "C", groupIds: ["g2"] }, + ], + { editingGroupId: "g1" }, + ); h.app.actionManager.executeAction(actionDuplicateSelection); expect(h.elements.map((element) => element.id)).toEqual([ "A", @@ -1056,11 +1059,14 @@ describe("z-index manipulation", () => { "C", ]); - populateElements([ - { id: "A", groupIds: ["g1", "g2"], isSelected: true }, - { id: "B", groupIds: ["g1", "g2"], isSelected: true }, - { id: "C", groupIds: ["g2"], isSelected: true }, - ]); + populateElements( + [ + { id: "A", groupIds: ["g1", "g2"], isSelected: true }, + { id: "B", groupIds: ["g1", "g2"], isSelected: true }, + { id: "C", groupIds: ["g2"] }, + ], + { editingGroupId: "g1" }, + ); h.app.actionManager.executeAction(actionDuplicateSelection); expect(h.elements.map((element) => element.id)).toEqual([ "A", @@ -1068,7 +1074,42 @@ describe("z-index manipulation", () => { "B", "B_copy", "C", + ]); + }); + + it("duplicating incorrectly interleaved elements (group elements should be together) should still produce reasonable result", () => { + populateElements([ + { id: "A", groupIds: ["g1"], isSelected: true }, + { id: "B" }, + { id: "C", groupIds: ["g1"], isSelected: true }, + ]); + h.app.actionManager.executeAction(actionDuplicateSelection); + expect(h.elements.map((element) => element.id)).toEqual([ + "A", + "C", + "A_copy", "C_copy", + "B", + ]); + }); + + it("group-selected duplication should includes deleted elements that weren't selected on account of being deleted", () => { + populateElements([ + { id: "A", groupIds: ["g1"], isDeleted: true }, + { id: "B", groupIds: ["g1"], isSelected: true }, + { id: "C", groupIds: ["g1"], isSelected: true }, + { id: "D" }, + ]); + expect(h.state.selectedGroupIds).toEqual({ g1: true }); + h.app.actionManager.executeAction(actionDuplicateSelection); + expect(h.elements.map((element) => element.id)).toEqual([ + "A", + "B", + "C", + "A_copy", + "B_copy", + "C_copy", + "D", ]); }); diff --git a/src/utils.ts b/src/utils.ts index 60ad24f1c..b2d85d4d3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -607,6 +607,14 @@ export const arrayToMap = ( }, new Map()); }; +export const arrayToMapWithIndex = ( + elements: readonly T[], +) => + elements.reduce((acc, element: T, idx) => { + acc.set(element.id, [element, idx]); + return acc; + }, new Map()); + export const isTestEnv = () => typeof process !== "undefined" && process.env?.NODE_ENV === "test"; From 4414069617ae3ebc3fcad7e926a87a9a76e68355 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Fri, 3 Feb 2023 17:07:14 +0100 Subject: [PATCH 163/276] feat: disable canvas smoothing (antialiasing) for right-angled elements (#6186)Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com> * feat: disable canvas smoothing for text and other types * disable smoothing for all right-angled elements * Update src/renderer/renderElement.ts Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com> * Update src/renderer/renderElement.ts Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com> * fix lint * always enable smoothing while zooming --------- Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com> --- src/math.ts | 12 ++++++++++++ src/renderer/renderElement.ts | 28 +++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/math.ts b/src/math.ts index 2deb221b6..cfa28e230 100644 --- a/src/math.ts +++ b/src/math.ts @@ -459,3 +459,15 @@ export const mapIntervalToBezierT = ( export const arePointsEqual = (p1: Point, p2: Point) => { return p1[0] === p2[0] && p1[1] === p2[1]; }; + +export const isRightAngle = (angle: number) => { + // if our angles were mathematically accurate, we could just check + // + // angle % (Math.PI / 2) === 0 + // + // but since we're in floating point land, we need to round. + // + // Below, after dividing by Math.PI, a multiple of 0.5 indicates a right + // angle, which we can check with modulo after rounding. + return Math.round((angle / Math.PI) * 10000) % 5000 === 0; +}; diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index f77a8c482..39173d7b5 100644 --- a/src/renderer/renderElement.ts +++ b/src/renderer/renderElement.ts @@ -27,7 +27,7 @@ import { RoughGenerator } from "roughjs/bin/generator"; import { RenderConfig } from "../scene/types"; import { distance, getFontString, getFontFamilyString, isRTL } from "../utils"; -import { getCornerRadius, isPathALoop } from "../math"; +import { getCornerRadius, isPathALoop, isRightAngle } from "../math"; import rough from "roughjs/bin/rough"; import { AppState, BinaryFiles, Zoom } from "../types"; import { getDefaultAppState } from "../appState"; @@ -989,7 +989,33 @@ export const renderElement = ( element, renderConfig, ); + + const currentImageSmoothingStatus = context.imageSmoothingEnabled; + + if ( + // do not disable smoothing during zoom as blurry shapes look better + // on low resolution (while still zooming in) than sharp ones + !renderConfig?.shouldCacheIgnoreZoom && + // angle is 0 -> always disable smoothing + (!element.angle || + // or check if angle is a right angle in which case we can still + // disable smoothing without adversely affecting the result + isRightAngle(element.angle)) + ) { + // Disabling smoothing makes output much sharper, especially for + // text. Unless for non-right angles, where the aliasing is really + // terrible on Chromium. + // + // Note that `context.imageSmoothingQuality="high"` has almost + // zero effect. + // + context.imageSmoothingEnabled = false; + } + drawElementFromCanvas(elementWithCanvas, rc, context, renderConfig); + + // reset + context.imageSmoothingEnabled = currentImageSmoothingStatus; } break; } From 4db87a0b6a2c58418df657a15050e764da3deeab Mon Sep 17 00:00:00 2001 From: Excalidraw Bot <77840495+excalibot@users.noreply.github.com> Date: Sat, 4 Feb 2023 10:04:15 +0100 Subject: [PATCH 164/276] chore: Update translations from Crowdin (#6150) --- src/locales/ar-SA.json | 52 ++++++++++++++++---------------- src/locales/bg-BG.json | 6 ++-- src/locales/bn-BD.json | 8 +++-- src/locales/ca-ES.json | 8 +++-- src/locales/cs-CZ.json | 6 ++-- src/locales/da-DK.json | 6 ++-- src/locales/de-DE.json | 8 +++-- src/locales/el-GR.json | 8 +++-- src/locales/es-ES.json | 8 +++-- src/locales/eu-ES.json | 8 +++-- src/locales/fa-IR.json | 8 +++-- src/locales/fi-FI.json | 8 +++-- src/locales/fr-FR.json | 10 ++++--- src/locales/gl-ES.json | 8 +++-- src/locales/he-IL.json | 8 +++-- src/locales/hi-IN.json | 22 +++++++------- src/locales/hu-HU.json | 8 +++-- src/locales/id-ID.json | 8 +++-- src/locales/it-IT.json | 8 +++-- src/locales/ja-JP.json | 8 +++-- src/locales/kab-KAB.json | 14 +++++---- src/locales/kk-KZ.json | 6 ++-- src/locales/ko-KR.json | 8 +++-- src/locales/ku-TR.json | 8 +++-- src/locales/lt-LT.json | 8 +++-- src/locales/lv-LV.json | 8 +++-- src/locales/mr-IN.json | 22 +++++++------- src/locales/my-MM.json | 6 ++-- src/locales/nb-NO.json | 8 +++-- src/locales/nl-NL.json | 8 +++-- src/locales/nn-NO.json | 8 +++-- src/locales/oc-FR.json | 16 +++++----- src/locales/pa-IN.json | 6 ++-- src/locales/percentages.json | 58 ++++++++++++++++++------------------ src/locales/pl-PL.json | 8 +++-- src/locales/pt-BR.json | 8 +++-- src/locales/pt-PT.json | 8 +++-- src/locales/ro-RO.json | 8 +++-- src/locales/ru-RU.json | 22 +++++++------- src/locales/si-LK.json | 6 ++-- src/locales/sk-SK.json | 8 +++-- src/locales/sl-SI.json | 8 +++-- src/locales/sv-SE.json | 36 +++++++++++----------- src/locales/ta-IN.json | 8 +++-- src/locales/tr-TR.json | 8 +++-- src/locales/uk-UA.json | 8 +++-- src/locales/vi-VN.json | 12 ++++---- src/locales/zh-CN.json | 8 +++-- src/locales/zh-HK.json | 6 ++-- src/locales/zh-TW.json | 8 +++-- 50 files changed, 333 insertions(+), 235 deletions(-) diff --git a/src/locales/ar-SA.json b/src/locales/ar-SA.json index 8594200c0..2a682ad70 100644 --- a/src/locales/ar-SA.json +++ b/src/locales/ar-SA.json @@ -109,35 +109,35 @@ "decreaseFontSize": "تصغير حجم الخط", "increaseFontSize": "تكبير حجم الخط", "unbindText": "فك ربط النص", - "bindText": "", + "bindText": "ربط النص بالحاوية", "link": { "edit": "تعديل الرابط", "create": "إنشاء رابط", "label": "رابط" }, "lineEditor": { - "edit": "", - "exit": "" + "edit": "تحرير السطر", + "exit": "الخروج من المُحرر" }, "elementLock": { - "lock": "", - "unlock": "", - "lockAll": "", - "unlockAll": "" + "lock": "قفل", + "unlock": "فتح", + "lockAll": "قفل الكل", + "unlockAll": "فتح الكل" }, - "statusPublished": "", - "sidebarLock": "" + "statusPublished": "نُشر", + "sidebarLock": "إبقاء الشريط الجانبي مفتوح" }, "library": { - "noItems": "", - "hint_emptyLibrary": "", - "hint_emptyPrivateLibrary": "" + "noItems": "لا توجد عناصر أضيفت بعد...", + "hint_emptyLibrary": "حدد عنصر على القماش لإضافته هنا، أو تثبيت مكتبة من المستودع العام أدناه.", + "hint_emptyPrivateLibrary": "حدد عنصر على القماش لإضافته هنا." }, "buttons": { "clearReset": "إعادة تعيين اللوحة", "exportJSON": "صدر الملف", - "exportImage": "", - "export": "", + "exportImage": "تصدير الصورة...", + "export": "حفظ إلى...", "exportToPng": "تصدير بصيغة PNG", "exportToSvg": "تصدير بصيغة SVG", "copyToClipboard": "نسخ إلى الحافظة", @@ -179,7 +179,7 @@ "couldNotLoadInvalidFile": "تعذر التحميل، الملف غير صالح", "importBackendFailed": "فشل الاستيراد من الخادوم.", "cannotExportEmptyCanvas": "لا يمكن تصدير لوحة فارغة.", - "couldNotCopyToClipboard": "", + "couldNotCopyToClipboard": "تعذر النسخ إلى الحافظة.", "decryptFailed": "تعذر فك تشفير البيانات.", "uploadedSecurly": "تم تأمين التحميل بتشفير النهاية إلى النهاية، مما يعني أن خادوم Excalidraw والأطراف الثالثة لا يمكنها قراءة المحتوى.", "loadSceneOverridePrompt": "تحميل الرسم الخارجي سيحل محل المحتوى الموجود لديك. هل ترغب في المتابعة؟", @@ -200,10 +200,10 @@ "fileTooBig": "الملف كبير جداً. الحد الأقصى المسموح به للحجم هو {{maxSize}}.", "svgImageInsertError": "تعذر إدراج صورة SVG. يبدو أن ترميز SVG غير صحيح.", "invalidSVGString": "SVG غير صالح.", - "cannotResolveCollabServer": "", - "importLibraryError": "", - "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "cannotResolveCollabServer": "تعذر الاتصال بخادم التعاون. الرجاء إعادة تحميل الصفحة والمحاولة مرة أخرى.", + "importLibraryError": "تعذر تحميل المكتبة", + "collabSaveFailed": "تعذر الحفظ في قاعدة البيانات. إذا استمرت المشاكل، يفضل أن تحفظ ملفك محليا كي لا تفقد عملك.", + "collabSaveFailed_sizeExceeded": "تعذر الحفظ في قاعدة البيانات، يبدو أن القماش كبير للغاية، يفضّل حفظ الملف محليا كي لا تفقد عملك." }, "toolBar": { "selection": "تحديد", @@ -217,9 +217,10 @@ "text": "نص", "library": "مكتبة", "lock": "الحفاظ على أداة التحديد نشطة بعد الرسم", - "penMode": "", - "link": "", - "eraser": "ممحاة" + "penMode": "وضع القلم - امنع اللمس", + "link": "إضافة/تحديث الرابط للشكل المحدد", + "eraser": "ممحاة", + "hand": "" }, "headings": { "canvasActions": "إجراءات اللوحة", @@ -227,7 +228,7 @@ "shapes": "الأشكال" }, "hints": { - "canvasPanning": "لتحريك لوحة الرسم ، استمر في الضغط على عجلة الماوس أو مفتاح المسافة أثناء السحب", + "canvasPanning": "لتحريك القماش، اضغط على عجلة الفأرة أو مفتاح المسافة أثناء السحب، أو استخدم أداة اليد", "linearElement": "انقر لبدء نقاط متعددة، اسحب لخط واحد", "freeDraw": "انقر واسحب، افرج عند الانتهاء", "text": "نصيحة: يمكنك أيضًا إضافة نص بالنقر المزدوج في أي مكان بأداة الاختيار", @@ -238,14 +239,15 @@ "resize": "يمكنك تقييد النسب بالضغط على SHIFT أثناء تغيير الحجم،\nاضغط على ALT لتغيير الحجم من المركز", "resizeImage": "يمكنك تغيير الحجم بحرية بالضغط بأستمرار على SHIFT،\nاضغط بأستمرار على ALT أيضا لتغيير الحجم من المركز", "rotate": "يمكنك تقييد الزوايا من خلال الضغط على SHIFT أثناء الدوران", - "lineEditor_info": "", + "lineEditor_info": "اضغط على مفتاح (Ctrl أو Cmd) و انقر بشكل مزدوج، أو اضغط على مفتاحي (Ctrl أو Cmd) و (Enter) لتعديل النقاط", "lineEditor_pointSelected": "", "lineEditor_nothingSelected": "", "placeImage": "", "publishLibrary": "نشر مكتبتك", "bindTextToElement": "", "deepBoxSelect": "", - "eraserRevert": "" + "eraserRevert": "", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "تعذر عرض المعاينة", diff --git a/src/locales/bg-BG.json b/src/locales/bg-BG.json index 06f45a9a7..7f59678c0 100644 --- a/src/locales/bg-BG.json +++ b/src/locales/bg-BG.json @@ -219,7 +219,8 @@ "lock": "Поддържайте избрания инструмент активен след рисуване", "penMode": "", "link": "", - "eraser": "" + "eraser": "", + "hand": "" }, "headings": { "canvasActions": "Действия по платното", @@ -245,7 +246,8 @@ "publishLibrary": "", "bindTextToElement": "Натиснете Enter, за да добавите", "deepBoxSelect": "", - "eraserRevert": "" + "eraserRevert": "", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "Невъзможност за показване на preview", diff --git a/src/locales/bn-BD.json b/src/locales/bn-BD.json index 2bac711c1..29c35eb63 100644 --- a/src/locales/bn-BD.json +++ b/src/locales/bn-BD.json @@ -219,7 +219,8 @@ "lock": "আঁকার পরে নির্বাচিত টুল সক্রিয় রাখুন", "penMode": "", "link": "একটি নির্বাচিত আকৃতির জন্য লিঙ্ক যোগ বা আপডেট করুন", - "eraser": "ঝাড়ন" + "eraser": "ঝাড়ন", + "hand": "" }, "headings": { "canvasActions": "ক্যানভাস কার্যকলাপ", @@ -227,7 +228,7 @@ "shapes": "আকার(গুলি)" }, "hints": { - "canvasPanning": "ক্যানভাস সরানোর জন্য মাউস হুইল বা স্পেসবার ধরে টানুন", + "canvasPanning": "", "linearElement": "একাধিক বিন্দু শুরু করতে ক্লিক করুন, একক লাইনের জন্য টেনে আনুন", "freeDraw": "ক্লিক করুন এবং টেনে আনুন, আপনার কাজ শেষ হলে ছেড়ে দিন", "text": "বিশেষ্য: আপনি নির্বাচন টুলের সাথে যে কোনো জায়গায় ডাবল-ক্লিক করে পাঠ্য যোগ করতে পারেন", @@ -245,7 +246,8 @@ "publishLibrary": "আপনার নিজস্ব সংগ্রহ প্রকাশ করুন", "bindTextToElement": "লেখা যোগ করতে এন্টার টিপুন", "deepBoxSelect": "", - "eraserRevert": "মুছে ফেলার জন্য চিহ্নিত উপাদানগুলিকে ফিরিয়ে আনতে অল্ট ধরে রাখুন" + "eraserRevert": "মুছে ফেলার জন্য চিহ্নিত উপাদানগুলিকে ফিরিয়ে আনতে অল্ট ধরে রাখুন", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "প্রিভিউ দেখাতে অপারগ", diff --git a/src/locales/ca-ES.json b/src/locales/ca-ES.json index e529383b6..82415ff97 100644 --- a/src/locales/ca-ES.json +++ b/src/locales/ca-ES.json @@ -219,7 +219,8 @@ "lock": "Mantenir activa l'eina seleccionada desprès de dibuixar", "penMode": "", "link": "Afegeix / actualitza l'enllaç per a la forma seleccionada", - "eraser": "Esborrador" + "eraser": "Esborrador", + "hand": "" }, "headings": { "canvasActions": "Accions del llenç", @@ -227,7 +228,7 @@ "shapes": "Formes" }, "hints": { - "canvasPanning": "Per a moure el llenç, mantingueu premuda la roda del ratolí o la tecla espai mentre l'arrossegueu", + "canvasPanning": "", "linearElement": "Feu clic per a dibuixar múltiples punts; arrossegueu per a una sola línia", "freeDraw": "Feu clic i arrossegueu, deixeu anar per a finalitzar", "text": "Consell: també podeu afegir text fent doble clic en qualsevol lloc amb l'eina de selecció", @@ -245,7 +246,8 @@ "publishLibrary": "Publiqueu la vostra pròpia llibreria", "bindTextToElement": "Premeu enter per a afegir-hi text", "deepBoxSelect": "Manteniu CtrlOrCmd per a selecció profunda, i per a evitar l'arrossegament", - "eraserRevert": "Mantingueu premuda Alt per a revertir els elements seleccionats per a esborrar" + "eraserRevert": "Mantingueu premuda Alt per a revertir els elements seleccionats per a esborrar", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "No es pot mostrar la previsualització", diff --git a/src/locales/cs-CZ.json b/src/locales/cs-CZ.json index 7dddb1116..fa225d2a1 100644 --- a/src/locales/cs-CZ.json +++ b/src/locales/cs-CZ.json @@ -219,7 +219,8 @@ "lock": "", "penMode": "", "link": "", - "eraser": "Guma" + "eraser": "Guma", + "hand": "" }, "headings": { "canvasActions": "", @@ -245,7 +246,8 @@ "publishLibrary": "", "bindTextToElement": "", "deepBoxSelect": "", - "eraserRevert": "" + "eraserRevert": "", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "", diff --git a/src/locales/da-DK.json b/src/locales/da-DK.json index 6f3876dcf..5b41ddb1e 100644 --- a/src/locales/da-DK.json +++ b/src/locales/da-DK.json @@ -219,7 +219,8 @@ "lock": "", "penMode": "", "link": "", - "eraser": "" + "eraser": "", + "hand": "" }, "headings": { "canvasActions": "", @@ -245,7 +246,8 @@ "publishLibrary": "", "bindTextToElement": "", "deepBoxSelect": "", - "eraserRevert": "" + "eraserRevert": "", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "", diff --git a/src/locales/de-DE.json b/src/locales/de-DE.json index f5dd739d1..a14f52859 100644 --- a/src/locales/de-DE.json +++ b/src/locales/de-DE.json @@ -219,7 +219,8 @@ "lock": "Ausgewähltes Werkzeug nach Zeichnen aktiv lassen", "penMode": "Stift-Modus - Berührung verhindern", "link": "Link für ausgewählte Form hinzufügen / aktualisieren", - "eraser": "Radierer" + "eraser": "Radierer", + "hand": "Hand (Schwenkwerkzeug)" }, "headings": { "canvasActions": "Aktionen für Zeichenfläche", @@ -227,7 +228,7 @@ "shapes": "Formen" }, "hints": { - "canvasPanning": "Um die Zeichenfläche zu verschieben, halte das Mausrad oder die Leertaste während des Ziehens", + "canvasPanning": "Um die Zeichenfläche zu verschieben, halte das Mausrad oder die Leertaste während des Ziehens, oder verwende das Hand-Werkzeug", "linearElement": "Klicken für Linie mit mehreren Punkten, Ziehen für einzelne Linie", "freeDraw": "Klicke und ziehe. Lass los, wenn du fertig bist", "text": "Tipp: Du kannst auch Text hinzufügen, indem du mit dem Auswahlwerkzeug auf eine beliebige Stelle doppelklickst", @@ -245,7 +246,8 @@ "publishLibrary": "Veröffentliche deine eigene Bibliothek", "bindTextToElement": "Zum Hinzufügen Eingabetaste drücken", "deepBoxSelect": "Halte CtrlOrCmd gedrückt, um innerhalb der Gruppe auszuwählen, und um Ziehen zu vermeiden", - "eraserRevert": "Halte Alt gedrückt, um die zum Löschen markierten Elemente zurückzusetzen" + "eraserRevert": "Halte Alt gedrückt, um die zum Löschen markierten Elemente zurückzusetzen", + "firefox_clipboard_write": "Diese Funktion kann wahrscheinlich aktiviert werden, indem die Einstellung \"dom.events.asyncClipboard.clipboardItem\" auf \"true\" gesetzt wird. Um die Browsereinstellungen in Firefox zu ändern, besuche die Seite \"about:config\"." }, "canvasError": { "cannotShowPreview": "Vorschau kann nicht angezeigt werden", diff --git a/src/locales/el-GR.json b/src/locales/el-GR.json index a603bd18f..02534c326 100644 --- a/src/locales/el-GR.json +++ b/src/locales/el-GR.json @@ -219,7 +219,8 @@ "lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο", "penMode": "Λειτουργία μολυβιού - αποτροπή αφής", "link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα", - "eraser": "Γόμα" + "eraser": "Γόμα", + "hand": "" }, "headings": { "canvasActions": "Ενέργειες καμβά", @@ -227,7 +228,7 @@ "shapes": "Σχήματα" }, "hints": { - "canvasPanning": "Για να μετακινήσετε καμβά, κρατήστε πατημένο τον τροχό του ποντικιού ή το πλήκτρο διαστήματος ενώ σύρετε", + "canvasPanning": "", "linearElement": "Κάνε κλικ για να ξεκινήσεις πολλαπλά σημεία, σύρε για μια γραμμή", "freeDraw": "Κάντε κλικ και σύρτε, απελευθερώσατε όταν έχετε τελειώσει", "text": "Tip: μπορείτε επίσης να προσθέστε κείμενο με διπλό-κλικ οπουδήποτε με το εργαλείο επιλογών", @@ -245,7 +246,8 @@ "publishLibrary": "Δημοσιεύστε τη δική σας βιβλιοθήκη", "bindTextToElement": "Πατήστε Enter για προσθήκη κειμένου", "deepBoxSelect": "Κρατήστε πατημένο το CtrlOrCmd για να επιλέξετε βαθιά, και να αποτρέψετε τη μεταφορά", - "eraserRevert": "Κρατήστε πατημένο το Alt για να επαναφέρετε τα στοιχεία που σημειώθηκαν για διαγραφή" + "eraserRevert": "Κρατήστε πατημένο το Alt για να επαναφέρετε τα στοιχεία που σημειώθηκαν για διαγραφή", + "firefox_clipboard_write": "Αυτή η επιλογή μπορεί πιθανώς να ενεργοποιηθεί αλλάζοντας την ρύθμιση \"dom.events.asyncClipboard.clipboardItem\" σε \"true\". Για να αλλάξετε τις ρυθμίσεις του προγράμματος περιήγησης στο Firefox, επισκεφθείτε τη σελίδα \"about:config\"." }, "canvasError": { "cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης", diff --git a/src/locales/es-ES.json b/src/locales/es-ES.json index 81fa8efbb..e70ad64e5 100644 --- a/src/locales/es-ES.json +++ b/src/locales/es-ES.json @@ -219,7 +219,8 @@ "lock": "Mantener la herramienta seleccionada activa después de dibujar", "penMode": "Modo Lápiz - previene toque", "link": "Añadir/Actualizar enlace para una forma seleccionada", - "eraser": "Borrar" + "eraser": "Borrar", + "hand": "Mano (herramienta de panoramización)" }, "headings": { "canvasActions": "Acciones del lienzo", @@ -227,7 +228,7 @@ "shapes": "Formas" }, "hints": { - "canvasPanning": "Para mover el lienzo, mantenga la rueda del ratón o la barra de espacio mientras arrastra", + "canvasPanning": "Para mover el lienzo, mantenga la rueda del ratón o la barra espaciadora mientras arrastra o utilice la herramienta de mano", "linearElement": "Haz clic para dibujar múltiples puntos, arrastrar para solo una línea", "freeDraw": "Haz clic y arrastra, suelta al terminar", "text": "Consejo: también puedes añadir texto haciendo doble clic en cualquier lugar con la herramienta de selección", @@ -245,7 +246,8 @@ "publishLibrary": "Publica tu propia biblioteca", "bindTextToElement": "Presione Entrar para agregar", "deepBoxSelect": "Mantén CtrlOrCmd para seleccionar en profundidad, y para evitar arrastrar", - "eraserRevert": "Mantenga pulsado Alt para revertir los elementos marcados para su eliminación" + "eraserRevert": "Mantenga pulsado Alt para revertir los elementos marcados para su eliminación", + "firefox_clipboard_write": "Esta característica puede ser habilitada estableciendo la bandera \"dom.events.asyncClipboard.clipboardItem\" a \"true\". Para cambiar las banderas del navegador en Firefox, visite la página \"about:config\"." }, "canvasError": { "cannotShowPreview": "No se puede mostrar la vista previa", diff --git a/src/locales/eu-ES.json b/src/locales/eu-ES.json index c606112cc..e35e8c40c 100644 --- a/src/locales/eu-ES.json +++ b/src/locales/eu-ES.json @@ -219,7 +219,8 @@ "lock": "Mantendu aktibo hautatutako tresna marraztu ondoren", "penMode": "Luma modua - ukipena saihestu", "link": "Gehitu / Eguneratu esteka hautatutako forma baterako", - "eraser": "Borragoma" + "eraser": "Borragoma", + "hand": "" }, "headings": { "canvasActions": "Canvas ekintzak", @@ -227,7 +228,7 @@ "shapes": "Formak" }, "hints": { - "canvasPanning": "Oihala mugitzeko, sakatu saguaren gurpila edo zuriune-barra arrastatzean", + "canvasPanning": "", "linearElement": "Egin klik hainbat puntu hasteko, arrastatu lerro bakarrerako", "freeDraw": "Egin klik eta arrastatu, askatu amaitutakoan", "text": "Aholkua: testua gehitu dezakezu edozein lekutan klik bikoitza eginez hautapen tresnarekin", @@ -245,7 +246,8 @@ "publishLibrary": "Argitaratu zure liburutegia", "bindTextToElement": "Sakatu Sartu testua gehitzeko", "deepBoxSelect": "Eutsi Ctrl edo Cmd sakatuta aukeraketa sakona egiteko eta arrastatzea saihesteko", - "eraserRevert": "Eduki Alt sakatuta ezabatzeko markatutako elementuak leheneratzeko" + "eraserRevert": "Eduki Alt sakatuta ezabatzeko markatutako elementuak leheneratzeko", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "Ezin da oihala aurreikusi", diff --git a/src/locales/fa-IR.json b/src/locales/fa-IR.json index 6d47df998..817ed51dd 100644 --- a/src/locales/fa-IR.json +++ b/src/locales/fa-IR.json @@ -219,7 +219,8 @@ "lock": "ابزار انتخاب شده را بعد از کشیدن نگه دار", "penMode": "حالت قلم - جلوگیری از تماس", "link": "افزودن/به‌روزرسانی پیوند برای شکل انتخابی", - "eraser": "پاک کن" + "eraser": "پاک کن", + "hand": "" }, "headings": { "canvasActions": "عملیات روی بوم", @@ -227,7 +228,7 @@ "shapes": "شکل‌ها" }, "hints": { - "canvasPanning": "برای حرکت دادن بوم، چرخ ماوس یا فاصله را در حین کشیدن نگه دارید", + "canvasPanning": "", "linearElement": "برای چند نقطه کلیک و برای یک خط بکشید", "freeDraw": "کلیک کنید و بکشید و وقتی کار تمام شد رها کنید", "text": "نکته: با برنامه انتخاب شده شما میتوانید با دوبار کلیک کردن هرکجا میخواید متن اظاف کنید", @@ -245,7 +246,8 @@ "publishLibrary": "کتابخانه خود را منتشر کنید", "bindTextToElement": "برای افزودن اینتر را بزنید", "deepBoxSelect": "CtrlOrCmd را برای انتخاب عمیق و جلوگیری از کشیدن نگه دارید", - "eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند" + "eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "پیش نمایش نشان داده نمی شود", diff --git a/src/locales/fi-FI.json b/src/locales/fi-FI.json index 1f934d87b..2346a018c 100644 --- a/src/locales/fi-FI.json +++ b/src/locales/fi-FI.json @@ -219,7 +219,8 @@ "lock": "Pidä valittu työkalu aktiivisena piirron jälkeen", "penMode": "", "link": "Lisää/päivitä linkki valitulle muodolle", - "eraser": "Poistotyökalu" + "eraser": "Poistotyökalu", + "hand": "" }, "headings": { "canvasActions": "Piirtoalueen toiminnot", @@ -227,7 +228,7 @@ "shapes": "Muodot" }, "hints": { - "canvasPanning": "Liikuttaaksesi piirtoaluetta, raahaa hiiren vieritysrulla tai välilyöntinäppäin alaspainettuna", + "canvasPanning": "", "linearElement": "Klikkaa piirtääksesi useampi piste, raahaa piirtääksesi yksittäinen viiva", "freeDraw": "Paina ja raahaa, päästä irti kun olet valmis", "text": "Vinkki: voit myös lisätä tekstiä kaksoisnapsauttamalla mihin tahansa valintatyökalulla", @@ -245,7 +246,8 @@ "publishLibrary": "Julkaise oma kirjasto", "bindTextToElement": "Lisää tekstiä painamalla enter", "deepBoxSelect": "Käytä syvävalintaa ja estä raahaus painamalla CtrlOrCmd", - "eraserRevert": "Pidä Alt alaspainettuna, kumotaksesi merkittyjen elementtien poistamisen" + "eraserRevert": "Pidä Alt alaspainettuna, kumotaksesi merkittyjen elementtien poistamisen", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "Esikatselua ei voitu näyttää", diff --git a/src/locales/fr-FR.json b/src/locales/fr-FR.json index 2d107b5d3..57afcf9b3 100644 --- a/src/locales/fr-FR.json +++ b/src/locales/fr-FR.json @@ -45,7 +45,7 @@ "exportEmbedScene": "Intégrer la scène", "exportEmbedScene_details": "Les données de scène seront enregistrées dans le fichier PNG/SVG exporté, afin que la scène puisse être restaurée à partir de celui-ci.\nCela augmentera la taille du fichier exporté.", "addWatermark": "Ajouter \"Réalisé avec Excalidraw\"", - "handDrawn": "Manuscrit", + "handDrawn": "À main levée", "normal": "Normale", "code": "Code", "small": "Petite", @@ -219,7 +219,8 @@ "lock": "Garder l'outil sélectionné actif après le dessin", "penMode": "Mode stylo - évite le toucher", "link": "Ajouter/mettre à jour le lien pour une forme sélectionnée", - "eraser": "Gomme" + "eraser": "Gomme", + "hand": "Mains (outil de déplacement de la vue)" }, "headings": { "canvasActions": "Actions du canevas", @@ -227,7 +228,7 @@ "shapes": "Formes" }, "hints": { - "canvasPanning": "Pour déplacer la zone de dessin, maintenez la molette de la souris enfoncée ou la barre d'espace tout en faisant glisser", + "canvasPanning": "Pour déplacer la zone de dessin, maintenez la molette de la souris enfoncée ou la barre d'espace tout en faisant glisser, ou utiliser l'outil main.", "linearElement": "Cliquez pour démarrer plusieurs points, faites glisser pour une seule ligne", "freeDraw": "Cliquez et faites glissez, relâchez quand vous avez terminé", "text": "Astuce : vous pouvez aussi ajouter du texte en double-cliquant n'importe où avec l'outil de sélection", @@ -245,7 +246,8 @@ "publishLibrary": "Publier votre propre bibliothèque", "bindTextToElement": "Appuyer sur Entrée pour ajouter du texte", "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", + "firefox_clipboard_write": "Cette fonctionnalité devrait pouvoir être activée en définissant l'option \"dom.events.asyncClipboard.clipboard.clipboardItem\" à \"true\". Pour modifier les paramètres du navigateur dans Firefox, visitez la page \"about:config\"." }, "canvasError": { "cannotShowPreview": "Impossible d’afficher l’aperçu", diff --git a/src/locales/gl-ES.json b/src/locales/gl-ES.json index 677975018..d7053c961 100644 --- a/src/locales/gl-ES.json +++ b/src/locales/gl-ES.json @@ -219,7 +219,8 @@ "lock": "Manter a ferramenta seleccionada activa despois de debuxar", "penMode": "Modo lapis - evitar o contacto", "link": "Engadir/ Actualizar ligazón para a forma seleccionada", - "eraser": "Goma de borrar" + "eraser": "Goma de borrar", + "hand": "Man (ferramenta de desprazamento)" }, "headings": { "canvasActions": "Accións do lenzo", @@ -227,7 +228,7 @@ "shapes": "Formas" }, "hints": { - "canvasPanning": "Para mover o lenzo, manteña a roda do rato ou a barra de espazo mentres arrastra", + "canvasPanning": "Para mover o lenzo, manteña pulsada a roda do rato ou a barra de espazo mentres arrastra, ou utilice a ferramenta da man", "linearElement": "Faga clic para iniciar varios puntos, arrastre para unha sola liña", "freeDraw": "Fai clic e arrastra, solta cando acabes", "text": "Consello: tamén podes engadir texto facendo dobre-clic en calquera lugar coa ferramenta de selección", @@ -245,7 +246,8 @@ "publishLibrary": "Publica a túa propia biblioteca", "bindTextToElement": "Prema a tecla enter para engadir texto", "deepBoxSelect": "Manteña pulsado CtrlOrCmd para seleccionar en profundidade e evitar o arrastre", - "eraserRevert": "Manteña pulsado Alt para reverter os elementos marcados para a súa eliminación" + "eraserRevert": "Manteña pulsado Alt para reverter os elementos marcados para a súa eliminación", + "firefox_clipboard_write": "Esta función pódese activar establecendo a opción \"dom.events.asyncClipboard.clipboardItem\" a \"true\". Para cambiar as opcións do navegador en Firefox, visita a páxina \"about:config\"." }, "canvasError": { "cannotShowPreview": "Non se pode mostrar a vista previa", diff --git a/src/locales/he-IL.json b/src/locales/he-IL.json index ba81af3ca..faa0013b6 100644 --- a/src/locales/he-IL.json +++ b/src/locales/he-IL.json @@ -219,7 +219,8 @@ "lock": "השאר את הכלי הנבחר פעיל גם לאחר סיום הציור", "penMode": "", "link": "הוספה/עדכון של קישור עבור הצורה הנבחרת", - "eraser": "מחק" + "eraser": "מחק", + "hand": "" }, "headings": { "canvasActions": "פעולות הלוח", @@ -227,7 +228,7 @@ "shapes": "צורות" }, "hints": { - "canvasPanning": "כדי להזיז את הקנבס לחצו על גלגל העכבר או על מקש הרווח תוך כדי גרירה", + "canvasPanning": "", "linearElement": "הקלק בשביל לבחור נקודות מרובות, גרור בשביל קו בודד", "freeDraw": "לחץ וגרור, שחרר כשסיימת", "text": "טיפ: אפשר להוסיף טקסט על ידי לחיצה כפולה בכל מקום עם כלי הבחירה", @@ -245,7 +246,8 @@ "publishLibrary": "פירסום ספריה אישית", "bindTextToElement": "יש להקיש Enter כדי להוסיף טקסט", "deepBoxSelect": "", - "eraserRevert": "" + "eraserRevert": "", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "לא הצלחנו להציג את התצוגה המקדימה", diff --git a/src/locales/hi-IN.json b/src/locales/hi-IN.json index b70c0d255..c0aca8982 100644 --- a/src/locales/hi-IN.json +++ b/src/locales/hi-IN.json @@ -219,7 +219,8 @@ "lock": "ड्राइंग के बाद चयनित टूल को सक्रिय रखें", "penMode": "पेन का मोड - स्पर्श टाले", "link": "", - "eraser": "रबड़" + "eraser": "रबड़", + "hand": "हाथ ( खिसकाने का औज़ार)" }, "headings": { "canvasActions": "कैनवास क्रिया", @@ -227,7 +228,7 @@ "shapes": "आकृतियाँ" }, "hints": { - "canvasPanning": "", + "canvasPanning": "कैनवास को सरकाने के लिए, ड्रैग करते समय माउस व्हील को पकड़े रखे या स्पेसबार को दबाए रखे, अथवा हाथ वाले औज़ार का उपयोग करें", "linearElement": "कई बिंदुओं को शुरू करने के लिए क्लिक करें, सिंगल लाइन के लिए खींचें", "freeDraw": "क्लिक करें और खींचें। समाप्त करने के लिए, छोड़ो", "text": "आप चयन टूल से कहीं भी डबल-क्लिक करके टेक्स्ट जोड़ सकते हैं", @@ -245,7 +246,8 @@ "publishLibrary": "", "bindTextToElement": "", "deepBoxSelect": "", - "eraserRevert": "मिटाने के लिए चुने हुए चीजों को ना चुनने के लिए Alt साथ में दबाए" + "eraserRevert": "मिटाने के लिए चुने हुए चीजों को ना चुनने के लिए Alt साथ में दबाए", + "firefox_clipboard_write": "\"dom.events.asyncClipboard.clipboardItem\" फ़्लैग को \"true\" पर सेट करके इस सुविधा को संभवतः सक्षम किया जा सकता है। Firefox में ब्राउज़र फ़्लैग बदलने के लिए, \"about:config\" पृष्ठ पर जाएँ।" }, "canvasError": { "cannotShowPreview": "पूर्वावलोकन नहीं दिखा सकते हैं", @@ -448,15 +450,15 @@ }, "welcomeScreen": { "app": { - "center_heading": "", - "center_heading_plus": "", - "menuHint": "" + "center_heading": "आपका सर्व डेटा ब्राउज़र के भीतर स्थानिक जगह पे सुरक्षित किया गया.", + "center_heading_plus": "बजाय आपको Excalidraw+ पर जाना है?", + "menuHint": "निर्यात, पसंद, भाषायें, ..." }, "defaults": { - "menuHint": "", - "center_heading": "", - "toolbarHint": "", - "helpHint": "" + "menuHint": "निर्यात, पसंद, और भी...", + "center_heading": "चित्रांकन। बनाया गया। सरल।", + "toolbarHint": "एक औजार चुने और चित्रकारी प्रारंभ करे!", + "helpHint": "शॉर्ट्कट और सहाय्य" } } } diff --git a/src/locales/hu-HU.json b/src/locales/hu-HU.json index 84dc9f61c..faaafed90 100644 --- a/src/locales/hu-HU.json +++ b/src/locales/hu-HU.json @@ -219,7 +219,8 @@ "lock": "Rajzolás után az aktív eszközt tartsa kijelölve", "penMode": "", "link": "Hivatkozás hozzáadása/frissítése a kiválasztott alakzathoz", - "eraser": "" + "eraser": "", + "hand": "" }, "headings": { "canvasActions": "Vászon műveletek", @@ -227,7 +228,7 @@ "shapes": "Alakzatok" }, "hints": { - "canvasPanning": "A vászon mozgatásához tartsd lenyomva az egér görgőjét vagy a szóköz billentyűt húzás közben", + "canvasPanning": "", "linearElement": "Kattintással görbe, az eger húzásával pedig egyenes nyilat rajzolhatsz", "freeDraw": "Kattints és húzd, majd engedd el, amikor végeztél", "text": "Tipp: A kijelölés eszközzel a dupla kattintás új szöveget hoz létre", @@ -245,7 +246,8 @@ "publishLibrary": "Tedd közzé saját könyvtáradat", "bindTextToElement": "Nyomd meg az Entert szöveg hozzáadáshoz", "deepBoxSelect": "Tartsd lenyomva a Ctrl/Cmd billentyűt a mély kijelöléshez és a húzás megakadályozásához", - "eraserRevert": "" + "eraserRevert": "", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "Előnézet nem jeleníthető meg", diff --git a/src/locales/id-ID.json b/src/locales/id-ID.json index 7804af62e..0632a87eb 100644 --- a/src/locales/id-ID.json +++ b/src/locales/id-ID.json @@ -219,7 +219,8 @@ "lock": "Biarkan alat yang dipilih aktif setelah menggambar", "penMode": "Mode pena - mencegah sentuhan", "link": "Tambah/Perbarui tautan untuk bentuk yang dipilih", - "eraser": "Penghapus" + "eraser": "Penghapus", + "hand": "" }, "headings": { "canvasActions": "Opsi Kanvas", @@ -227,7 +228,7 @@ "shapes": "Bentuk" }, "hints": { - "canvasPanning": "Untuk memindahkan kanvas, tekan roda mouse atau spasi ketika menarik", + "canvasPanning": "", "linearElement": "Klik untuk memulai banyak poin, seret untuk satu baris", "freeDraw": "Klik dan seret, lepaskan jika Anda selesai", "text": "Tip: Anda juga dapat menambahkan teks dengan klik ganda di mana saja dengan alat pemilihan", @@ -245,7 +246,8 @@ "publishLibrary": "Terbitkan pustaka Anda", "bindTextToElement": "Tekan enter untuk tambahkan teks", "deepBoxSelect": "Tekan Ctrl atau Cmd untuk memilih yang di dalam, dan mencegah penggeseran", - "eraserRevert": "Tahan Alt untuk mengembalikan elemen yang ditandai untuk dihapus" + "eraserRevert": "Tahan Alt untuk mengembalikan elemen yang ditandai untuk dihapus", + "firefox_clipboard_write": "Fitur ini dapat diaktifkan melalui pengaturan flag \"dom.events.asyncClipboard.clipboardItem\" ke \"true\". Untuk mengganti flag di Firefox, pergi ke laman \"about:config\"." }, "canvasError": { "cannotShowPreview": "Tidak dapat menampilkan pratinjau", diff --git a/src/locales/it-IT.json b/src/locales/it-IT.json index 9ef1fba53..790c63fe6 100644 --- a/src/locales/it-IT.json +++ b/src/locales/it-IT.json @@ -219,7 +219,8 @@ "lock": "Mantieni lo strumento selezionato attivo dopo aver disegnato", "penMode": "Modalità penna - previene il tocco", "link": "Aggiungi/ aggiorna il link per una forma selezionata", - "eraser": "Gomma" + "eraser": "Gomma", + "hand": "Mano (strumento di panoramica)" }, "headings": { "canvasActions": "Azioni sulla Tela", @@ -227,7 +228,7 @@ "shapes": "Forme" }, "hints": { - "canvasPanning": "Per spostare la tela, tieni premuta la rotella del mouse o la barra spaziatrice mentre la trascini", + "canvasPanning": "Per spostare la tela, tieni premuta la rotellina del mouse o la barra spaziatrice mentre trascini oppure usa lo strumento mano", "linearElement": "Clicca per iniziare una linea in più punti, trascina per singola linea", "freeDraw": "Clicca e trascina, rilascia quando avrai finito", "text": "Suggerimento: puoi anche aggiungere del testo facendo doppio clic ovunque con lo strumento di selezione", @@ -245,7 +246,8 @@ "publishLibrary": "Pubblica la tua libreria", "bindTextToElement": "Premi invio per aggiungere il testo", "deepBoxSelect": "Tieni premuto CtrlOCmd per selezionare in profondità e per impedire il trascinamento", - "eraserRevert": "Tieni premuto Alt per ripristinare gli elementi contrassegnati per l'eliminazione" + "eraserRevert": "Tieni premuto Alt per ripristinare gli elementi contrassegnati per l'eliminazione", + "firefox_clipboard_write": "Questa funzione può essere abilitata impostando il flag \"dom.events.asyncClipboard.clipboardItem\" su \"true\". Per modificare i flag del browser in Firefox, visitare la pagina \"about:config\"." }, "canvasError": { "cannotShowPreview": "Impossibile visualizzare l'anteprima", diff --git a/src/locales/ja-JP.json b/src/locales/ja-JP.json index 2ab2cb475..fad9b6f10 100644 --- a/src/locales/ja-JP.json +++ b/src/locales/ja-JP.json @@ -219,7 +219,8 @@ "lock": "描画後も使用中のツールを選択したままにする", "penMode": "ペンモード - タッチ防止", "link": "選択した図形のリンクを追加/更新", - "eraser": "消しゴム" + "eraser": "消しゴム", + "hand": "" }, "headings": { "canvasActions": "キャンバス操作", @@ -227,7 +228,7 @@ "shapes": "図形" }, "hints": { - "canvasPanning": "キャンバスを移動するには、マウスホイールまたはスペースバーを押しながらドラッグします", + "canvasPanning": "", "linearElement": "クリックすると複数の頂点からなる曲線を開始、ドラッグすると直線", "freeDraw": "クリックしてドラッグします。離すと終了します", "text": "ヒント: 選択ツールを使用して任意の場所をダブルクリックしてテキストを追加することもできます", @@ -245,7 +246,8 @@ "publishLibrary": "自分のライブラリを公開", "bindTextToElement": "Enterを押してテキストを追加", "deepBoxSelect": "CtrlOrCmd を押し続けることでドラッグを抑止し、深い選択を行います", - "eraserRevert": "Alt を押し続けることで削除マークされた要素を元に戻す" + "eraserRevert": "Alt を押し続けることで削除マークされた要素を元に戻す", + "firefox_clipboard_write": "この機能は、\"dom.events.asyncClipboard.clipboardItem\" フラグを \"true\" に設定することで有効になる可能性があります。Firefox でブラウザーの設定を変更するには、\"about:config\" ページを参照してください。" }, "canvasError": { "cannotShowPreview": "プレビューを表示できません", diff --git a/src/locales/kab-KAB.json b/src/locales/kab-KAB.json index 2818794e6..fdf94fa94 100644 --- a/src/locales/kab-KAB.json +++ b/src/locales/kab-KAB.json @@ -202,8 +202,8 @@ "invalidSVGString": "SVG armeɣtu.", "cannotResolveCollabServer": "Ulamek tuqqna s aqeddac n umyalel. Ma ulac uɣilif ales asali n usebter sakin eɛreḍ tikkelt-nniḍen.", "importLibraryError": "Ur d-ssalay ara tamkarḍit", - "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed": "Ulamek asekles deg uzadur n yisefka deg ugilal. Ma ikemmel wugur, isefk ad teskelseḍ afaylu s wudem adigan akken ad tetḥeqqeḍ ur tesruḥuyeḍ ara amahil-inek•inem.", + "collabSaveFailed_sizeExceeded": "Ulamek asekles deg uzadur n yisefka deg ugilal, taɣzut n usuneɣ tettban-d temqer aṭas. Isefk ad teskelseḍ afaylu s wudem adigan akken ad tetḥeqqeḍ ur tesruḥuyeḍ ara amahil-inek•inem." }, "toolBar": { "selection": "Tafrayt", @@ -217,9 +217,10 @@ "text": "Aḍris", "library": "Tamkarḍit", "lock": "Eǧǧ afecku n tefrayt yermed mbaɛd asuneɣ", - "penMode": "", + "penMode": "Askar n yimru - gdel tanalit", "link": "Rnu/leqqem aseɣwen i talɣa yettwafernen", - "eraser": "Sfeḍ" + "eraser": "Sfeḍ", + "hand": "Afus (afecku n usmutti n tmuɣli)" }, "headings": { "canvasActions": "Tigawin n teɣzut n usuneɣ", @@ -227,7 +228,7 @@ "shapes": "Talɣiwin" }, "hints": { - "canvasPanning": "Akken ad tesmuttiḍ taɣzut n usuneɣ, ṭṭef ṛṛuda n umumed, neɣ afeggag n tallunt mi ara tzuɣreḍ", + "canvasPanning": "", "linearElement": "Ssit akken ad tebduḍ aṭas n tenqiḍin, zuɣer i yiwen n yizirig", "freeDraw": "Ssit yerna zuɣer, serreḥ ticki tfukeḍ", "text": "Tixidest: tzemreḍ daɣen ad ternuḍ aḍris s usiti snat n tikkal anida tebɣiḍ s ufecku n tefrayt", @@ -245,7 +246,8 @@ "publishLibrary": "Siẓreg tamkarḍit-inek•inem", "bindTextToElement": "Ssed ɣef kcem akken ad ternuḍ aḍris", "deepBoxSelect": "Ṭṭef CtrlOrCmd akken ad tferneḍ s telqey, yerna ad trewleḍ i uzuɣer", - "eraserRevert": "Ssed Alt akken ad tsefsxeḍ iferdisen yettwacerḍen i tukksa" + "eraserRevert": "Ssed Alt akken ad tsefsxeḍ iferdisen yettwacerḍen i tukksa", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "Ulamek abeqqeḍ n teskant", diff --git a/src/locales/kk-KZ.json b/src/locales/kk-KZ.json index 6eae63dfc..25071aa5e 100644 --- a/src/locales/kk-KZ.json +++ b/src/locales/kk-KZ.json @@ -219,7 +219,8 @@ "lock": "", "penMode": "", "link": "", - "eraser": "" + "eraser": "", + "hand": "" }, "headings": { "canvasActions": "", @@ -245,7 +246,8 @@ "publishLibrary": "", "bindTextToElement": "", "deepBoxSelect": "", - "eraserRevert": "" + "eraserRevert": "", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "", diff --git a/src/locales/ko-KR.json b/src/locales/ko-KR.json index fc159a2ce..a0368663c 100644 --- a/src/locales/ko-KR.json +++ b/src/locales/ko-KR.json @@ -219,7 +219,8 @@ "lock": "선택된 도구 유지하기", "penMode": "펜 모드 - 터치 방지", "link": "선택한 도형에 대해서 링크를 추가/업데이트", - "eraser": "지우개" + "eraser": "지우개", + "hand": "" }, "headings": { "canvasActions": "캔버스 동작", @@ -227,7 +228,7 @@ "shapes": "모양" }, "hints": { - "canvasPanning": "캔버스를 옮기려면 마우스 휠이나 스페이스바를 누르고 드래그하기", + "canvasPanning": "", "linearElement": "여러 점을 연결하려면 클릭하고, 직선을 그리려면 바로 드래그하세요.", "freeDraw": "클릭 후 드래그하세요. 완료되면 놓으세요.", "text": "팁: 선택 툴로 아무 곳이나 더블 클릭해 텍스트를 추가할 수도 있습니다.", @@ -245,7 +246,8 @@ "publishLibrary": "당신만의 라이브러리를 게시하기", "bindTextToElement": "Enter 키를 눌러서 텍스트 추가하기", "deepBoxSelect": "CtrlOrCmd 키를 눌러서 깊게 선택하고, 드래그하지 않도록 하기", - "eraserRevert": "Alt를 눌러서 삭제하도록 지정된 요소를 되돌리기" + "eraserRevert": "Alt를 눌러서 삭제하도록 지정된 요소를 되돌리기", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "미리보기를 볼 수 없습니다", diff --git a/src/locales/ku-TR.json b/src/locales/ku-TR.json index 738bf5f8e..636a832af 100644 --- a/src/locales/ku-TR.json +++ b/src/locales/ku-TR.json @@ -219,7 +219,8 @@ "lock": "ئامێرە دیاریکراوەکان چالاک بهێڵەوە دوای وێنەکێشان", "penMode": "شێوازی قەڵەم - دەست لێدان ڕابگرە", "link": "زیادکردن/ نوێکردنەوەی لینک بۆ شێوەی دیاریکراو", - "eraser": "سڕەر" + "eraser": "سڕەر", + "hand": "" }, "headings": { "canvasActions": "کردارەکانی تابلۆ", @@ -227,7 +228,7 @@ "shapes": "شێوەکان" }, "hints": { - "canvasPanning": "بۆ جوڵاندنی تابلۆ، لە کاتی ڕاکێشاندا ویلی ماوس یان شریتی بۆشایی دابگرە", + "canvasPanning": "", "linearElement": "کرتە بکە بۆ دەستپێکردنی چەند خاڵێک، ڕایبکێشە بۆ یەک هێڵ", "freeDraw": "کرتە بکە و ڕایبکێشە، کاتێک تەواو بوویت دەست هەڵگرە", "text": "زانیاری: هەروەها دەتوانیت دەق زیادبکەیت بە دوو کرتەکردن لە هەر شوێنێک لەگەڵ ئامڕازی دەستنیشانکردن", @@ -245,7 +246,8 @@ "publishLibrary": "کتێبخانەی تایبەت بە خۆت بڵاوبکەرەوە", "bindTextToElement": "بۆ زیادکردنی دەق enter بکە", "deepBoxSelect": "CtrlOrCmd ڕابگرە بۆ هەڵبژاردنی قووڵ، و بۆ ڕێگریکردن لە ڕاکێشان", - "eraserRevert": "بۆ گەڕاندنەوەی ئەو توخمانەی کە بۆ سڕینەوە نیشانە کراون، Alt ڕابگرە" + "eraserRevert": "بۆ گەڕاندنەوەی ئەو توخمانەی کە بۆ سڕینەوە نیشانە کراون، Alt ڕابگرە", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "ناتوانرێ پێشبینین پیشان بدرێت", diff --git a/src/locales/lt-LT.json b/src/locales/lt-LT.json index b45b4fbb2..6eb83b7ac 100644 --- a/src/locales/lt-LT.json +++ b/src/locales/lt-LT.json @@ -219,7 +219,8 @@ "lock": "Baigus piešti, išlaikyti pasirinktą įrankį", "penMode": "Rašyklio režimas - neleisti prisilietimų", "link": "Pridėti / Atnaujinti pasirinktos figūros nuorodą", - "eraser": "Trintukas" + "eraser": "Trintukas", + "hand": "" }, "headings": { "canvasActions": "Veiksmai su drobe", @@ -227,7 +228,7 @@ "shapes": "Figūros" }, "hints": { - "canvasPanning": "Norint judinti drobę, judink pelę kartu įspaudus pelės ratuką arba tarpo klavišą", + "canvasPanning": "", "linearElement": "Paspaudimai sukurs papildomus taškus, nepertraukiamas tempimas sukurs liniją", "freeDraw": "Spausk ir tempk, paleisk kai norėsi pabaigti", "text": "Užuomina: tekstą taip pat galima pridėti bet kur su dvigubu pelės paspaudimu, kol parinkas žymėjimo įrankis", @@ -245,7 +246,8 @@ "publishLibrary": "", "bindTextToElement": "", "deepBoxSelect": "", - "eraserRevert": "" + "eraserRevert": "", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "", diff --git a/src/locales/lv-LV.json b/src/locales/lv-LV.json index 795330a9a..7341fd3fd 100644 --- a/src/locales/lv-LV.json +++ b/src/locales/lv-LV.json @@ -219,7 +219,8 @@ "lock": "Paturēt izvēlēto rīku pēc darbības", "penMode": "Pildspalvas režīms – novērst pieskaršanos", "link": "Pievienot/rediģēt atlasītās figūras saiti", - "eraser": "Dzēšgumija" + "eraser": "Dzēšgumija", + "hand": "" }, "headings": { "canvasActions": "Tāfeles darbības", @@ -227,7 +228,7 @@ "shapes": "Formas" }, "hints": { - "canvasPanning": "Lai bīdītu tāfeli, turiet nospiestu ritināšanas vai atstarpes taustiņu, velkot ar peli", + "canvasPanning": "", "linearElement": "Klikšķiniet, lai sāktu zīmēt vairākus punktus; velciet, lai zīmētu līniju", "freeDraw": "Spiediet un velciet; atlaidiet, kad pabeidzat", "text": "Ieteikums: lai pievienotu tekstu, varat arī jebkur dubultklikšķināt ar atlases rīku", @@ -245,7 +246,8 @@ "publishLibrary": "Publicēt savu bibliotēku", "bindTextToElement": "Spiediet ievades taustiņu, lai pievienotu tekstu", "deepBoxSelect": "Turient nospiestu Ctrl vai Cmd, lai atlasītu dziļumā un lai nepieļautu objektu pavilkšanu", - "eraserRevert": "Turiet Alt, lai noņemtu elementus no dzēsšanas atlases" + "eraserRevert": "Turiet Alt, lai noņemtu elementus no dzēsšanas atlases", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "Nevar rādīt priekšskatījumu", diff --git a/src/locales/mr-IN.json b/src/locales/mr-IN.json index 7ee3d0ced..6e198493d 100644 --- a/src/locales/mr-IN.json +++ b/src/locales/mr-IN.json @@ -219,7 +219,8 @@ "lock": "निवडलेले यंत्र चित्रकरण झाल्या नंतर ही सक्रिय ठेवा", "penMode": "पेन चा मोड - स्पर्श टाळा", "link": "निवडलेल्या आकारासाठी दुवा जोडा/बदल करा", - "eraser": "खोड रबर" + "eraser": "खोड रबर", + "hand": "हात ( सरकवण्या चे उपकरण)" }, "headings": { "canvasActions": "पटल क्रिया", @@ -227,7 +228,7 @@ "shapes": "आकार" }, "hints": { - "canvasPanning": "पटल हलवण्यासाठी, ड्रैग करताना माउस वील ला पकड़ा किव्हा स्पेसबार दाबून ठेवा", + "canvasPanning": "कॅनव्हास सरकवण्या साठी, ड्रॅग करताना माउस व्हील धरा किंवा स्पेसबार दाबून ठेवा अथवा हात वालं उपकरण वापरा", "linearElement": "अनेक बिंदु साठी क्लिक करा, रेघे साठी ड्रैग करा", "freeDraw": "क्लिक आणि ड्रैग करा, झालं तेव्हा सोडा", "text": "टीप: तुम्हीं निवड यंत्रानी कोठेही दुहेरी क्लिक करून टेक्स्ट जोडू शकता", @@ -245,7 +246,8 @@ "publishLibrary": "आपला खाजगी संग्रह प्रकाशित करा", "bindTextToElement": "मजकूर जोडण्यासाठी एंटर की दाबा", "deepBoxSelect": "खोल निवड ह्या साठी कंट्रोल किव्हा कमांड दाबून ठेवा, आणि बाहेर खेचणे वाचवण्या साठी पण", - "eraserRevert": "खोडण्या साठी घेतलेल्या वस्तु ना घेण्या साठी Alt दाबून ठेवावे" + "eraserRevert": "खोडण्या साठी घेतलेल्या वस्तु ना घेण्या साठी Alt दाबून ठेवावे", + "firefox_clipboard_write": "हे वैशिष्ट्य \"dom.events.asyncClipboard.clipboardItem\" फ्लॅग \"सत्य\" वर सेट करून शक्यतो सक्षम केले जाऊ शकते. Firefox मध्ये ब्राउझर फ्लॅग बदलण्यासाठी, \"about:config\" पृष्ठावर जा." }, "canvasError": { "cannotShowPreview": "पूर्वावलोकन दाखवू शकत नाही", @@ -448,15 +450,15 @@ }, "welcomeScreen": { "app": { - "center_heading": "", - "center_heading_plus": "", - "menuHint": "" + "center_heading": "तुमचा सर्व डेटा तुमच्या ब्राउझरमध्ये स्थानिक पातळीवर जतन केला जातो.", + "center_heading_plus": "त्याऐवजी तुम्हाला Excalidraw+ वर जायचे आहे का?", + "menuHint": "निर्यात, आवड़ी-निवडी, भाषा, ..." }, "defaults": { - "menuHint": "", - "center_heading": "", - "toolbarHint": "", - "helpHint": "" + "menuHint": "निर्यात, आवड़ी निवडी आणि आणकिही...", + "center_heading": "आकृत्या. काढणे. सोपे.", + "toolbarHint": "एक साधन निवडा आणि चित्रीकरण सुरु करा!", + "helpHint": "शॉर्टकट आणि सहाय्य" } } } diff --git a/src/locales/my-MM.json b/src/locales/my-MM.json index 5f2acd026..8b5e6859f 100644 --- a/src/locales/my-MM.json +++ b/src/locales/my-MM.json @@ -219,7 +219,8 @@ "lock": "ရွေးချယ်ထားသောကိရိယာကိုသာဆက်သုံး", "penMode": "", "link": "", - "eraser": "" + "eraser": "", + "hand": "" }, "headings": { "canvasActions": "ကားချပ်လုပ်ဆောင်ချက်", @@ -245,7 +246,8 @@ "publishLibrary": "", "bindTextToElement": "", "deepBoxSelect": "", - "eraserRevert": "" + "eraserRevert": "", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "နမူနာမပြသနိုင်ပါ", diff --git a/src/locales/nb-NO.json b/src/locales/nb-NO.json index 055e33a99..f8ac1e307 100644 --- a/src/locales/nb-NO.json +++ b/src/locales/nb-NO.json @@ -219,7 +219,8 @@ "lock": "Behold merket verktøy som aktivt", "penMode": "Pennemodus - forhindre berøring", "link": "Legg til / oppdater link for en valgt figur", - "eraser": "Viskelær" + "eraser": "Viskelær", + "hand": "Hånd (panoreringsverktøy)" }, "headings": { "canvasActions": "Handlinger: lerret", @@ -227,7 +228,7 @@ "shapes": "Former" }, "hints": { - "canvasPanning": "For å flytte lerretet, hold musehjulet eller mellomromstasten mens du drar", + "canvasPanning": "For å flytte lerretet, hold musehjulet eller mellomromstasten mens du drar, eller bruk hånd-verktøyet", "linearElement": "Klikk for å starte linje med flere punkter, eller dra for en enkel linje", "freeDraw": "Klikk og dra, slipp når du er ferdig", "text": "Tips: du kan også legge til tekst ved å dobbeltklikke hvor som helst med utvalgsverktøyet", @@ -245,7 +246,8 @@ "publishLibrary": "Publiser ditt eget bibliotek", "bindTextToElement": "Trykk Enter for å legge til tekst", "deepBoxSelect": "Hold CTRL/CMD for å markere dypt og forhindre flytting", - "eraserRevert": "Hold Alt for å reversere elementene merket for sletting" + "eraserRevert": "Hold Alt for å reversere elementene merket for sletting", + "firefox_clipboard_write": "Denne funksjonen kan sannsynligvis aktiveres ved å sette \"dom.events.asyncClipboard.clipboardItem\" flagget til \"true\". For å endre nettleserens flagg i Firefox, besøk \"about:config\"-siden." }, "canvasError": { "cannotShowPreview": "Kan ikke vise forhåndsvisning", diff --git a/src/locales/nl-NL.json b/src/locales/nl-NL.json index 3961f10fe..9152d3bbb 100644 --- a/src/locales/nl-NL.json +++ b/src/locales/nl-NL.json @@ -219,7 +219,8 @@ "lock": "Geselecteerde tool actief houden na tekenen", "penMode": "Pen modus - Blokkeer aanraken", "link": "", - "eraser": "Gum" + "eraser": "Gum", + "hand": "" }, "headings": { "canvasActions": "Canvasacties", @@ -227,7 +228,7 @@ "shapes": "Vormen" }, "hints": { - "canvasPanning": "Om canvas te verplaatsen, houd muiswiel of spatiebalk ingedrukt tijdens slepen", + "canvasPanning": "", "linearElement": "Klik om meerdere punten te starten, sleep voor één lijn", "freeDraw": "Klik en sleep, laat los als je klaar bent", "text": "Tip: je kunt tekst toevoegen door ergens dubbel te klikken met de selectietool", @@ -245,7 +246,8 @@ "publishLibrary": "Publiceer je eigen bibliotheek", "bindTextToElement": "", "deepBoxSelect": "", - "eraserRevert": "" + "eraserRevert": "", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "Kan voorbeeld niet tonen", diff --git a/src/locales/nn-NO.json b/src/locales/nn-NO.json index 20fc77651..fa740d258 100644 --- a/src/locales/nn-NO.json +++ b/src/locales/nn-NO.json @@ -219,7 +219,8 @@ "lock": "Hald fram med valt verktøy", "penMode": "", "link": "Legg til/ oppdater lenke til valt figur", - "eraser": "Viskelêr" + "eraser": "Viskelêr", + "hand": "" }, "headings": { "canvasActions": "Handlingar: lerret", @@ -227,7 +228,7 @@ "shapes": "Formar" }, "hints": { - "canvasPanning": "For å flytte lerretet, hald inne musehjulet eller mellomromstasten medan du dreg", + "canvasPanning": "", "linearElement": "Klikk for å starte linje med fleire punkt, eller drag for ei enkel linje", "freeDraw": "Klikk og drag, slepp når du er ferdig", "text": "Tips: du kan òg leggje til tekst ved å dobbeltklikke kor som helst med utvalgsverktyet", @@ -245,7 +246,8 @@ "publishLibrary": "Publiser ditt eige bibliotek", "bindTextToElement": "Trykk på enter for å legge til tekst", "deepBoxSelect": "Hald inne Ctrl / Cmd for å velje djupt, og forhindre flytting", - "eraserRevert": "Hald inne Alt for å reversere markering av element for sletting" + "eraserRevert": "Hald inne Alt for å reversere markering av element for sletting", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "Kan ikkje vise førehandsvising", diff --git a/src/locales/oc-FR.json b/src/locales/oc-FR.json index db03fb22b..c1d3ab341 100644 --- a/src/locales/oc-FR.json +++ b/src/locales/oc-FR.json @@ -38,8 +38,8 @@ "arrowhead_bar": "Barra", "arrowhead_dot": "Ponch", "arrowhead_triangle": "Triangle", - "fontSize": "Talha poliça", - "fontFamily": "Familha de poliça", + "fontSize": "Talha polissa", + "fontFamily": "Familha de polissa", "onlySelected": "Seleccion sonque", "withBackground": "Rèireplan", "exportEmbedScene": "Scèna embarcada", @@ -106,8 +106,8 @@ "toggleTheme": "Alternar tèma", "personalLib": "Bibliotèca personala", "excalidrawLib": "Bibliotèca Excalidraw", - "decreaseFontSize": "Reduire talha poliça", - "increaseFontSize": "Aumentar talha poliça", + "decreaseFontSize": "Reduire talha polissa", + "increaseFontSize": "Aumentar talha polissa", "unbindText": "Dessociar lo tèxte", "bindText": "Ligar lo tèxt al contenidor", "link": { @@ -219,7 +219,8 @@ "lock": "Mantenir activa l’aisina aprèp dessenhar", "penMode": "Mòde estilo - empachar lo contact", "link": "Apondre/Actualizar lo ligam per una fòrma seleccionada", - "eraser": "Goma" + "eraser": "Goma", + "hand": "" }, "headings": { "canvasActions": "Accions del canabàs", @@ -227,7 +228,7 @@ "shapes": "Formas" }, "hints": { - "canvasPanning": "Per desplaçar los canabasses, tenètz la rodeta de la mirga o la barra d’espaci pendent lo desplaçament", + "canvasPanning": "", "linearElement": "Clicatz per començar mantun punt, lisatz per una sola linha", "freeDraw": "Clicatz e lisatz, relargatz un còp acabat", "text": "Astúcia : podètz tanben apondre de tèxt en doble clicant ont que siá amb l’aisina de seleccion", @@ -245,7 +246,8 @@ "publishLibrary": "Publicar vòstra pròpria bibliotèca", "bindTextToElement": "Quichatz Entrada per apondre de tèxte", "deepBoxSelect": "Gardar CtrlOCmd per una seleccion gropada e empachar lo desplaçament", - "eraserRevert": "Tenètz quichat Alt per anullar los elements marcats per supression" + "eraserRevert": "Tenètz quichat Alt per anullar los elements marcats per supression", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "Afichatge impossible de l’apercebut", diff --git a/src/locales/pa-IN.json b/src/locales/pa-IN.json index 9e1bf6436..d5055a917 100644 --- a/src/locales/pa-IN.json +++ b/src/locales/pa-IN.json @@ -219,7 +219,8 @@ "lock": "ਡਰਾਇੰਗ ਤੋਂ ਬਾਅਦ ਵੀ ਚੁਣੇ ਹੋਏ ਸੰਦ ਨੂੰ ਸਰਗਰਮ ਰੱਖੋ ", "penMode": "", "link": "", - "eraser": "ਰਬੜ" + "eraser": "ਰਬੜ", + "hand": "" }, "headings": { "canvasActions": "ਕੈਨਵਸ ਦੀਆਂ ਕਾਰਵਾਈਆਂ", @@ -245,7 +246,8 @@ "publishLibrary": "ਆਪਣੀ ਲਾਇਬ੍ਰੇਰੀ ਪ੍ਰਕਾਸ਼ਿਤ ਕਰੋ", "bindTextToElement": "ਪਾਠ ਜੋੜਨ ਲਈ ਐੰਟਰ ਦਬਾਓ", "deepBoxSelect": "", - "eraserRevert": "" + "eraserRevert": "", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "ਝਲਕ ਨਹੀਂ ਦਿਖਾ ਸਕਦੇ", diff --git a/src/locales/percentages.json b/src/locales/percentages.json index 7a3f606ca..997a62077 100644 --- a/src/locales/percentages.json +++ b/src/locales/percentages.json @@ -1,49 +1,49 @@ { - "ar-SA": 87, - "bg-BG": 55, + "ar-SA": 92, + "bg-BG": 54, "bn-BD": 60, - "ca-ES": 94, + "ca-ES": 93, "cs-CZ": 75, "da-DK": 33, "de-DE": 100, - "el-GR": 100, + "el-GR": 99, "en": 100, "es-ES": 100, - "eu-ES": 100, - "fa-IR": 96, - "fi-FI": 93, + "eu-ES": 99, + "fa-IR": 95, + "fi-FI": 92, "fr-FR": 100, "gl-ES": 100, - "he-IL": 90, - "hi-IN": 69, + "he-IL": 89, + "hi-IN": 71, "hu-HU": 89, - "id-ID": 100, + "id-ID": 99, "it-IT": 100, - "ja-JP": 100, - "kab-KAB": 93, - "kk-KZ": 21, - "ko-KR": 99, - "ku-TR": 96, - "lt-LT": 64, - "lv-LV": 98, - "mr-IN": 98, + "ja-JP": 99, + "kab-KAB": 94, + "kk-KZ": 20, + "ko-KR": 98, + "ku-TR": 95, + "lt-LT": 63, + "lv-LV": 97, + "mr-IN": 100, "my-MM": 41, "nb-NO": 100, - "nl-NL": 91, - "nn-NO": 90, - "oc-FR": 98, + "nl-NL": 90, + "nn-NO": 89, + "oc-FR": 97, "pa-IN": 83, - "pl-PL": 85, - "pt-BR": 98, - "pt-PT": 100, - "ro-RO": 100, - "ru-RU": 98, + "pl-PL": 84, + "pt-BR": 97, + "pt-PT": 99, + "ro-RO": 99, + "ru-RU": 100, "si-LK": 8, "sk-SK": 100, "sl-SI": 100, - "sv-SE": 96, - "ta-IN": 93, - "tr-TR": 98, + "sv-SE": 100, + "ta-IN": 92, + "tr-TR": 97, "uk-UA": 96, "vi-VN": 20, "zh-CN": 100, diff --git a/src/locales/pl-PL.json b/src/locales/pl-PL.json index b02881e35..04cc86898 100644 --- a/src/locales/pl-PL.json +++ b/src/locales/pl-PL.json @@ -219,7 +219,8 @@ "lock": "Zablokuj wybrane narzędzie", "penMode": "", "link": "", - "eraser": "Gumka" + "eraser": "Gumka", + "hand": "" }, "headings": { "canvasActions": "Narzędzia", @@ -227,7 +228,7 @@ "shapes": "Kształty" }, "hints": { - "canvasPanning": "Aby przesunąć płótno, przytrzymaj kółko myszy lub spację podczas przeciągania", + "canvasPanning": "", "linearElement": "Naciśnij, aby zrobić punkt, przeciągnij, aby narysować linię", "freeDraw": "Naciśnij i przeciągnij by rysować, puść kiedy skończysz", "text": "Wskazówka: możesz również dodać tekst klikając dwukrotnie gdziekolwiek za pomocą narzędzia zaznaczania", @@ -245,7 +246,8 @@ "publishLibrary": "Opublikuj własną bibliotekę", "bindTextToElement": "", "deepBoxSelect": "", - "eraserRevert": "" + "eraserRevert": "", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "Nie można wyświetlić podglądu", diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json index 24da24ef3..dd3362137 100644 --- a/src/locales/pt-BR.json +++ b/src/locales/pt-BR.json @@ -219,7 +219,8 @@ "lock": "Manter ativa a ferramenta selecionada após desenhar", "penMode": "Modo caneta — impede o toque", "link": "Adicionar/Atualizar link para uma forma selecionada", - "eraser": "Borracha" + "eraser": "Borracha", + "hand": "" }, "headings": { "canvasActions": "Ações da tela", @@ -227,7 +228,7 @@ "shapes": "Formas" }, "hints": { - "canvasPanning": "Para mover a tela, segure a roda do mouse ou a barra de espaço enquanto arrasta", + "canvasPanning": "", "linearElement": "Clique para iniciar vários pontos, arraste para uma única linha", "freeDraw": "Toque e arraste, solte quando terminar", "text": "Dica: você também pode adicionar texto clicando duas vezes em qualquer lugar com a ferramenta de seleção", @@ -245,7 +246,8 @@ "publishLibrary": "Publicar sua própria biblioteca", "bindTextToElement": "Pressione Enter para adicionar o texto", "deepBoxSelect": "Segure Ctrl/Cmd para seleção profunda e para evitar arrastar", - "eraserRevert": "Segure a tecla Alt para inverter os elementos marcados para exclusão" + "eraserRevert": "Segure a tecla Alt para inverter os elementos marcados para exclusão", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "Não é possível mostrar pré-visualização", diff --git a/src/locales/pt-PT.json b/src/locales/pt-PT.json index 7a9dd400c..d72c7b98f 100644 --- a/src/locales/pt-PT.json +++ b/src/locales/pt-PT.json @@ -219,7 +219,8 @@ "lock": "Manter a ferramenta selecionada ativa após desenhar", "penMode": "Modo caneta - impedir toque", "link": "Acrescentar/ Adicionar ligação para uma forma seleccionada", - "eraser": "Borracha" + "eraser": "Borracha", + "hand": "" }, "headings": { "canvasActions": "Ações da área de desenho", @@ -227,7 +228,7 @@ "shapes": "Formas" }, "hints": { - "canvasPanning": "Para mover a tela, carregue na roda do rato ou na barra de espaço enquanto arrasta", + "canvasPanning": "", "linearElement": "Clique para iniciar vários pontos, arraste para uma única linha", "freeDraw": "Clique e arraste, large quando terminar", "text": "Dica: também pode adicionar texto clicando duas vezes em qualquer lugar com a ferramenta de seleção", @@ -245,7 +246,8 @@ "publishLibrary": "Publique a sua própria biblioteca", "bindTextToElement": "Carregue Enter para acrescentar texto", "deepBoxSelect": "Mantenha a tecla CtrlOrCmd carregada para selecção profunda, impedindo o arrastamento", - "eraserRevert": "Carregue também em Alt para reverter os elementos marcados para serem apagados" + "eraserRevert": "Carregue também em Alt para reverter os elementos marcados para serem apagados", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "Não é possível mostrar uma pré-visualização", diff --git a/src/locales/ro-RO.json b/src/locales/ro-RO.json index 4704426c1..ff76eea60 100644 --- a/src/locales/ro-RO.json +++ b/src/locales/ro-RO.json @@ -219,7 +219,8 @@ "lock": "Menține activ instrumentul selectat după desenare", "penMode": "Mod stilou – împiedică atingerea", "link": "Adăugare/actualizare URL pentru forma selectată", - "eraser": "Radieră" + "eraser": "Radieră", + "hand": "" }, "headings": { "canvasActions": "Acțiuni pentru pânză", @@ -227,7 +228,7 @@ "shapes": "Forme" }, "hints": { - "canvasPanning": "Pentru a muta pânză, ține apăsată rotița mausului sau bara de spațiu în timpul glisării", + "canvasPanning": "", "linearElement": "Dă clic pentru a crea mai multe puncte, glisează pentru a forma o singură linie", "freeDraw": "Dă clic pe pânză și glisează cursorul, apoi eliberează-l când ai terminat", "text": "Sfat: poți adăuga text și dând dublu clic oriunde cu instrumentul de selecție", @@ -245,7 +246,8 @@ "publishLibrary": "Publică propria bibliotecă", "bindTextToElement": "Apasă tasta Enter pentru a adăuga text", "deepBoxSelect": "Ține apăsată tasta Ctrl sau Cmd pentru a efectua selectarea de adâncime și pentru a preveni glisarea", - "eraserRevert": "Ține apăsată tasta Alt pentru a anula elementele marcate pentru ștergere" + "eraserRevert": "Ține apăsată tasta Alt pentru a anula elementele marcate pentru ștergere", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "Nu se poate afișa previzualizarea", diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json index d97c91971..ce785dc83 100644 --- a/src/locales/ru-RU.json +++ b/src/locales/ru-RU.json @@ -219,7 +219,8 @@ "lock": "Сохранять выбранный инструмент активным после рисования", "penMode": "Режим пера - предотвращение касания", "link": "Добавить/обновить ссылку для выбранной фигуры", - "eraser": "Ластик" + "eraser": "Ластик", + "hand": "Рука (перемещение холста)" }, "headings": { "canvasActions": "Операции холста", @@ -227,7 +228,7 @@ "shapes": "Фигуры" }, "hints": { - "canvasPanning": "Чтобы перемещать холст, удерживайте колесо мыши или пробел во время перетаскивания", + "canvasPanning": "Чтобы двигать холст, удерживайте колесо мыши или пробел во время перетаскивания, или используйте инструмент \"Рука\"", "linearElement": "Нажмите, чтобы начать несколько точек, перетащите для одной линии", "freeDraw": "Нажмите и перетаскивайте, отпустите по завершении", "text": "Совет: при выбранном инструменте выделения дважды щёлкните в любом месте, чтобы добавить текст", @@ -245,7 +246,8 @@ "publishLibrary": "Опубликовать свою собственную библиотеку", "bindTextToElement": "Нажмите Enter для добавления текста", "deepBoxSelect": "Удерживайте Ctrl или Cmd для глубокого выделения, чтобы предотвратить перетаскивание", - "eraserRevert": "Удерживайте Alt, чтобы вернуть элементы, отмеченные для удаления" + "eraserRevert": "Удерживайте Alt, чтобы вернуть элементы, отмеченные для удаления", + "firefox_clipboard_write": "Эта функция может быть включена при изменении значения флага \"dom.events.asyncClipboard.clipboardItem\" на \"true\". Чтобы изменить флаги браузера в Firefox, посетите страницу \"about:config\"." }, "canvasError": { "cannotShowPreview": "Не удается отобразить предпросмотр", @@ -448,15 +450,15 @@ }, "welcomeScreen": { "app": { - "center_heading": "", - "center_heading_plus": "", - "menuHint": "" + "center_heading": "Все ваши данные сохраняются локально в вашем браузере.", + "center_heading_plus": "Хотите перейти на Excalidraw+?", + "menuHint": "Экспорт, настройки, языки, ..." }, "defaults": { - "menuHint": "", - "center_heading": "", - "toolbarHint": "", - "helpHint": "" + "menuHint": "Экспорт, настройки и другое...", + "center_heading": "Диаграммы. Просто.", + "toolbarHint": "Выберите инструмент и начните рисовать!", + "helpHint": "Сочетания клавиш и помощь" } } } diff --git a/src/locales/si-LK.json b/src/locales/si-LK.json index 1567273da..dd458b31a 100644 --- a/src/locales/si-LK.json +++ b/src/locales/si-LK.json @@ -219,7 +219,8 @@ "lock": "", "penMode": "", "link": "", - "eraser": "" + "eraser": "", + "hand": "" }, "headings": { "canvasActions": "", @@ -245,7 +246,8 @@ "publishLibrary": "", "bindTextToElement": "", "deepBoxSelect": "", - "eraserRevert": "" + "eraserRevert": "", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "", diff --git a/src/locales/sk-SK.json b/src/locales/sk-SK.json index c3778c3e1..783de30af 100644 --- a/src/locales/sk-SK.json +++ b/src/locales/sk-SK.json @@ -219,7 +219,8 @@ "lock": "Nechať zvolený nástroj aktívny po skončení kreslenia", "penMode": "Režim pera – zabrániť dotyku", "link": "Pridať/ Upraviť odkaz pre vybraný tvar", - "eraser": "Guma" + "eraser": "Guma", + "hand": "Ruka (nástroj pre pohyb plátna)" }, "headings": { "canvasActions": "Akcie plátna", @@ -227,7 +228,7 @@ "shapes": "Tvary" }, "hints": { - "canvasPanning": "Pre pohyb plátna podržte koliesko myši alebo medzerník počas ťahania", + "canvasPanning": "Pre pohyb plátna podržte koliesko myši alebo medzerník počas ťahania, alebo použite nástroj ruka", "linearElement": "Kliknite na vloženie viacerých bodov, potiahnite na vytvorenie jednej priamky", "freeDraw": "Kliknite a ťahajte, pustite na ukončenie", "text": "Tip: text môžete pridať aj dvojklikom kdekoľvek, ak je zvolený nástroj výber", @@ -245,7 +246,8 @@ "publishLibrary": "Uverejniť vašu knižnicu", "bindTextToElement": "Stlačte enter na pridanie textu", "deepBoxSelect": "Podržte CtrlOrCmd na výber v skupine alebo zamedzeniu poťiahnutia", - "eraserRevert": "Podržte Alt pre prehodenie položiek určených na vymazanie" + "eraserRevert": "Podržte Alt pre prehodenie položiek určených na vymazanie", + "firefox_clipboard_write": "Táto sa funkcionalita sa dá zapnúť nastavením \"dom.events.asyncClipboard.clipboardItem\" na \"true\". Pre zmenu nastavení vo Firefox-e otvorte stránku \"about:config\"." }, "canvasError": { "cannotShowPreview": "Nie je možné zobraziť náhľad plátna", diff --git a/src/locales/sl-SI.json b/src/locales/sl-SI.json index 5f2359094..54274932f 100644 --- a/src/locales/sl-SI.json +++ b/src/locales/sl-SI.json @@ -219,7 +219,8 @@ "lock": "Ohrani izbrano orodje aktivno po risanju", "penMode": "Način peresa - prepreči dotik", "link": "Dodaj/posodobi povezavo za izbrano obliko", - "eraser": "Radirka" + "eraser": "Radirka", + "hand": "Roka (orodje za premikanje)" }, "headings": { "canvasActions": "Dejanja za platno", @@ -227,7 +228,7 @@ "shapes": "Oblike" }, "hints": { - "canvasPanning": "Za premik platna med vlečenjem držite kolesce miške ali preslednico", + "canvasPanning": "Za premikanje platna med vlečenjem držite kolesce miške ali preslednico ali uporabite orodje roka", "linearElement": "Kliknite za začetek več točk, povlecite za posamezno črto", "freeDraw": "Kliknite in povlecite, spustite, ko končate", "text": "Namig: besedilo lahko dodate tudi z dvoklikom kjer koli z orodjem za izbiro", @@ -245,7 +246,8 @@ "publishLibrary": "Objavi svojo knjižnico", "bindTextToElement": "Pritisnite tipko Enter za dodajanje besedila", "deepBoxSelect": "Držite tipko CtrlOrCmd za globoko izbiro in preprečitev vlečenja", - "eraserRevert": "Pridržite tipko Alt, da razveljavite elemente, označene za brisanje" + "eraserRevert": "Pridržite tipko Alt, da razveljavite elemente, označene za brisanje", + "firefox_clipboard_write": "To funkcijo lahko verjetno omogočite z nastavitvijo zastavice \"dom.events.asyncClipboard.clipboardItem\" na \"true\". Če želite spremeniti zastavice brskalnika v Firefoxu, obiščite stran \"about:config\"." }, "canvasError": { "cannotShowPreview": "Predogleda ni bilo mogoče prikazati", diff --git a/src/locales/sv-SE.json b/src/locales/sv-SE.json index 7f0ca8ab3..30638b9e4 100644 --- a/src/locales/sv-SE.json +++ b/src/locales/sv-SE.json @@ -1,7 +1,7 @@ { "labels": { "paste": "Klistra in", - "pasteAsPlaintext": "", + "pasteAsPlaintext": "Klistra som oformaterad text", "pasteCharts": "Klistra in diagram", "selectAll": "Markera alla", "multiSelect": "Lägg till element till markering", @@ -202,8 +202,8 @@ "invalidSVGString": "Ogiltig SVG.", "cannotResolveCollabServer": "Det gick inte att ansluta till samarbets-servern. Ladda om sidan och försök igen.", "importLibraryError": "Kunde inte ladda bibliotek", - "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed": "Det gick inte att spara i backend-databasen. Om problemen kvarstår bör du spara filen lokalt för att se till att du inte förlorar ditt arbete.", + "collabSaveFailed_sizeExceeded": "Det gick inte att spara till backend-databasen, whiteboarden verkar vara för stor. Du bör spara filen lokalt för att du inte ska förlora ditt arbete." }, "toolBar": { "selection": "Markering", @@ -219,7 +219,8 @@ "lock": "Håll valt verktyg aktivt efter ritande", "penMode": "Pennläge - förhindra touch", "link": "Lägg till / Uppdatera länk för en vald form", - "eraser": "Radergummi" + "eraser": "Radergummi", + "hand": "Hand (panoreringsverktyg)" }, "headings": { "canvasActions": "Canvas-åtgärder", @@ -227,7 +228,7 @@ "shapes": "Former" }, "hints": { - "canvasPanning": "För att flytta canvas, håll mushjulet eller mellanslagstangenten medan du drar", + "canvasPanning": "För att flytta whiteboarden, håll mushjulet eller mellanslagstangenten medan du drar eller använd handverktyget", "linearElement": "Klicka för att starta flera punkter, dra för en linje", "freeDraw": "Klicka och dra, släpp när du är klar", "text": "Tips: du kan också lägga till text genom att dubbelklicka var som helst med markeringsverktyget", @@ -238,14 +239,15 @@ "resize": "Du kan behålla proportioner genom att hålla SHIFT medan du ändrar storlek,\nhåller du ALT ändras storlek relativt mitten", "resizeImage": "Du kan ändra storlek fritt genom att hålla SHIFT,\nhåll ALT för att ändra storlek från mitten", "rotate": "Du kan begränsa vinklar genom att hålla SHIFT medan du roterar", - "lineEditor_info": "", + "lineEditor_info": "Håll Ctrl/Cmd och dubbelklicka eller tryck på Ctrl/Cmd + Enter för att redigera punkter", "lineEditor_pointSelected": "Tryck på Ta bort för att ta bort punkt(er), Ctrl + D eller Cmd + D för att duplicera, eller dra för att flytta", "lineEditor_nothingSelected": "Välj en punkt att redigera (håll SHIFT för att välja flera),\neller håll ned Alt och klicka för att lägga till nya punkter", "placeImage": "Klicka för att placera bilden, eller klicka och dra för att ställa in dess storlek manuellt", "publishLibrary": "Publicera ditt eget bibliotek", "bindTextToElement": "Tryck på Enter för att lägga till text", "deepBoxSelect": "Håll Ctrl eller Cmd för att djupvälja, och för att förhindra att dra", - "eraserRevert": "Håll Alt för att återställa de element som är markerade för borttagning" + "eraserRevert": "Håll Alt för att återställa de element som är markerade för borttagning", + "firefox_clipboard_write": "Denna funktion kan sannolikt aktiveras genom att ställa in \"dom.events.asyncClipboard.clipboardItem\" flaggan till \"true\". För att ändra webbläsarens flaggor i Firefox, besök \"about:config\" sidan." }, "canvasError": { "cannotShowPreview": "Kan inte visa förhandsgranskning", @@ -314,8 +316,8 @@ "zoomToFit": "Zooma för att rymma alla element", "zoomToSelection": "Zooma till markering", "toggleElementLock": "Lås/Lås upp valda", - "movePageUpDown": "", - "movePageLeftRight": "" + "movePageUpDown": "Flytta sida upp/ner", + "movePageLeftRight": "Flytta sida vänster/höger" }, "clearCanvasDialog": { "title": "Rensa canvas" @@ -397,7 +399,7 @@ "fileSavedToFilename": "Sparad till {filename}", "canvas": "canvas", "selection": "markering", - "pasteAsSingleElement": "" + "pasteAsSingleElement": "Använd {{shortcut}} för att klistra in som ett enda element,\neller klistra in i en befintlig textredigerare" }, "colors": { "ffffff": "Vit", @@ -448,15 +450,15 @@ }, "welcomeScreen": { "app": { - "center_heading": "", - "center_heading_plus": "", - "menuHint": "" + "center_heading": "All data sparas lokalt i din webbläsare.", + "center_heading_plus": "Ville du gå till Excalidraw+ istället?", + "menuHint": "Exportera, inställningar, språk, ..." }, "defaults": { - "menuHint": "", - "center_heading": "", - "toolbarHint": "", - "helpHint": "" + "menuHint": "Exportera, inställningar och mer...", + "center_heading": "Förenklade. Diagram.", + "toolbarHint": "Välj ett verktyg & börja rita!", + "helpHint": "Genvägar & hjälp" } } } diff --git a/src/locales/ta-IN.json b/src/locales/ta-IN.json index d01fabce8..2ff76b058 100644 --- a/src/locales/ta-IN.json +++ b/src/locales/ta-IN.json @@ -219,7 +219,8 @@ "lock": "தேர்ந்த கருவியை வரைந்த பின்பும் வைத்திரு", "penMode": "", "link": "தேர்தெடுத்த வடிவத்திற்குத் தொடுப்பைச் சேர்/ புதுப்பி", - "eraser": "அழிப்பி" + "eraser": "அழிப்பி", + "hand": "" }, "headings": { "canvasActions": "கித்தான் செயல்கள்", @@ -227,7 +228,7 @@ "shapes": "வடிவங்கள்" }, "hints": { - "canvasPanning": "கித்தானை நகர்த்த, பிடித்திழுக்கையில் சுட்டிச்சக்கரத்தை அ இடைவெளிப்பட்டையை அழுத்திப்பிடி", + "canvasPanning": "", "linearElement": "பல புள்ளிகளைத் துவக்க சொடுக்கு, ஒற்றை வரிக்கு பிடித்திழு", "freeDraw": "சொடுக்கி பிடித்திழு, முடித்ததும் விடுவி", "text": "துணுக்குதவி: தெரிவு கருவி கொண்டு எங்காவது இரு-சொடுக்கி உரையைச் சேர்க்கலாம்", @@ -245,7 +246,8 @@ "publishLibrary": "உம் சொந்த நூலகத்தைப் பிரசுரி", "bindTextToElement": "உரையைச் சேர்க்க enterஐ அழுத்து", "deepBoxSelect": "ஆழ்ந்துத் தேரவும் பிடித்திழுத்தலைத் தவிர்க்கவும் CtrlOrCmdஐ அழுத்திப்பிடி", - "eraserRevert": "" + "eraserRevert": "", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "முன்னோட்டம் காட்ட இயலவில்லை", diff --git a/src/locales/tr-TR.json b/src/locales/tr-TR.json index 44a0556e0..00260919c 100644 --- a/src/locales/tr-TR.json +++ b/src/locales/tr-TR.json @@ -219,7 +219,8 @@ "lock": "Seçilen aracı çizimden sonra aktif tut", "penMode": "Kalem modu - dokunmayı engelle", "link": "Seçilen şekil için bağlantı Ekle/Güncelle", - "eraser": "Silgi" + "eraser": "Silgi", + "hand": "" }, "headings": { "canvasActions": "Tuval eylemleri", @@ -227,7 +228,7 @@ "shapes": "Şekiller" }, "hints": { - "canvasPanning": "Tuvali taşımak için, tuvali sürüklerken aynı zamanda fare tekerleğine veya boşluk tuşuna basılı tutun", + "canvasPanning": "", "linearElement": "Birden fazla nokta için tıklayın, tek çizgi için sürükleyin", "freeDraw": "Tıkla ve sürükle, bitirdiğinde serbest bırak", "text": "İpucu: seçme aracıyla herhangi bir yere çift tıklayarak da yazı ekleyebilirsin", @@ -245,7 +246,8 @@ "publishLibrary": "Kendi kitaplığınızı yayınlayın", "bindTextToElement": "Enter tuşuna basarak metin ekleyin", "deepBoxSelect": "Ctrl/Cmd tuşuna basılı tutarak derin seçim yapın ya da sürüklemeyi engelleyin", - "eraserRevert": "Alt tuşuna basılı tutarak silinme için işaretlenmiş ögeleri tersine çevirin" + "eraserRevert": "Alt tuşuna basılı tutarak silinme için işaretlenmiş ögeleri tersine çevirin", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "Önizleme gösterilemiyor", diff --git a/src/locales/uk-UA.json b/src/locales/uk-UA.json index e02e0c07b..73597ce9d 100644 --- a/src/locales/uk-UA.json +++ b/src/locales/uk-UA.json @@ -219,7 +219,8 @@ "lock": "Залишити обраний інструмент після креслення", "penMode": "Режим пера - запобігання дотику", "link": "Додати/Оновити посилання для вибраної форми", - "eraser": "Очищувач" + "eraser": "Очищувач", + "hand": "" }, "headings": { "canvasActions": "Дії з полотном", @@ -227,7 +228,7 @@ "shapes": "Фігури" }, "hints": { - "canvasPanning": "Щоб перемістити полотно, утримуйте коліщатко миші або пробіл під час перетягування", + "canvasPanning": "", "linearElement": "Натисніть щоб додати кілька точок. Перетягніть щоб намалювати одну лінію", "freeDraw": "Натисніть і потягніть, відпустіть коли завершите", "text": "Порада: можна також додати текст, двічі клацнувши по будь-якому місці інструментом вибору", @@ -245,7 +246,8 @@ "publishLibrary": "Опублікувати свою власну бібліотеку", "bindTextToElement": "Натисніть Enter, щоб додати текст", "deepBoxSelect": "Втримуйте Ctrl/Cmd для глибокого виділення та щоб попередити перетягування", - "eraserRevert": "Втримуйте клавішу Alt, щоб повернути елементи позначені для видалення" + "eraserRevert": "Втримуйте клавішу Alt, щоб повернути елементи позначені для видалення", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "Не вдається показати попередній перегляд", diff --git a/src/locales/vi-VN.json b/src/locales/vi-VN.json index d3b1282e0..be5edd61d 100644 --- a/src/locales/vi-VN.json +++ b/src/locales/vi-VN.json @@ -66,9 +66,9 @@ "cartoonist": "Hoạt hình", "fileTitle": "", "colorPicker": "Chọn màu", - "canvasColors": "", - "canvasBackground": "", - "drawingCanvas": "", + "canvasColors": "Đã dùng trên canvas", + "canvasBackground": "Nền canvas", + "drawingCanvas": "Canvas vẽ", "layers": "Lớp", "actions": "Chức năng", "language": "Ngôn ngữ", @@ -219,7 +219,8 @@ "lock": "", "penMode": "", "link": "", - "eraser": "" + "eraser": "", + "hand": "" }, "headings": { "canvasActions": "", @@ -245,7 +246,8 @@ "publishLibrary": "", "bindTextToElement": "", "deepBoxSelect": "", - "eraserRevert": "" + "eraserRevert": "", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index cd573efd8..6020688a2 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -219,7 +219,8 @@ "lock": "绘制后保持所选的工具栏状态", "penMode": "笔模式 – 避免误触", "link": "为选中的形状添加/更新链接", - "eraser": "橡皮" + "eraser": "橡皮", + "hand": "抓手(平移工具)" }, "headings": { "canvasActions": "画布动作", @@ -227,7 +228,7 @@ "shapes": "形状" }, "hints": { - "canvasPanning": "要移动画布,请按住鼠标滚轮或空格键,再拖拽鼠标", + "canvasPanning": "要移动画布,请按住鼠标滚轮或空格键同时拖拽鼠标,或使用抓手工具。", "linearElement": "点击创建多个点 拖动创建直线", "freeDraw": "点击并拖动,完成时松开", "text": "提示:您也可以使用选择工具双击任意位置来添加文字", @@ -245,7 +246,8 @@ "publishLibrary": "发布您自己的素材库", "bindTextToElement": "按下 Enter 以添加文本", "deepBoxSelect": "按住 CtrlOrCmd 以深度选择,并避免拖拽", - "eraserRevert": "按住 Alt 以反选被标记删除的元素" + "eraserRevert": "按住 Alt 以反选被标记删除的元素", + "firefox_clipboard_write": "将高级配置首选项“dom.events.asyncClipboard.lipboarditem”设置为“true”可以启用此功能。要更改 Firefox 的高级配置首选项,请前往“about:config”页面。" }, "canvasError": { "cannotShowPreview": "无法显示预览", diff --git a/src/locales/zh-HK.json b/src/locales/zh-HK.json index f8968f2b7..4ff1176be 100644 --- a/src/locales/zh-HK.json +++ b/src/locales/zh-HK.json @@ -219,7 +219,8 @@ "lock": "", "penMode": "", "link": "", - "eraser": "" + "eraser": "", + "hand": "" }, "headings": { "canvasActions": "畫布動作", @@ -245,7 +246,8 @@ "publishLibrary": "", "bindTextToElement": "", "deepBoxSelect": "", - "eraserRevert": "" + "eraserRevert": "", + "firefox_clipboard_write": "" }, "canvasError": { "cannotShowPreview": "無法顯示預覽", diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index 21d7465af..28ac95a26 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -219,7 +219,8 @@ "lock": "可連續使用選取的工具", "penMode": "筆模式 - 避免觸摸", "link": "為所選的形狀增加\b/更新連結", - "eraser": "橡皮擦" + "eraser": "橡皮擦", + "hand": "手形(平移工具)" }, "headings": { "canvasActions": "canvas 動作", @@ -227,7 +228,7 @@ "shapes": "形狀" }, "hints": { - "canvasPanning": "若要移動畫布,請在拖曳時按住滑鼠滾輪或空白鍵", + "canvasPanning": "若要移動畫布,請在拖曳時按住滑鼠滾輪或空白鍵,或使用手形工具", "linearElement": "點擊以繪製多點曲線;或拖曳以繪製直線", "freeDraw": "點擊並拖曳來繪圖,放開即結束", "text": "提示:亦可使用選取工具在任何地方雙擊來加入文字", @@ -245,7 +246,8 @@ "publishLibrary": "發布個人資料庫", "bindTextToElement": "按下 Enter 以加入文字。", "deepBoxSelect": "按住 Ctrl 或 Cmd 以深度選取並避免拖曳", - "eraserRevert": "按住 Alt 以反選取已標記待刪除的元素" + "eraserRevert": "按住 Alt 以反選取已標記待刪除的元素", + "firefox_clipboard_write": "此功能有機會透過將 \"dom.events.asyncClipboard.clipboardItem\" 設定為 \"true\" 來開啟。\n若要變更 Firefox 瀏覽器的此設定值,請至 \"about:config\" 頁面。" }, "canvasError": { "cannotShowPreview": "無法顯示預覽", From 11e2f90ca1d726e609ece695f21a001ccb3b19cb Mon Sep 17 00:00:00 2001 From: Jang Min Date: Sat, 4 Feb 2023 21:33:40 +0900 Subject: [PATCH 165/276] feat: shortcut for clearCanvas confirmDialog (#6114) Co-authored-by: dwelle resolve https://github.com/excalidraw/excalidraw/issues/5818 --- src/actions/actionDeleteSelected.tsx | 4 ++- src/actions/shortcuts.ts | 2 ++ src/components/App.tsx | 10 ++++++- src/components/HelpDialog.tsx | 44 +++++++++++++++------------- 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/actions/actionDeleteSelected.tsx b/src/actions/actionDeleteSelected.tsx index a1820a330..86b985284 100644 --- a/src/actions/actionDeleteSelected.tsx +++ b/src/actions/actionDeleteSelected.tsx @@ -154,7 +154,9 @@ export const actionDeleteSelected = register({ }; }, contextItemLabel: "labels.delete", - keyTest: (event) => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE, + keyTest: (event, appState, elements) => + (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) && + !event[KEYS.CTRL_OR_CMD], PanelComponent: ({ elements, appState, updateData }) => ( = { toggleTheme: [getShortcutKey("Shift+Alt+D")], saveScene: [getShortcutKey("CtrlOrCmd+S")], loadScene: [getShortcutKey("CtrlOrCmd+O")], + clearCanvas: [getShortcutKey("CtrlOrCmd+Delete")], imageExport: [getShortcutKey("CtrlOrCmd+Shift+E")], cut: [getShortcutKey("CtrlOrCmd+X")], copy: [getShortcutKey("CtrlOrCmd+C")], diff --git a/src/components/App.tsx b/src/components/App.tsx index 7b2b5e40b..f01faa279 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -279,6 +279,8 @@ import { shouldShowBoundingBox } from "../element/transformHandles"; import { Fonts } from "../scene/Fonts"; import { actionPaste } from "../actions/actionClipboard"; import { actionToggleHandTool } from "../actions/actionCanvas"; +import { jotaiStore } from "../jotai"; +import { activeConfirmDialogAtom } from "./ActiveConfirmDialog"; const deviceContextInitialValue = { isSmScreen: false, @@ -1952,7 +1954,6 @@ class App extends React.Component { ); // Input handling - private onKeyDown = withBatchedUpdates( (event: React.KeyboardEvent | KeyboardEvent) => { // normalize `event.key` when CapsLock is pressed #2372 @@ -2194,6 +2195,13 @@ class App extends React.Component { event.stopPropagation(); } } + + if ( + event[KEYS.CTRL_OR_CMD] && + (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) + ) { + jotaiStore.set(activeConfirmDialogAtom, "clearCanvas"); + } }, ); diff --git a/src/components/HelpDialog.tsx b/src/components/HelpDialog.tsx index 69f3e6a5c..8cb775fc5 100644 --- a/src/components/HelpDialog.tsx +++ b/src/components/HelpDialog.tsx @@ -273,22 +273,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => { className="HelpDialog__island--editor" caption={t("helpDialog.editor")} > - - - - void }) => { ]} isOr={true} /> + + void }) => { label={t("labels.pasteAsPlaintext")} shortcuts={[getShortcutKey("CtrlOrCmd+Shift+V")]} /> + + + + {/* firefox supports clipboard API under a flag, so we'll show users what they can do in the error message */} {(probablySupportsClipboardBlob || isFirefox) && ( @@ -329,10 +337,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => { label={t("labels.pasteStyles")} shortcuts={[getShortcutKey("CtrlOrCmd+Alt+V")]} /> - Date: Sat, 4 Feb 2023 15:03:39 +0100 Subject: [PATCH 166/276] =?UTF-8?q?feat:=20show=20error=20message=20when?= =?UTF-8?q?=20not=20connected=20to=20internet=20while=20collabo=E2=80=A6?= =?UTF-8?q?=20(#6165)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dwelle Resolves https://github.com/excalidraw/excalidraw/issues/5994 --- src/components/LayerUI.tsx | 1 - src/css/theme.scss | 5 +++++ src/excalidraw-app/collab/Collab.tsx | 10 ++++++++++ src/excalidraw-app/index.scss | 17 +++++++++++++++++ src/excalidraw-app/index.tsx | 15 +++++++++++---- src/locales/en.json | 3 ++- 6 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index b7d765a8a..bea074d62 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -124,7 +124,6 @@ const LayerUI = ({ children, }: LayerUIProps) => { const device = useDevice(); - const tunnels = useInitializeTunnels(); const renderJSONExportDialog = () => { diff --git a/src/css/theme.scss b/src/css/theme.scss index 90b1bcdea..fd8067968 100644 --- a/src/css/theme.scss +++ b/src/css/theme.scss @@ -95,6 +95,9 @@ --color-gray-90: #1e1e1e; --color-gray-100: #121212; + --color-warning: #fceeca; + --color-text-warning: var(--text-primary-color); + --color-danger: #db6965; --color-promo: #e70078; @@ -163,6 +166,8 @@ --color-primary-darkest: #beb9ff; --color-primary-light: #4f4d6f; + --color-text-warning: var(--color-gray-80); + --color-danger: #ffa8a5; --color-promo: #d297ff; } diff --git a/src/excalidraw-app/collab/Collab.tsx b/src/excalidraw-app/collab/Collab.tsx index 4f98f58fd..1a996f032 100644 --- a/src/excalidraw-app/collab/Collab.tsx +++ b/src/excalidraw-app/collab/Collab.tsx @@ -75,6 +75,7 @@ import { jotaiStore } from "../../jotai"; export const collabAPIAtom = atom(null); export const collabDialogShownAtom = atom(false); export const isCollaboratingAtom = atom(false); +export const isOfflineAtom = atom(false); interface CollabState { errorMessage: string; @@ -152,6 +153,8 @@ class Collab extends PureComponent { componentDidMount() { window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload); + window.addEventListener("online", this.onOfflineStatusToggle); + window.addEventListener("offline", this.onOfflineStatusToggle); window.addEventListener(EVENT.UNLOAD, this.onUnload); const collabAPI: CollabAPI = { @@ -165,6 +168,7 @@ class Collab extends PureComponent { }; jotaiStore.set(collabAPIAtom, collabAPI); + this.onOfflineStatusToggle(); if ( process.env.NODE_ENV === ENV.TEST || @@ -180,7 +184,13 @@ class Collab extends PureComponent { } } + onOfflineStatusToggle = () => { + jotaiStore.set(isOfflineAtom, !window.navigator.onLine); + }; + componentWillUnmount() { + window.removeEventListener("online", this.onOfflineStatusToggle); + window.removeEventListener("offline", this.onOfflineStatusToggle); window.removeEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload); window.removeEventListener(EVENT.UNLOAD, this.onUnload); window.removeEventListener(EVENT.POINTER_MOVE, this.onPointerMove); diff --git a/src/excalidraw-app/index.scss b/src/excalidraw-app/index.scss index b9e693ec4..38c4c1a38 100644 --- a/src/excalidraw-app/index.scss +++ b/src/excalidraw-app/index.scss @@ -45,6 +45,23 @@ } } } + + .collab-offline-warning { + pointer-events: none; + position: absolute; + top: 6.5rem; + left: 50%; + transform: translateX(-50%); + padding: 0.5rem 1rem; + font-size: 0.875rem; + text-align: center; + line-height: 1.5; + border-radius: var(--border-radius-md); + background-color: var(--color-warning); + color: var(--color-text-warning); + z-index: 6; + white-space: pre; + } } .excalidraw-app.is-collaborating { diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index 6d6c9fe7d..fe2ac1a1d 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -52,6 +52,7 @@ import Collab, { collabAPIAtom, collabDialogShownAtom, isCollaboratingAtom, + isOfflineAtom, } from "./collab/Collab"; import { exportToBackend, @@ -66,10 +67,7 @@ import { } from "./data/localStorage"; import CustomStats from "./CustomStats"; import { restore, restoreAppState, RestoredDataState } from "../data/restore"; - -import "./index.scss"; import { ExportToExcalidrawPlus } from "./components/ExportToExcalidrawPlus"; - import { updateStaleImageStatuses } from "./data/FileManager"; import { newElementWith } from "../element/mutateElement"; import { isInitializedImageElement } from "../element/typeChecks"; @@ -77,7 +75,7 @@ import { loadFilesFromFirebase } from "./data/firebase"; import { LocalData } from "./data/LocalData"; import { isBrowserStorageStateNewer } from "./data/tabSync"; import clsx from "clsx"; -import { atom, Provider, useAtom } from "jotai"; +import { atom, Provider, useAtom, useAtomValue } from "jotai"; import { jotaiStore, useAtomWithInitialValue } from "../jotai"; import { reconcileElements } from "./collab/reconciliation"; import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library"; @@ -85,6 +83,8 @@ import { AppMainMenu } from "./components/AppMainMenu"; import { AppWelcomeScreen } from "./components/AppWelcomeScreen"; import { AppFooter } from "./components/AppFooter"; +import "./index.scss"; + polyfill(); window.EXCALIDRAW_THROTTLE_RENDER = true; @@ -599,6 +599,8 @@ const ExcalidrawWrapper = () => { localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems); }; + const isOffline = useAtomValue(isOfflineAtom); + return (
{ /> + {isCollaborating && isOffline && ( +
+ {t("alerts.collabOfflineWarning")} +
+ )} {excalidrawAPI && } {errorMessage && ( diff --git a/src/locales/en.json b/src/locales/en.json index 1a46b20e0..31005cb4c 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -193,7 +193,8 @@ "invalidSceneUrl": "Couldn't import scene from the supplied URL. It's either malformed, or doesn't contain valid Excalidraw JSON data.", "resetLibrary": "This will clear your library. Are you sure?", "removeItemsFromsLibrary": "Delete {{count}} item(s) from library?", - "invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled." + "invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled.", + "collabOfflineWarning": "No internet connection available.\nYour changes will not be saved!" }, "errors": { "unsupportedFileType": "Unsupported file type.", From c3c45a8c37bac75e9f21780283dcd6a55abc0840 Mon Sep 17 00:00:00 2001 From: Dejavu Moe Date: Tue, 7 Feb 2023 14:14:31 +0800 Subject: [PATCH 167/276] fix: docker build architecture:linux/amd64 error occur on linux/arm64 instance (#6197) fix docker build when in linux/arm64 use docker buildx plugin to build linux/amd64 image, a build error will occur causing the build to break --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f295f7f6a..d1fa424e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM node:14-alpine AS build WORKDIR /opt/node_app COPY package.json yarn.lock ./ -RUN yarn --ignore-optional +RUN yarn --ignore-optional --network-timeout 600000 ARG NODE_ENV=production From 8c1168ef3325dcbd74750076bc2264cbf23b8492 Mon Sep 17 00:00:00 2001 From: DanielJGeiger <1852529+DanielJGeiger@users.noreply.github.com> Date: Tue, 7 Feb 2023 01:11:20 -0600 Subject: [PATCH 168/276] refactor: Make the example React app reusable without duplication (#6188) --- src/packages/excalidraw/example/App.tsx | 24 ++++++++++++------ src/packages/excalidraw/example/index.tsx | 5 +++- .../example/public/{ => images}/doremon.png | Bin .../example/public/{ => images}/excalibot.png | Bin .../example/public/{ => images}/pika.jpeg | Bin .../example/public/{ => images}/rocket.jpeg | Bin .../example/sidebar/ExampleSidebar.tsx | 4 +-- 7 files changed, 22 insertions(+), 11 deletions(-) rename src/packages/excalidraw/example/public/{ => images}/doremon.png (100%) rename src/packages/excalidraw/example/public/{ => images}/excalibot.png (100%) rename src/packages/excalidraw/example/public/{ => images}/pika.jpeg (100%) rename src/packages/excalidraw/example/public/{ => images}/rocket.jpeg (100%) diff --git a/src/packages/excalidraw/example/App.tsx b/src/packages/excalidraw/example/App.tsx index e394d1a12..1f4a6c7fd 100644 --- a/src/packages/excalidraw/example/App.tsx +++ b/src/packages/excalidraw/example/App.tsx @@ -80,7 +80,13 @@ const COMMENT_ICON_DIMENSION = 32; const COMMENT_INPUT_HEIGHT = 50; const COMMENT_INPUT_WIDTH = 150; -export default function App() { +export interface AppProps { + appTitle: string; + useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void; + customArgs?: any[]; +} + +export default function App({ appTitle, useCustom, customArgs }: AppProps) { const appRef = useRef(null); const [viewModeEnabled, setViewModeEnabled] = useState(false); const [zenModeEnabled, setZenModeEnabled] = useState(false); @@ -107,6 +113,8 @@ export default function App() { const [excalidrawAPI, setExcalidrawAPI] = useState(null); + useCustom(excalidrawAPI, customArgs); + useHandleLibrary({ excalidrawAPI }); useEffect(() => { @@ -114,7 +122,7 @@ export default function App() { return; } const fetchData = async () => { - const res = await fetch("/rocket.jpeg"); + const res = await fetch("/images/rocket.jpeg"); const imageData = await res.blob(); const reader = new FileReader(); reader.readAsDataURL(imageData); @@ -150,7 +158,7 @@ export default function App() { /> )}
); @@ -525,7 +533,7 @@ export default function App() { }; return (
-

Excalidraw Example

+

{appTitle}

@@ -611,15 +619,15 @@ export default function App() { const collaborators = new Map(); collaborators.set("id1", { username: "Doremon", - avatarUrl: "doremon.png", + avatarUrl: "images/doremon.png", }); collaborators.set("id2", { username: "Excalibot", - avatarUrl: "excalibot.png", + avatarUrl: "images/excalibot.png", }); collaborators.set("id3", { username: "Pika", - avatarUrl: "pika.jpeg", + avatarUrl: "images/pika.jpeg", }); collaborators.set("id4", { username: "fallback", diff --git a/src/packages/excalidraw/example/index.tsx b/src/packages/excalidraw/example/index.tsx index 825a1016e..0f3bad30f 100644 --- a/src/packages/excalidraw/example/index.tsx +++ b/src/packages/excalidraw/example/index.tsx @@ -8,6 +8,9 @@ const root = createRoot(rootElement); root.render( - + {}} + /> , ); diff --git a/src/packages/excalidraw/example/public/doremon.png b/src/packages/excalidraw/example/public/images/doremon.png similarity index 100% rename from src/packages/excalidraw/example/public/doremon.png rename to src/packages/excalidraw/example/public/images/doremon.png diff --git a/src/packages/excalidraw/example/public/excalibot.png b/src/packages/excalidraw/example/public/images/excalibot.png similarity index 100% rename from src/packages/excalidraw/example/public/excalibot.png rename to src/packages/excalidraw/example/public/images/excalibot.png diff --git a/src/packages/excalidraw/example/public/pika.jpeg b/src/packages/excalidraw/example/public/images/pika.jpeg similarity index 100% rename from src/packages/excalidraw/example/public/pika.jpeg rename to src/packages/excalidraw/example/public/images/pika.jpeg diff --git a/src/packages/excalidraw/example/public/rocket.jpeg b/src/packages/excalidraw/example/public/images/rocket.jpeg similarity index 100% rename from src/packages/excalidraw/example/public/rocket.jpeg rename to src/packages/excalidraw/example/public/images/rocket.jpeg diff --git a/src/packages/excalidraw/example/sidebar/ExampleSidebar.tsx b/src/packages/excalidraw/example/sidebar/ExampleSidebar.tsx index 0fa5bf4e0..793d17b05 100644 --- a/src/packages/excalidraw/example/sidebar/ExampleSidebar.tsx +++ b/src/packages/excalidraw/example/sidebar/ExampleSidebar.tsx @@ -10,8 +10,8 @@ export default function Sidebar({ children }: { children: React.ReactNode }) { x
- - {" "} + + {" "}
From c9d18ecab685a8c1f52eba43a2d53314c339e6a2 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Thu, 9 Feb 2023 15:51:49 +0530 Subject: [PATCH 169/276] fix: don't allow blank space in collab name (#6211) * don't allow blank space in collab name * add spec * prevent blank --- src/clients.ts | 2 +- src/excalidraw-app/collab/RoomDialog.tsx | 2 +- src/tests/clients.test.ts | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/clients.ts b/src/clients.ts index e31b73eb9..9e1e6e144 100644 --- a/src/clients.ts +++ b/src/clients.ts @@ -21,7 +21,7 @@ export const getClientColors = (clientId: string, appState: AppState) => { }; export const getClientInitials = (userName?: string | null) => { - if (!userName) { + if (!userName?.trim()) { return "?"; } return userName.trim()[0].toUpperCase(); diff --git a/src/excalidraw-app/collab/RoomDialog.tsx b/src/excalidraw-app/collab/RoomDialog.tsx index d4adf9771..2c6949aac 100644 --- a/src/excalidraw-app/collab/RoomDialog.tsx +++ b/src/excalidraw-app/collab/RoomDialog.tsx @@ -144,7 +144,7 @@ const RoomDialog = ({ onUsernameChange(event.target.value)} onKeyPress={(event) => event.key === "Enter" && handleClose()} diff --git a/src/tests/clients.test.ts b/src/tests/clients.test.ts index f3fd174b4..a6a6901b1 100644 --- a/src/tests/clients.test.ts +++ b/src/tests/clients.test.ts @@ -36,4 +36,9 @@ describe("getClientInitials", () => { result = getClientInitials(null); expect(result).toBe("?"); }); + + it('returns "?" when value is blank', () => { + const result = getClientInitials(" "); + expect(result).toBe("?"); + }); }); From 71fb60394a43070b0564fa2f0fff562a26255ef9 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Mon, 13 Feb 2023 17:39:11 +0530 Subject: [PATCH 170/276] docs: enable Algolia for search (#6230) --- dev-docs/docusaurus.config.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dev-docs/docusaurus.config.js b/dev-docs/docusaurus.config.js index e24901f2e..390c619af 100644 --- a/dev-docs/docusaurus.config.js +++ b/dev-docs/docusaurus.config.js @@ -132,6 +132,11 @@ const config = { tableOfContents: { maxHeadingLevel: 4, }, + algolia: { + appId: "8FEAOD28DI", + apiKey: "4b07cca33ff2d2919bc95ff98f148e9e", + indexName: "excalidraw", + }, }), themes: ["@docusaurus/theme-live-codeblock"], plugins: ["docusaurus-plugin-sass"], From 0d7ee891e0c65c5cde078ab257fbf3894a2fda31 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Wed, 15 Feb 2023 10:41:11 +0530 Subject: [PATCH 171/276] feat: Make repair and refreshDimensions configurable in restoreElements (#6238) * fix: don't repair during reconcilation * Add opts to restoreElement and enable refreshDimensions and repair via config * remove * update changelog * fix tests * rename to repairBindings --- src/components/App.tsx | 2 +- src/data/blob.ts | 1 + src/data/restore.ts | 11 +++- src/excalidraw-app/collab/Collab.tsx | 2 +- src/excalidraw-app/data/index.ts | 5 +- src/excalidraw-app/index.tsx | 2 +- src/packages/excalidraw/CHANGELOG.md | 18 ++++++ src/tests/data/restore.test.ts | 92 +++++++++++++++++++++++----- 8 files changed, 112 insertions(+), 21 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index f01faa279..1fab2a2cf 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -836,7 +836,7 @@ class App extends React.Component { }, }; } - const scene = restore(initialData, null, null); + const scene = restore(initialData, null, null, { repairBindings: true }); scene.appState = { ...scene.appState, theme: this.props.theme || scene.appState.theme, diff --git a/src/data/blob.ts b/src/data/blob.ts index b5ca1dcad..473042b56 100644 --- a/src/data/blob.ts +++ b/src/data/blob.ts @@ -156,6 +156,7 @@ export const loadSceneOrLibraryFromBlob = async ( }, localAppState, localElements, + { repairBindings: true }, ), }; } else if (isValidLibrary(data)) { diff --git a/src/data/restore.ts b/src/data/restore.ts index bb779cf0f..0af2f9dc4 100644 --- a/src/data/restore.ts +++ b/src/data/restore.ts @@ -339,7 +339,7 @@ export const restoreElements = ( elements: ImportedDataState["elements"], /** NOTE doesn't serve for reconciliation */ localElements: readonly ExcalidrawElement[] | null | undefined, - refreshDimensions = false, + opts?: { refreshDimensions?: boolean; repairBindings?: boolean } | undefined, ): ExcalidrawElement[] => { const localElementsMap = localElements ? arrayToMap(localElements) : null; const restoredElements = (elements || []).reduce((elements, element) => { @@ -348,7 +348,7 @@ export const restoreElements = ( if (element.type !== "selection" && !isInvisiblySmallElement(element)) { let migratedElement: ExcalidrawElement | null = restoreElement( element, - refreshDimensions, + opts?.refreshDimensions, ); if (migratedElement) { const localElement = localElementsMap?.get(element.id); @@ -361,6 +361,10 @@ export const restoreElements = ( return elements; }, [] as ExcalidrawElement[]); + if (!opts?.repairBindings) { + return restoredElements; + } + // repair binding. Mutates elements. const restoredElementsMap = arrayToMap(restoredElements); for (const element of restoredElements) { @@ -497,9 +501,10 @@ export const restore = ( */ localAppState: Partial | null | undefined, localElements: readonly ExcalidrawElement[] | null | undefined, + elementsConfig?: { refreshDimensions?: boolean; repairBindings?: boolean }, ): RestoredDataState => { return { - elements: restoreElements(data?.elements, localElements), + elements: restoreElements(data?.elements, localElements, elementsConfig), appState: restoreAppState(data?.appState, localAppState || null), files: data?.files || {}, }; diff --git a/src/excalidraw-app/collab/Collab.tsx b/src/excalidraw-app/collab/Collab.tsx index 1a996f032..22f748773 100644 --- a/src/excalidraw-app/collab/Collab.tsx +++ b/src/excalidraw-app/collab/Collab.tsx @@ -610,7 +610,7 @@ class Collab extends PureComponent { const localElements = this.getSceneElementsIncludingDeleted(); const appState = this.excalidrawAPI.getAppState(); - remoteElements = restoreElements(remoteElements, null, false); + remoteElements = restoreElements(remoteElements, null); const reconciledElements = _reconcileElements( localElements, diff --git a/src/excalidraw-app/data/index.ts b/src/excalidraw-app/data/index.ts index a74c439f1..393c51580 100644 --- a/src/excalidraw-app/data/index.ts +++ b/src/excalidraw-app/data/index.ts @@ -263,9 +263,12 @@ export const loadScene = async ( await importFromBackend(id, privateKey), localDataState?.appState, localDataState?.elements, + { repairBindings: true }, ); } else { - data = restore(localDataState || null, null, null); + data = restore(localDataState || null, null, null, { + repairBindings: true, + }); } return { diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index fe2ac1a1d..9a778d27c 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -362,7 +362,7 @@ const ExcalidrawWrapper = () => { if (data.scene) { excalidrawAPI.updateScene({ ...data.scene, - ...restore(data.scene, null, null), + ...restore(data.scene, null, null, { repairBindings: true }), commitToHistory: true, }); } diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index d37020842..a809491d2 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -11,6 +11,24 @@ The change should be grouped under one of the below section and must contain PR Please add the latest change on the top under the correct section. --> +## Unreleased + +### Features + +- [`restoreElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restoreelements) API now takes an optional parameter `opts` which currently supports the below attributes + +```js +{ refreshDimensions?: boolean, repair?: boolean } +``` + +The same `opts` param has been added to [`restore`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restore) API as well. + +For more details refer to the [docs](https://docs.excalidraw.com) + +#### BREAKING CHANGE + +- The optional parameter `refreshDimensions` in [`restoreElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restoreelements) has been removed and can be enabled via `opts` + ## 0.14.2 (2023-02-01) ### Features diff --git a/src/tests/data/restore.test.ts b/src/tests/data/restore.test.ts index ef0f1a115..afd5ef918 100644 --- a/src/tests/data/restore.test.ts +++ b/src/tests/data/restore.test.ts @@ -534,7 +534,7 @@ describe("restore", () => { }); describe("repairing bindings", () => { - it("should repair container boundElements", () => { + it("should repair container boundElements when repair is true", () => { const container = API.createElement({ type: "rectangle", boundElements: [], @@ -546,11 +546,28 @@ describe("repairing bindings", () => { expect(container.boundElements).toEqual([]); - const restoredElements = restore.restoreElements( + let restoredElements = restore.restoreElements( [container, boundElement], null, ); + expect(restoredElements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [], + }), + expect.objectContaining({ + id: boundElement.id, + containerId: container.id, + }), + ]); + + restoredElements = restore.restoreElements( + [container, boundElement], + null, + { repairBindings: true }, + ); + expect(restoredElements).toEqual([ expect.objectContaining({ id: container.id, @@ -563,7 +580,7 @@ describe("repairing bindings", () => { ]); }); - it("should repair containerId of boundElements", () => { + it("should repair containerId of boundElements when repair is true", () => { const boundElement = API.createElement({ type: "text", containerId: null, @@ -573,11 +590,28 @@ describe("repairing bindings", () => { boundElements: [{ type: boundElement.type, id: boundElement.id }], }); - const restoredElements = restore.restoreElements( + let restoredElements = restore.restoreElements( [container, boundElement], null, ); + expect(restoredElements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [{ type: boundElement.type, id: boundElement.id }], + }), + expect.objectContaining({ + id: boundElement.id, + containerId: null, + }), + ]); + + restoredElements = restore.restoreElements( + [container, boundElement], + null, + { repairBindings: true }, + ); + expect(restoredElements).toEqual([ expect.objectContaining({ id: container.id, @@ -620,7 +654,7 @@ describe("repairing bindings", () => { ]); }); - it("should remove bindings of deleted elements from boundElements", () => { + it("should remove bindings of deleted elements from boundElements when repair is true", () => { const container = API.createElement({ type: "rectangle", boundElements: [], @@ -642,6 +676,8 @@ describe("repairing bindings", () => { type: invisibleBoundElement.type, id: invisibleBoundElement.id, }; + expect(container.boundElements).toEqual([]); + const nonExistentBinding = { type: "text", id: "non-existent" }; // @ts-ignore container.boundElements = [ @@ -650,17 +686,28 @@ describe("repairing bindings", () => { nonExistentBinding, ]; - expect(container.boundElements).toEqual([ - obsoleteBinding, - invisibleBinding, - nonExistentBinding, - ]); - - const restoredElements = restore.restoreElements( + let restoredElements = restore.restoreElements( [container, invisibleBoundElement, boundElement], null, ); + expect(restoredElements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [obsoleteBinding, invisibleBinding, nonExistentBinding], + }), + expect.objectContaining({ + id: boundElement.id, + containerId: container.id, + }), + ]); + + restoredElements = restore.restoreElements( + [container, invisibleBoundElement, boundElement], + null, + { repairBindings: true }, + ); + expect(restoredElements).toEqual([ expect.objectContaining({ id: container.id, @@ -673,7 +720,7 @@ describe("repairing bindings", () => { ]); }); - it("should remove containerId if container not exists", () => { + it("should remove containerId if container not exists when repair is true", () => { const boundElement = API.createElement({ type: "text", containerId: "non-existent", @@ -684,11 +731,28 @@ describe("repairing bindings", () => { isDeleted: true, }); - const restoredElements = restore.restoreElements( + let restoredElements = restore.restoreElements( [boundElement, boundElementDeleted], null, ); + expect(restoredElements).toEqual([ + expect.objectContaining({ + id: boundElement.id, + containerId: "non-existent", + }), + expect.objectContaining({ + id: boundElementDeleted.id, + containerId: "non-existent", + }), + ]); + + restoredElements = restore.restoreElements( + [boundElement, boundElementDeleted], + null, + { repairBindings: true }, + ); + expect(restoredElements).toEqual([ expect.objectContaining({ id: boundElement.id, From 9686141113be14eb646313e09de347917268787d Mon Sep 17 00:00:00 2001 From: Luka Hietala <95122845+LukaHietala@users.noreply.github.com> Date: Wed, 15 Feb 2023 07:31:07 +0200 Subject: [PATCH 172/276] docs: Fixed broken codesandbox link in the dev-docs (#6229) fixed broken link --- dev-docs/docs/introduction/contributing.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-docs/docs/introduction/contributing.mdx b/dev-docs/docs/introduction/contributing.mdx index 5e133ac8f..d384c97a1 100644 --- a/dev-docs/docs/introduction/contributing.mdx +++ b/dev-docs/docs/introduction/contributing.mdx @@ -20,7 +20,7 @@ Pull requests are welcome. For major changes, please [open an issue](https://git ### Option 2 - CodeSandbox -1. Go to https://codesandbox.io/s/github/excalidraw/excalidraw +1. Go to https://codesandbox.io/p/github/excalidraw/excalidraw 1. Connect your GitHub account 1. Go to Git tab on left side 1. Tap on `Fork Sandbox` From c587b85b4ee6f361fad6f8d7f82db4e93a7473a3 Mon Sep 17 00:00:00 2001 From: Milos Vetesnik Date: Wed, 15 Feb 2023 14:45:06 +0100 Subject: [PATCH 173/276] docs: new readme (#6240) Co-authored-by: David Luzar --- README.md | 140 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 111 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 21e2716d0..c5f7f5cd4 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,121 @@ -
- - Excalidraw logo: Sketch handrawn like diagrams. - -

Virtual whiteboard for sketching hand-drawn like diagrams.
Collaborative and end-to-end encrypted.

-

- - Follow Excalidraw on Twitter - - - Chat with us on Discord - -

+ + + + Excalidraw + + + +

+ Excalidraw Editor | + Blog | + Documentation | + Excalidraw+ +

+ +
+

+ An open source virtual hand-drawn style whiteboard.
+ Collaborative and end-to-end encrypted.
+
+

-## Try now +
+

+ + Excalidraw is released under the MIT license. + + + PRs welcome! + + + Chat on Discord + + + Follow Excalidraw on Twitter + +

-Visit [excalidraw.com](https://excalidraw.com) to start sketching. +
+
+ + Product showcase + +
+

+ Create beautiful hand-drawn like diagrams, wireframes, or whatever you like. +

+
+
+
-## Community +## Features -For latest updates, follow us on [twitter](https://twitter.com/excalidraw). If you need help or want to chat, join us on [Discord](https://discord.gg/UexuTaE). For releases and deep dives, check out our [blog](https://blog.excalidraw.com). Report bugs on [GitHub](https://github.com/excalidraw/excalidraw/issues). +The Excalidraw editor (npm package) supports: -## Supporting Excalidraw +- 💯 Free & open-source. +- 🎨 Infinite, canvas-based whiteboard. +- ✍️ Hand-drawn like style. +- 🌓 Dark mode. +- 🏗️ Customizable. +- 📷 Image support. +- 😀 Shape libraries support. +- 👅 Localization (i18n) support. +- 🖼️ Export to PNG, SVG & clipboard. +- 💾 Open format - export drawings as an `.excalidraw` json file. +- ⚒️ Wide range of tools - rectangle, circle, diamond, arrow, line, free-draw, eraser... +- ➡️ Arrow-binding & labeled arrows. +- 🔙 Undo / Redo. +- 🔍 Zoom and panning support. -If you like the project, you can become a sponsor at [Open Collective](https://opencollective.com/excalidraw). +## Excalidraw.com + +The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/maielo/new-readme/src/excalidraw-app) is part of this repository as well, and the app features: + +- 📡 PWA support (works offline). +- 🤼 Real-time collaboration. +- 🔒 End-to-end encryption. +- 💾 Local-first support (autosaves to the browser). +- 🔗 Shareable links (export to a readonly link you can share with others). + +We'll be adding these features as drop-in plugins for the npm package in the future. + +## Quick start + +Install the [Excalidraw npm package](https://www.npmjs.com/package/@excalidraw/excalidraw): + +``` +npm install react react-dom @excalidraw/excalidraw +``` + +or via yarn + +``` +yarn add react react-dom @excalidraw/excalidraw +``` + +Don't forget to check out our [Documentation](https://docs.excalidraw.com)! + +## Contributing + +- Missing something or found a bug? [Report here](https://github.com/excalidraw/excalidraw/issues). +- Want to contribute? Check out our [contribution guide](https://docs.excalidraw.com/docs/introduction/contributing) or let us know on [Discord](https://discord.gg/UexuTaE). +- Want to help with translations? See the [translation guide](https://docs.excalidraw.com/docs/introduction/contributing#translating). + +## Integrations + +- [VScode extension](https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor) +- [npm package](https://www.npmjs.com/package/@excalidraw/excalidraw) + +## Who's integrating Excalidraw + +[Google Cloud](https://googlecloudcheatsheet.withgoogle.com/architecture) • [Meta](https://meta.com/) • [CodeSandbox](https://codesandbox.io/) • [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) • [Replit](https://replit.com/) • [Slite](https://slite.com/) • [Notion](https://notion.so/) • [HackerRank](https://www.hackerrank.com/) • and many others + +## Sponsors & support + +If you like the project, you can become a sponsor at [Open Collective](https://opencollective.com/excalidraw) or use [Excalidraw+](https://plus.excalidraw.com/). + +## Thank you for supporting Excalidraw [](https://opencollective.com/excalidraw/tiers/sponsors/0/website) [](https://opencollective.com/excalidraw/tiers/sponsors/1/website) [](https://opencollective.com/excalidraw/tiers/sponsors/2/website) [](https://opencollective.com/excalidraw/tiers/sponsors/3/website) [](https://opencollective.com/excalidraw/tiers/sponsors/4/website) [](https://opencollective.com/excalidraw/tiers/sponsors/5/website) [](https://opencollective.com/excalidraw/tiers/sponsors/6/website) [](https://opencollective.com/excalidraw/tiers/sponsors/7/website) [](https://opencollective.com/excalidraw/tiers/sponsors/8/website) [](https://opencollective.com/excalidraw/tiers/sponsors/9/website) [](https://opencollective.com/excalidraw/tiers/sponsors/10/website) @@ -32,13 +124,3 @@ If you like the project, you can become a sponsor at [Open Collective](https://o Last but not least, we're thankful to these companies for offering their services for free: [![Vercel](./.github/assets/vercel.svg)](https://vercel.com) [![Sentry](./.github/assets/sentry.svg)](https://sentry.io) [![Crowdin](./.github/assets/crowdin.svg)](https://crowdin.com) - -## Developers - -You can integrate Excalidraw into your app by installing our [npm component](https://npmjs.com/package/@excalidraw/excalidraw). - -Visit our documentation on [https://docs.excalidraw.com](https://docs.excalidraw.com). - -## Who's integrating Excalidraw - -[Google Cloud](https://googlecloudcheatsheet.withgoogle.com/architecture) • [Meta](https://meta.com/) • [CodeSandbox](https://codesandbox.io/) • [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) • [Replit](https://replit.com/) • [Slite](https://slite.com/) • [Notion](https://notion.so/) • [HackerRank](https://www.hackerrank.com/) From b107c9af2a89866e0db856cf0371b3af07b50977 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Wed, 15 Feb 2023 15:14:15 +0100 Subject: [PATCH 174/276] docs: fix next.js example (#6241) --- dev-docs/docs/@excalidraw/excalidraw/integration.mdx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dev-docs/docs/@excalidraw/excalidraw/integration.mdx b/dev-docs/docs/@excalidraw/excalidraw/integration.mdx index 1fdcb8d8a..7080c32e9 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/integration.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/integration.mdx @@ -34,14 +34,16 @@ function App() { Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`. +The following worfklow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon. + ```jsx showLineNumbers import { useState, useEffect } from "react"; export default function App() { - const [Comp, setComp] = useState(null); + const [Excalidraw, setExcalidraw] = useState(null); useEffect(() => { - import("@excalidraw/excalidraw").then((comp) => setComp(comp.default)); + import("@excalidraw/excalidraw").then((comp) => setExcalidraw(comp.Excalidraw)); }, []); - return <>{Comp && }; + return <>{Excalidraw && }; } ``` From 5acb99777a0f9dd283f66667f13da1048bbb3d4f Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Thu, 16 Feb 2023 19:45:41 +0530 Subject: [PATCH 175/276] docs: fix typo (#6252) --- dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx | 2 +- src/packages/excalidraw/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx index 809c15a1b..198626eec 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx @@ -53,7 +53,7 @@ Parameter `refreshDimensions` indicates whether we should also `recalculate` tex **_Signature_**
-restoreElements(
+restore(
   data: ImportedDataState,
  localAppState: Partial<AppState> | null | undefined,
  localElements: ExcalidrawElement[] | null | undefined
): DataState diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index a809491d2..fc1c8e0f3 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -18,7 +18,7 @@ Please add the latest change on the top under the correct section. - [`restoreElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restoreelements) API now takes an optional parameter `opts` which currently supports the below attributes ```js -{ refreshDimensions?: boolean, repair?: boolean } +{ refreshDimensions?: boolean, repairBindings?: boolean } ``` The same `opts` param has been added to [`restore`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restore) API as well. From b9ba407f96fc4305443a4a3c0816b692a053378c Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Thu, 16 Feb 2023 20:46:51 +0530 Subject: [PATCH 176/276] feat: Bind text to container if double clicked on filled shape or stroke (#6250) * feat: bind text to container when clicked on filled shape or element stroke * Bind if double clicked on stroke as well * remove * specs * remove * shuffle * fix * back to normal --- src/components/App.tsx | 11 ++++- src/element/textWysiwyg.test.tsx | 75 ++++++++++++++++++++++++++++---- src/element/typeChecks.ts | 2 +- 3 files changed, 77 insertions(+), 11 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 1fab2a2cf..a48510bf9 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -226,6 +226,7 @@ import { setEraserCursor, updateActiveTool, getShortcutKey, + isTransparent, } from "../utils"; import { ContextMenu, @@ -2762,7 +2763,15 @@ class App extends React.Component { sceneY, ); if (container) { - if (isArrowElement(container) || hasBoundTextElement(container)) { + if ( + isArrowElement(container) || + hasBoundTextElement(container) || + !isTransparent(container.backgroundColor) || + isHittingElementNotConsideringBoundingBox(container, this.state, [ + sceneX, + sceneY, + ]) + ) { const midPoint = getContainerCenter(container, this.state); sceneX = midPoint.x; diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx index 83b3251d4..c61d4e68b 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/src/element/textWysiwyg.test.tsx @@ -463,14 +463,21 @@ describe("textWysiwyg", () => { }); }); - it("should bind text to container when double clicked on center of filled container", async () => { + it("should bind text to container when double clicked inside filled container", async () => { + const rectangle = API.createElement({ + type: "rectangle", + x: 10, + y: 20, + width: 90, + height: 75, + backgroundColor: "red", + }); + h.elements = [rectangle]; + expect(h.elements.length).toBe(1); expect(h.elements[0].id).toBe(rectangle.id); - mouse.doubleClickAt( - rectangle.x + rectangle.width / 2, - rectangle.y + rectangle.height / 2, - ); + mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10); expect(h.elements.length).toBe(2); const text = h.elements[1] as ExcalidrawTextElementWithContainer; @@ -504,24 +511,37 @@ describe("textWysiwyg", () => { }); h.elements = [rectangle]; + mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10); + expect(h.elements.length).toBe(2); + let text = h.elements[1] as ExcalidrawTextElementWithContainer; + expect(text.type).toBe("text"); + expect(text.containerId).toBe(null); + mouse.down(); + let editor = document.querySelector( + ".excalidraw-textEditorContainer > textarea", + ) as HTMLTextAreaElement; + await new Promise((r) => setTimeout(r, 0)); + editor.blur(); + mouse.doubleClickAt( rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 2, ); - expect(h.elements.length).toBe(2); + expect(h.elements.length).toBe(3); - const text = h.elements[1] as ExcalidrawTextElementWithContainer; + text = h.elements[1] as ExcalidrawTextElementWithContainer; expect(text.type).toBe("text"); expect(text.containerId).toBe(rectangle.id); + mouse.down(); - const editor = document.querySelector( + editor = document.querySelector( ".excalidraw-textEditorContainer > textarea", ) as HTMLTextAreaElement; fireEvent.change(editor, { target: { value: "Hello World!" } }); - await new Promise((r) => setTimeout(r, 0)); editor.blur(); + expect(rectangle.boundElements).toStrictEqual([ { id: text.id, type: "text" }, ]); @@ -551,6 +571,43 @@ describe("textWysiwyg", () => { ]); }); + it("should bind text to container when double clicked on container stroke", async () => { + const rectangle = API.createElement({ + type: "rectangle", + x: 10, + y: 20, + width: 90, + height: 75, + strokeWidth: 4, + }); + h.elements = [rectangle]; + + expect(h.elements.length).toBe(1); + expect(h.elements[0].id).toBe(rectangle.id); + + mouse.doubleClickAt(rectangle.x + 2, rectangle.y + 2); + expect(h.elements.length).toBe(2); + + const text = h.elements[1] as ExcalidrawTextElementWithContainer; + expect(text.type).toBe("text"); + expect(text.containerId).toBe(rectangle.id); + expect(rectangle.boundElements).toStrictEqual([ + { id: text.id, type: "text" }, + ]); + mouse.down(); + const editor = document.querySelector( + ".excalidraw-textEditorContainer > textarea", + ) as HTMLTextAreaElement; + + fireEvent.change(editor, { target: { value: "Hello World!" } }); + + await new Promise((r) => setTimeout(r, 0)); + editor.blur(); + expect(rectangle.boundElements).toStrictEqual([ + { id: text.id, type: "text" }, + ]); + }); + it("shouldn't bind to non-text-bindable containers", async () => { const freedraw = API.createElement({ type: "freedraw", diff --git a/src/element/typeChecks.ts b/src/element/typeChecks.ts index 72088e727..a6b6cb2db 100644 --- a/src/element/typeChecks.ts +++ b/src/element/typeChecks.ts @@ -137,7 +137,7 @@ export const isExcalidrawElement = (element: any): boolean => { export const hasBoundTextElement = ( element: ExcalidrawElement | null, -): element is ExcalidrawBindableElement => { +): element is MarkNonNullable => { return ( isBindableElement(element) && !!element.boundElements?.some(({ type }) => type === "text") From 0fcbddda8ebdbf6ec381eeed60e245e03e1739f5 Mon Sep 17 00:00:00 2001 From: Jan Klass Date: Mon, 20 Feb 2023 10:44:25 +0100 Subject: [PATCH 177/276] docs: Fix outdated link in README.md (#6263) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c5f7f5cd4..31ee567de 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ The Excalidraw editor (npm package) supports: ## Excalidraw.com -The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/maielo/new-readme/src/excalidraw-app) is part of this repository as well, and the app features: +The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/src/excalidraw-app) is part of this repository as well, and the app features: - 📡 PWA support (works offline). - 🤼 Real-time collaboration. From 88ff32e9b389b15577f0a6fa73df92a6eda7f914 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Tue, 21 Feb 2023 12:36:43 +0530 Subject: [PATCH 178/276] fix: improve text wrapping in ellipse and alignment (#6172) * fix: improve text wrapping in ellipse * compute height when font properties updated * fix alignment * fix alignment when resizing * fix * ad padding * always compute height when redrawing bounding box and refactor * lint * fix specs * fix * redraw text bounding box when pasted or refreshed * fix * Add specs * fix * restore on font load * add comments --- src/components/App.tsx | 10 ++ src/element/newElement.ts | 10 ++ src/element/textElement.test.ts | 53 ++++++++- src/element/textElement.ts | 194 +++++++++++++++++++------------ src/element/textWysiwyg.test.tsx | 52 ++++----- src/element/textWysiwyg.tsx | 14 ++- 6 files changed, 225 insertions(+), 108 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index a48510bf9..effa604bf 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -108,6 +108,7 @@ import { textWysiwyg, transformElements, updateTextElement, + redrawTextBoundingBox, } from "../element"; import { bindOrUnbindLinearElement, @@ -264,6 +265,7 @@ import { getBoundTextElement, getContainerCenter, getContainerDims, + getContainerElement, getTextBindableContainerAtPosition, isValidTextContainer, } from "../element/textElement"; @@ -1637,6 +1639,14 @@ class App extends React.Component { } this.scene.replaceAllElements(nextElements); + + nextElements.forEach((nextElement) => { + if (isTextElement(nextElement) && isBoundToContainer(nextElement)) { + const container = getContainerElement(nextElement); + redrawTextBoundingBox(nextElement, container); + } + }); + this.history.resumeRecording(); this.setState( diff --git a/src/element/newElement.ts b/src/element/newElement.ts index 8e7b8ee8a..c7f33a462 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -290,6 +290,11 @@ export const getMaxContainerWidth = (container: ExcalidrawElement) => { return BOUND_TEXT_PADDING * 8 * 2; } return containerWidth; + } else if (container.type === "ellipse") { + // The width of the largest rectangle inscribed inside an ellipse is + // Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from + // equation of an ellipse -https://github.com/excalidraw/excalidraw/pull/6172 + return Math.round((width / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2; } return width - BOUND_TEXT_PADDING * 2; }; @@ -306,6 +311,11 @@ export const getMaxContainerHeight = (container: ExcalidrawElement) => { return BOUND_TEXT_PADDING * 8 * 2; } return height; + } else if (container.type === "ellipse") { + // The height of the largest rectangle inscribed inside an ellipse is + // Math.round((ellipse.height / 2) * Math.sqrt(2)) which is derived from + // equation of an ellipse - https://github.com/excalidraw/excalidraw/pull/6172 + return Math.round((height / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2; } return height - BOUND_TEXT_PADDING * 2; }; diff --git a/src/element/textElement.test.ts b/src/element/textElement.test.ts index e1b9ff6f0..91974aca2 100644 --- a/src/element/textElement.test.ts +++ b/src/element/textElement.test.ts @@ -1,5 +1,11 @@ import { BOUND_TEXT_PADDING } from "../constants"; -import { measureText, wrapText } from "./textElement"; +import { API } from "../tests/helpers/api"; +import { + computeContainerHeightForBoundText, + getContainerCoords, + measureText, + wrapText, +} from "./textElement"; import { FontString } from "./types"; describe("Test wrapText", () => { @@ -193,4 +199,49 @@ describe("Test measureText", () => {
`); }); + + describe("Test getContainerCoords", () => { + const params = { width: 200, height: 100, x: 10, y: 20 }; + it("should compute coords correctly when ellipse", () => { + const ellipse = API.createElement({ + type: "ellipse", + ...params, + }); + expect(getContainerCoords(ellipse)).toEqual({ + x: 44.2893218813452455, + y: 39.64466094067262, + }); + }); + it("should compute coords correctly when rectangle", () => { + const rectangle = API.createElement({ + type: "rectangle", + ...params, + }); + expect(getContainerCoords(rectangle)).toEqual({ + x: 10, + y: 20, + }); + }); + }); + + describe("Test computeContainerHeightForBoundText", () => { + const params = { + width: 178, + height: 194, + }; + it("should compute container height correctly for rectangle", () => { + const element = API.createElement({ + type: "rectangle", + ...params, + }); + expect(computeContainerHeightForBoundText(element, 150)).toEqual(160); + }); + it("should compute container height correctly for ellipse", () => { + const element = API.createElement({ + type: "ellipse", + ...params, + }); + expect(computeContainerHeightForBoundText(element, 150)).toEqual(212); + }); + }); }); diff --git a/src/element/textElement.ts b/src/element/textElement.ts index c726c1c3f..ed4d27629 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -44,68 +44,69 @@ export const redrawTextBoundingBox = ( container: ExcalidrawElement | null, ) => { let maxWidth = undefined; - let text = textElement.text; + + const boundTextUpdates = { + x: textElement.x, + y: textElement.y, + text: textElement.text, + width: textElement.width, + height: textElement.height, + baseline: textElement.baseline, + }; + + boundTextUpdates.text = textElement.text; + if (container) { maxWidth = getMaxContainerWidth(container); - text = wrapText( + boundTextUpdates.text = wrapText( textElement.originalText, getFontString(textElement), maxWidth, ); } - const metrics = measureText(text, getFontString(textElement), maxWidth); - let coordY = textElement.y; - let coordX = textElement.x; - // Resize container and vertically center align the text + const metrics = measureText( + boundTextUpdates.text, + getFontString(textElement), + maxWidth, + ); + + boundTextUpdates.width = metrics.width; + boundTextUpdates.height = metrics.height; + boundTextUpdates.baseline = metrics.baseline; + if (container) { - if (!isArrowElement(container)) { - const containerDims = getContainerDims(container); - let nextHeight = containerDims.height; - if (textElement.verticalAlign === VERTICAL_ALIGN.TOP) { - coordY = container.y; - } else if (textElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) { - coordY = - container.y + - containerDims.height - - metrics.height - - BOUND_TEXT_PADDING; - } else { - coordY = container.y + containerDims.height / 2 - metrics.height / 2; - if (metrics.height > getMaxContainerHeight(container)) { - nextHeight = metrics.height + BOUND_TEXT_PADDING * 2; - coordY = container.y + nextHeight / 2 - metrics.height / 2; - } - } - if (textElement.textAlign === TEXT_ALIGN.LEFT) { - coordX = container.x + BOUND_TEXT_PADDING; - } else if (textElement.textAlign === TEXT_ALIGN.RIGHT) { - coordX = - container.x + - containerDims.width - - metrics.width - - BOUND_TEXT_PADDING; - } else { - coordX = container.x + containerDims.width / 2 - metrics.width / 2; - } - updateOriginalContainerCache(container.id, nextHeight); - mutateElement(container, { height: nextHeight }); - } else { + if (isArrowElement(container)) { const centerX = textElement.x + textElement.width / 2; const centerY = textElement.y + textElement.height / 2; const diffWidth = metrics.width - textElement.width; const diffHeight = metrics.height - textElement.height; - coordY = centerY - (textElement.height + diffHeight) / 2; - coordX = centerX - (textElement.width + diffWidth) / 2; + boundTextUpdates.x = centerY - (textElement.height + diffHeight) / 2; + boundTextUpdates.y = centerX - (textElement.width + diffWidth) / 2; + } else { + const containerDims = getContainerDims(container); + let maxContainerHeight = getMaxContainerHeight(container); + + let nextHeight = containerDims.height; + if (metrics.height > maxContainerHeight) { + nextHeight = computeContainerHeightForBoundText( + container, + metrics.height, + ); + mutateElement(container, { height: nextHeight }); + maxContainerHeight = getMaxContainerHeight(container); + updateOriginalContainerCache(container.id, nextHeight); + } + const updatedTextElement = { + ...textElement, + ...boundTextUpdates, + } as ExcalidrawTextElementWithContainer; + const { x, y } = computeBoundTextPosition(container, updatedTextElement); + boundTextUpdates.x = x; + boundTextUpdates.y = y; } } - mutateElement(textElement, { - width: metrics.width, - height: metrics.height, - baseline: metrics.baseline, - y: coordY, - x: coordX, - text, - }); + + mutateElement(textElement, boundTextUpdates); }; export const bindTextToShapeAfterDuplication = ( @@ -197,7 +198,11 @@ export const handleBindTextResize = ( } // increase height in case text element height exceeds if (nextHeight > maxHeight) { - containerHeight = nextHeight + getBoundTextElementOffset(textElement) * 2; + containerHeight = computeContainerHeightForBoundText( + container, + nextHeight, + ); + const diff = containerHeight - containerDims.height; // fix the y coord when resizing from ne/nw/n const updatedY = @@ -217,48 +222,57 @@ export const handleBindTextResize = ( text, width: nextWidth, height: nextHeight, - baseline: nextBaseLine, }); + if (!isArrowElement(container)) { - updateBoundTextPosition( - container, - textElement as ExcalidrawTextElementWithContainer, + mutateElement( + textElement, + computeBoundTextPosition( + container, + textElement as ExcalidrawTextElementWithContainer, + ), ); } } }; -const updateBoundTextPosition = ( +const computeBoundTextPosition = ( container: ExcalidrawElement, boundTextElement: ExcalidrawTextElementWithContainer, ) => { - const containerDims = getContainerDims(container); - const boundTextElementPadding = getBoundTextElementOffset(boundTextElement); + const containerCoords = getContainerCoords(container); + const maxContainerHeight = getMaxContainerHeight(container); + const maxContainerWidth = getMaxContainerWidth(container); + const padding = container.type === "ellipse" ? 0 : BOUND_TEXT_PADDING; + + let x; let y; if (boundTextElement.verticalAlign === VERTICAL_ALIGN.TOP) { - y = container.y + boundTextElementPadding; + y = containerCoords.y + padding; } else if (boundTextElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) { y = - container.y + - containerDims.height - - boundTextElement.height - - boundTextElementPadding; + containerCoords.y + + (maxContainerHeight - boundTextElement.height + padding); } else { - y = container.y + containerDims.height / 2 - boundTextElement.height / 2; + y = + containerCoords.y + + (maxContainerHeight / 2 - boundTextElement.height / 2 + padding); } - const x = - boundTextElement.textAlign === TEXT_ALIGN.LEFT - ? container.x + boundTextElementPadding - : boundTextElement.textAlign === TEXT_ALIGN.RIGHT - ? container.x + - containerDims.width - - boundTextElement.width - - boundTextElementPadding - : container.x + containerDims.width / 2 - boundTextElement.width / 2; - - mutateElement(boundTextElement, { x, y }); + if (boundTextElement.textAlign === TEXT_ALIGN.LEFT) { + x = containerCoords.x + padding; + } else if (boundTextElement.textAlign === TEXT_ALIGN.RIGHT) { + x = + containerCoords.x + + (maxContainerWidth - boundTextElement.width + padding); + } else { + x = + containerCoords.x + + (maxContainerWidth / 2 - boundTextElement.width / 2 + padding); + } + return { x, y }; }; + // https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js export const measureText = ( text: string, @@ -621,6 +635,24 @@ export const getContainerCenter = ( return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] }; }; +export const getContainerCoords = (container: NonDeletedExcalidrawElement) => { + if (container.type === "ellipse") { + // The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6172 + const offsetX = + (container.width / 2) * (1 - Math.sqrt(2) / 2) + BOUND_TEXT_PADDING; + const offsetY = + (container.height / 2) * (1 - Math.sqrt(2) / 2) + BOUND_TEXT_PADDING; + return { + x: container.x + offsetX, + y: container.y + offsetY, + }; + } + return { + x: container.x, + y: container.y, + }; +}; + export const getTextElementAngle = (textElement: ExcalidrawTextElement) => { const container = getContainerElement(textElement); if (!container || isArrowElement(container)) { @@ -633,12 +665,13 @@ export const getBoundTextElementOffset = ( boundTextElement: ExcalidrawTextElement | null, ) => { const container = getContainerElement(boundTextElement); - if (!container) { + if (!container || !boundTextElement) { return 0; } if (isArrowElement(container)) { return BOUND_TEXT_PADDING * 8; } + return BOUND_TEXT_PADDING; }; @@ -723,3 +756,16 @@ export const isValidTextContainer = (element: ExcalidrawElement) => { isArrowElement(element) ); }; + +export const computeContainerHeightForBoundText = ( + container: NonDeletedExcalidrawElement, + boundTextElementHeight: number, +) => { + if (container.type === "ellipse") { + return Math.round((boundTextElementHeight / Math.sqrt(2)) * 2); + } + if (isArrowElement(container)) { + return boundTextElementHeight + BOUND_TEXT_PADDING * 8 * 2; + } + return boundTextElementHeight + BOUND_TEXT_PADDING * 2; +}; diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx index c61d4e68b..87a6e0bf5 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/src/element/textWysiwyg.test.tsx @@ -791,9 +791,7 @@ describe("textWysiwyg", () => { text = h.elements[1] as ExcalidrawTextElementWithContainer; expect(text.text).toBe("Hello \nWorld!"); expect(text.originalText).toBe("Hello World!"); - expect(text.y).toBe( - rectangle.y + rectangle.height / 2 - (APPROX_LINE_HEIGHT * 2) / 2, - ); + expect(text.y).toBe(27.5); expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING); expect(text.height).toBe(APPROX_LINE_HEIGHT * 2); expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2); @@ -827,9 +825,7 @@ describe("textWysiwyg", () => { expect(text.text).toBe("Hello"); expect(text.originalText).toBe("Hello"); - expect(text.y).toBe( - rectangle.y + rectangle.height / 2 - APPROX_LINE_HEIGHT / 2, - ); + expect(text.y).toBe(40); expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING); expect(text.height).toBe(APPROX_LINE_HEIGHT); expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2); @@ -1248,7 +1244,7 @@ describe("textWysiwyg", () => { expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(` Array [ 15, - 20, + 25, ] `); }); @@ -1259,7 +1255,7 @@ describe("textWysiwyg", () => { expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(` Array [ 94.5, - 20, + 25, ] `); }); @@ -1269,22 +1265,22 @@ describe("textWysiwyg", () => { fireEvent.click(screen.getByTitle("Align top")); expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(` - Array [ - 174, - 20, - ] - `); + Array [ + 174, + 25, + ] + `); }); it("when center left", async () => { fireEvent.click(screen.getByTitle("Center vertically")); fireEvent.click(screen.getByTitle("Left")); expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(` - Array [ - 15, - 25, - ] - `); + Array [ + 15, + 20, + ] + `); }); it("when center center", async () => { @@ -1292,11 +1288,11 @@ describe("textWysiwyg", () => { fireEvent.click(screen.getByTitle("Center vertically")); expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(` - Array [ - -25, - 25, - ] - `); + Array [ + -25, + 20, + ] + `); }); it("when center right", async () => { @@ -1304,11 +1300,11 @@ describe("textWysiwyg", () => { fireEvent.click(screen.getByTitle("Center vertically")); expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(` - Array [ - 174, - 25, - ] - `); + Array [ + 174, + 20, + ] + `); }); it("when bottom left", async () => { diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx index 7b0098ce0..5536632b4 100644 --- a/src/element/textWysiwyg.tsx +++ b/src/element/textWysiwyg.tsx @@ -25,6 +25,7 @@ import { getApproxLineHeight, getBoundTextElementId, getBoundTextElementOffset, + getContainerCoords, getContainerDims, getContainerElement, getTextElementAngle, @@ -230,19 +231,22 @@ export const textWysiwyg = ({ // Start pushing text upward until a diff of 30px (padding) // is reached else { + const padding = + container.type === "ellipse" + ? 0 + : getBoundTextElementOffset(updatedTextElement); + const containerCoords = getContainerCoords(container); + // vertically center align the text if (verticalAlign === VERTICAL_ALIGN.MIDDLE) { if (!isArrowElement(container)) { coordY = - container.y + containerDims.height / 2 - textElementHeight / 2; + containerCoords.y + maxHeight / 2 - textElementHeight / 2; } } if (verticalAlign === VERTICAL_ALIGN.BOTTOM) { coordY = - container.y + - containerDims.height - - textElementHeight - - getBoundTextElementOffset(updatedTextElement); + containerCoords.y + (maxHeight - textElementHeight + padding); } } } From 5368ddef74570abd3889e7db3133038b04b99cc7 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Wed, 22 Feb 2023 16:28:12 +0530 Subject: [PATCH 179/276] fix: improve text wrapping inside rhombus and more fixes (#6265) * fix: improve text wrapping inside rhombus * Add comments * specs * fix: shift resize and multiple element regression for ellipse and rhombus * use container width for scaling font size * fix * fix multiple resize * lint * redraw on submit * redraw only newly pasted elements * no padding when center * fix tests * fix * dont add padding in rhombus when aligning * refactor * fix * move getMaxContainerHeight and getMaxContainerWidth to textElement.ts * Add specs --- src/components/App.tsx | 9 +-- src/element/newElement.ts | 46 +----------- src/element/resizeElements.ts | 26 ++++--- src/element/textElement.test.ts | 81 +++++++++++++++++++-- src/element/textElement.ts | 99 ++++++++++++++++++++------ src/element/textWysiwyg.test.tsx | 12 ++-- src/element/textWysiwyg.tsx | 13 ++-- src/tests/linearElementEditor.test.tsx | 7 +- 8 files changed, 192 insertions(+), 101 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index effa604bf..3c0ac9dc4 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1627,6 +1627,7 @@ class App extends React.Component { oldIdToDuplicatedId.set(element.id, newElement.id); return newElement; }); + bindTextToShapeAfterDuplication(newElements, elements, oldIdToDuplicatedId); const nextElements = [ ...this.scene.getElementsIncludingDeleted(), @@ -1640,10 +1641,10 @@ class App extends React.Component { this.scene.replaceAllElements(nextElements); - nextElements.forEach((nextElement) => { - if (isTextElement(nextElement) && isBoundToContainer(nextElement)) { - const container = getContainerElement(nextElement); - redrawTextBoundingBox(nextElement, container); + newElements.forEach((newElement) => { + if (isTextElement(newElement) && isBoundToContainer(newElement)) { + const container = getContainerElement(newElement); + redrawTextBoundingBox(newElement, container); } }); diff --git a/src/element/newElement.ts b/src/element/newElement.ts index c7f33a462..6024765ec 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -22,15 +22,15 @@ import { getElementAbsoluteCoords } from "."; import { adjustXYWithRotation } from "../math"; import { getResizedElementAbsoluteCoords } from "./bounds"; import { - getBoundTextElement, getBoundTextElementOffset, getContainerDims, getContainerElement, measureText, normalizeText, wrapText, + getMaxContainerWidth, } from "./textElement"; -import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants"; +import { VERTICAL_ALIGN } from "../constants"; import { isArrowElement } from "./typeChecks"; type ElementConstructorOpts = MarkOptional< @@ -278,48 +278,6 @@ export const refreshTextDimensions = ( return { text, ...dimensions }; }; -export const getMaxContainerWidth = (container: ExcalidrawElement) => { - const width = getContainerDims(container).width; - if (isArrowElement(container)) { - const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2; - if (containerWidth <= 0) { - const boundText = getBoundTextElement(container); - if (boundText) { - return boundText.width; - } - return BOUND_TEXT_PADDING * 8 * 2; - } - return containerWidth; - } else if (container.type === "ellipse") { - // The width of the largest rectangle inscribed inside an ellipse is - // Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from - // equation of an ellipse -https://github.com/excalidraw/excalidraw/pull/6172 - return Math.round((width / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2; - } - return width - BOUND_TEXT_PADDING * 2; -}; - -export const getMaxContainerHeight = (container: ExcalidrawElement) => { - const height = getContainerDims(container).height; - if (isArrowElement(container)) { - const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2; - if (containerHeight <= 0) { - const boundText = getBoundTextElement(container); - if (boundText) { - return boundText.height; - } - return BOUND_TEXT_PADDING * 8 * 2; - } - return height; - } else if (container.type === "ellipse") { - // The height of the largest rectangle inscribed inside an ellipse is - // Math.round((ellipse.height / 2) * Math.sqrt(2)) which is derived from - // equation of an ellipse - https://github.com/excalidraw/excalidraw/pull/6172 - return Math.round((height / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2; - } - return height - BOUND_TEXT_PADDING * 2; -}; - export const updateTextElement = ( textElement: ExcalidrawTextElement, { diff --git a/src/element/resizeElements.ts b/src/element/resizeElements.ts index 605ab0c2b..fe4f8e484 100644 --- a/src/element/resizeElements.ts +++ b/src/element/resizeElements.ts @@ -43,12 +43,12 @@ import { getApproxMinLineWidth, getBoundTextElement, getBoundTextElementId, - getBoundTextElementOffset, getContainerElement, handleBindTextResize, measureText, + getMaxContainerHeight, + getMaxContainerWidth, } from "./textElement"; -import { getMaxContainerWidth } from "./newElement"; export const normalizeAngle = (angle: number): number => { if (angle >= 2 * Math.PI) { @@ -427,12 +427,16 @@ export const resizeSingleElement = ( }; } if (shouldMaintainAspectRatio) { - const boundTextElementPadding = - getBoundTextElementOffset(boundTextElement); + const updatedElement = { + ...element, + width: eleNewWidth, + height: eleNewHeight, + }; + const nextFont = measureFontSizeFromWH( boundTextElement, - eleNewWidth - boundTextElementPadding * 2, - eleNewHeight - boundTextElementPadding * 2, + getMaxContainerWidth(updatedElement), + getMaxContainerHeight(updatedElement), ); if (nextFont === null) { return; @@ -697,11 +701,15 @@ const resizeMultipleElements = ( const boundTextElement = getBoundTextElement(element.latest); if (boundTextElement || isTextElement(element.orig)) { - const optionalPadding = getBoundTextElementOffset(boundTextElement) * 2; + const updatedElement = { + ...element.latest, + width, + height, + }; const textMeasurements = measureFontSizeFromWH( boundTextElement ?? (element.orig as ExcalidrawTextElement), - width - optionalPadding, - height - optionalPadding, + getMaxContainerWidth(updatedElement), + getMaxContainerHeight(updatedElement), ); if (!textMeasurements) { diff --git a/src/element/textElement.test.ts b/src/element/textElement.test.ts index 91974aca2..22086095f 100644 --- a/src/element/textElement.test.ts +++ b/src/element/textElement.test.ts @@ -3,6 +3,8 @@ import { API } from "../tests/helpers/api"; import { computeContainerHeightForBoundText, getContainerCoords, + getMaxContainerWidth, + getMaxContainerHeight, measureText, wrapText, } from "./textElement"; @@ -202,24 +204,37 @@ describe("Test measureText", () => { describe("Test getContainerCoords", () => { const params = { width: 200, height: 100, x: 10, y: 20 }; + it("should compute coords correctly when ellipse", () => { - const ellipse = API.createElement({ + const element = API.createElement({ type: "ellipse", ...params, }); - expect(getContainerCoords(ellipse)).toEqual({ + expect(getContainerCoords(element)).toEqual({ x: 44.2893218813452455, y: 39.64466094067262, }); }); + it("should compute coords correctly when rectangle", () => { - const rectangle = API.createElement({ + const element = API.createElement({ type: "rectangle", ...params, }); - expect(getContainerCoords(rectangle)).toEqual({ - x: 10, - y: 20, + expect(getContainerCoords(element)).toEqual({ + x: 15, + y: 25, + }); + }); + + it("should compute coords correctly when diamond", () => { + const element = API.createElement({ + type: "diamond", + ...params, + }); + expect(getContainerCoords(element)).toEqual({ + x: 65, + y: 50, }); }); }); @@ -229,6 +244,7 @@ describe("Test measureText", () => { width: 178, height: 194, }; + it("should compute container height correctly for rectangle", () => { const element = API.createElement({ type: "rectangle", @@ -236,6 +252,7 @@ describe("Test measureText", () => { }); expect(computeContainerHeightForBoundText(element, 150)).toEqual(160); }); + it("should compute container height correctly for ellipse", () => { const element = API.createElement({ type: "ellipse", @@ -243,5 +260,57 @@ describe("Test measureText", () => { }); expect(computeContainerHeightForBoundText(element, 150)).toEqual(212); }); + + it("should compute container height correctly for diamond", () => { + const element = API.createElement({ + type: "diamond", + ...params, + }); + expect(computeContainerHeightForBoundText(element, 150)).toEqual(300); + }); + }); + + describe("Test getMaxContainerWidth", () => { + const params = { + width: 178, + height: 194, + }; + + it("should return max width when container is rectangle", () => { + const container = API.createElement({ type: "rectangle", ...params }); + expect(getMaxContainerWidth(container)).toBe(168); + }); + + it("should return max width when container is ellipse", () => { + const container = API.createElement({ type: "ellipse", ...params }); + expect(getMaxContainerWidth(container)).toBe(116); + }); + + it("should return max width when container is diamond", () => { + const container = API.createElement({ type: "diamond", ...params }); + expect(getMaxContainerWidth(container)).toBe(79); + }); + }); + + describe("Test getMaxContainerHeight", () => { + const params = { + width: 178, + height: 194, + }; + + it("should return max height when container is rectangle", () => { + const container = API.createElement({ type: "rectangle", ...params }); + expect(getMaxContainerHeight(container)).toBe(184); + }); + + it("should return max height when container is ellipse", () => { + const container = API.createElement({ type: "ellipse", ...params }); + expect(getMaxContainerHeight(container)).toBe(127); + }); + + it("should return max height when container is diamond", () => { + const container = API.createElement({ type: "diamond", ...params }); + expect(getMaxContainerHeight(container)).toBe(87); + }); }); }); diff --git a/src/element/textElement.ts b/src/element/textElement.ts index ed4d27629..b91e9f2f6 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -12,7 +12,6 @@ import { BOUND_TEXT_PADDING, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants"; import { MaybeTransformHandleType } from "./transformHandles"; import Scene from "../scene/Scene"; import { isTextElement } from "."; -import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement"; import { isBoundToContainer, isImageElement, @@ -244,31 +243,25 @@ const computeBoundTextPosition = ( const containerCoords = getContainerCoords(container); const maxContainerHeight = getMaxContainerHeight(container); const maxContainerWidth = getMaxContainerWidth(container); - const padding = container.type === "ellipse" ? 0 : BOUND_TEXT_PADDING; let x; let y; if (boundTextElement.verticalAlign === VERTICAL_ALIGN.TOP) { - y = containerCoords.y + padding; + y = containerCoords.y; } else if (boundTextElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) { - y = - containerCoords.y + - (maxContainerHeight - boundTextElement.height + padding); + y = containerCoords.y + (maxContainerHeight - boundTextElement.height); } else { y = containerCoords.y + - (maxContainerHeight / 2 - boundTextElement.height / 2 + padding); + (maxContainerHeight / 2 - boundTextElement.height / 2); } if (boundTextElement.textAlign === TEXT_ALIGN.LEFT) { - x = containerCoords.x + padding; + x = containerCoords.x; } else if (boundTextElement.textAlign === TEXT_ALIGN.RIGHT) { - x = - containerCoords.x + - (maxContainerWidth - boundTextElement.width + padding); + x = containerCoords.x + (maxContainerWidth - boundTextElement.width); } else { x = - containerCoords.x + - (maxContainerWidth / 2 - boundTextElement.width / 2 + padding); + containerCoords.x + (maxContainerWidth / 2 - boundTextElement.width / 2); } return { x, y }; }; @@ -636,20 +629,22 @@ export const getContainerCenter = ( }; export const getContainerCoords = (container: NonDeletedExcalidrawElement) => { + let offsetX = BOUND_TEXT_PADDING; + let offsetY = BOUND_TEXT_PADDING; + if (container.type === "ellipse") { // The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6172 - const offsetX = - (container.width / 2) * (1 - Math.sqrt(2) / 2) + BOUND_TEXT_PADDING; - const offsetY = - (container.height / 2) * (1 - Math.sqrt(2) / 2) + BOUND_TEXT_PADDING; - return { - x: container.x + offsetX, - y: container.y + offsetY, - }; + offsetX += (container.width / 2) * (1 - Math.sqrt(2) / 2); + offsetY += (container.height / 2) * (1 - Math.sqrt(2) / 2); + } + // The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6265 + if (container.type === "diamond") { + offsetX += container.width / 4; + offsetY += container.height / 4; } return { - x: container.x, - y: container.y, + x: container.x + offsetX, + y: container.y + offsetY, }; }; @@ -767,5 +762,63 @@ export const computeContainerHeightForBoundText = ( if (isArrowElement(container)) { return boundTextElementHeight + BOUND_TEXT_PADDING * 8 * 2; } + if (container.type === "diamond") { + return 2 * boundTextElementHeight; + } return boundTextElementHeight + BOUND_TEXT_PADDING * 2; }; + +export const getMaxContainerWidth = (container: ExcalidrawElement) => { + const width = getContainerDims(container).width; + if (isArrowElement(container)) { + const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2; + if (containerWidth <= 0) { + const boundText = getBoundTextElement(container); + if (boundText) { + return boundText.width; + } + return BOUND_TEXT_PADDING * 8 * 2; + } + return containerWidth; + } + + if (container.type === "ellipse") { + // The width of the largest rectangle inscribed inside an ellipse is + // Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from + // equation of an ellipse -https://github.com/excalidraw/excalidraw/pull/6172 + return Math.round((width / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2; + } + if (container.type === "diamond") { + // The width of the largest rectangle inscribed inside a rhombus is + // Math.round(width / 2) - https://github.com/excalidraw/excalidraw/pull/6265 + return Math.round(width / 2) - BOUND_TEXT_PADDING * 2; + } + return width - BOUND_TEXT_PADDING * 2; +}; + +export const getMaxContainerHeight = (container: ExcalidrawElement) => { + const height = getContainerDims(container).height; + if (isArrowElement(container)) { + const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2; + if (containerHeight <= 0) { + const boundText = getBoundTextElement(container); + if (boundText) { + return boundText.height; + } + return BOUND_TEXT_PADDING * 8 * 2; + } + return height; + } + if (container.type === "ellipse") { + // The height of the largest rectangle inscribed inside an ellipse is + // Math.round((ellipse.height / 2) * Math.sqrt(2)) which is derived from + // equation of an ellipse - https://github.com/excalidraw/excalidraw/pull/6172 + return Math.round((height / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2; + } + if (container.type === "diamond") { + // The height of the largest rectangle inscribed inside a rhombus is + // Math.round(height / 2) - https://github.com/excalidraw/excalidraw/pull/6265 + return Math.round(height / 2) - BOUND_TEXT_PADDING * 2; + } + return height - BOUND_TEXT_PADDING * 2; +}; diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx index 87a6e0bf5..fb41a3813 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/src/element/textWysiwyg.test.tsx @@ -791,7 +791,7 @@ describe("textWysiwyg", () => { text = h.elements[1] as ExcalidrawTextElementWithContainer; expect(text.text).toBe("Hello \nWorld!"); expect(text.originalText).toBe("Hello World!"); - expect(text.y).toBe(27.5); + expect(text.y).toBe(57.5); expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING); expect(text.height).toBe(APPROX_LINE_HEIGHT * 2); expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2); @@ -825,7 +825,7 @@ describe("textWysiwyg", () => { expect(text.text).toBe("Hello"); expect(text.originalText).toBe("Hello"); - expect(text.y).toBe(40); + expect(text.y).toBe(57.5); expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING); expect(text.height).toBe(APPROX_LINE_HEIGHT); expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2); @@ -930,6 +930,8 @@ describe("textWysiwyg", () => { editor.select(); fireEvent.click(screen.getByTitle("Left")); + await new Promise((r) => setTimeout(r, 0)); + fireEvent.click(screen.getByTitle("Align bottom")); await new Promise((r) => setTimeout(r, 0)); @@ -1278,7 +1280,7 @@ describe("textWysiwyg", () => { expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(` Array [ 15, - 20, + 25, ] `); }); @@ -1290,7 +1292,7 @@ describe("textWysiwyg", () => { expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(` Array [ -25, - 20, + 25, ] `); }); @@ -1302,7 +1304,7 @@ describe("textWysiwyg", () => { expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(` Array [ 174, - 20, + 25, ] `); }); diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx index 5536632b4..d04dfb8df 100644 --- a/src/element/textWysiwyg.tsx +++ b/src/element/textWysiwyg.tsx @@ -24,14 +24,16 @@ import { mutateElement } from "./mutateElement"; import { getApproxLineHeight, getBoundTextElementId, - getBoundTextElementOffset, getContainerCoords, getContainerDims, getContainerElement, getTextElementAngle, getTextWidth, normalizeText, + redrawTextBoundingBox, wrapText, + getMaxContainerHeight, + getMaxContainerWidth, } from "./textElement"; import { actionDecreaseFontSize, @@ -39,7 +41,6 @@ import { } from "../actions/actionProperties"; import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas"; import App from "../components/App"; -import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement"; import { LinearElementEditor } from "./linearElementEditor"; import { parseClipboard } from "../clipboard"; @@ -231,10 +232,6 @@ export const textWysiwyg = ({ // Start pushing text upward until a diff of 30px (padding) // is reached else { - const padding = - container.type === "ellipse" - ? 0 - : getBoundTextElementOffset(updatedTextElement); const containerCoords = getContainerCoords(container); // vertically center align the text @@ -245,8 +242,7 @@ export const textWysiwyg = ({ } } if (verticalAlign === VERTICAL_ALIGN.BOTTOM) { - coordY = - containerCoords.y + (maxHeight - textElementHeight + padding); + coordY = containerCoords.y + (maxHeight - textElementHeight); } } } @@ -616,6 +612,7 @@ export const textWysiwyg = ({ ), }); } + redrawTextBoundingBox(updateElement, container); } onSubmit({ diff --git a/src/tests/linearElementEditor.test.tsx b/src/tests/linearElementEditor.test.tsx index d4b1d9038..3e5ebb8cc 100644 --- a/src/tests/linearElementEditor.test.tsx +++ b/src/tests/linearElementEditor.test.tsx @@ -17,8 +17,11 @@ import { KEYS } from "../keys"; import { LinearElementEditor } from "../element/linearElementEditor"; import { queryByTestId, queryByText } from "@testing-library/react"; import { resize, rotate } from "./utils"; -import { getBoundTextElementPosition, wrapText } from "../element/textElement"; -import { getMaxContainerWidth } from "../element/newElement"; +import { + getBoundTextElementPosition, + wrapText, + getMaxContainerWidth, +} from "../element/textElement"; import * as textElementUtils from "../element/textElement"; import { ROUNDNESS } from "../constants"; From 1e816e87bf6229632d6d90f3468f03e166bb12b2 Mon Sep 17 00:00:00 2001 From: Hikaru Yoshino <57059705+osushicrusher@users.noreply.github.com> Date: Wed, 22 Feb 2023 20:10:29 +0900 Subject: [PATCH 180/276] fix: indenting via `tab` clashing with IME compositor (#6258) --- src/element/textWysiwyg.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx index d04dfb8df..1ce43b4d1 100644 --- a/src/element/textWysiwyg.tsx +++ b/src/element/textWysiwyg.tsx @@ -463,7 +463,9 @@ export const textWysiwyg = ({ event.code === CODES.BRACKET_RIGHT)) ) { event.preventDefault(); - if (event.shiftKey || event.code === CODES.BRACKET_LEFT) { + if (event.isComposing) { + return; + } else if (event.shiftKey || event.code === CODES.BRACKET_LEFT) { outdent(); } else { indent(); From e4506be3e8ef07190c8fd28ebd2750ec211d04de Mon Sep 17 00:00:00 2001 From: Excalidraw Bot <77840495+excalibot@users.noreply.github.com> Date: Wed, 22 Feb 2023 12:23:10 +0100 Subject: [PATCH 181/276] chore: Update translations from Crowdin (#6191) --- src/locales/ar-SA.json | 3 +- src/locales/bg-BG.json | 3 +- src/locales/bn-BD.json | 3 +- src/locales/ca-ES.json | 53 +++++++++++++++--------------- src/locales/cs-CZ.json | 3 +- src/locales/da-DK.json | 3 +- src/locales/de-DE.json | 3 +- src/locales/el-GR.json | 3 +- src/locales/es-ES.json | 17 +++++----- src/locales/eu-ES.json | 9 +++--- src/locales/fa-IR.json | 3 +- src/locales/fi-FI.json | 63 ++++++++++++++++++------------------ src/locales/fr-FR.json | 3 +- src/locales/gl-ES.json | 3 +- src/locales/he-IL.json | 3 +- src/locales/hi-IN.json | 3 +- src/locales/hu-HU.json | 3 +- src/locales/id-ID.json | 3 +- src/locales/it-IT.json | 3 +- src/locales/ja-JP.json | 7 ++-- src/locales/kab-KAB.json | 3 +- src/locales/kk-KZ.json | 3 +- src/locales/ko-KR.json | 3 +- src/locales/ku-TR.json | 3 +- src/locales/lt-LT.json | 3 +- src/locales/lv-LV.json | 3 +- src/locales/mr-IN.json | 3 +- src/locales/my-MM.json | 3 +- src/locales/nb-NO.json | 3 +- src/locales/nl-NL.json | 3 +- src/locales/nn-NO.json | 3 +- src/locales/oc-FR.json | 11 ++++--- src/locales/pa-IN.json | 3 +- src/locales/percentages.json | 34 +++++++++---------- src/locales/pl-PL.json | 3 +- src/locales/pt-BR.json | 23 ++++++------- src/locales/pt-PT.json | 9 +++--- src/locales/ro-RO.json | 9 +++--- src/locales/ru-RU.json | 3 +- src/locales/si-LK.json | 3 +- src/locales/sk-SK.json | 3 +- src/locales/sl-SI.json | 3 +- src/locales/sv-SE.json | 3 +- src/locales/ta-IN.json | 21 ++++++------ src/locales/tr-TR.json | 3 +- src/locales/uk-UA.json | 9 +++--- src/locales/vi-VN.json | 3 +- src/locales/zh-CN.json | 5 +-- src/locales/zh-HK.json | 3 +- src/locales/zh-TW.json | 3 +- 50 files changed, 215 insertions(+), 166 deletions(-) diff --git a/src/locales/ar-SA.json b/src/locales/ar-SA.json index 2a682ad70..6c6c0cf39 100644 --- a/src/locales/ar-SA.json +++ b/src/locales/ar-SA.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.", "resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟", "removeItemsFromsLibrary": "حذف {{count}} عنصر (عناصر) من المكتبة؟", - "invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل." + "invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل.", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "نوع الملف غير مدعوم.", diff --git a/src/locales/bg-BG.json b/src/locales/bg-BG.json index 7f59678c0..ba052783d 100644 --- a/src/locales/bg-BG.json +++ b/src/locales/bg-BG.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "", "resetLibrary": "", "removeItemsFromsLibrary": "", - "invalidEncryptionKey": "" + "invalidEncryptionKey": "", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "Този файлов формат не се поддържа.", diff --git a/src/locales/bn-BD.json b/src/locales/bn-BD.json index 29c35eb63..47c6f02b2 100644 --- a/src/locales/bn-BD.json +++ b/src/locales/bn-BD.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "সরবরাহ করা লিঙ্ক থেকে দৃশ্য লোড করা যায়নি৷ এটি হয় বিকৃত, অথবা বৈধ এক্সক্যালিড্র জেসন তথ্য নেই৷", "resetLibrary": "এটি আপনার সংগ্রহ পরিষ্কার করবে। আপনি কি নিশ্চিত?", "removeItemsFromsLibrary": "সংগ্রহ থেকে {{count}} বস্তু বিয়োগ করা হবে। আপনি কি নিশ্চিত?", - "invalidEncryptionKey": "অবৈধ এনক্রীপশন কী।" + "invalidEncryptionKey": "অবৈধ এনক্রীপশন কী।", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "অসমর্থিত ফাইল।", diff --git a/src/locales/ca-ES.json b/src/locales/ca-ES.json index 82415ff97..e94523b29 100644 --- a/src/locales/ca-ES.json +++ b/src/locales/ca-ES.json @@ -1,7 +1,7 @@ { "labels": { "paste": "Enganxa", - "pasteAsPlaintext": "", + "pasteAsPlaintext": "Enganxar com a text pla", "pasteCharts": "Enganxa els diagrames", "selectAll": "Selecciona-ho tot", "multiSelect": "Afegeix un element a la selecció", @@ -72,7 +72,7 @@ "layers": "Capes", "actions": "Accions", "language": "Llengua", - "liveCollaboration": "", + "liveCollaboration": "Col·laboració en directe...", "duplicateSelection": "Duplica", "untitled": "Sense títol", "name": "Nom", @@ -116,8 +116,8 @@ "label": "Enllaç" }, "lineEditor": { - "edit": "", - "exit": "" + "edit": "Editar línia", + "exit": "Sortir de l'editor de línia" }, "elementLock": { "lock": "Bloca", @@ -136,8 +136,8 @@ "buttons": { "clearReset": "Neteja el llenç", "exportJSON": "Exporta a un fitxer", - "exportImage": "", - "export": "", + "exportImage": "Exporta la imatge...", + "export": "Guardar a...", "exportToPng": "Exporta a PNG", "exportToSvg": "Exporta a SNG", "copyToClipboard": "Copia al porta-retalls", @@ -145,7 +145,7 @@ "scale": "Escala", "save": "Desa al fitxer actual", "saveAs": "Anomena i desa", - "load": "", + "load": "Obrir", "getShareableLink": "Obté l'enllaç per a compartir", "close": "Tanca", "selectLanguage": "Trieu la llengua", @@ -192,7 +192,8 @@ "invalidSceneUrl": "No s'ha pogut importar l'escena des de l'adreça URL proporcionada. Està malformada o no conté dades Excalidraw JSON vàlides.", "resetLibrary": "Això buidarà la biblioteca. N'esteu segur?", "removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la biblioteca?", - "invalidEncryptionKey": "La clau d'encriptació ha de tenir 22 caràcters. La col·laboració en directe està desactivada." + "invalidEncryptionKey": "La clau d'encriptació ha de tenir 22 caràcters. La col·laboració en directe està desactivada.", + "collabOfflineWarning": "Sense connexió a internet disponible.\nEls vostres canvis no seran guardats!" }, "errors": { "unsupportedFileType": "Tipus de fitxer no suportat.", @@ -202,8 +203,8 @@ "invalidSVGString": "SVG no vàlid.", "cannotResolveCollabServer": "No ha estat possible connectar amb el servidor collab. Si us plau recarregueu la pàgina i torneu a provar.", "importLibraryError": "No s'ha pogut carregar la biblioteca", - "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed": "No s'ha pogut desar a la base de dades de fons. Si els problemes persisteixen, hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball.", + "collabSaveFailed_sizeExceeded": "No s'ha pogut desar a la base de dades de fons, sembla que el llenç és massa gran. Hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball." }, "toolBar": { "selection": "Selecció", @@ -217,10 +218,10 @@ "text": "Text", "library": "Biblioteca", "lock": "Mantenir activa l'eina seleccionada desprès de dibuixar", - "penMode": "", + "penMode": "Mode de llapis - evita tocar", "link": "Afegeix / actualitza l'enllaç per a la forma seleccionada", "eraser": "Esborrador", - "hand": "" + "hand": "Mà (eina de desplaçament)" }, "headings": { "canvasActions": "Accions del llenç", @@ -228,7 +229,7 @@ "shapes": "Formes" }, "hints": { - "canvasPanning": "", + "canvasPanning": "Per moure el llenç, manteniu premuda la roda del ratolí o la barra espaiadora mentre arrossegueu o utilitzeu l'eina manual", "linearElement": "Feu clic per a dibuixar múltiples punts; arrossegueu per a una sola línia", "freeDraw": "Feu clic i arrossegueu, deixeu anar per a finalitzar", "text": "Consell: també podeu afegir text fent doble clic en qualsevol lloc amb l'eina de selecció", @@ -239,7 +240,7 @@ "resize": "Per restringir les proporcions mentres es canvia la mida, mantenir premut el majúscul (SHIFT); per canviar la mida des del centre, mantenir premut ALT", "resizeImage": "Podeu redimensionar lliurement prement MAJÚSCULA;\nper a redimensionar des del centre, premeu ALT", "rotate": "Per restringir els angles mentre gira, mantenir premut el majúscul (SHIFT)", - "lineEditor_info": "", + "lineEditor_info": "Mantingueu premut Ctrl o Cmd i feu doble clic o premeu Ctrl o Cmd + Retorn per editar els punts", "lineEditor_pointSelected": "Premeu Suprimir per a eliminar el(s) punt(s), CtrlOrCmd+D per a duplicar-lo, o arrossegueu-lo per a moure'l", "lineEditor_nothingSelected": "Seleccioneu un punt per a editar-lo (premeu SHIFT si voleu\nselecció múltiple), o manteniu Alt i feu clic per a afegir més punts", "placeImage": "Feu clic per a col·locar la imatge o clic i arrossegar per a establir-ne la mida manualment", @@ -247,7 +248,7 @@ "bindTextToElement": "Premeu enter per a afegir-hi text", "deepBoxSelect": "Manteniu CtrlOrCmd per a selecció profunda, i per a evitar l'arrossegament", "eraserRevert": "Mantingueu premuda Alt per a revertir els elements seleccionats per a esborrar", - "firefox_clipboard_write": "" + "firefox_clipboard_write": "És probable que aquesta funció es pugui activar posant la marca \"dom.events.asyncClipboard.clipboardItem\" a \"true\". Per canviar les marques del navegador al Firefox, visiteu la pàgina \"about:config\"." }, "canvasError": { "cannotShowPreview": "No es pot mostrar la previsualització", @@ -295,7 +296,7 @@ "blog": "Llegiu el nostre blog", "click": "clic", "deepSelect": "Selecció profunda", - "deepBoxSelect": "", + "deepBoxSelect": "Seleccioneu profundament dins del quadre i eviteu arrossegar", "curvedArrow": "Fletxa corba", "curvedLine": "Línia corba", "documentation": "Documentació", @@ -316,8 +317,8 @@ "zoomToFit": "Zoom per veure tots els elements", "zoomToSelection": "Zoom per veure la selecció", "toggleElementLock": "Blocar/desblocar la selecció", - "movePageUpDown": "", - "movePageLeftRight": "" + "movePageUpDown": "Mou la pàgina cap amunt/a baix", + "movePageLeftRight": "Mou la pàgina cap a l'esquerra/dreta" }, "clearCanvasDialog": { "title": "Neteja el llenç" @@ -399,7 +400,7 @@ "fileSavedToFilename": "S'ha desat a {filename}", "canvas": "el llenç", "selection": "la selecció", - "pasteAsSingleElement": "" + "pasteAsSingleElement": "Fer servir {{shortcut}} per enganxar com un sol element,\no enganxeu-lo en un editor de text existent" }, "colors": { "ffffff": "Blanc", @@ -450,15 +451,15 @@ }, "welcomeScreen": { "app": { - "center_heading": "", - "center_heading_plus": "", - "menuHint": "" + "center_heading": "Totes les vostres dades es guarden localment al vostre navegador.", + "center_heading_plus": "Vols anar a Excalidraw+ en comptes?", + "menuHint": "Exportar, preferències, llenguatges..." }, "defaults": { - "menuHint": "", - "center_heading": "", - "toolbarHint": "", - "helpHint": "" + "menuHint": "Exportar, preferències i més...", + "center_heading": "Diagrames. Fer. Simple.", + "toolbarHint": "Selecciona una eina i comença a dibuixar!", + "helpHint": "Dreceres i ajuda" } } } diff --git a/src/locales/cs-CZ.json b/src/locales/cs-CZ.json index fa225d2a1..718d974b2 100644 --- a/src/locales/cs-CZ.json +++ b/src/locales/cs-CZ.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "", "resetLibrary": "", "removeItemsFromsLibrary": "", - "invalidEncryptionKey": "" + "invalidEncryptionKey": "", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "", diff --git a/src/locales/da-DK.json b/src/locales/da-DK.json index 5b41ddb1e..8e541f90d 100644 --- a/src/locales/da-DK.json +++ b/src/locales/da-DK.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "", "resetLibrary": "", "removeItemsFromsLibrary": "", - "invalidEncryptionKey": "" + "invalidEncryptionKey": "", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "", diff --git a/src/locales/de-DE.json b/src/locales/de-DE.json index a14f52859..b389fe5a4 100644 --- a/src/locales/de-DE.json +++ b/src/locales/de-DE.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Die Szene konnte nicht von der angegebenen URL importiert werden. Sie ist entweder fehlerhaft oder enthält keine gültigen Excalidraw JSON-Daten.", "resetLibrary": "Dieses löscht deine Bibliothek. Bist du sicher?", "removeItemsFromsLibrary": "{{count}} Element(e) aus der Bibliothek löschen?", - "invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert." + "invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert.", + "collabOfflineWarning": "Keine Internetverbindung verfügbar.\nDeine Änderungen werden nicht gespeichert!" }, "errors": { "unsupportedFileType": "Nicht unterstützter Dateityp.", diff --git a/src/locales/el-GR.json b/src/locales/el-GR.json index 02534c326..d478be3e7 100644 --- a/src/locales/el-GR.json +++ b/src/locales/el-GR.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Δεν ήταν δυνατή η εισαγωγή σκηνής από το URL που δώσατε. Είτε έχει λάθος μορφή, είτε δεν περιέχει έγκυρα δεδομένα JSON Excalidraw.", "resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;", "removeItemsFromsLibrary": "Διαγραφή {{count}} αντικειμένου(ων) από τη βιβλιοθήκη;", - "invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη." + "invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη.", + "collabOfflineWarning": "Δεν υπάρχει διαθέσιμη σύνδεση στο internet.\nΟι αλλαγές σας δεν θα αποθηκευτούν!" }, "errors": { "unsupportedFileType": "Μη υποστηριζόμενος τύπος αρχείου.", diff --git a/src/locales/es-ES.json b/src/locales/es-ES.json index e70ad64e5..eceb38ae3 100644 --- a/src/locales/es-ES.json +++ b/src/locales/es-ES.json @@ -103,7 +103,7 @@ "share": "Compartir", "showStroke": "Mostrar selector de color de trazo", "showBackground": "Mostrar el selector de color de fondo", - "toggleTheme": "Alternar tema", + "toggleTheme": "Cambiar tema", "personalLib": "Biblioteca personal", "excalidrawLib": "Biblioteca Excalidraw", "decreaseFontSize": "Disminuir tamaño de letra", @@ -192,7 +192,8 @@ "invalidSceneUrl": "No se ha podido importar la escena desde la URL proporcionada. Está mal formada, o no contiene datos de Excalidraw JSON válidos.", "resetLibrary": "Esto borrará tu biblioteca. ¿Estás seguro?", "removeItemsFromsLibrary": "¿Eliminar {{count}} elemento(s) de la biblioteca?", - "invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada." + "invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada.", + "collabOfflineWarning": "No hay conexión a internet disponible.\n¡No se guardarán los cambios!" }, "errors": { "unsupportedFileType": "Tipo de archivo no admitido.", @@ -233,7 +234,7 @@ "freeDraw": "Haz clic y arrastra, suelta al terminar", "text": "Consejo: también puedes añadir texto haciendo doble clic en cualquier lugar con la herramienta de selección", "text_selected": "Doble clic o pulse ENTER para editar el texto", - "text_editing": "Pulse Escape o CtrlOrCmd+ENTER para terminar de editar", + "text_editing": "Pulse Escape o Ctrl/Cmd + ENTER para terminar de editar", "linearElementMulti": "Haz clic en el último punto o presiona Escape o Enter para finalizar", "lockAngle": "Puedes restringir el ángulo manteniendo presionado el botón SHIFT", "resize": "Para mantener las proporciones mantén SHIFT presionado mientras modificas el tamaño, \nmantén presionado ALT para modificar el tamaño desde el centro", @@ -314,7 +315,7 @@ "title": "Ayuda", "view": "Vista", "zoomToFit": "Ajustar la vista para mostrar todos los elementos", - "zoomToSelection": "Zoom a la selección", + "zoomToSelection": "Ampliar selección", "toggleElementLock": "Bloquear/desbloquear selección", "movePageUpDown": "Mover página hacia arriba/abajo", "movePageLeftRight": "Mover página hacia la izquierda/derecha" @@ -326,9 +327,9 @@ "title": "Publicar biblioteca", "itemName": "Nombre del artículo", "authorName": "Nombre del autor", - "githubUsername": "Nombre de usuario de Github", + "githubUsername": "Nombre de usuario de GitHub", "twitterUsername": "Nombre de usuario de Twitter", - "libraryName": "Nombre de la librería", + "libraryName": "Nombre de la biblioteca", "libraryDesc": "Descripción de la biblioteca", "website": "Sitio Web", "placeholder": { @@ -336,7 +337,7 @@ "libraryName": "Nombre de tu biblioteca", "libraryDesc": "Descripción de su biblioteca para ayudar a la gente a entender su uso", "githubHandle": "Nombre de usuario de GitHub (opcional), así podrá editar la biblioteca una vez enviada para su revisión", - "twitterHandle": "Nombre de usuario de Twitter (opcional), así que sabemos a quién acreditar cuando se promociona en Twitter", + "twitterHandle": "Nombre de usuario de Twitter (opcional), así sabemos a quién acreditar cuando se promociona en Twitter", "website": "Enlace a su sitio web personal o en cualquier otro lugar (opcional)" }, "errors": { @@ -458,7 +459,7 @@ "menuHint": "Exportar, preferencias y más...", "center_heading": "Diagramas. Hecho. Simplemente.", "toolbarHint": "¡Elige una herramienta y empieza a dibujar!", - "helpHint": "Atajos & ayuda" + "helpHint": "Atajos y ayuda" } } } diff --git a/src/locales/eu-ES.json b/src/locales/eu-ES.json index e35e8c40c..d35baf4e4 100644 --- a/src/locales/eu-ES.json +++ b/src/locales/eu-ES.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Ezin izan da eszena inportatu emandako URLtik. Gaizki eratuta dago edo ez du baliozko Excalidraw JSON daturik.", "resetLibrary": "Honek zure liburutegia garbituko du. Ziur zaude?", "removeItemsFromsLibrary": "Liburutegitik {{count}} elementu ezabatu?", - "invalidEncryptionKey": "Enkriptazio-gakoak 22 karaktere izan behar ditu. Zuzeneko lankidetza desgaituta dago." + "invalidEncryptionKey": "Enkriptazio-gakoak 22 karaktere izan behar ditu. Zuzeneko lankidetza desgaituta dago.", + "collabOfflineWarning": "Ez dago Interneteko konexiorik.\nZure aldaketak ez dira gordeko!" }, "errors": { "unsupportedFileType": "Onartu gabeko fitxategi mota.", @@ -220,7 +221,7 @@ "penMode": "Luma modua - ukipena saihestu", "link": "Gehitu / Eguneratu esteka hautatutako forma baterako", "eraser": "Borragoma", - "hand": "" + "hand": "Eskua (panoratze tresna)" }, "headings": { "canvasActions": "Canvas ekintzak", @@ -228,7 +229,7 @@ "shapes": "Formak" }, "hints": { - "canvasPanning": "", + "canvasPanning": "Oihala mugitzeko, eutsi saguaren gurpila edo zuriune-barra arrastatzean, edo erabili esku tresna", "linearElement": "Egin klik hainbat puntu hasteko, arrastatu lerro bakarrerako", "freeDraw": "Egin klik eta arrastatu, askatu amaitutakoan", "text": "Aholkua: testua gehitu dezakezu edozein lekutan klik bikoitza eginez hautapen tresnarekin", @@ -247,7 +248,7 @@ "bindTextToElement": "Sakatu Sartu testua gehitzeko", "deepBoxSelect": "Eutsi Ctrl edo Cmd sakatuta aukeraketa sakona egiteko eta arrastatzea saihesteko", "eraserRevert": "Eduki Alt sakatuta ezabatzeko markatutako elementuak leheneratzeko", - "firefox_clipboard_write": "" + "firefox_clipboard_write": "Ezaugarri hau \"dom.events.asyncClipboard.clipboardItem\" marka \"true\" gisa ezarrita gaitu daiteke. Firefox-en arakatzailearen banderak aldatzeko, bisitatu \"about:config\" orrialdera." }, "canvasError": { "cannotShowPreview": "Ezin da oihala aurreikusi", diff --git a/src/locales/fa-IR.json b/src/locales/fa-IR.json index 817ed51dd..cc7de0dee 100644 --- a/src/locales/fa-IR.json +++ b/src/locales/fa-IR.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "بوم نقاشی از آدرس ارائه شده وارد نشد. این یا نادرست است، یا حاوی داده Excalidraw JSON معتبر نیست.", "resetLibrary": "ین کار کل صفحه را پاک میکند. آیا مطمئنید?", "removeItemsFromsLibrary": "حذف {{count}} آیتم(ها) از کتابخانه?", - "invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است." + "invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است.", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "نوع فایل پشتیبانی نشده.", diff --git a/src/locales/fi-FI.json b/src/locales/fi-FI.json index 2346a018c..6c357b167 100644 --- a/src/locales/fi-FI.json +++ b/src/locales/fi-FI.json @@ -1,7 +1,7 @@ { "labels": { "paste": "Liitä", - "pasteAsPlaintext": "", + "pasteAsPlaintext": "Liitä pelkkänä tekstinä", "pasteCharts": "Liitä kaaviot", "selectAll": "Valitse kaikki", "multiSelect": "Lisää kohde valintaan", @@ -72,7 +72,7 @@ "layers": "Tasot", "actions": "Toiminnot", "language": "Kieli", - "liveCollaboration": "", + "liveCollaboration": "Live Yhteistyö...", "duplicateSelection": "Monista", "untitled": "Nimetön", "name": "Nimi", @@ -116,14 +116,14 @@ "label": "Linkki" }, "lineEditor": { - "edit": "", - "exit": "" + "edit": "Muokkaa riviä", + "exit": "Poistu rivieditorista" }, "elementLock": { - "lock": "", - "unlock": "", - "lockAll": "", - "unlockAll": "" + "lock": "Lukitse", + "unlock": "Poista lukitus", + "lockAll": "Lukitse kaikki", + "unlockAll": "Poista lukitus kaikista" }, "statusPublished": "Julkaistu", "sidebarLock": "Pidä sivupalkki avoinna" @@ -136,8 +136,8 @@ "buttons": { "clearReset": "Tyhjennä piirtoalue", "exportJSON": "Vie tiedostoon", - "exportImage": "", - "export": "", + "exportImage": "Vie kuva...", + "export": "Tallenna nimellä...", "exportToPng": "Vie PNG-tiedostona", "exportToSvg": "Vie SVG-tiedostona", "copyToClipboard": "Kopioi leikepöydälle", @@ -145,7 +145,7 @@ "scale": "Koko", "save": "Tallenna nykyiseen tiedostoon", "saveAs": "Tallenna nimellä", - "load": "", + "load": "Avaa", "getShareableLink": "Hae jaettava linkki", "close": "Sulje", "selectLanguage": "Valitse kieli", @@ -192,7 +192,8 @@ "invalidSceneUrl": "Teosta ei voitu tuoda annetusta URL-osoitteesta. Tallenne on vioittunut, tai osoitteessa ei ole Excalidraw JSON-dataa.", "resetLibrary": "Tämä tyhjentää kirjastosi. Jatketaanko?", "removeItemsFromsLibrary": "Poista {{count}} kohdetta kirjastosta?", - "invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä." + "invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä.", + "collabOfflineWarning": "Internet-yhteyttä ei ole saatavilla.\nMuutoksiasi ei tallenneta!" }, "errors": { "unsupportedFileType": "Tiedostotyyppiä ei tueta.", @@ -201,9 +202,9 @@ "svgImageInsertError": "SVG- kuvaa ei voitu lisätä. Tiedoston SVG-sisältö näyttää virheelliseltä.", "invalidSVGString": "Virheellinen SVG.", "cannotResolveCollabServer": "Yhteyden muodostaminen collab-palvelimeen epäonnistui. Virkistä sivu ja yritä uudelleen.", - "importLibraryError": "", - "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "importLibraryError": "Kokoelman lataaminen epäonnistui", + "collabSaveFailed": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi.", + "collabSaveFailed_sizeExceeded": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi." }, "toolBar": { "selection": "Valinta", @@ -217,10 +218,10 @@ "text": "Teksti", "library": "Kirjasto", "lock": "Pidä valittu työkalu aktiivisena piirron jälkeen", - "penMode": "", + "penMode": "Kynätila - estä kosketus", "link": "Lisää/päivitä linkki valitulle muodolle", "eraser": "Poistotyökalu", - "hand": "" + "hand": "Käsi (panning-työkalu)" }, "headings": { "canvasActions": "Piirtoalueen toiminnot", @@ -228,7 +229,7 @@ "shapes": "Muodot" }, "hints": { - "canvasPanning": "", + "canvasPanning": "Piirtoalueen liikuttamiseksi pidä hiiren pyörää tai välilyöntiä pohjassa tai käytä käsityökalua", "linearElement": "Klikkaa piirtääksesi useampi piste, raahaa piirtääksesi yksittäinen viiva", "freeDraw": "Paina ja raahaa, päästä irti kun olet valmis", "text": "Vinkki: voit myös lisätä tekstiä kaksoisnapsauttamalla mihin tahansa valintatyökalulla", @@ -239,7 +240,7 @@ "resize": "Voit rajoittaa mittasuhteet pitämällä SHIFT-näppäintä alaspainettuna kun muutat kokoa, pidä ALT-näppäintä alaspainettuna muuttaaksesi kokoa keskipisteen suhteen", "resizeImage": "Voit muuttaa kokoa vapaasti pitämällä SHIFTiä pohjassa, pidä ALT pohjassa muuttaaksesi kokoa keskipisteen ympäri", "rotate": "Voit rajoittaa kulman pitämällä SHIFT pohjassa pyörittäessäsi", - "lineEditor_info": "", + "lineEditor_info": "Pidä CtrlOrCmd pohjassa ja kaksoisnapsauta tai paina CtrlOrCmd + Enter muokataksesi pisteitä", "lineEditor_pointSelected": "Poista piste(et) painamalla delete, monista painamalla CtrlOrCmd+D, tai liikuta raahaamalla", "lineEditor_nothingSelected": "Valitse muokattava piste (monivalinta pitämällä SHIFT pohjassa), tai paina Alt ja klikkaa lisätäksesi uusia pisteitä", "placeImage": "Klikkaa asettaaksesi kuvan, tai klikkaa ja raahaa asettaaksesi sen koon manuaalisesti", @@ -247,7 +248,7 @@ "bindTextToElement": "Lisää tekstiä painamalla enter", "deepBoxSelect": "Käytä syvävalintaa ja estä raahaus painamalla CtrlOrCmd", "eraserRevert": "Pidä Alt alaspainettuna, kumotaksesi merkittyjen elementtien poistamisen", - "firefox_clipboard_write": "" + "firefox_clipboard_write": "Tämä ominaisuus voidaan todennäköisesti ottaa käyttöön asettamalla \"dom.events.asyncClipboard.clipboardItem\" kohta \"true\":ksi. Vaihtaaksesi selaimen kohdan Firefoxissa, käy \"about:config\" sivulla." }, "canvasError": { "cannotShowPreview": "Esikatselua ei voitu näyttää", @@ -315,9 +316,9 @@ "view": "Näkymä", "zoomToFit": "Näytä kaikki elementit", "zoomToSelection": "Näytä valinta", - "toggleElementLock": "", - "movePageUpDown": "", - "movePageLeftRight": "" + "toggleElementLock": "Lukitse / poista lukitus valinta", + "movePageUpDown": "Siirrä sivua ylös/alas", + "movePageLeftRight": "Siirrä sivua vasemmalle/oikealle" }, "clearCanvasDialog": { "title": "Pyyhi piirtoalue" @@ -399,7 +400,7 @@ "fileSavedToFilename": "Tallennettiin kohteeseen {filename}", "canvas": "piirtoalue", "selection": "valinta", - "pasteAsSingleElement": "" + "pasteAsSingleElement": "Käytä {{shortcut}} liittääksesi yhtenä elementtinä,\ntai liittääksesi olemassa olevaan tekstieditoriin" }, "colors": { "ffffff": "Valkoinen", @@ -450,15 +451,15 @@ }, "welcomeScreen": { "app": { - "center_heading": "", - "center_heading_plus": "", - "menuHint": "" + "center_heading": "Kaikki tietosi on tallennettu paikallisesti selaimellesi.", + "center_heading_plus": "Haluatko sen sijaan mennä Excalidraw+:aan?", + "menuHint": "Vie, asetukset, kielet, ..." }, "defaults": { - "menuHint": "", - "center_heading": "", - "toolbarHint": "", - "helpHint": "" + "menuHint": "Vie, asetukset ja lisää...", + "center_heading": "Kaaviot. Tehty. Yksinkertaiseksi.", + "toolbarHint": "Valitse työkalu ja aloita piirtäminen!", + "helpHint": "Pikanäppäimet & ohje" } } } diff --git a/src/locales/fr-FR.json b/src/locales/fr-FR.json index 57afcf9b3..573e3fd1a 100644 --- a/src/locales/fr-FR.json +++ b/src/locales/fr-FR.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Impossible d'importer la scène depuis l'URL fournie. Elle est soit incorrecte, soit ne contient pas de données JSON Excalidraw valides.", "resetLibrary": "Cela va effacer votre bibliothèque. Êtes-vous sûr·e ?", "removeItemsFromsLibrary": "Supprimer {{count}} élément(s) de la bibliothèque ?", - "invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée." + "invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée.", + "collabOfflineWarning": "Aucune connexion internet disponible.\nVos modifications ne seront pas enregistrées !" }, "errors": { "unsupportedFileType": "Type de fichier non supporté.", diff --git a/src/locales/gl-ES.json b/src/locales/gl-ES.json index d7053c961..373fe4032 100644 --- a/src/locales/gl-ES.json +++ b/src/locales/gl-ES.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Non se puido importar a escena dende a URL proporcionada. Ou ben está malformada ou non contén un JSON con información válida para Excalidraw.", "resetLibrary": "Isto limpará a súa biblioteca. Está seguro?", "removeItemsFromsLibrary": "Eliminar {{count}} elemento(s) da biblioteca?", - "invalidEncryptionKey": "A clave de cifrado debe ter 22 caracteres. A colaboración en directo está desactivada." + "invalidEncryptionKey": "A clave de cifrado debe ter 22 caracteres. A colaboración en directo está desactivada.", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "Tipo de ficheiro non soportado.", diff --git a/src/locales/he-IL.json b/src/locales/he-IL.json index faa0013b6..9414da375 100644 --- a/src/locales/he-IL.json +++ b/src/locales/he-IL.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "ייבוא המידע מן סצינה מכתובת האינטרנט נכשלה. המידע בנוי באופן משובש או שהוא אינו קובץ JSON תקין של Excalidraw.", "resetLibrary": "פעולה זו תנקה את כל הלוח. אתה בטוח?", "removeItemsFromsLibrary": "מחיקת {{count}} פריטים(ים) מתוך הספריה?", - "invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל." + "invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל.", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "סוג הקובץ אינו נתמך.", diff --git a/src/locales/hi-IN.json b/src/locales/hi-IN.json index c0aca8982..9e3a14d64 100644 --- a/src/locales/hi-IN.json +++ b/src/locales/hi-IN.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "दिये गये युआरेल से दृश्य आयात नहीं किया जा सका. यह या तो अनुचित है, या इसमें उचित Excalidraw JSON डेटा नहीं है।", "resetLibrary": "यह पूरा संग्रह रिक्त करेगा. क्या आपको यक़ीन हैं?", "removeItemsFromsLibrary": "{{count}} वस्तु(यें) संग्रह से हटायें?", - "invalidEncryptionKey": "कूटलेखन कुंजी 22 अक्षरों की होनी चाहिये, इसलिये जीवंत सहयोग अक्षम हैं" + "invalidEncryptionKey": "कूटलेखन कुंजी 22 अक्षरों की होनी चाहिये, इसलिये जीवंत सहयोग अक्षम हैं", + "collabOfflineWarning": "कोई इंटरनेट कनेक्शन उपलब्ध नहीं है।\nआपके बदलाव सहेजे नहीं जाएंगे!" }, "errors": { "unsupportedFileType": "असमर्थित फाइल प्रकार", diff --git a/src/locales/hu-HU.json b/src/locales/hu-HU.json index faaafed90..a3cbd852c 100644 --- a/src/locales/hu-HU.json +++ b/src/locales/hu-HU.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Nem sikerült importálni a jelenetet a megadott URL-ről. Rossz formátumú, vagy nem tartalmaz érvényes Excalidraw JSON-adatokat.", "resetLibrary": "Ezzel törlöd a könyvtárát. biztos vagy ebben?", "removeItemsFromsLibrary": "{{count}} elemet törölsz a könyvtárból?", - "invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva." + "invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva.", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "Nem támogatott fájltípus.", diff --git a/src/locales/id-ID.json b/src/locales/id-ID.json index 0632a87eb..cd722c6a3 100644 --- a/src/locales/id-ID.json +++ b/src/locales/id-ID.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Tidak dapat impor pemandangan dari URL. Kemungkinan URL itu rusak atau tidak berisi data JSON Excalidraw yang valid.", "resetLibrary": "Ini akan menghapus pustaka Anda. Anda yakin?", "removeItemsFromsLibrary": "Hapus {{count}} item dari pustaka?", - "invalidEncryptionKey": "Sandi enkripsi harus 22 karakter. Kolaborasi langsung dinonaktifkan." + "invalidEncryptionKey": "Sandi enkripsi harus 22 karakter. Kolaborasi langsung dinonaktifkan.", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "Tipe file tidak didukung.", diff --git a/src/locales/it-IT.json b/src/locales/it-IT.json index 790c63fe6..3c6cf93a8 100644 --- a/src/locales/it-IT.json +++ b/src/locales/it-IT.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Impossibile importare la scena dall'URL fornito. Potrebbe essere malformato o non contenere dati JSON Excalidraw validi.", "resetLibrary": "Questa azione cancellerà l'intera libreria. Sei sicuro?", "removeItemsFromsLibrary": "Eliminare {{count}} elementi dalla libreria?", - "invalidEncryptionKey": "La chiave di cifratura deve essere composta da 22 caratteri. La collaborazione live è disabilitata." + "invalidEncryptionKey": "La chiave di cifratura deve essere composta da 22 caratteri. La collaborazione live è disabilitata.", + "collabOfflineWarning": "Nessuna connessione internet disponibile.\nLe tue modifiche non verranno salvate!" }, "errors": { "unsupportedFileType": "Tipo di file non supportato.", diff --git a/src/locales/ja-JP.json b/src/locales/ja-JP.json index fad9b6f10..4f7a76afe 100644 --- a/src/locales/ja-JP.json +++ b/src/locales/ja-JP.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "指定された URL からシーンをインポートできませんでした。不正な形式であるか、有効な Excalidraw JSON データが含まれていません。", "resetLibrary": "ライブラリを消去します。本当によろしいですか?", "removeItemsFromsLibrary": "{{count}} 個のアイテムをライブラリから削除しますか?", - "invalidEncryptionKey": "暗号化キーは22文字でなければなりません。ライブコラボレーションは無効化されています。" + "invalidEncryptionKey": "暗号化キーは22文字でなければなりません。ライブコラボレーションは無効化されています。", + "collabOfflineWarning": "インターネットに接続されていません。\n変更は保存されません!" }, "errors": { "unsupportedFileType": "サポートされていないファイル形式です。", @@ -220,7 +221,7 @@ "penMode": "ペンモード - タッチ防止", "link": "選択した図形のリンクを追加/更新", "eraser": "消しゴム", - "hand": "" + "hand": "手 (パンニングツール)" }, "headings": { "canvasActions": "キャンバス操作", @@ -228,7 +229,7 @@ "shapes": "図形" }, "hints": { - "canvasPanning": "", + "canvasPanning": "キャンバスを移動するには、マウスホイールまたはスペースバーを押しながらドラッグするか、手ツールを使用します", "linearElement": "クリックすると複数の頂点からなる曲線を開始、ドラッグすると直線", "freeDraw": "クリックしてドラッグします。離すと終了します", "text": "ヒント: 選択ツールを使用して任意の場所をダブルクリックしてテキストを追加することもできます", diff --git a/src/locales/kab-KAB.json b/src/locales/kab-KAB.json index fdf94fa94..3603e88c7 100644 --- a/src/locales/kab-KAB.json +++ b/src/locales/kab-KAB.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Ulamek taktert n usayes seg URL i d-ittunefken. Ahat mačči d tameɣtut neɣ ur tegbir ara isefka JSON n Excalidraw.", "resetLibrary": "Ayagi ad isfeḍ tamkarḍit-inek•m. Tetḥeqqeḍ?", "removeItemsFromsLibrary": "Ad tekkseḍ {{count}} n uferdis (en) si temkarḍit?", - "invalidEncryptionKey": "Tasarut n uwgelhen isefk ad tesɛu 22 n yiekkilen. Amɛiwen srid yensa." + "invalidEncryptionKey": "Tasarut n uwgelhen isefk ad tesɛu 22 n yiekkilen. Amɛiwen srid yensa.", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "Anaw n ufaylu ur yettwasefrak ara.", diff --git a/src/locales/kk-KZ.json b/src/locales/kk-KZ.json index 25071aa5e..ee1e44083 100644 --- a/src/locales/kk-KZ.json +++ b/src/locales/kk-KZ.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "", "resetLibrary": "", "removeItemsFromsLibrary": "", - "invalidEncryptionKey": "" + "invalidEncryptionKey": "", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "", diff --git a/src/locales/ko-KR.json b/src/locales/ko-KR.json index a0368663c..9c02b3ea1 100644 --- a/src/locales/ko-KR.json +++ b/src/locales/ko-KR.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "제공된 URL에서 화면을 가져오는데 실패했습니다. 주소가 잘못되거나, 유효한 Excalidraw JSON 데이터를 포함하고 있지 않은 것일 수 있습니다.", "resetLibrary": "당신의 라이브러리를 초기화 합니다. 계속하시겠습니까?", "removeItemsFromsLibrary": "{{count}}개의 아이템을 라이브러리에서 삭제하시겠습니까?", - "invalidEncryptionKey": "암호화 키는 반드시 22글자여야 합니다. 실시간 협업이 비활성화됩니다." + "invalidEncryptionKey": "암호화 키는 반드시 22글자여야 합니다. 실시간 협업이 비활성화됩니다.", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "지원하지 않는 파일 형식 입니다.", diff --git a/src/locales/ku-TR.json b/src/locales/ku-TR.json index 636a832af..4a8ebc95c 100644 --- a/src/locales/ku-TR.json +++ b/src/locales/ku-TR.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "ناتوانێت دیمەنەکە هاوردە بکات لە URL ی دابینکراو. یان نادروستە، یان داتای \"ئێکسکالیدراو\" JSON ی دروستی تێدا نییە.", "resetLibrary": "ئەمە کتێبخانەکەت خاوێن دەکاتەوە. ئایا دڵنیایت?", "removeItemsFromsLibrary": "سڕینەوەی {{count}} ئایتم(ەکان) لە کتێبخانە؟", - "invalidEncryptionKey": "کلیلی رەمزاندن دەبێت لە 22 پیت بێت. هاوکاری ڕاستەوخۆ لە کارخراوە." + "invalidEncryptionKey": "کلیلی رەمزاندن دەبێت لە 22 پیت بێت. هاوکاری ڕاستەوخۆ لە کارخراوە.", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "جۆری فایلی پشتگیری نەکراو.", diff --git a/src/locales/lt-LT.json b/src/locales/lt-LT.json index 6eb83b7ac..a8a8d6639 100644 --- a/src/locales/lt-LT.json +++ b/src/locales/lt-LT.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Nepavyko suimportuoti scenos iš pateiktos nuorodos (URL). Ji arba blogai suformatuota, arba savyje neturi teisingų Excalidraw JSON duomenų.", "resetLibrary": "Tai išvalys tavo biblioteką. Ar tikrai to nori?", "removeItemsFromsLibrary": "Ištrinti {{count}} elementą/-us iš bibliotekos?", - "invalidEncryptionKey": "Šifravimo raktas turi būti iš 22 simbolių. Redagavimas gyvai yra išjungtas." + "invalidEncryptionKey": "Šifravimo raktas turi būti iš 22 simbolių. Redagavimas gyvai yra išjungtas.", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "Nepalaikomas failo tipas.", diff --git a/src/locales/lv-LV.json b/src/locales/lv-LV.json index 7341fd3fd..ea3180149 100644 --- a/src/locales/lv-LV.json +++ b/src/locales/lv-LV.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Nevarēja importēt ainu no norādītā URL. Vai nu tas ir nederīgs, vai nesatur derīgus Excalidraw JSON datus.", "resetLibrary": "Šī funkcija iztukšos bibliotēku. Vai turpināt?", "removeItemsFromsLibrary": "Vai izņemt {{count}} vienumu(s) no bibliotēkas?", - "invalidEncryptionKey": "Šifrēšanas atslēgai jābūt 22 simbolus garai. Tiešsaistes sadarbība ir izslēgta." + "invalidEncryptionKey": "Šifrēšanas atslēgai jābūt 22 simbolus garai. Tiešsaistes sadarbība ir izslēgta.", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "Neatbalstīts datnes veids.", diff --git a/src/locales/mr-IN.json b/src/locales/mr-IN.json index 6e198493d..e67fc3159 100644 --- a/src/locales/mr-IN.json +++ b/src/locales/mr-IN.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "दिलेल्या यू-आर-एल पासून दृश्य आणू शकलो नाही. तो एकतर बरोबार नाही आहे किंवा त्यात वैध एक्सकेलीड्रॉ जेसन डेटा नाही.", "resetLibrary": "पटल स्वच्छ होणार, तुम्हाला खात्री आहे का?", "removeItemsFromsLibrary": "संग्रहातून {{count}} तत्व (एक किव्हा अनेक) काढू?", - "invalidEncryptionKey": "कूटबद्धन कुंजी 22 अक्षरांची असणे आवश्यक आहे. थेट सहयोग अक्षम केले आहे." + "invalidEncryptionKey": "कूटबद्धन कुंजी 22 अक्षरांची असणे आवश्यक आहे. थेट सहयोग अक्षम केले आहे.", + "collabOfflineWarning": "इंटरनेट कनेक्शन उपलब्ध नाही.\nतुमचे बदल जतन केले जाणार नाहीत!" }, "errors": { "unsupportedFileType": "असमर्थित फाइल प्रकार.", diff --git a/src/locales/my-MM.json b/src/locales/my-MM.json index 8b5e6859f..e479d3702 100644 --- a/src/locales/my-MM.json +++ b/src/locales/my-MM.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "", "resetLibrary": "", "removeItemsFromsLibrary": "", - "invalidEncryptionKey": "" + "invalidEncryptionKey": "", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "", diff --git a/src/locales/nb-NO.json b/src/locales/nb-NO.json index f8ac1e307..653779a56 100644 --- a/src/locales/nb-NO.json +++ b/src/locales/nb-NO.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Kunne ikke importere scene fra den oppgitte URL-en. Den er enten ødelagt, eller inneholder ikke gyldig Excalidraw JSON-data.", "resetLibrary": "Dette vil tømme biblioteket ditt. Er du sikker?", "removeItemsFromsLibrary": "Slett {{count}} element(er) fra biblioteket?", - "invalidEncryptionKey": "Krypteringsnøkkel må ha 22 tegn. Live-samarbeid er deaktivert." + "invalidEncryptionKey": "Krypteringsnøkkel må ha 22 tegn. Live-samarbeid er deaktivert.", + "collabOfflineWarning": "Ingen Internett-tilkobling tilgjengelig.\nEndringer dine vil ikke bli lagret!" }, "errors": { "unsupportedFileType": "Filtypen støttes ikke.", diff --git a/src/locales/nl-NL.json b/src/locales/nl-NL.json index 9152d3bbb..313c8dc47 100644 --- a/src/locales/nl-NL.json +++ b/src/locales/nl-NL.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Kan scène niet importeren vanuit de opgegeven URL. Het is onjuist of bevat geen geldige Excalidraw JSON-gegevens.", "resetLibrary": "Dit zal je bibliotheek wissen. Weet je het zeker?", "removeItemsFromsLibrary": "Verwijder {{count}} item(s) uit bibliotheek?", - "invalidEncryptionKey": "Encryptiesleutel moet 22 tekens zijn. Live samenwerking is uitgeschakeld." + "invalidEncryptionKey": "Encryptiesleutel moet 22 tekens zijn. Live samenwerking is uitgeschakeld.", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "Niet-ondersteund bestandstype.", diff --git a/src/locales/nn-NO.json b/src/locales/nn-NO.json index fa740d258..0311cc2fe 100644 --- a/src/locales/nn-NO.json +++ b/src/locales/nn-NO.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Kunne ikkje hente noko scene frå den URL-en. Ho er anten øydelagd eller inneheld ikkje gyldig Excalidraw JSON-data.", "resetLibrary": "Dette vil fjerne alt innhald frå biblioteket. Er du sikker?", "removeItemsFromsLibrary": "Slette {{count}} element frå biblioteket?", - "invalidEncryptionKey": "Krypteringsnøkkelen må ha 22 teikn. Sanntidssamarbeid er deaktivert." + "invalidEncryptionKey": "Krypteringsnøkkelen må ha 22 teikn. Sanntidssamarbeid er deaktivert.", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "Filtypen er ikkje støtta.", diff --git a/src/locales/oc-FR.json b/src/locales/oc-FR.json index c1d3ab341..51a5b9018 100644 --- a/src/locales/oc-FR.json +++ b/src/locales/oc-FR.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Importacion impossibla de la scèna a partir de l’URL provesida. Es siá mal formatada o siá conten pas cap de donada JSON Excalidraw valida.", "resetLibrary": "Aquò suprimirà vòstra bibliotèca. O volètz vertadièrament ?", "removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la bibliotèca ?", - "invalidEncryptionKey": "La clau de chiframent deu conténer 22 caractèrs. La collaboracion en dirèct es desactivada." + "invalidEncryptionKey": "La clau de chiframent deu conténer 22 caractèrs. La collaboracion en dirèct es desactivada.", + "collabOfflineWarning": "Cap de connexion pas disponibla.\nVòstras modificacions seràn pas salvadas !" }, "errors": { "unsupportedFileType": "Tipe de fichièr pas pres en carga.", @@ -220,7 +221,7 @@ "penMode": "Mòde estilo - empachar lo contact", "link": "Apondre/Actualizar lo ligam per una fòrma seleccionada", "eraser": "Goma", - "hand": "" + "hand": "Man (aisina de desplaçament de la vista)" }, "headings": { "canvasActions": "Accions del canabàs", @@ -239,7 +240,7 @@ "resize": "Podètz servar las proporcions en mantenent la tòca MAJ pendent lo redimensionament,\nmantenètz la tòca ALT per redimensionar a partir del centre", "resizeImage": "Podètz retalhar liurament en quichant CTRL,\nquichatz ALT per retalhar a partir del centre", "rotate": "Podètz restrénger los angles en mantenent MAJ pendent la rotacion", - "lineEditor_info": "", + "lineEditor_info": "Tenètz quichat Ctrl o Cmd e doble clic o quichatz Ctrl o Cmd + Entrada per modificar los ponches", "lineEditor_pointSelected": "Quichar Suprimir per tirar lo(s) punt(s),\nCtrlOCmd+D per duplicar, o lisatz per desplaçar", "lineEditor_nothingSelected": "Seleccionar un punt d’editar (manténer Maj. per ne seleccionar mantun),\no manténer Alt e clicar per n’apondre de novèls", "placeImage": "Clicatz per plaçar l’imatge, o clicatz e lisatz per definir sa talha manualament", @@ -316,8 +317,8 @@ "zoomToFit": "Zoomar per veire totes los elements", "zoomToSelection": "Zoomar la seleccion", "toggleElementLock": "Verrolhar/Desverrolhar la seleccion", - "movePageUpDown": "", - "movePageLeftRight": "" + "movePageUpDown": "Desplaçar la pagina ennaut/enbàs", + "movePageLeftRight": "Desplaçar la pagina a esquèrra/drecha" }, "clearCanvasDialog": { "title": "Escafar canabàs" diff --git a/src/locales/pa-IN.json b/src/locales/pa-IN.json index d5055a917..3bf61d83a 100644 --- a/src/locales/pa-IN.json +++ b/src/locales/pa-IN.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "ਦਿੱਤੀ ਗਈ URL 'ਚੋਂ ਦ੍ਰਿਸ਼ ਨੂੰ ਆਯਾਤ ਨਹੀਂ ਕਰ ਸਕੇ। ਇਹ ਜਾਂ ਤਾਂ ਖਰਾਬ ਹੈ, ਜਾਂ ਇਸ ਵਿੱਚ ਜਾਇਜ਼ Excalidraw JSON ਡਾਟਾ ਸ਼ਾਮਲ ਨਹੀਂ ਹੈ।", "resetLibrary": "ਇਹ ਤੁਹਾਡੀ ਲਾਇਬ੍ਰੇਰੀ ਨੂੰ ਸਾਫ ਕਰ ਦੇਵੇਗਾ। ਕੀ ਤੁਸੀਂ ਪੱਕਾ ਇੰਝ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?", "removeItemsFromsLibrary": "ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚੋਂ {{count}} ਚੀਜ਼(-ਜ਼ਾਂ) ਮਿਟਾਉਣੀਆਂ ਹਨ?", - "invalidEncryptionKey": "" + "invalidEncryptionKey": "", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "", diff --git a/src/locales/percentages.json b/src/locales/percentages.json index 997a62077..29a0c1f2b 100644 --- a/src/locales/percentages.json +++ b/src/locales/percentages.json @@ -1,26 +1,26 @@ { "ar-SA": 92, "bg-BG": 54, - "bn-BD": 60, - "ca-ES": 93, - "cs-CZ": 75, - "da-DK": 33, + "bn-BD": 59, + "ca-ES": 100, + "cs-CZ": 74, + "da-DK": 32, "de-DE": 100, "el-GR": 99, "en": 100, "es-ES": 100, - "eu-ES": 99, + "eu-ES": 100, "fa-IR": 95, - "fi-FI": 92, + "fi-FI": 100, "fr-FR": 100, - "gl-ES": 100, + "gl-ES": 99, "he-IL": 89, "hi-IN": 71, - "hu-HU": 89, + "hu-HU": 88, "id-ID": 99, "it-IT": 100, - "ja-JP": 99, - "kab-KAB": 94, + "ja-JP": 100, + "kab-KAB": 93, "kk-KZ": 20, "ko-KR": 98, "ku-TR": 95, @@ -31,22 +31,22 @@ "nb-NO": 100, "nl-NL": 90, "nn-NO": 89, - "oc-FR": 97, - "pa-IN": 83, + "oc-FR": 98, + "pa-IN": 82, "pl-PL": 84, - "pt-BR": 97, - "pt-PT": 99, - "ro-RO": 99, + "pt-BR": 100, + "pt-PT": 100, + "ro-RO": 100, "ru-RU": 100, "si-LK": 8, "sk-SK": 100, "sl-SI": 100, "sv-SE": 100, - "ta-IN": 92, + "ta-IN": 94, "tr-TR": 97, "uk-UA": 96, "vi-VN": 20, "zh-CN": 100, - "zh-HK": 26, + "zh-HK": 25, "zh-TW": 100 } diff --git a/src/locales/pl-PL.json b/src/locales/pl-PL.json index 04cc86898..89a52b493 100644 --- a/src/locales/pl-PL.json +++ b/src/locales/pl-PL.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Nie udało się zaimportować sceny z podanego adresu URL. Jest ona wadliwa lub nie zawiera poprawnych danych Excalidraw w formacie JSON.", "resetLibrary": "To wyczyści twoją bibliotekę. Jesteś pewien?", "removeItemsFromsLibrary": "Usunąć {{count}} element(ów) z biblioteki?", - "invalidEncryptionKey": "Klucz szyfrowania musi składać się z 22 znaków. Współpraca na żywo jest wyłączona." + "invalidEncryptionKey": "Klucz szyfrowania musi składać się z 22 znaków. Współpraca na żywo jest wyłączona.", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "Nieobsługiwany typ pliku.", diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json index dd3362137..cb7f045de 100644 --- a/src/locales/pt-BR.json +++ b/src/locales/pt-BR.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Não foi possível importar a cena da URL fornecida. Ela está incompleta ou não contém dados JSON válidos do Excalidraw.", "resetLibrary": "Isto limpará a sua biblioteca. Você tem certeza?", "removeItemsFromsLibrary": "Excluir {{count}} item(ns) da biblioteca?", - "invalidEncryptionKey": "A chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desabilitada." + "invalidEncryptionKey": "A chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desabilitada.", + "collabOfflineWarning": "Sem conexão com a internet disponível.\nSuas alterações não serão salvas!" }, "errors": { "unsupportedFileType": "Tipo de arquivo não suportado.", @@ -220,7 +221,7 @@ "penMode": "Modo caneta — impede o toque", "link": "Adicionar/Atualizar link para uma forma selecionada", "eraser": "Borracha", - "hand": "" + "hand": "Mão (ferramenta de rolagem)" }, "headings": { "canvasActions": "Ações da tela", @@ -228,7 +229,7 @@ "shapes": "Formas" }, "hints": { - "canvasPanning": "", + "canvasPanning": "Para mover a tela, segure a roda do mouse ou a barra de espaço enquanto arrasta ou use a ferramenta de mão", "linearElement": "Clique para iniciar vários pontos, arraste para uma única linha", "freeDraw": "Toque e arraste, solte quando terminar", "text": "Dica: você também pode adicionar texto clicando duas vezes em qualquer lugar com a ferramenta de seleção", @@ -247,7 +248,7 @@ "bindTextToElement": "Pressione Enter para adicionar o texto", "deepBoxSelect": "Segure Ctrl/Cmd para seleção profunda e para evitar arrastar", "eraserRevert": "Segure a tecla Alt para inverter os elementos marcados para exclusão", - "firefox_clipboard_write": "" + "firefox_clipboard_write": "Esse recurso pode ser ativado configurando a opção \"dom.events.asyncClipboard.clipboardItem\" como \"true\". Para alterar os sinalizadores do navegador no Firefox, visite a página \"about:config\"." }, "canvasError": { "cannotShowPreview": "Não é possível mostrar pré-visualização", @@ -450,15 +451,15 @@ }, "welcomeScreen": { "app": { - "center_heading": "", - "center_heading_plus": "", - "menuHint": "" + "center_heading": "Todos os dados são salvos localmente no seu navegador.", + "center_heading_plus": "Você queria ir para o Excalidraw+ em vez disso?", + "menuHint": "Exportar, preferências, idiomas..." }, "defaults": { - "menuHint": "", - "center_heading": "", - "toolbarHint": "", - "helpHint": "" + "menuHint": "Exportar, preferências e mais...", + "center_heading": "Diagramas, Feito. Simples.", + "toolbarHint": "Escolha uma ferramenta e comece a desenhar!", + "helpHint": "Atalhos e ajuda" } } } diff --git a/src/locales/pt-PT.json b/src/locales/pt-PT.json index d72c7b98f..f713b22d3 100644 --- a/src/locales/pt-PT.json +++ b/src/locales/pt-PT.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Não foi possível importar a cena a partir do URL fornecido. Ou está mal formado ou não contém dados JSON do Excalidraw válidos.", "resetLibrary": "Isto irá limpar a sua biblioteca. Tem a certeza?", "removeItemsFromsLibrary": "Apagar {{count}} item(ns) da biblioteca?", - "invalidEncryptionKey": "Chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desativada." + "invalidEncryptionKey": "Chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desativada.", + "collabOfflineWarning": "Sem ligação à internet disponível.\nAs suas alterações não serão salvas!" }, "errors": { "unsupportedFileType": "Tipo de ficheiro não suportado.", @@ -220,7 +221,7 @@ "penMode": "Modo caneta - impedir toque", "link": "Acrescentar/ Adicionar ligação para uma forma seleccionada", "eraser": "Borracha", - "hand": "" + "hand": "Mão (ferramenta de movimento da tela)" }, "headings": { "canvasActions": "Ações da área de desenho", @@ -228,7 +229,7 @@ "shapes": "Formas" }, "hints": { - "canvasPanning": "", + "canvasPanning": "Para mover a tela, carregue na roda do rato ou na barra de espaço enquanto arrasta, ou use a ferramenta da mão", "linearElement": "Clique para iniciar vários pontos, arraste para uma única linha", "freeDraw": "Clique e arraste, large quando terminar", "text": "Dica: também pode adicionar texto clicando duas vezes em qualquer lugar com a ferramenta de seleção", @@ -247,7 +248,7 @@ "bindTextToElement": "Carregue Enter para acrescentar texto", "deepBoxSelect": "Mantenha a tecla CtrlOrCmd carregada para selecção profunda, impedindo o arrastamento", "eraserRevert": "Carregue também em Alt para reverter os elementos marcados para serem apagados", - "firefox_clipboard_write": "" + "firefox_clipboard_write": "Esta função pode provavelmente ser ativada definindo a opção \"dom.events.asyncClipboard.clipboardItem\" como \"true\". Para alterar os sinalizadores do navegador no Firefox, visite a página \"about:config\"." }, "canvasError": { "cannotShowPreview": "Não é possível mostrar uma pré-visualização", diff --git a/src/locales/ro-RO.json b/src/locales/ro-RO.json index ff76eea60..8eda2385b 100644 --- a/src/locales/ro-RO.json +++ b/src/locales/ro-RO.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Scena nu a putut fi importată din URL-ul furnizat. Este fie incorect formată, fie nu conține date JSON Excalidraw valide.", "resetLibrary": "Această opțiune va elimina conținutul din bibliotecă. Confirmi?", "removeItemsFromsLibrary": "Ștergi {{count}} element(e) din bibliotecă?", - "invalidEncryptionKey": "Cheia de criptare trebuie să aibă 22 de caractere. Colaborarea în direct este dezactivată." + "invalidEncryptionKey": "Cheia de criptare trebuie să aibă 22 de caractere. Colaborarea în direct este dezactivată.", + "collabOfflineWarning": "Nu este disponibilă nicio conexiune la internet.\nModificările nu vor fi salvate!" }, "errors": { "unsupportedFileType": "Tip de fișier neacceptat.", @@ -220,7 +221,7 @@ "penMode": "Mod stilou – împiedică atingerea", "link": "Adăugare/actualizare URL pentru forma selectată", "eraser": "Radieră", - "hand": "" + "hand": "Mână (instrument de panoramare)" }, "headings": { "canvasActions": "Acțiuni pentru pânză", @@ -228,7 +229,7 @@ "shapes": "Forme" }, "hints": { - "canvasPanning": "", + "canvasPanning": "Pentru a muta pânză, ține apăsată rotița mausului sau bara de spațiu sau folosește instrumentul în formă de mână", "linearElement": "Dă clic pentru a crea mai multe puncte, glisează pentru a forma o singură linie", "freeDraw": "Dă clic pe pânză și glisează cursorul, apoi eliberează-l când ai terminat", "text": "Sfat: poți adăuga text și dând dublu clic oriunde cu instrumentul de selecție", @@ -247,7 +248,7 @@ "bindTextToElement": "Apasă tasta Enter pentru a adăuga text", "deepBoxSelect": "Ține apăsată tasta Ctrl sau Cmd pentru a efectua selectarea de adâncime și pentru a preveni glisarea", "eraserRevert": "Ține apăsată tasta Alt pentru a anula elementele marcate pentru ștergere", - "firefox_clipboard_write": "" + "firefox_clipboard_write": "Această caracteristică poate fi probabil activată prin setarea preferinței „dom.events.asyncClipboard.clipboardItem” ca „true”. Pentru a schimba preferințele navigatorului în Firefox, accesează pagina „about:config”." }, "canvasError": { "cannotShowPreview": "Nu se poate afișa previzualizarea", diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json index ce785dc83..cd0609505 100644 --- a/src/locales/ru-RU.json +++ b/src/locales/ru-RU.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Невозможно импортировать сцену с предоставленного URL. Неверный формат, или не содержит верных Excalidraw JSON данных.", "resetLibrary": "Это очистит вашу библиотеку. Вы уверены?", "removeItemsFromsLibrary": "Удалить {{count}} объект(ов) из библиотеки?", - "invalidEncryptionKey": "Ключ шифрования должен состоять из 22 символов. Одновременное редактирование отключено." + "invalidEncryptionKey": "Ключ шифрования должен состоять из 22 символов. Одновременное редактирование отключено.", + "collabOfflineWarning": "Отсутствует интернет-соединение.\nВаши изменения не будут сохранены!" }, "errors": { "unsupportedFileType": "Неподдерживаемый тип файла.", diff --git a/src/locales/si-LK.json b/src/locales/si-LK.json index dd458b31a..ee6ad46f7 100644 --- a/src/locales/si-LK.json +++ b/src/locales/si-LK.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "", "resetLibrary": "", "removeItemsFromsLibrary": "", - "invalidEncryptionKey": "" + "invalidEncryptionKey": "", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "", diff --git a/src/locales/sk-SK.json b/src/locales/sk-SK.json index 783de30af..2b78b4539 100644 --- a/src/locales/sk-SK.json +++ b/src/locales/sk-SK.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Nepodarilo sa načítať scénu z poskytnutej URL. Je nevalidná alebo neobsahuje žiadne validné Excalidraw JSON dáta.", "resetLibrary": "Týmto vyprázdnite vašu knižnicu. Ste si istý?", "removeItemsFromsLibrary": "Odstrániť {{count}} položiek z knižnice?", - "invalidEncryptionKey": "Šifrovací kľúč musí mať 22 znakov. Živá spolupráca je vypnutá." + "invalidEncryptionKey": "Šifrovací kľúč musí mať 22 znakov. Živá spolupráca je vypnutá.", + "collabOfflineWarning": "Internetové pripojenie nie je dostupné.\nVaše zmeny nebudú uložené!" }, "errors": { "unsupportedFileType": "Nepodporovaný typ súboru.", diff --git a/src/locales/sl-SI.json b/src/locales/sl-SI.json index 54274932f..01c8ba4ca 100644 --- a/src/locales/sl-SI.json +++ b/src/locales/sl-SI.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "S priloženega URL-ja ni bilo mogoče uvoziti scene. Je napačno oblikovana ali pa ne vsebuje veljavnih podatkov Excalidraw JSON.", "resetLibrary": "To bo počistilo vašo knjižnico. Ali ste prepričani?", "removeItemsFromsLibrary": "Izbriši elemente ({{count}}) iz knjižnice?", - "invalidEncryptionKey": "Ključ za šifriranje mora vsebovati 22 znakov. Sodelovanje v živo je onemogočeno." + "invalidEncryptionKey": "Ključ za šifriranje mora vsebovati 22 znakov. Sodelovanje v živo je onemogočeno.", + "collabOfflineWarning": "Internetna povezava ni na voljo.\nVaše spremembe ne bodo shranjene!" }, "errors": { "unsupportedFileType": "Nepodprt tip datoteke.", diff --git a/src/locales/sv-SE.json b/src/locales/sv-SE.json index 30638b9e4..3cf3b0cbd 100644 --- a/src/locales/sv-SE.json +++ b/src/locales/sv-SE.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Det gick inte att importera skiss från den angivna webbadressen. Antingen har den fel format, eller så innehåller den ingen giltig Excalidraw JSON data.", "resetLibrary": "Detta kommer att rensa ditt bibliotek. Är du säker?", "removeItemsFromsLibrary": "Ta bort {{count}} objekt från biblioteket?", - "invalidEncryptionKey": "Krypteringsnyckeln måste vara 22 tecken. Livesamarbetet är inaktiverat." + "invalidEncryptionKey": "Krypteringsnyckeln måste vara 22 tecken. Livesamarbetet är inaktiverat.", + "collabOfflineWarning": "Ingen internetanslutning tillgänglig.\nDina ändringar kommer inte att sparas!" }, "errors": { "unsupportedFileType": "Filtypen stöds inte.", diff --git a/src/locales/ta-IN.json b/src/locales/ta-IN.json index 2ff76b058..493dfa560 100644 --- a/src/locales/ta-IN.json +++ b/src/locales/ta-IN.json @@ -1,7 +1,7 @@ { "labels": { "paste": "ஒட்டு", - "pasteAsPlaintext": "", + "pasteAsPlaintext": "அலங்காரமின்றி ஒட்டு", "pasteCharts": "விளக்கப்படங்களை ஒட்டு", "selectAll": "எல்லாம் தேர்ந்தெடு", "multiSelect": "உறுப்பைத் தெரிவில் சேர்", @@ -54,7 +54,7 @@ "veryLarge": "மிகப் பெரிய", "solid": "திடமான", "hachure": "மலைக்குறிக்கோடு", - "crossHatch": "", + "crossHatch": "குறுக்குகதவு", "thin": "மெல்லிய", "bold": "பட்டை", "left": "இடது", @@ -72,7 +72,7 @@ "layers": "அடுக்குகள்", "actions": "செயல்கள்", "language": "மொழி", - "liveCollaboration": "", + "liveCollaboration": "நேரடி கூட்டுப்பணி...", "duplicateSelection": "நகலாக்கு", "untitled": "தலைப்பற்றது", "name": "பெயர்", @@ -116,7 +116,7 @@ "label": "தொடுப்பு" }, "lineEditor": { - "edit": "", + "edit": "தொடுப்பைத் திருத்து", "exit": "" }, "elementLock": { @@ -137,7 +137,7 @@ "clearReset": "கித்தானை அகரமாக்கு", "exportJSON": "கோப்புக்கு ஏற்றுமதிசெய்", "exportImage": "", - "export": "", + "export": "இதில் சேமி...", "exportToPng": "PNGக்கு ஏற்றுமதிசெய்", "exportToSvg": "SVGக்கு ஏற்றுமதிசெய்", "copyToClipboard": "நகலகத்திற்கு நகலெடு", @@ -145,7 +145,7 @@ "scale": "அளவு", "save": "தற்போதைய கோப்புக்குச் சேமி", "saveAs": "இப்படி சேமி", - "load": "", + "load": "திற", "getShareableLink": "பகிரக்கூடிய தொடுப்பைப் பெறு", "close": "மூடு", "selectLanguage": "மொழியைத் தேர்ந்தெடு", @@ -192,7 +192,8 @@ "invalidSceneUrl": "வழங்கப்பட்ட உரலியிலிருந்து காட்சியை இறக்கவியலா. இது தவறான வடிவத்தில் உள்ளது, அ செல்லத்தக்க எக்ஸ்கேலிட்ரா JSON தரவைக் கொண்டில்லை.", "resetLibrary": "இது உங்கள் நுலகத்தைத் துடைக்கும். நீங்கள் உறுதியா?", "removeItemsFromsLibrary": "{{count}} உருப்படி(கள்)-ஐ உம் நூலகத்திலிருந்து அழிக்கவா?", - "invalidEncryptionKey": "மறையாக்க விசை 22 வரியுருக்கள் கொண்டிருக்கவேண்டும். நேரடி கூட்டுப்பணி முடக்கப்பட்டது." + "invalidEncryptionKey": "மறையாக்க விசை 22 வரியுருக்கள் கொண்டிருக்கவேண்டும். நேரடி கூட்டுப்பணி முடக்கப்பட்டது.", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "ஆதரிக்கப்படா கோப்பு வகை.", @@ -456,9 +457,9 @@ }, "defaults": { "menuHint": "", - "center_heading": "", - "toolbarHint": "", - "helpHint": "" + "center_heading": "எளிமையாக வரைபடங்கள் உருவாக்க!", + "toolbarHint": "கருவியைத் தேர்ந்தெடு & வரை!", + "helpHint": "குறுக்குவழிகள் & உதவி" } } } diff --git a/src/locales/tr-TR.json b/src/locales/tr-TR.json index 00260919c..a05de9a54 100644 --- a/src/locales/tr-TR.json +++ b/src/locales/tr-TR.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "Verilen bağlantıdan çalışma alanı yüklenemedi. Dosya bozuk olabilir veya geçerli bir Excalidraw JSON verisi bulundurmuyor olabilir.", "resetLibrary": "Bu işlem kütüphanenizi sıfırlayacak. Emin misiniz?", "removeItemsFromsLibrary": "{{count}} öğe(ler) kitaplıktan kaldırılsın mı?", - "invalidEncryptionKey": "Şifreleme anahtarı 22 karakter olmalı. Canlı işbirliği devre dışı bırakıldı." + "invalidEncryptionKey": "Şifreleme anahtarı 22 karakter olmalı. Canlı işbirliği devre dışı bırakıldı.", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "Desteklenmeyen dosya türü.", diff --git a/src/locales/uk-UA.json b/src/locales/uk-UA.json index 73597ce9d..ac0bfcfee 100644 --- a/src/locales/uk-UA.json +++ b/src/locales/uk-UA.json @@ -72,7 +72,7 @@ "layers": "Шари", "actions": "Дії", "language": "Мова", - "liveCollaboration": "", + "liveCollaboration": "Спільна робота у режимі реального часу...", "duplicateSelection": "Дублювати", "untitled": "Без назви", "name": "Ім’я", @@ -192,7 +192,8 @@ "invalidSceneUrl": "Не вдалося імпортувати сцену з наданого URL. Він або недоформований, або не містить дійсних даних Excalidraw JSON.", "resetLibrary": "Це призведе до очищення бібліотеки. Ви впевнені?", "removeItemsFromsLibrary": "Видалити {{count}} елемент(ів) з бібліотеки?", - "invalidEncryptionKey": "Ключ шифрування повинен бути довжиною до 22 символів. Спільну роботу вимкнено." + "invalidEncryptionKey": "Ключ шифрування повинен бути довжиною до 22 символів. Спільну роботу вимкнено.", + "collabOfflineWarning": "Немає підключення до Інтернету.\nВаші зміни не будуть збережені!" }, "errors": { "unsupportedFileType": "Непідтримуваний тип файлу.", @@ -457,8 +458,8 @@ "defaults": { "menuHint": "", "center_heading": "", - "toolbarHint": "", - "helpHint": "" + "toolbarHint": "Оберіть інструмент і почніть малювати!", + "helpHint": "Гарячі клавіші і допомога" } } } diff --git a/src/locales/vi-VN.json b/src/locales/vi-VN.json index be5edd61d..1921035bc 100644 --- a/src/locales/vi-VN.json +++ b/src/locales/vi-VN.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "", "resetLibrary": "", "removeItemsFromsLibrary": "", - "invalidEncryptionKey": "" + "invalidEncryptionKey": "", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 6020688a2..2e5debd16 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "无法从提供的 URL 导入场景。它或者格式不正确,或者不包含有效的 Excalidraw JSON 数据。", "resetLibrary": "这将会清除你的素材库。你确定要这么做吗?", "removeItemsFromsLibrary": "确定要从素材库中删除 {{count}} 个项目吗?", - "invalidEncryptionKey": "密钥必须包含22个字符。实时协作已被禁用。" + "invalidEncryptionKey": "密钥必须包含22个字符。实时协作已被禁用。", + "collabOfflineWarning": "无网络连接。\n您的改动将不会被保存!" }, "errors": { "unsupportedFileType": "不支持的文件格式。", @@ -239,7 +240,7 @@ "resize": "您可以按住SHIFT来限制比例大小,\n按住ALT来调整中心大小", "resizeImage": "按住SHIFT可以自由缩放,\n按住ALT可以从中间缩放", "rotate": "旋转时可以按住 Shift 来约束角度", - "lineEditor_info": "按住 CtrlOrCmd 并双击或按 CtrlOrmd + Enter 来编辑点", + "lineEditor_info": "按住 CtrlOrCmd 并双击或按 CtrlOrCmd + Enter 来编辑点", "lineEditor_pointSelected": "按下 Delete 移除点,Ctrl 或 Cmd+D 以复制,拖动以移动", "lineEditor_nothingSelected": "选择要编辑的点 (按住 SHIFT 选择多个),\n或按住 Alt 并点击以添加新点", "placeImage": "点击放置图像,或者点击并拖动以手动设置图像大小", diff --git a/src/locales/zh-HK.json b/src/locales/zh-HK.json index 4ff1176be..e87a0d3e1 100644 --- a/src/locales/zh-HK.json +++ b/src/locales/zh-HK.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "", "resetLibrary": "", "removeItemsFromsLibrary": "", - "invalidEncryptionKey": "" + "invalidEncryptionKey": "", + "collabOfflineWarning": "" }, "errors": { "unsupportedFileType": "", diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index 28ac95a26..3293fad4a 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -192,7 +192,8 @@ "invalidSceneUrl": "無法由提供的 URL 匯入場景。可能是發生異常,或未包含有效的 Excalidraw JSON 資料。", "resetLibrary": "這會清除您的資料庫,是否確定?", "removeItemsFromsLibrary": "從資料庫刪除 {{count}} 項?", - "invalidEncryptionKey": "加密鍵必須為22字元。即時協作已停用。" + "invalidEncryptionKey": "加密鍵必須為22字元。即時協作已停用。", + "collabOfflineWarning": "沒有可用的網路連線。\n變更無法儲存!" }, "errors": { "unsupportedFileType": "不支援的檔案類型。", From 04a8c22f3908614243273f4614a983fc04f59e24 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Wed, 22 Feb 2023 15:01:23 +0100 Subject: [PATCH 182/276] fix: rerender i18n in host components on lang change (#6224) --- src/components/main-menu/DefaultItems.tsx | 33 +++++++------------ .../welcome-screen/WelcomeScreen.Center.tsx | 7 ++-- src/excalidraw-app/app-jotai.ts | 3 ++ src/excalidraw-app/collab/Collab.tsx | 12 +++---- src/excalidraw-app/collab/RoomDialog.tsx | 3 +- .../components/AppWelcomeScreen.tsx | 3 +- .../components/EncryptedIcon.tsx | 32 ++++++++++-------- .../components/ExportToExcalidrawPlus.tsx | 3 +- .../components/LanguageList.tsx | 15 +++++---- src/excalidraw-app/index.tsx | 17 +++++----- src/i18n.ts | 16 +++++++++ src/jotai.ts | 4 +-- src/packages/excalidraw/CHANGELOG.md | 2 ++ src/packages/excalidraw/index.tsx | 10 +++--- 14 files changed, 88 insertions(+), 72 deletions(-) create mode 100644 src/excalidraw-app/app-jotai.ts diff --git a/src/components/main-menu/DefaultItems.tsx b/src/components/main-menu/DefaultItems.tsx index 6e89b598c..b3cc23b90 100644 --- a/src/components/main-menu/DefaultItems.tsx +++ b/src/components/main-menu/DefaultItems.tsx @@ -1,5 +1,5 @@ import { getShortcutFromShortcutName } from "../../actions/shortcuts"; -import { t } from "../../i18n"; +import { useI18n } from "../../i18n"; import { useExcalidrawAppState, useExcalidrawSetAppState, @@ -33,9 +33,7 @@ import { useSetAtom } from "jotai"; import { activeConfirmDialogAtom } from "../ActiveConfirmDialog"; export const LoadScene = () => { - // FIXME Hack until we tie "t" to lang state - // eslint-disable-next-line - const appState = useExcalidrawAppState(); + const { t } = useI18n(); const actionManager = useExcalidrawActionManager(); if (!actionManager.isActionEnabled(actionLoadScene)) { @@ -57,9 +55,7 @@ export const LoadScene = () => { LoadScene.displayName = "LoadScene"; export const SaveToActiveFile = () => { - // FIXME Hack until we tie "t" to lang state - // eslint-disable-next-line - const appState = useExcalidrawAppState(); + const { t } = useI18n(); const actionManager = useExcalidrawActionManager(); if (!actionManager.isActionEnabled(actionSaveToActiveFile)) { @@ -80,9 +76,7 @@ SaveToActiveFile.displayName = "SaveToActiveFile"; export const SaveAsImage = () => { const setAppState = useExcalidrawSetAppState(); - // FIXME Hack until we tie "t" to lang state - // eslint-disable-next-line - const appState = useExcalidrawAppState(); + const { t } = useI18n(); return ( { SaveAsImage.displayName = "SaveAsImage"; export const Help = () => { - // FIXME Hack until we tie "t" to lang state - // eslint-disable-next-line - const appState = useExcalidrawAppState(); + const { t } = useI18n(); const actionManager = useExcalidrawActionManager(); @@ -119,9 +111,8 @@ export const Help = () => { Help.displayName = "Help"; export const ClearCanvas = () => { - // FIXME Hack until we tie "t" to lang state - // eslint-disable-next-line - const appState = useExcalidrawAppState(); + const { t } = useI18n(); + const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom); const actionManager = useExcalidrawActionManager(); @@ -143,6 +134,7 @@ export const ClearCanvas = () => { ClearCanvas.displayName = "ClearCanvas"; export const ToggleTheme = () => { + const { t } = useI18n(); const appState = useExcalidrawAppState(); const actionManager = useExcalidrawActionManager(); @@ -175,6 +167,7 @@ export const ToggleTheme = () => { ToggleTheme.displayName = "ToggleTheme"; export const ChangeCanvasBackground = () => { + const { t } = useI18n(); const appState = useExcalidrawAppState(); const actionManager = useExcalidrawActionManager(); @@ -195,9 +188,7 @@ export const ChangeCanvasBackground = () => { ChangeCanvasBackground.displayName = "ChangeCanvasBackground"; export const Export = () => { - // FIXME Hack until we tie "t" to lang state - // eslint-disable-next-line - const appState = useExcalidrawAppState(); + const { t } = useI18n(); const setAppState = useExcalidrawSetAppState(); return ( void; isCollaborating: boolean; }) => { - // FIXME Hack until we tie "t" to lang state - // eslint-disable-next-line - const appState = useExcalidrawAppState(); + const { t } = useI18n(); return ( any; }) => { - // FIXME when we tie t() to lang state - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const appState = useExcalidrawAppState(); - + const { t } = useI18n(); return ( {t("labels.liveCollaboration")} diff --git a/src/excalidraw-app/app-jotai.ts b/src/excalidraw-app/app-jotai.ts new file mode 100644 index 000000000..8c6c796f6 --- /dev/null +++ b/src/excalidraw-app/app-jotai.ts @@ -0,0 +1,3 @@ +import { unstable_createStore } from "jotai"; + +export const appJotaiStore = unstable_createStore(); diff --git a/src/excalidraw-app/collab/Collab.tsx b/src/excalidraw-app/collab/Collab.tsx index 22f748773..30c9846c8 100644 --- a/src/excalidraw-app/collab/Collab.tsx +++ b/src/excalidraw-app/collab/Collab.tsx @@ -70,7 +70,7 @@ import { decryptData } from "../../data/encryption"; import { resetBrowserStateVersions } from "../data/tabSync"; import { LocalData } from "../data/LocalData"; import { atom, useAtom } from "jotai"; -import { jotaiStore } from "../../jotai"; +import { appJotaiStore } from "../app-jotai"; export const collabAPIAtom = atom(null); export const collabDialogShownAtom = atom(false); @@ -167,7 +167,7 @@ class Collab extends PureComponent { setUsername: this.setUsername, }; - jotaiStore.set(collabAPIAtom, collabAPI); + appJotaiStore.set(collabAPIAtom, collabAPI); this.onOfflineStatusToggle(); if ( @@ -185,7 +185,7 @@ class Collab extends PureComponent { } onOfflineStatusToggle = () => { - jotaiStore.set(isOfflineAtom, !window.navigator.onLine); + appJotaiStore.set(isOfflineAtom, !window.navigator.onLine); }; componentWillUnmount() { @@ -208,10 +208,10 @@ class Collab extends PureComponent { } } - isCollaborating = () => jotaiStore.get(isCollaboratingAtom)!; + isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!; private setIsCollaborating = (isCollaborating: boolean) => { - jotaiStore.set(isCollaboratingAtom, isCollaborating); + appJotaiStore.set(isCollaboratingAtom, isCollaborating); }; private onUnload = () => { @@ -804,7 +804,7 @@ class Collab extends PureComponent { ); handleClose = () => { - jotaiStore.set(collabDialogShownAtom, false); + appJotaiStore.set(collabDialogShownAtom, false); }; setUsername = (username: string) => { diff --git a/src/excalidraw-app/collab/RoomDialog.tsx b/src/excalidraw-app/collab/RoomDialog.tsx index 2c6949aac..50f586efc 100644 --- a/src/excalidraw-app/collab/RoomDialog.tsx +++ b/src/excalidraw-app/collab/RoomDialog.tsx @@ -10,13 +10,13 @@ import { shareWindows, } from "../../components/icons"; import { ToolButton } from "../../components/ToolButton"; -import { t } from "../../i18n"; import "./RoomDialog.scss"; import Stack from "../../components/Stack"; import { AppState } from "../../types"; import { trackEvent } from "../../analytics"; import { getFrame } from "../../utils"; import DialogActionButton from "../../components/DialogActionButton"; +import { useI18n } from "../../i18n"; const getShareIcon = () => { const navigator = window.navigator as any; @@ -51,6 +51,7 @@ const RoomDialog = ({ setErrorMessage: (message: string) => void; theme: AppState["theme"]; }) => { + const { t } = useI18n(); const roomLinkInput = useRef(null); const copyRoomLink = async () => { diff --git a/src/excalidraw-app/components/AppWelcomeScreen.tsx b/src/excalidraw-app/components/AppWelcomeScreen.tsx index 9e760f734..1e34fa819 100644 --- a/src/excalidraw-app/components/AppWelcomeScreen.tsx +++ b/src/excalidraw-app/components/AppWelcomeScreen.tsx @@ -1,12 +1,13 @@ import React from "react"; import { PlusPromoIcon } from "../../components/icons"; -import { t } from "../../i18n"; +import { useI18n } from "../../i18n"; import { WelcomeScreen } from "../../packages/excalidraw/index"; import { isExcalidrawPlusSignedUser } from "../app_constants"; export const AppWelcomeScreen: React.FC<{ setCollabDialogShown: (toggle: boolean) => any; }> = React.memo((props) => { + const { t } = useI18n(); let headingContent; if (isExcalidrawPlusSignedUser) { diff --git a/src/excalidraw-app/components/EncryptedIcon.tsx b/src/excalidraw-app/components/EncryptedIcon.tsx index a3e6ff0ba..a91768917 100644 --- a/src/excalidraw-app/components/EncryptedIcon.tsx +++ b/src/excalidraw-app/components/EncryptedIcon.tsx @@ -1,17 +1,21 @@ import { shield } from "../../components/icons"; import { Tooltip } from "../../components/Tooltip"; -import { t } from "../../i18n"; +import { useI18n } from "../../i18n"; -export const EncryptedIcon = () => ( - - - {shield} - - -); +export const EncryptedIcon = () => { + const { t } = useI18n(); + + return ( + + + {shield} + + + ); +}; diff --git a/src/excalidraw-app/components/ExportToExcalidrawPlus.tsx b/src/excalidraw-app/components/ExportToExcalidrawPlus.tsx index 049a4ddf7..daf4b95c3 100644 --- a/src/excalidraw-app/components/ExportToExcalidrawPlus.tsx +++ b/src/excalidraw-app/components/ExportToExcalidrawPlus.tsx @@ -6,7 +6,7 @@ import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase"; import { FileId, NonDeletedExcalidrawElement } from "../../element/types"; import { AppState, BinaryFileData, BinaryFiles } from "../../types"; import { nanoid } from "nanoid"; -import { t } from "../../i18n"; +import { useI18n } from "../../i18n"; import { excalidrawPlusIcon } from "./icons"; import { encryptData, generateEncryptionKey } from "../../data/encryption"; import { isInitializedImageElement } from "../../element/typeChecks"; @@ -79,6 +79,7 @@ export const ExportToExcalidrawPlus: React.FC<{ files: BinaryFiles; onError: (error: Error) => void; }> = ({ elements, appState, files, onError }) => { + const { t } = useI18n(); return (
{excalidrawPlusIcon}
diff --git a/src/excalidraw-app/components/LanguageList.tsx b/src/excalidraw-app/components/LanguageList.tsx index 1b3606b57..aaa5f2137 100644 --- a/src/excalidraw-app/components/LanguageList.tsx +++ b/src/excalidraw-app/components/LanguageList.tsx @@ -1,22 +1,23 @@ -import { useAtom } from "jotai"; +import { useSetAtom } from "jotai"; import React from "react"; -import { langCodeAtom } from ".."; -import * as i18n from "../../i18n"; +import { appLangCodeAtom } from ".."; +import { defaultLang, useI18n } from "../../i18n"; import { languages } from "../../i18n"; export const LanguageList = ({ style }: { style?: React.CSSProperties }) => { - const [langCode, setLangCode] = useAtom(langCodeAtom); + const { t, langCode } = useI18n(); + const setLangCode = useSetAtom(appLangCodeAtom); return ( onChange(option.value)} - checked={value === option.value} + {props.options.map((option) => + props.type === "button" ? ( + + ) : ( + + ), + )}
); diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 046ee490b..784e81024 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -1008,6 +1008,13 @@ export const UngroupIcon = React.memo(({ theme }: { theme: Theme }) => ), ); +export const FillZigZagIcon = createIcon( + + + , + modifiedTablerIconProps, +); + export const FillHachureIcon = createIcon( <> Date: Wed, 12 Apr 2023 02:53:36 +0530 Subject: [PATCH 228/276] fix: fixing popover overflow on small screen (#6433) Co-authored-by: dwelle --- src/components/Popover.scss | 1 + src/components/Popover.tsx | 86 ++++++++++++++++++++++++++----------- 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/src/components/Popover.scss b/src/components/Popover.scss index 84d16e47f..9458b5026 100644 --- a/src/components/Popover.scss +++ b/src/components/Popover.scss @@ -3,5 +3,6 @@ position: absolute; z-index: 10; padding: 5px 0 5px; + outline: none; } } diff --git a/src/components/Popover.tsx b/src/components/Popover.tsx index 9a046599b..c1249f798 100644 --- a/src/components/Popover.tsx +++ b/src/components/Popover.tsx @@ -29,13 +29,15 @@ export const Popover = ({ }: Props) => { const popoverRef = useRef(null); - const container = popoverRef.current; - useEffect(() => { + const container = popoverRef.current; + if (!container) { return; } + container.focus(); + const handleKeyDown = (event: KeyboardEvent) => { if (event.key === KEYS.TAB) { const focusableElements = queryFocusableElements(container); @@ -44,15 +46,23 @@ export const Popover = ({ (element) => element === activeElement, ); - if (currentIndex === 0 && event.shiftKey) { - focusableElements[focusableElements.length - 1].focus(); + if (activeElement === container) { + if (event.shiftKey) { + focusableElements[focusableElements.length - 1]?.focus(); + } else { + focusableElements[0].focus(); + } + event.preventDefault(); + event.stopImmediatePropagation(); + } else if (currentIndex === 0 && event.shiftKey) { + focusableElements[focusableElements.length - 1]?.focus(); event.preventDefault(); event.stopImmediatePropagation(); } else if ( currentIndex === focusableElements.length - 1 && !event.shiftKey ) { - focusableElements[0].focus(); + focusableElements[0]?.focus(); event.preventDefault(); event.stopImmediatePropagation(); } @@ -62,35 +72,59 @@ export const Popover = ({ container.addEventListener("keydown", handleKeyDown); return () => container.removeEventListener("keydown", handleKeyDown); - }, [container]); + }, []); + + const lastInitializedPosRef = useRef<{ top: number; left: number } | null>( + null, + ); // ensure the popover doesn't overflow the viewport useLayoutEffect(() => { - if (fitInViewport && popoverRef.current) { - const element = popoverRef.current; - const { x, y, width, height } = element.getBoundingClientRect(); + if (fitInViewport && popoverRef.current && top != null && left != null) { + const container = popoverRef.current; + const { width, height } = container.getBoundingClientRect(); - //Position correctly when clicked on rightmost part or the bottom part of viewport - if (x + width - offsetLeft > viewportWidth) { - element.style.left = `${viewportWidth - width - 10}px`; - } - if (y + height - offsetTop > viewportHeight) { - element.style.top = `${viewportHeight - height}px`; + // hack for StrictMode so this effect only runs once for + // the same top/left position, otherwise + // we'd potentically reposition twice (once for viewport overflow) + // and once for top/left position afterwards + if ( + lastInitializedPosRef.current?.top === top && + lastInitializedPosRef.current?.left === left + ) { + return; } + lastInitializedPosRef.current = { top, left }; - //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"; + container.style.width = `${viewportWidth}px`; + container.style.left = "0px"; + container.style.overflowX = "scroll"; + } else if (left + width - offsetLeft > viewportWidth) { + container.style.left = `${viewportWidth - width - 10}px`; + } else { + container.style.left = `${left}px`; + } + + if (height >= viewportHeight) { + container.style.height = `${viewportHeight - 20}px`; + container.style.top = "10px"; + container.style.overflowY = "scroll"; + } else if (top + height - offsetTop > viewportHeight) { + container.style.top = `${viewportHeight - height}px`; + } else { + container.style.top = `${top}px`; } } - }, [fitInViewport, viewportWidth, viewportHeight, offsetLeft, offsetTop]); + }, [ + top, + left, + fitInViewport, + viewportWidth, + viewportHeight, + offsetLeft, + offsetTop, + ]); useEffect(() => { if (onCloseRequest) { @@ -105,7 +139,7 @@ export const Popover = ({ }, [onCloseRequest]); return ( -
+
{children}
); From 372743f59f03b2f368ea0bf704cb37c3dca60cc5 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Wed, 12 Apr 2023 10:57:00 +0200 Subject: [PATCH 229/276] fix: autoredirect to plus in prod only (#6446) --- public/index.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/index.html b/public/index.html index 47a59624b..a8633fc4d 100644 --- a/public/index.html +++ b/public/index.html @@ -79,6 +79,7 @@ + <% if (process.env.NODE_ENV === "production") { %> + <% } %> From 13b27afe0f39cc43d80fa05232e985530d0f0600 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Thu, 13 Apr 2023 11:45:58 +0530 Subject: [PATCH 230/276] fix: update coords when text unbinded from its container (#6445) * fix: update coords when text unbinded from its container * Add specs --- src/actions/actionBoundText.tsx | 6 ++- src/element/textElement.ts | 8 +++- src/element/textWysiwyg.test.tsx | 39 +++++++++++++++++ src/tests/linearElementEditor.test.tsx | 59 +++++++++++++++++++++++++- 4 files changed, 109 insertions(+), 3 deletions(-) diff --git a/src/actions/actionBoundText.tsx b/src/actions/actionBoundText.tsx index 9f9fbfc0f..990f98d41 100644 --- a/src/actions/actionBoundText.tsx +++ b/src/actions/actionBoundText.tsx @@ -2,6 +2,7 @@ import { BOUND_TEXT_PADDING, ROUNDNESS, VERTICAL_ALIGN } from "../constants"; import { getNonDeletedElements, isTextElement, newElement } from "../element"; import { mutateElement } from "../element/mutateElement"; import { + computeBoundTextPosition, computeContainerDimensionForBoundText, getBoundTextElement, measureText, @@ -33,6 +34,7 @@ export const actionUnbindText = register({ trackEvent: { category: "element" }, predicate: (elements, appState) => { const selectedElements = getSelectedElements(elements, appState); + return selectedElements.some((element) => hasBoundTextElement(element)); }, perform: (elements, appState) => { @@ -52,13 +54,15 @@ export const actionUnbindText = register({ element.id, ); resetOriginalContainerCache(element.id); - + const { x, y } = computeBoundTextPosition(element, boundTextElement); mutateElement(boundTextElement as ExcalidrawTextElement, { containerId: null, width, height, baseline, text: boundTextElement.originalText, + x, + y, }); mutateElement(element, { boundElements: element.boundElements?.filter( diff --git a/src/element/textElement.ts b/src/element/textElement.ts index 26cf91a89..38da5df5a 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -245,10 +245,16 @@ export const handleBindTextResize = ( } }; -const computeBoundTextPosition = ( +export const computeBoundTextPosition = ( container: ExcalidrawElement, boundTextElement: ExcalidrawTextElementWithContainer, ) => { + if (isArrowElement(container)) { + return LinearElementEditor.getBoundTextElementPosition( + container, + boundTextElement, + ); + } const containerCoords = getContainerCoords(container); const maxContainerHeight = getMaxContainerHeight(container); const maxContainerWidth = getMaxContainerWidth(container); diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx index cb852cd1c..c55a9befa 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/src/element/textWysiwyg.test.tsx @@ -740,6 +740,45 @@ describe("textWysiwyg", () => { expect(rectangle.boundElements).toBe(null); }); + it("should bind text to container when triggered via context menu", async () => { + expect(h.elements.length).toBe(1); + expect(h.elements[0].id).toBe(rectangle.id); + + UI.clickTool("text"); + mouse.clickAt(20, 30); + const editor = document.querySelector( + ".excalidraw-textEditorContainer > textarea", + ) as HTMLTextAreaElement; + + fireEvent.change(editor, { + target: { + value: "Excalidraw is an opensource virtual collaborative whiteboard", + }, + }); + + editor.dispatchEvent(new Event("input")); + await new Promise((cb) => setTimeout(cb, 0)); + expect(h.elements.length).toBe(2); + expect(h.elements[1].type).toBe("text"); + + API.setSelectedElements([h.elements[0], h.elements[1]]); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 20, + clientY: 30, + }); + const contextMenu = document.querySelector(".context-menu"); + fireEvent.click( + queryByText(contextMenu as HTMLElement, "Bind text to the container")!, + ); + const text = h.elements[1] as ExcalidrawTextElementWithContainer; + expect(rectangle.boundElements).toStrictEqual([ + { id: h.elements[1].id, type: "text" }, + ]); + expect(text.containerId).toBe(rectangle.id); + expect(text.verticalAlign).toBe(VERTICAL_ALIGN.MIDDLE); + }); + it("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => { expect(h.elements.length).toBe(1); diff --git a/src/tests/linearElementEditor.test.tsx b/src/tests/linearElementEditor.test.tsx index ac4d801bc..15fd105ec 100644 --- a/src/tests/linearElementEditor.test.tsx +++ b/src/tests/linearElementEditor.test.tsx @@ -23,7 +23,7 @@ import { getMaxContainerWidth, } from "../element/textElement"; import * as textElementUtils from "../element/textElement"; -import { ROUNDNESS } from "../constants"; +import { ROUNDNESS, VERTICAL_ALIGN } from "../constants"; const renderScene = jest.spyOn(Renderer, "renderScene"); @@ -1191,5 +1191,62 @@ describe("Test Linear Elements", () => { expect(queryByTestId(container, "align-horizontal-center")).toBeNull(); expect(queryByTestId(container, "align-right")).toBeNull(); }); + + it("should update label coords when a label binded via context menu is unbinded", async () => { + createTwoPointerLinearElement("arrow"); + const text = API.createElement({ + type: "text", + text: "Hello Excalidraw", + }); + expect(text.x).toBe(0); + expect(text.y).toBe(0); + + h.elements = [h.elements[0], text]; + + const container = h.elements[0]; + API.setSelectedElements([container, text]); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 20, + clientY: 30, + }); + let contextMenu = document.querySelector(".context-menu"); + + fireEvent.click( + queryByText(contextMenu as HTMLElement, "Bind text to the container")!, + ); + expect(container.boundElements).toStrictEqual([ + { id: h.elements[1].id, type: "text" }, + ]); + expect(text.containerId).toBe(container.id); + expect(text.verticalAlign).toBe(VERTICAL_ALIGN.MIDDLE); + + mouse.reset(); + mouse.clickAt( + container.x + container.width / 2, + container.y + container.height / 2, + ); + mouse.down(); + mouse.up(); + API.setSelectedElements([h.elements[0], h.elements[1]]); + + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 20, + clientY: 30, + }); + contextMenu = document.querySelector(".context-menu"); + fireEvent.click(queryByText(contextMenu as HTMLElement, "Unbind text")!); + expect(container.boundElements).toEqual([]); + expect(text).toEqual( + expect.objectContaining({ + containerId: null, + width: 160, + height: 25, + x: -40, + y: 7.5, + }), + ); + }); }); }); From ca3be2c678dfc5fae50d005fdcfe3b8c84fc2544 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Thu, 13 Apr 2023 17:19:46 +0530 Subject: [PATCH 231/276] fix: exporting labelled arrows via export utils (#6443) * fix: exporting labelled arrows via export utils * add comments * lint * update changelog * fix lint * initialize scene in the utils so it can be availabe in the helper functions * fix library rendering * add comments --- src/components/LibraryUnit.tsx | 10 +++--- src/packages/excalidraw/CHANGELOG.md | 4 +++ src/packages/utils.ts | 48 ++++++++++++++++++++++++---- 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/components/LibraryUnit.tsx b/src/components/LibraryUnit.tsx index 6723ac800..749877cdd 100644 --- a/src/components/LibraryUnit.tsx +++ b/src/components/LibraryUnit.tsx @@ -2,7 +2,7 @@ import clsx from "clsx"; import oc from "open-color"; import { useEffect, useRef, useState } from "react"; import { useDevice } from "../components/App"; -import { exportToSvg } from "../scene/export"; +import { exportToSvg } from "../packages/utils"; import { LibraryItem } from "../types"; import "./LibraryUnit.scss"; import { CheckboxItem } from "./CheckboxItem"; @@ -36,14 +36,14 @@ export const LibraryUnit = ({ if (!elements) { return; } - const svg = await exportToSvg( + const svg = await exportToSvg({ elements, - { + appState: { exportBackground: false, viewBackgroundColor: oc.white, }, - null, - ); + files: null, + }); svg.querySelector(".style-fonts")?.remove(); node.innerHTML = svg.outerHTML; })(); diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 628177a4a..c36883b8e 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -33,6 +33,10 @@ For more details refer to the [docs](https://docs.excalidraw.com) - The optional parameter `refreshDimensions` in [`restoreElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restoreelements) has been removed and can be enabled via `opts` +### Fixes + +- Exporting labelled arrows via export utils [#6443](https://github.com/excalidraw/excalidraw/pull/6443) + ## 0.14.2 (2023-02-01) ### Features diff --git a/src/packages/utils.ts b/src/packages/utils.ts index d81995080..5161e0cab 100644 --- a/src/packages/utils.ts +++ b/src/packages/utils.ts @@ -5,7 +5,6 @@ import { import { getDefaultAppState } from "../appState"; import { AppState, BinaryFiles } from "../types"; import { ExcalidrawElement, NonDeleted } from "../element/types"; -import { getNonDeletedElements } from "../element"; import { restore } from "../data/restore"; import { MIME_TYPES } from "../constants"; import { encodePngMetadata } from "../data/image"; @@ -15,6 +14,7 @@ import { copyTextToSystemClipboard, copyToClipboard, } from "../clipboard"; +import Scene from "../scene/Scene"; export { MIME_TYPES }; @@ -44,9 +44,17 @@ export const exportToCanvas = ({ null, null, ); + // The helper methods getContainerElement and getBoundTextElement are + // dependent on Scene which will not be available + // when these pure utils are called outside Excalidraw or even if called + // from inside Excalidraw when Scene isn't available eg when using library items from store, as a result the element cannot be extracted + // hence initailizing a new scene with the elements + // so its always available to helper methods + const scene = new Scene(); + scene.replaceAllElements(restoredElements); const { exportBackground, viewBackgroundColor } = restoredAppState; return _exportToCanvas( - getNonDeletedElements(restoredElements), + scene.getNonDeletedElements(), { ...restoredAppState, offsetTop: 0, offsetLeft: 0, width: 0, height: 0 }, files || {}, { exportBackground, exportPadding, viewBackgroundColor }, @@ -114,8 +122,18 @@ export const exportToBlob = async ( }; } - const canvas = await exportToCanvas(opts); - + // The helper methods getContainerElement and getBoundTextElement are + // dependent on Scene which will not be available + // when these pure utils are called outside Excalidraw or even if called + // from inside Excalidraw when Scene isn't available eg when using library items from store, as a result the element cannot be extracted + // hence initailizing a new scene with the elements + // so its always available to helper methods + const scene = new Scene(); + scene.replaceAllElements(opts.elements); + const canvas = await exportToCanvas({ + ...opts, + elements: scene.getNonDeletedElements(), + }); quality = quality ? quality : /image\/jpe?g/.test(mimeType) ? 0.92 : 0.8; return new Promise((resolve, reject) => { @@ -132,7 +150,7 @@ export const exportToBlob = async ( blob = await encodePngMetadata({ blob, metadata: serializeAsJSON( - opts.elements, + scene.getNonDeletedElements(), opts.appState, opts.files || {}, "local", @@ -160,8 +178,16 @@ export const exportToSvg = async ({ null, null, ); + // The helper methods getContainerElement and getBoundTextElement are + // dependent on Scene which will not be available + // when these pure utils are called outside Excalidraw or even if called + // from inside Excalidraw when Scene isn't available eg when using library items from store, as a result the element cannot be extracted + // hence initailizing a new scene with the elements + // so its always available to helper methods + const scene = new Scene(); + scene.replaceAllElements(restoredElements); return _exportToSvg( - getNonDeletedElements(restoredElements), + scene.getNonDeletedElements(), { ...restoredAppState, exportPadding, @@ -177,6 +203,14 @@ export const exportToClipboard = async ( type: "png" | "svg" | "json"; }, ) => { + // The helper methods getContainerElement and getBoundTextElement are + // dependent on Scene which will not be available + // when these pure utils are called outside Excalidraw or even if called + // from inside Excalidraw when Scene isn't available eg when using library items from store, as a result the element cannot be extracted + // hence initailizing a new scene with the elements + // so its always available to helper methods + const scene = new Scene(); + scene.replaceAllElements(opts.elements); if (opts.type === "svg") { const svg = await exportToSvg(opts); await copyTextToSystemClipboard(svg.outerHTML); @@ -191,7 +225,7 @@ export const exportToClipboard = async ( ...getDefaultAppState(), ...opts.appState, }; - await copyToClipboard(opts.elements, appState, opts.files); + await copyToClipboard(scene.getNonDeletedElements(), appState, opts.files); } else { throw new Error("Invalid export type"); } From 6164b5273c39de875e136aa808f1e8a7ad62ec92 Mon Sep 17 00:00:00 2001 From: Bear <68773271+SnorfYang@users.noreply.github.com> Date: Fri, 14 Apr 2023 21:22:39 +0800 Subject: [PATCH 232/276] fix: center align text when bind to container via context menu (#6451) --- src/actions/actionBoundText.tsx | 8 +++++++- src/element/textWysiwyg.test.tsx | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/actions/actionBoundText.tsx b/src/actions/actionBoundText.tsx index 990f98d41..3f240b5d8 100644 --- a/src/actions/actionBoundText.tsx +++ b/src/actions/actionBoundText.tsx @@ -1,4 +1,9 @@ -import { BOUND_TEXT_PADDING, ROUNDNESS, VERTICAL_ALIGN } from "../constants"; +import { + BOUND_TEXT_PADDING, + ROUNDNESS, + VERTICAL_ALIGN, + TEXT_ALIGN, +} from "../constants"; import { getNonDeletedElements, isTextElement, newElement } from "../element"; import { mutateElement } from "../element/mutateElement"; import { @@ -132,6 +137,7 @@ export const actionBindText = register({ mutateElement(textElement, { containerId: container.id, verticalAlign: VERTICAL_ALIGN.MIDDLE, + textAlign: TEXT_ALIGN.CENTER, }); mutateElement(container, { boundElements: (container.boundElements || []).concat({ diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx index c55a9befa..4ae3f26f9 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/src/element/textWysiwyg.test.tsx @@ -777,6 +777,13 @@ describe("textWysiwyg", () => { ]); expect(text.containerId).toBe(rectangle.id); expect(text.verticalAlign).toBe(VERTICAL_ALIGN.MIDDLE); + expect(text.textAlign).toBe(TEXT_ALIGN.CENTER); + expect(text.x).toBe( + h.elements[0].x + h.elements[0].width / 2 - text.width / 2, + ); + expect(text.y).toBe( + h.elements[0].y + h.elements[0].height / 2 - text.height / 2, + ); }); it("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => { From b0b23353cfbb2def4bcceabdfbd2f0f3cf6d7f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lie?= <110197391+fouqueta@users.noreply.github.com> Date: Fri, 14 Apr 2023 23:34:26 +0200 Subject: [PATCH 233/276] fix: split "Edit selected shape" shortcut (#6457) Co-authored-by: David Luzar --- src/components/HelpDialog.tsx | 11 ++++++----- src/locales/en.json | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/HelpDialog.tsx b/src/components/HelpDialog.tsx index 8cb775fc5..bd2f38417 100644 --- a/src/components/HelpDialog.tsx +++ b/src/components/HelpDialog.tsx @@ -165,11 +165,12 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => { shortcuts={[KEYS.E, KEYS["0"]]} /> + Date: Sun, 16 Apr 2023 11:56:25 +0200 Subject: [PATCH 234/276] fix: utils leaking Scene state (#6461 * fix: utils leaking Scene state * remove debug * doc * add tests for group duplicating * tweaks --- src/element/newElement.test.ts | 403 ++++++++++++++++++---- src/element/newElement.ts | 162 ++++++++- src/packages/utils.ts | 81 +++-- src/scene/export.ts | 7 +- src/tests/packages/utils.unmocked.test.ts | 67 ++++ 5 files changed, 604 insertions(+), 116 deletions(-) create mode 100644 src/tests/packages/utils.unmocked.test.ts diff --git a/src/element/newElement.test.ts b/src/element/newElement.test.ts index 991c034e0..ba7c63ee2 100644 --- a/src/element/newElement.test.ts +++ b/src/element/newElement.test.ts @@ -1,8 +1,9 @@ -import { duplicateElement } from "./newElement"; +import { duplicateElement, duplicateElements } from "./newElement"; import { mutateElement } from "./mutateElement"; import { API } from "../tests/helpers/api"; import { FONT_FAMILY, ROUNDNESS } from "../constants"; import { isPrimitive } from "../utils"; +import { ExcalidrawLinearElement } from "./types"; const assertCloneObjects = (source: any, clone: any) => { for (const key in clone) { @@ -15,79 +16,353 @@ const assertCloneObjects = (source: any, clone: any) => { } }; -it("clones arrow element", () => { - const element = API.createElement({ - type: "arrow", - x: 0, - y: 0, - strokeColor: "#000000", - backgroundColor: "transparent", - fillStyle: "hachure", - strokeWidth: 1, - strokeStyle: "solid", - roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS }, - roughness: 1, - opacity: 100, +describe("duplicating single elements", () => { + it("clones arrow element", () => { + const element = API.createElement({ + type: "arrow", + x: 0, + y: 0, + strokeColor: "#000000", + backgroundColor: "transparent", + fillStyle: "hachure", + strokeWidth: 1, + strokeStyle: "solid", + roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS }, + roughness: 1, + opacity: 100, + }); + + // @ts-ignore + element.__proto__ = { hello: "world" }; + + mutateElement(element, { + points: [ + [1, 2], + [3, 4], + ], + }); + + const copy = duplicateElement(null, new Map(), element); + + assertCloneObjects(element, copy); + + // assert we clone the object's prototype + // @ts-ignore + expect(copy.__proto__).toEqual({ hello: "world" }); + expect(copy.hasOwnProperty("hello")).toBe(false); + + expect(copy.points).not.toBe(element.points); + expect(copy).not.toHaveProperty("shape"); + expect(copy.id).not.toBe(element.id); + expect(typeof copy.id).toBe("string"); + expect(copy.seed).not.toBe(element.seed); + expect(typeof copy.seed).toBe("number"); + expect(copy).toEqual({ + ...element, + id: copy.id, + seed: copy.seed, + }); }); - // @ts-ignore - element.__proto__ = { hello: "world" }; + it("clones text element", () => { + const element = API.createElement({ + type: "text", + x: 0, + y: 0, + strokeColor: "#000000", + backgroundColor: "transparent", + fillStyle: "hachure", + strokeWidth: 1, + strokeStyle: "solid", + roundness: null, + roughness: 1, + opacity: 100, + text: "hello", + fontSize: 20, + fontFamily: FONT_FAMILY.Virgil, + textAlign: "left", + verticalAlign: "top", + }); - mutateElement(element, { - points: [ - [1, 2], - [3, 4], - ], - }); + const copy = duplicateElement(null, new Map(), element); - const copy = duplicateElement(null, new Map(), element); + assertCloneObjects(element, copy); - assertCloneObjects(element, copy); - - // @ts-ignore - expect(copy.__proto__).toEqual({ hello: "world" }); - expect(copy.hasOwnProperty("hello")).toBe(false); - - expect(copy.points).not.toBe(element.points); - expect(copy).not.toHaveProperty("shape"); - expect(copy.id).not.toBe(element.id); - expect(typeof copy.id).toBe("string"); - expect(copy.seed).not.toBe(element.seed); - expect(typeof copy.seed).toBe("number"); - expect(copy).toEqual({ - ...element, - id: copy.id, - seed: copy.seed, + expect(copy).not.toHaveProperty("points"); + expect(copy).not.toHaveProperty("shape"); + expect(copy.id).not.toBe(element.id); + expect(typeof copy.id).toBe("string"); + expect(typeof copy.seed).toBe("number"); }); }); -it("clones text element", () => { - const element = API.createElement({ - type: "text", - x: 0, - y: 0, - strokeColor: "#000000", - backgroundColor: "transparent", - fillStyle: "hachure", - strokeWidth: 1, - strokeStyle: "solid", - roundness: null, - roughness: 1, - opacity: 100, - text: "hello", - fontSize: 20, - fontFamily: FONT_FAMILY.Virgil, - textAlign: "left", - verticalAlign: "top", +describe("duplicating multiple elements", () => { + it("duplicateElements should clone bindings", () => { + const rectangle1 = API.createElement({ + type: "rectangle", + id: "rectangle1", + boundElements: [ + { id: "arrow1", type: "arrow" }, + { id: "arrow2", type: "arrow" }, + { id: "text1", type: "text" }, + ], + }); + + const text1 = API.createElement({ + type: "text", + id: "text1", + containerId: "rectangle1", + }); + + const arrow1 = API.createElement({ + type: "arrow", + id: "arrow1", + startBinding: { + elementId: "rectangle1", + focus: 0.2, + gap: 7, + }, + }); + + const arrow2 = API.createElement({ + type: "arrow", + id: "arrow2", + endBinding: { + elementId: "rectangle1", + focus: 0.2, + gap: 7, + }, + boundElements: [{ id: "text2", type: "text" }], + }); + + const text2 = API.createElement({ + type: "text", + id: "text2", + containerId: "arrow2", + }); + + // ------------------------------------------------------------------------- + + const origElements = [rectangle1, text1, arrow1, arrow2, text2] as const; + const clonedElements = duplicateElements(origElements); + + // generic id in-equality checks + // -------------------------------------------------------------------------- + expect(origElements.map((e) => e.type)).toEqual( + clonedElements.map((e) => e.type), + ); + origElements.forEach((origElement, idx) => { + const clonedElement = clonedElements[idx]; + expect(origElement).toEqual( + expect.objectContaining({ + id: expect.not.stringMatching(clonedElement.id), + type: clonedElement.type, + }), + ); + if ("containerId" in origElement) { + expect(origElement.containerId).not.toBe( + (clonedElement as any).containerId, + ); + } + if ("endBinding" in origElement) { + if (origElement.endBinding) { + expect(origElement.endBinding.elementId).not.toBe( + (clonedElement as any).endBinding?.elementId, + ); + } else { + expect((clonedElement as any).endBinding).toBeNull(); + } + } + if ("startBinding" in origElement) { + if (origElement.startBinding) { + expect(origElement.startBinding.elementId).not.toBe( + (clonedElement as any).startBinding?.elementId, + ); + } else { + expect((clonedElement as any).startBinding).toBeNull(); + } + } + }); + // -------------------------------------------------------------------------- + + const clonedArrows = clonedElements.filter( + (e) => e.type === "arrow", + ) as ExcalidrawLinearElement[]; + + const [clonedRectangle, clonedText1, , clonedArrow2, clonedArrowLabel] = + clonedElements as any as typeof origElements; + + expect(clonedText1.containerId).toBe(clonedRectangle.id); + expect( + clonedRectangle.boundElements!.find((e) => e.id === clonedText1.id), + ).toEqual( + expect.objectContaining({ + id: clonedText1.id, + type: clonedText1.type, + }), + ); + + clonedArrows.forEach((arrow) => { + // console.log(arrow); + expect( + clonedRectangle.boundElements!.find((e) => e.id === arrow.id), + ).toEqual( + expect.objectContaining({ + id: arrow.id, + type: arrow.type, + }), + ); + + if (arrow.endBinding) { + expect(arrow.endBinding.elementId).toBe(clonedRectangle.id); + } + if (arrow.startBinding) { + expect(arrow.startBinding.elementId).toBe(clonedRectangle.id); + } + }); + + expect(clonedArrow2.boundElements).toEqual([ + { type: "text", id: clonedArrowLabel.id }, + ]); + expect(clonedArrowLabel.containerId).toBe(clonedArrow2.id); }); - const copy = duplicateElement(null, new Map(), element); + it("should remove id references of elements that aren't found", () => { + const rectangle1 = API.createElement({ + type: "rectangle", + id: "rectangle1", + boundElements: [ + // should keep + { id: "arrow1", type: "arrow" }, + // should drop + { id: "arrow-not-exists", type: "arrow" }, + // should drop + { id: "text-not-exists", type: "text" }, + ], + }); - assertCloneObjects(element, copy); + const arrow1 = API.createElement({ + type: "arrow", + id: "arrow1", + startBinding: { + elementId: "rectangle1", + focus: 0.2, + gap: 7, + }, + }); - expect(copy).not.toHaveProperty("points"); - expect(copy).not.toHaveProperty("shape"); - expect(copy.id).not.toBe(element.id); - expect(typeof copy.id).toBe("string"); - expect(typeof copy.seed).toBe("number"); + const text1 = API.createElement({ + type: "text", + id: "text1", + containerId: "rectangle-not-exists", + }); + + const arrow2 = API.createElement({ + type: "arrow", + id: "arrow2", + startBinding: { + elementId: "rectangle1", + focus: 0.2, + gap: 7, + }, + endBinding: { + elementId: "rectangle-not-exists", + focus: 0.2, + gap: 7, + }, + }); + + const arrow3 = API.createElement({ + type: "arrow", + id: "arrow2", + startBinding: { + elementId: "rectangle-not-exists", + focus: 0.2, + gap: 7, + }, + endBinding: { + elementId: "rectangle1", + focus: 0.2, + gap: 7, + }, + }); + + // ------------------------------------------------------------------------- + + const origElements = [rectangle1, text1, arrow1, arrow2, arrow3] as const; + const clonedElements = duplicateElements( + origElements, + ) as any as typeof origElements; + const [ + clonedRectangle, + clonedText1, + clonedArrow1, + clonedArrow2, + clonedArrow3, + ] = clonedElements; + + expect(clonedRectangle.boundElements).toEqual([ + { id: clonedArrow1.id, type: "arrow" }, + ]); + + expect(clonedText1.containerId).toBe(null); + + expect(clonedArrow2.startBinding).toEqual({ + ...arrow2.startBinding, + elementId: clonedRectangle.id, + }); + expect(clonedArrow2.endBinding).toBe(null); + + expect(clonedArrow3.startBinding).toBe(null); + expect(clonedArrow3.endBinding).toEqual({ + ...arrow3.endBinding, + elementId: clonedRectangle.id, + }); + }); + + describe("should duplicate all group ids", () => { + it("should regenerate all group ids and keep them consistent across elements", () => { + const rectangle1 = API.createElement({ + type: "rectangle", + groupIds: ["g1"], + }); + const rectangle2 = API.createElement({ + type: "rectangle", + groupIds: ["g2", "g1"], + }); + const rectangle3 = API.createElement({ + type: "rectangle", + groupIds: ["g2", "g1"], + }); + + const origElements = [rectangle1, rectangle2, rectangle3] as const; + const clonedElements = duplicateElements( + origElements, + ) as any as typeof origElements; + const [clonedRectangle1, clonedRectangle2, clonedRectangle3] = + clonedElements; + + expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]); + expect(rectangle2.groupIds[0]).not.toBe(clonedRectangle2.groupIds[0]); + expect(rectangle2.groupIds[1]).not.toBe(clonedRectangle2.groupIds[1]); + + expect(clonedRectangle1.groupIds[0]).toBe(clonedRectangle2.groupIds[1]); + expect(clonedRectangle2.groupIds[0]).toBe(clonedRectangle3.groupIds[0]); + expect(clonedRectangle2.groupIds[1]).toBe(clonedRectangle3.groupIds[1]); + }); + + it("should keep and regenerate ids of groups even if invalid", () => { + // lone element shouldn't be able to be grouped with itself, + // but hard to check against in a performant way so we ignore it + const rectangle1 = API.createElement({ + type: "rectangle", + groupIds: ["g1"], + }); + + const [clonedRectangle1] = duplicateElements([rectangle1]); + + expect(typeof clonedRectangle1.groupIds[0]).toBe("string"); + expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]); + }); + }); }); diff --git a/src/element/newElement.ts b/src/element/newElement.ts index b6b6cad2d..72aa54684 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -13,7 +13,12 @@ import { FontFamilyValues, ExcalidrawTextContainer, } from "../element/types"; -import { getFontString, getUpdatedTimestamp, isTestEnv } from "../utils"; +import { + arrayToMap, + getFontString, + getUpdatedTimestamp, + isTestEnv, +} from "../utils"; import { randomInteger, randomId } from "../random"; import { mutateElement, newElementWith } from "./mutateElement"; import { getNewGroupIdsForDuplication } from "../groups"; @@ -357,16 +362,24 @@ export const newImageElement = ( }; }; -// Simplified deep clone for the purpose of cloning ExcalidrawElement only -// (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.) +// Simplified deep clone for the purpose of cloning ExcalidrawElement. +// +// Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set, +// Typed arrays and other non-null objects. // // Adapted from https://github.com/lukeed/klona -export const deepCopyElement = (val: any, depth: number = 0) => { +// +// The reason for `deepCopyElement()` wrapper is type safety (only allow +// passing ExcalidrawElement as the top-level argument). +const _deepCopyElement = (val: any, depth: number = 0) => { + // only clone non-primitives if (val == null || typeof val !== "object") { return val; } - if (Object.prototype.toString.call(val) === "[object Object]") { + const objectType = Object.prototype.toString.call(val); + + if (objectType === "[object Object]") { const tmp = typeof val.constructor === "function" ? Object.create(Object.getPrototypeOf(val)) @@ -378,7 +391,7 @@ export const deepCopyElement = (val: any, depth: number = 0) => { if (depth === 0 && (key === "shape" || key === "canvas")) { continue; } - tmp[key] = deepCopyElement(val[key], depth + 1); + tmp[key] = _deepCopyElement(val[key], depth + 1); } } return tmp; @@ -388,14 +401,44 @@ export const deepCopyElement = (val: any, depth: number = 0) => { let k = val.length; const arr = new Array(k); while (k--) { - arr[k] = deepCopyElement(val[k], depth + 1); + arr[k] = _deepCopyElement(val[k], depth + 1); } return arr; } + // we're not cloning non-array & non-plain-object objects because we + // don't support them on excalidraw elements yet. If we do, we need to make + // sure we start cloning them, so let's warn about it. + if (process.env.NODE_ENV === "development") { + if ( + objectType !== "[object Object]" && + objectType !== "[object Array]" && + objectType.startsWith("[object ") + ) { + console.warn( + `_deepCloneElement: unexpected object type ${objectType}. This value will not be cloned!`, + ); + } + } + return val; }; +/** + * Clones ExcalidrawElement data structure. Does not regenerate id, nonce, or + * any value. The purpose is to to break object references for immutability + * reasons, whenever we want to keep the original element, but ensure it's not + * mutated. + * + * Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set, + * Typed arrays and other non-null objects. + */ +export const deepCopyElement = ( + val: T, +): Mutable => { + return _deepCopyElement(val); +}; + /** * Duplicate an element, often used in the alt-drag operation. * Note that this method has gotten a bit complicated since the @@ -410,13 +453,13 @@ export const deepCopyElement = (val: any, depth: number = 0) => { * @param element Element to duplicate * @param overrides Any element properties to override */ -export const duplicateElement = >( +export const duplicateElement = ( editingGroupId: AppState["editingGroupId"], groupIdMapForOperation: Map, element: TElement, overrides?: Partial, -): TElement => { - let copy: TElement = deepCopyElement(element); +): Readonly => { + let copy = deepCopyElement(element); if (isTestEnv()) { copy.id = `${copy.id}_copy`; @@ -449,3 +492,102 @@ export const duplicateElement = >( } return copy; }; + +/** + * Clones elements, regenerating their ids (including bindings) and group ids. + * + * If bindings don't exist in the elements array, they are removed. Therefore, + * it's advised to supply the whole elements array, or sets of elements that + * are encapsulated (such as library items), if the purpose is to retain + * bindings to the cloned elements intact. + */ +export const duplicateElements = (elements: readonly ExcalidrawElement[]) => { + const clonedElements: ExcalidrawElement[] = []; + + const origElementsMap = arrayToMap(elements); + + // used for for migrating old ids to new ids + const elementNewIdsMap = new Map< + /* orig */ ExcalidrawElement["id"], + /* new */ ExcalidrawElement["id"] + >(); + + const maybeGetNewId = (id: ExcalidrawElement["id"]) => { + // if we've already migrated the element id, return the new one directly + if (elementNewIdsMap.has(id)) { + return elementNewIdsMap.get(id)!; + } + // if we haven't migrated the element id, but an old element with the same + // id exists, generate a new id for it and return it + if (origElementsMap.has(id)) { + const newId = randomId(); + elementNewIdsMap.set(id, newId); + return newId; + } + // if old element doesn't exist, return null to mark it for removal + return null; + }; + + const groupNewIdsMap = new Map(); + + for (const element of elements) { + const clonedElement: Mutable = _deepCopyElement(element); + + clonedElement.id = maybeGetNewId(element.id)!; + + if (clonedElement.groupIds) { + clonedElement.groupIds = clonedElement.groupIds.map((groupId) => { + if (!groupNewIdsMap.has(groupId)) { + groupNewIdsMap.set(groupId, randomId()); + } + return groupNewIdsMap.get(groupId)!; + }); + } + + if ("containerId" in clonedElement && clonedElement.containerId) { + const newContainerId = maybeGetNewId(clonedElement.containerId); + clonedElement.containerId = newContainerId; + } + + if ("boundElements" in clonedElement && clonedElement.boundElements) { + clonedElement.boundElements = clonedElement.boundElements.reduce( + ( + acc: Mutable>, + binding, + ) => { + const newBindingId = maybeGetNewId(binding.id); + if (newBindingId) { + acc.push({ ...binding, id: newBindingId }); + } + return acc; + }, + [], + ); + } + + if ("endBinding" in clonedElement && clonedElement.endBinding) { + const newEndBindingId = maybeGetNewId(clonedElement.endBinding.elementId); + clonedElement.endBinding = newEndBindingId + ? { + ...clonedElement.endBinding, + elementId: newEndBindingId, + } + : null; + } + if ("startBinding" in clonedElement && clonedElement.startBinding) { + const newEndBindingId = maybeGetNewId( + clonedElement.startBinding.elementId, + ); + clonedElement.startBinding = newEndBindingId + ? { + ...clonedElement.startBinding, + elementId: newEndBindingId, + } + : null; + } + + clonedElements.push(clonedElement); + } + + return clonedElements; +}; diff --git a/src/packages/utils.ts b/src/packages/utils.ts index 5161e0cab..1fb6cd3d9 100644 --- a/src/packages/utils.ts +++ b/src/packages/utils.ts @@ -15,6 +15,23 @@ import { copyToClipboard, } from "../clipboard"; import Scene from "../scene/Scene"; +import { duplicateElements } from "../element/newElement"; + +// getContainerElement and getBoundTextElement and potentially other helpers +// depend on `Scene` which will not be available when these pure utils are +// called outside initialized Excalidraw editor instance or even if called +// from inside Excalidraw if the elements were never cached by Scene (e.g. +// for library elements). +// +// As such, before passing the elements down, we need to initialize a custom +// Scene instance and assign them to it. +// +// FIXME This is a super hacky workaround and we'll need to rewrite this soon. +const passElementsSafely = (elements: readonly ExcalidrawElement[]) => { + const scene = new Scene(); + scene.replaceAllElements(duplicateElements(elements)); + return scene.getNonDeletedElements(); +}; export { MIME_TYPES }; @@ -44,17 +61,9 @@ export const exportToCanvas = ({ null, null, ); - // The helper methods getContainerElement and getBoundTextElement are - // dependent on Scene which will not be available - // when these pure utils are called outside Excalidraw or even if called - // from inside Excalidraw when Scene isn't available eg when using library items from store, as a result the element cannot be extracted - // hence initailizing a new scene with the elements - // so its always available to helper methods - const scene = new Scene(); - scene.replaceAllElements(restoredElements); const { exportBackground, viewBackgroundColor } = restoredAppState; return _exportToCanvas( - scene.getNonDeletedElements(), + passElementsSafely(restoredElements), { ...restoredAppState, offsetTop: 0, offsetLeft: 0, width: 0, height: 0 }, files || {}, { exportBackground, exportPadding, viewBackgroundColor }, @@ -122,17 +131,9 @@ export const exportToBlob = async ( }; } - // The helper methods getContainerElement and getBoundTextElement are - // dependent on Scene which will not be available - // when these pure utils are called outside Excalidraw or even if called - // from inside Excalidraw when Scene isn't available eg when using library items from store, as a result the element cannot be extracted - // hence initailizing a new scene with the elements - // so its always available to helper methods - const scene = new Scene(); - scene.replaceAllElements(opts.elements); const canvas = await exportToCanvas({ ...opts, - elements: scene.getNonDeletedElements(), + elements: passElementsSafely(opts.elements), }); quality = quality ? quality : /image\/jpe?g/.test(mimeType) ? 0.92 : 0.8; @@ -150,7 +151,10 @@ export const exportToBlob = async ( blob = await encodePngMetadata({ blob, metadata: serializeAsJSON( - scene.getNonDeletedElements(), + // NOTE as long as we're using the Scene hack, we need to ensure + // we pass the original, uncloned elements when serializing + // so that we keep ids stable + opts.elements, opts.appState, opts.files || {}, "local", @@ -178,21 +182,24 @@ export const exportToSvg = async ({ null, null, ); - // The helper methods getContainerElement and getBoundTextElement are - // dependent on Scene which will not be available - // when these pure utils are called outside Excalidraw or even if called - // from inside Excalidraw when Scene isn't available eg when using library items from store, as a result the element cannot be extracted - // hence initailizing a new scene with the elements - // so its always available to helper methods - const scene = new Scene(); - scene.replaceAllElements(restoredElements); + + const exportAppState = { + ...restoredAppState, + exportPadding, + }; + return _exportToSvg( - scene.getNonDeletedElements(), - { - ...restoredAppState, - exportPadding, - }, + passElementsSafely(restoredElements), + exportAppState, files, + { + // NOTE as long as we're using the Scene hack, we need to ensure + // we pass the original, uncloned elements when serializing + // so that we keep ids stable. Hence adding the serializeAsJSON helper + // support into the downstream exportToSvg function. + serializeAsJSON: () => + serializeAsJSON(restoredElements, exportAppState, files || {}, "local"), + }, ); }; @@ -203,14 +210,6 @@ export const exportToClipboard = async ( type: "png" | "svg" | "json"; }, ) => { - // The helper methods getContainerElement and getBoundTextElement are - // dependent on Scene which will not be available - // when these pure utils are called outside Excalidraw or even if called - // from inside Excalidraw when Scene isn't available eg when using library items from store, as a result the element cannot be extracted - // hence initailizing a new scene with the elements - // so its always available to helper methods - const scene = new Scene(); - scene.replaceAllElements(opts.elements); if (opts.type === "svg") { const svg = await exportToSvg(opts); await copyTextToSystemClipboard(svg.outerHTML); @@ -225,7 +224,7 @@ export const exportToClipboard = async ( ...getDefaultAppState(), ...opts.appState, }; - await copyToClipboard(scene.getNonDeletedElements(), appState, opts.files); + await copyToClipboard(opts.elements, appState, opts.files); } else { throw new Error("Invalid export type"); } diff --git a/src/scene/export.ts b/src/scene/export.ts index a6e4297ad..6c5712be6 100644 --- a/src/scene/export.ts +++ b/src/scene/export.ts @@ -90,6 +90,9 @@ export const exportToSvg = async ( exportEmbedScene?: boolean; }, files: BinaryFiles | null, + opts?: { + serializeAsJSON?: () => string; + }, ): Promise => { const { exportPadding = DEFAULT_EXPORT_PADDING, @@ -103,7 +106,9 @@ export const exportToSvg = async ( metadata = await ( await import(/* webpackChunkName: "image" */ "../../src/data/image") ).encodeSvgMetadata({ - text: serializeAsJSON(elements, appState, files || {}, "local"), + text: opts?.serializeAsJSON + ? opts?.serializeAsJSON?.() + : serializeAsJSON(elements, appState, files || {}, "local"), }); } catch (error: any) { console.error(error); diff --git a/src/tests/packages/utils.unmocked.test.ts b/src/tests/packages/utils.unmocked.test.ts new file mode 100644 index 000000000..28db08c49 --- /dev/null +++ b/src/tests/packages/utils.unmocked.test.ts @@ -0,0 +1,67 @@ +import { decodePngMetadata, decodeSvgMetadata } from "../../data/image"; +import { ImportedDataState } from "../../data/types"; +import * as utils from "../../packages/utils"; +import { API } from "../helpers/api"; + +// NOTE this test file is using the actual API, unmocked. Hence splitting it +// from the other test file, because I couldn't figure out how to test +// mocked and unmocked API in the same file. + +describe("embedding scene data", () => { + describe("exportToSvg", () => { + it("embedding scene data shouldn't modify them", async () => { + const rectangle = API.createElement({ type: "rectangle" }); + const ellipse = API.createElement({ type: "ellipse" }); + + const sourceElements = [rectangle, ellipse]; + + const svgNode = await utils.exportToSvg({ + elements: sourceElements, + appState: { + viewBackgroundColor: "#ffffff", + gridSize: null, + exportEmbedScene: true, + }, + files: null, + }); + + const svg = svgNode.outerHTML; + + const parsedString = await decodeSvgMetadata({ svg }); + const importedData: ImportedDataState = JSON.parse(parsedString); + + expect(sourceElements.map((x) => x.id)).toEqual( + importedData.elements?.map((el) => el.id), + ); + }); + }); + + // skipped because we can't test png encoding right now + // (canvas.toBlob not supported in jsdom) + describe.skip("exportToBlob", () => { + it("embedding scene data shouldn't modify them", async () => { + const rectangle = API.createElement({ type: "rectangle" }); + const ellipse = API.createElement({ type: "ellipse" }); + + const sourceElements = [rectangle, ellipse]; + + const blob = await utils.exportToBlob({ + mimeType: "image/png", + elements: sourceElements, + appState: { + viewBackgroundColor: "#ffffff", + gridSize: null, + exportEmbedScene: true, + }, + files: null, + }); + + const parsedString = await decodePngMetadata(blob); + const importedData: ImportedDataState = JSON.parse(parsedString); + + expect(sourceElements.map((x) => x.id)).toEqual( + importedData.elements?.map((el) => el.id), + ); + }); + }); +}); From e31230f78cc40ff40ef4b4add54131e7c427e655 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Sun, 16 Apr 2023 11:57:13 +0200 Subject: [PATCH 235/276] refactor: inline `SingleLibraryItem` into `PublishLibrary` (#6462 refactor: inline `SingleLibraryItem` into `PublishLibrary` to reduce api surface area --- src/components/PublishLibrary.scss | 76 +++++++++++++++++++ src/components/PublishLibrary.tsx | 104 ++++++++++++++++++++++++-- src/components/SingleLibraryItem.scss | 79 ------------------- src/components/SingleLibraryItem.tsx | 104 -------------------------- 4 files changed, 175 insertions(+), 188 deletions(-) delete mode 100644 src/components/SingleLibraryItem.scss delete mode 100644 src/components/SingleLibraryItem.tsx diff --git a/src/components/PublishLibrary.scss b/src/components/PublishLibrary.scss index 6040ff2f4..fd7db0fe4 100644 --- a/src/components/PublishLibrary.scss +++ b/src/components/PublishLibrary.scss @@ -93,4 +93,80 @@ display: block; } } + + .single-library-item { + position: relative; + + &-status { + position: absolute; + top: 0.3rem; + left: 0.3rem; + font-size: 0.7rem; + color: $oc-red-7; + background: rgba(255, 255, 255, 0.9); + padding: 0.1rem 0.2rem; + border-radius: 0.2rem; + } + + &__svg { + background-color: $oc-white; + padding: 0.3rem; + width: 7.5rem; + height: 7.5rem; + border: 1px solid var(--button-gray-2); + svg { + width: 100%; + height: 100%; + } + } + + .ToolIcon__icon { + background-color: $oc-white; + width: auto; + height: auto; + margin: 0 0.5rem; + } + .ToolIcon, + .ToolIcon_type_button:hover { + background-color: white; + } + .required, + .error { + color: $oc-red-8; + font-weight: bold; + font-size: 1rem; + margin: 0.2rem; + } + .error { + font-weight: 500; + margin: 0; + padding: 0.3em 0; + } + + &--remove { + position: absolute; + top: 0.2rem; + right: 1rem; + + .ToolIcon__icon { + margin: 0; + } + .ToolIcon__icon { + background-color: $oc-red-6; + &:hover { + background-color: $oc-red-7; + } + &:active { + background-color: $oc-red-8; + } + } + svg { + color: $oc-white; + padding: 0.26rem; + border-radius: 0.3em; + width: 1rem; + height: 1rem; + } + } + } } diff --git a/src/components/PublishLibrary.tsx b/src/components/PublishLibrary.tsx index 0d060dfaa..b4233222d 100644 --- a/src/components/PublishLibrary.tsx +++ b/src/components/PublishLibrary.tsx @@ -1,11 +1,11 @@ -import { ReactNode, useCallback, useEffect, useState } from "react"; +import { ReactNode, useCallback, useEffect, useRef, useState } from "react"; import OpenColor from "open-color"; import { Dialog } from "./Dialog"; import { t } from "../i18n"; import { AppState, LibraryItems, LibraryItem } from "../types"; -import { exportToCanvas } from "../packages/utils"; +import { exportToCanvas, exportToSvg } from "../packages/utils"; import { EXPORT_DATA_TYPES, EXPORT_SOURCE, @@ -13,12 +13,13 @@ import { VERSIONS, } from "../constants"; import { ExportedLibraryData } from "../data/types"; - -import "./PublishLibrary.scss"; -import SingleLibraryItem from "./SingleLibraryItem"; import { canvasToBlob, resizeImageFile } from "../data/blob"; import { chunk } from "../utils"; import DialogActionButton from "./DialogActionButton"; +import { CloseIcon } from "./icons"; +import { ToolButton } from "./ToolButton"; + +import "./PublishLibrary.scss"; interface PublishLibraryDataParams { authorName: string; @@ -126,6 +127,99 @@ const generatePreviewImage = async (libraryItems: LibraryItems) => { ); }; +const SingleLibraryItem = ({ + libItem, + appState, + index, + onChange, + onRemove, +}: { + libItem: LibraryItem; + appState: AppState; + index: number; + onChange: (val: string, index: number) => void; + onRemove: (id: string) => void; +}) => { + const svgRef = useRef(null); + const inputRef = useRef(null); + + useEffect(() => { + const node = svgRef.current; + if (!node) { + return; + } + (async () => { + const svg = await exportToSvg({ + elements: libItem.elements, + appState: { + ...appState, + viewBackgroundColor: OpenColor.white, + exportBackground: true, + }, + files: null, + }); + node.innerHTML = svg.outerHTML; + })(); + }, [libItem.elements, appState]); + + return ( +
+ {libItem.status === "published" && ( + + {t("labels.statusPublished")} + + )} +
+ +
+ + {libItem.error} +
+
+ ); +}; + const PublishLibrary = ({ onClose, libraryItems, diff --git a/src/components/SingleLibraryItem.scss b/src/components/SingleLibraryItem.scss deleted file mode 100644 index 0a42992a1..000000000 --- a/src/components/SingleLibraryItem.scss +++ /dev/null @@ -1,79 +0,0 @@ -@import "../css/variables.module"; - -.excalidraw { - .single-library-item { - position: relative; - - &-status { - position: absolute; - top: 0.3rem; - left: 0.3rem; - font-size: 0.7rem; - color: $oc-red-7; - background: rgba(255, 255, 255, 0.9); - padding: 0.1rem 0.2rem; - border-radius: 0.2rem; - } - - &__svg { - background-color: $oc-white; - padding: 0.3rem; - width: 7.5rem; - height: 7.5rem; - border: 1px solid var(--button-gray-2); - svg { - width: 100%; - height: 100%; - } - } - - .ToolIcon__icon { - background-color: $oc-white; - width: auto; - height: auto; - margin: 0 0.5rem; - } - .ToolIcon, - .ToolIcon_type_button:hover { - background-color: white; - } - .required, - .error { - color: $oc-red-8; - font-weight: bold; - font-size: 1rem; - margin: 0.2rem; - } - .error { - font-weight: 500; - margin: 0; - padding: 0.3em 0; - } - - &--remove { - position: absolute; - top: 0.2rem; - right: 1rem; - - .ToolIcon__icon { - margin: 0; - } - .ToolIcon__icon { - background-color: $oc-red-6; - &:hover { - background-color: $oc-red-7; - } - &:active { - background-color: $oc-red-8; - } - } - svg { - color: $oc-white; - padding: 0.26rem; - border-radius: 0.3em; - width: 1rem; - height: 1rem; - } - } - } -} diff --git a/src/components/SingleLibraryItem.tsx b/src/components/SingleLibraryItem.tsx deleted file mode 100644 index 45959199c..000000000 --- a/src/components/SingleLibraryItem.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import oc from "open-color"; -import { useEffect, useRef } from "react"; -import { t } from "../i18n"; -import { exportToSvg } from "../packages/utils"; -import { AppState, LibraryItem } from "../types"; -import { CloseIcon } from "./icons"; - -import "./SingleLibraryItem.scss"; -import { ToolButton } from "./ToolButton"; - -const SingleLibraryItem = ({ - libItem, - appState, - index, - onChange, - onRemove, -}: { - libItem: LibraryItem; - appState: AppState; - index: number; - onChange: (val: string, index: number) => void; - onRemove: (id: string) => void; -}) => { - const svgRef = useRef(null); - const inputRef = useRef(null); - - useEffect(() => { - const node = svgRef.current; - if (!node) { - return; - } - (async () => { - const svg = await exportToSvg({ - elements: libItem.elements, - appState: { - ...appState, - viewBackgroundColor: oc.white, - exportBackground: true, - }, - files: null, - }); - node.innerHTML = svg.outerHTML; - })(); - }, [libItem.elements, appState]); - - return ( -
- {libItem.status === "published" && ( - - {t("labels.statusPublished")} - - )} -
- -
- - {libItem.error} -
-
- ); -}; - -export default SingleLibraryItem; From d34cd3072f0789870a8591cafdabc0c06c08a469 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sun, 16 Apr 2023 15:33:16 +0200 Subject: [PATCH 236/276] fix: abort freedraw line if second touch is detected (#6440) --- src/components/App.tsx | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 9b4fc864d..1ebdc7713 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -3500,6 +3500,43 @@ class App extends React.Component { this.setState({ contextMenu: null }); } + this.updateGestureOnPointerDown(event); + + // if dragging element is freedraw and another pointerdown event occurs + // a second finger is on the screen + // discard the freedraw element if it is very short because it is likely + // just a spike, otherwise finalize the freedraw element when the second + // finger is lifted + if ( + event.pointerType === "touch" && + this.state.draggingElement && + this.state.draggingElement.type === "freedraw" + ) { + const element = this.state.draggingElement as ExcalidrawFreeDrawElement; + this.updateScene({ + ...(element.points.length < 10 + ? { + elements: this.scene + .getElementsIncludingDeleted() + .filter((el) => el.id !== element.id), + } + : {}), + appState: { + draggingElement: null, + editingElement: null, + startBoundElement: null, + suggestedBindings: [], + selectedElementIds: Object.keys(this.state.selectedElementIds) + .filter((key) => key !== element.id) + .reduce((obj: { [id: string]: boolean }, key) => { + obj[key] = this.state.selectedElementIds[key]; + return obj; + }, {}), + }, + }); + return; + } + // remove any active selection when we start to interact with canvas // (mainly, we care about removing selection outside the component which // would prevent our copy handling otherwise) @@ -3539,8 +3576,6 @@ class App extends React.Component { }); this.savePointer(event.clientX, event.clientY, "down"); - this.updateGestureOnPointerDown(event); - if (this.handleCanvasPanUsingWheelOrSpaceDrag(event)) { return; } From 034113772d80910b6e5335db946499d9135e46ce Mon Sep 17 00:00:00 2001 From: David Luzar Date: Sun, 16 Apr 2023 15:33:30 +0200 Subject: [PATCH 237/276] fix: color picker keyboard handling not working (#6464) --- src/components/Popover.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/Popover.tsx b/src/components/Popover.tsx index c1249f798..987e9fb91 100644 --- a/src/components/Popover.tsx +++ b/src/components/Popover.tsx @@ -36,7 +36,13 @@ export const Popover = ({ return; } - container.focus(); + // focus popover only if the caller didn't focus on something else nested + // within the popover, which should take precedence. Fixes cases + // like color picker listening to keydown events on containers nested + // in the popover. + if (!container.contains(document.activeElement)) { + container.focus(); + } const handleKeyDown = (event: KeyboardEvent) => { if (event.key === KEYS.TAB) { From e9064a4a87f1086238bc4c3e26bca1d0937e9517 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Sun, 16 Apr 2023 17:09:51 +0200 Subject: [PATCH 238/276] fix: library ids cross-contamination on multiple insert (#6466) --- src/components/LibraryMenuItems.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/LibraryMenuItems.tsx b/src/components/LibraryMenuItems.tsx index f6189fcb7..7ae6517a8 100644 --- a/src/components/LibraryMenuItems.tsx +++ b/src/components/LibraryMenuItems.tsx @@ -12,6 +12,7 @@ import { MIME_TYPES } from "../constants"; import Spinner from "./Spinner"; import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton"; import clsx from "clsx"; +import { duplicateElements } from "../element/newElement"; const CELLS_PER_ROW = 4; @@ -96,7 +97,14 @@ const LibraryMenuItems = ({ } else { targetElements = libraryItems.filter((item) => item.id === id); } - return targetElements; + return targetElements.map((item) => { + return { + ...item, + // duplicate each library item before inserting on canvas to confine + // ids and bindings to each library item. See #6465 + elements: duplicateElements(item.elements), + }; + }); }; const createLibraryItemCompo = (params: { From e7e54814e7fef6fbd89dbb6e740230c6df2a932f Mon Sep 17 00:00:00 2001 From: Excalidraw Bot <77840495+excalibot@users.noreply.github.com> Date: Sun, 16 Apr 2023 17:12:37 +0200 Subject: [PATCH 239/276] chore: Update translations from Crowdin (#6290) * New translations en.json (Occitan) * New translations en.json (Kabyle) * Auto commit: Calculate translation coverage * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Slovenian) * New translations en.json (Norwegian Bokmal) * Auto commit: Calculate translation coverage * New translations en.json (Dutch) * Auto commit: Calculate translation coverage * New translations en.json (Thai) * Auto commit: Calculate translation coverage * New translations en.json (Thai) * Auto commit: Calculate translation coverage * New translations en.json (Chinese Traditional) * New translations en.json (Thai) * Auto commit: Calculate translation coverage * New translations en.json (Galician) * Auto commit: Calculate translation coverage * New translations en.json (German) * Auto commit: Calculate translation coverage * New translations en.json (Marathi) * New translations en.json (Hindi) * Auto commit: Calculate translation coverage * New translations en.json (Thai) * Auto commit: Calculate translation coverage * New translations en.json (Thai) * Auto commit: Calculate translation coverage * New translations en.json (Thai) * Auto commit: Calculate translation coverage * New translations en.json (Thai) * Auto commit: Calculate translation coverage * New translations en.json (Basque) * Auto commit: Calculate translation coverage * New translations en.json (Swedish) * Auto commit: Calculate translation coverage * New translations en.json (Spanish) * Auto commit: Calculate translation coverage * New translations en.json (Hebrew) * Auto commit: Calculate translation coverage * New translations en.json (Hebrew) * Auto commit: Calculate translation coverage * New translations en.json (Hebrew) * New translations en.json (Chinese Simplified) * Auto commit: Calculate translation coverage * New translations en.json (Slovak) * Auto commit: Calculate translation coverage * New translations en.json (Thai) * Auto commit: Calculate translation coverage * New translations en.json (Thai) * Auto commit: Calculate translation coverage * New translations en.json (Turkish) * Auto commit: Calculate translation coverage * New translations en.json (Romanian) * New translations en.json (Spanish) * New translations en.json (Catalan) * New translations en.json (Basque) * New translations en.json (Finnish) * New translations en.json (Portuguese) * New translations en.json (Slovak) * New translations en.json (Chinese Simplified) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Tamil) * New translations en.json (Marathi) * New translations en.json (Hindi) * New translations en.json (French) * New translations en.json (Arabic) * New translations en.json (Bulgarian) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Kurdish) * New translations en.json (Lithuanian) * New translations en.json (Dutch) * New translations en.json (Punjabi) * New translations en.json (Polish) * New translations en.json (Russian) * New translations en.json (Slovenian) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Traditional) * New translations en.json (Vietnamese) * New translations en.json (Galician) * New translations en.json (Indonesian) * New translations en.json (Persian) * New translations en.json (Bengali) * New translations en.json (Norwegian Nynorsk) * New translations en.json (Kazakh) * New translations en.json (Latvian) * New translations en.json (Burmese) * New translations en.json (Chinese Traditional, Hong Kong) * New translations en.json (Sinhala) * New translations en.json (Norwegian Bokmal) * New translations en.json (Occitan) * New translations en.json (Kabyle) * New translations en.json (Thai) * Auto commit: Calculate translation coverage * New translations en.json (Chinese Simplified) * New translations en.json (Greek) * New translations en.json (Slovenian) * New translations en.json (Swedish) * New translations en.json (Norwegian Bokmal) * Auto commit: Calculate translation coverage * New translations en.json (Romanian) * Auto commit: Calculate translation coverage * New translations en.json (Chinese Traditional) * Auto commit: Calculate translation coverage * New translations en.json (Portuguese) * Auto commit: Calculate translation coverage * New translations en.json (Portuguese) * New translations en.json (German) * Auto commit: Calculate translation coverage * New translations en.json (Galician) * Auto commit: Calculate translation coverage * New translations en.json (Galician) * Auto commit: Calculate translation coverage * New translations en.json (Hebrew) * Auto commit: Calculate translation coverage * New translations en.json (Hebrew) * New translations en.json (French) * Auto commit: Calculate translation coverage * New translations en.json (Indonesian) * Auto commit: Calculate translation coverage * New translations en.json (Indonesian) * Auto commit: Calculate translation coverage * New translations en.json (Italian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Simplified) * New translations en.json (Kabyle) * Auto commit: Calculate translation coverage * New translations en.json (Kabyle) * Auto commit: Calculate translation coverage * New translations en.json (Marathi) * New translations en.json (Hindi) * Auto commit: Calculate translation coverage * New translations en.json (Hindi) * Auto commit: Calculate translation coverage * New translations en.json (Slovak) * Auto commit: Calculate translation coverage * New translations en.json (Spanish) * Auto commit: Calculate translation coverage * New translations en.json (Thai) * Auto commit: Calculate translation coverage * New translations en.json (Thai) * Auto commit: Calculate translation coverage * New translations en.json (Japanese) * Auto commit: Calculate translation coverage * New translations en.json (Dutch) * Auto commit: Calculate translation coverage * New translations en.json (Dutch) * Auto commit: Calculate translation coverage * New translations en.json (Basque) * Auto commit: Calculate translation coverage * New translations en.json (Basque) * Auto commit: Calculate translation coverage * New translations en.json (Marathi) * Auto commit: Calculate translation coverage * New translations en.json (Marathi) * Auto commit: Calculate translation coverage * New translations en.json (Marathi) * New translations en.json (Hindi) * Auto commit: Calculate translation coverage * New translations en.json (Polish) * Auto commit: Calculate translation coverage * New translations en.json (Polish) * New translations en.json (Chinese Simplified) * New translations en.json (Vietnamese) * Auto commit: Calculate translation coverage * New translations en.json (Vietnamese) * Auto commit: Calculate translation coverage * New translations en.json (Polish) * Auto commit: Calculate translation coverage * New translations en.json (Korean) * Auto commit: Calculate translation coverage * New translations en.json (Dutch) * New translations en.json (Basque) * New translations en.json (Marathi) * New translations en.json (Hindi) * New translations en.json (Polish) * New translations en.json (Chinese Simplified) * New translations en.json (Vietnamese) * New translations en.json (Korean) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Bulgarian) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Kurdish) * New translations en.json (Lithuanian) * New translations en.json (Punjabi) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Slovak) * New translations en.json (Slovenian) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Traditional) * New translations en.json (Galician) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Indonesian) * New translations en.json (Persian) * New translations en.json (Tamil) * New translations en.json (Bengali) * New translations en.json (Thai) * New translations en.json (Norwegian Nynorsk) * New translations en.json (Kazakh) * New translations en.json (Latvian) * New translations en.json (Burmese) * New translations en.json (Chinese Traditional, Hong Kong) * New translations en.json (Sinhala) * New translations en.json (Norwegian Bokmal) * New translations en.json (Occitan) * New translations en.json (Kabyle) * Auto commit: Calculate translation coverage * New translations en.json (Slovenian) * New translations en.json (Norwegian Bokmal) * Auto commit: Calculate translation coverage * New translations en.json (Japanese) * Auto commit: Calculate translation coverage * New translations en.json (German) * Auto commit: Calculate translation coverage * New translations en.json (Chinese Traditional) * Auto commit: Calculate translation coverage * New translations en.json (Marathi) * New translations en.json (Hindi) * Auto commit: Calculate translation coverage * New translations en.json (Romanian) * Auto commit: Calculate translation coverage * New translations en.json (Italian) * Auto commit: Calculate translation coverage * New translations en.json (Turkish) * Auto commit: Calculate translation coverage --- src/locales/ar-SA.json | 21 +- src/locales/bg-BG.json | 21 +- src/locales/bn-BD.json | 21 +- src/locales/ca-ES.json | 21 +- src/locales/cs-CZ.json | 21 +- src/locales/da-DK.json | 21 +- src/locales/de-DE.json | 21 +- src/locales/el-GR.json | 21 +- src/locales/es-ES.json | 21 +- src/locales/eu-ES.json | 21 +- src/locales/fa-IR.json | 21 +- src/locales/fi-FI.json | 21 +- src/locales/fr-FR.json | 21 +- src/locales/gl-ES.json | 23 +- src/locales/he-IL.json | 307 +++++++++++----------- src/locales/hi-IN.json | 21 +- src/locales/hu-HU.json | 21 +- src/locales/id-ID.json | 23 +- src/locales/it-IT.json | 21 +- src/locales/ja-JP.json | 21 +- src/locales/kab-KAB.json | 33 ++- src/locales/kk-KZ.json | 21 +- src/locales/ko-KR.json | 37 ++- src/locales/ku-TR.json | 21 +- src/locales/lt-LT.json | 21 +- src/locales/lv-LV.json | 21 +- src/locales/mr-IN.json | 21 +- src/locales/my-MM.json | 21 +- src/locales/nb-NO.json | 21 +- src/locales/nl-NL.json | 47 ++-- src/locales/nn-NO.json | 21 +- src/locales/oc-FR.json | 21 +- src/locales/pa-IN.json | 21 +- src/locales/percentages.json | 79 +++--- src/locales/pl-PL.json | 77 +++--- src/locales/pt-BR.json | 21 +- src/locales/pt-PT.json | 21 +- src/locales/ro-RO.json | 21 +- src/locales/ru-RU.json | 21 +- src/locales/si-LK.json | 21 +- src/locales/sk-SK.json | 21 +- src/locales/sl-SI.json | 21 +- src/locales/sv-SE.json | 21 +- src/locales/ta-IN.json | 21 +- src/locales/th-TH.json | 482 +++++++++++++++++++++++++++++++++++ src/locales/tr-TR.json | 31 ++- src/locales/uk-UA.json | 21 +- src/locales/vi-VN.json | 267 ++++++++++--------- src/locales/zh-CN.json | 27 +- src/locales/zh-HK.json | 21 +- src/locales/zh-TW.json | 21 +- 51 files changed, 1784 insertions(+), 468 deletions(-) create mode 100644 src/locales/th-TH.json diff --git a/src/locales/ar-SA.json b/src/locales/ar-SA.json index 6c6c0cf39..25a32f222 100644 --- a/src/locales/ar-SA.json +++ b/src/locales/ar-SA.json @@ -110,6 +110,7 @@ "increaseFontSize": "تكبير حجم الخط", "unbindText": "فك ربط النص", "bindText": "ربط النص بالحاوية", + "createContainerFromText": "", "link": { "edit": "تعديل الرابط", "create": "إنشاء رابط", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "تعذر الاتصال بخادم التعاون. الرجاء إعادة تحميل الصفحة والمحاولة مرة أخرى.", "importLibraryError": "تعذر تحميل المكتبة", "collabSaveFailed": "تعذر الحفظ في قاعدة البيانات. إذا استمرت المشاكل، يفضل أن تحفظ ملفك محليا كي لا تفقد عملك.", - "collabSaveFailed_sizeExceeded": "تعذر الحفظ في قاعدة البيانات، يبدو أن القماش كبير للغاية، يفضّل حفظ الملف محليا كي لا تفقد عملك." + "collabSaveFailed_sizeExceeded": "تعذر الحفظ في قاعدة البيانات، يبدو أن القماش كبير للغاية، يفضّل حفظ الملف محليا كي لا تفقد عملك.", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "تحديد", @@ -303,7 +319,8 @@ "doubleClick": "انقر مرتين", "drag": "اسحب", "editor": "المحرر", - "editSelectedShape": "تعديل الشكل المحدد (النص/السهم/الخط)", + "editLineArrowPoints": "", + "editText": "", "github": "عثرت على مشكلة؟ إرسال", "howto": "اتبع التعليمات", "or": "أو", diff --git a/src/locales/bg-BG.json b/src/locales/bg-BG.json index ba052783d..501ce7399 100644 --- a/src/locales/bg-BG.json +++ b/src/locales/bg-BG.json @@ -110,6 +110,7 @@ "increaseFontSize": "", "unbindText": "", "bindText": "", + "createContainerFromText": "", "link": { "edit": "", "create": "", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "", "importLibraryError": "", "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "Селекция", @@ -303,7 +319,8 @@ "doubleClick": "", "drag": "плъзнете", "editor": "Редактор", - "editSelectedShape": "", + "editLineArrowPoints": "", + "editText": "", "github": "Намерихте проблем? Изпратете", "howto": "Следвайте нашите ръководства", "or": "или", diff --git a/src/locales/bn-BD.json b/src/locales/bn-BD.json index 47c6f02b2..a5d9dec0f 100644 --- a/src/locales/bn-BD.json +++ b/src/locales/bn-BD.json @@ -110,6 +110,7 @@ "increaseFontSize": "লেখনীর মাত্রা বাড়ান", "unbindText": "", "bindText": "", + "createContainerFromText": "", "link": { "edit": "লিঙ্ক সংশোধন", "create": "লিঙ্ক তৈরী", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "কোল্যাব সার্ভারের সাথে সংযোগ করা যায়নি। পৃষ্ঠাটি পুনরায় লোড করে আবার চেষ্টা করুন।", "importLibraryError": "সংগ্রহ লোড করা যায়নি", "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "বাছাই", @@ -303,7 +319,8 @@ "doubleClick": "", "drag": "", "editor": "", - "editSelectedShape": "", + "editLineArrowPoints": "", + "editText": "", "github": "", "howto": "", "or": "অথবা", diff --git a/src/locales/ca-ES.json b/src/locales/ca-ES.json index e94523b29..ae45e764d 100644 --- a/src/locales/ca-ES.json +++ b/src/locales/ca-ES.json @@ -110,6 +110,7 @@ "increaseFontSize": "Augmenta la mida de la lletra", "unbindText": "Desvincular el text", "bindText": "Ajusta el text al contenidor", + "createContainerFromText": "", "link": { "edit": "Edita l'enllaç", "create": "Crea un enllaç", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "No ha estat possible connectar amb el servidor collab. Si us plau recarregueu la pàgina i torneu a provar.", "importLibraryError": "No s'ha pogut carregar la biblioteca", "collabSaveFailed": "No s'ha pogut desar a la base de dades de fons. Si els problemes persisteixen, hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball.", - "collabSaveFailed_sizeExceeded": "No s'ha pogut desar a la base de dades de fons, sembla que el llenç és massa gran. Hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball." + "collabSaveFailed_sizeExceeded": "No s'ha pogut desar a la base de dades de fons, sembla que el llenç és massa gran. Hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball.", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "Selecció", @@ -303,7 +319,8 @@ "doubleClick": "doble clic", "drag": "arrossega", "editor": "Editor", - "editSelectedShape": "Edita la forma seleccionada (text, fletxa o línia)", + "editLineArrowPoints": "", + "editText": "", "github": "Hi heu trobat un problema? Informeu-ne", "howto": "Seguiu les nostres guies", "or": "o", diff --git a/src/locales/cs-CZ.json b/src/locales/cs-CZ.json index 718d974b2..d57a8837d 100644 --- a/src/locales/cs-CZ.json +++ b/src/locales/cs-CZ.json @@ -110,6 +110,7 @@ "increaseFontSize": "Zvětšit písmo", "unbindText": "Zrušit vazbu textu", "bindText": "Vázat text s kontejnerem", + "createContainerFromText": "", "link": { "edit": "Upravit odkaz", "create": "Vytvořit odkaz", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "", "importLibraryError": "", "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "Výběr", @@ -303,7 +319,8 @@ "doubleClick": "dvojklik", "drag": "tažení", "editor": "Editor", - "editSelectedShape": "", + "editLineArrowPoints": "", + "editText": "", "github": "", "howto": "", "or": "nebo", diff --git a/src/locales/da-DK.json b/src/locales/da-DK.json index 8e541f90d..c8b5ad6e3 100644 --- a/src/locales/da-DK.json +++ b/src/locales/da-DK.json @@ -110,6 +110,7 @@ "increaseFontSize": "", "unbindText": "", "bindText": "", + "createContainerFromText": "", "link": { "edit": "", "create": "", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "", "importLibraryError": "", "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "", @@ -303,7 +319,8 @@ "doubleClick": "", "drag": "", "editor": "", - "editSelectedShape": "", + "editLineArrowPoints": "", + "editText": "", "github": "", "howto": "", "or": "", diff --git a/src/locales/de-DE.json b/src/locales/de-DE.json index b389fe5a4..bdf30a371 100644 --- a/src/locales/de-DE.json +++ b/src/locales/de-DE.json @@ -110,6 +110,7 @@ "increaseFontSize": "Schrift vergrößern", "unbindText": "Text lösen", "bindText": "Text an Container binden", + "createContainerFromText": "Text in Container einbetten", "link": { "edit": "Link bearbeiten", "create": "Link erstellen", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Konnte keine Verbindung zum Collab-Server herstellen. Bitte lade die Seite neu und versuche es erneut.", "importLibraryError": "Bibliothek konnte nicht geladen werden", "collabSaveFailed": "Keine Speicherung in der Backend-Datenbank möglich. Wenn die Probleme weiterhin bestehen, solltest Du Deine Datei lokal speichern, um sicherzustellen, dass Du Deine Arbeit nicht verlierst.", - "collabSaveFailed_sizeExceeded": "Keine Speicherung in der Backend-Datenbank möglich, die Zeichenfläche scheint zu groß zu sein. Du solltest Deine Datei lokal speichern, um sicherzustellen, dass Du Deine Arbeit nicht verlierst." + "collabSaveFailed_sizeExceeded": "Keine Speicherung in der Backend-Datenbank möglich, die Zeichenfläche scheint zu groß zu sein. Du solltest Deine Datei lokal speichern, um sicherzustellen, dass Du Deine Arbeit nicht verlierst.", + "brave_measure_text_error": { + "start": "Sieht so aus, als ob du den Brave Browser benutzt mit der", + "aggressive_block_fingerprint": "\"Fingerprinting aggressiv blockieren\"", + "setting_enabled": "Einstellung aktiviert", + "break": "Dies könnte zur inkorrekten Darstellung der", + "text_elements": "Textelemente", + "in_your_drawings": "in deinen Zeichnungen führen", + "strongly_recommend": "Wir empfehlen dringend, diese Einstellung zu deaktivieren. Du kannst", + "steps": "diesen Schritten entsprechend", + "how": "folgen", + "disable_setting": " Wenn die Deaktivierung dieser Einstellung nicht zu einer korrekten Textdarstellung führt, öffne bitte einen", + "issue": "Issue", + "write": "auf GitHub, oder schreibe uns auf", + "discord": "Discord" + } }, "toolBar": { "selection": "Auswahl", @@ -303,7 +319,8 @@ "doubleClick": "doppelklicken", "drag": "ziehen", "editor": "Editor", - "editSelectedShape": "Ausgewählte Form bearbeiten (Text/Pfeil/Linie)", + "editLineArrowPoints": "Linien-/Pfeil-Punkte bearbeiten", + "editText": "Text bearbeiten / Label hinzufügen", "github": "Ein Problem gefunden? Informiere uns", "howto": "Folge unseren Anleitungen", "or": "oder", diff --git a/src/locales/el-GR.json b/src/locales/el-GR.json index d478be3e7..888c39568 100644 --- a/src/locales/el-GR.json +++ b/src/locales/el-GR.json @@ -110,6 +110,7 @@ "increaseFontSize": "Αύξηση μεγέθους γραμματοσειράς", "unbindText": "Αποσύνδεση κειμένου", "bindText": "Δέσμευση κειμένου στο δοχείο", + "createContainerFromText": "", "link": { "edit": "Επεξεργασία συνδέσμου", "create": "Δημιουργία συνδέσμου", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Αδυναμία σύνδεσης με τον διακομιστή συνεργασίας. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά.", "importLibraryError": "Αδυναμία φόρτωσης βιβλιοθήκης", "collabSaveFailed": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή. Αν το προβλήματα παραμείνει, θα πρέπει να αποθηκεύσετε το αρχείο σας τοπικά για να βεβαιωθείτε ότι δεν χάνετε την εργασία σας.", - "collabSaveFailed_sizeExceeded": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή, ο καμβάς φαίνεται να είναι πολύ μεγάλος. Θα πρέπει να αποθηκεύσετε το αρχείο τοπικά για να βεβαιωθείτε ότι δεν θα χάσετε την εργασία σας." + "collabSaveFailed_sizeExceeded": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή, ο καμβάς φαίνεται να είναι πολύ μεγάλος. Θα πρέπει να αποθηκεύσετε το αρχείο τοπικά για να βεβαιωθείτε ότι δεν θα χάσετε την εργασία σας.", + "brave_measure_text_error": { + "start": "Φαίνεται ότι χρησιμοποιείτε το Brave browser με το", + "aggressive_block_fingerprint": "Αποκλεισμός \"Δακτυλικών Αποτυπωμάτων\"", + "setting_enabled": "ρύθμιση ενεργοποιημένη", + "break": "Αυτό θα μπορούσε να σπάσει το", + "text_elements": "Στοιχεία Κειμένου", + "in_your_drawings": "στα σχέδιά σας", + "strongly_recommend": "Συνιστούμε να απενεργοποιήσετε αυτή τη ρύθμιση. Μπορείτε να ακολουθήσετε", + "steps": "αυτά τα βήματα", + "how": "για το πώς να το κάνετε", + "disable_setting": " Εάν η απενεργοποίηση αυτής της ρύθμισης δεν διορθώνει την εμφάνιση των στοιχείων κειμένου, παρακαλώ ανοίξτε ένα", + "issue": "πρόβλημα", + "write": "στο GitHub, ή γράψτε μας στο", + "discord": "Discord" + } }, "toolBar": { "selection": "Επιλογή", @@ -303,7 +319,8 @@ "doubleClick": "διπλό κλικ", "drag": "σύρε", "editor": "Επεξεργαστής", - "editSelectedShape": "Επεξεργασία επιλεγμένου σχήματος (κείμενο/βέλος/γραμμή)", + "editLineArrowPoints": "", + "editText": "", "github": "Βρήκατε πρόβλημα; Υποβάλετε το", "howto": "Ακολουθήστε τους οδηγούς μας", "or": "ή", diff --git a/src/locales/es-ES.json b/src/locales/es-ES.json index eceb38ae3..67a110293 100644 --- a/src/locales/es-ES.json +++ b/src/locales/es-ES.json @@ -110,6 +110,7 @@ "increaseFontSize": "Aumentar el tamaño de letra", "unbindText": "Desvincular texto", "bindText": "Vincular texto al contenedor", + "createContainerFromText": "Envolver el texto en un contenedor", "link": { "edit": "Editar enlace", "create": "Crear enlace", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "No se pudo conectar al servidor colaborador. Por favor, vuelva a cargar la página y vuelva a intentarlo.", "importLibraryError": "No se pudo cargar la librería", "collabSaveFailed": "No se pudo guardar en la base de datos del backend. Si los problemas persisten, debería guardar su archivo localmente para asegurarse de que no pierde su trabajo.", - "collabSaveFailed_sizeExceeded": "No se pudo guardar en la base de datos del backend, el lienzo parece ser demasiado grande. Debería guardar el archivo localmente para asegurarse de que no pierde su trabajo." + "collabSaveFailed_sizeExceeded": "No se pudo guardar en la base de datos del backend, el lienzo parece ser demasiado grande. Debería guardar el archivo localmente para asegurarse de que no pierde su trabajo.", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "ajuste activado", + "break": "Esto podría resultar en romper los", + "text_elements": "Elementos de texto", + "in_your_drawings": "en tus dibujos", + "strongly_recommend": "Recomendamos desactivar esta configuración. Puedes seguir", + "steps": "estos pasos", + "how": "sobre cómo hacerlo", + "disable_setting": " Si deshabilitar esta opción no arregla la visualización de elementos de texto, por favor abre un", + "issue": "issue", + "write": "en GitHub, o escríbenos en", + "discord": "Discord" + } }, "toolBar": { "selection": "Selección", @@ -303,7 +319,8 @@ "doubleClick": "doble clic", "drag": "arrastrar", "editor": "Editor", - "editSelectedShape": "Editar la forma seleccionada (texto/flecha/línea)", + "editLineArrowPoints": "", + "editText": "", "github": "¿Ha encontrado un problema? Envíelo", "howto": "Siga nuestras guías", "or": "o", diff --git a/src/locales/eu-ES.json b/src/locales/eu-ES.json index d35baf4e4..1aec330cb 100644 --- a/src/locales/eu-ES.json +++ b/src/locales/eu-ES.json @@ -110,6 +110,7 @@ "increaseFontSize": "Handitu letra tamaina", "unbindText": "Askatu testua", "bindText": "Lotu testua edukiontziari", + "createContainerFromText": "Bilatu testua edukiontzi batean", "link": { "edit": "Editatu esteka", "create": "Sortu esteka", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Ezin izan da elkarlaneko zerbitzarira konektatu. Mesedez, berriro kargatu orria eta saiatu berriro.", "importLibraryError": "Ezin izan da liburutegia kargatu", "collabSaveFailed": "Ezin izan da backend datu-basean gorde. Arazoak jarraitzen badu, zure fitxategia lokalean gorde beharko zenuke zure lana ez duzula galtzen ziurtatzeko.", - "collabSaveFailed_sizeExceeded": "Ezin izan da backend datu-basean gorde, ohiala handiegia dela dirudi. Fitxategia lokalean gorde beharko zenuke zure lana galtzen ez duzula ziurtatzeko." + "collabSaveFailed_sizeExceeded": "Ezin izan da backend datu-basean gorde, ohiala handiegia dela dirudi. Fitxategia lokalean gorde beharko zenuke zure lana galtzen ez duzula ziurtatzeko.", + "brave_measure_text_error": { + "start": "Brave nabigatzailea erabiltzen ari zarela dirudi", + "aggressive_block_fingerprint": "Aggressively Block Fingerprinting", + "setting_enabled": "ezarpena gaituta", + "break": "Honek honen haustea eragin dezake", + "text_elements": "Testu-elementuak", + "in_your_drawings": "zure marrazkietan", + "strongly_recommend": "Ezarpen hau desgaitzea gomendatzen dugu. Jarrai dezakezu", + "steps": "urrats hauek", + "how": "jakiteko nola egin", + "disable_setting": " Ezarpen hau desgaitzeak testu-elementuen bistaratzea konpontzen ez badu, ireki", + "issue": "eskaera (issue) bat", + "write": "gure Github-en edo idatz iezaguzu", + "discord": "Discord-en" + } }, "toolBar": { "selection": "Hautapena", @@ -303,7 +319,8 @@ "doubleClick": "klik bikoitza", "drag": "arrastatu", "editor": "Editorea", - "editSelectedShape": "Editatu hautatutako forma (testua/gezia/lerroa)", + "editLineArrowPoints": "", + "editText": "", "github": "Arazorik izan al duzu? Eman horren berri", "howto": "Jarraitu gure gidak", "or": "edo", diff --git a/src/locales/fa-IR.json b/src/locales/fa-IR.json index cc7de0dee..44cf7ae00 100644 --- a/src/locales/fa-IR.json +++ b/src/locales/fa-IR.json @@ -110,6 +110,7 @@ "increaseFontSize": "افزایش دادن اندازه فونت", "unbindText": "بازکردن نوشته", "bindText": "بستن نوشته", + "createContainerFromText": "", "link": { "edit": "ویرایش لینک", "create": "ایجاد پیوند", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "به سرور collab متصل نشد. لطفا صفحه را مجددا بارگذاری کنید و دوباره تلاش کنید.", "importLibraryError": "داده‌ها بارگذاری نشدند", "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "گزینش", @@ -303,7 +319,8 @@ "doubleClick": "دابل کلیک", "drag": "کشیدن", "editor": "ویرایشگر", - "editSelectedShape": "ویرایش شکل انتخاب شده (متن/فلش/خط)", + "editLineArrowPoints": "", + "editText": "", "github": "اشکالی می بینید؟ گزارش دهید", "howto": "راهنمای ما را دنبال کنید", "or": "یا", diff --git a/src/locales/fi-FI.json b/src/locales/fi-FI.json index 6c357b167..e0701f2d2 100644 --- a/src/locales/fi-FI.json +++ b/src/locales/fi-FI.json @@ -110,6 +110,7 @@ "increaseFontSize": "Kasvata kirjasinkokoa", "unbindText": "Irroita teksti", "bindText": "Kiinnitä teksti säiliöön", + "createContainerFromText": "", "link": { "edit": "Muokkaa linkkiä", "create": "Luo linkki", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Yhteyden muodostaminen collab-palvelimeen epäonnistui. Virkistä sivu ja yritä uudelleen.", "importLibraryError": "Kokoelman lataaminen epäonnistui", "collabSaveFailed": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi.", - "collabSaveFailed_sizeExceeded": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi." + "collabSaveFailed_sizeExceeded": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi.", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "Valinta", @@ -303,7 +319,8 @@ "doubleClick": "kaksoisnapsautus", "drag": "vedä", "editor": "Muokkausohjelma", - "editSelectedShape": "Muokkaa valittua muotoa (teksti/nuoli/viiva)", + "editLineArrowPoints": "", + "editText": "", "github": "Löysitkö ongelman? Kerro meille", "howto": "Seuraa oppaitamme", "or": "tai", diff --git a/src/locales/fr-FR.json b/src/locales/fr-FR.json index 573e3fd1a..49135c3b6 100644 --- a/src/locales/fr-FR.json +++ b/src/locales/fr-FR.json @@ -110,6 +110,7 @@ "increaseFontSize": "Augmenter la taille de la police", "unbindText": "Dissocier le texte", "bindText": "Associer le texte au conteneur", + "createContainerFromText": "Encadrer le texte dans un conteneur", "link": { "edit": "Modifier le lien", "create": "Ajouter un lien", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Impossible de se connecter au serveur collaboratif. Veuillez recharger la page et réessayer.", "importLibraryError": "Impossible de charger la bibliothèque", "collabSaveFailed": "Impossible d'enregistrer dans la base de données en arrière-plan. Si des problèmes persistent, vous devriez enregistrer votre fichier localement pour vous assurer de ne pas perdre votre travail.", - "collabSaveFailed_sizeExceeded": "Impossible d'enregistrer dans la base de données en arrière-plan, le tableau semble trop grand. Vous devriez enregistrer le fichier localement pour vous assurer de ne pas perdre votre travail." + "collabSaveFailed_sizeExceeded": "Impossible d'enregistrer dans la base de données en arrière-plan, le tableau semble trop grand. Vous devriez enregistrer le fichier localement pour vous assurer de ne pas perdre votre travail.", + "brave_measure_text_error": { + "start": "Il semble que vous utilisiez le navigateur Brave avec le", + "aggressive_block_fingerprint": "blocage d'empreinte agressif", + "setting_enabled": "activé", + "break": "Ceci pourrait avoir pour conséquence de « casser » les", + "text_elements": "éléments texte", + "in_your_drawings": "dans vos dessins", + "strongly_recommend": "Nous recommandons fortement de désactiver ce paramètre. Vous pouvez suivre", + "steps": "ces étapes", + "how": "sur la manière de procéder", + "disable_setting": " Si la désactivation de ce paramètre ne résout pas l'affichage des éléments texte, veuillez ouvrir un", + "issue": "ticket", + "write": "sur notre GitHub, ou nous écrire sur", + "discord": "Discord" + } }, "toolBar": { "selection": "Sélection", @@ -303,7 +319,8 @@ "doubleClick": "double-clic", "drag": "glisser", "editor": "Éditeur", - "editSelectedShape": "Modifier la forme sélectionnée (texte/flèche/ligne)", + "editLineArrowPoints": "", + "editText": "", "github": "Problème trouvé ? Soumettre", "howto": "Suivez nos guides", "or": "ou", diff --git a/src/locales/gl-ES.json b/src/locales/gl-ES.json index 373fe4032..5571f3f15 100644 --- a/src/locales/gl-ES.json +++ b/src/locales/gl-ES.json @@ -110,6 +110,7 @@ "increaseFontSize": "Aumentar o tamaño da fonte", "unbindText": "Desvincular texto", "bindText": "Ligar o texto ao contedor", + "createContainerFromText": "Envolver o texto nun contedor", "link": { "edit": "Editar ligazón", "create": "Crear ligazón", @@ -193,7 +194,7 @@ "resetLibrary": "Isto limpará a súa biblioteca. Está seguro?", "removeItemsFromsLibrary": "Eliminar {{count}} elemento(s) da biblioteca?", "invalidEncryptionKey": "A clave de cifrado debe ter 22 caracteres. A colaboración en directo está desactivada.", - "collabOfflineWarning": "" + "collabOfflineWarning": "Non hai conexión a Internet dispoñible.\nOs teus cambios non serán gardados!" }, "errors": { "unsupportedFileType": "Tipo de ficheiro non soportado.", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Non se puido conectar ao servidor de colaboración. Por favor recargue a páxina e probe de novo.", "importLibraryError": "Non se puido cargar a biblioteca", "collabSaveFailed": "Non se puido gardar na base de datos. Se o problema persiste, deberías gardar o teu arquivo de maneira local para asegurarte de non perdelo teu traballo.", - "collabSaveFailed_sizeExceeded": "Non se puido gardar na base de datos, o lenzo semella demasiado grande. Deberías gardar o teu arquivo de maneira local para asegurarte de non perdelo teu traballo." + "collabSaveFailed_sizeExceeded": "Non se puido gardar na base de datos, o lenzo semella demasiado grande. Deberías gardar o teu arquivo de maneira local para asegurarte de non perdelo teu traballo.", + "brave_measure_text_error": { + "start": "Semella que estás usando o navegador Brave coa opción", + "aggressive_block_fingerprint": "Aggressively Block Fingerprinting", + "setting_enabled": "activada", + "break": "Isto podería provocar unha ruptura dos", + "text_elements": "Elementos de Texto", + "in_your_drawings": "nos seus debuxos", + "strongly_recommend": "Recomendámoslle encarecidamente que desactive esa opción. Pode seguir", + "steps": "estes pasos", + "how": "sobre como facelo", + "disable_setting": " Se ao desactivar esta opción non se arranxa o problema ao mostrar os elementos de texto, por favor abra unha", + "issue": "issue", + "write": "no noso GitHub, ou escríbenos ao", + "discord": "Discord" + } }, "toolBar": { "selection": "Selección", @@ -303,7 +319,8 @@ "doubleClick": "dobre-clic", "drag": "arrastrar", "editor": "Editor", - "editSelectedShape": "Editar a forma seleccionada (texto/frecha/liña)", + "editLineArrowPoints": "", + "editText": "", "github": "Encontrou un problema? Envíeo", "howto": "Sigue as nosas normas", "or": "ou", diff --git a/src/locales/he-IL.json b/src/locales/he-IL.json index 9414da375..810fc1776 100644 --- a/src/locales/he-IL.json +++ b/src/locales/he-IL.json @@ -1,18 +1,18 @@ { "labels": { "paste": "הדבק", - "pasteAsPlaintext": "", + "pasteAsPlaintext": "הדבק ללא עיצוב", "pasteCharts": "הדבק גרפים", "selectAll": "בחר הכל", - "multiSelect": "הוסף אובייקט לבחירה", + "multiSelect": "הוסף רכיב לבחירה", "moveCanvas": "הזז את הקנבס", - "cut": "חתוך", + "cut": "גזור", "copy": "העתק", "copyAsPng": "העתק ללוח כ PNG", "copyAsSvg": "העתק ללוח כ SVG", - "copyText": "העתק ללוח כ-PNG", + "copyText": "העתק ללוח כטקסט", "bringForward": "הבא שכבה קדימה", - "sendToBack": "העבר לסוף", + "sendToBack": "שלח אחורה", "bringToFront": "העבר לחזית", "sendBackward": "העבר שכבה אחורה", "delete": "מחק", @@ -26,7 +26,7 @@ "strokeStyle_solid": "מלא", "strokeStyle_dashed": "מקווקו", "strokeStyle_dotted": "מנוקד", - "sloppiness": "סגנון", + "sloppiness": "רישול", "opacity": "אטימות", "textAlign": "יישור טקסט", "edges": "קצוות", @@ -35,57 +35,57 @@ "arrowheads": "ראשי חצים", "arrowhead_none": "ללא", "arrowhead_arrow": "חץ", - "arrowhead_bar": "שורה", + "arrowhead_bar": "קצה אנכי", "arrowhead_dot": "נקודה", "arrowhead_triangle": "משולש", "fontSize": "גודל גופן", - "fontFamily": "סוג הגופן", + "fontFamily": "גופן", "onlySelected": "רק מה שנבחר", "withBackground": "רקע", "exportEmbedScene": "הטמעה של מידע הסצנה", - "exportEmbedScene_details": "מידע התצוגה יישמר לקובץ המיוצא מסוג PNG/SVG כך שיהיה ניתן לשחזרה ממנו.\nהפעולה תגדיל את גודל הקובץ המיוצא.", + "exportEmbedScene_details": "הייצוא יבוצע לקובץ מסוג PNG/SVG כדי שהמידע על הסצנה ישמר בו וניתן יהיה לבצע שחזור ממנו.\nיגדיל את גודל הקובץ של הייצוא.", "addWatermark": "הוסף \"נוצר באמצעות Excalidraw\"", - "handDrawn": "כתב יד", + "handDrawn": "ציור יד", "normal": "רגיל", "code": "קוד", "small": "קטן", - "medium": "בנוני", + "medium": "בינוני", "large": "גדול", - "veryLarge": "ענק", + "veryLarge": "גדול מאוד", "solid": "מוצק", - "hachure": "קווים משופעים", - "crossHatch": "קווים מוצלבים", + "hachure": "קווים מקבילים קצרים להצגת כיוון וחדות שיפוע במפה", + "crossHatch": "קווים מוצלבים שתי וערב", "thin": "דק", "bold": "מודגש", "left": "שמאל", "center": "מרכז", "right": "ימין", - "extraBold": "עבה", + "extraBold": "מודגש במיוחד", "architect": "ארכיטקט", "artist": "אמן", "cartoonist": "קריקטוריסט", "fileTitle": "שם קובץ", - "colorPicker": "בחירת צבע", + "colorPicker": "בוחר צבעים", "canvasColors": "בשימוש בקנבס", - "canvasBackground": "רקע הלוח", - "drawingCanvas": "לוח ציור", + "canvasBackground": "רקע קנבס", + "drawingCanvas": "קנבס ציור", "layers": "שכבות", "actions": "פעולות", "language": "שפה", - "liveCollaboration": "", + "liveCollaboration": "התחל שיתוף חי...", "duplicateSelection": "שכפל", "untitled": "ללא כותרת", "name": "שם", - "yourName": "שם", + "yourName": "שמך", "madeWithExcalidraw": "נוצר באמצעות Excalidraw", - "group": "אחד לקבוצה", + "group": "קבץ", "ungroup": "פרק קבוצה", "collaborators": "שותפים", "showGrid": "הצג רשת", "addToLibrary": "הוסף לספריה", "removeFromLibrary": "הסר מספריה", "libraryLoadingMessage": "טוען ספריה…", - "libraries": "דפדף בספריות", + "libraries": "עיין בספריות", "loadingScene": "טוען תצוגה…", "align": "יישר", "alignTop": "יישר למעלה", @@ -96,28 +96,29 @@ "centerHorizontally": "מרכז אופקית", "distributeHorizontally": "חלוקה אופקית", "distributeVertically": "חלוקה אנכית", - "flipHorizontal": "סובב אופקית", - "flipVertical": "סובב אנכית", + "flipHorizontal": "הפוך אופקית", + "flipVertical": "הפוך אנכית", "viewMode": "מצב תצוגה", - "toggleExportColorScheme": "שנה את ערכת צבעי הייצוא", + "toggleExportColorScheme": "מתג ערכת צבעים לייצוא", "share": "שתף", - "showStroke": "הצג צבעי קו מתאר", - "showBackground": "הצג צבעי רקע", + "showStroke": "הצג בוחר צבע מברשת", + "showBackground": "הצג בוחר צבע רקע", "toggleTheme": "שינוי ערכת העיצוב", "personalLib": "ספריה פרטית", "excalidrawLib": "הספריה של Excalidraw", "decreaseFontSize": "הקטן את גודל הגופן", "increaseFontSize": "הגדל את גודל הגופן", "unbindText": "ביטול קיבוע הטקסט", - "bindText": "קיבוע הטקסט לאוגד", + "bindText": "קיבוע הטקסט למיכל", + "createContainerFromText": "ארוז טקסט במיכל", "link": { "edit": "עריכת קישור", "create": "יצירת קישור", "label": "קישור" }, "lineEditor": { - "edit": "", - "exit": "" + "edit": "ערוך קו", + "exit": "צא מעורך הקו" }, "elementLock": { "lock": "נעילה", @@ -125,16 +126,16 @@ "lockAll": "לנעול הכל", "unlockAll": "שחרור הכול" }, - "statusPublished": "", - "sidebarLock": "" + "statusPublished": "פורסם", + "sidebarLock": "שמור את סרגל הצד פתוח" }, "library": { - "noItems": "", - "hint_emptyLibrary": "", - "hint_emptyPrivateLibrary": "" + "noItems": "עוד לא הוספת דברים...", + "hint_emptyLibrary": "בחר משהו בקנבס כדי להוסיף אותו לכאן, או שתתקין ספריה מהספריה הציבורית מטה.", + "hint_emptyPrivateLibrary": "בחר משהו בקנבס כדי להוסיף אותו לכאן." }, "buttons": { - "clearReset": "אפס את הלוח", + "clearReset": "אפס את הקנבאס", "exportJSON": "ייצא לקובץ", "exportImage": "ייצוא התמונה...", "export": "שמור ל...", @@ -143,7 +144,7 @@ "copyToClipboard": "העתק ללוח", "copyPngToClipboard": "העתק PNG ללוח", "scale": "קנה מידה", - "save": "שמירת קובץ נוכחי", + "save": "שמור לקובץ נוכחי", "saveAs": "שמירה בשם", "load": "פתח", "getShareableLink": "קבל קישור לשיתוף", @@ -159,58 +160,73 @@ "undo": "בטל", "redo": "בצע מחדש", "resetLibrary": "איפוס ספריה", - "createNewRoom": "צור חדר", + "createNewRoom": "צור חדר חדש", "fullScreen": "מסך מלא", "darkMode": "מצב כהה", "lightMode": "מצב בהיר", "zenMode": "מצב זן", - "exitZenMode": "צא ממצב תפריט מרחף", + "exitZenMode": "צא ממצב זן", "cancel": "ביטול", "clear": "ניקוי", - "remove": "מחיקה", - "publishLibrary": "פירסום", + "remove": "הסר", + "publishLibrary": "פרסום", "submit": "שליחה", - "confirm": "לאשר" + "confirm": "אשר" }, "alerts": { - "clearReset": "פעולה זו תנקה את כל הלוח. אתה בטוח?", - "couldNotCreateShareableLink": "לא ניתן לייצר לינק לשיתוף.", - "couldNotCreateShareableLinkTooBig": "לא הצלחנו לייצר קישור לשיתוף: התצוגה גדולה מדי", - "couldNotLoadInvalidFile": "לא ניתן לטעון קובץ שאיננו תואם", + "clearReset": "פעולה זו תנקה את כל הקנבס. אתה בטוח?", + "couldNotCreateShareableLink": "יצירת קישור לשיתוף נכשל.", + "couldNotCreateShareableLinkTooBig": "יצירת קישור לשיתוף נכשל: התצוגה גדולה מדי", + "couldNotLoadInvalidFile": "טעינת קובץ לא תקין נכשלה", "importBackendFailed": "ייבוא מהשרת נכשל.", - "cannotExportEmptyCanvas": "לא ניתן לייצא לוח ריק.", - "couldNotCopyToClipboard": "לא ניתן היה להעתיק ללוח", - "decryptFailed": "לא ניתן לפענח מידע.", - "uploadedSecurly": "ההעלאה הוצפנה מקצה לקצה, ולכן שרת Excalidraw וצד שלישי לא יכולים לקרוא את התוכן.", + "cannotExportEmptyCanvas": "לא ניתן לייצא קנבאס ריק.", + "couldNotCopyToClipboard": "לא ניתן היה להעתיק ללוח.", + "decryptFailed": "פיענוח ההצפנה של המידע נכשל.", + "uploadedSecurly": "ההעלאה אובטחה באמצעות הצפנה מקצה לקצה, פירוש הדבר שהשרת של Excalidraw וגורמי צד ג׳ לא יכולים לקרוא את התוכן.", "loadSceneOverridePrompt": "טעינה של ציור חיצוני תחליף את התוכן הקיים שלך. האם תרצה להמשיך?", - "collabStopOverridePrompt": "עצירת השיתוף תוביל למחיקת התרשימים השמורים בדפדפן. האם את/ה בטוח/ה?\n(אם תרצה לשמור את התרשימים הקיימים, תוכל לסגור את הדפדפן מבלי לסיים את השיתוף.)", + "collabStopOverridePrompt": "עצירת השיתוף תוביל למחיקת הציור הקודם ששמור מקומית בדפדפן. האם אתה בטוח?\n\n(אם תרצה לשמור את הציור המקומי, סגור את הטאב של הדפדפן במקום.)", "errorAddingToLibrary": "לא ניתן להוסיף פריט לספרייה", - "errorRemovingFromLibrary": "לא ניתן למחוק פריט מהספריה", - "confirmAddLibrary": "הפעולה תוסיף {{numShapes}} צורה(ות) לספריה שלך. האם אתה בטוח?", + "errorRemovingFromLibrary": "לא ניתן להסיר פריט מהספריה", + "confirmAddLibrary": "זה יוסיף {{numShapes}} צורה(ות) לספריה שלך. האם אתה בטוח?", "imageDoesNotContainScene": "נראה שהתמונה לא מכילה מידע על הסצינה. האם אפשרת הטמעת מידע הסצינה בעת השמירה?", - "cannotRestoreFromImage": "לא הצלחנו לשחזר את התצוגה מקובץ התמונה", - "invalidSceneUrl": "ייבוא המידע מן סצינה מכתובת האינטרנט נכשלה. המידע בנוי באופן משובש או שהוא אינו קובץ JSON תקין של Excalidraw.", - "resetLibrary": "פעולה זו תנקה את כל הלוח. אתה בטוח?", - "removeItemsFromsLibrary": "מחיקת {{count}} פריטים(ים) מתוך הספריה?", - "invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל.", - "collabOfflineWarning": "" + "cannotRestoreFromImage": "לא הצלחנו לשחזר את הסצנה מקובץ התמונה", + "invalidSceneUrl": "ייבוא מידע סצנה מהקישור שסופק כשל. או שהוא משובש, או שאינו מכיל מידע של Excalidraw בפורמט JSON.", + "resetLibrary": "פעולה זו תנקה את כל הספריה שלך. אתה בטוח?", + "removeItemsFromsLibrary": "מחק {{count}} פריט(ים) מהספריה?", + "invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מנוטרל.", + "collabOfflineWarning": "אין חיבור זמין לאינטרנט.\nהשינויים שלך לא ישמרו!" }, "errors": { "unsupportedFileType": "סוג הקובץ אינו נתמך.", - "imageInsertError": "לא ניתן היה להטמיע את התמונה, אנא נסו שוב מאוחר יותר...", - "fileTooBig": "הקובץ כבד מדי. הגודל המקסימלי המותר הוא {{maxSize}}.", - "svgImageInsertError": "לא ניתן היה להטמיע את תמונת ה-SVG. קידוד ה-SVG אינו תקני.", - "invalidSVGString": "SVG בלתי תקני.", - "cannotResolveCollabServer": "", + "imageInsertError": "לא ניתן היה להוסיף את התמונה. אנא נסה שוב מאוחר יותר...", + "fileTooBig": "הקובץ גדול מדי. הגודל המירבי המותר הינו {{maxSize}}.", + "svgImageInsertError": "לא ניתן היה להוסיף את תמונת ה-SVG. הסימונים בתוך קובץ ה-SVG עשויים להיות שגויים.", + "invalidSVGString": "SVG שגוי.", + "cannotResolveCollabServer": "לא הצלחתי להתחבר לשרת השיתוף. אנא רענן את הדף ונסה שוב.", "importLibraryError": "לא ניתן היה לטעון את הספריה", - "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed": "לא הצלחתי להתחבר למסד הנתונים האחורי. אם הבעיה ממשיכה, כדאי שתשמור את הקובץ מקומית כדי לוודא שלא תאבד את העבודה שלך.", + "collabSaveFailed_sizeExceeded": "לא הצלחתי לשמור למסד הנתונים האחורי, נראה שהקנבס שלך גדול מדי. כדאי שתשמור את הקובץ מקומית כדי לוודא שלא תאבד את העבודה שלך.", + "brave_measure_text_error": { + "start": "נראה שאתה משתמש בדפדפן Brave עם ה-", + "aggressive_block_fingerprint": "חסימת Fingerprinting אגרסיבית", + "setting_enabled": "ההגדרה מופעלת", + "break": "זה יכול לגרום לבעיה ב-", + "text_elements": "רכיבי טקסט", + "in_your_drawings": "בציורים שלך", + "strongly_recommend": "אנו ממליצים בחום לנטרל את ההגדרה הזו. אתה יכול לעקוב", + "steps": "הצעדים הבאים", + "how": "כיצד לעשות את זה", + "disable_setting": " אם נטרול ההגדרה לא מתקן את תצוגת רכיבי הטקסט, אנא פתח", + "issue": "בעיה", + "write": "ב- GitHub שלנו, או כתוב לנו ב-", + "discord": "Discord" + } }, "toolBar": { "selection": "בחירה", "image": "הוספת תמונה", - "rectangle": "מרובע", - "diamond": "מעוין", + "rectangle": "מלבן", + "diamond": "יהלום", "ellipse": "אליפסה", "arrow": "חץ", "line": "קו", @@ -218,79 +234,79 @@ "text": "טקסט", "library": "ספריה", "lock": "השאר את הכלי הנבחר פעיל גם לאחר סיום הציור", - "penMode": "", - "link": "הוספה/עדכון של קישור עבור הצורה הנבחרת", + "penMode": "מצב עט - מנע נגיעה", + "link": "הוספה/עדכון קישור של הצורה שנבחרה", "eraser": "מחק", - "hand": "" + "hand": "יד (כלי הזזה)" }, "headings": { - "canvasActions": "פעולות הלוח", - "selectedShapeActions": "פעולות צורה שנבחרה", + "canvasActions": "פעולות קנבאס", + "selectedShapeActions": "פעולות על הצורות שנבחרו", "shapes": "צורות" }, "hints": { - "canvasPanning": "", - "linearElement": "הקלק בשביל לבחור נקודות מרובות, גרור בשביל קו בודד", + "canvasPanning": "כדי להזיז את הקנבס, החזק את גלגל העכבר לחוץ או את מקש הרווח לחוץ תוך כדי גרירה, או השתמש בכלי היד", + "linearElement": "לחץ להתחלת מספר נקודות, גרור לקו יחיד", "freeDraw": "לחץ וגרור, שחרר כשסיימת", "text": "טיפ: אפשר להוסיף טקסט על ידי לחיצה כפולה בכל מקום עם כלי הבחירה", - "text_selected": "לחץ לחיצה כפולה או אנטר לעריכת הנקודות", - "text_editing": "כדי לסיים את העריכה לחצו על מקש Escape או על Ctrl ומקש Enter (Cmd במחשבי אפל)", + "text_selected": "לחץ לחיצה כפולה או הקש על אנטר לעריכת הטקסט", + "text_editing": "כדי לסיים את העריכה לחץ על מקש Escape או על Ctrl (Cmd במחשבי אפל) ומקש Enter", "linearElementMulti": "הקלק על הנקודה האחרונה או הקש Escape או Enter לסיום", - "lockAngle": "אתה יכול להגביל זווית ע״י לחיצה על SHIFT", + "lockAngle": "ניתן להגביל את הזוויות על ידי החזקה של מקש ה- SHIFT", "resize": "ניתן להגביל פרופורציות על ידי לחיצה על SHIFT תוך כדי שינוי גודל,\nהחזק ALT בשביל לשנות גודל ביחס למרכז", - "resizeImage": "", + "resizeImage": "אתה יכול לשנות את הגודל בחופשיות על ידי החזקת מקש SHIFT,\nהחזק את מקש ALT כדי לבצע שינוי גודל מהמרכז", "rotate": "ניתן להגביל זוויות על ידי לחיצה על SHIFT תוך כדי סיבוב", - "lineEditor_info": "", - "lineEditor_pointSelected": "", - "lineEditor_nothingSelected": "", - "placeImage": "", - "publishLibrary": "פירסום ספריה אישית", - "bindTextToElement": "יש להקיש Enter כדי להוסיף טקסט", - "deepBoxSelect": "", - "eraserRevert": "", - "firefox_clipboard_write": "" + "lineEditor_info": "החזק Ctrl / Cmd ובצע לחיצה כפולה או לחץ Ctrl / Cmd + Enter לעריכת נקודות", + "lineEditor_pointSelected": "לחץ Delete למחיקת נקודה/ות,\nCtrl / Cmd + D לשכפול, או גרור להזזה", + "lineEditor_nothingSelected": "בחר נקודה כדי לערוך (החזק SHIFT לבחירת כמה),\nאו החזק Alt והקלק להוספת נקודות חדשות", + "placeImage": "הקלק להנחת התמונה, או הקלק וגרור להגדרת הגודל שלו ידנית", + "publishLibrary": "פרסם ספריה משלך", + "bindTextToElement": "הקש Enter כדי להוספת טקסט", + "deepBoxSelect": "החזק Ctrl / Cmd לבחירה עמוקה ולמניעת גרירה", + "eraserRevert": "החזק Alt להחזרת רכיבים מסומנים למחיקה", + "firefox_clipboard_write": "יכולות זה ניתנת להפעלה על ידי שינוי הדגל של \"dom.events.asyncClipboard.clipboardItem\" למצב \"true\". כדי לשנות את הדגל בדפדפן Firefox, בקר בעמוד ״about:config״." }, "canvasError": { - "cannotShowPreview": "לא הצלחנו להציג את התצוגה המקדימה", + "cannotShowPreview": "לא ניתן להראות תצוגה מקדימה", "canvasTooBig": "הקנבס עלול להיות גדול מדי.", - "canvasTooBigTip": "טיפ: נסה להזיז את האלמנטים הרחוקים ביותר מעט קרוב יותר יחד." + "canvasTooBigTip": "טיפ: נסה להזיז את הרכיבים הרחוקים ביותר מעט קרוב יותר האחד לשני." }, "errorSplash": { "headingMain_pre": "אירעה שגיאה. נסה ", "headingMain_button": "טוען את העמוד מחדש.", "clearCanvasMessage": "אם טעינה מחדש לא עובדת, נסה ", - "clearCanvasMessage_button": "מנקה את הלוח.", - "clearCanvasCaveat": " זה יביא לאובדן עבודה ", - "trackedToSentry_pre": "שגיאה עם מזהה ", + "clearCanvasMessage_button": "מנקה את הקנבאס.", + "clearCanvasCaveat": " זה יגרום לאובדן העבודה ", + "trackedToSentry_pre": "השגיאה עם מזהה ", "trackedToSentry_post": " נמצאה במערכת שלנו.", - "openIssueMessage_pre": "נזהרנו מאוד שלא לכלול מידע שלך בשגיאה. אם המידע איננו אישי, בבקשה עקוב אחר ", + "openIssueMessage_pre": "נזהרנו מאוד שלא לכלול מידע מהקנבאס שלך בשגיאה. אם המידע בקנבאס אינו אישי, שקול לבצע מעקב אחר הטיפול שלנו ", "openIssueMessage_button": "מעקב באגים.", - "openIssueMessage_post": " בבקשה כלול את המידע למטה באמצעות העתקה והדבקה בנושא ב GitHub.", - "sceneContent": "תוכן הלוח:" + "openIssueMessage_post": " בבקשה כלול את המידע מטה באמצעות העתקתה שלו, והדבקה שלו ב- GitHub Issue.", + "sceneContent": "תוכן הקנבאס:" }, "roomDialog": { - "desc_intro": "אתה יכול להזמין אנשים ללוח הנוכחי שלך בכדי לשתף פעולה.", + "desc_intro": "אתה יכול להזמין אנשים לקנבאס הנוכחי שלך לעבודה משותפת.", "desc_privacy": "אל דאגה, השיתוף מוצפן מקצה לקצה, כך שכל מה שתצייר ישאר פרטי. אפילו השרתים שלנו לא יוכלו לראות את מה שאתה ממציא.", "button_startSession": "התחל שיתוף", "button_stopSession": "הפסק שיתוף", - "desc_inProgressIntro": "שיתוף חי כרגע בפעולה.", + "desc_inProgressIntro": "שיתוף חי פעיל כרגע.", "desc_shareLink": "שתף את הקישור עם כל מי שאתה מעוניין לעבוד אתו:", - "desc_exitSession": "עצירת השיתוף תנתק אותך מהחדר, אבל עדיין תוכל להמשיך לעבוד על הלוח, מקומית. שים לב שזה לא ישפיע על אנשים אחרים, והם עדיין יוכלו לשתף פעולה עם הגירסה שלהם.", - "shareTitle": "הצטרף לסשן שיתוף בזמן אמת של Excalidraw" + "desc_exitSession": "עצירת השיתוף תנתק אותך מהחדר, אבל עדיין תוכל להמשיך לעבוד על הקנבאס, מקומית. שים לב שזה לא ישפיע על אנשים אחרים, והם עדיין יוכלו לבצע שיתוף עם הגרסה שלהם.", + "shareTitle": "הצטרף לשיתוף לעבודה משותפת חיה, בזמן אמת, על גבי Excalidraw" }, "errorDialog": { "title": "שגיאה" }, "exportDialog": { "disk_title": "שמור לכונן", - "disk_details": "ייצוא מידע הסצינה לקובץ אותו ניתן יהיה לייבא בהמשך.", + "disk_details": "ייצא מידע של הקנבאס לקובץ שתוכל לייבא אחר כך.", "disk_button": "שמירה לקובץ", - "link_title": "העתקת קישור לשיתוף", + "link_title": "קבל קישור לשיתוף", "link_details": "ייצוא כקישור לקריאה בלבד.", - "link_button": "ייצוא כקישור", - "excalidrawplus_description": "שמור את המפה לסביבת העבודה שלך ב-Excalidraw+.", + "link_button": "ייצוא לקישור", + "excalidrawplus_description": "שמור את הקנבאס לסביבת העבודה שלך ב- +Excalidraw.", "excalidrawplus_button": "ייצוא", - "excalidrawplus_exportError": "הייצוא ל-Excalidraw+ לא הצליח לעת עתה..." + "excalidrawplus_exportError": "לא הצלחתי לייצא ל- +Excalidraw כרגע..." }, "helpDialog": { "blog": "קרא את הבלוג שלנו", @@ -301,30 +317,31 @@ "curvedLine": "קו מעוגל", "documentation": "תיעוד", "doubleClick": "לחיצה כפולה", - "drag": "לגרור", + "drag": "גרור", "editor": "עורך", - "editSelectedShape": "ערוך את הצורה הנבחרת (טקסט/חץ/קו)", + "editLineArrowPoints": "", + "editText": "", "github": "מצאת בעיה? דווח", "howto": "עקוב אחר המדריכים שלנו", "or": "או", "preventBinding": "למנוע נעיצת חיצים", "tools": "כלים", "shortcuts": "קיצורי מקלדת", - "textFinish": "סיים עריכה (טקסט)", - "textNewLine": "הוסף שורה חדשה (טקסט)", + "textFinish": "סיים עריכה (עורך טקסט)", + "textNewLine": "הוסף שורה חדשה (עורך טקסט)", "title": "עזרה", "view": "תצוגה", - "zoomToFit": "גלילה להצגת כל האלמנטים במסך", + "zoomToFit": "זום להתאמת כל האלמנטים למסך", "zoomToSelection": "התמקד בבחירה", "toggleElementLock": "נעילה/ביטול הנעילה של הרכיבים הנבחרים", - "movePageUpDown": "", - "movePageLeftRight": "" + "movePageUpDown": "זוז עמוד למעלה/למטה", + "movePageLeftRight": "זוז עמוד שמאלה/ימינה" }, "clearCanvasDialog": { "title": "ניקוי הקנבס" }, "publishDialog": { - "title": "פרסום ספריה", + "title": "פרסם ספריה", "itemName": "שם הפריט", "authorName": "שם היוצר", "githubUsername": "שם המשתמש שלך ב-GitHub", @@ -333,11 +350,11 @@ "libraryDesc": "תיאור הספריה", "website": "אתר", "placeholder": { - "authorName": "שם או שם משתמש", + "authorName": "שמך או שם המשתמש שלך", "libraryName": "תנו שם לספריה", "libraryDesc": "תיאור של הספריה שלך כדי לסייע למשתמשים להבין את השימוש בה", - "githubHandle": "", - "twitterHandle": "", + "githubHandle": "כינוי GitHub (לא חובה), כדי שתוכל לערוך את הספרית לאחר שנשלחה לבדיקה", + "twitterHandle": "שם משתמש טוויטר (לא חובה), כדי שנדע למי לתת קרדיט כשאנחנו מפרסמים בטוויטר", "website": "קישור לאתר הפרטי שלך או לכל מקום אחר (אופציונאלי)" }, "errors": { @@ -345,44 +362,44 @@ "website": "הזינו כתובת URL תקינה" }, "noteDescription": { - "pre": "להציע את הספריה שלך להיות כלולה ב", - "link": "מאגר הספריה הציבורי", - "post": "כך שאחרים יוכלו לעשות שימוש בציורים שלהם." + "pre": "הגש את הספריה שלך להכללתה ב ", + "link": "מאגר הספריה הציבורית", + "post": "לשימושם של אנשים אחרים בציורים שלהם." }, "noteGuidelines": { - "pre": "הספריה צריכה לקבל אישור ידני. אנא קרא את ", + "pre": "הספריה צריכה לקבל אישור ידני קודם לכן. אנא קרא את ", "link": "הנחיות", - "post": "" + "post": " לפני השליחה. אתה תצטרך לחשבון GitHub כדי לתקשר ולבצע שינויים אם תתבקש, אבל זה לא דרישה הכרחית." }, "noteLicense": { - "pre": "", + "pre": "על ידי שליחה, אתה מסכים שהסיפריה תפורסם תחת ה- ", "link": "רישיון MIT, ", - "post": "שאומר בקצרה שכל אחד יכול לעשות בהם שימוש ללא מגבלות." + "post": "שאומר בקצרה, שכל אחד יכול לעשות בהם שימוש ללא מגבלות." }, - "noteItems": "", - "atleastOneLibItem": "", - "republishWarning": "" + "noteItems": "לכל פריט בסיפריה חייב להיות שם כדי שאפשר יהיה לסנן. הפריטי סיפריה הבאים יהיו כלולים:", + "atleastOneLibItem": "אנא בחר לפחות פריט אחד מספריה כדי להתחיל", + "republishWarning": "הערה: חלק מהפריטים שבחרת מסומנים ככאלו שכבר פורסמו/נשלחו. אתה צריך לשלוח פריטים מחדש כאשר אתה מעדכן ספריה או הגשה קיימים." }, "publishSuccessDialog": { - "title": "הספריה נשלחה", + "title": "הספריה הוגשה", "content": "תודה {{authorName}}. הספריה שלך נשלחה לבחינה. תוכל לעקוב אחרי סטטוס הפרסום", "link": "כאן" }, "confirmDialog": { "resetLibrary": "איפוס ספריה", - "removeItemsFromLib": "להסיר את הפריטים הנבחרים מהספריה" + "removeItemsFromLib": "הסר את הפריטים הנבחרים מהספריה" }, "encrypted": { - "tooltip": "הרישומים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם.", + "tooltip": "הציורים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם.", "link": "פוסט בבלוג על הצפנה מקצה לקצב ב-Excalidraw" }, "stats": { "angle": "זווית", - "element": "אלמנט", - "elements": "אלמנטים", + "element": "רכיב", + "elements": "רכיבים", "height": "גובה", "scene": "תצוגה", - "selected": "נבחר/ים", + "selected": "נבחר", "storage": "אחסון", "title": "סטטיסטיקות לחנונים", "total": "סה״כ", @@ -393,14 +410,14 @@ }, "toast": { "addedToLibrary": "נוסף לספריה", - "copyStyles": "העתק סגנונות.", - "copyToClipboard": "הועתק אל הלוח.", + "copyStyles": "סגנונות הועתקו.", + "copyToClipboard": "הועתק ללוח.", "copyToClipboardAsPng": "{{exportSelection}} הועתקה ללוח כ-PNG\n({{exportColorScheme}})", "fileSaved": "קובץ נשמר.", "fileSavedToFilename": "נשמר לקובץ {filename}", - "canvas": "משטח ציור", + "canvas": "קנבאס", "selection": "בחירה", - "pasteAsSingleElement": "" + "pasteAsSingleElement": "השתמש ב- {{shortcut}} כדי להדביק כפריט יחיד,\nאו הדבק לתוך עורך טקסט קיים" }, "colors": { "ffffff": "לבן", @@ -443,23 +460,23 @@ "364fc7": "כחול כהה 9", "1864ab": "כחול 9", "0b7285": "טורקיז 9", - "087f5b": "ירקרק 9", + "087f5b": "ירוק-כחול 9", "2b8a3e": "ירוק 9", "5c940d": "ליים 9", - "e67700": "ירוק 9", + "e67700": "צהוב 9", "d9480f": "כתום 9" }, "welcomeScreen": { "app": { - "center_heading": "", - "center_heading_plus": "", - "menuHint": "" + "center_heading": "כל המידע שלח נשמר מקומית בדפדפן.", + "center_heading_plus": "אתה רוצה ללכת אל Excalidraw+ במקום?", + "menuHint": "ייצוא, העדפות, שפות, ..." }, "defaults": { - "menuHint": "", - "center_heading": "", - "toolbarHint": "", - "helpHint": "" + "menuHint": "ייצוא, העדפות, ועוד...", + "center_heading": "איורים. נעשים. פשוטים.", + "toolbarHint": "בחר כלי & והתחל לצייר!", + "helpHint": "קיצורים & עזרה" } } } diff --git a/src/locales/hi-IN.json b/src/locales/hi-IN.json index 9e3a14d64..77d6dae2d 100644 --- a/src/locales/hi-IN.json +++ b/src/locales/hi-IN.json @@ -110,6 +110,7 @@ "increaseFontSize": "फ़ॉन्ट आकार बढ़ाएँ", "unbindText": "", "bindText": "लेखन को कोश से जोड़े", + "createContainerFromText": "मूलपाठ कंटेनर में मोड के दिखाए", "link": { "edit": "", "create": "", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "कॉलेब सर्वर से कनेक्शन नहीं हो पा रहा. कृपया पृष्ठ को पुनः लाने का प्रयास करे.", "importLibraryError": "संग्रह प्रतिष्ठापित नहीं किया जा सका", "collabSaveFailed": "किसी कारण वश अंदरूनी डेटाबेस में सहेजा नहीं जा सका। यदि समस्या बनी रहती है, तो किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।", - "collabSaveFailed_sizeExceeded": "लगता है कि पृष्ठ तल काफ़ी बड़ा है, इस्कारण अंदरूनी डेटाबेस में सहेजा नहीं जा सका। किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।" + "collabSaveFailed_sizeExceeded": "लगता है कि पृष्ठ तल काफ़ी बड़ा है, इस्कारण अंदरूनी डेटाबेस में सहेजा नहीं जा सका। किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।", + "brave_measure_text_error": { + "start": "ऐसा लगता है कि आप ब्रेव ब्राउज़र का उपयोग कर रहे है, उसके साथ", + "aggressive_block_fingerprint": "उग्रतापूर्वक उंगलियों के निशान रोकने की", + "setting_enabled": "सेटिंग सक्रिय की गई है।", + "break": "इसके परिणाम स्वरूम टूट सकता है यह", + "text_elements": "मूल पाठ अवयव टूट सकते हैं", + "in_your_drawings": "आपके चित्र में उपस्थित", + "strongly_recommend": "हमारा शशक्त अनुरोध इस सेटिंग्स को अक्षम करने का है. आप", + "steps": "इन दिए हुए स्टेप्स को अनुकरीत करके देख सकते है कि", + "how": "ये कैसे करे", + "disable_setting": " यदि इन सेटिंग्स को निष्क्रिय करने पर भी, पाठ्य दिखना ठीक न हो तो", + "issue": "समस्या", + "write": "हमारे \"GitHub\" पर, अथवा", + "discord": "\"Discord\" पर लिख सकते हैं." + } }, "toolBar": { "selection": "चयन", @@ -303,7 +319,8 @@ "doubleClick": "", "drag": "खींचें", "editor": "संपादक", - "editSelectedShape": "", + "editLineArrowPoints": "रेखा/तीर बिंदु सम्पादित करे", + "editText": "पाठ्य सम्पादित करे/ लेबल जोड़े", "github": "मुद्दा मिला? प्रस्तुत करें", "howto": "हमारे गाइड का पालन करें", "or": "या", diff --git a/src/locales/hu-HU.json b/src/locales/hu-HU.json index a3cbd852c..d514520ed 100644 --- a/src/locales/hu-HU.json +++ b/src/locales/hu-HU.json @@ -110,6 +110,7 @@ "increaseFontSize": "Betűméret növelése", "unbindText": "Szövegkötés feloldása", "bindText": "", + "createContainerFromText": "", "link": { "edit": "Hivatkozás szerkesztése", "create": "Hivatkozás létrehozása", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "", "importLibraryError": "", "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "Kijelölés", @@ -303,7 +319,8 @@ "doubleClick": "dupla kattintás", "drag": "vonszolás", "editor": "Szerkesztő", - "editSelectedShape": "Kijelölt alakzat szerkesztése (szöveg/nyíl/vonal)", + "editLineArrowPoints": "", + "editText": "", "github": "Hibát találtál? Küld be", "howto": "Kövesd az útmutatóinkat", "or": "vagy", diff --git a/src/locales/id-ID.json b/src/locales/id-ID.json index cd722c6a3..01b510fcd 100644 --- a/src/locales/id-ID.json +++ b/src/locales/id-ID.json @@ -110,6 +110,7 @@ "increaseFontSize": "Besarkan ukuran font", "unbindText": "Lepas teks", "bindText": "Kunci teks ke kontainer", + "createContainerFromText": "", "link": { "edit": "Edit tautan", "create": "Buat tautan", @@ -193,7 +194,7 @@ "resetLibrary": "Ini akan menghapus pustaka Anda. Anda yakin?", "removeItemsFromsLibrary": "Hapus {{count}} item dari pustaka?", "invalidEncryptionKey": "Sandi enkripsi harus 22 karakter. Kolaborasi langsung dinonaktifkan.", - "collabOfflineWarning": "" + "collabOfflineWarning": "Tidak ada koneksi internet.\nPerubahan tidak akan disimpan!" }, "errors": { "unsupportedFileType": "Tipe file tidak didukung.", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Tidak dapat terhubung ke server kolab. Muat ulang laman dan coba lagi.", "importLibraryError": "Tidak dapat memuat pustaka", "collabSaveFailed": "Tidak dapat menyimpan ke dalam basis data server. Jika masih berlanjut, Anda sebaiknya simpan berkas Anda secara lokal untuk memastikan pekerjaan Anda tidak hilang.", - "collabSaveFailed_sizeExceeded": "Tidak dapat menyimpan ke dalam basis data server, tampaknya ukuran kanvas terlalu besar. Anda sebaiknya simpan berkas Anda secara lokal untuk memastikan pekerjaan Anda tidak hilang." + "collabSaveFailed_sizeExceeded": "Tidak dapat menyimpan ke dalam basis data server, tampaknya ukuran kanvas terlalu besar. Anda sebaiknya simpan berkas Anda secara lokal untuk memastikan pekerjaan Anda tidak hilang.", + "brave_measure_text_error": { + "start": "Sepertinya kamu menggunakan browser Brave dengan", + "aggressive_block_fingerprint": "", + "setting_enabled": "pengaturan diaktifkan", + "break": "", + "text_elements": "Elemen Teks", + "in_your_drawings": "dalam gambar anda", + "strongly_recommend": "Kami sangat menyarankan untuk mematikan pengaturan ini. Kamu dapat mengikuti", + "steps": "langkah-langkah ini", + "how": "dalam bagaimana melakukan itu", + "disable_setting": " Jika pengaturan ini dimatikan tidak mengatasi masalah tampilan dari elemen teks, silahkan buka", + "issue": "isu", + "write": "di GitHub kami, atau tulis kami di", + "discord": "Discord" + } }, "toolBar": { "selection": "Pilihan", @@ -303,7 +319,8 @@ "doubleClick": "klik-ganda", "drag": "seret", "editor": "Editor", - "editSelectedShape": "Edit bentuk yang dipilih (teks/panah/garis)", + "editLineArrowPoints": "", + "editText": "", "github": "Menemukan masalah? Kirimkan", "howto": "Ikuti panduan kami", "or": "atau", diff --git a/src/locales/it-IT.json b/src/locales/it-IT.json index 3c6cf93a8..8380fd8e5 100644 --- a/src/locales/it-IT.json +++ b/src/locales/it-IT.json @@ -110,6 +110,7 @@ "increaseFontSize": "Aumenta la dimensione dei caratteri", "unbindText": "Scollega testo", "bindText": "Associa il testo al container", + "createContainerFromText": "", "link": { "edit": "Modifica link", "create": "Crea link", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Impossibile connettersi al server di collab. Ricarica la pagina e riprova.", "importLibraryError": "Impossibile caricare la libreria", "collabSaveFailed": "Impossibile salvare nel database di backend. Se i problemi persistono, dovresti salvare il tuo file localmente per assicurarti di non perdere il tuo lavoro.", - "collabSaveFailed_sizeExceeded": "Impossibile salvare nel database di backend, la tela sembra essere troppo grande. Dovresti salvare il file localmente per assicurarti di non perdere il tuo lavoro." + "collabSaveFailed_sizeExceeded": "Impossibile salvare nel database di backend, la tela sembra essere troppo grande. Dovresti salvare il file localmente per assicurarti di non perdere il tuo lavoro.", + "brave_measure_text_error": { + "start": "Sembra che tu stia usando il browser Brave con il", + "aggressive_block_fingerprint": "Blocco Aggressivamente Impronte Digitali", + "setting_enabled": "impostazioni abilitate", + "break": "Questo potrebbe portare a rompere gli", + "text_elements": "Elementi Di Testo", + "in_your_drawings": "nei tuoi disegni", + "strongly_recommend": "Si consiglia vivamente di disabilitare questa impostazione. È possibile seguire", + "steps": "questi passaggi", + "how": "su come fare", + "disable_setting": " Se la disabilitazione di questa impostazione non corregge la visualizzazione degli elementi di testo, apri un", + "issue": "problema", + "write": "sul nostro GitHub, o scrivici su", + "discord": "Discord" + } }, "toolBar": { "selection": "Selezione", @@ -303,7 +319,8 @@ "doubleClick": "doppio-click", "drag": "trascina", "editor": "Editor", - "editSelectedShape": "Modifica la forma selezionata (testo/freccia/linea)", + "editLineArrowPoints": "", + "editText": "Modifica testo / aggiungi etichetta", "github": "Trovato un problema? Segnalalo", "howto": "Segui le nostre guide", "or": "oppure", diff --git a/src/locales/ja-JP.json b/src/locales/ja-JP.json index 4f7a76afe..53333aea3 100644 --- a/src/locales/ja-JP.json +++ b/src/locales/ja-JP.json @@ -110,6 +110,7 @@ "increaseFontSize": "フォントサイズを拡大", "unbindText": "テキストのバインド解除", "bindText": "テキストをコンテナにバインド", + "createContainerFromText": "", "link": { "edit": "リンクを編集", "create": "リンクを作成", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "コラボレーションサーバに接続できませんでした。ページを再読み込みして、もう一度お試しください。", "importLibraryError": "ライブラリを読み込めませんでした。", "collabSaveFailed": "バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。", - "collabSaveFailed_sizeExceeded": "キャンバスが大きすぎるため、バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。" + "collabSaveFailed_sizeExceeded": "キャンバスが大きすぎるため、バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "設定が有効化されました", + "break": "", + "text_elements": "テキスト要素", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "Discord" + } }, "toolBar": { "selection": "選択", @@ -303,7 +319,8 @@ "doubleClick": "ダブルクリック", "drag": "ドラッグ", "editor": "エディタ", - "editSelectedShape": "選択した図形の編集 (テキスト/矢印/線)", + "editLineArrowPoints": "", + "editText": "テキストの編集 / ラベルの追加", "github": "不具合報告はこちら", "howto": "ヘルプ・マニュアル", "or": "または", diff --git a/src/locales/kab-KAB.json b/src/locales/kab-KAB.json index 3603e88c7..ba6a3de7e 100644 --- a/src/locales/kab-KAB.json +++ b/src/locales/kab-KAB.json @@ -110,6 +110,7 @@ "increaseFontSize": "Sali tiddi n tsefsit", "unbindText": "Serreḥ iweḍris", "bindText": "Arez aḍris s anagbar", + "createContainerFromText": "", "link": { "edit": "Ẓreg aseɣwen", "create": "Snulfu-d aseɣwen", @@ -193,7 +194,7 @@ "resetLibrary": "Ayagi ad isfeḍ tamkarḍit-inek•m. Tetḥeqqeḍ?", "removeItemsFromsLibrary": "Ad tekkseḍ {{count}} n uferdis (en) si temkarḍit?", "invalidEncryptionKey": "Tasarut n uwgelhen isefk ad tesɛu 22 n yiekkilen. Amɛiwen srid yensa.", - "collabOfflineWarning": "" + "collabOfflineWarning": "Ulac tuqqna n internet.\nIbedilen-ik ur ttwaklasen ara!" }, "errors": { "unsupportedFileType": "Anaw n ufaylu ur yettwasefrak ara.", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Ulamek tuqqna s aqeddac n umyalel. Ma ulac uɣilif ales asali n usebter sakin eɛreḍ tikkelt-nniḍen.", "importLibraryError": "Ur d-ssalay ara tamkarḍit", "collabSaveFailed": "Ulamek asekles deg uzadur n yisefka deg ugilal. Ma ikemmel wugur, isefk ad teskelseḍ afaylu s wudem adigan akken ad tetḥeqqeḍ ur tesruḥuyeḍ ara amahil-inek•inem.", - "collabSaveFailed_sizeExceeded": "Ulamek asekles deg uzadur n yisefka deg ugilal, taɣzut n usuneɣ tettban-d temqer aṭas. Isefk ad teskelseḍ afaylu s wudem adigan akken ad tetḥeqqeḍ ur tesruḥuyeḍ ara amahil-inek•inem." + "collabSaveFailed_sizeExceeded": "Ulamek asekles deg uzadur n yisefka deg ugilal, taɣzut n usuneɣ tettban-d temqer aṭas. Isefk ad teskelseḍ afaylu s wudem adigan akken ad tetḥeqqeḍ ur tesruḥuyeḍ ara amahil-inek•inem.", + "brave_measure_text_error": { + "start": "Ittban-d am wakken tsseqdaceḍ iminig Brave akked", + "aggressive_block_fingerprint": "", + "setting_enabled": "yermed", + "break": "Ayagi yezmer ad d-iseglu s truẓi n", + "text_elements": "Iferdisen iḍrisen", + "in_your_drawings": "deg wunuɣen-inek", + "strongly_recommend": "", + "steps": "isurifen-agi", + "how": "", + "disable_setting": "", + "issue": "", + "write": "di GitHub inek neɣ aru-yaɣ-d di", + "discord": "" + } }, "toolBar": { "selection": "Tafrayt", @@ -229,7 +245,7 @@ "shapes": "Talɣiwin" }, "hints": { - "canvasPanning": "", + "canvasPanning": "Akken ad tesmuttiḍ taɣzut n usuneɣ, ṭṭef ṛṛuda n umumed, neɣ seqdec afecku Afus", "linearElement": "Ssit akken ad tebduḍ aṭas n tenqiḍin, zuɣer i yiwen n yizirig", "freeDraw": "Ssit yerna zuɣer, serreḥ ticki tfukeḍ", "text": "Tixidest: tzemreḍ daɣen ad ternuḍ aḍris s usiti snat n tikkal anida tebɣiḍ s ufecku n tefrayt", @@ -240,7 +256,7 @@ "resize": "Tzemreḍ ad tḥettemeḍ assaɣ s tuṭṭfa n tqeffalt SHIFT mi ara tettbeddileḍ tiddi,\nma teṭṭfeḍ ALT abeddel n tiddi ad yili si tlemmast", "resizeImage": "Tzemreḍ ad talseḍ tiddi s tilelli s tuṭṭfa n SHIFT,\nṭṭef ALT akken ad talseḍ tiddi si tlemmast", "rotate": "Tzemreḍ ad tḥettemeḍ tiɣemmar s tuṭṭfa n SHIFT di tuzzya", - "lineEditor_info": "", + "lineEditor_info": "Ssed ɣef CtrlOrCmd yerna ssit snat n tikkal neɣ ssed ɣef CtrlOrCmd + Kcem akken ad tẓergeḍ tineqqiḍin", "lineEditor_pointSelected": "Ssed taqeffalt kkes akken ad tekkseḍ tanqiḍ (tinqiḍin),\nCtrlOrCmd+D akken ad tsiselgeḍ, neɣ zuɣer akken ad tesmuttiḍ", "lineEditor_nothingSelected": "Fren tanqiḍt akken ad tẓergeḍ (ṭṭef SHIFT akken ad tferneḍ aṭas),\nneɣ ṭṭef Alt akken ad ternuḍ tinqiḍin timaynutin", "placeImage": "Ssit akken ad tserseḍ tugna, neɣ ssit u zuɣer akken ad tesbaduḍ tiddi-ines s ufus", @@ -303,7 +319,8 @@ "doubleClick": "ssit snat n tikkal", "drag": "zuɣer", "editor": "Amaẓrag", - "editSelectedShape": "Ẓreg talɣa yettwafernen (aḍris/taneccabt/izirig)", + "editLineArrowPoints": "", + "editText": "", "github": "Tufiḍ-d ugur? Azen-aɣ-d", "howto": "Ḍfer imniren-nneɣ", "or": "neɣ", @@ -452,11 +469,11 @@ "welcomeScreen": { "app": { "center_heading": "", - "center_heading_plus": "", - "menuHint": "" + "center_heading_plus": "Tebɣiḍ ad tedduḍ ɣer Excalidraw+ deg umḍiq?", + "menuHint": "Asifeḍ, ismenyifen, tutlayin, ..." }, "defaults": { - "menuHint": "", + "menuHint": "Asifeḍ, ismenyifen, d wayen-nniḍen...", "center_heading": "", "toolbarHint": "", "helpHint": "" diff --git a/src/locales/kk-KZ.json b/src/locales/kk-KZ.json index ee1e44083..97a9063fa 100644 --- a/src/locales/kk-KZ.json +++ b/src/locales/kk-KZ.json @@ -110,6 +110,7 @@ "increaseFontSize": "", "unbindText": "", "bindText": "", + "createContainerFromText": "", "link": { "edit": "", "create": "", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "", "importLibraryError": "", "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "", @@ -303,7 +319,8 @@ "doubleClick": "қос шерту", "drag": "апару", "editor": "Өңдеу", - "editSelectedShape": "Таңдалған пішінді өңдеу (мәтін/нұсқар/сызық)", + "editLineArrowPoints": "", + "editText": "", "github": "Қате таптыңыз ба? Жолдаңыз", "howto": "Біздің нұсқаулықтарды орындаңыз", "or": "немесе", diff --git a/src/locales/ko-KR.json b/src/locales/ko-KR.json index 9c02b3ea1..f170e4cbf 100644 --- a/src/locales/ko-KR.json +++ b/src/locales/ko-KR.json @@ -110,6 +110,7 @@ "increaseFontSize": "폰트 사이즈 키우기", "unbindText": "텍스트 분리", "bindText": "텍스트를 컨테이너에 결합", + "createContainerFromText": "텍스트를 컨테이너에 담기", "link": { "edit": "링크 수정하기", "create": "링크 만들기", @@ -193,7 +194,7 @@ "resetLibrary": "당신의 라이브러리를 초기화 합니다. 계속하시겠습니까?", "removeItemsFromsLibrary": "{{count}}개의 아이템을 라이브러리에서 삭제하시겠습니까?", "invalidEncryptionKey": "암호화 키는 반드시 22글자여야 합니다. 실시간 협업이 비활성화됩니다.", - "collabOfflineWarning": "" + "collabOfflineWarning": "인터넷에 연결되어 있지 않습니다.\n변경 사항들이 저장되지 않습니다!" }, "errors": { "unsupportedFileType": "지원하지 않는 파일 형식 입니다.", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "협업 서버에 접속하는데 실패했습니다. 페이지를 새로고침하고 다시 시도해보세요.", "importLibraryError": "라이브러리를 불러오지 못했습니다.", "collabSaveFailed": "데이터베이스에 저장하지 못했습니다. 문제가 계속 된다면, 작업 내용을 잃지 않도록 로컬 저장소에 저장해 주세요.", - "collabSaveFailed_sizeExceeded": "데이터베이스에 저장하지 못했습니다. 캔버스가 너무 큰 거 같습니다. 문제가 계속 된다면, 작업 내용을 잃지 않도록 로컬 저장소에 저장해 주세요." + "collabSaveFailed_sizeExceeded": "데이터베이스에 저장하지 못했습니다. 캔버스가 너무 큰 거 같습니다. 문제가 계속 된다면, 작업 내용을 잃지 않도록 로컬 저장소에 저장해 주세요.", + "brave_measure_text_error": { + "start": "귀하께서는", + "aggressive_block_fingerprint": "강력한 지문 차단", + "setting_enabled": "설정이 활성화된 Brave browser를 사용하고 계신 것 같습니다", + "break": "이 기능으로 인해 화이트보드의", + "text_elements": "텍스트 요소들이", + "in_your_drawings": "손상될 수 있습니다", + "strongly_recommend": "해당 기능을 설정에서 비활성화하는 것을 강력히 권장 드립니다. 비활성화 방법에 대해서는", + "steps": "이 게시글을", + "how": "참고해주세요.", + "disable_setting": " 만약 이 설정을 껐음에도 텍스트 요소들이 올바르게 표시되지 않는다면, 저희", + "issue": "Github에 이슈를", + "write": "올려주시거나", + "discord": "Discord에 제보해주세요" + } }, "toolBar": { "selection": "선택", @@ -221,7 +237,7 @@ "penMode": "펜 모드 - 터치 방지", "link": "선택한 도형에 대해서 링크를 추가/업데이트", "eraser": "지우개", - "hand": "" + "hand": "손 (패닝 도구)" }, "headings": { "canvasActions": "캔버스 동작", @@ -229,7 +245,7 @@ "shapes": "모양" }, "hints": { - "canvasPanning": "", + "canvasPanning": "캔버스를 옮기려면 마우스 휠이나 스페이스바를 누르고 드래그하거나, 손 도구를 사용하기", "linearElement": "여러 점을 연결하려면 클릭하고, 직선을 그리려면 바로 드래그하세요.", "freeDraw": "클릭 후 드래그하세요. 완료되면 놓으세요.", "text": "팁: 선택 툴로 아무 곳이나 더블 클릭해 텍스트를 추가할 수도 있습니다.", @@ -248,7 +264,7 @@ "bindTextToElement": "Enter 키를 눌러서 텍스트 추가하기", "deepBoxSelect": "CtrlOrCmd 키를 눌러서 깊게 선택하고, 드래그하지 않도록 하기", "eraserRevert": "Alt를 눌러서 삭제하도록 지정된 요소를 되돌리기", - "firefox_clipboard_write": "" + "firefox_clipboard_write": "이 기능은 설정에서 \"dom.events.asyncClipboard.clipboardItem\" 플래그를 \"true\"로 설정하여 활성화할 수 있습니다. Firefox에서 브라우저 플래그를 수정하려면, \"about:config\" 페이지에 접속하세요." }, "canvasError": { "cannotShowPreview": "미리보기를 볼 수 없습니다", @@ -303,7 +319,8 @@ "doubleClick": "더블 클릭", "drag": "드래그", "editor": "에디터", - "editSelectedShape": "선택한 도형 편집하기(텍스트/화살표/라인)", + "editLineArrowPoints": "", + "editText": "", "github": "문제 제보하기", "howto": "가이드 참고하기", "or": "또는", @@ -451,14 +468,14 @@ }, "welcomeScreen": { "app": { - "center_heading": "당신의 모든 데이터는 브라우저에 저장되었습니다.", + "center_heading": "모든 데이터는 브라우저에 안전하게 저장됩니다.", "center_heading_plus": "대신 Excalidraw+로 이동하시겠습니까?", "menuHint": "내보내기, 설정, 언어, ..." }, "defaults": { - "menuHint": "내보내기, 설정, 더 보기...", - "center_heading": "", - "toolbarHint": "도구 선택 & 그리기 시작", + "menuHint": "내보내기, 설정, 등등...", + "center_heading": "간단하게 만드는 다이어그램.", + "toolbarHint": "도구를 선택하고, 그리세요!", "helpHint": "단축키 & 도움말" } } diff --git a/src/locales/ku-TR.json b/src/locales/ku-TR.json index 4a8ebc95c..76b5086e8 100644 --- a/src/locales/ku-TR.json +++ b/src/locales/ku-TR.json @@ -110,6 +110,7 @@ "increaseFontSize": "زایدکردنی قەبارەی فۆنت", "unbindText": "دەقەکە جیابکەرەوە", "bindText": "دەقەکە ببەستەوە بە کۆنتەینەرەکەوە", + "createContainerFromText": "", "link": { "edit": "دەستکاریکردنی بەستەر", "create": "دروستکردنی بەستەر", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "ناتوانێت پەیوەندی بکات بە سێرڤەری کۆلاب. تکایە لاپەڕەکە دووبارە باربکەوە و دووبارە هەوڵ بدەوە.", "importLibraryError": "نەیتوانی کتێبخانە بار بکات", "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "دەستنیشانکردن", @@ -303,7 +319,8 @@ "doubleClick": "دوو گرتە", "drag": "راکێشان", "editor": "دەستکاریکەر", - "editSelectedShape": "دەستکاریکردنی شێوەی هەڵبژێردراو (دەق/تیر/هێڵ)", + "editLineArrowPoints": "", + "editText": "", "github": "کێشەیەکت دۆزیەوە؟ پێشکەشکردن", "howto": "شوێن ڕینماییەکانمان بکەوە", "or": "یان", diff --git a/src/locales/lt-LT.json b/src/locales/lt-LT.json index a8a8d6639..d80739f09 100644 --- a/src/locales/lt-LT.json +++ b/src/locales/lt-LT.json @@ -110,6 +110,7 @@ "increaseFontSize": "", "unbindText": "", "bindText": "", + "createContainerFromText": "", "link": { "edit": "Redeguoti nuorodą", "create": "Sukurti nuorodą", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Nepavyko prisijungti prie serverio bendradarbiavimui. Perkrauk puslapį ir pabandyk prisijungti dar kartą.", "importLibraryError": "Nepavyko įkelti bibliotekos", "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "Žymėjimas", @@ -303,7 +319,8 @@ "doubleClick": "dvigubas paspaudimas", "drag": "vilkti", "editor": "Redaktorius", - "editSelectedShape": "", + "editLineArrowPoints": "", + "editText": "", "github": "Radai klaidą? Pateik", "howto": "Vadovaukis mūsų gidu", "or": "arba", diff --git a/src/locales/lv-LV.json b/src/locales/lv-LV.json index ea3180149..4a311f7cd 100644 --- a/src/locales/lv-LV.json +++ b/src/locales/lv-LV.json @@ -110,6 +110,7 @@ "increaseFontSize": "Palielināt fonta izmēru", "unbindText": "Atdalīt tekstu", "bindText": "Piesaistīt tekstu figūrai", + "createContainerFromText": "", "link": { "edit": "Rediģēt saiti", "create": "Izveidot saiti", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Nevarēja savienoties ar sadarbošanās serveri. Lūdzu, pārlādējiet lapu un mēģiniet vēlreiz.", "importLibraryError": "Nevarēja ielādēt bibliotēku", "collabSaveFailed": "Darbs nav saglabāts datubāzē. Ja problēma turpinās, saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.", - "collabSaveFailed_sizeExceeded": "Darbs nav saglabāts datubāzē, šķiet, ka tāfele ir pārāk liela. Saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu." + "collabSaveFailed_sizeExceeded": "Darbs nav saglabāts datubāzē, šķiet, ka tāfele ir pārāk liela. Saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "Atlase", @@ -303,7 +319,8 @@ "doubleClick": "dubultklikšķis", "drag": "vilkt", "editor": "Redaktors", - "editSelectedShape": "Rediģēt atlasīto figūru (tekstu/bultu/līniju)", + "editLineArrowPoints": "", + "editText": "", "github": "Sastapāt kļūdu? Ziņot", "howto": "Sekojiet mūsu instrukcijām", "or": "vai", diff --git a/src/locales/mr-IN.json b/src/locales/mr-IN.json index e67fc3159..4f58009c9 100644 --- a/src/locales/mr-IN.json +++ b/src/locales/mr-IN.json @@ -110,6 +110,7 @@ "increaseFontSize": "अक्षर आकार मोठा करा", "unbindText": "लेखन संबंध संपवा", "bindText": "शब्द समूह ला पात्रात घ्या", + "createContainerFromText": "मजकूर कंटेनर मधे मोडून दाखवा", "link": { "edit": "दुवा संपादन", "create": "दुवा तयार करा", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "कॉलेब-सर्वर हे पोहोचत नाही आहे. पान परत लोड करायचा प्रयत्न करावे.", "importLibraryError": "संग्रह प्रतिस्थापित नाही करता आला", "collabSaveFailed": "काही कारणा निमित्त आतल्या डेटाबेसमध्ये जतन करू शकत नाही। समस्या तशिस राहिल्यास, तुम्ही तुमचे काम गमावणार नाही याची खात्री करण्यासाठी तुम्ही तुमची फाइल स्थानिक जतन करावी.", - "collabSaveFailed_sizeExceeded": "लगता है कि पृष्ठ तल काफ़ी बड़ा है, इस्कारण अंदरूनी डेटाबेस में सहेजा नहीं जा सका। किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।\n\nबॅकएंड डेटाबेसमध्ये जतन करू शकत नाही, कॅनव्हास खूप मोठा असल्याचे दिसते. तुम्ही तुमचे काम गमावणार नाही याची खात्री करण्यासाठी तुम्ही फाइल स्थानिक पातळीवर जतन करावी." + "collabSaveFailed_sizeExceeded": "लगता है कि पृष्ठ तल काफ़ी बड़ा है, इस्कारण अंदरूनी डेटाबेस में सहेजा नहीं जा सका। किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।\n\nबॅकएंड डेटाबेसमध्ये जतन करू शकत नाही, कॅनव्हास खूप मोठा असल्याचे दिसते. तुम्ही तुमचे काम गमावणार नाही याची खात्री करण्यासाठी तुम्ही फाइल स्थानिक पातळीवर जतन करावी.", + "brave_measure_text_error": { + "start": "असं वाटते की तुम्हीं \"Brave\" \"Browser\" वापरत आहात, त्या बरोबार", + "aggressive_block_fingerprint": "बोटांचे ठसे उग्रतेने थाम्बवाचे", + "setting_enabled": "सेटिंग्स सक्रिय केले आहेत", + "break": "ह्या कारणानिं", + "text_elements": "पाठ अवयव तुटु शकतात", + "in_your_drawings": "तुमच्या चित्रिकराणतले", + "strongly_recommend": "आमचा ज़ोरदार सल्ला असा कि सेटिंग्स निष्क्रिय करावे. तुम्हीं", + "steps": "ह्या स्टेप्स", + "how": "घेउ शकतात", + "disable_setting": " जर सेटिंग्स निष्क्रिय करून पाठ्य दिसणे ठीक नसेल होत तर", + "issue": "मुद्दा", + "write": "आमच्या Github वर, किव्हा आम्हाला", + "discord": "\"Discord\" वर लिहां" + } }, "toolBar": { "selection": "निवड", @@ -303,7 +319,8 @@ "doubleClick": "दुहेरी क्लिक", "drag": "ओढा", "editor": "संपादक", - "editSelectedShape": "निवडलेला प्रकार संपादित करा (मजकूर/बाण/रेघ)", + "editLineArrowPoints": "रेघ/तीर बिंदु सम्पादित करा", + "editText": "पाठ्य सम्पादित करा/ लेबल जोडा", "github": "समस्या मिळाली? प्रस्तुत करा", "howto": "आमच्या मार्गदर्शकाचे अनुसरण करा", "or": "किंवा", diff --git a/src/locales/my-MM.json b/src/locales/my-MM.json index e479d3702..efc874218 100644 --- a/src/locales/my-MM.json +++ b/src/locales/my-MM.json @@ -110,6 +110,7 @@ "increaseFontSize": "", "unbindText": "", "bindText": "", + "createContainerFromText": "", "link": { "edit": "", "create": "", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "", "importLibraryError": "", "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "ရွေးချယ်", @@ -303,7 +319,8 @@ "doubleClick": "", "drag": "", "editor": "", - "editSelectedShape": "", + "editLineArrowPoints": "", + "editText": "", "github": "", "howto": "", "or": "", diff --git a/src/locales/nb-NO.json b/src/locales/nb-NO.json index 653779a56..27e717d2a 100644 --- a/src/locales/nb-NO.json +++ b/src/locales/nb-NO.json @@ -110,6 +110,7 @@ "increaseFontSize": "Øk skriftstørrelse", "unbindText": "Avbind tekst", "bindText": "Bind tekst til beholderen", + "createContainerFromText": "La tekst flyte i en beholder", "link": { "edit": "Rediger lenke", "create": "Opprett lenke", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Kunne ikke koble til samarbeidsserveren. Vennligst oppdater siden og prøv på nytt.", "importLibraryError": "Kunne ikke laste bibliotek", "collabSaveFailed": "Kan ikke lagre i backend-databasen. Hvis problemer vedvarer, bør du lagre filen lokalt for å sikre at du ikke mister arbeidet.", - "collabSaveFailed_sizeExceeded": "Kunne ikke lagre til backend-databasen, lerretet ser ut til å være for stort. Du bør lagre filen lokalt for å sikre at du ikke mister arbeidet ditt." + "collabSaveFailed_sizeExceeded": "Kunne ikke lagre til backend-databasen, lerretet ser ut til å være for stort. Du bør lagre filen lokalt for å sikre at du ikke mister arbeidet ditt.", + "brave_measure_text_error": { + "start": "Ser ut som du bruker Brave nettleser med", + "aggressive_block_fingerprint": "Blokker fingeravtrykk aggressivt", + "setting_enabled": "innstilling aktivert", + "break": "Dette kan føre til at den bryter", + "text_elements": "Tekstelementer", + "in_your_drawings": "i tegningene dine", + "strongly_recommend": "Vi anbefaler på det sterkeste å deaktivere denne innstillingen. Du kan følge dette", + "steps": "disse trinnene", + "how": "om hvordan det skal gjøres", + "disable_setting": " Hvis deaktivering av denne innstillingen ikke fikser visningen av tekstelementer, kan du åpne en", + "issue": "sak", + "write": "på vår GitHub, eller skriv oss på", + "discord": "Discord" + } }, "toolBar": { "selection": "Velg", @@ -303,7 +319,8 @@ "doubleClick": "dobbeltklikk", "drag": "dra", "editor": "Redigeringsvisning", - "editSelectedShape": "Rediger valgt figur (tekst/pil/linje)", + "editLineArrowPoints": "Rediger linje/pilpunkter", + "editText": "Rediger tekst / legg til etikett", "github": "Funnet et problem? Send inn", "howto": "Følg våre veiledninger", "or": "eller", diff --git a/src/locales/nl-NL.json b/src/locales/nl-NL.json index 313c8dc47..7c2bb105b 100644 --- a/src/locales/nl-NL.json +++ b/src/locales/nl-NL.json @@ -110,6 +110,7 @@ "increaseFontSize": "Letters vergroten", "unbindText": "Ontkoppel tekst", "bindText": "Koppel tekst aan de container", + "createContainerFromText": "", "link": { "edit": "Wijzig link", "create": "Maak link", @@ -193,7 +194,7 @@ "resetLibrary": "Dit zal je bibliotheek wissen. Weet je het zeker?", "removeItemsFromsLibrary": "Verwijder {{count}} item(s) uit bibliotheek?", "invalidEncryptionKey": "Encryptiesleutel moet 22 tekens zijn. Live samenwerking is uitgeschakeld.", - "collabOfflineWarning": "" + "collabOfflineWarning": "Geen internetverbinding beschikbaar.\nJe wijzigingen worden niet opgeslagen!" }, "errors": { "unsupportedFileType": "Niet-ondersteund bestandstype.", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Kan geen verbinding maken met de collab server. Herlaad de pagina en probeer het opnieuw.", "importLibraryError": "Kon bibliotheek niet laden", "collabSaveFailed": "Kan niet opslaan in de backend database. Als de problemen blijven bestaan, moet u het bestand lokaal opslaan om ervoor te zorgen dat u uw werk niet verliest.", - "collabSaveFailed_sizeExceeded": "Kan de backend database niet opslaan, het canvas lijkt te groot te zijn. U moet het bestand lokaal opslaan om ervoor te zorgen dat u uw werk niet verliest." + "collabSaveFailed_sizeExceeded": "Kan de backend database niet opslaan, het canvas lijkt te groot te zijn. U moet het bestand lokaal opslaan om ervoor te zorgen dat u uw werk niet verliest.", + "brave_measure_text_error": { + "start": "Het ziet er uit dat u de Brave browser gebruikt met de", + "aggressive_block_fingerprint": "", + "setting_enabled": "instelling ingeschakeld", + "break": "Dit kan leiden tot het breken van de", + "text_elements": "Tekst Elementen", + "in_your_drawings": "in je tekeningen", + "strongly_recommend": "We raden u ten zeerste aan deze instelling uit te schakelen. U kunt dit volgen", + "steps": "deze stappen", + "how": "over hoe dit te doen", + "disable_setting": " Indien het uitschakelen van deze instelling de weergave van tekst elementen niet wijzigt, open dan een", + "issue": "probleem", + "write": "", + "discord": "Discord" + } }, "toolBar": { "selection": "Selectie", @@ -219,7 +235,7 @@ "library": "Bibliotheek", "lock": "Geselecteerde tool actief houden na tekenen", "penMode": "Pen modus - Blokkeer aanraken", - "link": "", + "link": "Link toevoegen / bijwerken voor een geselecteerde vorm", "eraser": "Gum", "hand": "" }, @@ -229,7 +245,7 @@ "shapes": "Vormen" }, "hints": { - "canvasPanning": "", + "canvasPanning": "Om de canvas te verplaatsen, houd muiswiel of spatiebalk ingedrukt tijdens slepen, of gebruik het handgereedschap", "linearElement": "Klik om meerdere punten te starten, sleep voor één lijn", "freeDraw": "Klik en sleep, laat los als je klaar bent", "text": "Tip: je kunt tekst toevoegen door ergens dubbel te klikken met de selectietool", @@ -245,7 +261,7 @@ "lineEditor_nothingSelected": "", "placeImage": "", "publishLibrary": "Publiceer je eigen bibliotheek", - "bindTextToElement": "", + "bindTextToElement": "Druk op enter om tekst toe te voegen", "deepBoxSelect": "", "eraserRevert": "", "firefox_clipboard_write": "" @@ -295,7 +311,7 @@ "helpDialog": { "blog": "Lees onze blog", "click": "klik", - "deepSelect": "", + "deepSelect": "Deep selecteer", "deepBoxSelect": "", "curvedArrow": "Gebogen pijl", "curvedLine": "Kromme lijn", @@ -303,7 +319,8 @@ "doubleClick": "dubbelklikken", "drag": "slepen", "editor": "Editor", - "editSelectedShape": "Bewerk geselecteerde vorm (tekst/pijl/lijn)", + "editLineArrowPoints": "", + "editText": "", "github": "Probleem gevonden? Verzenden", "howto": "Volg onze handleidingen", "or": "of", @@ -364,13 +381,13 @@ "republishWarning": "" }, "publishSuccessDialog": { - "title": "", + "title": "Bibliotheek ingediend", "content": "", "link": "Hier" }, "confirmDialog": { - "resetLibrary": "", - "removeItemsFromLib": "" + "resetLibrary": "Reset bibliotheek", + "removeItemsFromLib": "Verwijder geselecteerde items uit bibliotheek" }, "encrypted": { "tooltip": "Je tekeningen zijn beveiligd met end-to-end encryptie, dus Excalidraw's servers zullen nooit zien wat je tekent.", @@ -392,7 +409,7 @@ "width": "Breedte" }, "toast": { - "addedToLibrary": "", + "addedToLibrary": "Toegevoegd aan bibliotheek", "copyStyles": "Stijlen gekopieerd.", "copyToClipboard": "Gekopieerd naar het klembord.", "copyToClipboardAsPng": "{{exportSelection}} naar klembord gekopieerd als PNG\n({{exportColorScheme}})", @@ -456,10 +473,10 @@ "menuHint": "" }, "defaults": { - "menuHint": "", - "center_heading": "", - "toolbarHint": "", - "helpHint": "" + "menuHint": "Exporteren, voorkeuren en meer...", + "center_heading": "Diagrammen. Eenvoudig. Gemaakt.", + "toolbarHint": "Kies een tool & begin met tekenen!", + "helpHint": "Snelkoppelingen en hulp" } } } diff --git a/src/locales/nn-NO.json b/src/locales/nn-NO.json index 0311cc2fe..1733b84c4 100644 --- a/src/locales/nn-NO.json +++ b/src/locales/nn-NO.json @@ -110,6 +110,7 @@ "increaseFontSize": "Gjer skriftstorleik større", "unbindText": "Avbind tekst", "bindText": "", + "createContainerFromText": "", "link": { "edit": "Rediger lenke", "create": "Lag lenke", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Kunne ikkje kople til samarbeidsserveren. Ver vennleg å oppdatere inn sida og prøv på nytt.", "importLibraryError": "", "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "Vel", @@ -303,7 +319,8 @@ "doubleClick": "dobbelklikk", "drag": "drag", "editor": "Redigering", - "editSelectedShape": "Rediger valt form (tekst/pil/linje)", + "editLineArrowPoints": "", + "editText": "", "github": "Funne eit problem? Send inn", "howto": "Følg vegleiinga vår", "or": "eller", diff --git a/src/locales/oc-FR.json b/src/locales/oc-FR.json index 51a5b9018..a9ca70a84 100644 --- a/src/locales/oc-FR.json +++ b/src/locales/oc-FR.json @@ -110,6 +110,7 @@ "increaseFontSize": "Aumentar talha polissa", "unbindText": "Dessociar lo tèxte", "bindText": "Ligar lo tèxt al contenidor", + "createContainerFromText": "", "link": { "edit": "Modificar lo ligam", "create": "Crear un ligam", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Connexion impossibla al servidor collab. Mercés de recargar la pagina e tornar ensajar.", "importLibraryError": "Impossible de cargar la bibliotèca", "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "Seleccion", @@ -303,7 +319,8 @@ "doubleClick": "doble clic", "drag": "lisar", "editor": "Editor", - "editSelectedShape": "Modificar la fòrma seleccionada (tèxt/sageta/linha)", + "editLineArrowPoints": "", + "editText": "", "github": "Problèma trobat ? Senhalatz-lo", "howto": "Seguissètz nòstras guidas", "or": "o", diff --git a/src/locales/pa-IN.json b/src/locales/pa-IN.json index 3bf61d83a..31c11ceb8 100644 --- a/src/locales/pa-IN.json +++ b/src/locales/pa-IN.json @@ -110,6 +110,7 @@ "increaseFontSize": "ਫੌਂਟ ਦਾ ਅਕਾਰ ਵਧਾਓ", "unbindText": "", "bindText": "ਪਾਠ ਨੂੰ ਕੰਟੇਨਰ ਨਾਲ ਬੰਨ੍ਹੋ", + "createContainerFromText": "", "link": { "edit": "ਕੜੀ ਸੋਧੋ", "create": "ਕੜੀ ਬਣਾਓ", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "", "importLibraryError": "", "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "ਚੋਣਕਾਰ", @@ -303,7 +319,8 @@ "doubleClick": "ਡਬਲ-ਕਲਿੱਕ", "drag": "ਘਸੀਟੋ", "editor": "ਸੋਧਕ", - "editSelectedShape": "ਚੁਣਿਆ ਰੂਪ ਸੋਧੋ (ਪਾਠ/ਤੀਰ/ਲਾਈਨ)", + "editLineArrowPoints": "", + "editText": "", "github": "ਕੋਈ ਸਮੱਸਿਆ ਲੱਭੀ? ਜਮ੍ਹਾਂ ਕਰਵਾਓ", "howto": "ਸਾਡੀਆਂ ਗਾਈਡਾਂ ਦੀ ਪਾਲਣਾ ਕਰੋ", "or": "ਜਾਂ", diff --git a/src/locales/percentages.json b/src/locales/percentages.json index 29a0c1f2b..b28b6c259 100644 --- a/src/locales/percentages.json +++ b/src/locales/percentages.json @@ -1,52 +1,53 @@ { - "ar-SA": 92, - "bg-BG": 54, - "bn-BD": 59, - "ca-ES": 100, - "cs-CZ": 74, - "da-DK": 32, + "ar-SA": 89, + "bg-BG": 52, + "bn-BD": 57, + "ca-ES": 96, + "cs-CZ": 72, + "da-DK": 31, "de-DE": 100, - "el-GR": 99, + "el-GR": 98, "en": 100, - "es-ES": 100, - "eu-ES": 100, - "fa-IR": 95, - "fi-FI": 100, - "fr-FR": 100, + "es-ES": 99, + "eu-ES": 99, + "fa-IR": 91, + "fi-FI": 96, + "fr-FR": 99, "gl-ES": 99, - "he-IL": 89, - "hi-IN": 71, - "hu-HU": 88, - "id-ID": 99, - "it-IT": 100, - "ja-JP": 100, + "he-IL": 99, + "hi-IN": 73, + "hu-HU": 85, + "id-ID": 98, + "it-IT": 99, + "ja-JP": 97, "kab-KAB": 93, - "kk-KZ": 20, - "ko-KR": 98, - "ku-TR": 95, - "lt-LT": 63, - "lv-LV": 97, + "kk-KZ": 19, + "ko-KR": 99, + "ku-TR": 91, + "lt-LT": 61, + "lv-LV": 93, "mr-IN": 100, - "my-MM": 41, + "my-MM": 40, "nb-NO": 100, - "nl-NL": 90, - "nn-NO": 89, - "oc-FR": 98, - "pa-IN": 82, - "pl-PL": 84, - "pt-BR": 100, - "pt-PT": 100, + "nl-NL": 92, + "nn-NO": 86, + "oc-FR": 94, + "pa-IN": 79, + "pl-PL": 87, + "pt-BR": 96, + "pt-PT": 99, "ro-RO": 100, - "ru-RU": 100, + "ru-RU": 96, "si-LK": 8, - "sk-SK": 100, + "sk-SK": 99, "sl-SI": 100, - "sv-SE": 100, - "ta-IN": 94, - "tr-TR": 97, - "uk-UA": 96, - "vi-VN": 20, - "zh-CN": 100, + "sv-SE": 99, + "ta-IN": 90, + "th-TH": 39, + "tr-TR": 98, + "uk-UA": 93, + "vi-VN": 52, + "zh-CN": 99, "zh-HK": 25, "zh-TW": 100 } diff --git a/src/locales/pl-PL.json b/src/locales/pl-PL.json index 89a52b493..1abaa038f 100644 --- a/src/locales/pl-PL.json +++ b/src/locales/pl-PL.json @@ -1,7 +1,7 @@ { "labels": { "paste": "Wklej", - "pasteAsPlaintext": "", + "pasteAsPlaintext": "Wklej jako zwykły tekst", "pasteCharts": "Wklej wykresy", "selectAll": "Zaznacz wszystko", "multiSelect": "Dodaj element do zaznaczenia", @@ -72,7 +72,7 @@ "layers": "Warstwy", "actions": "Akcje", "language": "Język", - "liveCollaboration": "", + "liveCollaboration": "Współpraca w czasie rzeczywistym...", "duplicateSelection": "Powiel", "untitled": "Bez tytułu", "name": "Nazwa", @@ -96,8 +96,8 @@ "centerHorizontally": "Wyśrodkuj w poziomie", "distributeHorizontally": "Rozłóż poziomo", "distributeVertically": "Rozłóż pionowo", - "flipHorizontal": "Odbij w poziomie", - "flipVertical": "Odbij w pionie", + "flipHorizontal": "Odwróć w poziomie", + "flipVertical": "Odwróć w pionie", "viewMode": "Tryb widoku", "toggleExportColorScheme": "Przełącz schemat kolorów przy eksporcie", "share": "Udostępnij", @@ -106,46 +106,47 @@ "toggleTheme": "Przełącz motyw", "personalLib": "Biblioteka prywatna", "excalidrawLib": "Biblioteka Excalidraw", - "decreaseFontSize": "", - "increaseFontSize": "", + "decreaseFontSize": "Zmniejsz rozmiar czcionki", + "increaseFontSize": "Zwiększ rozmiar czcionki", "unbindText": "", "bindText": "", + "createContainerFromText": "", "link": { - "edit": "", - "create": "", - "label": "" + "edit": "Edytuj łącze", + "create": "Utwórz łącze", + "label": "Łącze" }, "lineEditor": { "edit": "", "exit": "" }, "elementLock": { - "lock": "", - "unlock": "", - "lockAll": "", - "unlockAll": "" + "lock": "Zablokuj", + "unlock": "Odblokuj", + "lockAll": "Zablokuj wszystko", + "unlockAll": "Odblokuj wszystko" }, - "statusPublished": "", - "sidebarLock": "" + "statusPublished": "Opublikowano", + "sidebarLock": "Panel boczny zawsze otwarty" }, "library": { - "noItems": "", + "noItems": "Nie dodano jeszcze żadnych elementów...", "hint_emptyLibrary": "", - "hint_emptyPrivateLibrary": "" + "hint_emptyPrivateLibrary": "Wybierz element, aby dodać go tutaj." }, "buttons": { "clearReset": "Wyczyść dokument i zresetuj kolor dokumentu", "exportJSON": "Eksportuj do pliku", - "exportImage": "", - "export": "", + "exportImage": "Eksportuj obraz...", + "export": "Zapisz jako...", "exportToPng": "Zapisz jako PNG", "exportToSvg": "Zapisz jako SVG", "copyToClipboard": "Skopiuj do schowka", "copyPngToClipboard": "Skopiuj do schowka jako plik PNG", "scale": "Skala", - "save": "", + "save": "Zapisz do bieżącego pliku", "saveAs": "Zapisz jako", - "load": "", + "load": "Otwórz", "getShareableLink": "Udostępnij", "close": "Zamknij", "selectLanguage": "Wybierz język", @@ -179,11 +180,11 @@ "couldNotLoadInvalidFile": "Nie udało się otworzyć pliku. Wybrany plik jest nieprawidłowy.", "importBackendFailed": "Wystąpił błąd podczas importowania pliku.", "cannotExportEmptyCanvas": "Najpierw musisz coś narysować, aby zapisać dokument.", - "couldNotCopyToClipboard": "", + "couldNotCopyToClipboard": "Nie udało się skopiować do schowka.", "decryptFailed": "Nie udało się odszyfrować danych.", "uploadedSecurly": "By zapewnić Ci prywatność, udostępnianie projektu jest zabezpieczone szyfrowaniem end-to-end, co oznacza, że poza tobą i osobą z którą podzielisz się linkiem, nikt nie ma dostępu do tego co udostępniasz.", "loadSceneOverridePrompt": "Wczytanie zewnętrznego rysunku zastąpi istniejącą zawartość. Czy chcesz kontynuować?", - "collabStopOverridePrompt": "Zatrzymanie sesji nadpisze poprzedni, zapisany lokalnie rysunk. Jesteś pewien?\n\n(Jeśli chcesz zachować swój lokalny rysunek, po prostu zamknij zakładkę przeglądarki.)", + "collabStopOverridePrompt": "Zatrzymanie sesji nadpisze poprzedni, zapisany lokalnie rysunek. Czy jesteś pewien?\n\n(Jeśli chcesz zachować swój lokalny rysunek, po prostu zamknij zakładkę przeglądarki.)", "errorAddingToLibrary": "Nie udało się dodać elementu do biblioteki", "errorRemovingFromLibrary": "Nie udało się usunąć elementu z biblioteki", "confirmAddLibrary": "To doda {{numShapes}} kształtów do twojej biblioteki. Jesteś pewien?", @@ -193,7 +194,7 @@ "resetLibrary": "To wyczyści twoją bibliotekę. Jesteś pewien?", "removeItemsFromsLibrary": "Usunąć {{count}} element(ów) z biblioteki?", "invalidEncryptionKey": "Klucz szyfrowania musi składać się z 22 znaków. Współpraca na żywo jest wyłączona.", - "collabOfflineWarning": "" + "collabOfflineWarning": "Brak połączenia z Internetem.\nTwoje zmiany nie zostaną zapisane!" }, "errors": { "unsupportedFileType": "Nieobsługiwany typ pliku.", @@ -201,10 +202,25 @@ "fileTooBig": "Plik jest zbyt duży. Maksymalny dozwolony rozmiar to {{maxSize}}.", "svgImageInsertError": "Nie udało się wstawić obrazu SVG. Znacznik SVG wygląda na nieprawidłowy.", "invalidSVGString": "Nieprawidłowy SVG.", - "cannotResolveCollabServer": "", - "importLibraryError": "", - "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "cannotResolveCollabServer": "Nie można połączyć się z serwerem współpracy w czasie rzeczywistym. Proszę odświeżyć stronę i spróbować ponownie.", + "importLibraryError": "Wystąpił błąd w trakcie ładowania biblioteki", + "collabSaveFailed": "Nie udało się zapisać w bazie danych. Jeśli problemy nie ustąpią, zapisz plik lokalnie, aby nie utracić swojej pracy.", + "collabSaveFailed_sizeExceeded": "Nie udało się zapisać w bazie danych — dokument jest za duży. Zapisz plik lokalnie, aby nie utracić swojej pracy.", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "problem", + "write": "na naszym GitHubie lub napisz do nas na", + "discord": "Discord" + } }, "toolBar": { "selection": "Zaznaczenie", @@ -303,7 +319,8 @@ "doubleClick": "podwójne kliknięcie", "drag": "przeciągnij", "editor": "Edytor", - "editSelectedShape": "Edytuj wybrany kształt (tekst/strzałka/linia)", + "editLineArrowPoints": "", + "editText": "", "github": "Znalazłeś problem? Prześlij", "howto": "Skorzystaj z instrukcji", "or": "lub", @@ -317,7 +334,7 @@ "zoomToFit": "Powiększ, aby wyświetlić wszystkie elementy", "zoomToSelection": "Przybliż do zaznaczenia", "toggleElementLock": "", - "movePageUpDown": "", + "movePageUpDown": "Przesuń stronę w górę/w dół", "movePageLeftRight": "" }, "clearCanvasDialog": { diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json index cb7f045de..d790c5db5 100644 --- a/src/locales/pt-BR.json +++ b/src/locales/pt-BR.json @@ -110,6 +110,7 @@ "increaseFontSize": "Aumentar o tamanho da fonte", "unbindText": "Desvincular texto", "bindText": "Vincular texto ao contêiner", + "createContainerFromText": "", "link": { "edit": "Editar link", "create": "Criar link", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Não foi possível conectar-se ao servidor colaborativo. Por favor, recarregue a página e tente novamente.", "importLibraryError": "Não foi possível carregar a biblioteca", "collabSaveFailed": "Não foi possível salvar no banco de dados do servidor. Se os problemas persistirem, salve o arquivo localmente para garantir que não perca o seu trabalho.", - "collabSaveFailed_sizeExceeded": "Não foi possível salvar no banco de dados do servidor, a tela parece ser muito grande. Se os problemas persistirem, salve o arquivo localmente para garantir que não perca o seu trabalho." + "collabSaveFailed_sizeExceeded": "Não foi possível salvar no banco de dados do servidor, a tela parece ser muito grande. Se os problemas persistirem, salve o arquivo localmente para garantir que não perca o seu trabalho.", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "Seleção", @@ -303,7 +319,8 @@ "doubleClick": "clique duplo", "drag": "arrastar", "editor": "Editor", - "editSelectedShape": "Editar forma selecionada (texto/seta/linha)", + "editLineArrowPoints": "", + "editText": "", "github": "Encontrou algum problema? Nos informe", "howto": "Siga nossos guias", "or": "ou", diff --git a/src/locales/pt-PT.json b/src/locales/pt-PT.json index f713b22d3..7ce28f00f 100644 --- a/src/locales/pt-PT.json +++ b/src/locales/pt-PT.json @@ -110,6 +110,7 @@ "increaseFontSize": "Aumentar o tamanho do tipo de letra", "unbindText": "Desvincular texto", "bindText": "Ligar texto ao recipiente", + "createContainerFromText": "Envolver texto num recipiente", "link": { "edit": "Editar ligação", "create": "Criar ligação", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Não foi possível fazer a ligação ao servidor colaborativo. Por favor, volte a carregar a página e tente novamente.", "importLibraryError": "Não foi possível carregar a biblioteca", "collabSaveFailed": "Não foi possível guardar na base de dados de backend. Se os problemas persistirem, guarde o ficheiro localmente para garantir que não perde o seu trabalho.", - "collabSaveFailed_sizeExceeded": "Não foi possível guardar na base de dados de backend, o ecrã parece estar muito grande. Deve guardar o ficheiro localmente para garantir que não perde o seu trabalho." + "collabSaveFailed_sizeExceeded": "Não foi possível guardar na base de dados de backend, o ecrã parece estar muito grande. Deve guardar o ficheiro localmente para garantir que não perde o seu trabalho.", + "brave_measure_text_error": { + "start": "Parece que está a usar o navegador Brave com o", + "aggressive_block_fingerprint": "Bloqueio Agressivo via Impressão Digital", + "setting_enabled": "activo", + "break": "Isso pode desconfigurar os", + "text_elements": "Elementos de Texto", + "in_your_drawings": "nos seus desenhos", + "strongly_recommend": "Recomendamos fortemente a desactivação desta configuração. Pode seguir", + "steps": "os seguintes passos", + "how": "para saber como o fazer", + "disable_setting": " Se desactivar esta configuração não consertar a exibição de elementos de texto, por favor, abra um", + "issue": "problema", + "write": "no nosso GitHub ou então escreva-nos no", + "discord": "Discord" + } }, "toolBar": { "selection": "Seleção", @@ -303,7 +319,8 @@ "doubleClick": "clique duplo", "drag": "arrastar", "editor": "Editor", - "editSelectedShape": "Editar forma selecionada (texto/seta/linha)", + "editLineArrowPoints": "", + "editText": "", "github": "Encontrou algum problema? Informe-nos", "howto": "Siga os nossos guias", "or": "ou", diff --git a/src/locales/ro-RO.json b/src/locales/ro-RO.json index 8eda2385b..a0e3b5883 100644 --- a/src/locales/ro-RO.json +++ b/src/locales/ro-RO.json @@ -110,6 +110,7 @@ "increaseFontSize": "Mărește dimensiunea fontului", "unbindText": "Deconectare text", "bindText": "Legare text de container", + "createContainerFromText": "Încadrare text într-un container", "link": { "edit": "Editare URL", "create": "Creare URL", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Nu a putut fi realizată conexiunea la serverul de colaborare. Reîncarcă pagina și încearcă din nou.", "importLibraryError": "Biblioteca nu a putut fi încărcată", "collabSaveFailed": "Nu s-a putut salva în baza de date la nivel de server. Dacă problemele persistă, ar trebui să salvezi fișierul la nivel local pentru a te asigura că nu îți pierzi munca.", - "collabSaveFailed_sizeExceeded": "Nu s-a putut salva în baza de date la nivel de server, întrucât se pare că pânza este prea mare. Ar trebui să salvezi fișierul la nivel local pentru a te asigura că nu îți pierzi munca." + "collabSaveFailed_sizeExceeded": "Nu s-a putut salva în baza de date la nivel de server, întrucât se pare că pânza este prea mare. Ar trebui să salvezi fișierul la nivel local pentru a te asigura că nu îți pierzi munca.", + "brave_measure_text_error": { + "start": "Se pare că folosești navigatorul Brave cu", + "aggressive_block_fingerprint": "setarea „Blocați amprentarea” în modul strict", + "setting_enabled": "activată", + "break": "Acest lucru ar putea duce la întreruperea", + "text_elements": "elementelor de text", + "in_your_drawings": "din desenele tale", + "strongly_recommend": "Recomandăm insistent dezactivarea acestei setări. Poți urma", + "steps": "acești pași", + "how": "pentru efectuarea procedurii", + "disable_setting": " Dacă dezactivarea acestei setări nu remediază afișarea elementelor de text, deschide", + "issue": "un tichet cu probleme", + "write": "pe pagina noastră de GitHub sau scrie-ne pe", + "discord": "Discord" + } }, "toolBar": { "selection": "Selecție", @@ -303,7 +319,8 @@ "doubleClick": "dublu clic", "drag": "glisare", "editor": "Editor", - "editSelectedShape": "Editează forma selectată (text/săgeată/linie)", + "editLineArrowPoints": "Editare puncte de săgeată/rând", + "editText": "Editare text/adăugare etichetă", "github": "Ai întâmpinat o problemă? Trimite un raport", "howto": "Urmărește ghidurile noastre", "or": "sau", diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json index cd0609505..d4030897c 100644 --- a/src/locales/ru-RU.json +++ b/src/locales/ru-RU.json @@ -110,6 +110,7 @@ "increaseFontSize": "Увеличить шрифт", "unbindText": "Отвязать текст", "bindText": "Привязать текст к контейнеру", + "createContainerFromText": "", "link": { "edit": "Редактировать ссылку", "create": "Создать ссылку", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Не удалось подключиться к серверу совместного редактирования. Перезагрузите страницу и повторите попытку.", "importLibraryError": "Не удалось загрузить библиотеку", "collabSaveFailed": "Не удалось сохранить в базу данных. Если проблема повторится, нужно будет сохранить файл локально, чтобы быть уверенным, что вы не потеряете вашу работу.", - "collabSaveFailed_sizeExceeded": "Не удалось сохранить в базу данных. Похоже, что холст слишком большой. Нужно сохранить файл локально, чтобы быть уверенным, что вы не потеряете вашу работу." + "collabSaveFailed_sizeExceeded": "Не удалось сохранить в базу данных. Похоже, что холст слишком большой. Нужно сохранить файл локально, чтобы быть уверенным, что вы не потеряете вашу работу.", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "Выделение области", @@ -303,7 +319,8 @@ "doubleClick": "двойной клик", "drag": "перетащить", "editor": "Редактор", - "editSelectedShape": "Редактировать выбранную фигуру (текст/стрелка/линия)", + "editLineArrowPoints": "", + "editText": "", "github": "Нашли проблему? Отправьте", "howto": "Следуйте нашим инструкциям", "or": "или", diff --git a/src/locales/si-LK.json b/src/locales/si-LK.json index ee6ad46f7..b84ad567f 100644 --- a/src/locales/si-LK.json +++ b/src/locales/si-LK.json @@ -110,6 +110,7 @@ "increaseFontSize": "", "unbindText": "", "bindText": "", + "createContainerFromText": "", "link": { "edit": "", "create": "", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "", "importLibraryError": "", "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "", @@ -303,7 +319,8 @@ "doubleClick": "", "drag": "", "editor": "", - "editSelectedShape": "", + "editLineArrowPoints": "", + "editText": "", "github": "", "howto": "", "or": "", diff --git a/src/locales/sk-SK.json b/src/locales/sk-SK.json index 2b78b4539..d6322ac1c 100644 --- a/src/locales/sk-SK.json +++ b/src/locales/sk-SK.json @@ -110,6 +110,7 @@ "increaseFontSize": "Zväčšiť veľkosť písma", "unbindText": "Zrušiť previazanie textu", "bindText": "Previazať text s kontajnerom", + "createContainerFromText": "Zabaliť text do kontajneru", "link": { "edit": "Upraviť odkaz", "create": "Vytvoriť odkaz", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Nepodarilo sa pripojiť ku kolaboračnému serveru. Prosím obnovte stránku a skúste to znovu.", "importLibraryError": "Nepodarilo sa načítať knižnicu", "collabSaveFailed": "Uloženie do databázy sa nepodarilo. Ak tento problém pretrváva uložte si váš súbor lokálne aby ste nestratili vašu prácu.", - "collabSaveFailed_sizeExceeded": "Uloženie do databázy sa nepodarilo, pretože veľkosť plátna je príliš veľká. Uložte si váš súbor lokálne aby ste nestratili vašu prácu." + "collabSaveFailed_sizeExceeded": "Uloženie do databázy sa nepodarilo, pretože veľkosť plátna je príliš veľká. Uložte si váš súbor lokálne aby ste nestratili vašu prácu.", + "brave_measure_text_error": { + "start": "Vyzerá to, že používate prehliadač Brave s", + "aggressive_block_fingerprint": "agresívnym blokovaním sledovania", + "setting_enabled": "zapnutým", + "break": "Toto môže znemožniť používanie", + "text_elements": "textových prvkov", + "in_your_drawings": "vo vašich kresbách", + "strongly_recommend": "Odporúčame vypnutie tohto nastavenia. Postupuje podľa", + "steps": "týchto krokov", + "how": "pre jeho vypnutie", + "disable_setting": " Ak sa vypnutím tohto nastavenia problém zobrazenia textových prvkov neodstráni, prosím vytvorte", + "issue": "issue", + "write": "na našom Github-e alebo nám to oznámte na", + "discord": "Discord" + } }, "toolBar": { "selection": "Výber", @@ -303,7 +319,8 @@ "doubleClick": "dvojklik", "drag": "potiahnutie", "editor": "Editovanie", - "editSelectedShape": "Editovať zvolený tvar (text/šípka/čiara)", + "editLineArrowPoints": "", + "editText": "", "github": "Objavili ste problém? Nahláste ho", "howto": "Postupujte podľa naších návodov", "or": "alebo", diff --git a/src/locales/sl-SI.json b/src/locales/sl-SI.json index 01c8ba4ca..57199c813 100644 --- a/src/locales/sl-SI.json +++ b/src/locales/sl-SI.json @@ -110,6 +110,7 @@ "increaseFontSize": "Povečaj velikost pisave", "unbindText": "Veži besedilo", "bindText": "Veži besedilo na element", + "createContainerFromText": "Zavij besedilo v vsebnik", "link": { "edit": "Uredi povezavo", "create": "Ustvari povezavo", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Povezave s strežnikom za sodelovanje ni bilo mogoče vzpostaviti. Ponovno naložite stran in poskusite znova.", "importLibraryError": "Nalaganje knjižnice ni uspelo", "collabSaveFailed": "Ni bilo mogoče shraniti v zaledno bazo podatkov. Če se težave nadaljujejo, shranite datoteko lokalno, da ne boste izgubili svojega dela.", - "collabSaveFailed_sizeExceeded": "Ni bilo mogoče shraniti v zaledno bazo podatkov, zdi se, da je platno preveliko. Datoteko shranite lokalno, da ne izgubite svojega dela." + "collabSaveFailed_sizeExceeded": "Ni bilo mogoče shraniti v zaledno bazo podatkov, zdi se, da je platno preveliko. Datoteko shranite lokalno, da ne izgubite svojega dela.", + "brave_measure_text_error": { + "start": "Videti je, da uporabljate brskalnik Brave z omogočeno nastavitvijo", + "aggressive_block_fingerprint": "Agresivno blokiranje prstnih odtisov", + "setting_enabled": " ", + "break": "To bi lahko povzročilo motnje v obnašanju", + "text_elements": "besedilnih elementov", + "in_your_drawings": "v vaših risbah", + "strongly_recommend": "Močno priporočamo, da onemogočite to nastavitev. Sledite", + "steps": "tem korakom,", + "how": "kako to storiti", + "disable_setting": " Če onemogočanje te nastavitve ne popravi prikaza besedilnih elementov, odprite", + "issue": "vprašanje", + "write": "na našem GitHubu ali nam pišite na", + "discord": "Discord" + } }, "toolBar": { "selection": "Izbor", @@ -303,7 +319,8 @@ "doubleClick": "dvojni klik", "drag": "vleci", "editor": "Urejevalnik", - "editSelectedShape": "Uredi izbrano obliko (besedilo/puščica/črta)", + "editLineArrowPoints": "Uredi črto/točke puščice", + "editText": "Uredi besedilo / dodaj oznako", "github": "Ste našli težavo? Pošljite", "howto": "Sledite našim vodičem", "or": "ali", diff --git a/src/locales/sv-SE.json b/src/locales/sv-SE.json index 3cf3b0cbd..d0392b119 100644 --- a/src/locales/sv-SE.json +++ b/src/locales/sv-SE.json @@ -110,6 +110,7 @@ "increaseFontSize": "Öka fontstorleken", "unbindText": "Koppla bort text", "bindText": "Bind texten till behållaren", + "createContainerFromText": "Radbryt text i en avgränsad yta", "link": { "edit": "Redigera länk", "create": "Skapa länk", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Det gick inte att ansluta till samarbets-servern. Ladda om sidan och försök igen.", "importLibraryError": "Kunde inte ladda bibliotek", "collabSaveFailed": "Det gick inte att spara i backend-databasen. Om problemen kvarstår bör du spara filen lokalt för att se till att du inte förlorar ditt arbete.", - "collabSaveFailed_sizeExceeded": "Det gick inte att spara till backend-databasen, whiteboarden verkar vara för stor. Du bör spara filen lokalt för att du inte ska förlora ditt arbete." + "collabSaveFailed_sizeExceeded": "Det gick inte att spara till backend-databasen, whiteboarden verkar vara för stor. Du bör spara filen lokalt för att du inte ska förlora ditt arbete.", + "brave_measure_text_error": { + "start": "Det ser ut som att du använder webbläsaren Brave med", + "aggressive_block_fingerprint": "Blockera \"Fingerprinting\" aggressivt", + "setting_enabled": "inställningen aktiverad", + "break": "Detta kan resultera i att ha sönder", + "text_elements": "Textelement", + "in_your_drawings": "i dina skisser", + "strongly_recommend": "Vi rekommenderar starkt att inaktivera denna inställning. Du kan följa", + "steps": "dessa steg", + "how": "om hur man gör det", + "disable_setting": " Om inaktivering av den här inställningen inte åtgärdar visningen av textelement, vänligen skapa ett", + "issue": "ärende", + "write": "på vår GitHub, eller skriv till oss på", + "discord": "Discord" + } }, "toolBar": { "selection": "Markering", @@ -303,7 +319,8 @@ "doubleClick": "dubbelklicka", "drag": "dra", "editor": "Redigerare", - "editSelectedShape": "Redigera markerad form (text/pil/linje)", + "editLineArrowPoints": "", + "editText": "", "github": "Hittat ett problem? Rapportera", "howto": "Följ våra guider", "or": "eller", diff --git a/src/locales/ta-IN.json b/src/locales/ta-IN.json index 493dfa560..6dc865766 100644 --- a/src/locales/ta-IN.json +++ b/src/locales/ta-IN.json @@ -110,6 +110,7 @@ "increaseFontSize": "எழுத்துரு அளவை அதிகரி", "unbindText": "உரையைப் பிணைவவிழ்", "bindText": "", + "createContainerFromText": "", "link": { "edit": "தொடுப்பைத் திருத்து", "create": "தொடுப்பைப் படை", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "", "importLibraryError": "நூலகத்தை ஏற்ற முடியவில்லை", "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "தெரிவு", @@ -303,7 +319,8 @@ "doubleClick": "இரு-சொடுக்கு", "drag": "பிடித்திழு", "editor": "திருத்தி", - "editSelectedShape": "தேர்ந்த வடிவத்தைத் திருத்து (உரை/அம்பு/வரி)", + "editLineArrowPoints": "", + "editText": "", "github": "சிக்கலைக் கண்டீரா? சமர்ப்பி", "howto": "எங்கள் கையேடுகளைப் பின்பற்றுக", "or": "அ", diff --git a/src/locales/th-TH.json b/src/locales/th-TH.json new file mode 100644 index 000000000..2dcb9cb43 --- /dev/null +++ b/src/locales/th-TH.json @@ -0,0 +1,482 @@ +{ + "labels": { + "paste": "วาง", + "pasteAsPlaintext": "วางโดยไม่มีการจัดรูปแบบ", + "pasteCharts": "วางแผนภูมิ", + "selectAll": "เลือกทั้งหมด", + "multiSelect": "", + "moveCanvas": "", + "cut": "ตัด", + "copy": "คัดลอก", + "copyAsPng": "คัดลองไปยังคลิปบอร์ดเป็น PNG", + "copyAsSvg": "คัดลองไปยังคลิปบอร์ดเป็น SVG", + "copyText": "คัดลองไปยังคลิปบอร์ดเป็นข้อความ", + "bringForward": "นำขึ้นข้างบน", + "sendToBack": "ย้ายไปข้างล่าง", + "bringToFront": "นำขึ้นข้างหน้า", + "sendBackward": "ย้ายไปข้างหลัง", + "delete": "ลบ", + "copyStyles": "คัดลอกรูปแบบ", + "pasteStyles": "วางรูปแบบ", + "stroke": "เส้นขอบ", + "background": "พื้นหลัง", + "fill": "เติมสี", + "strokeWidth": "น้ำหนักเส้นขอบ", + "strokeStyle": "", + "strokeStyle_solid": "", + "strokeStyle_dashed": "", + "strokeStyle_dotted": "", + "sloppiness": "ความเลอะเทอะ", + "opacity": "ความทึบแสง", + "textAlign": "จัดข้อความ", + "edges": "ขอบ", + "sharp": "", + "round": "", + "arrowheads": "", + "arrowhead_none": "", + "arrowhead_arrow": "", + "arrowhead_bar": "", + "arrowhead_dot": "", + "arrowhead_triangle": "", + "fontSize": "ขนาดตัวอักษร", + "fontFamily": "แบบตัวอักษร", + "onlySelected": "เฉพาะที่เลือก", + "withBackground": "พื้นหลัง", + "exportEmbedScene": "", + "exportEmbedScene_details": "", + "addWatermark": "เพิ่มลายน้ำ \"สร้างด้วย Excalidraw\"", + "handDrawn": "ลายมือ", + "normal": "ปกติ", + "code": "โค้ด", + "small": "เล็ก", + "medium": "กลาง", + "large": "ใหญ่", + "veryLarge": "ใหญ่มาก", + "solid": "", + "hachure": "", + "crossHatch": "", + "thin": "บาง", + "bold": "หนา", + "left": "ซ้าย", + "center": "กลาง", + "right": "ขวา", + "extraBold": "หนาพิเศษ", + "architect": "", + "artist": "", + "cartoonist": "", + "fileTitle": "ชื่อไฟล์", + "colorPicker": "เลือกสีที่กำหนดเอง", + "canvasColors": "", + "canvasBackground": "", + "drawingCanvas": "", + "layers": "", + "actions": "การกระทำ", + "language": "ภาษา", + "liveCollaboration": "", + "duplicateSelection": "ทำสำเนา", + "untitled": "ไม่มีชื่อ", + "name": "ชื่อ", + "yourName": "ชื่อของคุณ", + "madeWithExcalidraw": "", + "group": "จัดกลุ่ม", + "ungroup": "ยกเลิกการจัดกลุ่ม", + "collaborators": "", + "showGrid": "แสดงเส้นตาราง", + "addToLibrary": "เพิ่มไปในคลัง", + "removeFromLibrary": "นำออกจากคลัง", + "libraryLoadingMessage": "กำลังโหลดคลัง...", + "libraries": "", + "loadingScene": "กำลังโหลดฉาก", + "align": "จัดตำแหน่ง", + "alignTop": "จัดชิดด้านบน", + "alignBottom": "จัดชิดด้านล่าง", + "alignLeft": "จัดชิดซ้าย", + "alignRight": "จัดชิดขวา", + "centerVertically": "กึ่งกลางแนวตั้ง", + "centerHorizontally": "กึ่งกลางแนวนอน", + "distributeHorizontally": "กระจายแนวนอน", + "distributeVertically": "กระจายแนวตั้ง", + "flipHorizontal": "พลิกแนวนอน", + "flipVertical": "พลิกแนวตั้ง", + "viewMode": "โหมดมุมมอง", + "toggleExportColorScheme": "", + "share": "แชร์", + "showStroke": "", + "showBackground": "", + "toggleTheme": "สลับธีม", + "personalLib": "คลังของฉัน", + "excalidrawLib": "คลังของ Excalidraw", + "decreaseFontSize": "ลดขนาดตัวอักษร", + "increaseFontSize": "เพิ่มขนาดตัวอักษร", + "unbindText": "", + "bindText": "", + "createContainerFromText": "", + "link": { + "edit": "แก้ไขลิงก์", + "create": "สร้างลิงค์", + "label": "ลิงค์" + }, + "lineEditor": { + "edit": "แก้ไขเส้น", + "exit": "" + }, + "elementLock": { + "lock": "ล็อก", + "unlock": "ปลดล็อก", + "lockAll": "ล็อกทั้งหมด", + "unlockAll": "ปลดล็อกทั้งหมด" + }, + "statusPublished": "เผยแพร่", + "sidebarLock": "" + }, + "library": { + "noItems": "", + "hint_emptyLibrary": "", + "hint_emptyPrivateLibrary": "" + }, + "buttons": { + "clearReset": "", + "exportJSON": "ส่งออกไปยังไฟล์", + "exportImage": "ส่งออกเป็นรูปภาพ", + "export": "บันทึกไปยัง", + "exportToPng": "ส่งออกไปเป็น PNG", + "exportToSvg": "ส่งออกไปเป็น SVG", + "copyToClipboard": "คัดลอกไปยังคลิปบอร์ด", + "copyPngToClipboard": "คัดลอก PNG ไปยังคลิปบอร์ด", + "scale": "อัตราส่วน", + "save": "", + "saveAs": "", + "load": "เปิด", + "getShareableLink": "สร้างลิงค์ที่แชร์ได้", + "close": "ปิด", + "selectLanguage": "เลือกภาษา", + "scrollBackToContent": "เลื่อนกลับไปด้านบน", + "zoomIn": "ซูมเข้า", + "zoomOut": "ซูมออก", + "resetZoom": "รีเซ็ตการซูม", + "menu": "เมนู", + "done": "เสร็จสิ้น", + "edit": "แก้ไข", + "undo": "เลิกทำ", + "redo": "ทำซ้ำ", + "resetLibrary": "รีเซ็ตคลัง", + "createNewRoom": "สร้างห้องใหม่", + "fullScreen": "เต็มหน้าจอ", + "darkMode": "โหมดกลางคืน", + "lightMode": "โหมดกลางวัน", + "zenMode": "โหมด Zen", + "exitZenMode": "ออกจากโหมด Zen", + "cancel": "ยกเลิก", + "clear": "เคลียร์", + "remove": "ลบ", + "publishLibrary": "เผยแพร่", + "submit": "ตกลง", + "confirm": "ยืนยัน" + }, + "alerts": { + "clearReset": "", + "couldNotCreateShareableLink": "", + "couldNotCreateShareableLinkTooBig": "", + "couldNotLoadInvalidFile": "ไม่สามารถโหลดไฟล์ที่ผิดพลาดได้", + "importBackendFailed": "", + "cannotExportEmptyCanvas": "", + "couldNotCopyToClipboard": "ไม่สามารถคัดลอกไปยังคลิปบอร์ดได้", + "decryptFailed": "ไม่สามารถถอดรหัสข้อมูลได้", + "uploadedSecurly": "การอัพโหลดได้ถูกเข้ารหัสแบบ end-to-end หมายความว่าเซิร์ฟเวอร์ของ Excalidraw และบุคคลอื่นไม่สามารถอ่านข้อมูลได้", + "loadSceneOverridePrompt": "", + "collabStopOverridePrompt": "", + "errorAddingToLibrary": "ไม่สามารถเพิ่มรายการเข้าไปในคลังได้", + "errorRemovingFromLibrary": "ไม่สามารถลบรายการนี้ออกจากคลังได้", + "confirmAddLibrary": "", + "imageDoesNotContainScene": "", + "cannotRestoreFromImage": "", + "invalidSceneUrl": "", + "resetLibrary": "", + "removeItemsFromsLibrary": "", + "invalidEncryptionKey": "", + "collabOfflineWarning": "" + }, + "errors": { + "unsupportedFileType": "ไม่รองรับชนิดของไฟล์นี้", + "imageInsertError": "ไม่สามารถเพิ่มรูปภาพได้ ลองอีกครั้งในภายหลัง", + "fileTooBig": "", + "svgImageInsertError": "", + "invalidSVGString": "ไฟล์ SVG ผิดพลาด", + "cannotResolveCollabServer": "ไม่สามารถเชื่อต่อกับ collab เซิร์ฟเวอร์ได้ โปรดลองโหลดหน้านี้ใหม่และลองอีกครั้ง", + "importLibraryError": "", + "collabSaveFailed": "", + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "Discord" + } + }, + "toolBar": { + "selection": "", + "image": "", + "rectangle": "สี่เหลี่ยมผืนผ้า", + "diamond": "", + "ellipse": "", + "arrow": "", + "line": "", + "freedraw": "", + "text": "ข้อความ", + "library": "คลัง", + "lock": "", + "penMode": "", + "link": "", + "eraser": "ยางลบ", + "hand": "" + }, + "headings": { + "canvasActions": "", + "selectedShapeActions": "", + "shapes": "รูปร่าง" + }, + "hints": { + "canvasPanning": "", + "linearElement": "", + "freeDraw": "", + "text": "", + "text_selected": "คลิกสองครั้งหรือกด ENTER เพื่อแก้ไขข้อความ", + "text_editing": "กดปุ่ม Esc หรือกด Ctrl, Cmd + Enter เพื่อเสร็จการแก้ไข", + "linearElementMulti": "คลิกที่จุดสุดท้ายหรือกด Escape หรือ Enter เพื่อเสร็จสิ้น", + "lockAngle": "", + "resize": "", + "resizeImage": "", + "rotate": "", + "lineEditor_info": "", + "lineEditor_pointSelected": "กดปุ่ม Delete เพื่อลบจุด\nกด Ctrl หรือ Cmd + D เพื่อทำซ้ำหรือลากเพื่อเคลื่อนย้าย", + "lineEditor_nothingSelected": "", + "placeImage": "", + "publishLibrary": "", + "bindTextToElement": "", + "deepBoxSelect": "", + "eraserRevert": "", + "firefox_clipboard_write": "" + }, + "canvasError": { + "cannotShowPreview": "", + "canvasTooBig": "", + "canvasTooBigTip": "" + }, + "errorSplash": { + "headingMain_pre": "", + "headingMain_button": "กำลังรีโหลดหน้า", + "clearCanvasMessage": "ถ้าโหลดไม่ได้ ให้ลอง ", + "clearCanvasMessage_button": "เคลียร์ผืนผ้าใบ", + "clearCanvasCaveat": "", + "trackedToSentry_pre": "", + "trackedToSentry_post": "", + "openIssueMessage_pre": "", + "openIssueMessage_button": "", + "openIssueMessage_post": "", + "sceneContent": "" + }, + "roomDialog": { + "desc_intro": "", + "desc_privacy": "", + "button_startSession": "เริ่มเซสชัน", + "button_stopSession": "หยุดเซสชัน", + "desc_inProgressIntro": "", + "desc_shareLink": "", + "desc_exitSession": "", + "shareTitle": "" + }, + "errorDialog": { + "title": "" + }, + "exportDialog": { + "disk_title": "", + "disk_details": "", + "disk_button": "", + "link_title": "", + "link_details": "", + "link_button": "", + "excalidrawplus_description": "", + "excalidrawplus_button": "", + "excalidrawplus_exportError": "ไม่สามารถส่งออกไปที่ Excalidraw+ ได้ในขณะนี้" + }, + "helpDialog": { + "blog": "อ่านบล็อกของพวกเรา", + "click": "คลิก", + "deepSelect": "", + "deepBoxSelect": "", + "curvedArrow": "", + "curvedLine": "", + "documentation": "", + "doubleClick": "ดับเบิลคลิก", + "drag": "ลาก", + "editor": "", + "editLineArrowPoints": "", + "editText": "", + "github": "", + "howto": "", + "or": "", + "preventBinding": "", + "tools": "", + "shortcuts": "", + "textFinish": "", + "textNewLine": "", + "title": "ช่วยเหลือ", + "view": "ดู", + "zoomToFit": "", + "zoomToSelection": "", + "toggleElementLock": "", + "movePageUpDown": "", + "movePageLeftRight": "ย้ายหน้าไปด้าน ซ้าย/ขวา" + }, + "clearCanvasDialog": { + "title": "" + }, + "publishDialog": { + "title": "", + "itemName": "", + "authorName": "ชื่อเจ้าของ", + "githubUsername": "ชื่อผู้ใช้ GitHub", + "twitterUsername": "ชื่อผู้ใช้ Twitter", + "libraryName": "", + "libraryDesc": "", + "website": "", + "placeholder": { + "authorName": "", + "libraryName": "", + "libraryDesc": "", + "githubHandle": "", + "twitterHandle": "", + "website": "" + }, + "errors": { + "required": "", + "website": "" + }, + "noteDescription": { + "pre": "", + "link": "", + "post": "" + }, + "noteGuidelines": { + "pre": "", + "link": "", + "post": "" + }, + "noteLicense": { + "pre": "", + "link": "", + "post": "" + }, + "noteItems": "", + "atleastOneLibItem": "", + "republishWarning": "" + }, + "publishSuccessDialog": { + "title": "", + "content": "", + "link": "" + }, + "confirmDialog": { + "resetLibrary": "", + "removeItemsFromLib": "" + }, + "encrypted": { + "tooltip": "", + "link": "" + }, + "stats": { + "angle": "", + "element": "", + "elements": "", + "height": "", + "scene": "", + "selected": "", + "storage": "", + "title": "", + "total": "", + "version": "", + "versionCopy": "", + "versionNotAvailable": "", + "width": "" + }, + "toast": { + "addedToLibrary": "", + "copyStyles": "", + "copyToClipboard": "", + "copyToClipboardAsPng": "", + "fileSaved": "", + "fileSavedToFilename": "", + "canvas": "", + "selection": "", + "pasteAsSingleElement": "" + }, + "colors": { + "ffffff": "สีขาว", + "f8f9fa": "สีเทา 0", + "f1f3f5": "สีเทา 1", + "fff5f5": "สีแดง 0", + "fff0f6": "สีชมพู 0", + "f8f0fc": "", + "f3f0ff": "", + "edf2ff": "", + "e7f5ff": "", + "e3fafc": "", + "e6fcf5": "", + "ebfbee": "", + "f4fce3": "", + "fff9db": "", + "fff4e6": "", + "transparent": "", + "ced4da": "สีเทา 4", + "868e96": "สีเทา 6", + "fa5252": "สีแดง 6", + "e64980": "สีชมพู 6", + "be4bdb": "", + "7950f2": "", + "4c6ef5": "", + "228be6": "", + "15aabf": "", + "12b886": "", + "40c057": "", + "82c91e": "", + "fab005": "", + "fd7e14": "", + "000000": "", + "343a40": "", + "495057": "", + "c92a2a": "", + "a61e4d": "", + "862e9c": "", + "5f3dc4": "", + "364fc7": "", + "1864ab": "", + "0b7285": "", + "087f5b": "", + "2b8a3e": "", + "5c940d": "", + "e67700": "", + "d9480f": "" + }, + "welcomeScreen": { + "app": { + "center_heading": "", + "center_heading_plus": "", + "menuHint": "" + }, + "defaults": { + "menuHint": "", + "center_heading": "", + "toolbarHint": "", + "helpHint": "" + } + } +} diff --git a/src/locales/tr-TR.json b/src/locales/tr-TR.json index a05de9a54..78dd9329a 100644 --- a/src/locales/tr-TR.json +++ b/src/locales/tr-TR.json @@ -66,7 +66,7 @@ "cartoonist": "Karikatürist", "fileTitle": "Dosya adı", "colorPicker": "Renk seçici", - "canvasColors": "Tuvallerin üzerinde kullanıldı", + "canvasColors": "Tuvalin üzerinde kullanıldı", "canvasBackground": "Tuval arka planı", "drawingCanvas": "Çizim tuvali", "layers": "Katmanlar", @@ -110,6 +110,7 @@ "increaseFontSize": "Yazı Tipi Boyutunu Büyült", "unbindText": "Metni çöz", "bindText": "Metni taşıyıcıya bağla", + "createContainerFromText": "", "link": { "edit": "Bağlantıyı düzenle", "create": "Bağlantı oluştur", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "İş birliği sunucusuna bağlanılamıyor. Lütfen sayfayı yenileyip tekrar deneyin.", "importLibraryError": "Kütüphane yüklenemedi", "collabSaveFailed": "Backend veritabanına kaydedilemedi. Eğer problem devam ederse, çalışmanızı korumak için dosyayı yerel olarak kaydetmelisiniz.", - "collabSaveFailed_sizeExceeded": "Backend veritabanına kaydedilemedi; tuval çok büyük. Çalışmanızı korumak için dosyayı yerel olarak kaydetmelisiniz." + "collabSaveFailed_sizeExceeded": "Backend veritabanına kaydedilemedi; tuval çok büyük. Çalışmanızı korumak için dosyayı yerel olarak kaydetmelisiniz.", + "brave_measure_text_error": { + "start": "Görünüşe göre Brave gezginini", + "aggressive_block_fingerprint": "Agresif parmakizi bloklama", + "setting_enabled": "ayarları etkin şeklide kullanıyor gibisiniz", + "break": "Bu bir takım sorunlara yol açabilir", + "text_elements": "Metin elementlerinde bozulma", + "in_your_drawings": "çizimlerde bozulma gibi", + "strongly_recommend": "Bu ayarı devre dışı bırakmanızı şiddetle öneririz. Şu adımları", + "steps": "takip ederek", + "how": "nasıl yapılacağını", + "disable_setting": " Yapabilirsiniz. Eğer devre dışı bırakmak işe yaramazsa, lütfen", + "issue": "konu hakkında", + "write": "github'da sorun belirtin, ya da bize", + "discord": "Discord üzerinden iletin" + } }, "toolBar": { "selection": "Seçme", @@ -303,7 +319,8 @@ "doubleClick": "çift-tıklama", "drag": "sürükle", "editor": "Düzenleyici", - "editSelectedShape": "Seçili şekli düzenle (metin/ok/çizgi)", + "editLineArrowPoints": "Çizgi/ok noktalarını düzenle", + "editText": "Etiket / metin düzenle", "github": "Bir hata mı buldun? Bildir", "howto": "Rehberlerimizi takip edin", "or": "veya", @@ -453,13 +470,13 @@ "app": { "center_heading": "", "center_heading_plus": "", - "menuHint": "" + "menuHint": "Dışa aktar, seçenekler, diller, ..." }, "defaults": { - "menuHint": "", + "menuHint": "Dışa aktar, seçenekler, ve daha fazlası...", "center_heading": "", - "toolbarHint": "", - "helpHint": "" + "toolbarHint": "Bir araç seçin ve çizime başlayın!", + "helpHint": "Kısayollar & yardım" } } } diff --git a/src/locales/uk-UA.json b/src/locales/uk-UA.json index ac0bfcfee..ed1a01e68 100644 --- a/src/locales/uk-UA.json +++ b/src/locales/uk-UA.json @@ -110,6 +110,7 @@ "increaseFontSize": "Збільшити розмір шрифту", "unbindText": "Відв'язати текст", "bindText": "Прив’язати текст до контейнера", + "createContainerFromText": "", "link": { "edit": "Редагування посилання", "create": "Створити посилання", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "Не вдалося приєднатися до сервера. Перезавантажте сторінку та повторіть спробу.", "importLibraryError": "Не вдалося завантажити бібліотеку", "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "Виділення", @@ -303,7 +319,8 @@ "doubleClick": "подвійний клік", "drag": "перетягнути", "editor": "Редактор", - "editSelectedShape": "Змінити вибрану фігуру (текст/стрілку/рядок)", + "editLineArrowPoints": "", + "editText": "", "github": "Знайшли помилку? Повідомте", "howto": "Дотримуйтесь наших інструкцій", "or": "або", diff --git a/src/locales/vi-VN.json b/src/locales/vi-VN.json index 1921035bc..887c32e3e 100644 --- a/src/locales/vi-VN.json +++ b/src/locales/vi-VN.json @@ -5,7 +5,7 @@ "pasteCharts": "Dán biểu đồ", "selectAll": "Chọn tất cả", "multiSelect": "Thêm mới vào Select", - "moveCanvas": "Di chuyển Canvas", + "moveCanvas": "Di chuyển canvas", "cut": "Cắt", "copy": "Sao chép", "copyAsPng": "Sao chép vào bộ nhớ tạm dưới dạng PNG", @@ -60,11 +60,11 @@ "left": "Trái", "center": "Giữa", "right": "Phải", - "extraBold": "", + "extraBold": "Nét siêu đậm", "architect": "Kiến trúc sư", "artist": "Nghệ sỹ", "cartoonist": "Hoạt hình", - "fileTitle": "", + "fileTitle": "Tên tập tin", "colorPicker": "Chọn màu", "canvasColors": "Đã dùng trên canvas", "canvasBackground": "Nền canvas", @@ -72,28 +72,28 @@ "layers": "Lớp", "actions": "Chức năng", "language": "Ngôn ngữ", - "liveCollaboration": "", + "liveCollaboration": "Hợp tác trực tiếp...", "duplicateSelection": "Tạo bản sao", "untitled": "Không có tiêu đề", "name": "Tên", "yourName": "Tên của bạn", "madeWithExcalidraw": "Làm với Excalidraw", - "group": "", - "ungroup": "", + "group": "Gộp nhóm lại lựa chọn", + "ungroup": "Tách nhóm lựa chọn", "collaborators": "Cộng tác viên", - "showGrid": "", - "addToLibrary": "", - "removeFromLibrary": "", - "libraryLoadingMessage": "", - "libraries": "", + "showGrid": "Hiển thị lưới", + "addToLibrary": "Thêm vào thư viện", + "removeFromLibrary": "Xóa khỏi thư viện", + "libraryLoadingMessage": "Đang tải thư viện…", + "libraries": "Xem thư viện", "loadingScene": "", - "align": "", - "alignTop": "", - "alignBottom": "", - "alignLeft": "", - "alignRight": "", - "centerVertically": "", - "centerHorizontally": "", + "align": "Căn chỉnh", + "alignTop": "Căn trên", + "alignBottom": "Căn dưới", + "alignLeft": "Canh trái", + "alignRight": "Canh phải", + "centerVertically": "Giữa theo chiều dọc", + "centerHorizontally": "Giữa theo chiều ngang", "distributeHorizontally": "Phân bố theo chiều ngang", "distributeVertically": "Phân bố theo chiều dọc", "flipHorizontal": "Lật ngang", @@ -105,42 +105,43 @@ "showBackground": "Hiện thị chọn màu nền", "toggleTheme": "", "personalLib": "", - "excalidrawLib": "", - "decreaseFontSize": "", - "increaseFontSize": "", + "excalidrawLib": "Thư viện Excalidraw", + "decreaseFontSize": "Giảm cỡ chữ", + "increaseFontSize": "Tăng cỡ chữ", "unbindText": "", "bindText": "", + "createContainerFromText": "", "link": { - "edit": "", - "create": "", - "label": "" + "edit": "Sửa liên kết", + "create": "Tạo liên kết", + "label": "Liên kết" }, "lineEditor": { - "edit": "", - "exit": "" + "edit": "Điều chỉnh nét", + "exit": "Thoát chỉnh nét" }, "elementLock": { - "lock": "", - "unlock": "", - "lockAll": "", - "unlockAll": "" + "lock": "Khoá", + "unlock": "Mở khoá", + "lockAll": "Khóa tất cả", + "unlockAll": "Mở khóa tất cả" }, - "statusPublished": "", - "sidebarLock": "" + "statusPublished": "Đã đăng tải", + "sidebarLock": "Giữ thanh bên luôn mở" }, "library": { - "noItems": "", - "hint_emptyLibrary": "", - "hint_emptyPrivateLibrary": "" + "noItems": "Chưa có món nào...", + "hint_emptyLibrary": "Chọn một món trên canvas để thêm nó vào đây, hoặc cài đặt thư viện từ kho lưu trữ công cộng, ở bên dưới.", + "hint_emptyPrivateLibrary": "Chọn một món trên canvas để thêm nó vào đây." }, "buttons": { - "clearReset": "", - "exportJSON": "", - "exportImage": "", - "export": "", - "exportToPng": "", - "exportToSvg": "", - "copyToClipboard": "", + "clearReset": "Reset canvas", + "exportJSON": "Xuất ra tập tin", + "exportImage": "Xuất file ảnh...", + "export": "Lưu vào...", + "exportToPng": "Xuất ra tập tin PNG", + "exportToSvg": "Xuất ra tập tin SVG", + "copyToClipboard": "Sao chép vào bộ nhớ tạm", "copyPngToClipboard": "", "scale": "", "save": "", @@ -160,84 +161,99 @@ "redo": "", "resetLibrary": "", "createNewRoom": "", - "fullScreen": "", - "darkMode": "", - "lightMode": "", - "zenMode": "", - "exitZenMode": "", - "cancel": "", - "clear": "", - "remove": "", - "publishLibrary": "", - "submit": "", - "confirm": "" + "fullScreen": "Toàn màn hình", + "darkMode": "Chế độ tối", + "lightMode": "Chế độ sáng", + "zenMode": "Chế độ zen", + "exitZenMode": "Thoát chể độ zen", + "cancel": "Hủy", + "clear": "Làm sạch", + "remove": "Xóa", + "publishLibrary": "Đăng tải", + "submit": "Gửi", + "confirm": "Xác nhận" }, "alerts": { - "clearReset": "", - "couldNotCreateShareableLink": "", - "couldNotCreateShareableLinkTooBig": "", - "couldNotLoadInvalidFile": "", + "clearReset": "Điều này sẽ dọn hết canvas. Bạn có chắc không?", + "couldNotCreateShareableLink": "Không thể tạo đường dẫn chia sẻ.", + "couldNotCreateShareableLinkTooBig": "Không thể tạo đường dẫn chia sẻ: bản vẽ quá lớn", + "couldNotLoadInvalidFile": "Không thể load tập tin không hợp lệ", "importBackendFailed": "", - "cannotExportEmptyCanvas": "", + "cannotExportEmptyCanvas": "Không thể xuất canvas trống.", "couldNotCopyToClipboard": "", "decryptFailed": "", "uploadedSecurly": "", "loadSceneOverridePrompt": "", - "collabStopOverridePrompt": "", - "errorAddingToLibrary": "", - "errorRemovingFromLibrary": "", - "confirmAddLibrary": "", - "imageDoesNotContainScene": "", + "collabStopOverridePrompt": "Dừng phiên sẽ ghi đè lên bản vẽ được lưu trữ cục bộ trước đó của bạn. Bạn có chắc không?\n\n(Nếu bạn muốn giữ bản vẽ cục bộ của mình, chỉ cần đóng tab trình duyệt.)", + "errorAddingToLibrary": "Không thể thêm món vào thư viện", + "errorRemovingFromLibrary": "Không thể xoá món khỏi thư viện", + "confirmAddLibrary": "Hình {{numShapes}} sẽ được thêm vào thư viện. Bạn chắc chứ?", + "imageDoesNotContainScene": "Hình ảnh này dường như không chứa bất kỳ dữ liệu cảnh nào. Bạn đã bật tính năng nhúng cảnh khi xuất chưa?", "cannotRestoreFromImage": "", "invalidSceneUrl": "", "resetLibrary": "", - "removeItemsFromsLibrary": "", - "invalidEncryptionKey": "", - "collabOfflineWarning": "" + "removeItemsFromsLibrary": "Xoá {{count}} món từ thư viện?", + "invalidEncryptionKey": "Khóa mã hóa phải có 22 ký tự. Hợp tác trực tiếp bị vô hiệu hóa.", + "collabOfflineWarning": "Không có kết nối internet.\nThay đổi của bạn sẽ không được lưu!" }, "errors": { - "unsupportedFileType": "", - "imageInsertError": "", - "fileTooBig": "", - "svgImageInsertError": "", - "invalidSVGString": "", - "cannotResolveCollabServer": "", - "importLibraryError": "", - "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "unsupportedFileType": "Loại tập tin không được hỗ trợ.", + "imageInsertError": "Không thể thêm ảnh. Hãy thử lại sau...", + "fileTooBig": "Tệp tin quá lớn. Dung lượng tối đa cho phép là {{maxSize}}.", + "svgImageInsertError": "Không thể thêm ảnh SVG. Mã SVG có vẻ sai.", + "invalidSVGString": "SVG không hợp lệ.", + "cannotResolveCollabServer": "Không thể kết nối với máy chủ hợp tác. Hãy tải lại trang và thử lại.", + "importLibraryError": "Không thể tải thư viện", + "collabSaveFailed": "Không thể lưu vào cơ sở dữ liệu. Nếu vấn đề tiếp tục xảy ra, bạn nên lưu tệp vào máy để đảm bảo bạn không bị mất công việc.", + "collabSaveFailed_sizeExceeded": "Không thể lưu vào cơ sở dữ liệu, canvas có vẻ quá lớn. Bạn nên lưu tệp cục bộ để đảm bảo bạn không bị mất công việc.", + "brave_measure_text_error": { + "start": "Có vẻ bạn đang sử dụng trình duyện Brave với chức năng", + "aggressive_block_fingerprint": "Aggressively Block Fingerprinting", + "setting_enabled": "được bật", + "break": "Điều này có thể xảy ra lỗi các", + "text_elements": "Yếu Tố Chữ", + "in_your_drawings": "trong bản vẽ của bạn", + "strongly_recommend": "Chúng tôi khuyên rằng bạn nên tắt chức năng này. Bạn có thể theo", + "steps": "các bước sau đây", + "how": "để tắt nó", + "disable_setting": " Nếu tắt chức năng này vẫn không sửa lại lỗi hiện thị các yếu tố chữ, hảy mở", + "issue": "issue", + "write": "trên trang GitHUb của chúng tôi, hoặc nhắn chúng tôi tại", + "discord": "Discord" + } }, "toolBar": { - "selection": "", - "image": "", - "rectangle": "", - "diamond": "", - "ellipse": "", - "arrow": "", - "line": "", - "freedraw": "", - "text": "", - "library": "", - "lock": "", - "penMode": "", - "link": "", - "eraser": "", - "hand": "" + "selection": "Lựa chọn", + "image": "Chèn ảnh", + "rectangle": "Hình chữ nhật", + "diamond": "Kim cương", + "ellipse": "Hình elíp", + "arrow": "Mũi tên", + "line": "Đường kẻ", + "freedraw": "Vẽ", + "text": "Văn bản", + "library": "Thư viện", + "lock": "Giữ dụng cũ hiện tại sau khi vẽ", + "penMode": "Chế độ bút vẽ - ngăn ngừa chạm nhầm", + "link": "Thêm/ Chỉnh sửa liên kết cho hình được chọn", + "eraser": "Xóa", + "hand": "Tay kéo" }, "headings": { - "canvasActions": "", - "selectedShapeActions": "", - "shapes": "" + "canvasActions": "Hành động canvas", + "selectedShapeActions": "Các hành động cho hình dạng đã chọn", + "shapes": "Các hình khối" }, "hints": { - "canvasPanning": "", - "linearElement": "", - "freeDraw": "", - "text": "", - "text_selected": "", - "text_editing": "", - "linearElementMulti": "", - "lockAngle": "", - "resize": "", + "canvasPanning": "Để di chuyển canvas, giữ con lăn chuột hoặc phím cách trong khi kéo, hoặc sử dụng công cụ cầm tay", + "linearElement": "Ấn để bắt đầu nhiểm điểm vẽ, kéo để vẽ một đường thẳng", + "freeDraw": "Ấn bà kéo, thả khi bạn xong", + "text": "Mẹo: bạn có thể thêm văn bản tại bất cứ đâu bằng cách ấn hai lần bằng tool lựa chọn", + "text_selected": "Ấn 2 lần hoặc nhấn ENTER để chỉnh văn bản", + "text_editing": "Nhấn Escape hoặc Ctrl/Cmd+ENTER để hoàn thành chỉnh sửa", + "linearElementMulti": "Nhấn vào điểm cuối hoặc nhấn Escape hoặc Enter để kết thúc", + "lockAngle": "Bạn có thể chỉnh lại góc bằng cách giữ phím SHIFT", + "resize": "Bạn có thể chỉnh tỷ lệ bằng cách giữ SHIFT khi chỉnh kích cỡ,\ngiữ ALT để chỉnh kích cỡ từ trung tâm", "resizeImage": "", "rotate": "", "lineEditor_info": "", @@ -248,19 +264,19 @@ "bindTextToElement": "", "deepBoxSelect": "", "eraserRevert": "", - "firefox_clipboard_write": "" + "firefox_clipboard_write": "Tính năng này có thể được bật bằng cách đặt cờ \"dom.events.asyncClipboard.clipboardItem\" thành \"true\". Để thay đổi cờ trình duyệt trong Firefox, hãy truy cập trang \"about:config\"." }, "canvasError": { - "cannotShowPreview": "", - "canvasTooBig": "", - "canvasTooBigTip": "" + "cannotShowPreview": "Không thể xem trước", + "canvasTooBig": "Canvas này có thể hơi lớn.", + "canvasTooBigTip": "Mẹo: hãy thử di chuyển các elements nhất lại gần nhau hơn một chút." }, "errorSplash": { "headingMain_pre": "", "headingMain_button": "", - "clearCanvasMessage": "", - "clearCanvasMessage_button": "", - "clearCanvasCaveat": "", + "clearCanvasMessage": "Nếu không tải lại được, hãy thử ", + "clearCanvasMessage_button": "dọn canvas.", + "clearCanvasCaveat": " Điều này sẽ dẫn đến mất dữ liệu bạn đã làm ", "trackedToSentry_pre": "", "trackedToSentry_post": "", "openIssueMessage_pre": "", @@ -303,7 +319,8 @@ "doubleClick": "", "drag": "", "editor": "", - "editSelectedShape": "", + "editLineArrowPoints": "", + "editText": "", "github": "", "howto": "", "or": "", @@ -321,11 +338,11 @@ "movePageLeftRight": "" }, "clearCanvasDialog": { - "title": "" + "title": "Dọn canvas" }, "publishDialog": { "title": "", - "itemName": "", + "itemName": "Tên món", "authorName": "", "githubUsername": "", "twitterUsername": "", @@ -359,9 +376,9 @@ "link": "", "post": "" }, - "noteItems": "", - "atleastOneLibItem": "", - "republishWarning": "" + "noteItems": "Từng món trong thư viện phải có tên riêng để có thể lọc. Các món thư viện sau đây sẽ thêm:", + "atleastOneLibItem": "Vui lòng chọn ít nhất một món thư viện để bắt đầu", + "republishWarning": "Lưu ý: một số món đã chọn được đánh dấu là đã xuất bản/đã gửi. Bạn chỉ nên gửi lại các món khi cập nhật thư viện hiện có hoặc gửi." }, "publishSuccessDialog": { "title": "", @@ -370,7 +387,7 @@ }, "confirmDialog": { "resetLibrary": "", - "removeItemsFromLib": "" + "removeItemsFromLib": "Xóa món đã chọn khỏi thư viện" }, "encrypted": { "tooltip": "", @@ -398,7 +415,7 @@ "copyToClipboardAsPng": "", "fileSaved": "", "fileSavedToFilename": "", - "canvas": "", + "canvas": "canvas", "selection": "", "pasteAsSingleElement": "" }, @@ -440,14 +457,14 @@ "a61e4d": "", "862e9c": "", "5f3dc4": "", - "364fc7": "", - "1864ab": "", - "0b7285": "", - "087f5b": "", - "2b8a3e": "", - "5c940d": "", - "e67700": "", - "d9480f": "" + "364fc7": "Chàm 9", + "1864ab": "Xanh Dương 9", + "0b7285": "Lục Lam 9", + "087f5b": "Xanh Mòng Két 9", + "2b8a3e": "Xanh Lá 9", + "5c940d": "Chanh Xanh 9", + "e67700": "Vàng 9", + "d9480f": "Cam 9" }, "welcomeScreen": { "app": { diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 2e5debd16..5863f5ceb 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -35,12 +35,12 @@ "arrowheads": "端点", "arrowhead_none": "无", "arrowhead_arrow": "箭头", - "arrowhead_bar": "条", + "arrowhead_bar": "条状", "arrowhead_dot": "圆点", "arrowhead_triangle": "三角箭头", "fontSize": "字体大小", "fontFamily": "字体", - "onlySelected": "仅被选中", + "onlySelected": "仅选中", "withBackground": "背景", "exportEmbedScene": "包含画布数据", "exportEmbedScene_details": "画布数据将被保存到导出的 PNG/SVG 文件,以便恢复。\n将会增加导出的文件大小。", @@ -110,6 +110,7 @@ "increaseFontSize": "放大字体大小", "unbindText": "取消文本绑定", "bindText": "将文本绑定到容器", + "createContainerFromText": "将文本包围在容器中", "link": { "edit": "编辑链接", "create": "新建链接", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "无法连接到实时协作服务器。请重新加载页面并重试。", "importLibraryError": "无法加载素材库", "collabSaveFailed": "无法保存到后端数据库。如果问题持续存在,您应该保存文件到本地,以确保您的工作不会丢失。", - "collabSaveFailed_sizeExceeded": "无法保存到后端数据库,画布似乎过大。您应该保存文件到本地,以确保您的工作不会丢失。" + "collabSaveFailed_sizeExceeded": "无法保存到后端数据库,画布似乎过大。您应该保存文件到本地,以确保您的工作不会丢失。", + "brave_measure_text_error": { + "start": "看起来您正在使用 Brave 浏览器并启用了", + "aggressive_block_fingerprint": "积极阻止指纹识别", + "setting_enabled": "设置", + "break": "这可能会破坏绘图中的", + "text_elements": "文本元素", + "in_your_drawings": " ", + "strongly_recommend": "我们强烈建议禁用此设置。您可以按照", + "steps": "这些步骤", + "how": "来设置", + "disable_setting": " 如果禁用此设置无法修复文本元素的显示,请在 GitHub 提交一个", + "issue": "issue", + "write": ",或者通过", + "discord": "Discord 反馈" + } }, "toolBar": { "selection": "选择", @@ -248,7 +264,7 @@ "bindTextToElement": "按下 Enter 以添加文本", "deepBoxSelect": "按住 CtrlOrCmd 以深度选择,并避免拖拽", "eraserRevert": "按住 Alt 以反选被标记删除的元素", - "firefox_clipboard_write": "将高级配置首选项“dom.events.asyncClipboard.lipboarditem”设置为“true”可以启用此功能。要更改 Firefox 的高级配置首选项,请前往“about:config”页面。" + "firefox_clipboard_write": "将高级配置首选项“dom.events.asyncClipboard.clipboardItem”设置为“true”可以启用此功能。要更改 Firefox 的高级配置首选项,请前往“about:config”页面。" }, "canvasError": { "cannotShowPreview": "无法显示预览", @@ -303,7 +319,8 @@ "doubleClick": "双击", "drag": "拖动", "editor": "编辑器", - "editSelectedShape": "编辑选中的形状 (文本、箭头或线条)", + "editLineArrowPoints": "", + "editText": "", "github": "提交问题", "howto": "帮助文档", "or": "或", diff --git a/src/locales/zh-HK.json b/src/locales/zh-HK.json index e87a0d3e1..bbf23f7f7 100644 --- a/src/locales/zh-HK.json +++ b/src/locales/zh-HK.json @@ -110,6 +110,7 @@ "increaseFontSize": "", "unbindText": "", "bindText": "", + "createContainerFromText": "", "link": { "edit": "", "create": "", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "", "importLibraryError": "", "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "" + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "start": "", + "aggressive_block_fingerprint": "", + "setting_enabled": "", + "break": "", + "text_elements": "", + "in_your_drawings": "", + "strongly_recommend": "", + "steps": "", + "how": "", + "disable_setting": "", + "issue": "", + "write": "", + "discord": "" + } }, "toolBar": { "selection": "", @@ -303,7 +319,8 @@ "doubleClick": "", "drag": "", "editor": "", - "editSelectedShape": "", + "editLineArrowPoints": "", + "editText": "", "github": "", "howto": "", "or": "", diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index 3293fad4a..f4462842c 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -110,6 +110,7 @@ "increaseFontSize": "放大文字", "unbindText": "取消綁定文字", "bindText": "結合文字至容器", + "createContainerFromText": "將文字包於容器中", "link": { "edit": "編輯連結", "create": "建立連結", @@ -204,7 +205,22 @@ "cannotResolveCollabServer": "無法連結至 collab 伺服器。請重新整理後再試一次。", "importLibraryError": "無法載入資料庫", "collabSaveFailed": "無法儲存至後端資料庫。若此問題持續發生,請將檔案儲存於本機以確保資料不會遺失。", - "collabSaveFailed_sizeExceeded": "無法儲存至後端資料庫,可能的原因為畫布尺寸過大。請將檔案儲存於本機以確保資料不會遺失。" + "collabSaveFailed_sizeExceeded": "無法儲存至後端資料庫,可能的原因為畫布尺寸過大。請將檔案儲存於本機以確保資料不會遺失。", + "brave_measure_text_error": { + "start": "您似乎正在使用 Brave 瀏覽器並且將", + "aggressive_block_fingerprint": "\"Aggressively Block Fingerprinting\" 設定", + "setting_enabled": "設為開啟", + "break": "這可能導致破壞", + "text_elements": "文字元素", + "in_your_drawings": "在您的繪圖中", + "strongly_recommend": "我們強烈建議您關掉此設定。您可以參考", + "steps": "這些步驟", + "how": "來變更", + "disable_setting": " 若關閉此設定無法修正文字元素的顯示問題,請回報", + "issue": "問題", + "write": "至我們的 GitHub,或反應在我們的", + "discord": "Discord" + } }, "toolBar": { "selection": "選取", @@ -303,7 +319,8 @@ "doubleClick": "雙擊", "drag": "拖曳", "editor": "編輯器", - "editSelectedShape": "編輯選定的形狀(文字/箭號/線條)", + "editLineArrowPoints": "編輯線/箭頭控制點", + "editText": "編輯文字/增加標籤", "github": "發現異常?回報問題", "howto": "參照我們的說明", "or": "或", From f640ddc2aa1320e674c7d8aed06e78572bf61213 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Sun, 16 Apr 2023 17:22:16 +0200 Subject: [PATCH 240/276] fix: incorrectly duplicating items on paste/library insert (#6467 * fix: incorrectly duplicating items on paste/library insert * fix: deduplicate element ids on restore * tests --- src/components/App.tsx | 35 +++---- src/data/restore.ts | 7 ++ src/element/newElement.ts | 43 +++++---- .../regressionTests.test.tsx.snap | 12 +-- src/tests/helpers/api.ts | 5 +- src/tests/library.test.tsx | 94 +++++++++++++++++++ 6 files changed, 153 insertions(+), 43 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 1ebdc7713..be5561995 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -127,7 +127,11 @@ import { } from "../element/binding"; import { LinearElementEditor } from "../element/linearElementEditor"; import { mutateElement, newElementWith } from "../element/mutateElement"; -import { deepCopyElement, newFreeDrawElement } from "../element/newElement"; +import { + deepCopyElement, + duplicateElements, + newFreeDrawElement, +} from "../element/newElement"; import { hasBoundTextElement, isArrowElement, @@ -1625,35 +1629,22 @@ class App extends React.Component { const dx = x - elementsCenterX; const dy = y - elementsCenterY; - const groupIdMap = new Map(); const [gridX, gridY] = getGridPoint(dx, dy, this.state.gridSize); - const oldIdToDuplicatedId = new Map(); - const newElements = elements.map((element) => { - const newElement = duplicateElement( - this.state.editingGroupId, - groupIdMap, - element, - { + const newElements = duplicateElements( + elements.map((element) => { + return newElementWith(element, { x: element.x + gridX - minX, y: element.y + gridY - minY, - }, - ); - oldIdToDuplicatedId.set(element.id, newElement.id); - return newElement; - }); + }); + }), + ); - bindTextToShapeAfterDuplication(newElements, elements, oldIdToDuplicatedId); const nextElements = [ ...this.scene.getElementsIncludingDeleted(), ...newElements, ]; - fixBindingsAfterDuplication(nextElements, elements, oldIdToDuplicatedId); - - if (opts.files) { - this.files = { ...this.files, ...opts.files }; - } this.scene.replaceAllElements(nextElements); @@ -1664,6 +1655,10 @@ class App extends React.Component { } }); + if (opts.files) { + this.files = { ...this.files, ...opts.files }; + } + this.history.resumeRecording(); this.setState( diff --git a/src/data/restore.ts b/src/data/restore.ts index 2735d91d2..fcf5fa132 100644 --- a/src/data/restore.ts +++ b/src/data/restore.ts @@ -369,6 +369,9 @@ export const restoreElements = ( localElements: readonly ExcalidrawElement[] | null | undefined, opts?: { refreshDimensions?: boolean; repairBindings?: boolean } | undefined, ): ExcalidrawElement[] => { + // used to detect duplicate top-level element ids + const existingIds = new Set(); + const localElementsMap = localElements ? arrayToMap(localElements) : null; const restoredElements = (elements || []).reduce((elements, element) => { // filtering out selection, which is legacy, no longer kept in elements, @@ -383,6 +386,10 @@ export const restoreElements = ( if (localElement && localElement.version > migratedElement.version) { migratedElement = bumpVersion(migratedElement, localElement.version); } + if (existingIds.has(migratedElement.id)) { + migratedElement = { ...migratedElement, id: randomId() }; + } + existingIds.add(migratedElement.id); elements.push(migratedElement); } } diff --git a/src/element/newElement.ts b/src/element/newElement.ts index 72aa54684..e3b25e848 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -439,6 +439,29 @@ export const deepCopyElement = ( return _deepCopyElement(val); }; +/** + * utility wrapper to generate new id. In test env it reuses the old + postfix + * for test assertions. + */ +const regenerateId = ( + /** supply null if no previous id exists */ + previousId: string | null, +) => { + if (isTestEnv() && previousId) { + let nextId = `${previousId}_copy`; + // `window.h` may not be defined in some unit tests + if ( + window.h?.app + ?.getSceneElementsIncludingDeleted() + .find((el) => el.id === nextId) + ) { + nextId += "_copy"; + } + return nextId; + } + return randomId(); +}; + /** * Duplicate an element, often used in the alt-drag operation. * Note that this method has gotten a bit complicated since the @@ -461,19 +484,7 @@ export const duplicateElement = ( ): Readonly => { let copy = deepCopyElement(element); - if (isTestEnv()) { - copy.id = `${copy.id}_copy`; - // `window.h` may not be defined in some unit tests - if ( - window.h?.app - ?.getSceneElementsIncludingDeleted() - .find((el) => el.id === copy.id) - ) { - copy.id += "_copy"; - } - } else { - copy.id = randomId(); - } + copy.id = regenerateId(copy.id); copy.boundElements = null; copy.updated = getUpdatedTimestamp(); copy.seed = randomInteger(); @@ -482,7 +493,7 @@ export const duplicateElement = ( editingGroupId, (groupId) => { if (!groupIdMapForOperation.has(groupId)) { - groupIdMapForOperation.set(groupId, randomId()); + groupIdMapForOperation.set(groupId, regenerateId(groupId)); } return groupIdMapForOperation.get(groupId)!; }, @@ -520,7 +531,7 @@ export const duplicateElements = (elements: readonly ExcalidrawElement[]) => { // if we haven't migrated the element id, but an old element with the same // id exists, generate a new id for it and return it if (origElementsMap.has(id)) { - const newId = randomId(); + const newId = regenerateId(id); elementNewIdsMap.set(id, newId); return newId; } @@ -538,7 +549,7 @@ export const duplicateElements = (elements: readonly ExcalidrawElement[]) => { if (clonedElement.groupIds) { clonedElement.groupIds = clonedElement.groupIds.map((groupId) => { if (!groupNewIdsMap.has(groupId)) { - groupNewIdsMap.set(groupId, randomId()); + groupNewIdsMap.set(groupId, regenerateId(groupId)); } return groupNewIdsMap.get(groupId)!; }); diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap index 35f9eb7c8..967a0cf69 100644 --- a/src/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap @@ -13431,7 +13431,7 @@ Object { "boundElements": null, "fillStyle": "hachure", "groupIds": Array [ - "id6", + "id4_copy", ], "height": 10, "id": "id0_copy", @@ -13464,7 +13464,7 @@ Object { "boundElements": null, "fillStyle": "hachure", "groupIds": Array [ - "id6", + "id4_copy", ], "height": 10, "id": "id1_copy", @@ -13497,7 +13497,7 @@ Object { "boundElements": null, "fillStyle": "hachure", "groupIds": Array [ - "id6", + "id4_copy", ], "height": 10, "id": "id2_copy", @@ -13981,7 +13981,7 @@ Object { "boundElements": null, "fillStyle": "hachure", "groupIds": Array [ - "id6", + "id4_copy", ], "height": 10, "id": "id0_copy", @@ -14011,7 +14011,7 @@ Object { "boundElements": null, "fillStyle": "hachure", "groupIds": Array [ - "id6", + "id4_copy", ], "height": 10, "id": "id1_copy", @@ -14041,7 +14041,7 @@ Object { "boundElements": null, "fillStyle": "hachure", "groupIds": Array [ - "id6", + "id4_copy", ], "height": 10, "id": "id2_copy", diff --git a/src/tests/helpers/api.ts b/src/tests/helpers/api.ts index bc8bfc8a9..705180d6a 100644 --- a/src/tests/helpers/api.ts +++ b/src/tests/helpers/api.ts @@ -211,7 +211,10 @@ export class API { type, startArrowhead: null, endArrowhead: null, - points: rest.points ?? [], + points: rest.points ?? [ + [0, 0], + [100, 100], + ], }); break; case "image": diff --git a/src/tests/library.test.tsx b/src/tests/library.test.tsx index 6b3ff5dd7..86847aeee 100644 --- a/src/tests/library.test.tsx +++ b/src/tests/library.test.tsx @@ -72,6 +72,100 @@ describe("library", () => { }); }); + it("should regenerate ids but retain bindings on library insert", async () => { + const rectangle = API.createElement({ + id: "rectangle1", + type: "rectangle", + boundElements: [ + { type: "text", id: "text1" }, + { type: "arrow", id: "arrow1" }, + ], + }); + const text = API.createElement({ + id: "text1", + type: "text", + text: "ola", + containerId: "rectangle1", + }); + const arrow = API.createElement({ + id: "arrow1", + type: "arrow", + endBinding: { elementId: "rectangle1", focus: -1, gap: 0 }, + }); + + await API.drop( + new Blob( + [ + serializeLibraryAsJSON([ + { + id: "item1", + status: "published", + elements: [rectangle, text, arrow], + created: 1, + }, + ]), + ], + { + type: MIME_TYPES.excalidrawlib, + }, + ), + ); + + await waitFor(() => { + expect(h.elements).toEqual([ + expect.objectContaining({ + id: "rectangle1_copy", + boundElements: expect.arrayContaining([ + { type: "text", id: "text1_copy" }, + { type: "arrow", id: "arrow1_copy" }, + ]), + }), + expect.objectContaining({ + id: "text1_copy", + containerId: "rectangle1_copy", + }), + expect.objectContaining({ + id: "arrow1_copy", + endBinding: expect.objectContaining({ elementId: "rectangle1_copy" }), + }), + ]); + }); + }); + + it("should fix duplicate ids between items on insert", async () => { + // note, we're not testing for duplicate group ids and such because + // deduplication of that happens upstream in the library component + // which would be very hard to orchestrate in this test + + const elem1 = API.createElement({ + id: "elem1", + type: "rectangle", + }); + const item1: LibraryItem = { + id: "item1", + status: "published", + elements: [elem1], + created: 1, + }; + + await API.drop( + new Blob([serializeLibraryAsJSON([item1, item1])], { + type: MIME_TYPES.excalidrawlib, + }), + ); + + await waitFor(() => { + expect(h.elements).toEqual([ + expect.objectContaining({ + id: "elem1_copy", + }), + expect.objectContaining({ + id: expect.not.stringMatching(/^(elem1_copy|elem1)$/), + }), + ]); + }); + }); + it("inserting library item should revert to selection tool", async () => { UI.clickTool("rectangle"); expect(h.elements).toEqual([]); From c3e8ddaf580f395366ff1fded0c9592a4485c701 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Mon, 17 Apr 2023 11:41:27 +0200 Subject: [PATCH 241/276] fix: improperly cache-busting on canvas scale instead of zoom (#6473) --- src/renderer/renderElement.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index 486e52bdf..21de70cb9 100644 --- a/src/renderer/renderElement.ts +++ b/src/renderer/renderElement.ts @@ -88,6 +88,7 @@ export interface ExcalidrawElementWithCanvas { canvas: HTMLCanvasElement; theme: RenderConfig["theme"]; scale: number; + zoomValue: RenderConfig["zoom"]["value"]; canvasOffsetX: number; canvasOffsetY: number; boundTextElementVersion: number | null; @@ -202,6 +203,7 @@ const generateElementCanvas = ( canvas, theme: renderConfig.theme, scale, + zoomValue: zoom.value, canvasOffsetX, canvasOffsetY, boundTextElementVersion: getBoundTextElement(element)?.version || null, @@ -712,7 +714,7 @@ const generateElementWithCanvas = ( const prevElementWithCanvas = elementWithCanvasCache.get(element); const shouldRegenerateBecauseZoom = prevElementWithCanvas && - prevElementWithCanvas.scale !== zoom.value && + prevElementWithCanvas.zoomValue !== zoom.value && !renderConfig?.shouldCacheIgnoreZoom; const boundTextElementVersion = getBoundTextElement(element)?.version || null; From 21726e22cce04aa2d3bdac7f270e2748156ca40e Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Tue, 18 Apr 2023 18:42:48 +0530 Subject: [PATCH 242/276] fix: mark more props as optional for element (#6448) * fix: mark more props as optional for element * fix --- src/appState.ts | 17 +++++----- src/charts.ts | 18 +---------- src/components/App.tsx | 2 -- src/constants.ts | 23 ++++++++++++- src/element/newElement.ts | 68 +++++++++++++++++++++++++++------------ 5 files changed, 80 insertions(+), 48 deletions(-) diff --git a/src/appState.ts b/src/appState.ts index f02d5943c..6f4db7557 100644 --- a/src/appState.ts +++ b/src/appState.ts @@ -1,5 +1,6 @@ import oc from "open-color"; import { + DEFAULT_ELEMENT_PROPS, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, DEFAULT_TEXT_ALIGN, @@ -23,18 +24,18 @@ export const getDefaultAppState = (): Omit< theme: THEME.LIGHT, collaborators: new Map(), currentChartType: "bar", - currentItemBackgroundColor: "transparent", + currentItemBackgroundColor: DEFAULT_ELEMENT_PROPS.backgroundColor, currentItemEndArrowhead: "arrow", - currentItemFillStyle: "hachure", + currentItemFillStyle: DEFAULT_ELEMENT_PROPS.fillStyle, currentItemFontFamily: DEFAULT_FONT_FAMILY, currentItemFontSize: DEFAULT_FONT_SIZE, - currentItemOpacity: 100, - currentItemRoughness: 1, + currentItemOpacity: DEFAULT_ELEMENT_PROPS.opacity, + currentItemRoughness: DEFAULT_ELEMENT_PROPS.roughness, currentItemStartArrowhead: null, - currentItemStrokeColor: oc.black, + currentItemStrokeColor: DEFAULT_ELEMENT_PROPS.strokeColor, currentItemRoundness: "round", - currentItemStrokeStyle: "solid", - currentItemStrokeWidth: 1, + currentItemStrokeStyle: DEFAULT_ELEMENT_PROPS.strokeStyle, + currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth, currentItemTextAlign: DEFAULT_TEXT_ALIGN, cursorButton: "up", draggingElement: null, @@ -44,7 +45,7 @@ export const getDefaultAppState = (): Omit< activeTool: { type: "selection", customType: null, - locked: false, + locked: DEFAULT_ELEMENT_PROPS.locked, lastActiveTool: null, }, penMode: false, diff --git a/src/charts.ts b/src/charts.ts index e8980db6c..c3b0950d1 100644 --- a/src/charts.ts +++ b/src/charts.ts @@ -1,10 +1,5 @@ import colors from "./colors"; -import { - DEFAULT_FONT_FAMILY, - DEFAULT_FONT_SIZE, - ENV, - VERTICAL_ALIGN, -} from "./constants"; +import { DEFAULT_FONT_SIZE, ENV } from "./constants"; import { newElement, newLinearElement, newTextElement } from "./element"; import { NonDeletedExcalidrawElement } from "./element/types"; import { randomId } from "./random"; @@ -166,17 +161,7 @@ const bgColors = colors.elementBackground.slice( // Put all the common properties here so when the whole chart is selected // the properties dialog shows the correct selected values const commonProps = { - fillStyle: "hachure", - fontFamily: DEFAULT_FONT_FAMILY, - fontSize: DEFAULT_FONT_SIZE, - opacity: 100, - roughness: 1, strokeColor: colors.elementStroke[0], - roundness: null, - strokeStyle: "solid", - strokeWidth: 1, - verticalAlign: VERTICAL_ALIGN.MIDDLE, - locked: false, } as const; const getChartDimentions = (spreadsheet: Spreadsheet) => { @@ -323,7 +308,6 @@ const chartBaseElements = ( x: x + chartWidth / 2, y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE, roundness: null, - strokeStyle: "solid", textAlign: "center", }) : null; diff --git a/src/components/App.tsx b/src/components/App.tsx index be5561995..fcbcca873 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -2732,7 +2732,6 @@ class App extends React.Component { strokeStyle: this.state.currentItemStrokeStyle, roughness: this.state.currentItemRoughness, opacity: this.state.currentItemOpacity, - roundness: null, text: "", fontSize, fontFamily, @@ -2744,7 +2743,6 @@ class App extends React.Component { : DEFAULT_VERTICAL_ALIGN, containerId: shouldBindToContainer ? container?.id : undefined, groupIds: container?.groupIds ?? [], - locked: false, lineHeight, }); diff --git a/src/constants.ts b/src/constants.ts index ef563e4a4..23fefa6e5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,6 +1,7 @@ import cssVariables from "./css/variables.module.scss"; import { AppProps } from "./types"; -import { FontFamilyValues } from "./element/types"; +import { ExcalidrawElement, FontFamilyValues } from "./element/types"; +import oc from "open-color"; export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform); export const isWindows = /^Win/.test(navigator.platform); @@ -254,3 +255,23 @@ export const ROUNDNESS = { /** key containt id of precedeing elemnt id we use in reconciliation during * collaboration */ export const PRECEDING_ELEMENT_KEY = "__precedingElement__"; + +export const DEFAULT_ELEMENT_PROPS: { + strokeColor: ExcalidrawElement["strokeColor"]; + backgroundColor: ExcalidrawElement["backgroundColor"]; + fillStyle: ExcalidrawElement["fillStyle"]; + strokeWidth: ExcalidrawElement["strokeWidth"]; + strokeStyle: ExcalidrawElement["strokeStyle"]; + roughness: ExcalidrawElement["roughness"]; + opacity: ExcalidrawElement["opacity"]; + locked: ExcalidrawElement["locked"]; +} = { + strokeColor: oc.black, + backgroundColor: "transparent", + fillStyle: "hachure", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + locked: false, +}; diff --git a/src/element/newElement.ts b/src/element/newElement.ts index e3b25e848..36c8cc0e0 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -36,7 +36,14 @@ import { getMaxContainerWidth, getDefaultLineHeight, } from "./textElement"; -import { VERTICAL_ALIGN } from "../constants"; +import { + DEFAULT_ELEMENT_PROPS, + DEFAULT_FONT_FAMILY, + DEFAULT_FONT_SIZE, + DEFAULT_TEXT_ALIGN, + DEFAULT_VERTICAL_ALIGN, + VERTICAL_ALIGN, +} from "../constants"; import { isArrowElement } from "./typeChecks"; import { MarkOptional, Merge, Mutable } from "../utility-types"; @@ -51,6 +58,15 @@ type ElementConstructorOpts = MarkOptional< | "version" | "versionNonce" | "link" + | "strokeStyle" + | "fillStyle" + | "strokeColor" + | "backgroundColor" + | "roughness" + | "strokeWidth" + | "roundness" + | "locked" + | "opacity" >; const _newElementBase = ( @@ -58,13 +74,13 @@ const _newElementBase = ( { x, y, - strokeColor, - backgroundColor, - fillStyle, - strokeWidth, - strokeStyle, - roughness, - opacity, + strokeColor = DEFAULT_ELEMENT_PROPS.strokeColor, + backgroundColor = DEFAULT_ELEMENT_PROPS.backgroundColor, + fillStyle = DEFAULT_ELEMENT_PROPS.fillStyle, + strokeWidth = DEFAULT_ELEMENT_PROPS.strokeWidth, + strokeStyle = DEFAULT_ELEMENT_PROPS.strokeStyle, + roughness = DEFAULT_ELEMENT_PROPS.roughness, + opacity = DEFAULT_ELEMENT_PROPS.opacity, width = 0, height = 0, angle = 0, @@ -72,7 +88,7 @@ const _newElementBase = ( roundness = null, boundElements = null, link = null, - locked, + locked = DEFAULT_ELEMENT_PROPS.locked, ...rest }: ElementConstructorOpts & Omit, "type">, ) => { @@ -138,27 +154,39 @@ const getTextElementPositionOffsets = ( export const newTextElement = ( opts: { text: string; - fontSize: number; - fontFamily: FontFamilyValues; - textAlign: TextAlign; - verticalAlign: VerticalAlign; + fontSize?: number; + fontFamily?: FontFamilyValues; + textAlign?: TextAlign; + verticalAlign?: VerticalAlign; containerId?: ExcalidrawTextContainer["id"]; lineHeight?: ExcalidrawTextElement["lineHeight"]; + strokeWidth?: ExcalidrawTextElement["strokeWidth"]; } & ElementConstructorOpts, ): NonDeleted => { - const lineHeight = opts.lineHeight || getDefaultLineHeight(opts.fontFamily); + const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY; + const fontSize = opts.fontSize || DEFAULT_FONT_SIZE; + const lineHeight = opts.lineHeight || getDefaultLineHeight(fontFamily); const text = normalizeText(opts.text); - const metrics = measureText(text, getFontString(opts), lineHeight); - const offsets = getTextElementPositionOffsets(opts, metrics); + const metrics = measureText( + text, + getFontString({ fontFamily, fontSize }), + lineHeight, + ); + const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN; + const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN; + const offsets = getTextElementPositionOffsets( + { textAlign, verticalAlign }, + metrics, + ); const textElement = newElementWith( { ..._newElementBase("text", opts), text, - fontSize: opts.fontSize, - fontFamily: opts.fontFamily, - textAlign: opts.textAlign, - verticalAlign: opts.verticalAlign, + fontSize, + fontFamily, + textAlign, + verticalAlign, x: opts.x - offsets.x, y: opts.y - offsets.y, width: metrics.width, From 801412bf6bd15b81403ec976a2089f2e303fff45 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Tue, 18 Apr 2023 18:50:25 +0530 Subject: [PATCH 243/276] fix: restore original container height when unbinding text which was binded via context menu (#6444) * fix: restore original container height when unbinding text which was binded via context menu * remove flag * comment --- src/actions/actionBoundText.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/actions/actionBoundText.tsx b/src/actions/actionBoundText.tsx index 3f240b5d8..830af3733 100644 --- a/src/actions/actionBoundText.tsx +++ b/src/actions/actionBoundText.tsx @@ -16,6 +16,7 @@ import { import { getOriginalContainerHeightFromCache, resetOriginalContainerCache, + updateOriginalContainerCache, } from "../element/textWysiwyg"; import { hasBoundTextElement, @@ -145,7 +146,11 @@ export const actionBindText = register({ id: textElement.id, }), }); + const originalContainerHeight = container.height; redrawTextBoundingBox(textElement, container); + // overwritting the cache with original container height so + // it can be restored when unbind + updateOriginalContainerCache(container.id, originalContainerHeight); return { elements: pushTextAboveContainer(elements, container, textElement), From 4d0d844e390e8b41d059f1fe646783c0465d1a41 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Tue, 18 Apr 2023 15:27:51 +0200 Subject: [PATCH 244/276] feat: constrain export dialog preview size (#6475) --- src/components/ImageExportDialog.tsx | 24 +++++++++++------------- src/packages/utils.ts | 6 +++++- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/components/ImageExportDialog.tsx b/src/components/ImageExportDialog.tsx index fb2c1ec81..0e4eff365 100644 --- a/src/components/ImageExportDialog.tsx +++ b/src/components/ImageExportDialog.tsx @@ -4,7 +4,6 @@ import { canvasToBlob } from "../data/blob"; import { NonDeletedExcalidrawElement } from "../element/types"; import { t } from "../i18n"; import { getSelectedElements, isSomeElementSelected } from "../scene"; -import { exportToCanvas } from "../scene/export"; import { AppState, BinaryFiles } from "../types"; import { Dialog } from "./Dialog"; import { clipboard } from "./icons"; @@ -15,6 +14,7 @@ import { CheckboxItem } from "./CheckboxItem"; import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants"; import { nativeFileSystemSupported } from "../data/filesystem"; import { ActionManager } from "../actions/manager"; +import { exportToCanvas } from "../packages/utils"; const supportsContextFilters = "filter" in document.createElement("canvas").getContext("2d")!; @@ -83,7 +83,6 @@ const ImageExportModal = ({ const someElementIsSelected = isSomeElementSelected(elements, appState); const [exportSelected, setExportSelected] = useState(someElementIsSelected); const previewRef = useRef(null); - const { exportBackground, viewBackgroundColor } = appState; const [renderError, setRenderError] = useState(null); const exportedElements = exportSelected @@ -99,10 +98,16 @@ const ImageExportModal = ({ if (!previewNode) { return; } - exportToCanvas(exportedElements, appState, files, { - exportBackground, - viewBackgroundColor, + const maxWidth = previewNode.offsetWidth; + if (!maxWidth) { + return; + } + exportToCanvas({ + elements: exportedElements, + appState, + files, exportPadding, + maxWidthOrHeight: maxWidth, }) .then((canvas) => { setRenderError(null); @@ -116,14 +121,7 @@ const ImageExportModal = ({ console.error(error); setRenderError(error); }); - }, [ - appState, - files, - exportedElements, - exportBackground, - exportPadding, - viewBackgroundColor, - ]); + }, [appState, files, exportedElements, exportPadding]); return (
diff --git a/src/packages/utils.ts b/src/packages/utils.ts index 1fb6cd3d9..560fa13ca 100644 --- a/src/packages/utils.ts +++ b/src/packages/utils.ts @@ -79,7 +79,11 @@ export const exportToCanvas = ({ const max = Math.max(width, height); - const scale = maxWidthOrHeight / max; + // if content is less then maxWidthOrHeight, fallback on supplied scale + const scale = + maxWidthOrHeight < max + ? maxWidthOrHeight / max + : appState?.exportScale ?? 1; canvas.width = width * scale; canvas.height = height * scale; From 979312f779b675b9e6ad917ae0b04512fcfbd783 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Tue, 18 Apr 2023 19:44:14 +0530 Subject: [PATCH 245/276] fix: center align text when wrapped in container via context menu (#6480) * rename action to wrapTextInContainer * fix: center align text when wrapped in container via context menu * revert translation key * fix tests --- src/actions/actionBoundText.tsx | 5 +++-- src/actions/types.ts | 2 +- src/components/App.tsx | 4 ++-- src/element/textWysiwyg.test.tsx | 2 +- src/tests/__snapshots__/contextmenu.test.tsx.snap | 10 +++++----- src/tests/binding.test.tsx | 4 ++-- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/actions/actionBoundText.tsx b/src/actions/actionBoundText.tsx index 830af3733..658bdf8ce 100644 --- a/src/actions/actionBoundText.tsx +++ b/src/actions/actionBoundText.tsx @@ -196,8 +196,8 @@ const pushContainerBelowText = ( return updatedElements; }; -export const actionCreateContainerFromText = register({ - name: "createContainerFromText", +export const actionWrapTextInContainer = register({ + name: "wrapTextInContainer", contextItemLabel: "labels.createContainerFromText", trackEvent: { category: "element" }, predicate: (elements, appState) => { @@ -286,6 +286,7 @@ export const actionCreateContainerFromText = register({ containerId: container.id, verticalAlign: VERTICAL_ALIGN.MIDDLE, boundElements: null, + textAlign: TEXT_ALIGN.CENTER, }, false, ); diff --git a/src/actions/types.ts b/src/actions/types.ts index e46cd2ab8..b03e1053b 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -115,7 +115,7 @@ export type ActionName = | "toggleLinearEditor" | "toggleEraserTool" | "toggleHandTool" - | "createContainerFromText"; + | "wrapTextInContainer"; export type PanelComponentProps = { elements: readonly ExcalidrawElement[]; diff --git a/src/components/App.tsx b/src/components/App.tsx index fcbcca873..20def468a 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -295,7 +295,7 @@ import { } from "../actions/actionCanvas"; import { jotaiStore } from "../jotai"; import { activeConfirmDialogAtom } from "./ActiveConfirmDialog"; -import { actionCreateContainerFromText } from "../actions/actionBoundText"; +import { actionWrapTextInContainer } from "../actions/actionBoundText"; import BraveMeasureTextError from "./BraveMeasureTextError"; const deviceContextInitialValue = { @@ -6364,7 +6364,7 @@ class App extends React.Component { actionGroup, actionUnbindText, actionBindText, - actionCreateContainerFromText, + actionWrapTextInContainer, actionUngroup, CONTEXT_MENU_SEPARATOR, actionAddToLibrary, diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx index 4ae3f26f9..71c75c5a0 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/src/element/textWysiwyg.test.tsx @@ -1506,7 +1506,7 @@ describe("textWysiwyg", () => { expect.objectContaining({ text: "Excalidraw is an opensource virtual collaborative whiteboard", verticalAlign: VERTICAL_ALIGN.MIDDLE, - textAlign: TEXT_ALIGN.LEFT, + textAlign: TEXT_ALIGN.CENTER, boundElements: null, }), ); diff --git a/src/tests/__snapshots__/contextmenu.test.tsx.snap b/src/tests/__snapshots__/contextmenu.test.tsx.snap index 18656edd1..c5b61e42f 100644 --- a/src/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/src/tests/__snapshots__/contextmenu.test.tsx.snap @@ -121,7 +121,7 @@ Object { }, Object { "contextItemLabel": "labels.createContainerFromText", - "name": "createContainerFromText", + "name": "wrapTextInContainer", "perform": [Function], "predicate": [Function], "trackEvent": Object { @@ -4518,7 +4518,7 @@ Object { }, Object { "contextItemLabel": "labels.createContainerFromText", - "name": "createContainerFromText", + "name": "wrapTextInContainer", "perform": [Function], "predicate": [Function], "trackEvent": Object { @@ -5068,7 +5068,7 @@ Object { }, Object { "contextItemLabel": "labels.createContainerFromText", - "name": "createContainerFromText", + "name": "wrapTextInContainer", "perform": [Function], "predicate": [Function], "trackEvent": Object { @@ -5917,7 +5917,7 @@ Object { }, Object { "contextItemLabel": "labels.createContainerFromText", - "name": "createContainerFromText", + "name": "wrapTextInContainer", "perform": [Function], "predicate": [Function], "trackEvent": Object { @@ -6263,7 +6263,7 @@ Object { }, Object { "contextItemLabel": "labels.createContainerFromText", - "name": "createContainerFromText", + "name": "wrapTextInContainer", "perform": [Function], "predicate": [Function], "trackEvent": Object { diff --git a/src/tests/binding.test.tsx b/src/tests/binding.test.tsx index c615eb925..07af36569 100644 --- a/src/tests/binding.test.tsx +++ b/src/tests/binding.test.tsx @@ -4,7 +4,7 @@ import { UI, Pointer, Keyboard } from "./helpers/ui"; import { getTransformHandles } from "../element/transformHandles"; import { API } from "./helpers/api"; import { KEYS } from "../keys"; -import { actionCreateContainerFromText } from "../actions/actionBoundText"; +import { actionWrapTextInContainer } from "../actions/actionBoundText"; const { h } = window; @@ -277,7 +277,7 @@ describe("element binding", () => { expect(h.state.selectedElementIds[text1.id]).toBe(true); - h.app.actionManager.executeAction(actionCreateContainerFromText); + h.app.actionManager.executeAction(actionWrapTextInContainer); // new text container will be placed before the text element const container = h.elements.at(-2)!; From c9c79646c535dbca2342c5f3de26c24c6aadce19 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Tue, 18 Apr 2023 20:48:30 +0530 Subject: [PATCH 246/276] =?UTF-8?q?docs:=20release=20@excalidraw/excalidra?= =?UTF-8?q?w@0.15.0=20=20=F0=9F=8E=89=20(#6481)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/excalidraw/CHANGELOG.md | 150 ++++++++++++++++++++++++++- src/packages/excalidraw/package.json | 2 +- 2 files changed, 150 insertions(+), 2 deletions(-) diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index c36883b8e..5143bda31 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -11,7 +11,7 @@ The change should be grouped under one of the below section and must contain PR Please add the latest change on the top under the correct section. --> -## Unreleased +## 0.15.0 (2023-04-18) ### Features @@ -37,6 +37,154 @@ For more details refer to the [docs](https://docs.excalidraw.com) - Exporting labelled arrows via export utils [#6443](https://github.com/excalidraw/excalidraw/pull/6443) +## Excalidraw Library + +**_This section lists the updates made to the excalidraw library and will not affect the integration._** + +### Features + +- Constrain export dialog preview size [#6475](https://github.com/excalidraw/excalidraw/pull/6475) + +- Zigzag fill easter egg [#6439](https://github.com/excalidraw/excalidraw/pull/6439) + +- Add container to multiple text elements [#6428](https://github.com/excalidraw/excalidraw/pull/6428) + +- Starting migration from GA to Matomo for better privacy [#6398](https://github.com/excalidraw/excalidraw/pull/6398) + +- Add line height attribute to text element [#6360](https://github.com/excalidraw/excalidraw/pull/6360) + +- Add thai lang support [#6314](https://github.com/excalidraw/excalidraw/pull/6314) + +- Create bound container from text [#6301](https://github.com/excalidraw/excalidraw/pull/6301) + +- Improve text measurements in bound containers [#6187](https://github.com/excalidraw/excalidraw/pull/6187) + +- Bind text to container if double clicked on filled shape or stroke [#6250](https://github.com/excalidraw/excalidraw/pull/6250) + +- Make repair and refreshDimensions configurable in restoreElements [#6238](https://github.com/excalidraw/excalidraw/pull/6238) + +- Show error message when not connected to internet while collabo… [#6165](https://github.com/excalidraw/excalidraw/pull/6165) + +- Shortcut for clearCanvas confirmDialog [#6114](https://github.com/excalidraw/excalidraw/pull/6114) + +- Disable canvas smoothing (antialiasing) for right-angled elements [#6186](https://github.com/excalidraw/excalidraw/pull/6186)Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com> + +### Fixes + +- Center align text when wrapped in container via context menu [#6480](https://github.com/excalidraw/excalidraw/pull/6480) + +- Restore original container height when unbinding text which was binded via context menu [#6444](https://github.com/excalidraw/excalidraw/pull/6444) + +- Mark more props as optional for element [#6448](https://github.com/excalidraw/excalidraw/pull/6448) + +- Improperly cache-busting on canvas scale instead of zoom [#6473](https://github.com/excalidraw/excalidraw/pull/6473) + +- Incorrectly duplicating items on paste/library insert [#6467](https://github.com/excalidraw/excalidraw/pull/6467) + +- Library ids cross-contamination on multiple insert [#6466](https://github.com/excalidraw/excalidraw/pull/6466) + +- Color picker keyboard handling not working [#6464](https://github.com/excalidraw/excalidraw/pull/6464) + +- Abort freedraw line if second touch is detected [#6440](https://github.com/excalidraw/excalidraw/pull/6440) + +- Utils leaking Scene state [#6461](https://github.com/excalidraw/excalidraw/pull/6461) + +- Split "Edit selected shape" shortcut [#6457](https://github.com/excalidraw/excalidraw/pull/6457) + +- Center align text when bind to container via context menu [#6451](https://github.com/excalidraw/excalidraw/pull/6451) + +- Update coords when text unbinded from its container [#6445](https://github.com/excalidraw/excalidraw/pull/6445) + +- Autoredirect to plus in prod only [#6446](https://github.com/excalidraw/excalidraw/pull/6446) + +- Fixing popover overflow on small screen [#6433](https://github.com/excalidraw/excalidraw/pull/6433) + +- Introduce baseline to fix the layout shift when switching to text editor [#6397](https://github.com/excalidraw/excalidraw/pull/6397) + +- Don't refresh dimensions for deleted text elements [#6438](https://github.com/excalidraw/excalidraw/pull/6438) + +- Element vanishes when zoomed in [#6417](https://github.com/excalidraw/excalidraw/pull/6417) + +- Don't jump text to end when out of viewport in safari [#6416](https://github.com/excalidraw/excalidraw/pull/6416) + +- GetDefaultLineHeight should return default font family line height for unknown font [#6399](https://github.com/excalidraw/excalidraw/pull/6399) + +- Revert use `ideographic` textBaseline to improve layout shift when editing text" [#6400](https://github.com/excalidraw/excalidraw/pull/6400) + +- Call stack size exceeded when paste large text [#6373](https://github.com/excalidraw/excalidraw/pull/6373) (#6396) + +- Use `ideographic` textBaseline to improve layout shift when editing text [#6384](https://github.com/excalidraw/excalidraw/pull/6384) + +- Chrome crashing when embedding scene on chrome arm [#6383](https://github.com/excalidraw/excalidraw/pull/6383) + +- Division by zero in findFocusPointForEllipse leads to infinite loop in wrapText freezing Excalidraw [#6377](https://github.com/excalidraw/excalidraw/pull/6377) + +- Containerizing text incorrectly updates arrow bindings [#6369](https://github.com/excalidraw/excalidraw/pull/6369) + +- Ensure export preview is centered [#6337](https://github.com/excalidraw/excalidraw/pull/6337) + +- Hide text align for labelled arrows [#6339](https://github.com/excalidraw/excalidraw/pull/6339) + +- Refresh dimensions when elements loaded from shareable link and blob [#6333](https://github.com/excalidraw/excalidraw/pull/6333) + +- Show error message when measureText API breaks in brave [#6336](https://github.com/excalidraw/excalidraw/pull/6336) + +- Add an offset of 0.5px for text editor in containers [#6328](https://github.com/excalidraw/excalidraw/pull/6328) + +- Move utility types out of `.d.ts` file to fix exported declaration files [#6315](https://github.com/excalidraw/excalidraw/pull/6315) + +- More jotai scopes missing [#6313](https://github.com/excalidraw/excalidraw/pull/6313) + +- Provide HelpButton title prop [#6209](https://github.com/excalidraw/excalidraw/pull/6209) + +- Respect text align when wrapping in a container [#6310](https://github.com/excalidraw/excalidraw/pull/6310) + +- Compute bounding box correctly for text element when multiple element resizing [#6307](https://github.com/excalidraw/excalidraw/pull/6307) + +- Use jotai scope for editor-specific atoms [#6308](https://github.com/excalidraw/excalidraw/pull/6308) + +- Consider arrow for bound text element [#6297](https://github.com/excalidraw/excalidraw/pull/6297) + +- Text never goes beyond max width for unbound text elements [#6288](https://github.com/excalidraw/excalidraw/pull/6288) + +- Svg text baseline [#6285](https://github.com/excalidraw/excalidraw/pull/6273) + +- Compute container height from bound text correctly [#6273](https://github.com/excalidraw/excalidraw/pull/6273) + +- Fit mobile toolbar and make scrollable [#6270](https://github.com/excalidraw/excalidraw/pull/6270) + +- Indenting via `tab` clashing with IME compositor [#6258](https://github.com/excalidraw/excalidraw/pull/6258) + +- Improve text wrapping inside rhombus and more fixes [#6265](https://github.com/excalidraw/excalidraw/pull/6265) + +- Improve text wrapping in ellipse and alignment [#6172](https://github.com/excalidraw/excalidraw/pull/6172) + +- Don't allow blank space in collab name [#6211](https://github.com/excalidraw/excalidraw/pull/6211) + +- Docker build architecture:linux/amd64 error occur on linux/arm64 instance [#6197](https://github.com/excalidraw/excalidraw/pull/6197) + +- Sort bound text elements to fix text duplication z-index error [#5130](https://github.com/excalidraw/excalidraw/pull/5130) + +- Hide welcome screen on mobile once user interacts [#6185](https://github.com/excalidraw/excalidraw/pull/6185) + +- Edit link in docs [#6182](https://github.com/excalidraw/excalidraw/pull/6182) + +### Refactor + +- Inline `SingleLibraryItem` into `PublishLibrary` [#6462](https://github.com/excalidraw/excalidraw/pull/6462) + +- Make the example React app reusable without duplication [#6188](https://github.com/excalidraw/excalidraw/pull/6188) + +### Performance + +- Break early if the line width <= max width of the container [#6347](https://github.com/excalidraw/excalidraw/pull/6347) + +### Build + +- Move TS and types to devDependencies [#6346](https://github.com/excalidraw/excalidraw/pull/6346) + +--- + ## 0.14.2 (2023-02-01) ### Features diff --git a/src/packages/excalidraw/package.json b/src/packages/excalidraw/package.json index be4e61d27..2351ee6a5 100644 --- a/src/packages/excalidraw/package.json +++ b/src/packages/excalidraw/package.json @@ -1,6 +1,6 @@ { "name": "@excalidraw/excalidraw", - "version": "0.14.2", + "version": "0.15.0", "main": "main.js", "types": "types/packages/excalidraw/index.d.ts", "files": [ From 1d0653ce50cfc56b92235bf793cfa638f39144c4 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Tue, 18 Apr 2023 21:03:17 +0530 Subject: [PATCH 247/276] docs: update docs for next version (#6251) * update docs for opts param inr estore utils * docs: add docs for `useI18n` hook (#6269) Co-authored-by: Aakansha Doshi * upgrade excal --------- Co-authored-by: David Luzar --- .../excalidraw/api/utils/restore.mdx | 28 +++++++++--- .../excalidraw/api/utils/utils-intro.md | 44 +++++++++++++++++++ dev-docs/package.json | 2 +- dev-docs/src/theme/ReactLiveScope/index.js | 1 + dev-docs/yarn.lock | 8 ++-- 5 files changed, 73 insertions(+), 10 deletions(-) diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx index 198626eec..665a1ef9f 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx @@ -31,10 +31,29 @@ You can pass `null` / `undefined` if not applicable. restoreElements( elements: ImportedDataState["elements"],
  localElements: ExcalidrawElement[] | null | undefined): ExcalidrawElement[],
  - refreshDimensions?: boolean
+ opts: { refreshDimensions?: boolean, repairBindings?: boolean }
) +| Prop | Type | Description | +| ---- | ---- | ---- | +| `elements` | ImportedDataState["elements"] | The `elements` to be restored | +| [`localElements`](#localelements) | ExcalidrawElement[] | null | undefined | When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. | +| [`opts`](#opts) | `Object` | The extra optional parameter to configure restored elements + +#### localElements + +When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. +Use this when you `import` elements which may already be present in the scene to ensure that you do not disregard the newly imported elements if you're using element version to detect the update + +#### opts +The extra optional parameter to configure restored elements. It has the following attributes + +| Prop | Type | Description| +| --- | --- | ------| +| `refreshDimensions` | `boolean` | Indicates whether we should also `recalculate` text element dimensions. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration. | +| `repairBindings` |`boolean` | Indicates whether the `bindings` for the elements should be repaired. This is to make sure there are no containers with non existent bound text element id and no bound text elements with non existent container id. | + **_How to use_** ```js @@ -43,9 +62,6 @@ import { restoreElements } from "@excalidraw/excalidraw"; This function will make sure all properties of element is correctly set and if any attribute is missing, it will be set to its default value. -When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. -Use this when you import elements which may already be present in the scene to ensure that you do not disregard the newly imported elements if you're using element version to detect the updates. - Parameter `refreshDimensions` indicates whether we should also `recalculate` text element dimensions. Defaults to `false`. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration. ### restore @@ -56,7 +72,9 @@ Parameter `refreshDimensions` indicates whether we should also `recalculate` tex restore( data: ImportedDataState,
  localAppState: Partial<AppState> | null | undefined,
  - localElements: ExcalidrawElement[] | null | undefined
): DataState + localElements: ExcalidrawElement[] | null | undefined
): DataState
+ opts: { refreshDimensions?: boolean, repairBindings?: boolean }
+ ) diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md b/dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md index c21592382..4d2745c09 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md +++ b/dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md @@ -339,3 +339,47 @@ The `device` has the following `attributes` | `isMobile` | `boolean` | Set to `true` when the device is `mobile` | | `isTouchScreen` | `boolean` | Set to `true` for `touch` devices | | `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` | + +### i18n + +To help with localization, we export the following. + +| name | type | +| --- | --- | +| `defaultLang` | `string` | +| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) | +| `useI18n` | [`() => { langCode, t }`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) | + +```js +import { defaultLang, languages, useI18n } from "@excalidraw/excalidraw"; +``` + +#### defaultLang + +Default language code, `en`. + +#### languages + +List of supported language codes. You can pass any of these to `Excalidraw`'s [`langCode` prop](/docs/@excalidraw/excalidraw/api/props/#langcode). + +#### useI18n + +A hook that returns the current language code and translation helper function. You can use this to translate strings in the components you render as children of ``. + +```jsx live +function App() { + const { t } = useI18n(); + return ( +
+ + + +
+ ); +} +``` diff --git a/dev-docs/package.json b/dev-docs/package.json index dd3c45872..1e8745910 100644 --- a/dev-docs/package.json +++ b/dev-docs/package.json @@ -18,7 +18,7 @@ "@docusaurus/core": "2.2.0", "@docusaurus/preset-classic": "2.2.0", "@docusaurus/theme-live-codeblock": "2.2.0", - "@excalidraw/excalidraw": "0.14.2", + "@excalidraw/excalidraw": "0.15.0", "@mdx-js/react": "^1.6.22", "clsx": "^1.2.1", "docusaurus-plugin-sass": "0.2.3", diff --git a/dev-docs/src/theme/ReactLiveScope/index.js b/dev-docs/src/theme/ReactLiveScope/index.js index a282ad6f0..e5263e1db 100644 --- a/dev-docs/src/theme/ReactLiveScope/index.js +++ b/dev-docs/src/theme/ReactLiveScope/index.js @@ -24,6 +24,7 @@ const ExcalidrawScope = { Sidebar: ExcalidrawComp.Sidebar, exportToCanvas: ExcalidrawComp.exportToCanvas, initialData, + useI18n: ExcalidrawComp.useI18n, }; export default ExcalidrawScope; diff --git a/dev-docs/yarn.lock b/dev-docs/yarn.lock index 041f39b55..ee3d50cbf 100644 --- a/dev-docs/yarn.lock +++ b/dev-docs/yarn.lock @@ -1631,10 +1631,10 @@ url-loader "^4.1.1" webpack "^5.73.0" -"@excalidraw/excalidraw@0.14.2": - version "0.14.2" - resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.14.2.tgz#150cb4b7a1bf0d11cd64295936c930e7e0db8375" - integrity sha512-8LdjpTBWEK5waDWB7Bt/G9YBI4j0OxkstUhvaDGz7dwQGfzF6FW5CXBoYHNEoX0qmb+Fg/NPOlZ7FrKsrSVCqg== +"@excalidraw/excalidraw@0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.0.tgz#47170de8d3ff006e9d09dfede2815682b0d4485b" + integrity sha512-PJmh1VcuRHG4l+Zgt9qhezxrJ16tYCZFZ8if5IEfmTL9A/7c5mXxY/qrPTqiGlVC7jYs+ciePXQ0YUDzfOfbzw== "@hapi/hoek@^9.0.0": version "9.3.0" From 89304c9f66ac9949edb47d254ae3d99602b2dff4 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Tue, 18 Apr 2023 21:23:47 +0530 Subject: [PATCH 248/276] fix: add readme back to npm package which was mistakenly removed (#6484) * fix: remove update readme script from release * update docs * remove * fix --- scripts/release.js | 20 -------------------- src/packages/excalidraw/CHANGELOG.md | 6 ++++++ 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/scripts/release.js b/scripts/release.js index 986eadc2a..24ac89c6b 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -1,22 +1,9 @@ -const fs = require("fs"); const { execSync } = require("child_process"); const excalidrawDir = `${__dirname}/../src/packages/excalidraw`; const excalidrawPackage = `${excalidrawDir}/package.json`; const pkg = require(excalidrawPackage); -const originalReadMe = fs.readFileSync(`${excalidrawDir}/README.md`, "utf8"); - -const updateReadme = () => { - const excalidrawIndex = originalReadMe.indexOf("### Excalidraw"); - - // remove note for stable readme - const data = originalReadMe.slice(excalidrawIndex); - - // update readme - fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8"); -}; - const publish = () => { try { execSync(`yarn --frozen-lockfile`); @@ -30,15 +17,8 @@ const publish = () => { }; const release = () => { - updateReadme(); - console.info("Note for stable readme removed"); - publish(); console.info(`Published ${pkg.version}!`); - - // revert readme after release - fs.writeFileSync(`${excalidrawDir}/README.md`, originalReadMe, "utf8"); - console.info("Readme reverted"); }; release(); diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 5143bda31..b2ac3a682 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -11,6 +11,12 @@ The change should be grouped under one of the below section and must contain PR Please add the latest change on the top under the correct section. --> +## Unreleased + +### Docs + +- Add the readme back to the package which was mistakenly removed [#6484](https://github.com/excalidraw/excalidraw/pull/6484) + ## 0.15.0 (2023-04-18) ### Features From b64beaf5ba42ab55302fb9f5b752b06b28c0c213 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Tue, 18 Apr 2023 21:32:33 +0530 Subject: [PATCH 249/276] =?UTF-8?q?docs:=20release=20@excalidraw/excalidra?= =?UTF-8?q?w@0.15.1=20=20=F0=9F=8E=89=20(#6485)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/excalidraw/CHANGELOG.md | 8 +++++++- src/packages/excalidraw/package.json | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index b2ac3a682..f285349f0 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -11,12 +11,18 @@ The change should be grouped under one of the below section and must contain PR Please add the latest change on the top under the correct section. --> -## Unreleased +## 0.15.1 (2023-04-18) ### Docs - Add the readme back to the package which was mistakenly removed [#6484](https://github.com/excalidraw/excalidraw/pull/6484) +## Excalidraw Library + +**_This section lists the updates made to the excalidraw library and will not affect the integration._** + +--- + ## 0.15.0 (2023-04-18) ### Features diff --git a/src/packages/excalidraw/package.json b/src/packages/excalidraw/package.json index 2351ee6a5..a59837454 100644 --- a/src/packages/excalidraw/package.json +++ b/src/packages/excalidraw/package.json @@ -1,6 +1,6 @@ { "name": "@excalidraw/excalidraw", - "version": "0.15.0", + "version": "0.15.1", "main": "main.js", "types": "types/packages/excalidraw/index.d.ts", "files": [ From ff3c2e5a160d2982d48657091f1fbcaacf82a6e2 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Tue, 18 Apr 2023 21:52:08 +0530 Subject: [PATCH 250/276] docs: fix docs link in readme (#6486) * docs: fix docs link in readme * update changelog --- src/packages/excalidraw/CHANGELOG.md | 6 ++++++ src/packages/excalidraw/README.md | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index f285349f0..e69f05d0a 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -11,6 +11,12 @@ The change should be grouped under one of the below section and must contain PR Please add the latest change on the top under the correct section. --> +## Unreleased + +### Docs + +- Fix docs link in readme [#6486](https://github.com/excalidraw/excalidraw/pull/6486) + ## 0.15.1 (2023-04-18) ### Docs diff --git a/src/packages/excalidraw/README.md b/src/packages/excalidraw/README.md index eaeef4b0c..d650885df 100644 --- a/src/packages/excalidraw/README.md +++ b/src/packages/excalidraw/README.md @@ -38,8 +38,8 @@ Excalidraw takes _100%_ of `width` and `height` of the containing block so make ## Integration -Head over to the [docs](https://docs.excalidraw.com/docs/package/integration) +Head over to the [docs](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/integration) ## API -Head over to the [docs](https://docs.excalidraw.com/docs/package/api) +Head over to the [docs](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api) From 98a77d7426ea3de9c26418f35ef9c6389f3e1d25 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Wed, 19 Apr 2023 17:02:20 +0530 Subject: [PATCH 251/276] chore: show bounding box only when flag is true (#6490) --- src/renderer/renderElement.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index 21de70cb9..f85c83a6b 100644 --- a/src/renderer/renderElement.ts +++ b/src/renderer/renderElement.ts @@ -864,7 +864,8 @@ const drawElementFromCanvas = ( ); if ( - process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX && + process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX === + "true" && hasBoundTextElement(element) ) { const coords = getContainerCoords(element); From eea30da05a5847e28df7f56051575c5daf7ae49e Mon Sep 17 00:00:00 2001 From: David Luzar Date: Wed, 19 Apr 2023 16:23:24 +0200 Subject: [PATCH 252/276] fix: incorrect background fill button active state (#6491) --- src/actions/actionProperties.tsx | 12 +++++++----- src/locales/en.json | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/actions/actionProperties.tsx b/src/actions/actionProperties.tsx index ed714816b..382e964b9 100644 --- a/src/actions/actionProperties.tsx +++ b/src/actions/actionProperties.tsx @@ -84,7 +84,7 @@ import { isSomeElementSelected, } from "../scene"; import { hasStrokeColor } from "../scene/comparisons"; -import { arrayToMap } from "../utils"; +import { arrayToMap, getShortcutKey } from "../utils"; import { register } from "./register"; const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1; @@ -314,9 +314,9 @@ export const actionChangeFillStyle = register({ }, PanelComponent: ({ elements, appState, updateData }) => { const selectedElements = getSelectedElements(elements, appState); - const allElementsZigZag = selectedElements.every( - (el) => el.fillStyle === "zigzag", - ); + const allElementsZigZag = + selectedElements.length > 0 && + selectedElements.every((el) => el.fillStyle === "zigzag"); return (
@@ -326,7 +326,9 @@ export const actionChangeFillStyle = register({ options={[ { value: "hachure", - text: t("labels.hachure"), + text: `${ + allElementsZigZag ? t("labels.zigzag") : t("labels.hachure") + } (${getShortcutKey("Alt-Click")})`, icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon, active: allElementsZigZag ? true : undefined, }, diff --git a/src/locales/en.json b/src/locales/en.json index 8752d415a..7e250a800 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -54,6 +54,7 @@ "veryLarge": "Very large", "solid": "Solid", "hachure": "Hachure", + "zigzag": "Zigzag", "crossHatch": "Cross-hatch", "thin": "Thin", "bold": "Bold", From 404a79e241e4aa2eda287513e9cef497e40da832 Mon Sep 17 00:00:00 2001 From: Max Kovalenko Date: Wed, 19 Apr 2023 13:18:03 -0400 Subject: [PATCH 253/276] chore: typo (collab) - reconciliation.ts (#6447) --- src/excalidraw-app/collab/reconciliation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/excalidraw-app/collab/reconciliation.ts b/src/excalidraw-app/collab/reconciliation.ts index 76b6f052a..3f50bc358 100644 --- a/src/excalidraw-app/collab/reconciliation.ts +++ b/src/excalidraw-app/collab/reconciliation.ts @@ -65,7 +65,7 @@ export const reconcileElements = ( // Mark duplicate for removal as it'll be replaced with the remote element if (local) { - // Unless the ramote and local elements are the same element in which case + // Unless the remote and local elements are the same element in which case // we need to keep it as we'd otherwise discard it from the resulting // array. if (local[0] === remoteElement) { From 5ddb28d37830174798ad3a4e7b1ce319ea8b1cfb Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Thu, 20 Apr 2023 11:10:46 +0530 Subject: [PATCH 254/276] fix: support breaking words containing hyphen - (#6014) * fix: support breaking words containing hyphen - * fix * add spec * fix * fix * fix * fix and add spec * improve code and add more tests --- src/element/textElement.test.ts | 51 +++++++++++++++++++++++++++++++++ src/element/textElement.ts | 47 ++++++++++++++++++++++++------ 2 files changed, 89 insertions(+), 9 deletions(-) diff --git a/src/element/textElement.test.ts b/src/element/textElement.test.ts index 106ed7bea..f83eafd1b 100644 --- a/src/element/textElement.test.ts +++ b/src/element/textElement.test.ts @@ -9,6 +9,7 @@ import { detectLineHeight, getLineHeightInPx, getDefaultLineHeight, + parseTokens, } from "./textElement"; import { FontString } from "./types"; @@ -183,6 +184,56 @@ now`, expect(wrapText(text, font, -1)).toEqual(text); expect(wrapText(text, font, Infinity)).toEqual(text); }); + + it("should wrap the text correctly when text contains hyphen", () => { + let text = + "Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects"; + const res = wrapText(text, font, 110); + expect(res).toBe( + `Wikipedia \nis hosted \nby \nWikimedia-\nFoundation,\na non-\nprofit \norganizati\non that \nalso hosts\na range-of\nother \nprojects`, + ); + + text = "Hello thereusing-now"; + expect(wrapText(text, font, 100)).toEqual("Hello \nthereusin\ng-now"); + }); +}); + +describe("Test parseTokens", () => { + it("should split into tokens correctly", () => { + let text = "Excalidraw is a virtual collaborative whiteboard"; + expect(parseTokens(text)).toEqual([ + "Excalidraw", + "is", + "a", + "virtual", + "collaborative", + "whiteboard", + ]); + + text = + "Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects"; + expect(parseTokens(text)).toEqual([ + "Wikipedia", + "is", + "hosted", + "by", + "Wikimedia-", + "", + "Foundation,", + "a", + "non-", + "profit", + "organization", + "that", + "also", + "hosts", + "a", + "range-", + "of", + "other", + "projects", + ]); + }); }); describe("Test measureText", () => { diff --git a/src/element/textElement.ts b/src/element/textElement.ts index 38da5df5a..f01ba3e1b 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -419,6 +419,24 @@ export const getTextHeight = ( return getLineHeightInPx(fontSize, lineHeight) * lineCount; }; +export const parseTokens = (text: string) => { + // Splitting words containing "-" as those are treated as separate words + // by css wrapping algorithm eg non-profit => non-, profit + const words = text.split("-"); + if (words.length > 1) { + // non-proft org => ['non-', 'profit org'] + words.forEach((word, index) => { + if (index !== words.length - 1) { + words[index] = word += "-"; + } + }); + } + // Joining the words with space and splitting them again with space to get the + // final list of tokens + // ['non-', 'profit org'] =>,'non- proft org' => ['non-','profit','org'] + return words.join(" ").split(" "); +}; + export const wrapText = (text: string, font: FontString, maxWidth: number) => { // if maxWidth is not finite or NaN which can happen in case of bugs in // computation, we need to make sure we don't continue as we'll end up @@ -444,17 +462,16 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => { currentLine = ""; currentLineWidthTillNow = 0; }; - originalLines.forEach((originalLine) => { const currentLineWidth = getTextWidth(originalLine, font); - //Push the line if its <= maxWidth + // Push the line if its <= maxWidth if (currentLineWidth <= maxWidth) { lines.push(originalLine); return; // continue } - const words = originalLine.split(" "); + const words = parseTokens(originalLine); resetParams(); let index = 0; @@ -472,6 +489,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => { else if (currentWordWidth > maxWidth) { // push current line since the current word exceeds the max width // so will be appended in next line + push(currentLine); resetParams(); @@ -492,15 +510,15 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => { currentLine += currentChar; } } - // push current line if appending space exceeds max width if (currentLineWidthTillNow + spaceWidth >= maxWidth) { push(currentLine); resetParams(); - } else { // space needs to be appended before next word // as currentLine contains chars which couldn't be appended - // to previous line + // to previous line unless the line ends with hyphen to sync + // with css word-wrap + } else if (!currentLine.endsWith("-")) { currentLine += " "; currentLineWidthTillNow += spaceWidth; } @@ -518,12 +536,23 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => { break; } index++; - currentLine += `${word} `; + + // if word ends with "-" then we don't need to add space + // to sync with css word-wrap + const shouldAppendSpace = !word.endsWith("-"); + currentLine += word; + + if (shouldAppendSpace) { + currentLine += " "; + } // Push the word if appending space exceeds max width if (currentLineWidthTillNow + spaceWidth >= maxWidth) { - const word = currentLine.slice(0, -1); - push(word); + if (shouldAppendSpace) { + lines.push(currentLine.slice(0, -1)); + } else { + lines.push(currentLine); + } resetParams(); break; } From 851b9b7aecb97bee6a55da669f4a431a3fca733c Mon Sep 17 00:00:00 2001 From: siddhant <30566406+siddhant1@users.noreply.github.com> Date: Thu, 20 Apr 2023 16:34:39 +0530 Subject: [PATCH 255/276] fix: rotate the text element when binding to a rotated container (#6477) * Updated logic to update the bound child angle from the parent * update angle when generating text element * add test * remove * fix --------- Co-authored-by: Aakansha Doshi --- src/components/App.tsx | 1 + src/element/textWysiwyg.test.tsx | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/components/App.tsx b/src/components/App.tsx index 20def468a..413a130d8 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -2744,6 +2744,7 @@ class App extends React.Component { containerId: shouldBindToContainer ? container?.id : undefined, groupIds: container?.groupIds ?? [], lineHeight, + angle: container?.angle ?? 0, }); if (!existingTextElement && shouldBindToContainer && container) { diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx index 71c75c5a0..f3c75db8b 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/src/element/textWysiwyg.test.tsx @@ -526,6 +526,36 @@ describe("textWysiwyg", () => { ]); }); + it("should set the text element angle to same as container angle when binding to rotated container", async () => { + const rectangle = API.createElement({ + type: "rectangle", + width: 90, + height: 75, + angle: 45, + }); + h.elements = [rectangle]; + mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10); + const text = h.elements[1] as ExcalidrawTextElementWithContainer; + expect(text.type).toBe("text"); + expect(text.containerId).toBe(rectangle.id); + expect(rectangle.boundElements).toStrictEqual([ + { id: text.id, type: "text" }, + ]); + expect(text.angle).toBe(rectangle.angle); + mouse.down(); + const editor = document.querySelector( + ".excalidraw-textEditorContainer > textarea", + ) as HTMLTextAreaElement; + + fireEvent.change(editor, { target: { value: "Hello World!" } }); + + await new Promise((r) => setTimeout(r, 0)); + editor.blur(); + expect(rectangle.boundElements).toStrictEqual([ + { id: text.id, type: "text" }, + ]); + }); + it("should compute the container height correctly and not throw error when height is updated while editing the text", async () => { const diamond = API.createElement({ type: "diamond", From 9368a9ce3ec3a49d92b0d8cf2a6910d94d7b6191 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Apr 2023 17:05:03 +0530 Subject: [PATCH 256/276] build(deps): bump webpack from 5.75.0 to 5.76.1 (#6357) Bumps [webpack](https://github.com/webpack/webpack) from 5.75.0 to 5.76.1. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.75.0...v5.76.1) --- updated-dependencies: - dependency-name: webpack dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 31624f92b..89d153909 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10601,9 +10601,9 @@ webpack-sources@^3.2.3: integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== webpack@^5.64.4: - version "5.75.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.75.0.tgz#1e440468647b2505860e94c9ff3e44d5b582c152" - integrity sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ== + version "5.76.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.1.tgz#7773de017e988bccb0f13c7d75ec245f377d295c" + integrity sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ== dependencies: "@types/eslint-scope" "^3.7.3" "@types/estree" "^0.0.51" From d12a9fdd40a10394a2f78f8cef825633ad0f0b87 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Apr 2023 17:07:56 +0530 Subject: [PATCH 257/276] build(deps): bump ua-parser-js from 0.7.31 to 0.7.33 in /dev-docs (#6164) Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.31 to 0.7.33. - [Release notes](https://github.com/faisalman/ua-parser-js/releases) - [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md) - [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.31...0.7.33) --- updated-dependencies: - dependency-name: ua-parser-js dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dev-docs/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dev-docs/yarn.lock b/dev-docs/yarn.lock index ee3d50cbf..1b547bbbc 100644 --- a/dev-docs/yarn.lock +++ b/dev-docs/yarn.lock @@ -7159,9 +7159,9 @@ typescript@^4.7.4: integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== ua-parser-js@^0.7.30: - version "0.7.31" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" - integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ== + version "0.7.33" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532" + integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw== unescape@^1.0.1: version "1.0.1" From c4445c181b9f1e87b346a38a6d3e39ed1e1e67b1 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Thu, 20 Apr 2023 17:34:06 +0530 Subject: [PATCH 258/276] =?UTF-8?q?docs:=20release=20@excalidraw/excalidra?= =?UTF-8?q?w@0.15.2=20=20=F0=9F=8E=89=20(#6495)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/excalidraw/CHANGELOG.md | 16 +++++++++++++++- src/packages/excalidraw/package.json | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index e69f05d0a..a2f7466b8 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -11,12 +11,26 @@ The change should be grouped under one of the below section and must contain PR Please add the latest change on the top under the correct section. --> -## Unreleased +## 0.15.2 (2023-04-20) ### Docs - Fix docs link in readme [#6486](https://github.com/excalidraw/excalidraw/pull/6486) +## Excalidraw Library + +**_This section lists the updates made to the excalidraw library and will not affect the integration._** + +### Fixes + +- Rotate the text element when binding to a rotated container [#6477](https://github.com/excalidraw/excalidraw/pull/6477) + +- Support breaking words containing hyphen - [#6014](https://github.com/excalidraw/excalidraw/pull/6014) + +- Incorrect background fill button active state [#6491](https://github.com/excalidraw/excalidraw/pull/6491) + +--- + ## 0.15.1 (2023-04-18) ### Docs diff --git a/src/packages/excalidraw/package.json b/src/packages/excalidraw/package.json index a59837454..57f6ce395 100644 --- a/src/packages/excalidraw/package.json +++ b/src/packages/excalidraw/package.json @@ -1,6 +1,6 @@ { "name": "@excalidraw/excalidraw", - "version": "0.15.1", + "version": "0.15.2", "main": "main.js", "types": "types/packages/excalidraw/index.d.ts", "files": [ From 2a4799d8c86964a3e3fd6f08defc3452c033d8c9 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Thu, 20 Apr 2023 17:40:08 +0530 Subject: [PATCH 259/276] chore: upgrade excalidraw version to 0.15.2 (#6496) chore: updragde excalidraw version to 0.15.2 --- dev-docs/package.json | 2 +- dev-docs/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dev-docs/package.json b/dev-docs/package.json index 1e8745910..0aee8e01f 100644 --- a/dev-docs/package.json +++ b/dev-docs/package.json @@ -18,7 +18,7 @@ "@docusaurus/core": "2.2.0", "@docusaurus/preset-classic": "2.2.0", "@docusaurus/theme-live-codeblock": "2.2.0", - "@excalidraw/excalidraw": "0.15.0", + "@excalidraw/excalidraw": "0.15.2", "@mdx-js/react": "^1.6.22", "clsx": "^1.2.1", "docusaurus-plugin-sass": "0.2.3", diff --git a/dev-docs/yarn.lock b/dev-docs/yarn.lock index 1b547bbbc..6206a60e9 100644 --- a/dev-docs/yarn.lock +++ b/dev-docs/yarn.lock @@ -1631,10 +1631,10 @@ url-loader "^4.1.1" webpack "^5.73.0" -"@excalidraw/excalidraw@0.15.0": - version "0.15.0" - resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.0.tgz#47170de8d3ff006e9d09dfede2815682b0d4485b" - integrity sha512-PJmh1VcuRHG4l+Zgt9qhezxrJ16tYCZFZ8if5IEfmTL9A/7c5mXxY/qrPTqiGlVC7jYs+ciePXQ0YUDzfOfbzw== +"@excalidraw/excalidraw@0.15.2": + version "0.15.2" + resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2.tgz#7dba4f6e10c52015a007efb75a9fc1afe598574c" + integrity sha512-rTI02kgWSTXiUdIkBxt9u/581F3eXcqQgJdIxmz54TFtG3ughoxO5fr4t7Fr2LZIturBPqfocQHGKZ0t2KLKgw== "@hapi/hoek@^9.0.0": version "9.3.0" From fee760d38c18c4d90c053b98d7cfa355707fc427 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Fri, 21 Apr 2023 22:53:49 +0200 Subject: [PATCH 260/276] feat: allow `avif`, `jfif`, `webp`, `bmp`, `ico` image types (#6500 * feat: allow `avif`, `jfif`, `webp`, `bmp`, `ico` image types * dedupe for SSOT * more SSOT --- src/components/App.tsx | 5 ++++- src/constants.ts | 32 ++++++++++++++++---------------- src/data/blob.ts | 8 +++----- src/data/filesystem.ts | 11 +---------- src/types.ts | 8 ++++---- 5 files changed, 28 insertions(+), 36 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 413a130d8..8000ea560 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -60,6 +60,7 @@ import { ENV, EVENT, GRID_SIZE, + IMAGE_MIME_TYPES, IMAGE_RENDER_TIMEOUT, isAndroid, isBrave, @@ -5743,7 +5744,9 @@ class App extends React.Component { const imageFile = await fileOpen({ description: "Image", - extensions: ["jpg", "png", "svg", "gif"], + extensions: Object.keys( + IMAGE_MIME_TYPES, + ) as (keyof typeof IMAGE_MIME_TYPES)[], }); const imageElement = this.createImageElement({ diff --git a/src/constants.ts b/src/constants.ts index 23fefa6e5..19b41b688 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -105,20 +105,30 @@ export const CANVAS_ONLY_ACTIONS = ["selectAll"]; export const GRID_SIZE = 20; // TODO make it configurable? -export const MIME_TYPES = { - excalidraw: "application/vnd.excalidraw+json", - excalidrawlib: "application/vnd.excalidrawlib+json", - json: "application/json", +export const IMAGE_MIME_TYPES = { svg: "image/svg+xml", - "excalidraw.svg": "image/svg+xml", png: "image/png", - "excalidraw.png": "image/png", jpg: "image/jpeg", gif: "image/gif", webp: "image/webp", bmp: "image/bmp", ico: "image/x-icon", + avif: "image/avif", + jfif: "image/jfif", +} as const; + +export const MIME_TYPES = { + json: "application/json", + // excalidraw data + excalidraw: "application/vnd.excalidraw+json", + excalidrawlib: "application/vnd.excalidrawlib+json", + // image-encoded excalidraw data + "excalidraw.svg": "image/svg+xml", + "excalidraw.png": "image/png", + // binary binary: "application/octet-stream", + // image + ...IMAGE_MIME_TYPES, } as const; export const EXPORT_DATA_TYPES = { @@ -189,16 +199,6 @@ export const DEFAULT_EXPORT_PADDING = 10; // px export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440; -export const ALLOWED_IMAGE_MIME_TYPES = [ - MIME_TYPES.png, - MIME_TYPES.jpg, - MIME_TYPES.svg, - MIME_TYPES.gif, - MIME_TYPES.webp, - MIME_TYPES.bmp, - MIME_TYPES.ico, -] as const; - export const MAX_ALLOWED_FILE_BYTES = 2 * 1024 * 1024; export const SVG_NS = "http://www.w3.org/2000/svg"; diff --git a/src/data/blob.ts b/src/data/blob.ts index 47cff293f..4565b5cb5 100644 --- a/src/data/blob.ts +++ b/src/data/blob.ts @@ -1,6 +1,6 @@ import { nanoid } from "nanoid"; import { cleanAppStateForExport } from "../appState"; -import { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "../constants"; +import { IMAGE_MIME_TYPES, MIME_TYPES } from "../constants"; import { clearElementsForExport } from "../element"; import { ExcalidrawElement, FileId } from "../element/types"; import { CanvasError } from "../errors"; @@ -117,11 +117,9 @@ export const isImageFileHandle = (handle: FileSystemHandle | null) => { export const isSupportedImageFile = ( blob: Blob | null | undefined, -): blob is Blob & { type: typeof ALLOWED_IMAGE_MIME_TYPES[number] } => { +): blob is Blob & { type: ValueOf } => { const { type } = blob || {}; - return ( - !!type && (ALLOWED_IMAGE_MIME_TYPES as readonly string[]).includes(type) - ); + return !!type && (Object.values(IMAGE_MIME_TYPES) as string[]).includes(type); }; export const loadSceneOrLibraryFromBlob = async ( diff --git a/src/data/filesystem.ts b/src/data/filesystem.ts index ffe088faf..fa29604f4 100644 --- a/src/data/filesystem.ts +++ b/src/data/filesystem.ts @@ -8,16 +8,7 @@ import { EVENT, MIME_TYPES } from "../constants"; import { AbortError } from "../errors"; import { debounce } from "../utils"; -type FILE_EXTENSION = - | "gif" - | "jpg" - | "png" - | "excalidraw.png" - | "svg" - | "excalidraw.svg" - | "json" - | "excalidraw" - | "excalidrawlib"; +type FILE_EXTENSION = Exclude; const INPUT_CHANGE_INTERVAL_MS = 500; diff --git a/src/types.ts b/src/types.ts index 09848df1d..e5ad01b59 100644 --- a/src/types.ts +++ b/src/types.ts @@ -29,9 +29,9 @@ import { isOverScrollBars } from "./scene"; import { MaybeTransformHandleType } from "./element/transformHandles"; import Library from "./data/library"; import type { FileSystemHandle } from "./data/filesystem"; -import type { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants"; +import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants"; import { ContextMenuItems } from "./components/ContextMenu"; -import { Merge, ForwardRef } from "./utility-types"; +import { Merge, ForwardRef, ValueOf } from "./utility-types"; import React from "react"; export type Point = Readonly; @@ -60,7 +60,7 @@ export type DataURL = string & { _brand: "DataURL" }; export type BinaryFileData = { mimeType: - | typeof ALLOWED_IMAGE_MIME_TYPES[number] + | ValueOf // future user or unknown file type | typeof MIME_TYPES.binary; id: FileId; @@ -419,7 +419,7 @@ export type AppClassProperties = { FileId, { image: HTMLImageElement | Promise; - mimeType: typeof ALLOWED_IMAGE_MIME_TYPES[number]; + mimeType: ValueOf; } >; files: BinaryFiles; From 9d5cfbbfb73c9abcf9ae534eb61548a9aa6fe225 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sat, 22 Apr 2023 14:17:13 +0200 Subject: [PATCH 261/276] fix: text jumps when editing on Android Chrome (#6503) * debug logging * debug * debugging * Update textWysiwyg.tsx * Update textWysiwyg.tsx * extended debug information * debug * debug * trace * further debug * don't drag while editing * removing all console.logs * revert all changes to textWysiwyt.tsx * updated comment --- src/components/App.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 8000ea560..546261436 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -4721,7 +4721,12 @@ class App extends React.Component { pointerDownState.drag.hasOccurred = true; // prevent dragging even if we're no longer holding cmd/ctrl otherwise // it would have weird results (stuff jumping all over the screen) - if (selectedElements.length > 0 && !pointerDownState.withCmdOrCtrl) { + // Checking for editingElement to avoid jump while editing on mobile #6503 + if ( + selectedElements.length > 0 && + !pointerDownState.withCmdOrCtrl && + !this.state.editingElement + ) { const [dragX, dragY] = getGridPoint( pointerCoords.x - pointerDownState.drag.offset.x, pointerCoords.y - pointerDownState.drag.offset.y, From d35386755f951ae7612c2c793ad11d79503e3482 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Mon, 24 Apr 2023 10:26:21 +0200 Subject: [PATCH 262/276] feat: retain `seed` on shift-paste (#6509) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit thanks for the review 👍 --- src/actions/actionClipboard.tsx | 2 +- src/clipboard.ts | 38 ++++++++++++------ src/components/App.tsx | 5 +++ src/components/LibraryMenuItems.tsx | 2 +- src/element/newElement.ts | 17 +++++++- src/packages/utils.ts | 10 +---- src/tests/clipboard.test.tsx | 61 ++++++++++++++++++++--------- src/tests/flip.test.tsx | 21 ++++------ src/tests/test-utils.ts | 21 ++++++++++ 9 files changed, 120 insertions(+), 57 deletions(-) diff --git a/src/actions/actionClipboard.tsx b/src/actions/actionClipboard.tsx index 661f65f38..18fefafd2 100644 --- a/src/actions/actionClipboard.tsx +++ b/src/actions/actionClipboard.tsx @@ -18,7 +18,7 @@ export const actionCopy = register({ perform: (elements, appState, _, app) => { const selectedElements = getSelectedElements(elements, appState, true); - copyToClipboard(selectedElements, appState, app.files); + copyToClipboard(selectedElements, app.files); return { commitToHistory: false, diff --git a/src/clipboard.ts b/src/clipboard.ts index 5f7950c53..c0f5844dd 100644 --- a/src/clipboard.ts +++ b/src/clipboard.ts @@ -2,12 +2,12 @@ import { ExcalidrawElement, NonDeletedExcalidrawElement, } from "./element/types"; -import { AppState, BinaryFiles } from "./types"; +import { BinaryFiles } from "./types"; import { SVG_EXPORT_TAG } from "./scene/export"; import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts"; import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants"; import { isInitializedImageElement } from "./element/typeChecks"; -import { isPromiseLike } from "./utils"; +import { isPromiseLike, isTestEnv } from "./utils"; type ElementsClipboard = { type: typeof EXPORT_DATA_TYPES.excalidrawClipboard; @@ -55,24 +55,40 @@ const clipboardContainsElements = ( export const copyToClipboard = async ( elements: readonly NonDeletedExcalidrawElement[], - appState: AppState, files: BinaryFiles | null, ) => { + let foundFile = false; + + const _files = elements.reduce((acc, element) => { + if (isInitializedImageElement(element)) { + foundFile = true; + if (files && files[element.fileId]) { + acc[element.fileId] = files[element.fileId]; + } + } + return acc; + }, {} as BinaryFiles); + + if (foundFile && !files) { + console.warn( + "copyToClipboard: attempting to file element(s) without providing associated `files` object.", + ); + } + // select binded text elements when copying const contents: ElementsClipboard = { type: EXPORT_DATA_TYPES.excalidrawClipboard, elements, - files: files - ? elements.reduce((acc, element) => { - if (isInitializedImageElement(element) && files[element.fileId]) { - acc[element.fileId] = files[element.fileId]; - } - return acc; - }, {} as BinaryFiles) - : undefined, + files: files ? _files : undefined, }; const json = JSON.stringify(contents); + + if (isTestEnv()) { + return json; + } + CLIPBOARD = json; + try { PREFER_APP_CLIPBOARD = false; await copyTextToSystemClipboard(json); diff --git a/src/components/App.tsx b/src/components/App.tsx index 546261436..d22a0507c 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1590,6 +1590,7 @@ class App extends React.Component { elements: data.elements, files: data.files || null, position: "cursor", + retainSeed: isPlainPaste, }); } else if (data.text) { this.addTextFromPaste(data.text, isPlainPaste); @@ -1603,6 +1604,7 @@ class App extends React.Component { elements: readonly ExcalidrawElement[]; files: BinaryFiles | null; position: { clientX: number; clientY: number } | "cursor" | "center"; + retainSeed?: boolean; }) => { const elements = restoreElements(opts.elements, null); const [minX, minY, maxX, maxY] = getCommonBounds(elements); @@ -1640,6 +1642,9 @@ class App extends React.Component { y: element.y + gridY - minY, }); }), + { + randomizeSeed: !opts.retainSeed, + }, ); const nextElements = [ diff --git a/src/components/LibraryMenuItems.tsx b/src/components/LibraryMenuItems.tsx index 7ae6517a8..19bb33308 100644 --- a/src/components/LibraryMenuItems.tsx +++ b/src/components/LibraryMenuItems.tsx @@ -102,7 +102,7 @@ const LibraryMenuItems = ({ ...item, // duplicate each library item before inserting on canvas to confine // ids and bindings to each library item. See #6465 - elements: duplicateElements(item.elements), + elements: duplicateElements(item.elements, { randomizeSeed: true }), }; }); }; diff --git a/src/element/newElement.ts b/src/element/newElement.ts index 36c8cc0e0..c1b0f17bf 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -20,7 +20,7 @@ import { isTestEnv, } from "../utils"; import { randomInteger, randomId } from "../random"; -import { mutateElement, newElementWith } from "./mutateElement"; +import { bumpVersion, mutateElement, newElementWith } from "./mutateElement"; import { getNewGroupIdsForDuplication } from "../groups"; import { AppState } from "../types"; import { getElementAbsoluteCoords } from "."; @@ -539,8 +539,16 @@ export const duplicateElement = ( * it's advised to supply the whole elements array, or sets of elements that * are encapsulated (such as library items), if the purpose is to retain * bindings to the cloned elements intact. + * + * NOTE by default does not randomize or regenerate anything except the id. */ -export const duplicateElements = (elements: readonly ExcalidrawElement[]) => { +export const duplicateElements = ( + elements: readonly ExcalidrawElement[], + opts?: { + /** NOTE also updates version flags and `updated` */ + randomizeSeed: boolean; + }, +) => { const clonedElements: ExcalidrawElement[] = []; const origElementsMap = arrayToMap(elements); @@ -574,6 +582,11 @@ export const duplicateElements = (elements: readonly ExcalidrawElement[]) => { clonedElement.id = maybeGetNewId(element.id)!; + if (opts?.randomizeSeed) { + clonedElement.seed = randomInteger(); + bumpVersion(clonedElement); + } + if (clonedElement.groupIds) { clonedElement.groupIds = clonedElement.groupIds.map((groupId) => { if (!groupNewIdsMap.has(groupId)) { diff --git a/src/packages/utils.ts b/src/packages/utils.ts index 560fa13ca..d9365895e 100644 --- a/src/packages/utils.ts +++ b/src/packages/utils.ts @@ -220,15 +220,7 @@ export const exportToClipboard = async ( } else if (opts.type === "png") { await copyBlobToClipboardAsPng(exportToBlob(opts)); } else if (opts.type === "json") { - const appState = { - offsetTop: 0, - offsetLeft: 0, - width: 0, - height: 0, - ...getDefaultAppState(), - ...opts.appState, - }; - await copyToClipboard(opts.elements, appState, opts.files); + await copyToClipboard(opts.elements, opts.files); } else { throw new Error("Invalid export type"); } diff --git a/src/tests/clipboard.test.tsx b/src/tests/clipboard.test.tsx index 1fdc0f452..bbaa4d179 100644 --- a/src/tests/clipboard.test.tsx +++ b/src/tests/clipboard.test.tsx @@ -1,5 +1,10 @@ import ReactDOM from "react-dom"; -import { render, waitFor, GlobalTestState } from "./test-utils"; +import { + render, + waitFor, + GlobalTestState, + createPasteEvent, +} from "./test-utils"; import { Pointer, Keyboard } from "./helpers/ui"; import ExcalidrawApp from "../excalidraw-app"; import { KEYS } from "../keys"; @@ -9,6 +14,8 @@ import { } from "../element/textElement"; import { getElementBounds } from "../element"; import { NormalizedZoomValue } from "../types"; +import { API } from "./helpers/api"; +import { copyToClipboard } from "../clipboard"; const { h } = window; @@ -35,38 +42,28 @@ const setClipboardText = (text: string) => { }); }; -const sendPasteEvent = () => { - const clipboardEvent = new Event("paste", { - bubbles: true, - cancelable: true, - composed: true, - }); - - // set `clipboardData` properties. - // @ts-ignore - clipboardEvent.clipboardData = { - getData: () => window.navigator.clipboard.readText(), - files: [], - }; - +const sendPasteEvent = (text?: string) => { + const clipboardEvent = createPasteEvent( + text || (() => window.navigator.clipboard.readText()), + ); document.dispatchEvent(clipboardEvent); }; -const pasteWithCtrlCmdShiftV = () => { +const pasteWithCtrlCmdShiftV = (text?: string) => { Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { //triggering keydown with an empty clipboard Keyboard.keyPress(KEYS.V); //triggering paste event with faked clipboard - sendPasteEvent(); + sendPasteEvent(text); }); }; -const pasteWithCtrlCmdV = () => { +const pasteWithCtrlCmdV = (text?: string) => { Keyboard.withModifierKeys({ ctrl: true }, () => { //triggering keydown with an empty clipboard Keyboard.keyPress(KEYS.V); //triggering paste event with faked clipboard - sendPasteEvent(); + sendPasteEvent(text); }); }; @@ -89,6 +86,32 @@ beforeEach(async () => { }); }); +describe("general paste behavior", () => { + it("should randomize seed on paste", async () => { + const rectangle = API.createElement({ type: "rectangle" }); + const clipboardJSON = (await copyToClipboard([rectangle], null))!; + + pasteWithCtrlCmdV(clipboardJSON); + + await waitFor(() => { + expect(h.elements.length).toBe(1); + expect(h.elements[0].seed).not.toBe(rectangle.seed); + }); + }); + + it("should retain seed on shift-paste", async () => { + const rectangle = API.createElement({ type: "rectangle" }); + const clipboardJSON = (await copyToClipboard([rectangle], null))!; + + // assert we don't randomize seed on shift-paste + pasteWithCtrlCmdShiftV(clipboardJSON); + await waitFor(() => { + expect(h.elements.length).toBe(1); + expect(h.elements[0].seed).toBe(rectangle.seed); + }); + }); +}); + describe("paste text as single lines", () => { it("should create an element for each line when copying with Ctrl/Cmd+V", async () => { const text = "sajgfakfn\naaksfnknas\nakefnkasf"; diff --git a/src/tests/flip.test.tsx b/src/tests/flip.test.tsx index 45a5e1477..c1469bc83 100644 --- a/src/tests/flip.test.tsx +++ b/src/tests/flip.test.tsx @@ -1,5 +1,10 @@ import ReactDOM from "react-dom"; -import { GlobalTestState, render, waitFor } from "./test-utils"; +import { + createPasteEvent, + GlobalTestState, + render, + waitFor, +} from "./test-utils"; import { UI, Pointer } from "./helpers/ui"; import { API } from "./helpers/api"; import { actionFlipHorizontal, actionFlipVertical } from "../actions"; @@ -680,19 +685,7 @@ describe("freedraw", () => { describe("image", () => { const createImage = async () => { const sendPasteEvent = (file?: File) => { - const clipboardEvent = new Event("paste", { - bubbles: true, - cancelable: true, - composed: true, - }); - - // set `clipboardData` properties. - // @ts-ignore - clipboardEvent.clipboardData = { - getData: () => window.navigator.clipboard.readText(), - files: [file], - }; - + const clipboardEvent = createPasteEvent("", file ? [file] : []); document.dispatchEvent(clipboardEvent); }; diff --git a/src/tests/test-utils.ts b/src/tests/test-utils.ts index c33e80c7d..9560f681f 100644 --- a/src/tests/test-utils.ts +++ b/src/tests/test-utils.ts @@ -190,3 +190,24 @@ export const toggleMenu = (container: HTMLElement) => { // open menu fireEvent.click(container.querySelector(".dropdown-menu-button")!); }; + +export const createPasteEvent = ( + text: + | string + | /* getData function */ ((type: string) => string | Promise), + files?: File[], +) => { + return Object.assign( + new Event("paste", { + bubbles: true, + cancelable: true, + composed: true, + }), + { + clipboardData: { + getData: typeof text === "string" ? () => text : text, + files: files || [], + }, + }, + ); +}; From 1815cf3213b1d19cb6aa1a54dabd969196a65a28 Mon Sep 17 00:00:00 2001 From: Nainterceptor Date: Tue, 25 Apr 2023 13:21:25 +0200 Subject: [PATCH 263/276] build: Add version tags to Docker build (#6508) ci: Add version tags --- .github/workflows/publish-docker.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index a4a8a4c5f..3602bb660 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -12,14 +12,24 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v3 + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + excalidraw/excalidraw + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} - name: Login to DockerHub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: . push: true - tags: excalidraw/excalidraw:latest + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} From 1e9943323ad5f34bb4efc48c5b5a9b01387dbc36 Mon Sep 17 00:00:00 2001 From: suwalkanishka Date: Tue, 25 Apr 2023 17:35:19 +0545 Subject: [PATCH 264/276] style: fix font family inconsistencies (#6501) style: font fix for four components The browser default font was showing up in various locations. Fixed them to show the desired ui font. --- src/components/ColorPicker.scss | 1 + src/components/ContextMenu.scss | 1 + src/components/Tooltip.scss | 3 +++ src/css/styles.scss | 2 ++ 4 files changed, 7 insertions(+) diff --git a/src/components/ColorPicker.scss b/src/components/ColorPicker.scss index 52ea20a19..b816b2553 100644 --- a/src/components/ColorPicker.scss +++ b/src/components/ColorPicker.scss @@ -183,6 +183,7 @@ width: 100%; margin: 0; font-size: 0.875rem; + font-family: inherit; background-color: transparent; color: var(--text-primary-color); border: 0; diff --git a/src/components/ContextMenu.scss b/src/components/ContextMenu.scss index 579763119..81ced3880 100644 --- a/src/components/ContextMenu.scss +++ b/src/components/ContextMenu.scss @@ -30,6 +30,7 @@ background-color: transparent; border: none; white-space: nowrap; + font-family: inherit; display: grid; grid-template-columns: 1fr 0.2fr; diff --git a/src/components/Tooltip.scss b/src/components/Tooltip.scss index bb2b2f72e..490e25578 100644 --- a/src/components/Tooltip.scss +++ b/src/components/Tooltip.scss @@ -2,6 +2,9 @@ // container in body where the actual tooltip is appended to .excalidraw-tooltip { + --ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system, Segoe UI, + Roboto, Helvetica, Arial, sans-serif; + font-family: var(--ui-font); position: fixed; z-index: 1000; diff --git a/src/css/styles.scss b/src/css/styles.scss index 8dafbfbdf..29e52011e 100644 --- a/src/css/styles.scss +++ b/src/css/styles.scss @@ -354,6 +354,7 @@ border-radius: var(--space-factor); border: 1px solid var(--button-gray-2); font-size: 0.8rem; + font-family: inherit; outline: none; appearance: none; background-image: var(--dropdown-icon); @@ -413,6 +414,7 @@ bottom: 30px; transform: translateX(-50%); pointer-events: all; + font-family: inherit; &:hover { background-color: var(--button-hover-bg); From dae81c0a2cb8c21381830635d5bb992eb30b6f36 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Tue, 25 Apr 2023 17:57:53 +0530 Subject: [PATCH 265/276] fix: cleanup redrawTextBoundingBox (#6518) * chore: cleanup redrawTextBoundingBox * fix --- src/element/textElement.ts | 44 +++++++++++++++----------------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/src/element/textElement.ts b/src/element/textElement.ts index f01ba3e1b..339f68b96 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -83,35 +83,25 @@ export const redrawTextBoundingBox = ( boundTextUpdates.baseline = metrics.baseline; if (container) { - if (isArrowElement(container)) { - const centerX = textElement.x + textElement.width / 2; - const centerY = textElement.y + textElement.height / 2; - const diffWidth = metrics.width - textElement.width; - const diffHeight = metrics.height - textElement.height; - boundTextUpdates.x = centerY - (textElement.height + diffHeight) / 2; - boundTextUpdates.y = centerX - (textElement.width + diffWidth) / 2; - } else { - const containerDims = getContainerDims(container); - let maxContainerHeight = getMaxContainerHeight(container); + const containerDims = getContainerDims(container); + const maxContainerHeight = getMaxContainerHeight(container); - let nextHeight = containerDims.height; - if (metrics.height > maxContainerHeight) { - nextHeight = computeContainerDimensionForBoundText( - metrics.height, - container.type, - ); - mutateElement(container, { height: nextHeight }); - maxContainerHeight = getMaxContainerHeight(container); - updateOriginalContainerCache(container.id, nextHeight); - } - const updatedTextElement = { - ...textElement, - ...boundTextUpdates, - } as ExcalidrawTextElementWithContainer; - const { x, y } = computeBoundTextPosition(container, updatedTextElement); - boundTextUpdates.x = x; - boundTextUpdates.y = y; + let nextHeight = containerDims.height; + if (metrics.height > maxContainerHeight) { + nextHeight = computeContainerDimensionForBoundText( + metrics.height, + container.type, + ); + mutateElement(container, { height: nextHeight }); + updateOriginalContainerCache(container.id, nextHeight); } + const updatedTextElement = { + ...textElement, + ...boundTextUpdates, + } as ExcalidrawTextElementWithContainer; + const { x, y } = computeBoundTextPosition(container, updatedTextElement); + boundTextUpdates.x = x; + boundTextUpdates.y = y; } mutateElement(textElement, boundTextUpdates); From da8dd389a9dd7e6528fbd6bf85b12e8ec052f325 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Tue, 25 Apr 2023 18:06:23 +0530 Subject: [PATCH 266/276] fix: cleanup getMaxContainerHeight and getMaxContainerWidth (#6519) * fix: cleanup getMaxContainerHeight and getMaxContainerWidth * rename getMaxContainerWidth -> getBoundTextMaxMaxWidth and getMaxContainerHeight -> getBoundTextMaxHeight * add specs --- src/element/newElement.ts | 4 +- src/element/resizeElements.ts | 14 +++--- src/element/textElement.test.ts | 59 +++++++++++++++++++++----- src/element/textElement.ts | 41 +++++++++--------- src/element/textWysiwyg.tsx | 15 ++++--- src/renderer/renderElement.ts | 11 +++-- src/tests/linearElementEditor.test.tsx | 8 ++-- 7 files changed, 96 insertions(+), 56 deletions(-) diff --git a/src/element/newElement.ts b/src/element/newElement.ts index c1b0f17bf..4922a5b4e 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -33,7 +33,7 @@ import { measureText, normalizeText, wrapText, - getMaxContainerWidth, + getBoundTextMaxWidth, getDefaultLineHeight, } from "./textElement"; import { @@ -310,7 +310,7 @@ export const refreshTextDimensions = ( text = wrapText( text, getFontString(textElement), - getMaxContainerWidth(container), + getBoundTextMaxWidth(container), ); } const dimensions = getAdjustedDimensions(textElement, text); diff --git a/src/element/resizeElements.ts b/src/element/resizeElements.ts index 69b8afae7..67a6346be 100644 --- a/src/element/resizeElements.ts +++ b/src/element/resizeElements.ts @@ -44,10 +44,10 @@ import { getBoundTextElementId, getContainerElement, handleBindTextResize, - getMaxContainerWidth, + getBoundTextMaxWidth, getApproxMinLineHeight, measureText, - getMaxContainerHeight, + getBoundTextMaxHeight, } from "./textElement"; export const normalizeAngle = (angle: number): number => { @@ -204,7 +204,7 @@ const measureFontSizeFromWidth = ( if (hasContainer) { const container = getContainerElement(element); if (container) { - width = getMaxContainerWidth(container); + width = getBoundTextMaxWidth(container); } } const nextFontSize = element.fontSize * (nextWidth / width); @@ -435,8 +435,8 @@ export const resizeSingleElement = ( const nextFont = measureFontSizeFromWidth( boundTextElement, - getMaxContainerWidth(updatedElement), - getMaxContainerHeight(updatedElement), + getBoundTextMaxWidth(updatedElement), + getBoundTextMaxHeight(updatedElement, boundTextElement), ); if (nextFont === null) { return; @@ -718,10 +718,10 @@ const resizeMultipleElements = ( const metrics = measureFontSizeFromWidth( boundTextElement ?? (element.orig as ExcalidrawTextElement), boundTextElement - ? getMaxContainerWidth(updatedElement) + ? getBoundTextMaxWidth(updatedElement) : updatedElement.width, boundTextElement - ? getMaxContainerHeight(updatedElement) + ? getBoundTextMaxHeight(updatedElement, boundTextElement) : updatedElement.height, ); diff --git a/src/element/textElement.test.ts b/src/element/textElement.test.ts index f83eafd1b..b6221336d 100644 --- a/src/element/textElement.test.ts +++ b/src/element/textElement.test.ts @@ -3,15 +3,15 @@ import { API } from "../tests/helpers/api"; import { computeContainerDimensionForBoundText, getContainerCoords, - getMaxContainerWidth, - getMaxContainerHeight, + getBoundTextMaxWidth, + getBoundTextMaxHeight, wrapText, detectLineHeight, getLineHeightInPx, getDefaultLineHeight, parseTokens, } from "./textElement"; -import { FontString } from "./types"; +import { ExcalidrawTextElementWithContainer, FontString } from "./types"; describe("Test wrapText", () => { const font = "20px Cascadia, width: Segoe UI Emoji" as FontString; @@ -311,7 +311,7 @@ describe("Test measureText", () => { }); }); - describe("Test getMaxContainerWidth", () => { + describe("Test getBoundTextMaxWidth", () => { const params = { width: 178, height: 194, @@ -319,39 +319,76 @@ describe("Test measureText", () => { it("should return max width when container is rectangle", () => { const container = API.createElement({ type: "rectangle", ...params }); - expect(getMaxContainerWidth(container)).toBe(168); + expect(getBoundTextMaxWidth(container)).toBe(168); }); it("should return max width when container is ellipse", () => { const container = API.createElement({ type: "ellipse", ...params }); - expect(getMaxContainerWidth(container)).toBe(116); + expect(getBoundTextMaxWidth(container)).toBe(116); }); it("should return max width when container is diamond", () => { const container = API.createElement({ type: "diamond", ...params }); - expect(getMaxContainerWidth(container)).toBe(79); + expect(getBoundTextMaxWidth(container)).toBe(79); }); }); - describe("Test getMaxContainerHeight", () => { + describe("Test getBoundTextMaxHeight", () => { const params = { width: 178, height: 194, + id: '"container-id', }; + const boundTextElement = API.createElement({ + type: "text", + id: "text-id", + x: 560.51171875, + y: 202.033203125, + width: 154, + height: 175, + fontSize: 20, + fontFamily: 1, + text: "Excalidraw is a\nvirtual \nopensource \nwhiteboard for \nsketching \nhand-drawn like\ndiagrams", + textAlign: "center", + verticalAlign: "middle", + containerId: params.id, + }) as ExcalidrawTextElementWithContainer; + it("should return max height when container is rectangle", () => { const container = API.createElement({ type: "rectangle", ...params }); - expect(getMaxContainerHeight(container)).toBe(184); + expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(184); }); it("should return max height when container is ellipse", () => { const container = API.createElement({ type: "ellipse", ...params }); - expect(getMaxContainerHeight(container)).toBe(127); + expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(127); }); it("should return max height when container is diamond", () => { const container = API.createElement({ type: "diamond", ...params }); - expect(getMaxContainerHeight(container)).toBe(87); + expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(87); + }); + + it("should return max height when container is arrow", () => { + const container = API.createElement({ + type: "arrow", + ...params, + }); + expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(194); + }); + + it("should return max height when container is arrow and height is less than threshold", () => { + const container = API.createElement({ + type: "arrow", + ...params, + height: 70, + boundElements: [{ type: "text", id: "text-id" }], + }); + + expect(getBoundTextMaxHeight(container, boundTextElement)).toBe( + boundTextElement.height, + ); }); }); }); diff --git a/src/element/textElement.ts b/src/element/textElement.ts index 339f68b96..a6d0c3271 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -65,7 +65,7 @@ export const redrawTextBoundingBox = ( boundTextUpdates.text = textElement.text; if (container) { - maxWidth = getMaxContainerWidth(container); + maxWidth = getBoundTextMaxWidth(container); boundTextUpdates.text = wrapText( textElement.originalText, getFontString(textElement), @@ -84,7 +84,10 @@ export const redrawTextBoundingBox = ( if (container) { const containerDims = getContainerDims(container); - const maxContainerHeight = getMaxContainerHeight(container); + const maxContainerHeight = getBoundTextMaxHeight( + container, + textElement as ExcalidrawTextElementWithContainer, + ); let nextHeight = containerDims.height; if (metrics.height > maxContainerHeight) { @@ -173,8 +176,11 @@ export const handleBindTextResize = ( let nextHeight = textElement.height; let nextWidth = textElement.width; const containerDims = getContainerDims(container); - const maxWidth = getMaxContainerWidth(container); - const maxHeight = getMaxContainerHeight(container); + const maxWidth = getBoundTextMaxWidth(container); + const maxHeight = getBoundTextMaxHeight( + container, + textElement as ExcalidrawTextElementWithContainer, + ); let containerHeight = containerDims.height; let nextBaseLine = textElement.baseline; if (transformHandleType !== "n" && transformHandleType !== "s") { @@ -246,8 +252,8 @@ export const computeBoundTextPosition = ( ); } const containerCoords = getContainerCoords(container); - const maxContainerHeight = getMaxContainerHeight(container); - const maxContainerWidth = getMaxContainerWidth(container); + const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement); + const maxContainerWidth = getBoundTextMaxWidth(container); let x; let y; @@ -880,18 +886,10 @@ export const computeContainerDimensionForBoundText = ( return dimension + padding; }; -export const getMaxContainerWidth = (container: ExcalidrawElement) => { +export const getBoundTextMaxWidth = (container: ExcalidrawElement) => { const width = getContainerDims(container).width; if (isArrowElement(container)) { - const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2; - if (containerWidth <= 0) { - const boundText = getBoundTextElement(container); - if (boundText) { - return boundText.width; - } - return BOUND_TEXT_PADDING * 8 * 2; - } - return containerWidth; + return width - BOUND_TEXT_PADDING * 8 * 2; } if (container.type === "ellipse") { @@ -908,16 +906,15 @@ export const getMaxContainerWidth = (container: ExcalidrawElement) => { return width - BOUND_TEXT_PADDING * 2; }; -export const getMaxContainerHeight = (container: ExcalidrawElement) => { +export const getBoundTextMaxHeight = ( + container: ExcalidrawElement, + boundTextElement: ExcalidrawTextElementWithContainer, +) => { const height = getContainerDims(container).height; if (isArrowElement(container)) { const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2; if (containerHeight <= 0) { - const boundText = getBoundTextElement(container); - if (boundText) { - return boundText.height; - } - return BOUND_TEXT_PADDING * 8 * 2; + return boundTextElement.height; } return height; } diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx index ef4f7c926..63bc9e4a4 100644 --- a/src/element/textWysiwyg.tsx +++ b/src/element/textWysiwyg.tsx @@ -32,8 +32,8 @@ import { normalizeText, redrawTextBoundingBox, wrapText, - getMaxContainerHeight, - getMaxContainerWidth, + getBoundTextMaxHeight, + getBoundTextMaxWidth, computeContainerDimensionForBoundText, detectLineHeight, } from "./textElement"; @@ -205,8 +205,11 @@ export const textWysiwyg = ({ } } - maxWidth = getMaxContainerWidth(container); - maxHeight = getMaxContainerHeight(container); + maxWidth = getBoundTextMaxWidth(container); + maxHeight = getBoundTextMaxHeight( + container, + updatedTextElement as ExcalidrawTextElementWithContainer, + ); // autogrow container height if text exceeds if (!isArrowElement(container) && textElementHeight > maxHeight) { @@ -377,7 +380,7 @@ export const textWysiwyg = ({ const wrappedText = wrapText( `${editable.value}${data}`, font, - getMaxContainerWidth(container), + getBoundTextMaxWidth(container), ); const width = getTextWidth(wrappedText, font); editable.style.width = `${width}px`; @@ -394,7 +397,7 @@ export const textWysiwyg = ({ const wrappedText = wrapText( normalizeText(editable.value), font, - getMaxContainerWidth(container!), + getBoundTextMaxWidth(container!), ); const { width, height } = measureText( wrappedText, diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index f85c83a6b..77ea14587 100644 --- a/src/renderer/renderElement.ts +++ b/src/renderer/renderElement.ts @@ -44,8 +44,8 @@ import { getContainerCoords, getContainerElement, getLineHeightInPx, - getMaxContainerHeight, - getMaxContainerWidth, + getBoundTextMaxHeight, + getBoundTextMaxWidth, } from "../element/textElement"; import { LinearElementEditor } from "../element/linearElementEditor"; @@ -868,14 +868,17 @@ const drawElementFromCanvas = ( "true" && hasBoundTextElement(element) ) { + const textElement = getBoundTextElement( + element, + ) as ExcalidrawTextElementWithContainer; const coords = getContainerCoords(element); context.strokeStyle = "#c92a2a"; context.lineWidth = 3; context.strokeRect( (coords.x + renderConfig.scrollX) * window.devicePixelRatio, (coords.y + renderConfig.scrollY) * window.devicePixelRatio, - getMaxContainerWidth(element) * window.devicePixelRatio, - getMaxContainerHeight(element) * window.devicePixelRatio, + getBoundTextMaxWidth(element) * window.devicePixelRatio, + getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio, ); } } diff --git a/src/tests/linearElementEditor.test.tsx b/src/tests/linearElementEditor.test.tsx index 15fd105ec..c71283a4c 100644 --- a/src/tests/linearElementEditor.test.tsx +++ b/src/tests/linearElementEditor.test.tsx @@ -20,7 +20,7 @@ import { resize, rotate } from "./utils"; import { getBoundTextElementPosition, wrapText, - getMaxContainerWidth, + getBoundTextMaxWidth, } from "../element/textElement"; import * as textElementUtils from "../element/textElement"; import { ROUNDNESS, VERTICAL_ALIGN } from "../constants"; @@ -729,7 +729,7 @@ describe("Test Linear Elements", () => { type: "text", x: 0, y: 0, - text: wrapText(text, font, getMaxContainerWidth(container)), + text: wrapText(text, font, getBoundTextMaxWidth(container)), containerId: container.id, width: 30, height: 20, @@ -1149,7 +1149,7 @@ describe("Test Linear Elements", () => { expect(rect.x).toBe(400); expect(rect.y).toBe(0); expect( - wrapText(textElement.originalText, font, getMaxContainerWidth(arrow)), + wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)), ).toMatchInlineSnapshot(` "Online whiteboard collaboration made easy" @@ -1172,7 +1172,7 @@ describe("Test Linear Elements", () => { false, ); expect( - wrapText(textElement.originalText, font, getMaxContainerWidth(arrow)), + wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)), ).toMatchInlineSnapshot(` "Online whiteboard collaboration made From 45a57d70de75fbb47f7c3c45cae675341c9bcab5 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Wed, 26 Apr 2023 21:35:06 +0530 Subject: [PATCH 267/276] fix: don't refresh dimensions for text containers on font load (#6523) --- src/data/blob.ts | 2 +- src/excalidraw-app/data/index.ts | 2 +- src/scene/Fonts.ts | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/data/blob.ts b/src/data/blob.ts index 4565b5cb5..c0aa66ee7 100644 --- a/src/data/blob.ts +++ b/src/data/blob.ts @@ -155,7 +155,7 @@ export const loadSceneOrLibraryFromBlob = async ( }, localAppState, localElements, - { repairBindings: true, refreshDimensions: true }, + { repairBindings: true, refreshDimensions: false }, ), }; } else if (isValidLibrary(data)) { diff --git a/src/excalidraw-app/data/index.ts b/src/excalidraw-app/data/index.ts index 7f13bc615..2e50abf1f 100644 --- a/src/excalidraw-app/data/index.ts +++ b/src/excalidraw-app/data/index.ts @@ -263,7 +263,7 @@ export const loadScene = async ( await importFromBackend(id, privateKey), localDataState?.appState, localDataState?.elements, - { repairBindings: true, refreshDimensions: true }, + { repairBindings: true, refreshDimensions: false }, ); } else { data = restore(localDataState || null, null, null, { diff --git a/src/scene/Fonts.ts b/src/scene/Fonts.ts index cc206c776..e245eb16e 100644 --- a/src/scene/Fonts.ts +++ b/src/scene/Fonts.ts @@ -1,5 +1,6 @@ import { isTextElement, refreshTextDimensions } from "../element"; import { newElementWith } from "../element/mutateElement"; +import { isBoundToContainer } from "../element/typeChecks"; import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types"; import { invalidateShapeForElement } from "../renderer/renderElement"; import { getFontString } from "../utils"; @@ -52,7 +53,7 @@ export class Fonts { let didUpdate = false; this.scene.mapElements((element) => { - if (isTextElement(element)) { + if (isTextElement(element) && !isBoundToContainer(element)) { invalidateShapeForElement(element); didUpdate = true; return newElementWith(element, { From 6b0218b01251dfd2143cd974279b70e10a2180a6 Mon Sep 17 00:00:00 2001 From: Milos Vetesnik Date: Thu, 27 Apr 2023 19:11:42 +0200 Subject: [PATCH 268/276] feat: testing simple analytics and fathom analytics for better privacy of the users (#6529) Co-authored-by: dwelle --- public/index.html | 45 ++++++++++++++++++++------------------------- src/analytics.ts | 17 ++++++++++++++--- src/global.d.ts | 4 ++-- 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/public/index.html b/public/index.html index a8633fc4d..f65e481f3 100644 --- a/public/index.html +++ b/public/index.html @@ -150,6 +150,14 @@ <% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %> + + + + <% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %> - <% } %> - - <% } %> @@ -244,5 +227,17 @@

Excalidraw

+ + + diff --git a/src/analytics.ts b/src/analytics.ts index 1e9a429b6..e952bc680 100644 --- a/src/analytics.ts +++ b/src/analytics.ts @@ -20,9 +20,20 @@ export const trackEvent = ( }); } - // MATOMO event tracking _paq must be same as the one in index.html - if (window._paq) { - window._paq.push(["trackEvent", category, action, label, value]); + if (window.sa_event) { + window.sa_event(action, { + category, + label, + value, + }); + } + + if (window.fathom) { + window.fathom.trackEvent(action, { + category, + label, + value, + }); } } catch (error) { console.error("error during analytics", error); diff --git a/src/global.d.ts b/src/global.d.ts index 73c8fc813..3a666e11a 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -18,8 +18,8 @@ interface Window { EXCALIDRAW_EXPORT_SOURCE: string; EXCALIDRAW_THROTTLE_RENDER: boolean | undefined; gtag: Function; - _paq: any[]; - _mtm: any[]; + sa_event: Function; + fathom: { trackEvent: Function }; } interface CanvasRenderingContext2D { From 2a39d0b9a72b65679d213683cf4786d847a29dc6 Mon Sep 17 00:00:00 2001 From: Excalidraw Bot <77840495+excalibot@users.noreply.github.com> Date: Thu, 27 Apr 2023 19:27:36 +0200 Subject: [PATCH 269/276] chore: Update translations from Crowdin (#6471) --- src/locales/ar-SA.json | 1 + src/locales/bg-BG.json | 1 + src/locales/bn-BD.json | 1 + src/locales/ca-ES.json | 1 + src/locales/cs-CZ.json | 1 + src/locales/da-DK.json | 1 + src/locales/de-DE.json | 1 + src/locales/el-GR.json | 1 + src/locales/es-ES.json | 9 ++--- src/locales/eu-ES.json | 1 + src/locales/fa-IR.json | 1 + src/locales/fi-FI.json | 1 + src/locales/fr-FR.json | 5 +-- src/locales/gl-ES.json | 1 + src/locales/he-IL.json | 1 + src/locales/hi-IN.json | 1 + src/locales/hu-HU.json | 1 + src/locales/id-ID.json | 1 + src/locales/it-IT.json | 3 +- src/locales/ja-JP.json | 1 + src/locales/kab-KAB.json | 1 + src/locales/kk-KZ.json | 1 + src/locales/ko-KR.json | 11 +++--- src/locales/ku-TR.json | 69 ++++++++++++++++++------------------ src/locales/lt-LT.json | 1 + src/locales/lv-LV.json | 55 ++++++++++++++-------------- src/locales/mr-IN.json | 1 + src/locales/my-MM.json | 1 + src/locales/nb-NO.json | 1 + src/locales/nl-NL.json | 1 + src/locales/nn-NO.json | 1 + src/locales/oc-FR.json | 1 + src/locales/pa-IN.json | 1 + src/locales/percentages.json | 36 +++++++++---------- src/locales/pl-PL.json | 1 + src/locales/pt-BR.json | 1 + src/locales/pt-PT.json | 5 +-- src/locales/ro-RO.json | 1 + src/locales/ru-RU.json | 33 ++++++++--------- src/locales/si-LK.json | 1 + src/locales/sk-SK.json | 5 +-- src/locales/sl-SI.json | 1 + src/locales/sv-SE.json | 5 +-- src/locales/ta-IN.json | 1 + src/locales/th-TH.json | 1 + src/locales/tr-TR.json | 1 + src/locales/uk-UA.json | 1 + src/locales/vi-VN.json | 1 + src/locales/zh-CN.json | 5 +-- src/locales/zh-HK.json | 1 + src/locales/zh-TW.json | 1 + 51 files changed, 165 insertions(+), 115 deletions(-) diff --git a/src/locales/ar-SA.json b/src/locales/ar-SA.json index 25a32f222..77387b11d 100644 --- a/src/locales/ar-SA.json +++ b/src/locales/ar-SA.json @@ -54,6 +54,7 @@ "veryLarge": "كبير جدا", "solid": "كامل", "hachure": "خطوط", + "zigzag": "", "crossHatch": "خطوط متقطعة", "thin": "نحيف", "bold": "داكن", diff --git a/src/locales/bg-BG.json b/src/locales/bg-BG.json index 501ce7399..d5421ccef 100644 --- a/src/locales/bg-BG.json +++ b/src/locales/bg-BG.json @@ -54,6 +54,7 @@ "veryLarge": "Много голям", "solid": "Солиден", "hachure": "Хералдика", + "zigzag": "", "crossHatch": "Двойно-пресечено", "thin": "Тънък", "bold": "Ясно очертан", diff --git a/src/locales/bn-BD.json b/src/locales/bn-BD.json index a5d9dec0f..ce17d6670 100644 --- a/src/locales/bn-BD.json +++ b/src/locales/bn-BD.json @@ -54,6 +54,7 @@ "veryLarge": "অনেক বড়", "solid": "দৃঢ়", "hachure": "ভ্রুলেখা", + "zigzag": "", "crossHatch": "ক্রস হ্যাচ", "thin": "পাতলা", "bold": "পুরু", diff --git a/src/locales/ca-ES.json b/src/locales/ca-ES.json index ae45e764d..425070e49 100644 --- a/src/locales/ca-ES.json +++ b/src/locales/ca-ES.json @@ -54,6 +54,7 @@ "veryLarge": "Molt gran", "solid": "Sòlid", "hachure": "Ratlletes", + "zigzag": "", "crossHatch": "Ratlletes creuades", "thin": "Fi", "bold": "Negreta", diff --git a/src/locales/cs-CZ.json b/src/locales/cs-CZ.json index d57a8837d..d039a78a2 100644 --- a/src/locales/cs-CZ.json +++ b/src/locales/cs-CZ.json @@ -54,6 +54,7 @@ "veryLarge": "Velmi velké", "solid": "Plný", "hachure": "", + "zigzag": "", "crossHatch": "", "thin": "Tenký", "bold": "Tlustý", diff --git a/src/locales/da-DK.json b/src/locales/da-DK.json index c8b5ad6e3..4d74ab80f 100644 --- a/src/locales/da-DK.json +++ b/src/locales/da-DK.json @@ -54,6 +54,7 @@ "veryLarge": "Meget stor", "solid": "Solid", "hachure": "Skravering", + "zigzag": "", "crossHatch": "Krydsskravering", "thin": "Tynd", "bold": "Fed", diff --git a/src/locales/de-DE.json b/src/locales/de-DE.json index bdf30a371..86b168ae5 100644 --- a/src/locales/de-DE.json +++ b/src/locales/de-DE.json @@ -54,6 +54,7 @@ "veryLarge": "Sehr groß", "solid": "Deckend", "hachure": "Schraffiert", + "zigzag": "Zickzack", "crossHatch": "Kreuzschraffiert", "thin": "Dünn", "bold": "Fett", diff --git a/src/locales/el-GR.json b/src/locales/el-GR.json index 888c39568..f4e0cfcaa 100644 --- a/src/locales/el-GR.json +++ b/src/locales/el-GR.json @@ -54,6 +54,7 @@ "veryLarge": "Πολύ μεγάλο", "solid": "Συμπαγής", "hachure": "Εκκόλαψη", + "zigzag": "", "crossHatch": "Διασταυρούμενη εκκόλαψη", "thin": "Λεπτή", "bold": "Έντονη", diff --git a/src/locales/es-ES.json b/src/locales/es-ES.json index 67a110293..c345c5f83 100644 --- a/src/locales/es-ES.json +++ b/src/locales/es-ES.json @@ -54,6 +54,7 @@ "veryLarge": "Muy grande", "solid": "Sólido", "hachure": "Folleto", + "zigzag": "Zigzag", "crossHatch": "Rayado transversal", "thin": "Fino", "bold": "Grueso", @@ -207,8 +208,8 @@ "collabSaveFailed": "No se pudo guardar en la base de datos del backend. Si los problemas persisten, debería guardar su archivo localmente para asegurarse de que no pierde su trabajo.", "collabSaveFailed_sizeExceeded": "No se pudo guardar en la base de datos del backend, el lienzo parece ser demasiado grande. Debería guardar el archivo localmente para asegurarse de que no pierde su trabajo.", "brave_measure_text_error": { - "start": "", - "aggressive_block_fingerprint": "", + "start": "Parece que estás usando el navegador Brave", + "aggressive_block_fingerprint": "Bloquear huellas dactilares agresivamente", "setting_enabled": "ajuste activado", "break": "Esto podría resultar en romper los", "text_elements": "Elementos de texto", @@ -319,8 +320,8 @@ "doubleClick": "doble clic", "drag": "arrastrar", "editor": "Editor", - "editLineArrowPoints": "", - "editText": "", + "editLineArrowPoints": "Editar puntos de línea/flecha", + "editText": "Editar texto / añadir etiqueta", "github": "¿Ha encontrado un problema? Envíelo", "howto": "Siga nuestras guías", "or": "o", diff --git a/src/locales/eu-ES.json b/src/locales/eu-ES.json index 1aec330cb..9c5f14fac 100644 --- a/src/locales/eu-ES.json +++ b/src/locales/eu-ES.json @@ -54,6 +54,7 @@ "veryLarge": "Oso handia", "solid": "Solidoa", "hachure": "Itzalduna", + "zigzag": "", "crossHatch": "Marraduna", "thin": "Mehea", "bold": "Lodia", diff --git a/src/locales/fa-IR.json b/src/locales/fa-IR.json index 44cf7ae00..a22ad86a9 100644 --- a/src/locales/fa-IR.json +++ b/src/locales/fa-IR.json @@ -54,6 +54,7 @@ "veryLarge": "بسیار بزرگ", "solid": "توپر", "hachure": "هاشور", + "zigzag": "", "crossHatch": "هاشور متقاطع", "thin": "نازک", "bold": "ضخیم", diff --git a/src/locales/fi-FI.json b/src/locales/fi-FI.json index e0701f2d2..403bf0073 100644 --- a/src/locales/fi-FI.json +++ b/src/locales/fi-FI.json @@ -54,6 +54,7 @@ "veryLarge": "Erittäin suuri", "solid": "Yhtenäinen", "hachure": "Vinoviivoitus", + "zigzag": "", "crossHatch": "Ristiviivoitus", "thin": "Ohut", "bold": "Lihavoitu", diff --git a/src/locales/fr-FR.json b/src/locales/fr-FR.json index 49135c3b6..406a11a16 100644 --- a/src/locales/fr-FR.json +++ b/src/locales/fr-FR.json @@ -54,6 +54,7 @@ "veryLarge": "Très grande", "solid": "Solide", "hachure": "Hachures", + "zigzag": "", "crossHatch": "Hachures croisées", "thin": "Fine", "bold": "Épaisse", @@ -319,8 +320,8 @@ "doubleClick": "double-clic", "drag": "glisser", "editor": "Éditeur", - "editLineArrowPoints": "", - "editText": "", + "editLineArrowPoints": "Modifier les points de ligne/flèche", + "editText": "Modifier le texte / ajouter un libellé", "github": "Problème trouvé ? Soumettre", "howto": "Suivez nos guides", "or": "ou", diff --git a/src/locales/gl-ES.json b/src/locales/gl-ES.json index 5571f3f15..53ae05d6b 100644 --- a/src/locales/gl-ES.json +++ b/src/locales/gl-ES.json @@ -54,6 +54,7 @@ "veryLarge": "Moi grande", "solid": "Sólido", "hachure": "Folleto", + "zigzag": "", "crossHatch": "Raiado transversal", "thin": "Estreito", "bold": "Groso", diff --git a/src/locales/he-IL.json b/src/locales/he-IL.json index 810fc1776..4cd8c1140 100644 --- a/src/locales/he-IL.json +++ b/src/locales/he-IL.json @@ -54,6 +54,7 @@ "veryLarge": "גדול מאוד", "solid": "מוצק", "hachure": "קווים מקבילים קצרים להצגת כיוון וחדות שיפוע במפה", + "zigzag": "", "crossHatch": "קווים מוצלבים שתי וערב", "thin": "דק", "bold": "מודגש", diff --git a/src/locales/hi-IN.json b/src/locales/hi-IN.json index 77d6dae2d..d9462e78b 100644 --- a/src/locales/hi-IN.json +++ b/src/locales/hi-IN.json @@ -54,6 +54,7 @@ "veryLarge": "बहुत बड़ा", "solid": "दृढ़", "hachure": "हैशूर", + "zigzag": "तेढ़ी मेढ़ी", "crossHatch": "क्रॉस हैच", "thin": "पतला", "bold": "मोटा", diff --git a/src/locales/hu-HU.json b/src/locales/hu-HU.json index d514520ed..5dc19945b 100644 --- a/src/locales/hu-HU.json +++ b/src/locales/hu-HU.json @@ -54,6 +54,7 @@ "veryLarge": "Nagyon nagy", "solid": "Kitöltött", "hachure": "Vonalkázott", + "zigzag": "", "crossHatch": "Keresztcsíkozott", "thin": "Vékony", "bold": "Félkövér", diff --git a/src/locales/id-ID.json b/src/locales/id-ID.json index 01b510fcd..eb5d8df71 100644 --- a/src/locales/id-ID.json +++ b/src/locales/id-ID.json @@ -54,6 +54,7 @@ "veryLarge": "Sangat besar", "solid": "Padat", "hachure": "Garis-garis", + "zigzag": "", "crossHatch": "Asiran silang", "thin": "Lembut", "bold": "Tebal", diff --git a/src/locales/it-IT.json b/src/locales/it-IT.json index 8380fd8e5..c31462ce2 100644 --- a/src/locales/it-IT.json +++ b/src/locales/it-IT.json @@ -54,6 +54,7 @@ "veryLarge": "Molto grande", "solid": "Pieno", "hachure": "Tratteggio obliquo", + "zigzag": "Zig zag", "crossHatch": "Tratteggio incrociato", "thin": "Sottile", "bold": "Grassetto", @@ -319,7 +320,7 @@ "doubleClick": "doppio-click", "drag": "trascina", "editor": "Editor", - "editLineArrowPoints": "", + "editLineArrowPoints": "Modifica punti linea/freccia", "editText": "Modifica testo / aggiungi etichetta", "github": "Trovato un problema? Segnalalo", "howto": "Segui le nostre guide", diff --git a/src/locales/ja-JP.json b/src/locales/ja-JP.json index 53333aea3..a457b1dfe 100644 --- a/src/locales/ja-JP.json +++ b/src/locales/ja-JP.json @@ -54,6 +54,7 @@ "veryLarge": "特大", "solid": "ベタ塗り", "hachure": "斜線", + "zigzag": "", "crossHatch": "網掛け", "thin": "細", "bold": "太字", diff --git a/src/locales/kab-KAB.json b/src/locales/kab-KAB.json index ba6a3de7e..62c6071c4 100644 --- a/src/locales/kab-KAB.json +++ b/src/locales/kab-KAB.json @@ -54,6 +54,7 @@ "veryLarge": "Meqqer aṭas", "solid": "Aččuran", "hachure": "Azerreg", + "zigzag": "", "crossHatch": "Azerreg anmidag", "thin": "Arqaq", "bold": "Azuran", diff --git a/src/locales/kk-KZ.json b/src/locales/kk-KZ.json index 97a9063fa..38acace0e 100644 --- a/src/locales/kk-KZ.json +++ b/src/locales/kk-KZ.json @@ -54,6 +54,7 @@ "veryLarge": "Өте үлкен", "solid": "", "hachure": "", + "zigzag": "", "crossHatch": "", "thin": "", "bold": "", diff --git a/src/locales/ko-KR.json b/src/locales/ko-KR.json index f170e4cbf..aa4647f28 100644 --- a/src/locales/ko-KR.json +++ b/src/locales/ko-KR.json @@ -54,6 +54,7 @@ "veryLarge": "매우 크게", "solid": "단색", "hachure": "평행선", + "zigzag": "지그재그", "crossHatch": "교차선", "thin": "얇게", "bold": "굵게", @@ -256,7 +257,7 @@ "resize": "SHIFT 키를 누르면서 조정하면 크기의 비율이 제한됩니다.\nALT를 누르면서 조정하면 중앙을 기준으로 크기를 조정합니다.", "resizeImage": "SHIFT를 눌러서 자유롭게 크기를 변경하거나,\nALT를 눌러서 중앙을 고정하고 크기를 변경하기", "rotate": "SHIFT 키를 누르면서 회전하면 각도를 제한할 수 있습니다.", - "lineEditor_info": "포인트를 편집하려면 Ctrl/Cmd을 누르고 더블 클릭을 하거나 Ctrl/Cmd + Enter를 누르세요", + "lineEditor_info": "꼭짓점을 수정하려면 CtrlOrCmd 키를 누르고 더블 클릭을 하거나 CtrlOrCmd + Enter를 누르세요.", "lineEditor_pointSelected": "Delete 키로 꼭짓점을 제거하거나,\nCtrlOrCmd+D 로 복제하거나, 드래그 해서 이동시키기", "lineEditor_nothingSelected": "꼭짓점을 선택해서 수정하거나 (SHIFT를 눌러서 여러개 선택),\nAlt를 누르고 클릭해서 새로운 꼭짓점 추가하기", "placeImage": "클릭해서 이미지를 배치하거나, 클릭하고 드래그해서 사이즈를 조정하기", @@ -319,8 +320,8 @@ "doubleClick": "더블 클릭", "drag": "드래그", "editor": "에디터", - "editLineArrowPoints": "", - "editText": "", + "editLineArrowPoints": "직선 / 화살표 꼭짓점 수정", + "editText": "텍스트 수정 / 라벨 추가", "github": "문제 제보하기", "howto": "가이드 참고하기", "or": "또는", @@ -382,8 +383,8 @@ }, "publishSuccessDialog": { "title": "라이브러리 제출됨", - "content": "{{authorName}}님 감사합니다. 당신의 라이브러리가 심사를 위해 제출되었습니다. 진행 상황을 다음의 링크에서 확인할 수 있습니다.", - "link": "여기" + "content": "{{authorName}}님 감사합니다. 당신의 라이브러리가 심사를 위해 제출되었습니다. 진행 상황을", + "link": "여기에서 확인하실 수 있습니다." }, "confirmDialog": { "resetLibrary": "라이브러리 리셋", diff --git a/src/locales/ku-TR.json b/src/locales/ku-TR.json index 76b5086e8..4fbf60492 100644 --- a/src/locales/ku-TR.json +++ b/src/locales/ku-TR.json @@ -1,7 +1,7 @@ { "labels": { "paste": "دانانەوە", - "pasteAsPlaintext": "", + "pasteAsPlaintext": "دایبنێ وەک دەقی سادە", "pasteCharts": "دانانەوەی خشتەکان", "selectAll": "دیاریکردنی هەموو", "multiSelect": "زیادکردنی بۆ دیاریکراوەکان", @@ -54,6 +54,7 @@ "veryLarge": "زۆر گه‌وره‌", "solid": "سادە", "hachure": "هاچور", + "zigzag": "زیگزاگ", "crossHatch": "کرۆس هاتچ", "thin": "تەنک", "bold": "تۆخ", @@ -110,7 +111,7 @@ "increaseFontSize": "زایدکردنی قەبارەی فۆنت", "unbindText": "دەقەکە جیابکەرەوە", "bindText": "دەقەکە ببەستەوە بە کۆنتەینەرەکەوە", - "createContainerFromText": "", + "createContainerFromText": "دەق لە چوارچێوەیەکدا بپێچە", "link": { "edit": "دەستکاریکردنی بەستەر", "create": "دروستکردنی بەستەر", @@ -194,7 +195,7 @@ "resetLibrary": "ئەمە کتێبخانەکەت خاوێن دەکاتەوە. ئایا دڵنیایت?", "removeItemsFromsLibrary": "سڕینەوەی {{count}} ئایتم(ەکان) لە کتێبخانە؟", "invalidEncryptionKey": "کلیلی رەمزاندن دەبێت لە 22 پیت بێت. هاوکاری ڕاستەوخۆ لە کارخراوە.", - "collabOfflineWarning": "" + "collabOfflineWarning": "هێڵی ئینتەرنێت بەردەست نییە.\n گۆڕانکارییەکانت سەیڤ ناکرێن!" }, "errors": { "unsupportedFileType": "جۆری فایلی پشتگیری نەکراو.", @@ -204,22 +205,22 @@ "invalidSVGString": "ئێس ڤی جی نادروستە.", "cannotResolveCollabServer": "ناتوانێت پەیوەندی بکات بە سێرڤەری کۆلاب. تکایە لاپەڕەکە دووبارە باربکەوە و دووبارە هەوڵ بدەوە.", "importLibraryError": "نەیتوانی کتێبخانە بار بکات", - "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "", + "collabSaveFailed": "نەتوانرا لە بنکەدراوەی ڕاژەدا پاشەکەوت بکرێت. ئەگەر کێشەکان بەردەوام بوون، پێویستە فایلەکەت لە ناوخۆدا هەڵبگریت بۆ ئەوەی دڵنیا بیت کە کارەکانت لەدەست نادەیت.", + "collabSaveFailed_sizeExceeded": "نەتوانرا لە بنکەدراوەی ڕاژەدا پاشەکەوت بکرێت، پێدەچێت تابلۆکە زۆر گەورە بێت. پێویستە فایلەکە لە ناوخۆدا هەڵبگریت بۆ ئەوەی دڵنیا بیت کە کارەکانت لەدەست نادەیت.", "brave_measure_text_error": { - "start": "", - "aggressive_block_fingerprint": "", - "setting_enabled": "", - "break": "", - "text_elements": "", - "in_your_drawings": "", - "strongly_recommend": "", - "steps": "", - "how": "", - "disable_setting": "", - "issue": "", - "write": "", - "discord": "" + "start": "پێدەچێت وێبگەڕی Brave بەکاربهێنیت لەگەڵ", + "aggressive_block_fingerprint": "بلۆککردنی Fingerprinting بەشێوەیەکی توندوتیژانە", + "setting_enabled": "ڕێکخستن چالاک کراوە", + "break": "ئەمە دەکرێت ببێتە هۆی تێکدانی", + "text_elements": "دانە دەقییەکان", + "in_your_drawings": "لە وێنەکێشانەکانتدا", + "strongly_recommend": "بە توندی پێشنیار دەکەین ئەم ڕێکخستنە لەکاربخەیت. دەتوانیت بڕۆیت بە دوای", + "steps": "ئەم هەنگاوانەدا", + "how": "بۆ ئەوەی ئەنجامی بدەیت", + "disable_setting": " ئەگەر لەکارخستنی ئەم ڕێکخستنە پیشاندانی توخمەکانی دەق چاک نەکاتەوە، تکایە هەڵبستە بە کردنەوەی", + "issue": "کێشەیەک", + "write": "لەسەر گیتهەبەکەمان، یان بۆمان بنوسە لە", + "discord": "دیسکۆرد" } }, "toolBar": { @@ -237,7 +238,7 @@ "penMode": "شێوازی قەڵەم - دەست لێدان ڕابگرە", "link": "زیادکردن/ نوێکردنەوەی لینک بۆ شێوەی دیاریکراو", "eraser": "سڕەر", - "hand": "" + "hand": "دەست (ئامرازی پانکردن)" }, "headings": { "canvasActions": "کردارەکانی تابلۆ", @@ -245,7 +246,7 @@ "shapes": "شێوەکان" }, "hints": { - "canvasPanning": "", + "canvasPanning": "بۆ جوڵاندنی تابلۆ، ویلی ماوسەکەت یان دوگمەی سپەیس بگرە لەکاتی ڕاکێشاندە، یانیش ئامرازی دەستەکە بەکاربهێنە", "linearElement": "کرتە بکە بۆ دەستپێکردنی چەند خاڵێک، ڕایبکێشە بۆ یەک هێڵ", "freeDraw": "کرتە بکە و ڕایبکێشە، کاتێک تەواو بوویت دەست هەڵگرە", "text": "زانیاری: هەروەها دەتوانیت دەق زیادبکەیت بە دوو کرتەکردن لە هەر شوێنێک لەگەڵ ئامڕازی دەستنیشانکردن", @@ -256,7 +257,7 @@ "resize": "دەتوانیت ڕێژەکان سنووردار بکەیت بە ڕاگرتنی SHIFT لەکاتی گۆڕینی قەبارەدا،\nALT ڕابگرە بۆ گۆڕینی قەبارە لە ناوەندەوە", "resizeImage": "دەتوانیت بە ئازادی قەبارە بگۆڕیت بە ڕاگرتنی SHIFT،\nALT ڕابگرە بۆ گۆڕینی قەبارە لە ناوەندەوە", "rotate": "دەتوانیت گۆشەکان سنووردار بکەیت بە ڕاگرتنی SHIFT لەکاتی سوڕانەوەدا", - "lineEditor_info": "", + "lineEditor_info": "یان Ctrl یان Cmd بگرە و دوانە کلیک بکە یانیش پەنجە بنێ بە Ctrl یان Cmd + ئینتەر بۆ دەستکاریکردنی خاڵەکان", "lineEditor_pointSelected": "بۆ لابردنی خاڵەکان Delete دابگرە، Ctrl Cmd+D بکە بۆ لەبەرگرتنەوە، یان بۆ جووڵە ڕاکێشان بکە", "lineEditor_nothingSelected": "خاڵێک هەڵبژێرە بۆ دەستکاریکردن (SHIFT ڕابگرە بۆ هەڵبژاردنی چەندین)،\nیان Alt ڕابگرە و کلیک بکە بۆ زیادکردنی خاڵە نوێیەکان", "placeImage": "کلیک بکە بۆ دانانی وێنەکە، یان کلیک بکە و ڕایبکێشە بۆ ئەوەی قەبارەکەی بە دەستی دابنێیت", @@ -264,7 +265,7 @@ "bindTextToElement": "بۆ زیادکردنی دەق enter بکە", "deepBoxSelect": "CtrlOrCmd ڕابگرە بۆ هەڵبژاردنی قووڵ، و بۆ ڕێگریکردن لە ڕاکێشان", "eraserRevert": "بۆ گەڕاندنەوەی ئەو توخمانەی کە بۆ سڕینەوە نیشانە کراون، Alt ڕابگرە", - "firefox_clipboard_write": "" + "firefox_clipboard_write": "ئەم تایبەتمەندییە بە ئەگەرێکی زۆرەوە دەتوانرێت چالاک بکرێت بە ڕێکخستنی ئاڵای \"dom.events.asyncClipboard.clipboardItem\" بۆ \"true\". بۆ گۆڕینی ئاڵاکانی وێبگەڕ لە فایەرفۆکسدا، سەردانی لاپەڕەی \"about:config\" بکە." }, "canvasError": { "cannotShowPreview": "ناتوانرێ پێشبینین پیشان بدرێت", @@ -319,8 +320,8 @@ "doubleClick": "دوو گرتە", "drag": "راکێشان", "editor": "دەستکاریکەر", - "editLineArrowPoints": "", - "editText": "", + "editLineArrowPoints": "دەستکاری خاڵەکانی هێڵ/تیر بکە", + "editText": "دەستکاری دەق بکە / لەیبڵێک زیاد بکە", "github": "کێشەیەکت دۆزیەوە؟ پێشکەشکردن", "howto": "شوێن ڕینماییەکانمان بکەوە", "or": "یان", @@ -334,8 +335,8 @@ "zoomToFit": "زووم بکە بۆ ئەوەی لەگەڵ هەموو توخمەکاندا بگونجێت", "zoomToSelection": "زووم بکە بۆ دەستنیشانکراوەکان", "toggleElementLock": "قفڵ/کردنەوەی دەستنیشانکراوەکان", - "movePageUpDown": "", - "movePageLeftRight": "" + "movePageUpDown": "لاپەڕەکە بجوڵێنە بۆ سەرەوە/خوارەوە", + "movePageLeftRight": "لاپەڕەکە بجوڵێنە بۆ چەپ/ڕاست" }, "clearCanvasDialog": { "title": "تابلۆکە خاوێن بکەرەوە" @@ -417,7 +418,7 @@ "fileSavedToFilename": "هەڵگیراوە بۆ {filename}", "canvas": "تابلۆ", "selection": "دەستنیشانکراوەکان", - "pasteAsSingleElement": "" + "pasteAsSingleElement": "بۆ دانانەوە وەکو یەک توخم یان دانانەوە بۆ نێو دەسکاریکەرێکی دەق کە بوونی هەیە {{shortcut}} بەکاربهێنە" }, "colors": { "ffffff": "سپی", @@ -468,15 +469,15 @@ }, "welcomeScreen": { "app": { - "center_heading": "", - "center_heading_plus": "", - "menuHint": "" + "center_heading": "هەموو داتاکانت لە ناوخۆی وێنگەڕەکەتدا پاشەکەوت کراوە.", + "center_heading_plus": "ویستت بڕۆیت بۆ Excalidraw+?", + "menuHint": "هەناردەکردن، ڕێکخستنەکان، زمانەکان، ..." }, "defaults": { - "menuHint": "", - "center_heading": "", - "toolbarHint": "", - "helpHint": "" + "menuHint": "هەناردەکردن، ڕێکخستنەکان، و زیاتر...", + "center_heading": "دایاگرامەکان. ئاسان. کراون.", + "toolbarHint": "ئامرازێک هەڵبگرە و دەستبکە بە کێشان!", + "helpHint": "قەدبڕەکان و یارمەتی" } } } diff --git a/src/locales/lt-LT.json b/src/locales/lt-LT.json index d80739f09..1c18c2729 100644 --- a/src/locales/lt-LT.json +++ b/src/locales/lt-LT.json @@ -54,6 +54,7 @@ "veryLarge": "Labai didelis", "solid": "", "hachure": "", + "zigzag": "", "crossHatch": "", "thin": "Plonas", "bold": "Pastorintas", diff --git a/src/locales/lv-LV.json b/src/locales/lv-LV.json index 4a311f7cd..bc8a3a678 100644 --- a/src/locales/lv-LV.json +++ b/src/locales/lv-LV.json @@ -54,6 +54,7 @@ "veryLarge": "Ļoti liels", "solid": "Pilns", "hachure": "Svītrots", + "zigzag": "Zigzaglīnija", "crossHatch": "Šķērssvītrots", "thin": "Šaurs", "bold": "Trekns", @@ -110,7 +111,7 @@ "increaseFontSize": "Palielināt fonta izmēru", "unbindText": "Atdalīt tekstu", "bindText": "Piesaistīt tekstu figūrai", - "createContainerFromText": "", + "createContainerFromText": "Ietilpināt tekstu figurā", "link": { "edit": "Rediģēt saiti", "create": "Izveidot saiti", @@ -194,7 +195,7 @@ "resetLibrary": "Šī funkcija iztukšos bibliotēku. Vai turpināt?", "removeItemsFromsLibrary": "Vai izņemt {{count}} vienumu(s) no bibliotēkas?", "invalidEncryptionKey": "Šifrēšanas atslēgai jābūt 22 simbolus garai. Tiešsaistes sadarbība ir izslēgta.", - "collabOfflineWarning": "" + "collabOfflineWarning": "Nav pieejams interneta pieslēgums.\nJūsu izmaiņas netiks saglabātas!" }, "errors": { "unsupportedFileType": "Neatbalstīts datnes veids.", @@ -207,19 +208,19 @@ "collabSaveFailed": "Darbs nav saglabāts datubāzē. Ja problēma turpinās, saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.", "collabSaveFailed_sizeExceeded": "Darbs nav saglabāts datubāzē, šķiet, ka tāfele ir pārāk liela. Saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.", "brave_measure_text_error": { - "start": "", - "aggressive_block_fingerprint": "", - "setting_enabled": "", - "break": "", - "text_elements": "", - "in_your_drawings": "", - "strongly_recommend": "", - "steps": "", - "how": "", - "disable_setting": "", - "issue": "", - "write": "", - "discord": "" + "start": "Izskatās, ka izmanto Brave interneta plārlūku ar ieslēgtu", + "aggressive_block_fingerprint": "Aggressively Block Fingerprinting", + "setting_enabled": "ieslēgtu iestatījumu", + "break": "Tas var salauzt", + "text_elements": "Teksta elementus", + "in_your_drawings": "tavos zīmējumos", + "strongly_recommend": "Mēs iesakām izslēgt šo iestatījumu. Tu vari sekot", + "steps": "šiem soļiem", + "how": "kā to izdarīt", + "disable_setting": " Ja šī iestatījuma izslēgšana neatrisina teksta elementu attēlošanu, tad, lūdzu, atver", + "issue": "problēmu", + "write": "mūsu GitHub vai raksti mums", + "discord": "Discord" } }, "toolBar": { @@ -237,7 +238,7 @@ "penMode": "Pildspalvas režīms – novērst pieskaršanos", "link": "Pievienot/rediģēt atlasītās figūras saiti", "eraser": "Dzēšgumija", - "hand": "" + "hand": "Roka (panoramēšanas rīks)" }, "headings": { "canvasActions": "Tāfeles darbības", @@ -245,7 +246,7 @@ "shapes": "Formas" }, "hints": { - "canvasPanning": "", + "canvasPanning": "Lai bīdītu tāfeli, turiet nospiestu ritināšanas vai atstarpes taustiņu, vai izmanto rokas rīku", "linearElement": "Klikšķiniet, lai sāktu zīmēt vairākus punktus; velciet, lai zīmētu līniju", "freeDraw": "Spiediet un velciet; atlaidiet, kad pabeidzat", "text": "Ieteikums: lai pievienotu tekstu, varat arī jebkur dubultklikšķināt ar atlases rīku", @@ -264,7 +265,7 @@ "bindTextToElement": "Spiediet ievades taustiņu, lai pievienotu tekstu", "deepBoxSelect": "Turient nospiestu Ctrl vai Cmd, lai atlasītu dziļumā un lai nepieļautu objektu pavilkšanu", "eraserRevert": "Turiet Alt, lai noņemtu elementus no dzēsšanas atlases", - "firefox_clipboard_write": "" + "firefox_clipboard_write": "Šis iestatījums var tikt ieslēgts ar \"dom.events.asyncClipboard.clipboardItem\" marķieri pārslēgtu uz \"true\". Lai mainītu pārlūka marķierus Firefox, apmeklē \"about:config\" lapu." }, "canvasError": { "cannotShowPreview": "Nevar rādīt priekšskatījumu", @@ -319,8 +320,8 @@ "doubleClick": "dubultklikšķis", "drag": "vilkt", "editor": "Redaktors", - "editLineArrowPoints": "", - "editText": "", + "editLineArrowPoints": "Rediģēt līniju/bultu punktus", + "editText": "Rediģēt tekstu/pievienot birku", "github": "Sastapāt kļūdu? Ziņot", "howto": "Sekojiet mūsu instrukcijām", "or": "vai", @@ -468,15 +469,15 @@ }, "welcomeScreen": { "app": { - "center_heading": "", - "center_heading_plus": "", - "menuHint": "" + "center_heading": "Visi jūsu dati tiek glabāti uz vietas jūsu pārlūkā.", + "center_heading_plus": "Vai tā vietā vēlies doties uz Excalidraw+?", + "menuHint": "Eksportēšana, iestatījumi, valodas..." }, "defaults": { - "menuHint": "", - "center_heading": "", - "toolbarHint": "", - "helpHint": "" + "menuHint": "Eksportēšana, iestatījumi un vēl...", + "center_heading": "Diagrammas. Izveidotas. Vienkārši.", + "toolbarHint": "Izvēlies rīku un sāc zīmēt!", + "helpHint": "Īsceļi un palīdzība" } } } diff --git a/src/locales/mr-IN.json b/src/locales/mr-IN.json index 4f58009c9..346ae5603 100644 --- a/src/locales/mr-IN.json +++ b/src/locales/mr-IN.json @@ -54,6 +54,7 @@ "veryLarge": "फार मोठं", "solid": "भरीव", "hachure": "हैशूर रेखांकन", + "zigzag": "वाकडी तिकड़ी", "crossHatch": "आडव्या रेघा", "thin": "पातळ", "bold": "जाड", diff --git a/src/locales/my-MM.json b/src/locales/my-MM.json index efc874218..437e9d13b 100644 --- a/src/locales/my-MM.json +++ b/src/locales/my-MM.json @@ -54,6 +54,7 @@ "veryLarge": "ပိုကြီး", "solid": "အပြည့်", "hachure": "မျဉ်းစောင်း", + "zigzag": "", "crossHatch": "ဇကာကွက်", "thin": "ပါး", "bold": "ထူ", diff --git a/src/locales/nb-NO.json b/src/locales/nb-NO.json index 27e717d2a..7a2e72c55 100644 --- a/src/locales/nb-NO.json +++ b/src/locales/nb-NO.json @@ -54,6 +54,7 @@ "veryLarge": "Svært stor", "solid": "Helfarge", "hachure": "Skravert", + "zigzag": "Sikk-sakk", "crossHatch": "Krysskravert", "thin": "Tynn", "bold": "Tykk", diff --git a/src/locales/nl-NL.json b/src/locales/nl-NL.json index 7c2bb105b..62baedf83 100644 --- a/src/locales/nl-NL.json +++ b/src/locales/nl-NL.json @@ -54,6 +54,7 @@ "veryLarge": "Zeer groot", "solid": "Ingekleurd", "hachure": "Arcering", + "zigzag": "", "crossHatch": "Tweemaal gearceerd", "thin": "Dun", "bold": "Vet", diff --git a/src/locales/nn-NO.json b/src/locales/nn-NO.json index 1733b84c4..49333b910 100644 --- a/src/locales/nn-NO.json +++ b/src/locales/nn-NO.json @@ -54,6 +54,7 @@ "veryLarge": "Svært stor", "solid": "Solid", "hachure": "Skravert", + "zigzag": "", "crossHatch": "Krysskravert", "thin": "Tynn", "bold": "Tjukk", diff --git a/src/locales/oc-FR.json b/src/locales/oc-FR.json index a9ca70a84..cae6682df 100644 --- a/src/locales/oc-FR.json +++ b/src/locales/oc-FR.json @@ -54,6 +54,7 @@ "veryLarge": "Gradassa", "solid": "Solide", "hachure": "Raia", + "zigzag": "", "crossHatch": "Raia crosada", "thin": "Fin", "bold": "Espés", diff --git a/src/locales/pa-IN.json b/src/locales/pa-IN.json index 31c11ceb8..2b7c66a33 100644 --- a/src/locales/pa-IN.json +++ b/src/locales/pa-IN.json @@ -54,6 +54,7 @@ "veryLarge": "ਬਹੁਤ ਵੱਡਾ", "solid": "ਠੋਸ", "hachure": "ਤਿਰਛੀਆਂ ਗਰਿੱਲਾਂ", + "zigzag": "", "crossHatch": "ਜਾਲੀ", "thin": "ਪਤਲੀ", "bold": "ਮੋਟੀ", diff --git a/src/locales/percentages.json b/src/locales/percentages.json index b28b6c259..b12928698 100644 --- a/src/locales/percentages.json +++ b/src/locales/percentages.json @@ -1,17 +1,17 @@ { - "ar-SA": 89, + "ar-SA": 88, "bg-BG": 52, "bn-BD": 57, - "ca-ES": 96, - "cs-CZ": 72, + "ca-ES": 95, + "cs-CZ": 71, "da-DK": 31, "de-DE": 100, "el-GR": 98, "en": 100, - "es-ES": 99, + "es-ES": 100, "eu-ES": 99, "fa-IR": 91, - "fi-FI": 96, + "fi-FI": 95, "fr-FR": 99, "gl-ES": 99, "he-IL": 99, @@ -19,35 +19,35 @@ "hu-HU": 85, "id-ID": 98, "it-IT": 99, - "ja-JP": 97, + "ja-JP": 96, "kab-KAB": 93, "kk-KZ": 19, - "ko-KR": 99, - "ku-TR": 91, + "ko-KR": 100, + "ku-TR": 100, "lt-LT": 61, - "lv-LV": 93, + "lv-LV": 100, "mr-IN": 100, - "my-MM": 40, + "my-MM": 39, "nb-NO": 100, "nl-NL": 92, - "nn-NO": 86, + "nn-NO": 85, "oc-FR": 94, "pa-IN": 79, "pl-PL": 87, - "pt-BR": 96, - "pt-PT": 99, + "pt-BR": 95, + "pt-PT": 100, "ro-RO": 100, - "ru-RU": 96, + "ru-RU": 100, "si-LK": 8, "sk-SK": 99, "sl-SI": 100, - "sv-SE": 99, + "sv-SE": 100, "ta-IN": 90, "th-TH": 39, - "tr-TR": 98, - "uk-UA": 93, + "tr-TR": 97, + "uk-UA": 92, "vi-VN": 52, "zh-CN": 99, - "zh-HK": 25, + "zh-HK": 24, "zh-TW": 100 } diff --git a/src/locales/pl-PL.json b/src/locales/pl-PL.json index 1abaa038f..797d88fa1 100644 --- a/src/locales/pl-PL.json +++ b/src/locales/pl-PL.json @@ -54,6 +54,7 @@ "veryLarge": "Bardzo duży", "solid": "Pełne", "hachure": "Linie", + "zigzag": "", "crossHatch": "Zakreślone", "thin": "Cienkie", "bold": "Pogrubione", diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json index d790c5db5..12a1a5d46 100644 --- a/src/locales/pt-BR.json +++ b/src/locales/pt-BR.json @@ -54,6 +54,7 @@ "veryLarge": "Muito grande", "solid": "Sólido", "hachure": "Hachura", + "zigzag": "", "crossHatch": "Hachura cruzada", "thin": "Fino", "bold": "Espesso", diff --git a/src/locales/pt-PT.json b/src/locales/pt-PT.json index 7ce28f00f..947682311 100644 --- a/src/locales/pt-PT.json +++ b/src/locales/pt-PT.json @@ -54,6 +54,7 @@ "veryLarge": "Muito grande", "solid": "Sólido", "hachure": "Eclosão", + "zigzag": "ziguezague", "crossHatch": "Sombreado", "thin": "Fino", "bold": "Espesso", @@ -319,8 +320,8 @@ "doubleClick": "clique duplo", "drag": "arrastar", "editor": "Editor", - "editLineArrowPoints": "", - "editText": "", + "editLineArrowPoints": "Editar pontos de linha/seta", + "editText": "Editar texto / adicionar etiqueta", "github": "Encontrou algum problema? Informe-nos", "howto": "Siga os nossos guias", "or": "ou", diff --git a/src/locales/ro-RO.json b/src/locales/ro-RO.json index a0e3b5883..611ed5676 100644 --- a/src/locales/ro-RO.json +++ b/src/locales/ro-RO.json @@ -54,6 +54,7 @@ "veryLarge": "Foarte mare", "solid": "Plină", "hachure": "Hașură", + "zigzag": "Zigzag", "crossHatch": "Hașură transversală", "thin": "Subțire", "bold": "Îngroșată", diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json index d4030897c..cda20347a 100644 --- a/src/locales/ru-RU.json +++ b/src/locales/ru-RU.json @@ -54,6 +54,7 @@ "veryLarge": "Очень большой", "solid": "Однотонная", "hachure": "Штрихованная", + "zigzag": "Зигзаг", "crossHatch": "Перекрестная", "thin": "Тонкая", "bold": "Жирная", @@ -110,7 +111,7 @@ "increaseFontSize": "Увеличить шрифт", "unbindText": "Отвязать текст", "bindText": "Привязать текст к контейнеру", - "createContainerFromText": "", + "createContainerFromText": "Поместить текст в контейнер", "link": { "edit": "Редактировать ссылку", "create": "Создать ссылку", @@ -207,19 +208,19 @@ "collabSaveFailed": "Не удалось сохранить в базу данных. Если проблема повторится, нужно будет сохранить файл локально, чтобы быть уверенным, что вы не потеряете вашу работу.", "collabSaveFailed_sizeExceeded": "Не удалось сохранить в базу данных. Похоже, что холст слишком большой. Нужно сохранить файл локально, чтобы быть уверенным, что вы не потеряете вашу работу.", "brave_measure_text_error": { - "start": "", - "aggressive_block_fingerprint": "", - "setting_enabled": "", - "break": "", - "text_elements": "", - "in_your_drawings": "", - "strongly_recommend": "", - "steps": "", - "how": "", - "disable_setting": "", - "issue": "", - "write": "", - "discord": "" + "start": "Похоже, вы используете браузер Brave с", + "aggressive_block_fingerprint": "Агрессивно блокировать фингерпринтинг", + "setting_enabled": "параметр включен", + "break": "Это может привести к поломке", + "text_elements": "Текстовых элементов", + "in_your_drawings": "в ваших рисунках", + "strongly_recommend": "Мы настоятельно рекомендуем отключить эту настройку. Вы можете выполнить", + "steps": "эти действия", + "how": "для отключения", + "disable_setting": " Если отключение этого параметра не исправит отображение текстовых элементов, пожалуйста, откройте", + "issue": "issue", + "write": "на нашем GitHub, или напишите нам в", + "discord": "Discord" } }, "toolBar": { @@ -319,8 +320,8 @@ "doubleClick": "двойной клик", "drag": "перетащить", "editor": "Редактор", - "editLineArrowPoints": "", - "editText": "", + "editLineArrowPoints": "Редактировать концы линий/стрелок", + "editText": "Редактировать текст / добавить метку", "github": "Нашли проблему? Отправьте", "howto": "Следуйте нашим инструкциям", "or": "или", diff --git a/src/locales/si-LK.json b/src/locales/si-LK.json index b84ad567f..01f5bcac2 100644 --- a/src/locales/si-LK.json +++ b/src/locales/si-LK.json @@ -54,6 +54,7 @@ "veryLarge": "ඉතා විශාල", "solid": "විශාල", "hachure": "මධ්‍යම", + "zigzag": "", "crossHatch": "", "thin": "කෙට්ටු", "bold": "තද", diff --git a/src/locales/sk-SK.json b/src/locales/sk-SK.json index d6322ac1c..4bb6f0e76 100644 --- a/src/locales/sk-SK.json +++ b/src/locales/sk-SK.json @@ -54,6 +54,7 @@ "veryLarge": "Veľmi veľké", "solid": "Plná", "hachure": "Šrafovaná", + "zigzag": "", "crossHatch": "Mriežkovaná", "thin": "Tenká", "bold": "Hrubá", @@ -319,8 +320,8 @@ "doubleClick": "dvojklik", "drag": "potiahnutie", "editor": "Editovanie", - "editLineArrowPoints": "", - "editText": "", + "editLineArrowPoints": "Editácia bodov čiary/šípky", + "editText": "Editácia textu / pridanie štítku", "github": "Objavili ste problém? Nahláste ho", "howto": "Postupujte podľa naších návodov", "or": "alebo", diff --git a/src/locales/sl-SI.json b/src/locales/sl-SI.json index 57199c813..dc073c0cb 100644 --- a/src/locales/sl-SI.json +++ b/src/locales/sl-SI.json @@ -54,6 +54,7 @@ "veryLarge": "Zelo velika", "solid": "Polno", "hachure": "Šrafura", + "zigzag": "Cikcak", "crossHatch": "Križno", "thin": "Tanko", "bold": "Krepko", diff --git a/src/locales/sv-SE.json b/src/locales/sv-SE.json index d0392b119..502c1bb1e 100644 --- a/src/locales/sv-SE.json +++ b/src/locales/sv-SE.json @@ -54,6 +54,7 @@ "veryLarge": "Mycket stor", "solid": "Solid", "hachure": "Skraffering", + "zigzag": "Sicksack", "crossHatch": "Skraffera med kors", "thin": "Tunn", "bold": "Fet", @@ -319,8 +320,8 @@ "doubleClick": "dubbelklicka", "drag": "dra", "editor": "Redigerare", - "editLineArrowPoints": "", - "editText": "", + "editLineArrowPoints": "Redigera linje-/pilpunkter", + "editText": "Redigera text / lägg till etikett", "github": "Hittat ett problem? Rapportera", "howto": "Följ våra guider", "or": "eller", diff --git a/src/locales/ta-IN.json b/src/locales/ta-IN.json index 6dc865766..47c6e644b 100644 --- a/src/locales/ta-IN.json +++ b/src/locales/ta-IN.json @@ -54,6 +54,7 @@ "veryLarge": "மிகப் பெரிய", "solid": "திடமான", "hachure": "மலைக்குறிக்கோடு", + "zigzag": "", "crossHatch": "குறுக்குகதவு", "thin": "மெல்லிய", "bold": "பட்டை", diff --git a/src/locales/th-TH.json b/src/locales/th-TH.json index 2dcb9cb43..021c0ce97 100644 --- a/src/locales/th-TH.json +++ b/src/locales/th-TH.json @@ -54,6 +54,7 @@ "veryLarge": "ใหญ่มาก", "solid": "", "hachure": "", + "zigzag": "", "crossHatch": "", "thin": "บาง", "bold": "หนา", diff --git a/src/locales/tr-TR.json b/src/locales/tr-TR.json index 78dd9329a..8e1f2b4a0 100644 --- a/src/locales/tr-TR.json +++ b/src/locales/tr-TR.json @@ -54,6 +54,7 @@ "veryLarge": "Çok geniş", "solid": "Dolu", "hachure": "Taralı", + "zigzag": "", "crossHatch": "Çapraz-taralı", "thin": "İnce", "bold": "Kalın", diff --git a/src/locales/uk-UA.json b/src/locales/uk-UA.json index ed1a01e68..417e0a16a 100644 --- a/src/locales/uk-UA.json +++ b/src/locales/uk-UA.json @@ -54,6 +54,7 @@ "veryLarge": "Дуже великий", "solid": "Суцільна", "hachure": "Штриховка", + "zigzag": "", "crossHatch": "Перехресна штриховка", "thin": "Тонкий", "bold": "Жирний", diff --git a/src/locales/vi-VN.json b/src/locales/vi-VN.json index 887c32e3e..27fccfb53 100644 --- a/src/locales/vi-VN.json +++ b/src/locales/vi-VN.json @@ -54,6 +54,7 @@ "veryLarge": "Rất lớn", "solid": "Đặc", "hachure": "Nét gạch gạch", + "zigzag": "", "crossHatch": "Nét gạch chéo", "thin": "Mỏng", "bold": "In đậm", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 5863f5ceb..18584cda7 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -54,6 +54,7 @@ "veryLarge": "加大", "solid": "实心", "hachure": "线条", + "zigzag": "", "crossHatch": "交叉线条", "thin": "细", "bold": "粗", @@ -319,8 +320,8 @@ "doubleClick": "双击", "drag": "拖动", "editor": "编辑器", - "editLineArrowPoints": "", - "editText": "", + "editLineArrowPoints": "编辑线条或箭头的点", + "editText": "添加或编辑文本", "github": "提交问题", "howto": "帮助文档", "or": "或", diff --git a/src/locales/zh-HK.json b/src/locales/zh-HK.json index bbf23f7f7..5cff35a5c 100644 --- a/src/locales/zh-HK.json +++ b/src/locales/zh-HK.json @@ -54,6 +54,7 @@ "veryLarge": "勁大", "solid": "實心", "hachure": "斜線", + "zigzag": "", "crossHatch": "交叉格仔", "thin": "幼", "bold": "粗", diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index f4462842c..25c9b0f57 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -54,6 +54,7 @@ "veryLarge": "特大", "solid": "實心", "hachure": "斜線筆觸", + "zigzag": "Z字形", "crossHatch": "交叉筆觸", "thin": "細", "bold": "粗", From b1311a407a636c87ee0ca326fd20599d0ce4ba9b Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Tue, 2 May 2023 12:49:11 +0530 Subject: [PATCH 270/276] fix: Revert add version tags to Docker build (#6540) Revert "build: Add version tags to Docker build (#6508)" This reverts commit 1815cf3213b1d19cb6aa1a54dabd969196a65a28. --- .github/workflows/publish-docker.yml | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 3602bb660..a4a8a4c5f 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -12,24 +12,14 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v3 - - name: Docker meta - id: meta - uses: docker/metadata-action@v4 - with: - images: | - excalidraw/excalidraw - tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - name: Login to DockerHub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v3 with: context: . push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + tags: excalidraw/excalidraw:latest From e9cae918a7c83c577afb5f6385f6aba9d3544670 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Thu, 4 May 2023 19:33:31 +0200 Subject: [PATCH 271/276] feat: sidebar tabs support (#6213) * feat: Sidebar tabs support [wip] * tab trigger styling tweaks * add `:hover` & `:active` states * replace `@dwelle/tunnel-rat` with `tunnel-rat` * make stuff more explicit - remove `Sidebar.Header` fallback (host apps need to render manually), and stop tunneling it (render in place) - make `docked` state explicit - stop tunneling `Sidebar.TabTriggers` (render in place) * redesign sidebar / library as per latest spec * support no label on `Sidebar.Trigger` * add Sidebar `props.onStateChange` * style fixes * make `appState.isSidebarDocked` into a soft user preference * px -> rem & refactor * remove `props.renderSidebar` * update tests * remove * refactor * rename constants * tab triggers styling fixes * factor out library-related logic from generic sidebar trigger * change `props.onClose` to `onToggle` * rename `props.value` -> `props.tab` * add displayNames * allow HTMLAttributes on applicable compos * fix example App * more styling tweaks and fixes * fix not setting `dockable` * more style fixes * fix and align sidebar header button styling * make DefaultSidebar dockable on if host apps supplies `onDock` * stop `Sidebar.Trigger` hiding label on mobile this should be only the default sidebar trigger behavior, and for that we don't need to use `device` hook as we handle in CSS * fix `dockable` prop of defaultSidebar * remove extra `typescript` dep * remove `defaultTab` prop in favor of explicit `tab` value in `` and `toggleSidebar()`, to reduce API surface area and solve inconsistency of `appState.openSidebar.tab` not reflecting actual UI value if `defaultTab` was supported (without additional syncing logic which feels like the wrong solution). * remove `onToggle` in favor of `onStateChange` reducing API surface area * fix restore * comment no longer applies * reuse `Button` component in sidebar buttons * fix tests * split Sidebar sub-components into files * remove `props.dockable` in favor of `props.onDock` only * split tests * fix sidebar showing dock button if no `props.docked` supplied & add more tests * reorder and group sidebar tests * clarify * rename classes & dedupe css * refactor tests * update changelog * update changelog --------- Co-authored-by: barnabasmolnar --- package.json | 4 +- src/appState.ts | 8 +- src/components/App.tsx | 229 ++++--- src/components/Button.tsx | 12 +- src/components/ConfirmDialog.tsx | 7 +- src/components/DefaultSidebar.test.tsx | 144 +++++ src/components/DefaultSidebar.tsx | 118 ++++ src/components/Dialog.tsx | 2 +- src/components/HintViewer.tsx | 2 +- src/components/LayerUI.tsx | 137 +++-- src/components/LibraryButton.scss | 32 - src/components/LibraryButton.tsx | 57 -- src/components/LibraryMenu.scss | 72 +-- src/components/LibraryMenu.tsx | 212 ++----- src/components/LibraryMenuControlButtons.tsx | 33 + src/components/LibraryMenuHeaderContent.tsx | 65 +- src/components/LibraryMenuItems.scss | 4 +- src/components/LibraryMenuItems.tsx | 22 +- src/components/MobileMenu.tsx | 23 +- src/components/PasteChartDialog.tsx | 8 +- src/components/Sidebar/Sidebar.scss | 211 ++++--- src/components/Sidebar/Sidebar.test.tsx | 572 +++++++++--------- src/components/Sidebar/Sidebar.tsx | 358 +++++++---- src/components/Sidebar/SidebarHeader.tsx | 98 +-- src/components/Sidebar/SidebarTab.tsx | 18 + src/components/Sidebar/SidebarTabTrigger.tsx | 26 + src/components/Sidebar/SidebarTabTriggers.tsx | 16 + src/components/Sidebar/SidebarTabs.tsx | 36 ++ src/components/Sidebar/SidebarTrigger.scss | 34 ++ src/components/Sidebar/SidebarTrigger.tsx | 45 ++ src/components/Sidebar/common.ts | 34 +- src/components/context/tunnels.ts | 32 - .../dropdownMenu/DropdownMenuContent.tsx | 4 +- src/components/footer/Footer.tsx | 8 +- src/components/footer/FooterCenter.tsx | 8 +- src/components/hoc/withInternalFallback.tsx | 26 +- src/components/hoc/withUpstreamOverride.tsx | 63 -- src/components/main-menu/MainMenu.tsx | 8 +- .../welcome-screen/WelcomeScreen.Center.tsx | 8 +- .../welcome-screen/WelcomeScreen.Hints.tsx | 20 +- src/constants.ts | 7 + src/context/tunnels.ts | 36 ++ src/context/ui-appState.ts | 5 + src/css/styles.scss | 2 +- src/css/theme.scss | 6 + src/css/variables.module.scss | 18 +- src/data/library.ts | 22 +- src/data/restore.ts | 32 +- src/data/types.ts | 6 +- src/hooks/useOutsideClick.ts | 2 +- src/packages/excalidraw/CHANGELOG.md | 16 + src/packages/excalidraw/example/App.tsx | 51 +- src/packages/excalidraw/index.tsx | 6 +- .../__snapshots__/contextmenu.test.tsx.snap | 34 +- .../regressionTests.test.tsx.snap | 106 ++-- src/tests/data/restore.test.ts | 25 +- src/tests/library.test.tsx | 9 +- .../packages/__snapshots__/utils.test.ts.snap | 2 +- src/types.ts | 23 +- src/utils.ts | 20 +- yarn.lock | 159 ++++- 61 files changed, 1972 insertions(+), 1431 deletions(-) create mode 100644 src/components/DefaultSidebar.test.tsx create mode 100644 src/components/DefaultSidebar.tsx delete mode 100644 src/components/LibraryButton.scss delete mode 100644 src/components/LibraryButton.tsx create mode 100644 src/components/LibraryMenuControlButtons.tsx create mode 100644 src/components/Sidebar/SidebarTab.tsx create mode 100644 src/components/Sidebar/SidebarTabTrigger.tsx create mode 100644 src/components/Sidebar/SidebarTabTriggers.tsx create mode 100644 src/components/Sidebar/SidebarTabs.tsx create mode 100644 src/components/Sidebar/SidebarTrigger.scss create mode 100644 src/components/Sidebar/SidebarTrigger.tsx delete mode 100644 src/components/context/tunnels.ts delete mode 100644 src/components/hoc/withUpstreamOverride.tsx create mode 100644 src/context/tunnels.ts create mode 100644 src/context/ui-appState.ts diff --git a/package.json b/package.json index 5816786e3..57bdfe562 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ ] }, "dependencies": { - "@dwelle/tunnel-rat": "0.1.1", + "@radix-ui/react-tabs": "1.0.2", "@sentry/browser": "6.2.5", "@sentry/integrations": "6.2.5", "@testing-library/jest-dom": "5.16.2", @@ -51,7 +51,7 @@ "roughjs": "4.5.2", "sass": "1.51.0", "socket.io-client": "2.3.1", - "tunnel-rat": "0.1.0", + "tunnel-rat": "0.1.2", "workbox-background-sync": "^6.5.4", "workbox-broadcast-update": "^6.5.4", "workbox-cacheable-response": "^6.5.4", diff --git a/src/appState.ts b/src/appState.ts index 6f4db7557..aaac94879 100644 --- a/src/appState.ts +++ b/src/appState.ts @@ -58,7 +58,7 @@ export const getDefaultAppState = (): Omit< fileHandle: null, gridSize: null, isBindingEnabled: true, - isSidebarDocked: false, + defaultSidebarDockedPreference: false, isLoading: false, isResizing: false, isRotating: false, @@ -150,7 +150,11 @@ const APP_STATE_STORAGE_CONF = (< gridSize: { browser: true, export: true, server: true }, height: { browser: false, export: false, server: false }, isBindingEnabled: { browser: false, export: false, server: false }, - isSidebarDocked: { browser: true, export: false, server: false }, + defaultSidebarDockedPreference: { + browser: true, + export: false, + server: false, + }, isLoading: { browser: false, export: false, server: false }, isResizing: { browser: false, export: false, server: false }, isRotating: { browser: false, export: false, server: false }, diff --git a/src/components/App.tsx b/src/components/App.tsx index d22a0507c..a317dd9b6 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -210,6 +210,8 @@ import { PointerDownState, SceneData, Device, + SidebarName, + SidebarTabName, } from "../types"; import { debounce, @@ -299,6 +301,9 @@ import { activeConfirmDialogAtom } from "./ActiveConfirmDialog"; import { actionWrapTextInContainer } from "../actions/actionBoundText"; import BraveMeasureTextError from "./BraveMeasureTextError"; +const AppContext = React.createContext(null!); +const AppPropsContext = React.createContext(null!); + const deviceContextInitialValue = { isSmScreen: false, isMobile: false, @@ -340,6 +345,8 @@ const ExcalidrawActionManagerContext = React.createContext( ); ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext"; +export const useApp = () => useContext(AppContext); +export const useAppProps = () => useContext(AppPropsContext); export const useDevice = () => useContext(DeviceContext); export const useExcalidrawContainer = () => useContext(ExcalidrawContainerContext); @@ -400,7 +407,7 @@ class App extends React.Component { private nearestScrollableContainer: HTMLElement | Document | undefined; public library: AppClassProperties["library"]; public libraryItemsFromStorage: LibraryItems | undefined; - private id: string; + public id: string; private history: History; private excalidrawContainerValue: { container: HTMLDivElement | null; @@ -438,7 +445,7 @@ class App extends React.Component { width: window.innerWidth, height: window.innerHeight, showHyperlinkPopup: false, - isSidebarDocked: false, + defaultSidebarDockedPreference: false, }; this.id = nanoid(); @@ -469,7 +476,7 @@ class App extends React.Component { setActiveTool: this.setActiveTool, setCursor: this.setCursor, resetCursor: this.resetCursor, - toggleMenu: this.toggleMenu, + toggleSidebar: this.toggleSidebar, } as const; if (typeof excalidrawRef === "function") { excalidrawRef(api); @@ -577,101 +584,91 @@ class App extends React.Component { this.props.handleKeyboardGlobally ? undefined : this.onKeyDown } > - - - - - - - - this.addElementsFromPasteOrLibrary({ - elements, - position: "center", - files: null, - }) - } - langCode={getLanguage().code} - renderTopRightUI={renderTopRightUI} - renderCustomStats={renderCustomStats} - renderCustomSidebar={this.props.renderSidebar} - showExitZenModeBtn={ - typeof this.props?.zenModeEnabled === "undefined" && - this.state.zenModeEnabled - } - libraryReturnUrl={this.props.libraryReturnUrl} - UIOptions={this.props.UIOptions} - focusContainer={this.focusContainer} - library={this.library} - id={this.id} - onImageAction={this.onImageAction} - renderWelcomeScreen={ - !this.state.isLoading && - this.state.showWelcomeScreen && - this.state.activeTool.type === "selection" && - !this.scene.getElementsIncludingDeleted().length - } + + + + + + + - {this.props.children} - -
-
- {selectedElement.length === 1 && - !this.state.contextMenu && - this.state.showHyperlinkPopup && ( - + - )} - {this.state.toast !== null && ( - this.setToast(null)} - duration={this.state.toast.duration} - closable={this.state.toast.closable} - /> - )} - {this.state.contextMenu && ( - - )} -
{this.renderCanvas()}
- - {" "} - - - - + actionManager={this.actionManager} + elements={this.scene.getNonDeletedElements()} + onLockToggle={this.toggleLock} + onPenModeToggle={this.togglePenMode} + onHandToolToggle={this.onHandToolToggle} + langCode={getLanguage().code} + renderTopRightUI={renderTopRightUI} + renderCustomStats={renderCustomStats} + showExitZenModeBtn={ + typeof this.props?.zenModeEnabled === "undefined" && + this.state.zenModeEnabled + } + UIOptions={this.props.UIOptions} + onImageAction={this.onImageAction} + renderWelcomeScreen={ + !this.state.isLoading && + this.state.showWelcomeScreen && + this.state.activeTool.type === "selection" && + !this.scene.getElementsIncludingDeleted().length + } + > + {this.props.children} +
+
+
+ {selectedElement.length === 1 && + !this.state.contextMenu && + this.state.showHyperlinkPopup && ( + + )} + {this.state.toast !== null && ( + this.setToast(null)} + duration={this.state.toast.duration} + closable={this.state.toast.closable} + /> + )} + {this.state.contextMenu && ( + + )} +
{this.renderCanvas()}
+ + {" "} + + + + + +
); } public focusContainer: AppClassProperties["focusContainer"] = () => { - if (this.props.autoFocus) { - this.excalidrawContainerRef.current?.focus(); - } + this.excalidrawContainerRef.current?.focus(); }; public getSceneElementsIncludingDeleted = () => { @@ -682,6 +679,14 @@ class App extends React.Component { return this.scene.getNonDeletedElements(); }; + public onInsertElements = (elements: readonly ExcalidrawElement[]) => { + this.addElementsFromPasteOrLibrary({ + elements, + position: "center", + files: null, + }); + }; + private syncActionResult = withBatchedUpdates( (actionResult: ActionResult) => { if (this.unmounted || actionResult === false) { @@ -951,7 +956,7 @@ class App extends React.Component { this.scene.addCallback(this.onSceneUpdated); this.addEventListeners(); - if (this.excalidrawContainerRef.current) { + if (this.props.autoFocus && this.excalidrawContainerRef.current) { this.focusContainer(); } @@ -1679,7 +1684,7 @@ class App extends React.Component { openSidebar: this.state.openSidebar && this.device.canDeviceFitSidebar && - this.state.isSidebarDocked + this.state.defaultSidebarDockedPreference ? this.state.openSidebar : null, selectedElementIds: newElements.reduce( @@ -2017,30 +2022,24 @@ class App extends React.Component { /** * @returns whether the menu was toggled on or off */ - public toggleMenu = ( - type: "library" | "customSidebar", - force?: boolean, - ): boolean => { - if (type === "customSidebar" && !this.props.renderSidebar) { - console.warn( - `attempting to toggle "customSidebar", but no "props.renderSidebar" is defined`, - ); - return false; + public toggleSidebar = ({ + name, + tab, + force, + }: { + name: SidebarName; + tab?: SidebarTabName; + force?: boolean; + }): boolean => { + let nextName; + if (force === undefined) { + nextName = this.state.openSidebar?.name === name ? null : name; + } else { + nextName = force ? name : null; } + this.setState({ openSidebar: nextName ? { name: nextName, tab } : null }); - if (type === "library" || type === "customSidebar") { - let nextValue; - if (force === undefined) { - nextValue = this.state.openSidebar === type ? null : type; - } else { - nextValue = force ? type : null; - } - this.setState({ openSidebar: nextValue }); - - return !!nextValue; - } - - return false; + return !!nextName; }; private updateCurrentCursorPosition = withBatchedUpdates( diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 3303c3ebf..bf548d72f 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,8 +1,12 @@ +import clsx from "clsx"; +import { composeEventHandlers } from "../utils"; import "./Button.scss"; interface ButtonProps extends React.HTMLAttributes { type?: "button" | "submit" | "reset"; onSelect: () => any; + /** whether button is in active state */ + selected?: boolean; children: React.ReactNode; className?: string; } @@ -15,18 +19,18 @@ interface ButtonProps extends React.HTMLAttributes { export const Button = ({ type = "button", onSelect, + selected, children, className = "", ...rest }: ButtonProps) => { return (
@@ -334,21 +325,21 @@ const LayerUI = ({ }; const renderSidebars = () => { - return appState.openSidebar === "customSidebar" ? ( - renderCustomSidebar?.() || null - ) : appState.openSidebar === "library" ? ( - { + trackEvent( + "sidebar", + `toggleDock (${docked ? "dock" : "undock"})`, + `(${device.isMobile ? "mobile" : "desktop"})`, + ); + }} /> - ) : null; + ); }; - const [hostSidebarCounters] = useAtom(hostSidebarCountersAtom, jotaiScope); + const isSidebarDocked = useAtomValue(isSidebarDockedAtom, jotaiScope); const layerUIJSX = ( <> @@ -358,8 +349,25 @@ const LayerUI = ({ {children} {/* render component fallbacks. Can be rendered anywhere as they'll be tunneled away. We only render tunneled components that actually - have defaults when host do not render anything. */} + have defaults when host do not render anything. */} + { + if (open) { + trackEvent( + "sidebar", + `${DEFAULT_SIDEBAR.name} (open)`, + `button (${device.isMobile ? "mobile" : "desktop"})`, + ); + } + }} + tab={DEFAULT_SIDEBAR.defaultTab} + > + {t("toolBar.library")} + {/* ------------------------------------------------------------------ */} {appState.isLoading && } @@ -382,7 +390,6 @@ const LayerUI = ({ setAppState({ pasteDialog: { shown: false, data: null }, @@ -410,7 +417,6 @@ const LayerUI = ({ renderWelcomeScreen={renderWelcomeScreen} /> )} - {!device.isMobile && ( <>
- {renderWelcomeScreen && } + {renderWelcomeScreen && } {renderFixedSideContainer()}
- - {layerUIJSX} - - + + + + {layerUIJSX} + + + ); }; const stripIrrelevantAppStateProps = ( appState: AppState, -): Partial => { +): Omit< + AppState, + "suggestedBindings" | "startBoundElement" | "cursorButton" +> => { const { suggestedBindings, startBoundElement, cursorButton, ...ret } = appState; return ret; @@ -491,24 +501,17 @@ const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => { return false; } - const { - canvas: _prevCanvas, - // not stable, but shouldn't matter in our case - onInsertElements: _prevOnInsertElements, - appState: prevAppState, - ...prev - } = prevProps; - const { - canvas: _nextCanvas, - onInsertElements: _nextOnInsertElements, - appState: nextAppState, - ...next - } = nextProps; + const { canvas: _prevCanvas, appState: prevAppState, ...prev } = prevProps; + const { canvas: _nextCanvas, appState: nextAppState, ...next } = nextProps; return ( isShallowEqual( stripIrrelevantAppStateProps(prevAppState), stripIrrelevantAppStateProps(nextAppState), + { + selectedElementIds: isShallowEqual, + selectedGroupIds: isShallowEqual, + }, ) && isShallowEqual(prev, next) ); }; diff --git a/src/components/LibraryButton.scss b/src/components/LibraryButton.scss deleted file mode 100644 index 1a3db79f3..000000000 --- a/src/components/LibraryButton.scss +++ /dev/null @@ -1,32 +0,0 @@ -@import "../css/variables.module"; - -.library-button { - @include outlineButtonStyles; - - background-color: var(--island-bg-color); - - width: auto; - height: var(--lg-button-size); - - display: flex; - align-items: center; - gap: 0.5rem; - - line-height: 0; - - font-size: 0.75rem; - letter-spacing: 0.4px; - - svg { - width: var(--lg-icon-size); - height: var(--lg-icon-size); - } - - &__label { - display: none; - - @media screen and (min-width: 1024px) { - display: block; - } - } -} diff --git a/src/components/LibraryButton.tsx b/src/components/LibraryButton.tsx deleted file mode 100644 index 517e802a8..000000000 --- a/src/components/LibraryButton.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from "react"; -import { t } from "../i18n"; -import { AppState } from "../types"; -import { capitalizeString } from "../utils"; -import { trackEvent } from "../analytics"; -import { useDevice } from "./App"; -import "./LibraryButton.scss"; -import { LibraryIcon } from "./icons"; - -export const LibraryButton: React.FC<{ - appState: AppState; - setAppState: React.Component["setState"]; - isMobile?: boolean; -}> = ({ appState, setAppState, isMobile }) => { - const device = useDevice(); - const showLabel = !isMobile; - - // TODO barnabasmolnar/redesign - // not great, toolbar jumps in a jarring manner - if (appState.isSidebarDocked && appState.openSidebar === "library") { - return null; - } - - return ( - - ); -}; diff --git a/src/components/LibraryMenu.scss b/src/components/LibraryMenu.scss index c06b01fd5..6598b151b 100644 --- a/src/components/LibraryMenu.scss +++ b/src/components/LibraryMenu.scss @@ -1,9 +1,9 @@ @import "open-color/open-color"; .excalidraw { - .layer-ui__library-sidebar { - display: flex; - flex-direction: column; + .library-menu-items-container { + height: 100%; + width: 100%; } .layer-ui__library { @@ -11,28 +11,6 @@ flex-direction: column; flex: 1 1 auto; - - .layer-ui__library-header { - display: flex; - align-items: center; - width: 100%; - margin: 2px 0 15px 0; - .Spinner { - margin-right: 1rem; - } - - button { - // 2px from the left to account for focus border of left-most button - margin: 0 2px; - } - } - } - - .layer-ui__sidebar { - .library-menu-items-container { - height: 100%; - width: 100%; - } } .library-actions-counter { @@ -87,10 +65,17 @@ } } - .library-menu-browse-button { - margin: 1rem auto; + .library-menu-control-buttons { + display: flex; + align-items: center; + justify-content: center; + gap: 0.625rem; + } - padding: 0.875rem 1rem; + .library-menu-browse-button { + flex: 1; + + height: var(--lg-button-size); display: flex; align-items: center; @@ -122,30 +107,19 @@ } } - .library-menu-browse-button--mobile { - min-height: 22px; - margin-left: auto; - a { - padding-right: 0; - } + &.excalidraw--mobile .library-menu-browse-button { + height: var(--default-button-size); } - .layer-ui__sidebar__header .dropdown-menu { - &.dropdown-menu--mobile { - top: 100%; - } + .layer-ui__library .dropdown-menu { + width: auto; + top: initial; + right: 0; + left: initial; + bottom: 100%; + margin-bottom: 0.625rem; + .dropdown-menu-container { - --gap: 0; - z-index: 1; - position: absolute; - top: 100%; - left: 0; - - :root[dir="rtl"] & { - right: 0; - left: auto; - } - width: 196px; box-shadow: var(--library-dropdown-shadow); border-radius: var(--border-radius-lg); diff --git a/src/components/LibraryMenu.tsx b/src/components/LibraryMenu.tsx index d3a718322..eb81bd2e4 100644 --- a/src/components/LibraryMenu.tsx +++ b/src/components/LibraryMenu.tsx @@ -1,11 +1,4 @@ -import { - useRef, - useState, - useEffect, - useCallback, - RefObject, - forwardRef, -} from "react"; +import React, { useState, useCallback } from "react"; import Library, { distributeLibraryItemsOnSquareGrid, libraryItemsAtom, @@ -13,65 +6,29 @@ import Library, { import { t } from "../i18n"; import { randomId } from "../random"; import { LibraryItems, LibraryItem, AppState, ExcalidrawProps } from "../types"; - -import "./LibraryMenu.scss"; import LibraryMenuItems from "./LibraryMenuItems"; -import { EVENT } from "../constants"; -import { KEYS } from "../keys"; import { trackEvent } from "../analytics"; -import { useAtom } from "jotai"; +import { atom, useAtom } from "jotai"; import { jotaiScope } from "../jotai"; import Spinner from "./Spinner"; import { - useDevice, + useApp, + useAppProps, useExcalidrawElements, useExcalidrawSetAppState, } from "./App"; -import { Sidebar } from "./Sidebar/Sidebar"; import { getSelectedElements } from "../scene"; -import { NonDeletedExcalidrawElement } from "../element/types"; -import { LibraryMenuHeader } from "./LibraryMenuHeaderContent"; -import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton"; +import { useUIAppState } from "../context/ui-appState"; -const useOnClickOutside = ( - ref: RefObject, - cb: (event: MouseEvent) => void, -) => { - useEffect(() => { - const listener = (event: MouseEvent) => { - if (!ref.current) { - return; - } +import "./LibraryMenu.scss"; +import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons"; - if ( - event.target instanceof Element && - (ref.current.contains(event.target) || - !document.body.contains(event.target)) - ) { - return; - } +export const isLibraryMenuOpenAtom = atom(false); - cb(event); - }; - document.addEventListener("pointerdown", listener, false); - - return () => { - document.removeEventListener("pointerdown", listener); - }; - }, [ref, cb]); +const LibraryMenuWrapper = ({ children }: { children: React.ReactNode }) => { + return
{children}
; }; -const LibraryMenuWrapper = forwardRef< - HTMLDivElement, - { children: React.ReactNode } ->(({ children }, ref) => { - return ( -
- {children} -
- ); -}); - export const LibraryMenuContent = ({ onInsertLibraryItems, pendingElements, @@ -158,81 +115,31 @@ export const LibraryMenuContent = ({ theme={appState.theme} /> {showBtn && ( - )} ); }; -export const LibraryMenu: React.FC<{ - appState: AppState; - onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void; - libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; - focusContainer: () => void; - library: Library; - id: string; -}> = ({ - appState, - onInsertElements, - libraryReturnUrl, - focusContainer, - library, - id, -}) => { +/** + * This component is meant to be rendered inside inside our + * or host apps Sidebar components. + */ +export const LibraryMenu = () => { + const { library, id, onInsertElements } = useApp(); + const appProps = useAppProps(); + const appState = useUIAppState(); const setAppState = useExcalidrawSetAppState(); const elements = useExcalidrawElements(); - const device = useDevice(); const [selectedItems, setSelectedItems] = useState([]); - const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope); - - const ref = useRef(null); - - const closeLibrary = useCallback(() => { - const isDialogOpen = !!document.querySelector(".Dialog"); - - // Prevent closing if any dialog is open - if (isDialogOpen) { - return; - } - setAppState({ openSidebar: null }); - }, [setAppState]); - - useOnClickOutside( - ref, - useCallback( - (event) => { - // If click on the library icon, do nothing so that LibraryButton - // can toggle library menu - if ((event.target as Element).closest(".ToolIcon__library")) { - return; - } - if (!appState.isSidebarDocked || !device.canDeviceFitSidebar) { - closeLibrary(); - } - }, - [closeLibrary, appState.isSidebarDocked, device.canDeviceFitSidebar], - ), - ); - - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if ( - event.key === KEYS.ESCAPE && - (!appState.isSidebarDocked || !device.canDeviceFitSidebar) - ) { - closeLibrary(); - } - }; - document.addEventListener(EVENT.KEYDOWN, handleKeyDown); - return () => { - document.removeEventListener(EVENT.KEYDOWN, handleKeyDown); - }; - }, [closeLibrary, appState.isSidebarDocked, device.canDeviceFitSidebar]); const deselectItems = useCallback(() => { setAppState({ @@ -241,69 +148,20 @@ export const LibraryMenu: React.FC<{ }); }, [setAppState]); - const removeFromLibrary = useCallback( - async (libraryItems: LibraryItems) => { - const nextItems = libraryItems.filter( - (item) => !selectedItems.includes(item.id), - ); - library.setLibrary(nextItems).catch(() => { - setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") }); - }); - setSelectedItems([]); - }, - [library, setAppState, selectedItems, setSelectedItems], - ); - - const resetLibrary = useCallback(() => { - library.resetLibrary(); - focusContainer(); - }, [library, focusContainer]); - return ( - { - trackEvent( - "library", - `toggleLibraryDock (${docked ? "dock" : "undock"})`, - `sidebar (${device.isMobile ? "mobile" : "desktop"})`, - ); + { + onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems)); }} - ref={ref} - > - - - removeFromLibrary(libraryItemsData.libraryItems) - } - resetLibrary={resetLibrary} - /> - - { - onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems)); - }} - onAddToLibrary={deselectItems} - setAppState={setAppState} - libraryReturnUrl={libraryReturnUrl} - library={library} - id={id} - appState={appState} - selectedItems={selectedItems} - onSelectItems={setSelectedItems} - /> - + onAddToLibrary={deselectItems} + setAppState={setAppState} + libraryReturnUrl={appProps.libraryReturnUrl} + library={library} + id={id} + appState={appState} + selectedItems={selectedItems} + onSelectItems={setSelectedItems} + /> ); }; diff --git a/src/components/LibraryMenuControlButtons.tsx b/src/components/LibraryMenuControlButtons.tsx new file mode 100644 index 000000000..78edcb3b5 --- /dev/null +++ b/src/components/LibraryMenuControlButtons.tsx @@ -0,0 +1,33 @@ +import { LibraryItem, ExcalidrawProps, AppState } from "../types"; +import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton"; +import { LibraryDropdownMenu } from "./LibraryMenuHeaderContent"; + +export const LibraryMenuControlButtons = ({ + selectedItems, + onSelectItems, + libraryReturnUrl, + theme, + id, + style, +}: { + selectedItems: LibraryItem["id"][]; + onSelectItems: (id: LibraryItem["id"][]) => void; + libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; + theme: AppState["theme"]; + id: string; + style: React.CSSProperties; +}) => { + return ( +
+ + +
+ ); +}; diff --git a/src/components/LibraryMenuHeaderContent.tsx b/src/components/LibraryMenuHeaderContent.tsx index 8e6b93523..58e2c8ec5 100644 --- a/src/components/LibraryMenuHeaderContent.tsx +++ b/src/components/LibraryMenuHeaderContent.tsx @@ -1,8 +1,10 @@ -import React, { useCallback, useState } from "react"; +import { useCallback, useState } from "react"; +import { t } from "../i18n"; +import { jotaiScope } from "../jotai"; +import { AppState, LibraryItem, LibraryItems } from "../types"; +import { useApp, useExcalidrawAppState, useExcalidrawSetAppState } from "./App"; import { saveLibraryAsJSON } from "../data/json"; import Library, { libraryItemsAtom } from "../data/library"; -import { t } from "../i18n"; -import { AppState, LibraryItem, LibraryItems } from "../types"; import { DotsIcon, ExportIcon, @@ -13,22 +15,19 @@ import { import { ToolButton } from "./ToolButton"; import { fileOpen } from "../data/filesystem"; import { muteFSAbortError } from "../utils"; -import { atom, useAtom } from "jotai"; -import { jotaiScope } from "../jotai"; +import { useAtom } from "jotai"; import ConfirmDialog from "./ConfirmDialog"; import PublishLibrary from "./PublishLibrary"; import { Dialog } from "./Dialog"; - import DropdownMenu from "./dropdownMenu/DropdownMenu"; - -export const isLibraryMenuOpenAtom = atom(false); +import { isLibraryMenuOpenAtom } from "./LibraryMenu"; const getSelectedItems = ( libraryItems: LibraryItems, selectedItems: LibraryItem["id"][], ) => libraryItems.filter((item) => selectedItems.includes(item.id)); -export const LibraryMenuHeader: React.FC<{ +export const LibraryDropdownMenuButton: React.FC<{ setAppState: React.Component["setState"]; selectedItems: LibraryItem["id"][]; library: Library; @@ -50,6 +49,7 @@ export const LibraryMenuHeader: React.FC<{ isLibraryMenuOpenAtom, jotaiScope, ); + const renderRemoveLibAlert = useCallback(() => { const content = selectedItems.length ? t("alerts.removeItemsFromsLibrary", { count: selectedItems.length }) @@ -181,7 +181,6 @@ export const LibraryMenuHeader: React.FC<{ return ( setIsLibraryMenuOpen(!isLibraryMenuOpen)} > {DotsIcon} @@ -230,6 +229,7 @@ export const LibraryMenuHeader: React.FC<{ ); }; + return (
{renderLibraryMenu()} @@ -261,3 +261,48 @@ export const LibraryMenuHeader: React.FC<{
); }; + +export const LibraryDropdownMenu = ({ + selectedItems, + onSelectItems, +}: { + selectedItems: LibraryItem["id"][]; + onSelectItems: (id: LibraryItem["id"][]) => void; +}) => { + const { library } = useApp(); + const appState = useExcalidrawAppState(); + const setAppState = useExcalidrawSetAppState(); + + const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope); + + const removeFromLibrary = useCallback( + async (libraryItems: LibraryItems) => { + const nextItems = libraryItems.filter( + (item) => !selectedItems.includes(item.id), + ); + library.setLibrary(nextItems).catch(() => { + setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") }); + }); + onSelectItems([]); + }, + [library, setAppState, selectedItems, onSelectItems], + ); + + const resetLibrary = useCallback(() => { + library.resetLibrary(); + }, [library]); + + return ( + + removeFromLibrary(libraryItemsData.libraryItems) + } + resetLibrary={resetLibrary} + /> + ); +}; diff --git a/src/components/LibraryMenuItems.scss b/src/components/LibraryMenuItems.scss index 1e81fdfd1..01c2a36ea 100644 --- a/src/components/LibraryMenuItems.scss +++ b/src/components/LibraryMenuItems.scss @@ -47,7 +47,7 @@ &__items { row-gap: 0.5rem; - padding: var(--container-padding-y) var(--container-padding-x); + padding: var(--container-padding-y) 0; flex: 1; overflow-y: auto; overflow-x: hidden; @@ -61,7 +61,7 @@ margin-bottom: 0.75rem; &--excal { - margin-top: 2.5rem; + margin-top: 2rem; } } diff --git a/src/components/LibraryMenuItems.tsx b/src/components/LibraryMenuItems.tsx index 19bb33308..a5770a69b 100644 --- a/src/components/LibraryMenuItems.tsx +++ b/src/components/LibraryMenuItems.tsx @@ -10,9 +10,8 @@ import Stack from "./Stack"; import "./LibraryMenuItems.scss"; import { MIME_TYPES } from "../constants"; import Spinner from "./Spinner"; -import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton"; -import clsx from "clsx"; import { duplicateElements } from "../element/newElement"; +import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons"; const CELLS_PER_ROW = 4; @@ -201,11 +200,7 @@ const LibraryMenuItems = ({ (item) => item.status === "published", ); - const showBtn = - !libraryItems.length && - !unpublishedItems.length && - !publishedItems.length && - !pendingElements.length; + const showBtn = !libraryItems.length && !pendingElements.length; return (
{!pendingElements.length && !unpublishedItems.length ? (
-
+
{t("library.noItems")}
@@ -303,10 +294,13 @@ const LibraryMenuItems = ({ {showBtn && ( - )} diff --git a/src/components/MobileMenu.tsx b/src/components/MobileMenu.tsx index 75e08867c..34eb83a05 100644 --- a/src/components/MobileMenu.tsx +++ b/src/components/MobileMenu.tsx @@ -13,13 +13,12 @@ import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; import { Section } from "./Section"; import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars"; import { LockButton } from "./LockButton"; -import { LibraryButton } from "./LibraryButton"; import { PenModeButton } from "./PenModeButton"; import { Stats } from "./Stats"; import { actionToggleStats } from "../actions"; import { HandButton } from "./HandButton"; import { isHandToolActive } from "../appState"; -import { useTunnels } from "./context/tunnels"; +import { useTunnels } from "../context/tunnels"; type MobileMenuProps = { appState: AppState; @@ -60,11 +59,15 @@ export const MobileMenu = ({ device, renderWelcomeScreen, }: MobileMenuProps) => { - const { welcomeScreenCenterTunnel, mainMenuTunnel } = useTunnels(); + const { + WelcomeScreenCenterTunnel, + MainMenuTunnel, + DefaultSidebarTriggerTunnel, + } = useTunnels(); const renderToolbar = () => { return ( - {renderWelcomeScreen && } + {renderWelcomeScreen && }
{(heading: React.ReactNode) => ( @@ -88,11 +91,7 @@ export const MobileMenu = ({ {renderTopRightUI && renderTopRightUI(true, appState)}
{!appState.viewModeEnabled && ( - + )} - +
); } return (
- + {actionManager.renderAction("toggleEditMenu")} {actionManager.renderAction("undo")} {actionManager.renderAction("redo")} @@ -190,7 +189,7 @@ export const MobileMenu = ({ {renderAppToolbar()} {appState.scrolledOutside && !appState.openMenu && - appState.openSidebar !== "library" && ( + !appState.openSidebar && ( - )} -
- )} + {PinIcon} + + + )} + +
); }; -const [Context, Component] = withUpstreamOverride(_SidebarHeader); - -/** @private */ -export const SidebarHeaderComponents = { Context, Component }; +SidebarHeader.displayName = "SidebarHeader"; diff --git a/src/components/Sidebar/SidebarTab.tsx b/src/components/Sidebar/SidebarTab.tsx new file mode 100644 index 000000000..741a69fd1 --- /dev/null +++ b/src/components/Sidebar/SidebarTab.tsx @@ -0,0 +1,18 @@ +import * as RadixTabs from "@radix-ui/react-tabs"; +import { SidebarTabName } from "../../types"; + +export const SidebarTab = ({ + tab, + children, + ...rest +}: { + tab: SidebarTabName; + children: React.ReactNode; +} & React.HTMLAttributes) => { + return ( + + {children} + + ); +}; +SidebarTab.displayName = "SidebarTab"; diff --git a/src/components/Sidebar/SidebarTabTrigger.tsx b/src/components/Sidebar/SidebarTabTrigger.tsx new file mode 100644 index 000000000..cf25f7024 --- /dev/null +++ b/src/components/Sidebar/SidebarTabTrigger.tsx @@ -0,0 +1,26 @@ +import * as RadixTabs from "@radix-ui/react-tabs"; +import { SidebarTabName } from "../../types"; + +export const SidebarTabTrigger = ({ + children, + tab, + onSelect, + ...rest +}: { + children: React.ReactNode; + tab: SidebarTabName; + onSelect?: React.ReactEventHandler | undefined; +} & Omit, "onSelect">) => { + return ( + + + + ); +}; +SidebarTabTrigger.displayName = "SidebarTabTrigger"; diff --git a/src/components/Sidebar/SidebarTabTriggers.tsx b/src/components/Sidebar/SidebarTabTriggers.tsx new file mode 100644 index 000000000..0be187b76 --- /dev/null +++ b/src/components/Sidebar/SidebarTabTriggers.tsx @@ -0,0 +1,16 @@ +import * as RadixTabs from "@radix-ui/react-tabs"; + +export const SidebarTabTriggers = ({ + children, + ...rest +}: { children: React.ReactNode } & Omit< + React.RefAttributes, + "onSelect" +>) => { + return ( + + {children} + + ); +}; +SidebarTabTriggers.displayName = "SidebarTabTriggers"; diff --git a/src/components/Sidebar/SidebarTabs.tsx b/src/components/Sidebar/SidebarTabs.tsx new file mode 100644 index 000000000..a681b5ee5 --- /dev/null +++ b/src/components/Sidebar/SidebarTabs.tsx @@ -0,0 +1,36 @@ +import * as RadixTabs from "@radix-ui/react-tabs"; +import { useUIAppState } from "../../context/ui-appState"; +import { useExcalidrawSetAppState } from "../App"; + +export const SidebarTabs = ({ + children, + ...rest +}: { + children: React.ReactNode; +} & Omit, "onSelect">) => { + const appState = useUIAppState(); + const setAppState = useExcalidrawSetAppState(); + + if (!appState.openSidebar) { + return null; + } + + const { name } = appState.openSidebar; + + return ( + + setAppState((state) => ({ + ...state, + openSidebar: { ...state.openSidebar, name, tab }, + })) + } + {...rest} + > + {children} + + ); +}; +SidebarTabs.displayName = "SidebarTabs"; diff --git a/src/components/Sidebar/SidebarTrigger.scss b/src/components/Sidebar/SidebarTrigger.scss new file mode 100644 index 000000000..5aeef52eb --- /dev/null +++ b/src/components/Sidebar/SidebarTrigger.scss @@ -0,0 +1,34 @@ +@import "../../css/variables.module"; + +.excalidraw { + .sidebar-trigger { + @include outlineButtonStyles; + + background-color: var(--island-bg-color); + + width: auto; + height: var(--lg-button-size); + + display: flex; + align-items: center; + gap: 0.5rem; + + line-height: 0; + + font-size: 0.75rem; + letter-spacing: 0.4px; + + svg { + width: var(--lg-icon-size); + height: var(--lg-icon-size); + } + } + + .default-sidebar-trigger .sidebar-trigger__label { + display: none; + + @media screen and (min-width: 1024px) { + display: block; + } + } +} diff --git a/src/components/Sidebar/SidebarTrigger.tsx b/src/components/Sidebar/SidebarTrigger.tsx new file mode 100644 index 000000000..358cc3502 --- /dev/null +++ b/src/components/Sidebar/SidebarTrigger.tsx @@ -0,0 +1,45 @@ +import { useExcalidrawSetAppState, useExcalidrawAppState } from "../App"; +import { SidebarTriggerProps } from "./common"; + +import "./SidebarTrigger.scss"; +import clsx from "clsx"; + +export const SidebarTrigger = ({ + name, + tab, + icon, + title, + children, + onToggle, + className, + style, +}: SidebarTriggerProps) => { + const setAppState = useExcalidrawSetAppState(); + // TODO replace with sidebar context + const appState = useExcalidrawAppState(); + + return ( + + ); +}; +SidebarTrigger.displayName = "SidebarTrigger"; diff --git a/src/components/Sidebar/common.ts b/src/components/Sidebar/common.ts index 97c72b5d3..c7161bd17 100644 --- a/src/components/Sidebar/common.ts +++ b/src/components/Sidebar/common.ts @@ -1,23 +1,41 @@ import React from "react"; +import { AppState, SidebarName, SidebarTabName } from "../../types"; + +export type SidebarTriggerProps = { + name: SidebarName; + tab?: SidebarTabName; + icon?: JSX.Element; + children?: React.ReactNode; + title?: string; + className?: string; + onToggle?: (open: boolean) => void; + style?: React.CSSProperties; +}; export type SidebarProps

= { + name: SidebarName; children: React.ReactNode; /** - * Called on sidebar close (either by user action or by the editor). + * Called on sidebar open/close or tab change. + */ + onStateChange?: (state: AppState["openSidebar"]) => void; + /** + * supply alongside `docked` prop in order to make the Sidebar user-dockable */ - onClose?: () => void | boolean; - /** if not supplied, sidebar won't be dockable */ onDock?: (docked: boolean) => void; docked?: boolean; - initialDockedState?: boolean; - dockable?: boolean; className?: string; + // NOTE sidebars we use internally inside the editor must have this flag set. + // It indicates that this sidebar should have lower precedence over host + // sidebars, if both are open. + /** @private internal */ + __fallback?: boolean; } & P; export type SidebarPropsContextValue = Pick< SidebarProps, - "onClose" | "onDock" | "docked" | "dockable" ->; + "onDock" | "docked" +> & { onCloseRequest: () => void; shouldRenderDockButton: boolean }; export const SidebarPropsContext = - React.createContext({}); + React.createContext({} as SidebarPropsContextValue); diff --git a/src/components/context/tunnels.ts b/src/components/context/tunnels.ts deleted file mode 100644 index 981fcb0ad..000000000 --- a/src/components/context/tunnels.ts +++ /dev/null @@ -1,32 +0,0 @@ -import React from "react"; -import tunnel from "@dwelle/tunnel-rat"; - -type Tunnel = ReturnType; - -type TunnelsContextValue = { - mainMenuTunnel: Tunnel; - welcomeScreenMenuHintTunnel: Tunnel; - welcomeScreenToolbarHintTunnel: Tunnel; - welcomeScreenHelpHintTunnel: Tunnel; - welcomeScreenCenterTunnel: Tunnel; - footerCenterTunnel: Tunnel; - jotaiScope: symbol; -}; - -export const TunnelsContext = React.createContext(null!); - -export const useTunnels = () => React.useContext(TunnelsContext); - -export const useInitializeTunnels = () => { - return React.useMemo((): TunnelsContextValue => { - return { - mainMenuTunnel: tunnel(), - welcomeScreenMenuHintTunnel: tunnel(), - welcomeScreenToolbarHintTunnel: tunnel(), - welcomeScreenHelpHintTunnel: tunnel(), - welcomeScreenCenterTunnel: tunnel(), - footerCenterTunnel: tunnel(), - jotaiScope: Symbol(), - }; - }, []); -}; diff --git a/src/components/dropdownMenu/DropdownMenuContent.tsx b/src/components/dropdownMenu/DropdownMenuContent.tsx index 8ec2b6e63..3c50d474c 100644 --- a/src/components/dropdownMenu/DropdownMenuContent.tsx +++ b/src/components/dropdownMenu/DropdownMenuContent.tsx @@ -1,4 +1,4 @@ -import { useOutsideClickHook } from "../../hooks/useOutsideClick"; +import { useOutsideClick } from "../../hooks/useOutsideClick"; import { Island } from "../Island"; import { useDevice } from "../App"; @@ -24,7 +24,7 @@ const MenuContent = ({ style?: React.CSSProperties; }) => { const device = useDevice(); - const menuRef = useOutsideClickHook(() => { + const menuRef = useOutsideClick(() => { onClickOutside?.(); }); diff --git a/src/components/footer/Footer.tsx b/src/components/footer/Footer.tsx index 788462b86..dae2847cd 100644 --- a/src/components/footer/Footer.tsx +++ b/src/components/footer/Footer.tsx @@ -9,7 +9,7 @@ import { ZoomActions, } from "../Actions"; import { useDevice } from "../App"; -import { useTunnels } from "../context/tunnels"; +import { useTunnels } from "../../context/tunnels"; import { HelpButton } from "../HelpButton"; import { Section } from "../Section"; import Stack from "../Stack"; @@ -25,7 +25,7 @@ const Footer = ({ showExitZenModeBtn: boolean; renderWelcomeScreen: boolean; }) => { - const { footerCenterTunnel, welcomeScreenHelpHintTunnel } = useTunnels(); + const { FooterCenterTunnel, WelcomeScreenHelpHintTunnel } = useTunnels(); const device = useDevice(); const showFinalize = @@ -70,14 +70,14 @@ const Footer = ({

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�?qd(5Hh*uAIxK&qTYu3V< z53h!Xv(NSZ?$Z%i2s$|#kcAp&_oYvwetm$i%lYlX0o9z6?%Ik8$R_M`Y-`}hux~W-WL}~Eq}Kl_HW~+y%Yt5APWnI%Gv+)QNoXz= z$*z;0rU)ig&Y$%C&8$W_TLXpcuY69He#NR9f^Kj~D2Hx;$3|tL@n_|`YM@=PF9`TG zSAEj^e8=HL)&-CdJP#F(=l6_)V$T34nEiT_-FN1g#dfuQS!oC5T{htZ)~8b%A{wBW z4LMG`L99K2YUV}%-s)E-`#+`7!?3~(P^z5~{E2Cwl+@07z{#?xaeA{gB90yf&(eUq zwLT9^6O6~ra4i~Z>28|LJ4k=^7=h&KkQhuA_VXKo)~wx~aN1UF zP8xKN47*PWeEYXD1o=m`>9^0!g1@}*Kw=v3HML|JE;f^PY(2kss)4{>`_t8W`{`Vv z<4?~ApB7)9e!Nt@{#aVUNM?U0d_+r@udZ7vs*5FcK?%cW?`~}6Z_;*a*ly5gY!e)y z>1~{_#$U0Qz0Sg>%JA=mD`eLEFyd~?yXQ%+G+!Zt!vq6eMr)v1!!$l~+kWF;_5w}^ zFbnt0QW#o#dzw6EAf~=zOc{@wde0`adwmWM3(mUNZxz(s5Wl;mL%jI_cAm+!Q=_;{@=@)4h+N#kvt|p$nEySEBq1i~RhsFf$6R=xmBNT(K{X<{=~hDK7%u<>?(mBHepE6#qV)1~~uqt?RN@ZooHYz2|dYD33dyEBXA2;#&kD zn8i+YYK`_kng6`dJ_E^34msNZY6P6FZ=FS7imH%#c*8vCIV3hht*QBhT?4&hCn3|P|4-q_Y4cO&00-#!u&LSeDfAid$UG8D}M zt~3EG%m*qc1*xh~sM~1#t9{BX*GPmNgB|zk{ri5r?58v8{a4=ATRQ^gIjl&lB6z0^ ze!Aa=a#b6z>sj1B;3~1x>pU?(8N2xw9;;`%a zfwS}9ISwH7!)Rk#Ipr8C$n}3_7gZ3#AL?4QTL=;4U3&?WX)S{72L-_JE89m}P176q zfCic$4>P+ayXiYTPs*K)WQ1KxJ>d_xCAB54vyD9FWR`?aYD);VfaJPGcM+Xy_>*mO zC-XA!u6?~*=hQRv>WwlUsk@=kssEZE7grxBNoO#y>GQzPsr|K`ox#}l9yN2NRC+jB zn2G9bG0BZnoonJpE_!W&0FsS_BBQ)NsVMOsbo9E07bFE2cpa66XY*IhO`{aCUkF%3 z&o)|h9qO9XpL09_K79=^g)?}M1Eys`{(Js9k%Q&aF|aG6FFgT_Ich&Xy#&<6*m@uB zFb`%QK7PNW`pABqI}w2{0n``LEDVNLFA^CMHf^g~MiCiw?(g}Os8|I&!%&R{bOVy# zPiL>G-~G39V=92`q2bZPi5U>2;h-Kq~T#PH2X~sDouaF;pINJp|A(?eD`R(I{D!gr8aDz zSrNYmBSg#foHTn*yk}H`a8gyNN>!wGbZ~!j;qJ1I{@eK5C;mW#T=I{{gUp1k7Rt2< zq{Sg|2w%sSuJbJ19ufbXKqte~IRN$*9lx{YjMx0MMZj@cv&4!#=0;`S*23QY6I1hWu`lWf(Nwp>1L1-$ ztYBpnW!nY=z@0)bo9+^g;D;o)tgJ}5$G{j(^Ig+k+BU`_fi-=E@zb0mb;j5M&m5Jn z2=8Rf-jq|>n^|#A1agB`mx%5Qz~|3Kw=~83`5M^7R;B5hv^z_HG(2A&Zig4^)Q=P| zzdyG`o0Z!D{5#>_@KF(Jy_lCtx?wqSGCKz-c;!p`=z2vWD@9gCby^JJovT6NQxKI&c7BnR}UE^}w4TAgRr)#fN z@(vk6=D*Pebv&z?@-p z@`j*ziK)W-<>Jrulli8h#P^gXm2Q%;NZE+3$vfuj_QQV}VO}r07(tHF^~e3-7W+c0 zvn(EFx}*dPj6ouYzWU*+A46o~0~mA)ravhFm1|AN0(n9TeL|KcoL9Szd=rQ;e;>q;i3xs1b_0T*Tm3}CQEJ%uJ8xS9!deAz|-Dv zY>Z=PjK~fn>RxNKIa&F>!hZPZ2dUi=R!Vkr6wCvl{svbZ$w-I7?>E-p_8KTsHPT&S zzYYpmx1_61A)xoo<9y;YPkdg!Nn4&@$-ECEe|7*`xw{?X4;K9{>BZDhanlNZIhlAu z1!+w-N4VVKaAD<#=Zx=#c&1kcQJoR7$2l@iZ_Rxj!ZrdQcX3PHCv% z*iY+w5^4@Q86DeB+Ul*<uyO5PS)c}2=|K6|u;;^tio^ZW4$*QVy`Bz~m%M@xEs+sW8%_VR?!0xJSDxq(>8Gv@L zh@xxpCB7Mh*=+gv)k2&O*2nMZZeejIXNfCVhzg@^MzBk&u6P*ZM<2uNKgoD&T z>NrqK)!rB1?Exg1HgE!RJAe$0X1ZHIp|`SvAkWJhqFt@s6C`>EkfxWDZpIGa;isE1 z+mE&St51VkW}=D=zuq&NYr>3Fma7;a+@tYV< z0#V(|+?)`3kr!1Reok_L)X_!FP@@`g^kX_o*a>0_keKH;Rnz6hx7#z+T^`-ttdSR)4kVSxG3*zpYP3h-b{G=tmg!i2Cg@xA+N?`Vh34r zJG^LGQW2J)7j})_M&$nNXYB1{<|w6Uk}eXc-zJhbw}lXvPq?aFofc_LHQ*7+rIS}N z(!_C97c2>%0vVUkEv+C8AoU!ROg>n@vUbCJ-Hzc7xPqhVYV}t*0x4WC#vV;9;`kN~ zKXmVP339MF7FjEWF(Wi1Awgh{?x^b{?cm(GAF8l+{xv=atJp`KW@2nzlRAv$zX)mV&OVw4ddXLj#GLlCu5t{#9Udk#fl=l(UD<1VB3 zuMA5d0F5v4z3fL^LgnZx9OKoXGMgHTB@Iz5jn>8+PpdjzJ#fABXK&jIp_hJX*>Jwm zX^$Q$BY5iO7CXGu! ziNsQ*;$NdQ36;R+Lzm<5L)$ij=U$rb@qri+M;-va%1v7L-iczchY92D26&=@3>`ejkist3dN9-T6V7)V$viguN2+9r>a!zvSgjM+&??fJ zy*@24cP#neV>7gWU=6ANC@v_zTul6n@zmMk90s+E-l~D{_J@B$+bnMVeEW^OQ2p_U zbQfbNWf@*^*K^el8eZseBu7hN-1IjVMP~HyKN-y3he?Gce@GNxN`qZ_Ln`9>AXTVH`{`|y4@OF9Y~rh z=SWNY<*m^R<-`<`|8@y}vktcAel3v#-v4H>mw(fz-d`+mSVnhyQdzZ_?;7loc(riV zq~-TQ&xLFv-!QG-jR2Q<0zl(YJp(EVqDJGmnCr4PBlastDZA9Gv-JziD!!6ZqY=$g z%vKesB$Ii%`gzjriiSEqZk9|z(+zU6f4(|pKaF=JsBwNylRH+^i@8kMy-@34ejK*@ z81t7(_b3bx-5$C4#|?fbP+G^dn3+v8JUyz}6h^k6mIJ?;Sog~4D!`(VJ#=Veg_q%e zvB(iho}_PXF^y~>Z;amkbu`<+l&mq#{S=T&#UM~t_**KjHaay`WqzEN&uS)N>RmL^ z>oaP|-%>6Slf7WuKI3ZuV7JKug|6T?{uZ<5o$r=iCHe|xxAr;~Znqb&y^VILG*zRy z625wFPw+S7jlVV#*x$LeO{&?d=&xd-V%cF{*JF`VPA{1NJ6RFl!R7-XZG znfVwFoU1}4JH?wP&*ebrquGC6q74BCCWo_C;2}YkKCU0IdnoR2)YKg9)ug0CtLbJaP(IDO#hnmG#fW*5Xyf4h_i?DZ-c3!t;kP#+Af12^L^=e7&{0sTbScsk zYC@AH0R<`2rG&1O(2EENh@qqOCcPJt4pO8y=^gd#_&sOl%sjvI3Lb_(G7ON+PVRkQ z_qsmUT9#K!(2Z)nxeJ@fQ(}cSKQ>rF`sg#wt0I3kPo?YUrZhjSqXm!WvR9gJNOKfN z^e*g;7fm*zzHFtjEijfQ!}9vlpB=(?qKFgdB*Wozck#AQOi?A{$|WbHz7D13(VR$= z!JMcIC$X94g*zH)q_}EaBgW+{vdN>0N8?uZlZp0~7B;{AAnmuB!lj~gF{voX+iCE3 zyZ|8zr6#W-GW9}(Cf)h^wSu=R+s5?F9&U(C-*%&lQCiiD({_=fS^m3}B2I-yp=~{& z?0E5;VB0yJWX|LdYUBRG%U*|j_ICZx(HkxZ+)qZ5Zm)C+ zycr&It9wnu&ML9rSC2}1p3=!Od|{vkwhx8=oD|cwdS3@AowS!}ngP6T_4$Iu=J)lp)3#pZs^gCXOWE0zL z!%t1fq5uvVXvmb3JH}f+0K9|h#jF-gAX9&`ynd4Ym`?PQQEueg`cWMjIQ5Fq2C_U5 z-)ICr*sq$b8}h3dNUYiyp4nc-5+)&` zPBVl8zG3wahg|>N1nAbyJ7V+_)#|2hp=8Yfb2k5k`%@a7eXNcXMpSu1FFM! zGeGv`I2nI&oa}OvO0y;E2ELiU68-USYI+K31)^}|hv09qT^dv3?~5$kil@K~rgRlD zz7}f~a3#+UAAOd7=o6~Xb{}8vyvgBs(jKrw(d(GTII{ z$XxAwb{vNrl^85!$JEU%B4C^rW~NC?x8%UxYcjvqIN^c^2$%QeUxBx==9jEH;Q20{ zM7v%G%KL?u;Z}Fl`|>-0RUH#1Q&%t6cbSVHP@ALmYX65Hq#mDhGP4g;6wWA(fbN^3)TrKl8E=V~?Yxv5I)m7b{5QY2q9tWsc_oRFEC&Y(J3IR2wD0|ySRKfYX&;zXzm?t^? zOPm}6SQ%v8dGI_l#;|N!4M`f<@67H7*i95kvnFm$)W10_wLsHD?1^TpP#>Q+ofz&x zT5}zrq#f)FtzZRgdy~p93SqYI`k_N5!k@zEnf5YPC>Yj^kN$+sj&UulnDp+Y21`LC ziTJ+}@d^jrT@wY97n=G)$e%a4=0ISN&YQP}Q zfGn-UoY0pum=Z|Kl0;6_FULr4b!E? zF3O&)KJXj;+72u4kAzPB<)mkJEg}2!@>MbB-IZc7iB;CKYoH5%ltxyHf=E0VdPC1u zXwQz~y&^imf96IMz%U^dk9Z^Li$}y`X2J<-jWg;2KLwh?XkRsS9tY*B{Ngan*6+S}~tZIithw z>l^#LuOc~jze{W9Q1B#eRDcwfN~E~o1~{A*;!FXN*%Sm}mc+;W;(P`|Xcg-sPO>y{ z$!$8bUBj)M;L;}8i2p76<2ijmSa?G^War!Q+hCT3E}@!5wL_vQr@P2Pp>1zCS)w7O--+G3~@JYaJyp}Y|Q44@|%L&yr5aaOa)&%JLFBH8%J2n)B=eiGM z@f1q`0;XXd;P+pwGa+5sx72O%mxP4+VqT<%plcIZo?CIc*bWpXo%k5Ovoy{VsVB?e z7_)RThddx>1@b!jFn_T;w6?Ifx+?#qJO^|b^rnsL{6c>Ur0sp*%2fUoS7;)K zG$`E+7IKUkW==sNnsn^pjC~3HN5>T!Ycz%oX6~2Gt|h-WbzWny%+RPq;6j?PC+1z~ z1^w%|X8(k12uC@mn&hIFEC!x)+uN5r$R}O_o;|QWKV@^j&)d%*#V|mufUrm@Ui@lN zW14TR_i=urfS=Fi{Z89t6V(J!d$XT@gpI1=`b5&mSXNngM)4j}pAwvU+SCwp!5?>f zz%q%(T`%d5{tcf-P*%Kot>Ux6jNw#Cw*o_@g!P+CL{n;omElE_QrTFFX|u;&tI|xW){lWAlyJR1Q?54ehk0qQ zHlCVhr589^D34Ynyf1nx&o6xOz>OVL<*p)7`Yq%LAQiCyTcDgEihe0S-pV|atq=kO zaxRZ`rNOFd>K#XfR``-il`Kxt?9X&B%NNUXplyVU75k~WTl;su9)>eYK#mnUZ=?n1 z*0d5cZ3NPQpdea0tYS(1)vpF|D^)+Kz5)eI4wZE`OJ7oKXwg7td2C+@x;0On9NeRx zz_V&TocjBYxFakf_!l4vt#?gmUS=R7?Bchl@-3%TLb065%}8p!tUoGIh$%SZP0yah;Z8CZ686+r?Unz;#^=*_m}aYHjLU{y2z2B`NyuaLyiN|iuCEMRj912z&9qX%sc_9B8(I*Vj4HVYMHKx1wF)iCFI5A8cRzt7fk3rEz#BLQb5)`|I;g8aBDI zwTf`*)7H$1Y>J@O-jju!bNJcghw$KzFDN5yN zx28XG>)obRCIjny_a%ig*e!r0FxN=JI7>MybP1X5r~I|_^mbO`0`ao^h{Q4dtz7*! zjYgmuKJILYpL&FTJJl&PH767WRT4K{UgL@UPce9S@KW6oquP)nde`tul9!-&h>MVt z*|me_h=B3XC(O?$2olXn`pHo!Kj%$dk{Ddp$Tz>g&RsTEE-Lppz4c6F0T8wt#<`U> zNEq-c!#di&RDr6?J`YJ1X>Ven*+Ti8oT*V-*Feq-J(XYzJ6o?Bx5W<__K| zv@fV0H**~GJnJkqA^pb4TUC^1_ZiD6-R!BF^5^k0OpeZ_RoZ&c5dtUU=;9YOauLb% z-4#Gr5#1ne0Sxh+MAP=+mVOMsn$L`q^z}NIB~lzGO{HV-jFA8XvtHjRAP^SuHwCdw z!mmItZ|d)`(A-3|5Ljup>WzhFDry9$^H;b8gvCN5U09A=gRVdJf=T|IOj^8e;L}~r zo#zJSO7?{6h^W!WF3m2h;H0{5QN1v|j4%kXvDhT{=F?9s!ge5x?YoEt)(6iKR?>R$ zCRv;fqe9YGw$oP-4Fh^oZr&oRy z^ZR9$ZYqoCltTC&cB@kC{pLz@1EcX8Zo9Mec74PtY6Y9OJfpPE0%aWy-O|&N;%*?i;3dzGnl>9R3@iG%Q;9Q#>y`0i`s=po3G;*yX)Ir7zrzkPZ- z>Sn{vJj-O|%ml5ct|6(j-fZgbNvf0w_SZd4Vj6ZIrfBocA&LD7vlp9;onJ04`BFWqYA*z^-1oRk^=1cI4mr3#*oOI^ zt1j0NZzmsn0m)~r^^4?FRb8=c@1?g>^F<~D52pdayrK7r(XP+qOAc1*EoGB?&*3es zw#UdSV$2ln*`}BqG5I#&QkbJIJu38llA7c;EvZ$|i6CAw2D0!z$^C_TFXq!Q^aq1o zhGsI>(RmR*Rv1pixds4@a_u2IOM;)WP@7yt_&Q;@EZDtr7@_S~_D&Y{B zX94<-hl)a`ViCcsUAVVYKsgdUD9*fl>1Dz;fuk~KY!bA#>}>yBuRq#W)OjBu_$g`d zSh_iSBA2i|R6yhVcwc4?Gz11#A4kQQ+{ZgI+kDP7;1+cMf+uC^ubMCpL^(dm%G$@JPJT@liH9T3X{PAE3+Q2w<&RJ~n^; zoVEBqMl}4IYUn6>@Jb)SSlyXCfANa4g!kUi>`8#U%dfh1)*l&3jXX3*S=z5Z3!Yat zcM)8?eK4;J7*EO<%)q(Kb;rsN0^e7Se-voha$6T zVDRHKZ~mv>%v2sRjg=IY)@!`7@qdKVR;+d4eDqE_87wv~_!Ch|gZGs6afslg|Dl(XnS$Wb=U;t{lJ+W72y< zDdl-dv6~QgmE2^myW7{Q>$ziJ71%-JRmudbeD5V-IZ@^K8i3cZNY7~%49WL*q zOIAv=2Q{-i!|e*YwjiRVLqXNsO7!59U0ki4%I5f z7pXoA+kFwJY5C+EO76F8rf*y^Fph47^j=N!VWrN{(AD`&do#hN9;kJ;YL{caCoz_1 zC0o7c=eI3?5{+=<^*HLU0Lpk=A1V;S0FL~U1QLpT+csv8qdvs>PC5vPpeZOti!?qz z6>utiCJh1quqLD+#C1{;wQ*TI8>dkVY{mC0JW7D<;p(M2T}XHWjk7TWC$okXws3waWv#2moAc^v7B!>+)O1oHAf z^Q(-NHeL}QTyA`b{qi#O?55Oqwqx3kL+=@N)+Ds3IQPTG*$hgU;<2!%u+4fFjxkB;T=d{M64%Uqe?; zTDK#zDGIMrpN7__HO~7b-L|MI=vhO!b+s`>!h}N_=_RS(gxvinx0r>7<#D_57a&cp z$;q`$SA(ne0r&8cM+V9HpQ2!L?{$MX8_rameBmC(;wf8t#N{qs!HkAw?sq5jH3jpB zr1xY>Na`qB8|Vf#=$G8{WWcM&nX$~0`U$)sC4)sp@cxmPMQc4VoJiyL&mM)2xUO?x z%P5-u3{IWV$HSvDQCC)g*$+pzrVNEv*$f*^$Levx*;jbkySx>`XMhD(cDnHTwewO3 zGnk}At5xG8rL_bKhih2tVktF}kDGt1%B=8x|NcJnP(;-4kxa7Eg#&}N zUEArN@MQnJ8#bV5k49l_$yGX*>YzcgIF-nH4E-Z&+KGHkhJRAT)hw{yJZ#%*U6;oI zp7x_)jMtM)rothT&9A<+D}=3}hh^gV#o}>W$Eet+?V>-86Ucr72bb*LGoWFe5147| zu{gN`m4*XnRIrut4OerQ0Dx{@RxX!+u27l%BzP{C?UDH-sx^XW9xAZb?;XIk@DMu-pg9$0b-Jbv?@#}J1@CZ@umFh|Q!BiY z{|c)AR%9eB%amxij|e(KU*Wj|mfs16UD>5UZ!AwKlb;yVEAX_5jD3n3mN|NpWlvBM zs)(jj8R%0GRg4FYuRT;_@9rPq1@$ZLUi<+Ntn{6wD&>bHS<-5^V0Ac%FySt!ozI}a z!FDV1Zq~^9Dj<~@KJAWye|Q`_QFiao5YDAN-7>g1Sa<1;3wQA|rn_s^BCn%BU>UGa zwWFuPLK;iT#njqNM8W`fm9nzG$&saPW;Zk{>tDNQO{ug0B&1MHBWXpn{Jf1y1RTAo z!X_ZYjx3i__bGs=ql6Yd^O*{vmGmnG)PE;nWo+5PAqPX@y7h%dLMzmL#Ngi_-+87R zKKX7#ZbJ}>CJ}ImtQ@Tb2cJKhXGSONsSn`1x}OW`;+;P>UGvJ#!3=K5EN4TS%n8m#Hp8G$Q!X5X?ok z%X_b%k)|fnKs_nG|9ar%&r`tR+H_cxl1&a_b_Mu(0JR&ILn+L`7g8KR!e|0$ARZl$ zBF1nW?bG9U5Q$=__Ghsy=`sE}HZ?&25ca(}-Gn2V2xZJ~#Pua(w+lzBfl)_ndg_<1 zr~d>66H4?a_W(LN*s4g-o2R3+0}q^$s* z6@741kAyVLrl3zH-!)SH6$kHVo}7AGF={_tS?{odRm1liVmG??Z@cFy4u;|9&_#@5 z4NHzl3?IhPOpZk7?kP+XW$rz-Ycf*!DzBW=hJ#Ae?|=2Y$w|yjiYEBvt6P{I733t} zN^lTZX%MD3b|-Khf6;qsk8JD_Vf^AAl~aOlNj7$=aq*gc>L!%8N0P*FlxkD68k;_N z_^!-RJKD)Dpi3R4jbN>bn#}xH*lmX9{xC>+h@34IR-3c~hU@E$+c@ulvpyMo+*0t$ zV9~XH>-IMKlmjL)x{{H+q1c6Rj*>evYDQX>{xIKMxbdy+dW7aNbB=SN$s`qcGF8C` z*BV!DNjYeCRS$Gad*Hg+bp+>==PX}30Cxfck#MxbB~E#py|N@ii-ROu-%8A= z-MUw3!_Lnx$ZjvFHQ=aB)=h9iHK7RNoBErr&vBbj?@ISFP9yRT-oX1HSP+^=9j-aO zk4^2zOnAU97!3-$0LB?RjxlqSrvNxzQ;K);1xxPi)Lx zq_1XmCsX(F@_XS%8RJ5k@6z<+l~8~8ZFdh z?SOYpLff#12HFAF4;dOrk9!PwFY8GZSkQ(-QCQS~v-LJU&dn zY#ffB2~o%5euXg27u0LY50t ziBBZDVounTj2}4CxS;@fgz_C~ZZgFTi=}K&oi^{2g8#{q4xQ*CN1dj(0v9u72m*0S zt6(isH!g2S;#_PzZ`j7>?28s8zsDM{Bx%K3-y zQV#2Wt^ADDHDwyl;)Q>5u5E*$x3AWcd=Os*J@N4o z@p^oD7$fNTYz`yF@3){36VzHQa;muczLCUqsDHoyM?|X5$*7`ya0cy7e%H zT5XdeWcP+<{S?7Ln}`hYp+e)6PY&hG)u>5r>IE+7_2qV%5wN+}J32U|ro1!;2fb8R z{zCtIh87$R)mE`S?uRwXSIWo8{W*Wi()u=wD{)$}w~y?r+6_MULUMV_8+3y<^$5+R zPhh>WJ`GI){_)Z`$Ul|qk28*p*B;N-e$gz&Zet4%&8i*M^p*Z;bB>Y8HujJ9+I8w@ zpU-;S_n(?rYXA{Z6>(q&Kyj9w7PXWbq~Iep4W;_&lA3v}Z$|T9Z&9=V&1t&&Om?kCNqb}LM;u#xnrGu}ZGZRL^>(k<7Odt*tyxs9F{IOgLZW>&bJI;^PiZZF1{ z#!M$ZPhml_Aoobime0J@a5;ClDSv0OdGjrth~XluVU5HH`qZ;X91DlHtO0j-9Au~6 zP-JTs;wSq@R}#eZcAf=v*LcuUvZHN2|9+MK)oHy9!xDJ}9j;8jp9WaQc8>w8t>kIZ zYhc=@X|a2`V7(2Sc>c-)G&pVPgEeZuLuFFv%rQ zn-v1V;oFttsM0!Bh8E5Zd<`a_5P3xx0d{WPo+w%z{FECT>E)lP6Q`M}0}P`B?Y6By zS9?Z)XOY3(_kNYyp=ie{Al(WJH#AWh9#D^jr|*02OzDMNWcPqQ@E*QrH<9a$+9T+% zPEEqR;beItWJM<=A)nG&Nu85073HtL*r!j+4e2(jICy(~s@1`7ZB(Su>c&T$Qt-l1 z^2M5uT-530Kl6xz?o)+L>zs}33K;$VZ;*$UOTzrk9)`6hkRY`+A?7|{6gduX*Cn|& zC%)>W1Q=R{pO#u3{#J4nM`}&JXA=;cSlLtE$IPy19v&bAJCbTw-ti|;uwr}J*Sdjr ze=gu-g-xK-JG}%DG%S$0091J0l!u1=KEHuG)>=}pH7oS(jPtqVwM+nYOKcoyj&1Fo z%inu;10#klXA7?__=ANT_k#`haams_YdPCn=gYB^3S6lbfUWF%fEKH|pVM~KA;7#= zB3~@8h8$)YV!{yR$XjPoJ;RifHJ?nFUEW4*s3k{I0=g#&N6fs?)>A9 z2qYTdV-xGN*-K(ItB!T?s=b;{_t+u2Ir8q=xtSglCZ_mGZ)DWu&FB1R0uvVl6n=p& z14qr{K9?dJww{X?DgjMiAot1BOdLp2WQlABdbmaYr_+yTx3UX4HlE`2;=&Ixqd7F) ztiHvDA^XCCjGus^5Jyb-`$zjoN22~8{G7p04gJGP7qMt5#4>-_JQFGh&>$0q^Q!Pa5x zw&QoqiRY0^zeL_r>4yau#hSW1EclOI(U_YnmwFff6eW=&@fdK2_UErhj?)A8Bj zfpgDg6E>C}P20!d6vS@G9-A7ZG;xz&_gc}T$NQ^DfP-v5z$vUeSwy%6#yaa z{Sg{2`FUTvRoA4Vxx8b9K0lIEZawj9vL@?K>vJpX`*-ozygIIN>#K2{66**4bY^%`uXrnIdFb5nNI( z8ELGU3l`G*8Q+k``p0My?1hWCjzf)~3O$xuI1BkI*#qEAH_~LuZj0lhs)rRegOD^iEbI;u zCEd()5qIb~3hl((CJ=dolCQ&_eyvzcb#Z#AI>oHOYxM|CPm|mQm_0vJ3|)_0j~@F9 zxI{Zzlj$dEQzv39CVyOUMz3)m39IZ_s*uL?&m~1)E=w`3=cb-x-iTSav2CXnkr{#2 zg+k)glQPA2qf{Pf_EisGH49^Pd7g1YaB|ar*3wIOZJ&MxwlTJ{@ef|lZ6`O~SMqAv z$AOvId5p?LvKH)uBqOyumHo#QjJfNg??b)$3sp!?t3*{FDFr;s5;H|JP6dAJ6@Ny}19MzKn>8u)jCk1{Td}T!#zs PfG>3wZRJu$%i#Y78=43z literal 0 HcmV?d00001 diff --git a/dev-docs/static/img/og-image-sm.png b/dev-docs/static/img/og-image-sm.png new file mode 100644 index 0000000000000000000000000000000000000000..5e88dba059f7f09db43886d3d4affca8340688cd GIT binary patch literal 77545 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�D$~l`8ghbnpaFyHqYKG~9i16z7$g=h^ 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≪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�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 bV)6$J{LTY@w*E+#Nn8IA2B!L8iTmLH$+OqQ literal 0 HcmV?d00001 diff --git a/dev-docs/static/img/pika.jpeg b/dev-docs/static/img/pika.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..455ed52a638a0b060fba54892835e8923d671bfd GIT binary patch literal 6250 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 Vod0WA{QK4aPuTdMX5h>5{{x3Dqs0IK literal 0 HcmV?d00001 diff --git a/src/packages/excalidraw/welcome-screen-overview.png b/dev-docs/static/img/welcome-screen-overview.png similarity index 100% rename from src/packages/excalidraw/welcome-screen-overview.png rename to dev-docs/static/img/welcome-screen-overview.png diff --git a/dev-docs/yarn.lock b/dev-docs/yarn.lock index 9b9337c9c..5aaa9689f 100644 --- a/dev-docs/yarn.lock +++ b/dev-docs/yarn.lock @@ -1191,10 +1191,10 @@ "@docsearch/css" "3.1.1" algoliasearch "^4.0.0" -"@docusaurus/core@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-2.0.0-rc.1.tgz#828d93d241171565d8947a9ab404091e04759141" - integrity sha512-b9FX0Z+EddfQ6wAiNh+Wx4fysKfcvEcWJrZ5USROn3C+EVU5P4luaa8mwWK//O+hTwD9ur7/A44IZ/tWCTAoLQ== +"@docusaurus/core@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-2.2.0.tgz#64c9ee31502c23b93c869f8188f73afaf5fd4867" + integrity sha512-Vd6XOluKQqzG12fEs9prJgDtyn6DPok9vmUWDR2E6/nV5Fl9SVkhEQOBxwObjk3kQh7OY7vguFaLh0jqdApWsA== dependencies: "@babel/core" "^7.18.6" "@babel/generator" "^7.18.7" @@ -1206,13 +1206,13 @@ "@babel/runtime" "^7.18.6" "@babel/runtime-corejs3" "^7.18.6" "@babel/traverse" "^7.18.8" - "@docusaurus/cssnano-preset" "2.0.0-rc.1" - "@docusaurus/logger" "2.0.0-rc.1" - "@docusaurus/mdx-loader" "2.0.0-rc.1" + "@docusaurus/cssnano-preset" "2.2.0" + "@docusaurus/logger" "2.2.0" + "@docusaurus/mdx-loader" "2.2.0" "@docusaurus/react-loadable" "5.5.2" - "@docusaurus/utils" "2.0.0-rc.1" - "@docusaurus/utils-common" "2.0.0-rc.1" - "@docusaurus/utils-validation" "2.0.0-rc.1" + "@docusaurus/utils" "2.2.0" + "@docusaurus/utils-common" "2.2.0" + "@docusaurus/utils-validation" "2.2.0" "@slorber/static-site-generator-webpack-plugin" "^4.0.7" "@svgr/webpack" "^6.2.1" autoprefixer "^10.4.7" @@ -1268,33 +1268,33 @@ webpack-merge "^5.8.0" webpackbar "^5.0.2" -"@docusaurus/cssnano-preset@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-2.0.0-rc.1.tgz#76bbd7f6912779a0667f8f2fd8fc1a05618a6148" - integrity sha512-9/KmQvF+eTlMqUqG6UcXbRgxbGed/8bQInXuKEs+95/jI6jO/3xSzuRwuHHHP0naUvSVWjnNI9jngPrQerXE5w== +"@docusaurus/cssnano-preset@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-2.2.0.tgz#fc05044659051ae74ab4482afcf4a9936e81d523" + integrity sha512-mAAwCo4n66TMWBH1kXnHVZsakW9VAXJzTO4yZukuL3ro4F+JtkMwKfh42EG75K/J/YIFQG5I/Bzy0UH/hFxaTg== dependencies: cssnano-preset-advanced "^5.3.8" postcss "^8.4.14" postcss-sort-media-queries "^4.2.1" tslib "^2.4.0" -"@docusaurus/logger@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-2.0.0-rc.1.tgz#db95e9b15bc243695830a5b791c0eff705ef1b54" - integrity sha512-daa3g+SXuO9K60PVMiSUmDEK9Vro+Ed7i7uF8CH6QQJLcNZy/zJc0Xz62eH7ip1x77fmeb6Rg4Us1TqTFc9AbQ== +"@docusaurus/logger@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-2.2.0.tgz#ea2f7feda7b8675485933b87f06d9c976d17423f" + integrity sha512-DF3j1cA5y2nNsu/vk8AG7xwpZu6f5MKkPPMaaIbgXLnWGfm6+wkOeW7kNrxnM95YOhKUkJUophX69nGUnLsm0A== dependencies: chalk "^4.1.2" tslib "^2.4.0" -"@docusaurus/mdx-loader@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-2.0.0-rc.1.tgz#e78d7d416aacc289f2427c5ccdb9145820acb0cb" - integrity sha512-8Fg0c/ceu39knmr7w0dutm7gq3YxKYCqWVS2cB/cPATzChCCNH/AGLfBT6sz/Z4tjVXE+NyREq2pfOFvkhjVXg== +"@docusaurus/mdx-loader@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-2.2.0.tgz#fd558f429e5d9403d284bd4214e54d9768b041a0" + integrity sha512-X2bzo3T0jW0VhUU+XdQofcEeozXOTmKQMvc8tUnWRdTnCvj4XEcBVdC3g+/jftceluiwSTNRAX4VBOJdNt18jA== dependencies: "@babel/parser" "^7.18.8" "@babel/traverse" "^7.18.8" - "@docusaurus/logger" "2.0.0-rc.1" - "@docusaurus/utils" "2.0.0-rc.1" + "@docusaurus/logger" "2.2.0" + "@docusaurus/utils" "2.2.0" "@mdx-js/mdx" "^1.6.22" escape-html "^1.0.3" file-loader "^6.2.0" @@ -1323,18 +1323,32 @@ react-helmet-async "*" react-loadable "npm:@docusaurus/react-loadable@5.5.2" -"@docusaurus/plugin-content-blog@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.0.0-rc.1.tgz#8ae5d5ec2da08c583a057bf2754a5b9278b3eb08" - integrity sha512-BVVrAGZujpjS/0rarY2o24rlylRRh2NZuM65kg0JNkkViF79SeEHsepog7IuHyoqGWPm1N/I7LpEp7k+gowZzQ== +"@docusaurus/module-type-aliases@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-2.2.0.tgz#1e23e54a1bbb6fde1961e4fa395b1b69f4803ba5" + integrity sha512-wDGW4IHKoOr9YuJgy7uYuKWrDrSpsUSDHLZnWQYM9fN7D5EpSmYHjFruUpKWVyxLpD/Wh0rW8hYZwdjJIQUQCQ== dependencies: - "@docusaurus/core" "2.0.0-rc.1" - "@docusaurus/logger" "2.0.0-rc.1" - "@docusaurus/mdx-loader" "2.0.0-rc.1" - "@docusaurus/types" "2.0.0-rc.1" - "@docusaurus/utils" "2.0.0-rc.1" - "@docusaurus/utils-common" "2.0.0-rc.1" - "@docusaurus/utils-validation" "2.0.0-rc.1" + "@docusaurus/react-loadable" "5.5.2" + "@docusaurus/types" "2.2.0" + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-router-config" "*" + "@types/react-router-dom" "*" + react-helmet-async "*" + react-loadable "npm:@docusaurus/react-loadable@5.5.2" + +"@docusaurus/plugin-content-blog@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.2.0.tgz#dc55982e76771f4e678ac10e26d10e1da2011dc1" + integrity sha512-0mWBinEh0a5J2+8ZJXJXbrCk1tSTNf7Nm4tYAl5h2/xx+PvH/Bnu0V+7mMljYm/1QlDYALNIIaT/JcoZQFUN3w== + dependencies: + "@docusaurus/core" "2.2.0" + "@docusaurus/logger" "2.2.0" + "@docusaurus/mdx-loader" "2.2.0" + "@docusaurus/types" "2.2.0" + "@docusaurus/utils" "2.2.0" + "@docusaurus/utils-common" "2.2.0" + "@docusaurus/utils-validation" "2.2.0" cheerio "^1.0.0-rc.12" feed "^4.2.2" fs-extra "^10.1.0" @@ -1345,18 +1359,18 @@ utility-types "^3.10.0" webpack "^5.73.0" -"@docusaurus/plugin-content-docs@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.0.0-rc.1.tgz#2dda88166bf21b0eeb3821ef748059b20c8c49f7" - integrity sha512-Yk5Hu6uaw3tRplzJnbDygwRhmZ3PCzEXD4SJpBA6cPC73ylfqOEh6qhiU+BWhMTtDXNhY+athk5Kycfk3DW1aQ== +"@docusaurus/plugin-content-docs@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.2.0.tgz#0fcb85226fcdb80dc1e2d4a36ef442a650dcc84d" + integrity sha512-BOazBR0XjzsHE+2K1wpNxz5QZmrJgmm3+0Re0EVPYFGW8qndCWGNtXW/0lGKhecVPML8yyFeAmnUCIs7xM2wPw== dependencies: - "@docusaurus/core" "2.0.0-rc.1" - "@docusaurus/logger" "2.0.0-rc.1" - "@docusaurus/mdx-loader" "2.0.0-rc.1" - "@docusaurus/module-type-aliases" "2.0.0-rc.1" - "@docusaurus/types" "2.0.0-rc.1" - "@docusaurus/utils" "2.0.0-rc.1" - "@docusaurus/utils-validation" "2.0.0-rc.1" + "@docusaurus/core" "2.2.0" + "@docusaurus/logger" "2.2.0" + "@docusaurus/mdx-loader" "2.2.0" + "@docusaurus/module-type-aliases" "2.2.0" + "@docusaurus/types" "2.2.0" + "@docusaurus/utils" "2.2.0" + "@docusaurus/utils-validation" "2.2.0" "@types/react-router-config" "^5.0.6" combine-promises "^1.1.0" fs-extra "^10.1.0" @@ -1367,84 +1381,84 @@ utility-types "^3.10.0" webpack "^5.73.0" -"@docusaurus/plugin-content-pages@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.0.0-rc.1.tgz#2be82f53d6c77e6aa66787726c30dc60b210e6f8" - integrity sha512-FdO79WC5hfWDQu3/CTFLRQzTNc0e5n+HNzavm2MNkSzGV08BFJ6RAkbPbtra5CWef+6iXZav6D/tzv2jDPvLzA== +"@docusaurus/plugin-content-pages@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.2.0.tgz#e3f40408787bbe229545dd50595f87e1393bc3ae" + integrity sha512-+OTK3FQHk5WMvdelz8v19PbEbx+CNT6VSpx7nVOvMNs5yJCKvmqBJBQ2ZSxROxhVDYn+CZOlmyrC56NSXzHf6g== dependencies: - "@docusaurus/core" "2.0.0-rc.1" - "@docusaurus/mdx-loader" "2.0.0-rc.1" - "@docusaurus/types" "2.0.0-rc.1" - "@docusaurus/utils" "2.0.0-rc.1" - "@docusaurus/utils-validation" "2.0.0-rc.1" + "@docusaurus/core" "2.2.0" + "@docusaurus/mdx-loader" "2.2.0" + "@docusaurus/types" "2.2.0" + "@docusaurus/utils" "2.2.0" + "@docusaurus/utils-validation" "2.2.0" fs-extra "^10.1.0" tslib "^2.4.0" webpack "^5.73.0" -"@docusaurus/plugin-debug@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-2.0.0-rc.1.tgz#73c06ad08d66810941e456d50b07be008f5235cb" - integrity sha512-aOsyYrPMbnsyqHwsVZ+0frrMRtnYqm4eaJpG4sC/6LYAJ07IDRQ9j3GOku2dKr5GsFK1Vx7VlE6ZLwe0MaGstg== +"@docusaurus/plugin-debug@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-2.2.0.tgz#b38741d2c492f405fee01ee0ef2e0029cedb689a" + integrity sha512-p9vOep8+7OVl6r/NREEYxf4HMAjV8JMYJ7Bos5fCFO0Wyi9AZEo0sCTliRd7R8+dlJXZEgcngSdxAUo/Q+CJow== dependencies: - "@docusaurus/core" "2.0.0-rc.1" - "@docusaurus/types" "2.0.0-rc.1" - "@docusaurus/utils" "2.0.0-rc.1" + "@docusaurus/core" "2.2.0" + "@docusaurus/types" "2.2.0" + "@docusaurus/utils" "2.2.0" fs-extra "^10.1.0" react-json-view "^1.21.3" tslib "^2.4.0" -"@docusaurus/plugin-google-analytics@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.0.0-rc.1.tgz#0136cc7534573ca56e023178ec2bda5c1e89ce71" - integrity sha512-f+G8z5OJWfg5QqWDLIdcN2SDoK5J5Gg8HMrqCI6Pfl+rxPb5I1niA+/UkAM+kMCpnekvhSt5AWz2fgkRenkPLA== +"@docusaurus/plugin-google-analytics@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.2.0.tgz#63c7137eff5a1208d2059fea04b5207c037d7954" + integrity sha512-+eZVVxVeEnV5nVQJdey9ZsfyEVMls6VyWTIj8SmX0k5EbqGvnIfET+J2pYEuKQnDIHxy+syRMoRM6AHXdHYGIg== dependencies: - "@docusaurus/core" "2.0.0-rc.1" - "@docusaurus/types" "2.0.0-rc.1" - "@docusaurus/utils-validation" "2.0.0-rc.1" + "@docusaurus/core" "2.2.0" + "@docusaurus/types" "2.2.0" + "@docusaurus/utils-validation" "2.2.0" tslib "^2.4.0" -"@docusaurus/plugin-google-gtag@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.0.0-rc.1.tgz#61698fdc41a4ace912fb8f6c834efd288edad3c0" - integrity sha512-yE1Et9hhhX9qMRnMJzpNq0854qIYiSEc2dZaXNk537HN7Q0rKkr/YONUHz2iqNYwPX2hGOY4LdpTxlMP88uVhA== +"@docusaurus/plugin-google-gtag@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.2.0.tgz#7b086d169ac5fe9a88aca10ab0fd2bf00c6c6b12" + integrity sha512-6SOgczP/dYdkqUMGTRqgxAS1eTp6MnJDAQMy8VCF1QKbWZmlkx4agHDexihqmYyCujTYHqDAhm1hV26EET54NQ== dependencies: - "@docusaurus/core" "2.0.0-rc.1" - "@docusaurus/types" "2.0.0-rc.1" - "@docusaurus/utils-validation" "2.0.0-rc.1" + "@docusaurus/core" "2.2.0" + "@docusaurus/types" "2.2.0" + "@docusaurus/utils-validation" "2.2.0" tslib "^2.4.0" -"@docusaurus/plugin-sitemap@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.0.0-rc.1.tgz#0b638e774b253d90e9f2d11663e961250f557bc4" - integrity sha512-5JmbNpssUF03odFM4ArvIsrO9bv7HnAJ0VtefXhh0WBpaFs8NgI3rTkCTFimvtRQjDR9U2bh23fXz2vjQQz6oA== +"@docusaurus/plugin-sitemap@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.2.0.tgz#876da60937886032d63143253d420db6a4b34773" + integrity sha512-0jAmyRDN/aI265CbWZNZuQpFqiZuo+5otk2MylU9iVrz/4J7gSc+ZJ9cy4EHrEsW7PV8s1w18hIEsmcA1YgkKg== dependencies: - "@docusaurus/core" "2.0.0-rc.1" - "@docusaurus/logger" "2.0.0-rc.1" - "@docusaurus/types" "2.0.0-rc.1" - "@docusaurus/utils" "2.0.0-rc.1" - "@docusaurus/utils-common" "2.0.0-rc.1" - "@docusaurus/utils-validation" "2.0.0-rc.1" + "@docusaurus/core" "2.2.0" + "@docusaurus/logger" "2.2.0" + "@docusaurus/types" "2.2.0" + "@docusaurus/utils" "2.2.0" + "@docusaurus/utils-common" "2.2.0" + "@docusaurus/utils-validation" "2.2.0" fs-extra "^10.1.0" sitemap "^7.1.1" tslib "^2.4.0" -"@docusaurus/preset-classic@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-2.0.0-rc.1.tgz#5e5b1cf80b3dd4e2c3f824c78a111f105858d853" - integrity sha512-5jjTVZkhArjyoNHwCI9x4PSG0zPmBJILjZLVrxPcHpm/K0ltkYcp6J3GxYpf5EbMuOh5+yCWM63cSshGcNOo3Q== +"@docusaurus/preset-classic@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-2.2.0.tgz#bece5a043eeb74430f7c6c7510000b9c43669eb7" + integrity sha512-yKIWPGNx7BT8v2wjFIWvYrS+nvN04W+UameSFf8lEiJk6pss0kL6SG2MRvyULiI3BDxH+tj6qe02ncpSPGwumg== dependencies: - "@docusaurus/core" "2.0.0-rc.1" - "@docusaurus/plugin-content-blog" "2.0.0-rc.1" - "@docusaurus/plugin-content-docs" "2.0.0-rc.1" - "@docusaurus/plugin-content-pages" "2.0.0-rc.1" - "@docusaurus/plugin-debug" "2.0.0-rc.1" - "@docusaurus/plugin-google-analytics" "2.0.0-rc.1" - "@docusaurus/plugin-google-gtag" "2.0.0-rc.1" - "@docusaurus/plugin-sitemap" "2.0.0-rc.1" - "@docusaurus/theme-classic" "2.0.0-rc.1" - "@docusaurus/theme-common" "2.0.0-rc.1" - "@docusaurus/theme-search-algolia" "2.0.0-rc.1" - "@docusaurus/types" "2.0.0-rc.1" + "@docusaurus/core" "2.2.0" + "@docusaurus/plugin-content-blog" "2.2.0" + "@docusaurus/plugin-content-docs" "2.2.0" + "@docusaurus/plugin-content-pages" "2.2.0" + "@docusaurus/plugin-debug" "2.2.0" + "@docusaurus/plugin-google-analytics" "2.2.0" + "@docusaurus/plugin-google-gtag" "2.2.0" + "@docusaurus/plugin-sitemap" "2.2.0" + "@docusaurus/theme-classic" "2.2.0" + "@docusaurus/theme-common" "2.2.0" + "@docusaurus/theme-search-algolia" "2.2.0" + "@docusaurus/types" "2.2.0" "@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2": version "5.5.2" @@ -1454,23 +1468,23 @@ "@types/react" "*" prop-types "^15.6.2" -"@docusaurus/theme-classic@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-2.0.0-rc.1.tgz#4ab30745e6b03b0f277837debae786a0a83aee6a" - integrity sha512-qNiz7ieeq3AC+V8TbW6S63pWLJph1CbzWDDPTqxDLHgA8VQaNaSmJM8S92pH+yKALRb9u14ogjjYYc75Nj2JmQ== +"@docusaurus/theme-classic@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-2.2.0.tgz#a048bb1bc077dee74b28bec25f4b84b481863742" + integrity sha512-kjbg/qJPwZ6H1CU/i9d4l/LcFgnuzeiGgMQlt6yPqKo0SOJIBMPuz7Rnu3r/WWbZFPi//o8acclacOzmXdUUEg== dependencies: - "@docusaurus/core" "2.0.0-rc.1" - "@docusaurus/mdx-loader" "2.0.0-rc.1" - "@docusaurus/module-type-aliases" "2.0.0-rc.1" - "@docusaurus/plugin-content-blog" "2.0.0-rc.1" - "@docusaurus/plugin-content-docs" "2.0.0-rc.1" - "@docusaurus/plugin-content-pages" "2.0.0-rc.1" - "@docusaurus/theme-common" "2.0.0-rc.1" - "@docusaurus/theme-translations" "2.0.0-rc.1" - "@docusaurus/types" "2.0.0-rc.1" - "@docusaurus/utils" "2.0.0-rc.1" - "@docusaurus/utils-common" "2.0.0-rc.1" - "@docusaurus/utils-validation" "2.0.0-rc.1" + "@docusaurus/core" "2.2.0" + "@docusaurus/mdx-loader" "2.2.0" + "@docusaurus/module-type-aliases" "2.2.0" + "@docusaurus/plugin-content-blog" "2.2.0" + "@docusaurus/plugin-content-docs" "2.2.0" + "@docusaurus/plugin-content-pages" "2.2.0" + "@docusaurus/theme-common" "2.2.0" + "@docusaurus/theme-translations" "2.2.0" + "@docusaurus/types" "2.2.0" + "@docusaurus/utils" "2.2.0" + "@docusaurus/utils-common" "2.2.0" + "@docusaurus/utils-validation" "2.2.0" "@mdx-js/react" "^1.6.22" clsx "^1.2.1" copy-text-to-clipboard "^3.0.1" @@ -1485,17 +1499,17 @@ tslib "^2.4.0" utility-types "^3.10.0" -"@docusaurus/theme-common@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-2.0.0-rc.1.tgz#ea5d9732a16b03b488555e50107161bfa2abad98" - integrity sha512-1r9ZLKD9SeoCYVzWzcdR79Dia4ANlrlRjNl6uzETOEybjK6FF7yEa9Yra8EJcOCbi3coyYz5xFh/r1YHFTFHug== +"@docusaurus/theme-common@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-2.2.0.tgz#2303498d80448aafdd588b597ce9d6f4cfa930e4" + integrity sha512-R8BnDjYoN90DCL75gP7qYQfSjyitXuP9TdzgsKDmSFPNyrdE3twtPNa2dIN+h+p/pr+PagfxwWbd6dn722A1Dw== dependencies: - "@docusaurus/mdx-loader" "2.0.0-rc.1" - "@docusaurus/module-type-aliases" "2.0.0-rc.1" - "@docusaurus/plugin-content-blog" "2.0.0-rc.1" - "@docusaurus/plugin-content-docs" "2.0.0-rc.1" - "@docusaurus/plugin-content-pages" "2.0.0-rc.1" - "@docusaurus/utils" "2.0.0-rc.1" + "@docusaurus/mdx-loader" "2.2.0" + "@docusaurus/module-type-aliases" "2.2.0" + "@docusaurus/plugin-content-blog" "2.2.0" + "@docusaurus/plugin-content-docs" "2.2.0" + "@docusaurus/plugin-content-pages" "2.2.0" + "@docusaurus/utils" "2.2.0" "@types/history" "^4.7.11" "@types/react" "*" "@types/react-router-config" "*" @@ -1505,19 +1519,34 @@ tslib "^2.4.0" utility-types "^3.10.0" -"@docusaurus/theme-search-algolia@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.0.0-rc.1.tgz#e78c0aeaea6a3717ae3a6ecd75a8652bd7c8e974" - integrity sha512-H5yq6V/B4qo6GZrDKMbeSpk3T9e9K2MliDzLonRu0w3QHW9orVGe0c/lZvRbGlDZjnsOo7XGddhXXIDWGwnpaA== +"@docusaurus/theme-live-codeblock@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-live-codeblock/-/theme-live-codeblock-2.2.0.tgz#a507e496a1a74d261beee30ad072e4341310809a" + integrity sha512-4XRFxfZGcyqmbLmNbnbZ2ZOsoY7FYCJUZKsYW5yzhZYjmjGg7lkdJH5trt9otUoKBsZopBpPWvcDZwCu1SENYg== + dependencies: + "@docusaurus/core" "2.2.0" + "@docusaurus/theme-common" "2.2.0" + "@docusaurus/theme-translations" "2.2.0" + "@docusaurus/utils-validation" "2.2.0" + "@philpl/buble" "^0.19.7" + clsx "^1.2.1" + fs-extra "^10.1.0" + react-live "2.2.3" + tslib "^2.4.0" + +"@docusaurus/theme-search-algolia@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.2.0.tgz#77fd9f7a600917e6024fe3ac7fb6cfdf2ce84737" + integrity sha512-2h38B0tqlxgR2FZ9LpAkGrpDWVdXZ7vltfmTdX+4RsDs3A7khiNsmZB+x/x6sA4+G2V2CvrsPMlsYBy5X+cY1w== dependencies: "@docsearch/react" "^3.1.1" - "@docusaurus/core" "2.0.0-rc.1" - "@docusaurus/logger" "2.0.0-rc.1" - "@docusaurus/plugin-content-docs" "2.0.0-rc.1" - "@docusaurus/theme-common" "2.0.0-rc.1" - "@docusaurus/theme-translations" "2.0.0-rc.1" - "@docusaurus/utils" "2.0.0-rc.1" - "@docusaurus/utils-validation" "2.0.0-rc.1" + "@docusaurus/core" "2.2.0" + "@docusaurus/logger" "2.2.0" + "@docusaurus/plugin-content-docs" "2.2.0" + "@docusaurus/theme-common" "2.2.0" + "@docusaurus/theme-translations" "2.2.0" + "@docusaurus/utils" "2.2.0" + "@docusaurus/utils-validation" "2.2.0" algoliasearch "^4.13.1" algoliasearch-helper "^3.10.0" clsx "^1.2.1" @@ -1527,10 +1556,10 @@ tslib "^2.4.0" utility-types "^3.10.0" -"@docusaurus/theme-translations@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-2.0.0-rc.1.tgz#bd647f78c741ee7f6c6d2cbbd3e3f282ef2f89ad" - integrity sha512-JLhNdlnbQhxVQzOnLyiCaTzKFa1lpVrM3nCrkGQKscoG2rY6ARGYMgMN2DkoH6hm7TflQ8+PE1S5MzzASeLs4Q== +"@docusaurus/theme-translations@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-2.2.0.tgz#5fbd4693679806f80c26eeae1381e1f2c23d83e7" + integrity sha512-3T140AG11OjJrtKlY4pMZ5BzbGRDjNs2co5hJ6uYJG1bVWlhcaFGqkaZ5lCgKflaNHD7UHBHU9Ec5f69jTdd6w== dependencies: fs-extra "^10.1.0" tslib "^2.4.0" @@ -1549,30 +1578,44 @@ webpack "^5.73.0" webpack-merge "^5.8.0" -"@docusaurus/utils-common@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-2.0.0-rc.1.tgz#3e233a28794325d5d9d3af3f7b1c22b59aa8b847" - integrity sha512-+iZICpeFPZJ9oGJXuG92WTWee6WRnVx5BdzlcfuKf/f5KQX8PvwXR2tDME78FGGhShB8zr+vjuNEXuLvXT7j2A== +"@docusaurus/types@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-2.2.0.tgz#02c577a4041ab7d058a3c214ccb13647e21a9857" + integrity sha512-b6xxyoexfbRNRI8gjblzVOnLr4peCJhGbYGPpJ3LFqpi5nsFfoK4mmDLvWdeah0B7gmJeXabN7nQkFoqeSdmOw== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + commander "^5.1.0" + joi "^17.6.0" + react-helmet-async "^1.3.0" + utility-types "^3.10.0" + webpack "^5.73.0" + webpack-merge "^5.8.0" + +"@docusaurus/utils-common@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-2.2.0.tgz#a401c1b93a8697dd566baf6ac64f0fdff1641a78" + integrity sha512-qebnerHp+cyovdUseDQyYFvMW1n1nv61zGe5JJfoNQUnjKuApch3IVsz+/lZ9a38pId8kqehC1Ao2bW/s0ntDA== dependencies: tslib "^2.4.0" -"@docusaurus/utils-validation@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-2.0.0-rc.1.tgz#dded12f036cda8a54a19e01694b35859fe0cf1d5" - integrity sha512-lj36gm9Ksu4tt/EUeLDWoMbXe3sfBxeIPIUUdqYcBYkF/rpQkh+uL/dncjNGiw6uvBOqXhOfsFVP045HtgShVw== +"@docusaurus/utils-validation@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-2.2.0.tgz#04d4d103137ad0145883971d3aa497f4a1315f25" + integrity sha512-I1hcsG3yoCkasOL5qQAYAfnmVoLei7apugT6m4crQjmDGxq+UkiRrq55UqmDDyZlac/6ax/JC0p+usZ6W4nVyg== dependencies: - "@docusaurus/logger" "2.0.0-rc.1" - "@docusaurus/utils" "2.0.0-rc.1" + "@docusaurus/logger" "2.2.0" + "@docusaurus/utils" "2.2.0" joi "^17.6.0" js-yaml "^4.1.0" tslib "^2.4.0" -"@docusaurus/utils@2.0.0-rc.1": - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-2.0.0-rc.1.tgz#53584b800df9e13864d5ef1a76aa7655a90ec86e" - integrity sha512-ym9I1OwIYbKs1LGaUajaA/vDG8VweJj/6YoZjHp+eDQHhTRIrHXiYoGDqorafRhftKwnA1EnyomuXpNd9bq8Gg== +"@docusaurus/utils@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-2.2.0.tgz#3d6f9b7a69168d5c92d371bf21c556a4f50d1da6" + integrity sha512-oNk3cjvx7Tt1Lgh/aeZAmFpGV2pDr5nHKrBVx6hTkzGhrnMuQqLt6UPlQjdYQ3QHXwyF/ZtZMO1D5Pfi0lu7SA== dependencies: - "@docusaurus/logger" "2.0.0-rc.1" + "@docusaurus/logger" "2.2.0" "@svgr/webpack" "^6.2.1" file-loader "^6.2.0" fs-extra "^10.1.0" @@ -1588,6 +1631,11 @@ url-loader "^4.1.1" webpack "^5.73.0" +"@excalidraw/excalidraw@0.14.2": + version "0.14.2" + resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.14.2.tgz#150cb4b7a1bf0d11cd64295936c930e7e0db8375" + integrity sha512-8LdjpTBWEK5waDWB7Bt/G9YBI4j0OxkstUhvaDGz7dwQGfzF6FW5CXBoYHNEoX0qmb+Fg/NPOlZ7FrKsrSVCqg== + "@hapi/hoek@^9.0.0": version "9.3.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" @@ -1709,6 +1757,21 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@philpl/buble@^0.19.7": + version "0.19.7" + resolved "https://registry.yarnpkg.com/@philpl/buble/-/buble-0.19.7.tgz#27231e6391393793b64bc1c982fc7b593198b893" + integrity sha512-wKTA2DxAGEW+QffRQvOhRQ0VBiYU2h2p8Yc1oBNlqSKws48/8faxqKNIuub0q4iuyTuLwtB8EkwiKwhlfV1PBA== + dependencies: + acorn "^6.1.1" + acorn-class-fields "^0.2.1" + acorn-dynamic-import "^4.0.0" + acorn-jsx "^5.0.1" + chalk "^2.4.2" + magic-string "^0.25.2" + minimist "^1.2.0" + os-homedir "^1.0.1" + regexpu-core "^4.5.4" + "@polka/url@^1.0.0-next.20": version "1.0.0-next.21" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" @@ -2242,16 +2305,36 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" +acorn-class-fields@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/acorn-class-fields/-/acorn-class-fields-0.2.1.tgz#748058bceeb0ef25164bbc671993984083f5a085" + integrity sha512-US/kqTe0H8M4LN9izoL+eykVAitE68YMuYZ3sHn3i1fjniqR7oQ3SPvuMK/VT1kjOQHrx5Q88b90TtOKgAv2hQ== + +acorn-dynamic-import@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" + integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw== + acorn-import-assertions@^1.7.6: version "1.8.0" resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== +acorn-jsx@^5.0.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + acorn-walk@^8.0.0: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== +acorn@^6.1.1: + version "6.4.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" + integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== + acorn@^8.0.4, acorn@^8.5.0, acorn@^8.7.1: version "8.8.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" @@ -2618,6 +2701,18 @@ browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4 node-releases "^2.0.6" update-browserslist-db "^1.0.4" +buble@0.19.6: + version "0.19.6" + resolved "https://registry.yarnpkg.com/buble/-/buble-0.19.6.tgz#915909b6bd5b11ee03b1c885ec914a8b974d34d3" + integrity sha512-9kViM6nJA1Q548Jrd06x0geh+BG2ru2+RMDkIHHgJY/8AcyCs34lTHwra9BX7YdPrZXd5aarkpr/SY8bmPgPdg== + dependencies: + chalk "^2.4.1" + magic-string "^0.25.1" + minimist "^1.2.0" + os-homedir "^1.0.1" + regexpu-core "^4.2.0" + vlq "^1.0.0" + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -2697,7 +2792,7 @@ ccount@^1.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== -chalk@^2.0.0: +chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -2754,7 +2849,7 @@ cheerio@^1.0.0-rc.12: parse5 "^7.0.0" parse5-htmlparser2-tree-adapter "^7.0.0" -chokidar@^3.4.2, chokidar@^3.5.3: +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.2, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -2905,6 +3000,16 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== +component-props@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/component-props/-/component-props-1.1.1.tgz#f9b7df9b9927b6e6d97c9bd272aa867670f34944" + integrity sha512-69pIRJs9fCCHRqCz3390YF2LV1Lu6iEMZ5zuVqqUn+G20V9BNXlMs0cWawWeW9g4Ynmg29JmkG6R7/lUJoGd1Q== + +component-xor@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/component-xor/-/component-xor-0.0.4.tgz#c55d83ccc1b94cd5089a4e93fa7891c7263e59aa" + integrity sha512-ZIt6sla8gfo+AFVRZoZOertcnD5LJaY2T9CKE2j13NJxQt/mUafD69Bl7/Y4AnpI2LGjiXH7cOfJDx/n2G9edA== + compressible@~2.0.16: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" @@ -3016,6 +3121,11 @@ core-js-pure@^3.20.2: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.24.0.tgz#10eeb90dbf0d670a6b22b081aecc7deb2faec7e1" integrity sha512-uzMmW8cRh7uYw4JQtzqvGWRyC2T5+4zipQLQdi2FmiRqP83k3d6F3stv2iAlNhOs6cXN401FCD5TL0vvleuHgA== +core-js@^2.4.1: + version "2.6.12" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" + integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== + core-js@^3.23.3: version "3.24.0" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.24.0.tgz#4928d4e99c593a234eb1a1f9abd3122b04d3ac57" @@ -3345,6 +3455,13 @@ dns-packet@^5.2.2: dependencies: "@leichtgewicht/ip-codec" "^2.0.1" +docusaurus-plugin-sass@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/docusaurus-plugin-sass/-/docusaurus-plugin-sass-0.2.3.tgz#5b61f7e560d236cfc1531ed497ac32fc166fc5e2" + integrity sha512-FbaE06K8NF8SPUYTwiG+83/jkXrwHJ/Afjqz3SUIGon6QvFwSSoKOcoxGQmUBnjTOk+deUONDx8jNWsegFJcBQ== + dependencies: + sass-loader "^10.1.1" + dom-converter@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" @@ -3352,6 +3469,14 @@ dom-converter@^0.2.0: dependencies: utila "~0.4" +dom-iterator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dom-iterator/-/dom-iterator-1.0.0.tgz#9c09899846ec41c2d257adc4d6015e4759ef05ad" + integrity sha512-7dsMOQI07EMU98gQM8NSB3GsAiIeBYIPKpnxR3c9xOvdvBjChAcOM0iJ222I3p5xyiZO9e5oggkNaCusuTdYig== + dependencies: + component-props "1.1.1" + component-xor "0.0.4" + dom-serializer@^1.0.1: version "1.4.1" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" @@ -4340,6 +4465,11 @@ immer@^9.0.7: resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.15.tgz#0b9169e5b1d22137aba7d43f8a81a495dd1b62dc" integrity sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ== +immutable@^4.0.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.2.tgz#2da9ff4384a4330c36d4d1bc88e90f9e0b0ccd16" + integrity sha512-fTMKDwtbvO5tldky9QZ2fMX7slR0mYpY5nbnFWYp0fOzDhHqhgIw9KoYgxLWsoNTS9ZHGauHj18DTyEw6BK3Og== + import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -4722,7 +4852,7 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -klona@^2.0.5: +klona@^2.0.4, klona@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc" integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ== @@ -4851,6 +4981,13 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +magic-string@^0.25.1, magic-string@^0.25.2: + version "0.25.9" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" + integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== + dependencies: + sourcemap-codec "^1.4.8" + make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -5208,6 +5345,11 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== +os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ== + p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" @@ -5737,7 +5879,7 @@ pretty-time@^1.1.0: resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e" integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== -prism-react-renderer@^1.3.5: +prism-react-renderer@^1.0.1, prism-react-renderer@^1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.5.tgz#786bb69aa6f73c32ba1ee813fbe17a0115435085" integrity sha512-IJ+MSwBWKG+SM3b2SUfdrhC+gu01QkV2KmRQgREThBfSQRoufqRfxfHUxpG1WcaFjP+kojcFyO9Qqtpgt3qLCg== @@ -5767,7 +5909,7 @@ prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -5967,6 +6109,19 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== +react-live@2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/react-live/-/react-live-2.2.3.tgz#260f99194213799f0005e473e7a4154c699d6a7c" + integrity sha512-tpKruvfytNETuzO3o1mrQUj180GVrq35IE8F5gH1NJVPt4szYCx83/dOSCOyjgRhhc3gQvl0pQ3k/CjOjwJkKQ== + dependencies: + buble "0.19.6" + core-js "^2.4.1" + dom-iterator "^1.0.0" + prism-react-renderer "^1.0.1" + prop-types "^15.5.8" + react-simple-code-editor "^0.10.0" + unescape "^1.0.1" + react-loadable-ssr-addon-v5-slorber@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz#2cdc91e8a744ffdf9e3556caabeb6e4278689883" @@ -6010,6 +6165,11 @@ react-router@5.3.3, react-router@^5.3.3: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" +react-simple-code-editor@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/react-simple-code-editor/-/react-simple-code-editor-0.10.0.tgz#73e7ac550a928069715482aeb33ccba36efe2373" + integrity sha512-bL5W5mAxSW6+cLwqqVWY47Silqgy2DKDTR4hDBrLrUqC5BXc29YVx17l2IZk5v36VcDEq1Bszu2oHm1qBwKqBA== + react-textarea-autosize@^8.3.2: version "8.3.4" resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.3.4.tgz#270a343de7ad350534141b02c9cb78903e553524" @@ -6082,6 +6242,13 @@ regenerate-unicode-properties@^10.0.1: dependencies: regenerate "^1.4.2" +regenerate-unicode-properties@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326" + integrity sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA== + dependencies: + regenerate "^1.4.2" + regenerate@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" @@ -6099,6 +6266,18 @@ regenerator-transform@^0.15.0: dependencies: "@babel/runtime" "^7.8.4" +regexpu-core@^4.2.0, regexpu-core@^4.5.4: + version "4.8.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.8.0.tgz#e5605ba361b67b1718478501327502f4479a98f0" + integrity sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^9.0.0" + regjsgen "^0.5.2" + regjsparser "^0.7.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.0.0" + regexpu-core@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.1.0.tgz#2f8504c3fd0ebe11215783a41541e21c79942c6d" @@ -6125,11 +6304,23 @@ registry-url@^5.0.0: dependencies: rc "^1.2.8" +regjsgen@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" + integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== + regjsgen@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.6.0.tgz#83414c5354afd7d6627b16af5f10f41c4e71808d" integrity sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA== +regjsparser@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.7.0.tgz#a6b667b54c885e18b52554cb4960ef71187e9968" + integrity sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ== + dependencies: + jsesc "~0.5.0" + regjsparser@^0.8.2: version "0.8.4" resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.8.4.tgz#8a14285ffcc5de78c5b95d62bbf413b6bc132d5f" @@ -6317,6 +6508,26 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sass-loader@^10.1.1: + version "10.4.1" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.4.1.tgz#bea4e173ddf512c9d7f53e9ec686186146807cbf" + integrity sha512-aX/iJZTTpNUNx/OSYzo2KsjIUQHqvWsAhhUijFjAPdZTEhstjZI9zTNvkTTwsx+uNUJqUwOw5gacxQMx4hJxGQ== + dependencies: + klona "^2.0.4" + loader-utils "^2.0.0" + neo-async "^2.6.2" + schema-utils "^3.0.0" + semver "^7.3.2" + +sass@1.57.1: + version "1.57.1" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.57.1.tgz#dfafd46eb3ab94817145e8825208ecf7281119b5" + integrity sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -6594,7 +6805,7 @@ sort-css-media-queries@2.0.4: resolved "https://registry.yarnpkg.com/sort-css-media-queries/-/sort-css-media-queries-2.0.4.tgz#b2badfa519cb4a938acbc6d3aaa913d4949dc908" integrity sha512-PAIsEK/XupCQwitjv7XxoMvYhT7EAfyzI3hsy/MyDgTvc+Ft55ctdkctJLOy6cQejaIC+zjpUL4djFVm2ivOOw== -source-map-js@^1.0.2: +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== @@ -6617,6 +6828,11 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + space-separated-tokens@^1.0.0: version "1.1.5" resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" @@ -6947,6 +7163,13 @@ ua-parser-js@^0.7.30: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ== +unescape@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/unescape/-/unescape-1.0.1.tgz#956e430f61cad8a4d57d82c518f5e6cc5d0dda96" + integrity sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ== + dependencies: + extend-shallow "^2.0.1" + unherit@^1.0.4: version "1.1.3" resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22" @@ -7203,6 +7426,11 @@ vfile@^4.0.0: unist-util-stringify-position "^2.0.0" vfile-message "^2.0.0" +vlq@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468" + integrity sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w== + wait-on@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-6.0.1.tgz#16bbc4d1e4ebdd41c5b4e63a2e16dbd1f4e5601e" diff --git a/src/packages/excalidraw/README.md b/src/packages/excalidraw/README.md index a0dda8571..eaeef4b0c 100644 --- a/src/packages/excalidraw/README.md +++ b/src/packages/excalidraw/README.md @@ -1,22 +1,18 @@ -#### Note +# Excalidraw -⚠️ ⚠️ ⚠️ You are viewing the docs for the **next** release, in case you want to check the docs for the stable release, you can view it [here](https://www.npmjs.com/package/@excalidraw/excalidraw). +**Excalidraw** is exported as a component to directly embed in your projects. -### Excalidraw +## Installation -Excalidraw exported as a component to directly embed in your projects. +You can use `npm` -### Installation - -You can use npm - -``` +```bash npm install react react-dom @excalidraw/excalidraw ``` -or via yarn +or via `yarn` -``` +```bash yarn add react react-dom @excalidraw/excalidraw ``` @@ -24,7 +20,7 @@ After installation you will see a folder `excalidraw-assets` and `excalidraw-ass Move the folder `excalidraw-assets` and `excalidraw-assets-dev` to the path where your assets are served. -By default it will try to load the files from `https://unpkg.com/@excalidraw/excalidraw/dist/` +By default it will try to load the files from [`https://unpkg.com/@excalidraw/excalidraw/dist/`](https://unpkg.com/@excalidraw/excalidraw/dist) If you want to load assets from a different path you can set a variable `window.EXCALIDRAW_ASSET_PATH` depending on environment (for example if you have different URL's for dev and prod) to the url from where you want to load the assets. @@ -32,1820 +28,18 @@ If you want to load assets from a different path you can set a variable `window. **If you don't want to wait for the next stable release and try out the unreleased changes you can use `@excalidraw/excalidraw@next`.** +## Dimensions of Excalidraw + +Excalidraw takes _100%_ of `width` and `height` of the containing block so make sure the container in which you render Excalidraw has non zero dimensions. + ### Demo [Try here](https://codesandbox.io/s/excalidraw-ehlz3). -### Usage +## Integration -#### Using Web Bundler +Head over to the [docs](https://docs.excalidraw.com/docs/package/integration) -If you are using a Web bundler (for instance, Webpack), you can import it as an ES6 module as shown below +## API -

- +
- {renderWelcomeScreen && } + {renderWelcomeScreen && } actionManager.executeAction(actionShortcuts)} /> diff --git a/src/components/footer/FooterCenter.tsx b/src/components/footer/FooterCenter.tsx index 90f0f96c5..294a65f3f 100644 --- a/src/components/footer/FooterCenter.tsx +++ b/src/components/footer/FooterCenter.tsx @@ -1,13 +1,13 @@ import clsx from "clsx"; import { useExcalidrawAppState } from "../App"; -import { useTunnels } from "../context/tunnels"; +import { useTunnels } from "../../context/tunnels"; import "./FooterCenter.scss"; const FooterCenter = ({ children }: { children?: React.ReactNode }) => { - const { footerCenterTunnel } = useTunnels(); + const { FooterCenterTunnel } = useTunnels(); const appState = useExcalidrawAppState(); return ( - +
{ > {children}
-
+
); }; diff --git a/src/components/hoc/withInternalFallback.tsx b/src/components/hoc/withInternalFallback.tsx index 4ad61a11e..581a1874f 100644 --- a/src/components/hoc/withInternalFallback.tsx +++ b/src/components/hoc/withInternalFallback.tsx @@ -1,32 +1,46 @@ import { atom, useAtom } from "jotai"; import React, { useLayoutEffect } from "react"; -import { useTunnels } from "../context/tunnels"; +import { useTunnels } from "../../context/tunnels"; export const withInternalFallback = ( componentName: string, Component: React.FC

, ) => { - const counterAtom = atom(0); + const renderAtom = atom(0); // flag set on initial render to tell the fallback component to skip the // render until mount counter are initialized. This is because the counter // is initialized in an effect, and thus we could end rendering both // components at the same time until counter is initialized. let preferHost = false; + let counter = 0; + const WrapperComponent: React.FC< P & { __fallback?: boolean; } > = (props) => { const { jotaiScope } = useTunnels(); - const [counter, setCounter] = useAtom(counterAtom, jotaiScope); + const [, setRender] = useAtom(renderAtom, jotaiScope); useLayoutEffect(() => { - setCounter((counter) => counter + 1); + setRender((c) => { + const next = c + 1; + counter = next; + + return next; + }); return () => { - setCounter((counter) => counter - 1); + setRender((c) => { + const next = c - 1; + counter = next; + if (!next) { + preferHost = false; + } + return next; + }); }; - }, [setCounter]); + }, [setRender]); if (!props.__fallback) { preferHost = true; diff --git a/src/components/hoc/withUpstreamOverride.tsx b/src/components/hoc/withUpstreamOverride.tsx deleted file mode 100644 index acbc800b3..000000000 --- a/src/components/hoc/withUpstreamOverride.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { - useMemo, - useContext, - useLayoutEffect, - useState, - createContext, -} from "react"; - -export const withUpstreamOverride = (Component: React.ComponentType

) => { - type ContextValue = [boolean, React.Dispatch>]; - - const DefaultComponentContext = createContext([ - false, - () => {}, - ]); - - const ComponentContext: React.FC<{ children: React.ReactNode }> = ({ - children, - }) => { - const [isRenderedUpstream, setIsRenderedUpstream] = useState(false); - const contextValue: ContextValue = useMemo( - () => [isRenderedUpstream, setIsRenderedUpstream], - [isRenderedUpstream], - ); - - return ( - - {children} - - ); - }; - - const DefaultComponent = ( - props: P & { - // indicates whether component should render when not rendered upstream - /** @private internal */ - __isFallback?: boolean; - }, - ) => { - const [isRenderedUpstream, setIsRenderedUpstream] = useContext( - DefaultComponentContext, - ); - - useLayoutEffect(() => { - if (!props.__isFallback) { - setIsRenderedUpstream(true); - return () => setIsRenderedUpstream(false); - } - }, [props.__isFallback, setIsRenderedUpstream]); - - if (props.__isFallback && isRenderedUpstream) { - return null; - } - - return ; - }; - if (Component.name) { - DefaultComponent.displayName = `${Component.name}_upstreamOverrideWrapper`; - ComponentContext.displayName = `${Component.name}_upstreamOverrideContextWrapper`; - } - - return [ComponentContext, DefaultComponent] as const; -}; diff --git a/src/components/main-menu/MainMenu.tsx b/src/components/main-menu/MainMenu.tsx index 636ffc6e5..1a4e171cf 100644 --- a/src/components/main-menu/MainMenu.tsx +++ b/src/components/main-menu/MainMenu.tsx @@ -13,7 +13,7 @@ import { t } from "../../i18n"; import { HamburgerMenuIcon } from "../icons"; import { withInternalFallback } from "../hoc/withInternalFallback"; import { composeEventHandlers } from "../../utils"; -import { useTunnels } from "../context/tunnels"; +import { useTunnels } from "../../context/tunnels"; const MainMenu = Object.assign( withInternalFallback( @@ -28,7 +28,7 @@ const MainMenu = Object.assign( */ onSelect?: (event: Event) => void; }) => { - const { mainMenuTunnel } = useTunnels(); + const { MainMenuTunnel } = useTunnels(); const device = useDevice(); const appState = useExcalidrawAppState(); const setAppState = useExcalidrawSetAppState(); @@ -37,7 +37,7 @@ const MainMenu = Object.assign( : () => setAppState({ openMenu: null }); return ( - + { @@ -66,7 +66,7 @@ const MainMenu = Object.assign( )} - + ); }, ), diff --git a/src/components/welcome-screen/WelcomeScreen.Center.tsx b/src/components/welcome-screen/WelcomeScreen.Center.tsx index 8ab010057..f4ea6d4d4 100644 --- a/src/components/welcome-screen/WelcomeScreen.Center.tsx +++ b/src/components/welcome-screen/WelcomeScreen.Center.tsx @@ -6,7 +6,7 @@ import { useExcalidrawActionManager, useExcalidrawAppState, } from "../App"; -import { useTunnels } from "../context/tunnels"; +import { useTunnels } from "../../context/tunnels"; import { ExcalLogo, HelpIcon, LoadIcon, usersIcon } from "../icons"; const WelcomeScreenMenuItemContent = ({ @@ -89,9 +89,9 @@ const WelcomeScreenMenuItemLink = ({ WelcomeScreenMenuItemLink.displayName = "WelcomeScreenMenuItemLink"; const Center = ({ children }: { children?: React.ReactNode }) => { - const { welcomeScreenCenterTunnel } = useTunnels(); + const { WelcomeScreenCenterTunnel } = useTunnels(); return ( - +

{children || ( <> @@ -104,7 +104,7 @@ const Center = ({ children }: { children?: React.ReactNode }) => { )}
- + ); }; Center.displayName = "Center"; diff --git a/src/components/welcome-screen/WelcomeScreen.Hints.tsx b/src/components/welcome-screen/WelcomeScreen.Hints.tsx index 3ba36211d..ccd42ed27 100644 --- a/src/components/welcome-screen/WelcomeScreen.Hints.tsx +++ b/src/components/welcome-screen/WelcomeScreen.Hints.tsx @@ -1,5 +1,5 @@ import { t } from "../../i18n"; -import { useTunnels } from "../context/tunnels"; +import { useTunnels } from "../../context/tunnels"; import { WelcomeScreenHelpArrow, WelcomeScreenMenuArrow, @@ -7,44 +7,44 @@ import { } from "../icons"; const MenuHint = ({ children }: { children?: React.ReactNode }) => { - const { welcomeScreenMenuHintTunnel } = useTunnels(); + const { WelcomeScreenMenuHintTunnel } = useTunnels(); return ( - +
{WelcomeScreenMenuArrow}
{children || t("welcomeScreen.defaults.menuHint")}
-
+
); }; MenuHint.displayName = "MenuHint"; const ToolbarHint = ({ children }: { children?: React.ReactNode }) => { - const { welcomeScreenToolbarHintTunnel } = useTunnels(); + const { WelcomeScreenToolbarHintTunnel } = useTunnels(); return ( - +
{children || t("welcomeScreen.defaults.toolbarHint")}
{WelcomeScreenTopToolbarArrow}
-
+
); }; ToolbarHint.displayName = "ToolbarHint"; const HelpHint = ({ children }: { children?: React.ReactNode }) => { - const { welcomeScreenHelpHintTunnel } = useTunnels(); + const { WelcomeScreenHelpHintTunnel } = useTunnels(); return ( - +
{children || t("welcomeScreen.defaults.helpHint")}
{WelcomeScreenHelpArrow}
-
+
); }; HelpHint.displayName = "HelpHint"; diff --git a/src/constants.ts b/src/constants.ts index 19b41b688..5b59679cf 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -275,3 +275,10 @@ export const DEFAULT_ELEMENT_PROPS: { opacity: 100, locked: false, }; + +export const LIBRARY_SIDEBAR_TAB = "library"; + +export const DEFAULT_SIDEBAR = { + name: "default", + defaultTab: LIBRARY_SIDEBAR_TAB, +} as const; diff --git a/src/context/tunnels.ts b/src/context/tunnels.ts new file mode 100644 index 000000000..c5eaef9be --- /dev/null +++ b/src/context/tunnels.ts @@ -0,0 +1,36 @@ +import React from "react"; +import tunnel from "tunnel-rat"; + +export type Tunnel = ReturnType; + +type TunnelsContextValue = { + MainMenuTunnel: Tunnel; + WelcomeScreenMenuHintTunnel: Tunnel; + WelcomeScreenToolbarHintTunnel: Tunnel; + WelcomeScreenHelpHintTunnel: Tunnel; + WelcomeScreenCenterTunnel: Tunnel; + FooterCenterTunnel: Tunnel; + DefaultSidebarTriggerTunnel: Tunnel; + DefaultSidebarTabTriggersTunnel: Tunnel; + jotaiScope: symbol; +}; + +export const TunnelsContext = React.createContext(null!); + +export const useTunnels = () => React.useContext(TunnelsContext); + +export const useInitializeTunnels = () => { + return React.useMemo((): TunnelsContextValue => { + return { + MainMenuTunnel: tunnel(), + WelcomeScreenMenuHintTunnel: tunnel(), + WelcomeScreenToolbarHintTunnel: tunnel(), + WelcomeScreenHelpHintTunnel: tunnel(), + WelcomeScreenCenterTunnel: tunnel(), + FooterCenterTunnel: tunnel(), + DefaultSidebarTriggerTunnel: tunnel(), + DefaultSidebarTabTriggersTunnel: tunnel(), + jotaiScope: Symbol(), + }; + }, []); +}; diff --git a/src/context/ui-appState.ts b/src/context/ui-appState.ts new file mode 100644 index 000000000..f74ec6e0c --- /dev/null +++ b/src/context/ui-appState.ts @@ -0,0 +1,5 @@ +import React from "react"; +import { AppState } from "../types"; + +export const UIAppStateContext = React.createContext(null!); +export const useUIAppState = () => React.useContext(UIAppStateContext); diff --git a/src/css/styles.scss b/src/css/styles.scss index 29e52011e..d9d8544a0 100644 --- a/src/css/styles.scss +++ b/src/css/styles.scss @@ -567,7 +567,7 @@ border-radius: 0; } - .library-button { + .default-sidebar-trigger { border: 0; } } diff --git a/src/css/theme.scss b/src/css/theme.scss index fd8067968..c8abc4fff 100644 --- a/src/css/theme.scss +++ b/src/css/theme.scss @@ -78,10 +78,13 @@ --color-selection: #6965db; + --color-icon-white: #{$oc-white}; + --color-primary: #6965db; --color-primary-darker: #5b57d1; --color-primary-darkest: #4a47b1; --color-primary-light: #e3e2fe; + --color-primary-light-darker: #d7d5ff; --color-gray-10: #f5f5f5; --color-gray-20: #ebebeb; @@ -161,10 +164,13 @@ // will be inverted to a lighter color. --color-selection: #3530c4; + --color-icon-white: var(--color-gray-90); + --color-primary: #a8a5ff; --color-primary-darker: #b2aeff; --color-primary-darkest: #beb9ff; --color-primary-light: #4f4d6f; + --color-primary-light-darker: #43415e; --color-text-warning: var(--color-gray-80); diff --git a/src/css/variables.module.scss b/src/css/variables.module.scss index 779c52bff..10e42dc72 100644 --- a/src/css/variables.module.scss +++ b/src/css/variables.module.scss @@ -72,7 +72,14 @@ &:hover { background-color: var(--button-hover-bg, var(--island-bg-color)); - border-color: var(--button-hover-border, var(--default-border-color)); + border-color: var( + --button-hover-border, + var(--button-border, var(--default-border-color)) + ); + color: var( + --button-hover-color, + var(--button-color, var(--text-primary-color, inherit)) + ); } &:active { @@ -81,11 +88,14 @@ } &.active { - background-color: var(--color-primary-light); - border-color: var(--color-primary-light); + background-color: var(--button-selected-bg, var(--color-primary-light)); + border-color: var(--button-selected-border, var(--color-primary-light)); &:hover { - background-color: var(--color-primary-light); + background-color: var( + --button-selected-hover-bg, + var(--color-primary-light) + ); } svg { diff --git a/src/data/library.ts b/src/data/library.ts index 564ccc7d3..b9033bace 100644 --- a/src/data/library.ts +++ b/src/data/library.ts @@ -14,7 +14,14 @@ import { getCommonBoundingBox } from "../element/bounds"; import { AbortError } from "../errors"; import { t } from "../i18n"; import { useEffect, useRef } from "react"; -import { URL_HASH_KEYS, URL_QUERY_KEYS, APP_NAME, EVENT } from "../constants"; +import { + URL_HASH_KEYS, + URL_QUERY_KEYS, + APP_NAME, + EVENT, + DEFAULT_SIDEBAR, + LIBRARY_SIDEBAR_TAB, +} from "../constants"; export const libraryItemsAtom = atom<{ status: "loading" | "loaded"; @@ -148,7 +155,9 @@ class Library { defaultStatus?: "unpublished" | "published"; }): Promise => { if (openLibraryMenu) { - this.app.setState({ openSidebar: "library" }); + this.app.setState({ + openSidebar: { name: DEFAULT_SIDEBAR.name, tab: LIBRARY_SIDEBAR_TAB }, + }); } return this.setLibrary(() => { @@ -174,6 +183,13 @@ class Library { }), ) ) { + if (prompt) { + // focus container if we've prompted. We focus conditionally + // lest `props.autoFocus` is disabled (in which case we should + // focus only on user action such as prompt confirm) + this.app.focusContainer(); + } + if (merge) { resolve(mergeLibraryItems(this.lastLibraryItems, nextItems)); } else { @@ -186,8 +202,6 @@ class Library { reject(error); } }); - }).finally(() => { - this.app.focusContainer(); }); }; diff --git a/src/data/restore.ts b/src/data/restore.ts index fcf5fa132..63f3dbcfb 100644 --- a/src/data/restore.ts +++ b/src/data/restore.ts @@ -27,6 +27,7 @@ import { PRECEDING_ELEMENT_KEY, FONT_FAMILY, ROUNDNESS, + DEFAULT_SIDEBAR, } from "../constants"; import { getDefaultAppState } from "../appState"; import { LinearElementEditor } from "../element/linearElementEditor"; @@ -431,21 +432,15 @@ const LegacyAppStateMigrations: { defaultAppState: ReturnType, ) => [LegacyAppState[K][1], AppState[LegacyAppState[K][1]]]; } = { - isLibraryOpen: (appState, defaultAppState) => { + isSidebarDocked: (appState, defaultAppState) => { return [ - "openSidebar", - "isLibraryOpen" in appState - ? appState.isLibraryOpen - ? "library" - : null - : coalesceAppStateValue("openSidebar", appState, defaultAppState), - ]; - }, - isLibraryMenuDocked: (appState, defaultAppState) => { - return [ - "isSidebarDocked", - appState.isLibraryMenuDocked ?? - coalesceAppStateValue("isSidebarDocked", appState, defaultAppState), + "defaultSidebarDockedPreference", + appState.isSidebarDocked ?? + coalesceAppStateValue( + "defaultSidebarDockedPreference", + appState, + defaultAppState, + ), ]; }, }; @@ -517,13 +512,10 @@ export const restoreAppState = ( : appState.zoom?.value ? appState.zoom : defaultAppState.zoom, - // when sidebar docked and user left it open in last session, - // keep it open. If not docked, keep it closed irrespective of last state. openSidebar: - nextAppState.openSidebar === "library" - ? nextAppState.isSidebarDocked - ? "library" - : null + // string (legacy) + typeof (appState.openSidebar as any as string) === "string" + ? { name: DEFAULT_SIDEBAR.name } : nextAppState.openSidebar, }; }; diff --git a/src/data/types.ts b/src/data/types.ts index b8c959218..69112b6b6 100644 --- a/src/data/types.ts +++ b/src/data/types.ts @@ -25,10 +25,8 @@ export interface ExportedDataState { * Don't consume on its own. */ export type LegacyAppState = { - /** @deprecated #5663 TODO remove 22-12-15 */ - isLibraryOpen: [boolean, "openSidebar"]; - /** @deprecated #5663 TODO remove 22-12-15 */ - isLibraryMenuDocked: [boolean, "isSidebarDocked"]; + /** @deprecated #6213 TODO remove 23-06-01 */ + isSidebarDocked: [boolean, "defaultSidebarDockedPreference"]; }; export interface ImportedDataState { diff --git a/src/hooks/useOutsideClick.ts b/src/hooks/useOutsideClick.ts index 4b7512069..cda7e50d7 100644 --- a/src/hooks/useOutsideClick.ts +++ b/src/hooks/useOutsideClick.ts @@ -1,6 +1,6 @@ import { useEffect, useRef } from "react"; -export const useOutsideClickHook = (handler: (event: Event) => void) => { +export const useOutsideClick = (handler: (event: Event) => void) => { const ref = useRef(null); useEffect( diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index a2f7466b8..30292159b 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -11,6 +11,22 @@ The change should be grouped under one of the below section and must contain PR Please add the latest change on the top under the correct section. --> +## Unreleased + +### Features + +- Sidebar component now supports tabs — for more detailed description of new behavior and breaking changes, see the linked PR. [#6213](https://github.com/excalidraw/excalidraw/pull/6213) +- Exposed `DefaultSidebar` component to allow modifying the default sidebar, such as adding custom tabs to it. [#6213](https://github.com/excalidraw/excalidraw/pull/6213) + + #### BREAKING CHANGES + + - `props.renderSidebar` is removed in favor of rendering as `children`. + - `appState.isSidebarDocked` replaced with `appState.defaultSidebarDockedPreference` with slightly different semantics, and relating only to the default sidebar. You need to handle `docked` state for your custom sidebars yourself. + - Sidebar `props.dockable` is removed. To indicate dockability, supply `props.onDock()` alongside setting `props.docked`. + - `Sidebar.Header` is no longer rendered by default. You need to render it yourself. + - `props.onClose` replaced with `props.onStateChange`. + - `restore()`/`restoreAppState()` now retains `appState.openSidebar` regardless of docked state. + ## 0.15.2 (2023-04-20) ### Docs diff --git a/src/packages/excalidraw/example/App.tsx b/src/packages/excalidraw/example/App.tsx index 1f4a6c7fd..85993a683 100644 --- a/src/packages/excalidraw/example/App.tsx +++ b/src/packages/excalidraw/example/App.tsx @@ -494,15 +494,6 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { ); }; - const renderSidebar = () => { - return ( - - Custom header! - Custom sidebar! - - ); - }; - const renderMenu = () => { return ( @@ -668,23 +659,6 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
-
- -
setExcalidrawAPI(api)} initialData={initialStatePromiseRef.current.promise} @@ -706,7 +680,6 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { onLinkOpen={onLinkOpen} onPointerDown={onPointerDown} onScrollChange={rerenderCommentIcons} - renderSidebar={renderSidebar} > {excalidrawAPI && (
@@ -714,6 +687,30 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
)} + + + + Tab one! + Tab two! + + One + Two + + + + + Toggle Custom Sidebar + {renderMenu()}
{Object.keys(commentIcons || []).length > 0 && renderCommentIcons()} diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index 4f768c6d5..22f79dd33 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -24,7 +24,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => { isCollaborating = false, onPointerUpdate, renderTopRightUI, - renderSidebar, langCode = defaultLang.code, viewModeEnabled, zenModeEnabled, @@ -47,6 +46,8 @@ const ExcalidrawBase = (props: ExcalidrawProps) => { const canvasActions = props.UIOptions?.canvasActions; + // FIXME normalize/set defaults in parent component so that the memo resolver + // compares the same values const UIOptions: AppProps["UIOptions"] = { ...props.UIOptions, canvasActions: { @@ -114,7 +115,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => { onLinkOpen={onLinkOpen} onPointerDown={onPointerDown} onScrollChange={onScrollChange} - renderSidebar={renderSidebar} > {children} @@ -245,3 +245,5 @@ export { MainMenu }; export { useDevice } from "../../components/App"; export { WelcomeScreen }; export { LiveCollaborationTrigger }; + +export { DefaultSidebar } from "../../components/DefaultSidebar"; diff --git a/src/tests/__snapshots__/contextmenu.test.tsx.snap b/src/tests/__snapshots__/contextmenu.test.tsx.snap index c5b61e42f..43e4c0f7d 100644 --- a/src/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/src/tests/__snapshots__/contextmenu.test.tsx.snap @@ -284,6 +284,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -300,7 +301,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -464,6 +464,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -480,7 +481,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -650,6 +650,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -666,7 +667,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -1005,6 +1005,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -1021,7 +1022,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -1360,6 +1360,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -1376,7 +1377,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -1546,6 +1546,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -1562,7 +1563,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -1768,6 +1768,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -1784,7 +1785,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -2053,6 +2053,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -2069,7 +2070,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -2426,6 +2426,7 @@ Object { "currentItemStrokeWidth": 2, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -2442,7 +2443,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -3273,6 +3273,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -3289,7 +3290,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -3628,6 +3628,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -3644,7 +3645,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -3983,6 +3983,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -3999,7 +4000,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -4681,6 +4681,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -4697,7 +4698,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -5231,6 +5231,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -5247,7 +5248,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -5712,6 +5712,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -5728,7 +5729,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -6080,6 +6080,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -6096,7 +6097,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -6426,6 +6426,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -6442,7 +6443,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap index 967a0cf69..82d3dbbff 100644 --- a/src/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap @@ -25,6 +25,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -41,7 +42,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -561,6 +561,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -577,7 +578,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -1103,6 +1103,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": "id10", @@ -1119,7 +1120,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -2010,6 +2010,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -2026,7 +2027,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -2240,6 +2240,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -2256,7 +2257,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -2773,6 +2773,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -2789,7 +2790,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -3062,6 +3062,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -3078,7 +3079,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -3246,6 +3246,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -3262,7 +3263,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -3762,6 +3762,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -3778,7 +3779,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -4030,6 +4030,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -4046,7 +4047,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -4260,6 +4260,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -4276,7 +4277,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -4536,6 +4536,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -4552,7 +4553,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -4824,6 +4824,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -4840,7 +4841,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -5242,6 +5242,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "down", + "defaultSidebarDockedPreference": false, "draggingElement": Object { "angle": 0, "backgroundColor": "transparent", @@ -5285,7 +5286,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -5583,6 +5583,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": Object { "angle": 0, "backgroundColor": "transparent", @@ -5626,7 +5627,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -5897,6 +5897,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "down", + "defaultSidebarDockedPreference": false, "draggingElement": Object { "angle": 0, "backgroundColor": "transparent", @@ -5940,7 +5941,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -6135,6 +6135,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -6151,7 +6152,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -6321,6 +6321,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": "id3", @@ -6337,7 +6338,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -6849,6 +6849,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -6865,7 +6866,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -7214,6 +7214,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -7230,7 +7231,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -9566,6 +9566,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -9582,7 +9583,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -9985,6 +9985,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -10001,7 +10002,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -10274,6 +10274,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -10290,7 +10291,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -10522,6 +10522,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -10538,7 +10539,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -10843,6 +10843,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -10859,7 +10860,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -11027,6 +11027,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -11043,7 +11044,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -11211,6 +11211,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -11227,7 +11228,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -11395,6 +11395,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -11411,7 +11412,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -11632,6 +11632,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -11648,7 +11649,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -11869,6 +11869,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -11885,7 +11886,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -12097,6 +12097,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -12113,7 +12114,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -12334,6 +12334,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -12350,7 +12351,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -12518,6 +12518,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -12534,7 +12535,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -12755,6 +12755,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -12771,7 +12772,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -12939,6 +12939,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -12955,7 +12956,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -13167,6 +13167,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -13183,7 +13184,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -13351,6 +13351,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -13367,7 +13368,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -14190,6 +14190,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -14206,7 +14207,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -14479,6 +14479,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "down", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -14495,7 +14496,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "touch", "multiElement": null, "name": "Untitled-201933152653", @@ -14590,6 +14590,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -14606,7 +14607,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -14699,6 +14699,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -14715,7 +14716,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -14886,6 +14886,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -14902,7 +14903,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -15254,6 +15254,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -15270,7 +15271,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -15885,6 +15885,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -15901,7 +15902,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -16111,6 +16111,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -16127,7 +16128,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -17074,6 +17074,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -17090,7 +17091,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -17183,6 +17183,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": "id3", @@ -17199,7 +17200,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -18042,6 +18042,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "down", + "defaultSidebarDockedPreference": false, "draggingElement": Object { "angle": 0, "backgroundColor": "transparent", @@ -18085,7 +18086,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -18514,6 +18514,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "down", + "defaultSidebarDockedPreference": false, "draggingElement": Object { "angle": 0, "backgroundColor": "transparent", @@ -18557,7 +18558,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -18855,6 +18855,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "down", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -18871,7 +18872,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "touch", "multiElement": null, "name": "Untitled-201933152653", @@ -18966,6 +18966,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -18982,7 +18983,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -19537,6 +19537,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -19553,7 +19554,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", @@ -19646,6 +19646,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -19662,7 +19663,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "Untitled-201933152653", diff --git a/src/tests/data/restore.test.ts b/src/tests/data/restore.test.ts index afd5ef918..aaa2f087a 100644 --- a/src/tests/data/restore.test.ts +++ b/src/tests/data/restore.test.ts @@ -10,7 +10,7 @@ import { API } from "../helpers/api"; import { getDefaultAppState } from "../../appState"; import { ImportedDataState } from "../../data/types"; import { NormalizedZoomValue } from "../../types"; -import { FONT_FAMILY, ROUNDNESS } from "../../constants"; +import { DEFAULT_SIDEBAR, FONT_FAMILY, ROUNDNESS } from "../../constants"; import { newElementWith } from "../../element/mutateElement"; describe("restoreElements", () => { @@ -453,6 +453,29 @@ describe("restoreAppState", () => { expect(restoredAppState.zoom).toMatchObject(getDefaultAppState().zoom); }); }); + + it("should handle appState.openSidebar legacy values", () => { + expect(restore.restoreAppState({}, null).openSidebar).toBe(null); + expect( + restore.restoreAppState({ openSidebar: "library" } as any, null) + .openSidebar, + ).toEqual({ name: DEFAULT_SIDEBAR.name }); + expect( + restore.restoreAppState({ openSidebar: "xxx" } as any, null).openSidebar, + ).toEqual({ name: DEFAULT_SIDEBAR.name }); + // while "library" was our legacy sidebar name, we can't assume it's legacy + // value as it may be some host app's custom sidebar name ¯\_(ツ)_/¯ + expect( + restore.restoreAppState({ openSidebar: { name: "library" } } as any, null) + .openSidebar, + ).toEqual({ name: "library" }); + expect( + restore.restoreAppState( + { openSidebar: { name: DEFAULT_SIDEBAR.name, tab: "ola" } } as any, + null, + ).openSidebar, + ).toEqual({ name: DEFAULT_SIDEBAR.name, tab: "ola" }); + }); }); describe("restore", () => { diff --git a/src/tests/library.test.tsx b/src/tests/library.test.tsx index 86847aeee..2a2886354 100644 --- a/src/tests/library.test.tsx +++ b/src/tests/library.test.tsx @@ -189,10 +189,15 @@ describe("library menu", () => { const latestLibrary = await h.app.library.getLatestLibrary(); expect(latestLibrary.length).toBe(0); - const libraryButton = container.querySelector(".library-button"); + const libraryButton = container.querySelector(".sidebar-trigger"); fireEvent.click(libraryButton!); - fireEvent.click(container.querySelector(".Sidebar__dropdown-btn")!); + fireEvent.click( + queryByTestId( + container.querySelector(".layer-ui__library")!, + "dropdown-menu-button", + )!, + ); queryByTestId(container, "lib-dropdown--load")!.click(); const libraryItems = parseLibraryJSON(await libraryJSONPromise); diff --git a/src/tests/packages/__snapshots__/utils.test.ts.snap b/src/tests/packages/__snapshots__/utils.test.ts.snap index b1002a406..aa5f84334 100644 --- a/src/tests/packages/__snapshots__/utils.test.ts.snap +++ b/src/tests/packages/__snapshots__/utils.test.ts.snap @@ -25,6 +25,7 @@ Object { "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", "cursorButton": "up", + "defaultSidebarDockedPreference": false, "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -41,7 +42,6 @@ Object { "isLoading": false, "isResizing": false, "isRotating": false, - "isSidebarDocked": false, "lastPointerDownWith": "mouse", "multiElement": null, "name": "name", diff --git a/src/types.ts b/src/types.ts index e5ad01b59..2a95d347e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -94,6 +94,9 @@ export type LastActiveTool = } | null; +export type SidebarName = string; +export type SidebarTabName = string; + export type AppState = { contextMenu: { items: ContextMenuItems; @@ -159,16 +162,22 @@ export type AppState = { isResizing: boolean; isRotating: boolean; zoom: Zoom; - // mobile-only openMenu: "canvas" | "shape" | null; openPopup: | "canvasColorPicker" | "backgroundColorPicker" | "strokeColorPicker" | null; - openSidebar: "library" | "customSidebar" | null; + openSidebar: { name: SidebarName; tab?: SidebarTabName } | null; openDialog: "imageExport" | "help" | "jsonExport" | null; - isSidebarDocked: boolean; + /** + * Reflects user preference for whether the default sidebar should be docked. + * + * NOTE this is only a user preference and does not reflect the actual docked + * state of the sidebar, because the host apps can override this through + * a DefaultSidebar prop, which is not reflected back to the appState. + */ + defaultSidebarDockedPreference: boolean; lastPointerDownWith: PointerType; selectedElementIds: { [id: string]: boolean }; @@ -335,10 +344,6 @@ export interface ExcalidrawProps { pointerDownState: PointerDownState, ) => void; onScrollChange?: (scrollX: number, scrollY: number) => void; - /** - * Render function that renders custom component. - */ - renderSidebar?: () => JSX.Element | null; children?: React.ReactNode; } @@ -426,6 +431,8 @@ export type AppClassProperties = { device: App["device"]; scene: App["scene"]; pasteFromClipboard: App["pasteFromClipboard"]; + id: App["id"]; + onInsertElements: App["onInsertElements"]; }; export type PointerDownState = Readonly<{ @@ -517,7 +524,7 @@ export type ExcalidrawImperativeAPI = { setActiveTool: InstanceType["setActiveTool"]; setCursor: InstanceType["setCursor"]; resetCursor: InstanceType["resetCursor"]; - toggleMenu: InstanceType["toggleMenu"]; + toggleSidebar: InstanceType["toggleSidebar"]; }; export type Device = Readonly<{ diff --git a/src/utils.ts b/src/utils.ts index 1d545f977..f35abe808 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -767,16 +767,30 @@ export const queryFocusableElements = (container: HTMLElement | null) => { : []; }; -export const isShallowEqual = >( +export const isShallowEqual = < + T extends Record, + I extends keyof T, +>( objA: T, objB: T, + comparators?: Record boolean>, + debug = false, ) => { const aKeys = Object.keys(objA); - const bKeys = Object.keys(objA); + const bKeys = Object.keys(objB); if (aKeys.length !== bKeys.length) { return false; } - return aKeys.every((key) => objA[key] === objB[key]); + return aKeys.every((key) => { + const comparator = comparators?.[key as I]; + const ret = comparator + ? comparator(objA[key], objB[key]) + : objA[key] === objB[key]; + if (!ret && debug) { + console.warn(`isShallowEqual: ${key} not equal ->`, objA[key], objB[key]); + } + return ret; + }); }; // taken from Radix UI diff --git a/yarn.lock b/yarn.lock index 89d153909..b4cc8c6f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1262,6 +1262,13 @@ dependencies: regenerator-runtime "^0.13.10" +"@babel/runtime@^7.13.10": + version "7.20.13" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b" + integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA== + dependencies: + regenerator-runtime "^0.13.11" + "@babel/template@^7.18.10", "@babel/template@^7.3.3": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" @@ -1437,13 +1444,6 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36" integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg== -"@dwelle/tunnel-rat@0.1.1": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@dwelle/tunnel-rat/-/tunnel-rat-0.1.1.tgz#0a0b235f8fc22ff1cf47ed102f4cc612eb51bc71" - integrity sha512-jb5/ZsT/af1J7tnbBXp7KO1xEyw61lWSDqJ+Bqdc6JlL3vbAvsifNhe+/mRFs6aSBCRaDqp5f2pJDHtA3MUZLw== - dependencies: - zustand "^4.3.2" - "@eslint/eslintrc@^0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" @@ -2164,6 +2164,131 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@radix-ui/primitive@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.0.tgz#e1d8ef30b10ea10e69c76e896f608d9276352253" + integrity sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-collection@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.1.tgz#259506f97c6703b36291826768d3c1337edd1de5" + integrity sha512-uuiFbs+YCKjn3X1DTSx9G7BHApu4GHbi3kgiwsnFUbOKCrwejAJv4eE4Vc8C0Oaxt9T0aV4ox0WCOdx+39Xo+g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.0" + "@radix-ui/react-context" "1.0.0" + "@radix-ui/react-primitive" "1.0.1" + "@radix-ui/react-slot" "1.0.1" + +"@radix-ui/react-compose-refs@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz#37595b1f16ec7f228d698590e78eeed18ff218ae" + integrity sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-context@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0" + integrity sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-direction@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.0.tgz#a2e0b552352459ecf96342c79949dd833c1e6e45" + integrity sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-id@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.0.tgz#8d43224910741870a45a8c9d092f25887bb6d11e" + integrity sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-layout-effect" "1.0.0" + +"@radix-ui/react-presence@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.0.tgz#814fe46df11f9a468808a6010e3f3ca7e0b2e84a" + integrity sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.0" + "@radix-ui/react-use-layout-effect" "1.0.0" + +"@radix-ui/react-primitive@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz#c1ebcce283dd2f02e4fbefdaa49d1cb13dbc990a" + integrity sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-slot" "1.0.1" + +"@radix-ui/react-roving-focus@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.2.tgz#d8ac2e3b8006697bdfc2b0eb06bef7e15b6245de" + integrity sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.0" + "@radix-ui/react-collection" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.0" + "@radix-ui/react-context" "1.0.0" + "@radix-ui/react-direction" "1.0.0" + "@radix-ui/react-id" "1.0.0" + "@radix-ui/react-primitive" "1.0.1" + "@radix-ui/react-use-callback-ref" "1.0.0" + "@radix-ui/react-use-controllable-state" "1.0.0" + +"@radix-ui/react-slot@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.1.tgz#e7868c669c974d649070e9ecbec0b367ee0b4d81" + integrity sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.0" + +"@radix-ui/react-tabs@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.2.tgz#8f5ec73ca41b151a413bdd6e00553408ff34ce07" + integrity sha512-gOUwh+HbjCuL0UCo8kZ+kdUEG8QtpdO4sMQduJ34ZEz0r4922g9REOBM+vIsfwtGxSug4Yb1msJMJYN2Bk8TpQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.0" + "@radix-ui/react-context" "1.0.0" + "@radix-ui/react-direction" "1.0.0" + "@radix-ui/react-id" "1.0.0" + "@radix-ui/react-presence" "1.0.0" + "@radix-ui/react-primitive" "1.0.1" + "@radix-ui/react-roving-focus" "1.0.2" + "@radix-ui/react-use-controllable-state" "1.0.0" + +"@radix-ui/react-use-callback-ref@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz#9e7b8b6b4946fe3cbe8f748c82a2cce54e7b6a90" + integrity sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-use-controllable-state@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz#a64deaafbbc52d5d407afaa22d493d687c538b7f" + integrity sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-callback-ref" "1.0.0" + +"@radix-ui/react-use-layout-effect@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz#2fc19e97223a81de64cd3ba1dc42ceffd82374dc" + integrity sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@rollup/plugin-babel@^5.2.0": version "5.3.1" resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" @@ -9160,7 +9285,7 @@ regenerator-runtime@^0.13.10: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee" integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw== -regenerator-runtime@^0.13.9: +regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.9: version "0.13.11" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== @@ -10245,12 +10370,12 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" -tunnel-rat@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/tunnel-rat/-/tunnel-rat-0.1.0.tgz#62cfbaf1b24cabac9318fe45ef26d70dc40e86fe" - integrity sha512-/FKZLBXCoKhA7Wz+dsqitrItaLXYmT2bkZXod+1UuR4JqHtdb54yHvHhmMgLg+eyH1Od/CCnhA2VQQ2A/54Tcw== +tunnel-rat@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/tunnel-rat/-/tunnel-rat-0.1.2.tgz#1717efbc474ea2d8aa05a91622457a6e201c0aeb" + integrity sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ== dependencies: - zustand "^4.1.0" + zustand "^4.3.2" type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" @@ -11035,9 +11160,9 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -zustand@^4.1.0, zustand@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.2.tgz#bb121fcad84c5a569e94bd1a2695e1a93ba85d39" - integrity sha512-rd4haDmlwMTVWVqwvgy00ny8rtti/klRoZjFbL/MAcDnmD5qSw/RZc+Vddstdv90M5Lv6RPgWvm1Hivyn0QgJw== +zustand@^4.3.2: + version "4.3.7" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.7.tgz#501b1f0393a7f1d103332e45ab574be5747fedce" + integrity sha512-dY8ERwB9Nd21ellgkBZFhudER8KVlelZm8388B5nDAXhO/+FZDhYMuRnqDgu5SYyRgz/iaf8RKnbUs/cHfOGlQ== dependencies: use-sync-external-store "1.2.0" From 1184a8c0e953312ff4a2b5beae467a1e11897477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=20Zakraj=C5=A1ek?= Date: Fri, 5 May 2023 18:05:18 +0200 Subject: [PATCH 272/276] feat: Add Trans component for interpolating JSX in translations (#6534) * feat: add Trans component * Add comments * tweak * Move brave to trans component * fix test and tweaks * remove any * fix * fix * comment * replace render function type * Use tags for Trans * Fix a typo Co-authored-by: Aakansha Doshi * Cleanup, add comments, add support for kebab case * tweaks --------- Co-authored-by: Aakansha Doshi Co-authored-by: dwelle --- src/components/BraveMeasureTextError.tsx | 57 +++--- src/components/Trans.test.tsx | 67 +++++++ src/components/Trans.tsx | 169 ++++++++++++++++++ .../__snapshots__/App.test.tsx.snap | 35 ++-- src/locales/en.json | 16 +- 5 files changed, 280 insertions(+), 64 deletions(-) create mode 100644 src/components/Trans.test.tsx create mode 100644 src/components/Trans.tsx diff --git a/src/components/BraveMeasureTextError.tsx b/src/components/BraveMeasureTextError.tsx index 8a4a71e4f..1932d7a29 100644 --- a/src/components/BraveMeasureTextError.tsx +++ b/src/components/BraveMeasureTextError.tsx @@ -1,39 +1,40 @@ -import { t } from "../i18n"; +import Trans from "./Trans"; + const BraveMeasureTextError = () => { return (

- {t("errors.brave_measure_text_error.start")}   - - {t("errors.brave_measure_text_error.aggressive_block_fingerprint")} - {" "} - {t("errors.brave_measure_text_error.setting_enabled")}. -
-
- {t("errors.brave_measure_text_error.break")}{" "} - - {t("errors.brave_measure_text_error.text_elements")} - {" "} - {t("errors.brave_measure_text_error.in_your_drawings")}. + {el}} + />

- {t("errors.brave_measure_text_error.strongly_recommend")}{" "} - - {" "} - {t("errors.brave_measure_text_error.steps")} - {" "} - {t("errors.brave_measure_text_error.how")}. + {el}} + />

- {t("errors.brave_measure_text_error.disable_setting")}{" "} - - {t("errors.brave_measure_text_error.issue")} - {" "} - {t("errors.brave_measure_text_error.write")}{" "} - - {t("errors.brave_measure_text_error.discord")} - - . + ( + + {el} + + )} + /> +

+

+ ( + + {el} + + )} + discordLink={(el) => {el}.} + />

); diff --git a/src/components/Trans.test.tsx b/src/components/Trans.test.tsx new file mode 100644 index 000000000..e3e9a462a --- /dev/null +++ b/src/components/Trans.test.tsx @@ -0,0 +1,67 @@ +import { render } from "@testing-library/react"; + +import fallbackLangData from "../locales/en.json"; + +import Trans from "./Trans"; + +describe("Test ", () => { + it("should translate the the strings correctly", () => { + //@ts-ignore + fallbackLangData.transTest = { + key1: "Hello {{audience}}", + key2: "Please click the button to continue.", + key3: "Please click {{location}} to continue.", + key4: "Please click {{location}} to continue.", + key5: "Please click the button to continue.", + }; + + const { getByTestId } = render( + <> +
+ +
+
+ {el}} + /> +
+
+ {el}} + location="the button" + /> +
+
+ {el}} + location="the button" + bold={(el) => {el}} + /> +
+
+ {el}} + /> +
+ , + ); + + expect(getByTestId("test1").innerHTML).toEqual("Hello world"); + expect(getByTestId("test2").innerHTML).toEqual( + `Please click the button to continue.`, + ); + expect(getByTestId("test3").innerHTML).toEqual( + `Please click the button to continue.`, + ); + expect(getByTestId("test4").innerHTML).toEqual( + `Please click the button to continue.`, + ); + expect(getByTestId("test5").innerHTML).toEqual( + `Please click the button to continue.`, + ); + }); +}); diff --git a/src/components/Trans.tsx b/src/components/Trans.tsx new file mode 100644 index 000000000..189cda23c --- /dev/null +++ b/src/components/Trans.tsx @@ -0,0 +1,169 @@ +import React from "react"; + +import { useI18n } from "../i18n"; + +// Used for splitting i18nKey into tokens in Trans component +// Example: +// "Please click {{location}} to continue.".split(SPLIT_REGEX).filter(Boolean) +// produces +// ["Please ", "", "click ", "{{location}}", "", " to continue."] +const SPLIT_REGEX = /({{[\w-]+}})|(<[\w-]+>)|(<\/[\w-]+>)/g; +// Used for extracting "location" from "{{location}}" +const KEY_REGEXP = /{{([\w-]+)}}/; +// Used for extracting "link" from "" +const TAG_START_REGEXP = /<([\w-]+)>/; +// Used for extracting "link" from "" +const TAG_END_REGEXP = /<\/([\w-]+)>/; + +const getTransChildren = ( + format: string, + props: { + [key: string]: React.ReactNode | ((el: React.ReactNode) => React.ReactNode); + }, +): React.ReactNode[] => { + const stack: { name: string; children: React.ReactNode[] }[] = [ + { + name: "", + children: [], + }, + ]; + + format + .split(SPLIT_REGEX) + .filter(Boolean) + .forEach((match) => { + const tagStartMatch = match.match(TAG_START_REGEXP); + const tagEndMatch = match.match(TAG_END_REGEXP); + const keyMatch = match.match(KEY_REGEXP); + + if (tagStartMatch !== null) { + // The match is . Set the tag name as the name if it's one of the + // props, e.g. for "Please click the button to continue" + // tagStartMatch[1] = "link" and props contain "link" then it will be + // pushed to stack. + const name = tagStartMatch[1]; + if (props.hasOwnProperty(name)) { + stack.push({ + name, + children: [], + }); + } else { + console.warn( + `Trans: missed to pass in prop ${name} for interpolating ${format}`, + ); + } + } else if (tagEndMatch !== null) { + // If tag end match is found, this means we need to replace the content with + // its actual value in prop e.g. format = "Please click the + // button to continue", tagEndMatch is for "", stack last item name = + // "link" and props.link = (el) => {el} then its prop value will be + // pushed to "link"'s children so on DOM when rendering it's rendered as + // click the button + const name = tagEndMatch[1]; + if (name === stack[stack.length - 1].name) { + const item = stack.pop()!; + const itemChildren = React.createElement( + React.Fragment, + {}, + ...item.children, + ); + const fn = props[item.name]; + if (typeof fn === "function") { + stack[stack.length - 1].children.push(fn(itemChildren)); + } + } else { + console.warn( + `Trans: unexpected end tag ${match} for interpolating ${format}`, + ); + } + } else if (keyMatch !== null) { + // The match is for {{key}}. Check if the key is present in props and set + // the prop value as children of last stack item e.g. format = "Hello + // {{name}}", key = "name" and props.name = "Excalidraw" then its prop + // value will be pushed to "name"'s children so it's rendered on DOM as + // "Hello Excalidraw" + const name = keyMatch[1]; + if (props.hasOwnProperty(name)) { + stack[stack.length - 1].children.push(props[name] as React.ReactNode); + } else { + console.warn( + `Trans: key ${name} not in props for interpolating ${format}`, + ); + } + } else { + // If none of cases match means we just need to push the string + // to stack eg - "Hello {{name}} Whats up?" "Hello", "Whats up" will be pushed + stack[stack.length - 1].children.push(match); + } + }); + + if (stack.length !== 1) { + console.warn(`Trans: stack not empty for interpolating ${format}`); + } + + return stack[0].children; +}; + +/* +Trans component is used for translating JSX. + +```json +{ + "example1": "Hello {{audience}}", + "example2": "Please click the button to continue.", + "example3": "Please click {{location}} to continue.", + "example4": "Please click {{location}} to continue.", +} +``` + +```jsx + + + {el}} +/> + + {el}} + location="the button" +/> + + {el}} + location="the button" + bold={(el) => {el}} +/> +``` + +Output: + +```html +Hello world +Please click the button to continue. +Please click the button to continue. +Please click the button to continue. +``` +*/ +const Trans = ({ + i18nKey, + children, + ...props +}: { + i18nKey: string; + [key: string]: React.ReactNode | ((el: React.ReactNode) => React.ReactNode); +}) => { + const { t } = useI18n(); + + // This is needed to avoid unique key error in list which gets rendered from getTransChildren + return React.createElement( + React.Fragment, + {}, + ...getTransChildren(t(i18nKey), props), + ); +}; + +export default Trans; diff --git a/src/components/__snapshots__/App.test.tsx.snap b/src/components/__snapshots__/App.test.tsx.snap index b36d678cd..25da39e3c 100644 --- a/src/components/__snapshots__/App.test.tsx.snap +++ b/src/components/__snapshots__/App.test.tsx.snap @@ -5,59 +5,46 @@ exports[`Test should show error modal when using brave and measureText AP data-testid="brave-measure-text-error" >

- Looks like you are using Brave browser with the -   + Looks like you are using Brave browser with the Aggressively Block Fingerprinting - - setting enabled - . -
-
- This could result in breaking the - + setting enabled. +

+

+ This could result in breaking the Text Elements - - in your drawings - . + in your drawings.

- We strongly recommend disabling this setting. You can follow - + We strongly recommend disabling this setting. You can follow - these steps - - on how to do so - . + on how to do so.

- If disabling this setting doesn't fix the display of text elements, please open an - + If disabling this setting doesn't fix the display of text elements, please open an issue - - on our GitHub, or write us on - + on our GitHub, or write us on Discord + . - .

`; diff --git a/src/locales/en.json b/src/locales/en.json index 7e250a800..041fb5643 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -208,18 +208,10 @@ "collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.", "collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work.", "brave_measure_text_error": { - "start": "Looks like you are using Brave browser with the", - "aggressive_block_fingerprint": "Aggressively Block Fingerprinting", - "setting_enabled": "setting enabled", - "break": "This could result in breaking the", - "text_elements": "Text Elements", - "in_your_drawings": "in your drawings", - "strongly_recommend": "We strongly recommend disabling this setting. You can follow", - "steps": "these steps", - "how": "on how to do so", - "disable_setting": " If disabling this setting doesn't fix the display of text elements, please open an", - "issue": "issue", - "write": "on our GitHub, or write us on", + "line1": "Looks like you are using Brave browser with the Aggressively Block Fingerprinting setting enabled.", + "line2": "This could result in breaking the Text Elements in your drawings.", + "line3": "We strongly recommend disabling this setting. You can follow these steps on how to do so.", + "line4": " If disabling this setting doesn't fix the display of text elements, please open an issue on our GitHub, or write us on Discord", "discord": "Discord" } }, From 026949204d7ca8a243e6e7e648005e5546b695d2 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Sat, 6 May 2023 10:36:42 +0200 Subject: [PATCH 273/276] fix: fix brave error i18n string and remove unused (#6561) --- src/components/__snapshots__/App.test.tsx.snap | 2 +- src/locales/en.json | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/__snapshots__/App.test.tsx.snap b/src/components/__snapshots__/App.test.tsx.snap index 25da39e3c..2cdf4bb8b 100644 --- a/src/components/__snapshots__/App.test.tsx.snap +++ b/src/components/__snapshots__/App.test.tsx.snap @@ -32,7 +32,7 @@ exports[`Test should show error modal when using brave and measureText AP on how to do so.

- If disabling this setting doesn't fix the display of text elements, please open an + If disabling this setting doesn't fix the display of text elements, please open an diff --git a/src/locales/en.json b/src/locales/en.json index 041fb5643..86029c30c 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -211,8 +211,7 @@ "line1": "Looks like you are using Brave browser with the Aggressively Block Fingerprinting setting enabled.", "line2": "This could result in breaking the Text Elements in your drawings.", "line3": "We strongly recommend disabling this setting. You can follow these steps on how to do so.", - "line4": " If disabling this setting doesn't fix the display of text elements, please open an issue on our GitHub, or write us on Discord", - "discord": "Discord" + "line4": "If disabling this setting doesn't fix the display of text elements, please open an issue on our GitHub, or write us on Discord" } }, "toolBar": { From 560231d3659520958261778cb0ffee4aa2877b2a Mon Sep 17 00:00:00 2001 From: David Luzar Date: Mon, 8 May 2023 10:14:02 +0200 Subject: [PATCH 274/276] perf: use `UIAppState` where possible to reduce UI rerenders (#6560) --- src/components/Actions.tsx | 13 +++--- src/components/HintViewer.tsx | 8 ++-- src/components/ImageExportDialog.tsx | 11 ++--- src/components/JSONExportDialog.tsx | 8 ++-- src/components/LayerUI.tsx | 40 ++++++++++++------- src/components/LibraryMenu.tsx | 11 +++-- src/components/LibraryMenuBrowseButton.tsx | 4 +- src/components/LibraryMenuControlButtons.tsx | 4 +- src/components/LibraryMenuHeaderContent.tsx | 11 ++--- src/components/LibraryMenuItems.tsx | 13 ++++-- src/components/MobileMenu.tsx | 10 ++--- src/components/PasteChartDialog.tsx | 7 ++-- src/components/PublishLibrary.tsx | 6 +-- src/components/Sidebar/Sidebar.tsx | 9 ++--- src/components/Sidebar/SidebarTrigger.tsx | 8 ++-- src/components/Stats.tsx | 6 +-- .../dropdownMenu/DropdownMenuTrigger.tsx | 5 ++- src/components/footer/Footer.tsx | 4 +- src/components/footer/FooterCenter.tsx | 4 +- .../LiveCollaborationTrigger.tsx | 4 +- src/components/main-menu/DefaultItems.tsx | 11 ++--- src/components/main-menu/MainMenu.tsx | 9 ++--- .../welcome-screen/WelcomeScreen.Center.tsx | 9 ++--- src/context/ui-appState.ts | 4 +- src/element/Hyperlink.tsx | 5 ++- src/element/showSelectedShapeActions.ts | 4 +- src/excalidraw-app/CustomStats.tsx | 5 ++- .../components/ExportToExcalidrawPlus.tsx | 4 +- src/excalidraw-app/data/index.ts | 2 +- src/excalidraw-app/index.tsx | 5 ++- src/scene/selection.ts | 8 ++-- src/types.ts | 19 ++++++--- src/utils.ts | 9 ++++- 33 files changed, 155 insertions(+), 125 deletions(-) diff --git a/src/components/Actions.tsx b/src/components/Actions.tsx index 3bbc0ff1a..875d8447c 100644 --- a/src/components/Actions.tsx +++ b/src/components/Actions.tsx @@ -14,7 +14,7 @@ import { hasText, } from "../scene"; import { SHAPES } from "../shapes"; -import { AppState, Zoom } from "../types"; +import { UIAppState, Zoom } from "../types"; import { capitalizeString, isTransparent, @@ -28,19 +28,20 @@ import { trackEvent } from "../analytics"; import { hasBoundTextElement } from "../element/typeChecks"; import clsx from "clsx"; import { actionToggleZenMode } from "../actions"; -import "./Actions.scss"; import { Tooltip } from "./Tooltip"; import { shouldAllowVerticalAlign, suppportsHorizontalAlign, } from "../element/textElement"; +import "./Actions.scss"; + export const SelectedShapeActions = ({ appState, elements, renderAction, }: { - appState: AppState; + appState: UIAppState; elements: readonly ExcalidrawElement[]; renderAction: ActionManager["renderAction"]; }) => { @@ -215,10 +216,10 @@ export const ShapesSwitcher = ({ appState, }: { canvas: HTMLCanvasElement | null; - activeTool: AppState["activeTool"]; - setAppState: React.Component["setState"]; + activeTool: UIAppState["activeTool"]; + setAppState: React.Component["setState"]; onImageAction: (data: { pointerType: PointerType | null }) => void; - appState: AppState; + appState: UIAppState; }) => ( <> {SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => { diff --git a/src/components/HintViewer.tsx b/src/components/HintViewer.tsx index a193754d2..a1b6308f4 100644 --- a/src/components/HintViewer.tsx +++ b/src/components/HintViewer.tsx @@ -1,9 +1,7 @@ import { t } from "../i18n"; import { NonDeletedExcalidrawElement } from "../element/types"; import { getSelectedElements } from "../scene"; - -import "./HintViewer.scss"; -import { AppState, Device } from "../types"; +import { Device, UIAppState } from "../types"; import { isImageElement, isLinearElement, @@ -13,8 +11,10 @@ import { import { getShortcutKey } from "../utils"; import { isEraserActive } from "../appState"; +import "./HintViewer.scss"; + interface HintViewerProps { - appState: AppState; + appState: UIAppState; elements: readonly NonDeletedExcalidrawElement[]; isMobile: boolean; device: Device; diff --git a/src/components/ImageExportDialog.tsx b/src/components/ImageExportDialog.tsx index 0e4eff365..3f8a9679f 100644 --- a/src/components/ImageExportDialog.tsx +++ b/src/components/ImageExportDialog.tsx @@ -4,11 +4,10 @@ import { canvasToBlob } from "../data/blob"; import { NonDeletedExcalidrawElement } from "../element/types"; import { t } from "../i18n"; import { getSelectedElements, isSomeElementSelected } from "../scene"; -import { AppState, BinaryFiles } from "../types"; +import { BinaryFiles, UIAppState } from "../types"; import { Dialog } from "./Dialog"; import { clipboard } from "./icons"; import Stack from "./Stack"; -import "./ExportDialog.scss"; import OpenColor from "open-color"; import { CheckboxItem } from "./CheckboxItem"; import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants"; @@ -16,6 +15,8 @@ import { nativeFileSystemSupported } from "../data/filesystem"; import { ActionManager } from "../actions/manager"; import { exportToCanvas } from "../packages/utils"; +import "./ExportDialog.scss"; + const supportsContextFilters = "filter" in document.createElement("canvas").getContext("2d")!; @@ -70,7 +71,7 @@ const ImageExportModal = ({ onExportToSvg, onExportToClipboard, }: { - appState: AppState; + appState: UIAppState; elements: readonly NonDeletedExcalidrawElement[]; files: BinaryFiles; exportPadding?: number; @@ -216,8 +217,8 @@ export const ImageExportDialog = ({ onExportToSvg, onExportToClipboard, }: { - appState: AppState; - setAppState: React.Component["setState"]; + appState: UIAppState; + setAppState: React.Component["setState"]; elements: readonly NonDeletedExcalidrawElement[]; files: BinaryFiles; exportPadding?: number; diff --git a/src/components/JSONExportDialog.tsx b/src/components/JSONExportDialog.tsx index 8f89ebcb7..f40ebe453 100644 --- a/src/components/JSONExportDialog.tsx +++ b/src/components/JSONExportDialog.tsx @@ -2,7 +2,7 @@ import React from "react"; import { NonDeletedExcalidrawElement } from "../element/types"; import { t } from "../i18n"; -import { AppState, ExportOpts, BinaryFiles } from "../types"; +import { ExportOpts, BinaryFiles, UIAppState } from "../types"; import { Dialog } from "./Dialog"; import { exportToFileIcon, LinkIcon } from "./icons"; import { ToolButton } from "./ToolButton"; @@ -28,7 +28,7 @@ const JSONExportModal = ({ exportOpts, canvas, }: { - appState: AppState; + appState: UIAppState; files: BinaryFiles; elements: readonly NonDeletedExcalidrawElement[]; actionManager: ActionManager; @@ -96,12 +96,12 @@ export const JSONExportDialog = ({ setAppState, }: { elements: readonly NonDeletedExcalidrawElement[]; - appState: AppState; + appState: UIAppState; files: BinaryFiles; actionManager: ActionManager; exportOpts: ExportOpts; canvas: HTMLCanvasElement | null; - setAppState: React.Component["setState"]; + setAppState: React.Component["setState"]; }) => { const handleClose = React.useCallback(() => { setAppState({ openDialog: null }); diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 6ae01d6a1..0e08eafa8 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -8,7 +8,13 @@ import { NonDeletedExcalidrawElement } from "../element/types"; import { Language, t } from "../i18n"; import { calculateScrollCenter } from "../scene"; import { ExportType } from "../scene/types"; -import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types"; +import { + AppProps, + AppState, + ExcalidrawProps, + BinaryFiles, + UIAppState, +} from "../types"; import { capitalizeString, isShallowEqual, muteFSAbortError } from "../utils"; import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; import { ErrorDialog } from "./ErrorDialog"; @@ -49,7 +55,7 @@ import "./Toolbar.scss"; interface LayerUIProps { actionManager: ActionManager; - appState: AppState; + appState: UIAppState; files: BinaryFiles; canvas: HTMLCanvasElement | null; setAppState: React.Component["setState"]; @@ -144,7 +150,8 @@ const LayerUI = ({ const fileHandle = await exportCanvas( type, exportedElements, - appState, + // FIXME once we split UI canvas from element canvas + appState as AppState, files, { exportBackground: appState.exportBackground, @@ -458,9 +465,9 @@ const LayerUI = ({ + ( + + )} + />

- {t("errorSplash.clearCanvasMessage")} - + ( + + )} + />
@@ -106,16 +113,17 @@ export class TopErrorBoundary extends React.Component<
- {t("errorSplash.trackedToSentry_pre")} - {this.state.sentryEventId} - {t("errorSplash.trackedToSentry_post")} + {t("errorSplash.trackedToSentry", { + eventId: this.state.sentryEventId, + })}
- {t("errorSplash.openIssueMessage_pre")} - - {t("errorSplash.openIssueMessage_post")} + ( + + )} + />
diff --git a/src/locales/bg-BG.json b/src/locales/bg-BG.json index 292a1544f..ef9a3759d 100644 --- a/src/locales/bg-BG.json +++ b/src/locales/bg-BG.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Подсказка: пробвайте да приближите далечните елементи по-близко." }, "errorSplash": { - "headingMain_pre": "Среща грешка. Опитайте ", - "headingMain_button": "презареждане на страницата.", - "clearCanvasMessage": "Ако презареждането не работи, опитайте ", - "clearCanvasMessage_button": "изчистване на платното.", + "headingMain": "Среща грешка. Опитайте ", + "clearCanvasMessage": "Ако презареждането не работи, опитайте ", "clearCanvasCaveat": " Това ще доведе до загуба на работа ", - "trackedToSentry_pre": "Грешката с идентификатор ", - "trackedToSentry_post": " беше проследен в нашата система.", - "openIssueMessage_pre": "Бяхме много предпазливи да не включите информацията за вашата сцена при грешката. Ако сцената ви не е частна, моля, помислете за последващи действия на нашата ", - "openIssueMessage_button": "тракер за грешки.", - "openIssueMessage_post": " Моля, включете информация по-долу, като я копирате и добавите в GitHub.", + "trackedToSentry": "Грешката с идентификатор {{eventId}} беше проследен в нашата система.", + "openIssueMessage": "Бяхме много предпазливи да не включите информацията за вашата сцена при грешката. Ако сцената ви не е частна, моля, помислете за последващи действия на нашата Моля, включете информация по-долу, като я копирате и добавите в GitHub.", "sceneContent": "Съдържание на сцената:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "", "website": "" }, - "noteDescription": { - "pre": "", - "link": "", - "post": "" - }, - "noteGuidelines": { - "pre": "", - "link": "", - "post": "" - }, - "noteLicense": { - "pre": "", - "link": "", - "post": "" - }, + "noteDescription": "", + "noteGuidelines": "", + "noteLicense": "", "noteItems": "", "atleastOneLibItem": "", "republishWarning": "" }, "publishSuccessDialog": { "title": "", - "content": "", - "link": "" + "content": "" }, "confirmDialog": { "resetLibrary": "", diff --git a/src/locales/bn-BD.json b/src/locales/bn-BD.json index 8972effd4..d652c1ebc 100644 --- a/src/locales/bn-BD.json +++ b/src/locales/bn-BD.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "বিশেষ্য: দূরতম উপাদানগুলোকে একটু কাছাকাছি নিয়ে যাওয়ার চেষ্টা করুন।" }, "errorSplash": { - "headingMain_pre": "একটি ত্রুটির সম্মুখীন হয়েছে৷ চেষ্টা করুন ", - "headingMain_button": "পৃষ্ঠাটি পুনরায় লোড করার।", - "clearCanvasMessage": "যদি পুনরায় লোড করা কাজ না করে, চেষ্টা করুন ", - "clearCanvasMessage_button": "ক্যানভাস পরিষ্কার করার।", + "headingMain": "একটি ত্রুটির সম্মুখীন হয়েছে৷ চেষ্টা করুন ", + "clearCanvasMessage": "যদি পুনরায় লোড করা কাজ না করে, চেষ্টা করুন ", "clearCanvasCaveat": " এর ফলে কাজের ক্ষতি হবে ", - "trackedToSentry_pre": "ত্রুটি ", - "trackedToSentry_post": " আমাদের সিস্টেমে ট্র্যাক করা হয়েছিল।", - "openIssueMessage_pre": "আমরা ত্রুটিতে আপনার দৃশ্যের তথ্য অন্তর্ভুক্ত না করার জন্য খুব সতর্ক ছিলাম। আপনার দৃশ্য ব্যক্তিগত না হলে, আমাদের অনুসরণ করার কথা বিবেচনা করুন ", - "openIssueMessage_button": "ত্রুটি ইতিবৃত্ত।", - "openIssueMessage_post": " অনুগ্রহ করে GitHub ইস্যুতে অনুলিপি এবং পেস্ট করে নীচের তথ্য অন্তর্ভুক্ত করুন।", + "trackedToSentry": "ত্রুটি {{eventId}} আমাদের সিস্টেমে ট্র্যাক করা হয়েছিল।", + "openIssueMessage": "আমরা ত্রুটিতে আপনার দৃশ্যের তথ্য অন্তর্ভুক্ত না করার জন্য খুব সতর্ক ছিলাম। আপনার দৃশ্য ব্যক্তিগত না হলে, আমাদের অনুসরণ করার কথা বিবেচনা করুন অনুগ্রহ করে GitHub ইস্যুতে অনুলিপি এবং পেস্ট করে নীচের তথ্য অন্তর্ভুক্ত করুন।", "sceneContent": "দৃশ্য বিষয়বস্তু:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "", "website": "" }, - "noteDescription": { - "pre": "", - "link": "", - "post": "" - }, - "noteGuidelines": { - "pre": "", - "link": "", - "post": "" - }, - "noteLicense": { - "pre": "", - "link": "", - "post": "" - }, + "noteDescription": "", + "noteGuidelines": "", + "noteLicense": "", "noteItems": "", "atleastOneLibItem": "", "republishWarning": "" }, "publishSuccessDialog": { "title": "", - "content": "", - "link": "" + "content": "" }, "confirmDialog": { "resetLibrary": "", diff --git a/src/locales/ca-ES.json b/src/locales/ca-ES.json index 1f5cdd661..02bb253d5 100644 --- a/src/locales/ca-ES.json +++ b/src/locales/ca-ES.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Consell: proveu d’acostar una mica els elements més allunyats." }, "errorSplash": { - "headingMain_pre": "S'ha produït un error. Proveu ", - "headingMain_button": "recarregar la pàgina.", - "clearCanvasMessage": "Si la recàrrega no funciona, proveu ", - "clearCanvasMessage_button": "esborrar el llenç.", + "headingMain": "S'ha produït un error. Proveu ", + "clearCanvasMessage": "Si la recàrrega no funciona, proveu ", "clearCanvasCaveat": " Això resultarà en la pèrdua de feina ", - "trackedToSentry_pre": "L'error amb l'identificador ", - "trackedToSentry_post": " s'ha rastrejat en el nostre sistema.", - "openIssueMessage_pre": "Anàvem amb molta cura de no incloure la informació de la vostra escena en l'error. Si l'escena no és privada, podeu fer-ne el seguiment al nostre ", - "openIssueMessage_button": "rastrejador d'errors.", - "openIssueMessage_post": " Incloeu la informació a continuació copiant i enganxant a GitHub Issues.", + "trackedToSentry": "L'error amb l'identificador {{eventId}} s'ha rastrejat en el nostre sistema.", + "openIssueMessage": "Anàvem amb molta cura de no incloure la informació de la vostra escena en l'error. Si l'escena no és privada, podeu fer-ne el seguiment al nostre Incloeu la informació a continuació copiant i enganxant a GitHub Issues.", "sceneContent": "Contingut de l'escena:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Requerit", "website": "Introduïu una URL vàlida" }, - "noteDescription": { - "pre": "Envieu la vostra biblioteca perquè sigui inclosa al ", - "link": "repositori públic", - "post": "per tal que altres persones puguin fer-ne ús en els seus dibuixos." - }, - "noteGuidelines": { - "pre": "La biblioteca ha de ser aprovada manualment. Si us plau, llegiu les ", - "link": "directrius", - "post": " abans d'enviar-hi res. Necessitareu un compte de GitHub per a comunicar i fer-hi canvis si cal, però no és requisit imprescindible." - }, - "noteLicense": { - "pre": "Quan l'envieu, accepteu que la biblioteca sigui publicada sota la ", - "link": "llicència MIT, ", - "post": "que, en resum, vol dir que qualsevol persona pot fer-ne ús sense restriccions." - }, + "noteDescription": "Envieu la vostra biblioteca perquè sigui inclosa al repositori públicper tal que altres persones puguin fer-ne ús en els seus dibuixos.", + "noteGuidelines": "La biblioteca ha de ser aprovada manualment. Si us plau, llegiu les directrius abans d'enviar-hi res. Necessitareu un compte de GitHub per a comunicar i fer-hi canvis si cal, però no és requisit imprescindible.", + "noteLicense": "Quan l'envieu, accepteu que la biblioteca sigui publicada sota la llicència MIT, que, en resum, vol dir que qualsevol persona pot fer-ne ús sense restriccions.", "noteItems": "Cada element de la biblioteca ha de tenir el seu propi nom per tal que sigui filtrable. S'hi inclouran els elements següents:", "atleastOneLibItem": "Si us plau, seleccioneu si més no un element de la biblioteca per a començar", "republishWarning": "Nota: alguns dels elements seleccionats s'han marcat com a publicats/enviats. Només hauríeu de reenviar elements quan actualitzeu una biblioteca existent." }, "publishSuccessDialog": { "title": "Biblioteca enviada", - "content": "Gràcies, {{authorName}}. La vostra biblioteca ha estat enviada per a ser revisada. Podeu comprovar-ne l'estat", - "link": "aquí" + "content": "Gràcies, {{authorName}}. La vostra biblioteca ha estat enviada per a ser revisada. Podeu comprovar-ne l'estataquí" }, "confirmDialog": { "resetLibrary": "Restableix la biblioteca", diff --git a/src/locales/cs-CZ.json b/src/locales/cs-CZ.json index 35ddf3a3b..3ab4827b7 100644 --- a/src/locales/cs-CZ.json +++ b/src/locales/cs-CZ.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "" }, "errorSplash": { - "headingMain_pre": "", - "headingMain_button": "", + "headingMain": "", "clearCanvasMessage": "", - "clearCanvasMessage_button": "", "clearCanvasCaveat": "", - "trackedToSentry_pre": "Chyba identifikátoru ", - "trackedToSentry_post": " byl zaznamenán v našem systému.", - "openIssueMessage_pre": "", - "openIssueMessage_button": "", - "openIssueMessage_post": "", + "trackedToSentry": "Chyba identifikátoru {{eventId}} byl zaznamenán v našem systému.", + "openIssueMessage": "", "sceneContent": "" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Povinné", "website": "Zadejte platnou URL adresu" }, - "noteDescription": { - "pre": "Odešlete svou knihovnu, pro zařazení do ", - "link": "veřejného úložiště knihoven", - "post": ", odkud ji budou moci při kreslení využít i ostatní uživatelé." - }, - "noteGuidelines": { - "pre": "Knihovna musí být nejdříve ručně schválena. Přečtěte si prosím ", - "link": "pokyny", - "post": "" - }, - "noteLicense": { - "pre": "", - "link": "", - "post": "" - }, + "noteDescription": "Odešlete svou knihovnu, pro zařazení do veřejného úložiště knihoven, odkud ji budou moci při kreslení využít i ostatní uživatelé.", + "noteGuidelines": "Knihovna musí být nejdříve ručně schválena. Přečtěte si prosím pokyny", + "noteLicense": "", "noteItems": "", "atleastOneLibItem": "", "republishWarning": "" }, "publishSuccessDialog": { "title": "Knihovna byla odeslána", - "content": "", - "link": "" + "content": "" }, "confirmDialog": { "resetLibrary": "", diff --git a/src/locales/da-DK.json b/src/locales/da-DK.json index 5edc77f50..15801ed4d 100644 --- a/src/locales/da-DK.json +++ b/src/locales/da-DK.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "" }, "errorSplash": { - "headingMain_pre": "", - "headingMain_button": "", + "headingMain": "", "clearCanvasMessage": "", - "clearCanvasMessage_button": "", "clearCanvasCaveat": "", - "trackedToSentry_pre": "", - "trackedToSentry_post": "", - "openIssueMessage_pre": "", - "openIssueMessage_button": "", - "openIssueMessage_post": " Kopiere og indsæt venligst oplysningerne nedenfor i et GitHub problem.", + "trackedToSentry": "", + "openIssueMessage": " Kopiere og indsæt venligst oplysningerne nedenfor i et GitHub problem.", "sceneContent": "Scene indhold:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "", "website": "" }, - "noteDescription": { - "pre": "", - "link": "", - "post": "" - }, - "noteGuidelines": { - "pre": "", - "link": "", - "post": "" - }, - "noteLicense": { - "pre": "", - "link": "", - "post": "" - }, + "noteDescription": "", + "noteGuidelines": "", + "noteLicense": "", "noteItems": "", "atleastOneLibItem": "", "republishWarning": "" }, "publishSuccessDialog": { "title": "", - "content": "", - "link": "" + "content": "" }, "confirmDialog": { "resetLibrary": "", diff --git a/src/locales/de-DE.json b/src/locales/de-DE.json index fb9d82e84..f0003f8a2 100644 --- a/src/locales/de-DE.json +++ b/src/locales/de-DE.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Tipp: Schiebe die am weitesten entfernten Elemente ein wenig näher zusammen." }, "errorSplash": { - "headingMain_pre": "Es ist ein Fehler aufgetreten. Versuche ", - "headingMain_button": "die Seite neu zu laden.", - "clearCanvasMessage": "Wenn das Neuladen nicht funktioniert, versuche ", - "clearCanvasMessage_button": "die Zeichenfläche zu löschen.", + "headingMain": "Es ist ein Fehler aufgetreten. Versuche ", + "clearCanvasMessage": "Wenn das Neuladen nicht funktioniert, versuche ", "clearCanvasCaveat": " Dies wird zum Verlust von Daten führen ", - "trackedToSentry_pre": "Der Fehler mit der Kennung ", - "trackedToSentry_post": " wurde in unserem System registriert.", - "openIssueMessage_pre": "Wir waren sehr vorsichtig und haben deine Zeichnungsinformationen nicht in die Fehlerinformationen aufgenommen. Wenn deine Zeichnung nicht privat ist, unterstütze uns bitte über unseren ", - "openIssueMessage_button": "Bug-Tracker.", - "openIssueMessage_post": " Bitte teile die unten stehenden Informationen mit uns im GitHub Issue (Kopieren und Einfügen).", + "trackedToSentry": "Der Fehler mit der Kennung {{eventId}} wurde in unserem System registriert.", + "openIssueMessage": "Wir waren sehr vorsichtig und haben deine Zeichnungsinformationen nicht in die Fehlerinformationen aufgenommen. Wenn deine Zeichnung nicht privat ist, unterstütze uns bitte über unseren Bitte teile die unten stehenden Informationen mit uns im GitHub Issue (Kopieren und Einfügen).", "sceneContent": "Zeichnungsinhalt:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Erforderlich", "website": "Gültige URL eingeben" }, - "noteDescription": { - "pre": "Sende deine Bibliothek ein, um in die ", - "link": "öffentliche Bibliotheks-Repository aufgenommen zu werden", - "post": "damit andere Nutzer sie in ihren Zeichnungen verwenden können." - }, - "noteGuidelines": { - "pre": "Die Bibliothek muss zuerst manuell freigegeben werden. Bitte lies die ", - "link": "Richtlinien", - "post": " vor dem Absenden. Du benötigst ein GitHub-Konto, um zu kommunizieren und Änderungen vorzunehmen, falls erforderlich, aber es ist nicht unbedingt erforderlich." - }, - "noteLicense": { - "pre": "Mit dem Absenden stimmst du zu, dass die Bibliothek unter der ", - "link": "MIT-Lizenz, ", - "post": "die zusammengefasst beinhaltet, dass jeder sie ohne Einschränkungen nutzen kann." - }, + "noteDescription": "Sende deine Bibliothek ein, um in die öffentliche Bibliotheks-Repository aufgenommen zu werdendamit andere Nutzer sie in ihren Zeichnungen verwenden können.", + "noteGuidelines": "Die Bibliothek muss zuerst manuell freigegeben werden. Bitte lies die Richtlinien vor dem Absenden. Du benötigst ein GitHub-Konto, um zu kommunizieren und Änderungen vorzunehmen, falls erforderlich, aber es ist nicht unbedingt erforderlich.", + "noteLicense": "Mit dem Absenden stimmst du zu, dass die Bibliothek unter der MIT-Lizenz, die zusammengefasst beinhaltet, dass jeder sie ohne Einschränkungen nutzen kann.", "noteItems": "Jedes Bibliothekselement muss einen eigenen Namen haben, damit es gefiltert werden kann. Die folgenden Bibliothekselemente werden hinzugefügt:", "atleastOneLibItem": "Bitte wähle mindestens ein Bibliothekselement aus, um zu beginnen", "republishWarning": "Hinweis: Einige der ausgewählten Elemente sind bereits als veröffentlicht/eingereicht markiert. Du solltest Elemente nur erneut einreichen, wenn Du eine existierende Bibliothek oder Einreichung aktualisierst." }, "publishSuccessDialog": { "title": "Bibliothek übermittelt", - "content": "Vielen Dank {{authorName}}. Deine Bibliothek wurde zur Überprüfung eingereicht. Du kannst den Status verfolgen", - "link": "hier" + "content": "Vielen Dank {{authorName}}. Deine Bibliothek wurde zur Überprüfung eingereicht. Du kannst den Status verfolgenhier" }, "confirmDialog": { "resetLibrary": "Bibliothek zurücksetzen", diff --git a/src/locales/el-GR.json b/src/locales/el-GR.json index 3b80e375c..ddd6af340 100644 --- a/src/locales/el-GR.json +++ b/src/locales/el-GR.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Συμβουλή: προσπαθήστε να μετακινήσετε τα πιο απομακρυσμένα στοιχεία λίγο πιο κοντά μαζί." }, "errorSplash": { - "headingMain_pre": "Συνέβη κάποιο σφάλμα. Προσπάθησε ", - "headingMain_button": "φόρτωσε ξανά την σελίδα.", - "clearCanvasMessage": "Εάν το παραπάνω δεν δουλέψει, προσπάθησε ", - "clearCanvasMessage_button": "καθαρίσετε τον κανβά.", + "headingMain": "Συνέβη κάποιο σφάλμα. Προσπάθησε ", + "clearCanvasMessage": "Εάν το παραπάνω δεν δουλέψει, προσπάθησε ", "clearCanvasCaveat": " Αυτό θα προκαλέσει απώλεια της δουλειάς σου ", - "trackedToSentry_pre": "Το σφάλμα με αναγνωριστικό ", - "trackedToSentry_post": " παρακολουθήθηκε στο σύστημά μας.", - "openIssueMessage_pre": "Ήμασταν πολύ προσεκτικοί για να μην συμπεριλάβουμε τις πληροφορίες της σκηνής σου στο σφάλμα. Αν η σκηνή σου δεν είναι ιδιωτική, παρακαλώ σκέψου να ακολουθήσεις το δικό μας ", - "openIssueMessage_button": "ανιχνευτής σφαλμάτων.", - "openIssueMessage_post": " Παρακαλώ να συμπεριλάβετε τις παρακάτω πληροφορίες, αντιγράφοντας και επικολλώντας το ζήτημα στο GitHub.", + "trackedToSentry": "Το σφάλμα με αναγνωριστικό {{eventId}} παρακολουθήθηκε στο σύστημά μας.", + "openIssueMessage": "Ήμασταν πολύ προσεκτικοί για να μην συμπεριλάβουμε τις πληροφορίες της σκηνής σου στο σφάλμα. Αν η σκηνή σου δεν είναι ιδιωτική, παρακαλώ σκέψου να ακολουθήσεις το δικό μας Παρακαλώ να συμπεριλάβετε τις παρακάτω πληροφορίες, αντιγράφοντας και επικολλώντας το ζήτημα στο GitHub.", "sceneContent": "Περιεχόμενο σκηνής:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Απαιτείται", "website": "Εισάγετε μια έγκυρη διεύθυνση URL" }, - "noteDescription": { - "pre": "Υποβάλετε τη βιβλιοθήκη σας για να συμπεριληφθεί στο ", - "link": "δημόσιο αποθετήριο βιβλιοθήκης", - "post": "ώστε να χρησιμοποιηθεί από άλλα άτομα στα σχέδιά τους." - }, - "noteGuidelines": { - "pre": "Η βιβλιοθήκη πρέπει πρώτα να εγκριθεί χειροκίνητα. Παρακαλώ διαβάστε τους ", - "link": "οδηγίες", - "post": " πριν την υποβολή. Θα χρειαστείτε έναν λογαριασμό GitHub για την επικοινωνία και για να προβείτε σε αλλαγές εφ' όσον χρειαστεί, αλλά δεν είναι αυστηρή απαίτηση." - }, - "noteLicense": { - "pre": "Με την υποβολή, συμφωνείτε ότι η βιβλιοθήκη θα δημοσιευθεί υπό την ", - "link": "Άδεια MIT, ", - "post": "που εν συντομία σημαίνει ότι ο καθένας μπορεί να τα χρησιμοποιήσει χωρίς περιορισμούς." - }, + "noteDescription": "Υποβάλετε τη βιβλιοθήκη σας για να συμπεριληφθεί στο δημόσιο αποθετήριο βιβλιοθήκηςώστε να χρησιμοποιηθεί από άλλα άτομα στα σχέδιά τους.", + "noteGuidelines": "Η βιβλιοθήκη πρέπει πρώτα να εγκριθεί χειροκίνητα. Παρακαλώ διαβάστε τους οδηγίες πριν την υποβολή. Θα χρειαστείτε έναν λογαριασμό GitHub για την επικοινωνία και για να προβείτε σε αλλαγές εφ' όσον χρειαστεί, αλλά δεν είναι αυστηρή απαίτηση.", + "noteLicense": "Με την υποβολή, συμφωνείτε ότι η βιβλιοθήκη θα δημοσιευθεί υπό την Άδεια MIT, που εν συντομία σημαίνει ότι ο καθένας μπορεί να τα χρησιμοποιήσει χωρίς περιορισμούς.", "noteItems": "Κάθε αντικείμενο της βιβλιοθήκης πρέπει να έχει το δικό του όνομα ώστε να μπορεί να φιλτραριστεί. Θα συμπεριληφθούν τα ακόλουθα αντικείμενα βιβλιοθήκης:", "atleastOneLibItem": "Παρακαλώ επιλέξτε τουλάχιστον ένα αντικείμενο βιβλιοθήκης για να ξεκινήσετε", "republishWarning": "Σημείωση: μερικά από τα επιλεγμένα αντικέιμενα έχουν ήδη επισημανθεί ως δημοσιευμένα/υποβεβλημένα. Θα πρέπει να υποβάλετε αντικείμενα εκ νέου μόνο για να ενημερώσετε μία ήδη υπάρχουσα βιβλιοθήκη ή υποβολή." }, "publishSuccessDialog": { "title": "Η βιβλιοθήκη υποβλήθηκε", - "content": "Ευχαριστούμε {{authorName}}. Η βιβλιοθήκη σας έχει υποβληθεί για αξιολόγηση. Μπορείτε να παρακολουθείτε τη διαδικασία", - "link": "εδώ" + "content": "Ευχαριστούμε {{authorName}}. Η βιβλιοθήκη σας έχει υποβληθεί για αξιολόγηση. Μπορείτε να παρακολουθείτε τη διαδικασίαεδώ" }, "confirmDialog": { "resetLibrary": "Καθαρισμός βιβλιοθήκης", diff --git a/src/locales/en.json b/src/locales/en.json index 86029c30c..136a0bc80 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Tip: try moving the farthest elements a bit closer together." }, "errorSplash": { - "headingMain_pre": "Encountered an error. Try ", - "headingMain_button": "reloading the page.", - "clearCanvasMessage": "If reloading doesn't work, try ", - "clearCanvasMessage_button": "clearing the canvas.", + "headingMain": "Encountered an error. Try .", + "clearCanvasMessage": "If reloading doesn't work, try .", "clearCanvasCaveat": " This will result in loss of work ", - "trackedToSentry_pre": "The error with identifier ", - "trackedToSentry_post": " was tracked on our system.", - "openIssueMessage_pre": "We were very cautious not to include your scene information on the error. If your scene is not private, please consider following up on our ", - "openIssueMessage_button": "bug tracker.", - "openIssueMessage_post": " Please include information below by copying and pasting into the GitHub issue.", + "trackedToSentry": "The error with identifier {{eventId}} was tracked on our system.", + "openIssueMessage": "We were very cautious not to include your scene information on the error. If your scene is not private, please consider following up on our . Please include information below by copying and pasting into the GitHub issue.", "sceneContent": "Scene content:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Required", "website": "Enter a valid URL" }, - "noteDescription": { - "pre": "Submit your library to be included in the ", - "link": "public library repository", - "post": "for other people to use in their drawings." - }, - "noteGuidelines": { - "pre": "The library needs to be manually approved first. Please read the ", - "link": "guidelines", - "post": " before submitting. You will need a GitHub account to communicate and make changes if requested, but it is not strictly required." - }, - "noteLicense": { - "pre": "By submitting, you agree the library will be published under the ", - "link": "MIT License, ", - "post": "which in short means anyone can use them without restrictions." - }, + "noteDescription": "Submit your library to be included in the public library repository for other people to use in their drawings.", + "noteGuidelines": "The library needs to be manually approved first. Please read the guidelines before submitting. You will need a GitHub account to communicate and make changes if requested, but it is not strictly required.", + "noteLicense": "By submitting, you agree the library will be published under the MIT License, which in short means anyone can use them without restrictions.", "noteItems": "Each library item must have its own name so it's filterable. The following library items will be included:", "atleastOneLibItem": "Please select at least one library item to get started", "republishWarning": "Note: some of the selected items are marked as already published/submitted. You should only resubmit items when updating an existing library or submission." }, "publishSuccessDialog": { "title": "Library submitted", - "content": "Thank you {{authorName}}. Your library has been submitted for review. You can track the status", - "link": "here" + "content": "Thank you {{authorName}}. Your library has been submitted for review. You can track the status here" }, "confirmDialog": { "resetLibrary": "Reset library", diff --git a/src/locales/es-ES.json b/src/locales/es-ES.json index 0362f31e2..594efb663 100644 --- a/src/locales/es-ES.json +++ b/src/locales/es-ES.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Sugerencia: intenta acercar un poco más los elementos más lejanos." }, "errorSplash": { - "headingMain_pre": "Se encontró un error. Intente ", - "headingMain_button": "recargando la página.", - "clearCanvasMessage": "Si la recarga no funciona, intente ", - "clearCanvasMessage_button": "limpiando el lienzo.", + "headingMain": "Se encontró un error. Intente ", + "clearCanvasMessage": "Si la recarga no funciona, intente ", "clearCanvasCaveat": " Esto provocará la pérdida de su trabajo ", - "trackedToSentry_pre": "El error con el identificador ", - "trackedToSentry_post": " fue rastreado en nuestro sistema.", - "openIssueMessage_pre": "Fuimos muy cautelosos de no incluir la información de tu escena en el error. Si tu escena no es privada, por favor considera seguir nuestro ", - "openIssueMessage_button": "rastreador de errores.", - "openIssueMessage_post": " Por favor, incluya la siguiente información copiándola y pegándola en el issue de GitHub.", + "trackedToSentry": "El error con el identificador {{eventId}} fue rastreado en nuestro sistema.", + "openIssueMessage": "Fuimos muy cautelosos de no incluir la información de tu escena en el error. Si tu escena no es privada, por favor considera seguir nuestro Por favor, incluya la siguiente información copiándola y pegándola en el issue de GitHub.", "sceneContent": "Contenido de la escena:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Requerido", "website": "Introduce una URL válida" }, - "noteDescription": { - "pre": "Envía tu biblioteca para ser incluida en el ", - "link": "repositorio de librería pública", - "post": "para que otras personas utilicen en sus dibujos." - }, - "noteGuidelines": { - "pre": "La biblioteca debe ser aprobada manualmente primero. Por favor, lea la ", - "link": "pautas", - "post": " antes de enviar. Necesitará una cuenta de GitHub para comunicarse y hacer cambios si se solicita, pero no es estrictamente necesario." - }, - "noteLicense": { - "pre": "Al enviar, usted acepta que la biblioteca se publicará bajo el ", - "link": "Licencia MIT ", - "post": "que en breve significa que cualquiera puede utilizarlos sin restricciones." - }, + "noteDescription": "Envía tu biblioteca para ser incluida en el repositorio de librería públicapara que otras personas utilicen en sus dibujos.", + "noteGuidelines": "La biblioteca debe ser aprobada manualmente primero. Por favor, lea la pautas antes de enviar. Necesitará una cuenta de GitHub para comunicarse y hacer cambios si se solicita, pero no es estrictamente necesario.", + "noteLicense": "Al enviar, usted acepta que la biblioteca se publicará bajo el Licencia MIT que en breve significa que cualquiera puede utilizarlos sin restricciones.", "noteItems": "Cada elemento de la biblioteca debe tener su propio nombre para que sea filtrable. Los siguientes elementos de la biblioteca serán incluidos:", "atleastOneLibItem": "Por favor, seleccione al menos un elemento de la biblioteca para empezar", "republishWarning": "Nota: algunos de los elementos seleccionados están marcados como ya publicados/enviados. Sólo debería volver a enviar elementos cuando se actualice una biblioteca o envío." }, "publishSuccessDialog": { "title": "Biblioteca enviada", - "content": "Gracias {{authorName}}. Su biblioteca ha sido enviada para ser revisada. Puede seguir el estado", - "link": "aquí" + "content": "Gracias {{authorName}}. Su biblioteca ha sido enviada para ser revisada. Puede seguir el estadoaquí" }, "confirmDialog": { "resetLibrary": "Reiniciar biblioteca", diff --git a/src/locales/eu-ES.json b/src/locales/eu-ES.json index 26aa47b1e..a3fd66cf0 100644 --- a/src/locales/eu-ES.json +++ b/src/locales/eu-ES.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Aholkua: saiatu urrunen dauden elementuak pixka bat hurbiltzen." }, "errorSplash": { - "headingMain_pre": "Errore bat aurkitu da. Saiatu ", - "headingMain_button": "orria birkargatzen.", - "clearCanvasMessage": "Birkargatzea ez bada burutzen, saiatu ", - "clearCanvasMessage_button": "oihala garbitzen.", + "headingMain": "Errore bat aurkitu da. Saiatu ", + "clearCanvasMessage": "Birkargatzea ez bada burutzen, saiatu ", "clearCanvasCaveat": " Honen ondorioz lana galduko da ", - "trackedToSentry_pre": "Identifikatzailearen errorea ", - "trackedToSentry_post": " gure sistemak behatu du.", - "openIssueMessage_pre": "Oso kontuz ibili gara zure eszenaren informazioa errorean ez sartzeko. Zure eszena pribatua ez bada, kontuan hartu gure ", - "openIssueMessage_button": "erroreen jarraipena egitea.", - "openIssueMessage_post": " Sartu beheko informazioa kopiatu eta itsatsi bidez GitHub issue-n.", + "trackedToSentry": "Identifikatzailearen errorea {{eventId}} gure sistemak behatu du.", + "openIssueMessage": "Oso kontuz ibili gara zure eszenaren informazioa errorean ez sartzeko. Zure eszena pribatua ez bada, kontuan hartu gure Sartu beheko informazioa kopiatu eta itsatsi bidez GitHub issue-n.", "sceneContent": "Eszenaren edukia:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Beharrezkoa", "website": "Sartu baliozko URL bat" }, - "noteDescription": { - "pre": "Bidali zure liburutegira sartu ahal izateko ", - "link": "zure liburutegiko biltegian", - "post": "beste jendeak bere marrazkietan erabili ahal izateko." - }, - "noteGuidelines": { - "pre": "Liburutegia eskuz onartu behar da. Irakurri ", - "link": "gidalerroak", - "post": " bidali aurretik. GitHub kontu bat edukitzea komeni da komunikatzeko eta aldaketak egin ahal izateko, baina ez da guztiz beharrezkoa." - }, - "noteLicense": { - "pre": "Bidaltzen baduzu, onartzen duzu liburutegia ", - "link": "MIT lizentziarekin argitaratuko dela, ", - "post": "zeinak, laburbilduz, esan nahi du edozeinek erabiltzen ahal duela murrizketarik gabe." - }, + "noteDescription": "Bidali zure liburutegira sartu ahal izateko zure liburutegiko biltegianbeste jendeak bere marrazkietan erabili ahal izateko.", + "noteGuidelines": "Liburutegia eskuz onartu behar da. Irakurri gidalerroak bidali aurretik. GitHub kontu bat edukitzea komeni da komunikatzeko eta aldaketak egin ahal izateko, baina ez da guztiz beharrezkoa.", + "noteLicense": "Bidaltzen baduzu, onartzen duzu liburutegia MIT lizentziarekin argitaratuko dela, zeinak, laburbilduz, esan nahi du edozeinek erabiltzen ahal duela murrizketarik gabe.", "noteItems": "Liburutegiko elementu bakoitzak bere izena eduki behar du iragazi ahal izateko. Liburutegiko hurrengo elementuak barne daude:", "atleastOneLibItem": "Hautatu gutxienez liburutegiko elementu bat gutxienez hasi ahal izateko", "republishWarning": "Oharra: hautatutako elementu batzuk dagoeneko argitaratuta/bidalita bezala markatuta daude. Elementuak berriro bidali behar dituzu lehendik dagoen liburutegi edo bidalketa eguneratzen duzunean." }, "publishSuccessDialog": { "title": "Liburutegia bidali da", - "content": "Eskerrik asko {{authorName}}. Zure liburutegia bidali da berrikustera. Jarraitu dezakezu haren egoera", - "link": "hemen" + "content": "Eskerrik asko {{authorName}}. Zure liburutegia bidali da berrikustera. Jarraitu dezakezu haren egoerahemen" }, "confirmDialog": { "resetLibrary": "Leheneratu liburutegia", diff --git a/src/locales/fi-FI.json b/src/locales/fi-FI.json index 35cbd6cdb..399876265 100644 --- a/src/locales/fi-FI.json +++ b/src/locales/fi-FI.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Vinkki: yritä siirtää kaukaisimpia elementtejä hieman lähemmäs toisiaan." }, "errorSplash": { - "headingMain_pre": "Tapahtui virhe. Yritä ", - "headingMain_button": "sivun lataamista uudelleen.", - "clearCanvasMessage": "Mikäli sivun lataaminen uudelleen ei auta, yritä ", - "clearCanvasMessage_button": "tyhjentää piirtoalue.", + "headingMain": "Tapahtui virhe. Yritä ", + "clearCanvasMessage": "Mikäli sivun lataaminen uudelleen ei auta, yritä ", "clearCanvasCaveat": " Tämä johtaa työn menetykseen ", - "trackedToSentry_pre": "Virhe tunnisteella ", - "trackedToSentry_post": " tallennettiin järjestelmäämme.", - "openIssueMessage_pre": "Olimme varovaisia emmekä sisällyttäneet tietoa piirroksestasi virheeseen. Mikäli piirroksesi ei ole yksityinen, harkitsethan kertovasi meille ", - "openIssueMessage_button": "virheenseurantajärjestelmässämme.", - "openIssueMessage_post": " Sisällytä alla olevat tiedot kopioimalla ne GitHub-ongelmaan.", + "trackedToSentry": "Virhe tunnisteella {{eventId}} tallennettiin järjestelmäämme.", + "openIssueMessage": "Olimme varovaisia emmekä sisällyttäneet tietoa piirroksestasi virheeseen. Mikäli piirroksesi ei ole yksityinen, harkitsethan kertovasi meille Sisällytä alla olevat tiedot kopioimalla ne GitHub-ongelmaan.", "sceneContent": "Piirroksen tiedot:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Pakollinen", "website": "Syötä oikeamuotoinen URL-osoite" }, - "noteDescription": { - "pre": "Lähetä kirjastosi, jotta se voidaan sisällyttää ", - "link": "julkisessa kirjastolistauksessa", - "post": "muiden käyttöön omissa piirrustuksissaan." - }, - "noteGuidelines": { - "pre": "Kirjasto on ensin hyväksyttävä manuaalisesti. Ole hyvä ja lue ", - "link": "ohjeet", - "post": " ennen lähettämistä. Tarvitset GitHub-tilin, jotta voit viestiä ja tehdä muutoksia pyydettäessä, mutta se ei ole ehdottoman välttämätöntä." - }, - "noteLicense": { - "pre": "Lähettämällä hyväksyt että kirjasto julkaistaan ", - "link": "MIT-lisenssin ", - "post": "alla, mikä lyhyesti antaa muiden käyttää sitä ilman rajoituksia." - }, + "noteDescription": "Lähetä kirjastosi, jotta se voidaan sisällyttää julkisessa kirjastolistauksessamuiden käyttöön omissa piirrustuksissaan.", + "noteGuidelines": "Kirjasto on ensin hyväksyttävä manuaalisesti. Ole hyvä ja lue ohjeet ennen lähettämistä. Tarvitset GitHub-tilin, jotta voit viestiä ja tehdä muutoksia pyydettäessä, mutta se ei ole ehdottoman välttämätöntä.", + "noteLicense": "Lähettämällä hyväksyt että kirjasto julkaistaan MIT-lisenssin alla, mikä lyhyesti antaa muiden käyttää sitä ilman rajoituksia.", "noteItems": "Jokaisella kirjaston kohteella on oltava oma nimensä suodatusta varten. Seuraavat kirjaston kohteet sisältyvät:", "atleastOneLibItem": "Valitse vähintään yksi kirjaston kohde aloittaaksesi", "republishWarning": "Huom! Osa valituista kohteista on merkitty jo julkaistu/lähetetyiksi. Lähetä kohteita uudelleen vain päivitettäessä olemassa olevaa kirjastoa tai ehdotusta." }, "publishSuccessDialog": { "title": "Kirjasto lähetetty", - "content": "Kiitos {{authorName}}. Kirjastosi on lähetetty tarkistettavaksi. Voit seurata sen tilaa", - "link": "täällä" + "content": "Kiitos {{authorName}}. Kirjastosi on lähetetty tarkistettavaksi. Voit seurata sen tilaatäällä" }, "confirmDialog": { "resetLibrary": "Tyhjennä kirjasto", diff --git a/src/locales/fr-FR.json b/src/locales/fr-FR.json index 0997997ee..b97f2c285 100644 --- a/src/locales/fr-FR.json +++ b/src/locales/fr-FR.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Astuce : essayez de rapprocher un peu les éléments les plus éloignés." }, "errorSplash": { - "headingMain_pre": "Une erreur est survenue. Essayez ", - "headingMain_button": "de recharger la page.", - "clearCanvasMessage": "Si le rechargement ne résout pas l'erreur, essayez ", - "clearCanvasMessage_button": "effacement du canevas.", + "headingMain": "Une erreur est survenue. Essayez ", + "clearCanvasMessage": "Si le rechargement ne résout pas l'erreur, essayez ", "clearCanvasCaveat": " Cela entraînera une perte du travail ", - "trackedToSentry_pre": "L'erreur avec l'identifiant ", - "trackedToSentry_post": " a été enregistrée dans notre système.", - "openIssueMessage_pre": "Nous avons fait très attention à ne pas inclure les informations de votre scène dans l'erreur. Si votre scène n'est pas privée, veuillez envisager de poursuivre sur notre ", - "openIssueMessage_button": "outil de suivi des bugs.", - "openIssueMessage_post": " Veuillez inclure les informations ci-dessous en les copiant-collant dans le ticket GitHub.", + "trackedToSentry": "L'erreur avec l'identifiant {{eventId}} a été enregistrée dans notre système.", + "openIssueMessage": "Nous avons fait très attention à ne pas inclure les informations de votre scène dans l'erreur. Si votre scène n'est pas privée, veuillez envisager de poursuivre sur notre Veuillez inclure les informations ci-dessous en les copiant-collant dans le ticket GitHub.", "sceneContent": "Contenu de la scène :" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Requis", "website": "Entrer une URL valide" }, - "noteDescription": { - "pre": "Soumets ta bibliothèque pour l'inclure au ", - "link": "dépôt de bibliothèque publique", - "post": "pour permettre son utilisation par autrui dans leurs dessins." - }, - "noteGuidelines": { - "pre": "La bibliothèque doit d'abord être approuvée manuellement. Veuillez lire les ", - "link": "lignes directrices", - "post": " avant de la soumettre. Vous aurez besoin d'un compte GitHub pour communiquer et apporter des modifications si demandé, mais ce n'est pas obligatoire." - }, - "noteLicense": { - "pre": "En soumettant, vous acceptez que la bibliothèque soit publiée sous la ", - "link": "Licence MIT, ", - "post": "ce qui en gros signifie que tout le monde peut l'utiliser sans restrictions." - }, + "noteDescription": "Soumets ta bibliothèque pour l'inclure au dépôt de bibliothèque publiquepour permettre son utilisation par autrui dans leurs dessins.", + "noteGuidelines": "La bibliothèque doit d'abord être approuvée manuellement. Veuillez lire les lignes directrices avant de la soumettre. Vous aurez besoin d'un compte GitHub pour communiquer et apporter des modifications si demandé, mais ce n'est pas obligatoire.", + "noteLicense": "En soumettant, vous acceptez que la bibliothèque soit publiée sous la Licence MIT, ce qui en gros signifie que tout le monde peut l'utiliser sans restrictions.", "noteItems": "Chaque élément de la bibliothèque doit avoir son propre nom afin qu'il soit filtrable. Les éléments de bibliothèque suivants seront inclus :", "atleastOneLibItem": "Veuillez sélectionner au moins un élément de bibliothèque pour commencer", "republishWarning": "Remarque : certains des éléments sélectionnés sont marqués comme étant déjà publiés/soumis. Vous devez uniquement resoumettre des éléments lors de la mise à jour d'une bibliothèque ou d'une soumission existante." }, "publishSuccessDialog": { "title": "Bibliothèque soumise", - "content": "Merci {{authorName}}. Votre bibliothèque a été soumise pour examen. Vous pouvez suivre le statut", - "link": "ici" + "content": "Merci {{authorName}}. Votre bibliothèque a été soumise pour examen. Vous pouvez suivre le statutici" }, "confirmDialog": { "resetLibrary": "Réinitialiser la bibliothèque", diff --git a/src/locales/gl-ES.json b/src/locales/gl-ES.json index 7a6051771..602ddac17 100644 --- a/src/locales/gl-ES.json +++ b/src/locales/gl-ES.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Consello: Probe a acercar un pouco os elementos máis afastados." }, "errorSplash": { - "headingMain_pre": "Atopouse un erro. Probe ", - "headingMain_button": "recargando a páxina.", - "clearCanvasMessage": "Se recargar non funcionou, probe ", - "clearCanvasMessage_button": "limpando o lenzo.", + "headingMain": "Atopouse un erro. Probe ", + "clearCanvasMessage": "Se recargar non funcionou, probe ", "clearCanvasCaveat": " Isto resultará nunha perda do seu traballo ", - "trackedToSentry_pre": "O erro con identificador ", - "trackedToSentry_post": " foi rastrexado no noso sistema.", - "openIssueMessage_pre": "Fomos moi cautelosos de non incluír a información da súa escena no erro. Se a súa escena non é privada, por favor, considere o seguimento do noso ", - "openIssueMessage_button": "rastrexador de erros.", - "openIssueMessage_post": " Por favor inclúa a seguinte información copiándoa e pegándoa na issue de Github.", + "trackedToSentry": "O erro con identificador {{eventId}} foi rastrexado no noso sistema.", + "openIssueMessage": "Fomos moi cautelosos de non incluír a información da súa escena no erro. Se a súa escena non é privada, por favor, considere o seguimento do noso Por favor inclúa a seguinte información copiándoa e pegándoa na issue de Github.", "sceneContent": "Contido da escena:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Obrigatorio", "website": "Introduza unha URL válida" }, - "noteDescription": { - "pre": "Envíe a súa biblioteca para que sexa incluída no ", - "link": "repositorio público de bibliotecas", - "post": "para que outra xente a poida usar nos seus debuxos." - }, - "noteGuidelines": { - "pre": "A biblioteca necesita ser aprobada manualmente primeiro. Por favor, lea as ", - "link": "normas", - "post": " antes de ser enviado. Necesitarás unha conta de GitHub para comunicarte ou facer cambios se se solicitan, pero non é estritamente necesario." - }, - "noteLicense": { - "pre": "Ao enviar, estás de acordo con que a biblioteca sexa publicada baixo a ", - "link": "Licenza MIT, ", - "post": "o cal significa que, en resumo, calquera pode usalo sen restricións." - }, + "noteDescription": "Envíe a súa biblioteca para que sexa incluída no repositorio público de bibliotecaspara que outra xente a poida usar nos seus debuxos.", + "noteGuidelines": "A biblioteca necesita ser aprobada manualmente primeiro. Por favor, lea as normas antes de ser enviado. Necesitarás unha conta de GitHub para comunicarte ou facer cambios se se solicitan, pero non é estritamente necesario.", + "noteLicense": "Ao enviar, estás de acordo con que a biblioteca sexa publicada baixo a Licenza MIT, o cal significa que, en resumo, calquera pode usalo sen restricións.", "noteItems": "Cada elemento da biblioteca debe ter o seu nome propio para que se poida filtrar. Os seguintes elementos da biblioteca serán incluídos:", "atleastOneLibItem": "Por favor seleccione polo menos un elemento da biblioteca para comezar", "republishWarning": "Nota: algúns dos elementos seleccionados están marcados como xa publicados/enviados. Só deberías reenviar elementos cando se actualice unha biblioteca ou envío." }, "publishSuccessDialog": { "title": "Biblioteca enviada", - "content": "Grazas {{authorName}}. A súa biblioteca foi enviada para ser revisada. Pode seguir o estado", - "link": "aquí" + "content": "Grazas {{authorName}}. A súa biblioteca foi enviada para ser revisada. Pode seguir o estadoaquí" }, "confirmDialog": { "resetLibrary": "Restablecer biblioteca", diff --git a/src/locales/hi-IN.json b/src/locales/hi-IN.json index 08398344e..063a53a0d 100644 --- a/src/locales/hi-IN.json +++ b/src/locales/hi-IN.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "कैनवास बहुत बड़ा टिप" }, "errorSplash": { - "headingMain_pre": "एक त्रुटि का सामना करना पड़ा। प्रयत्न ", - "headingMain_button": "इस पृष्ठ को पुनः लोड करें", - "clearCanvasMessage": "यदि पुनः लोड करना काम नहीं करता है, तो प्रयास करें ", - "clearCanvasMessage_button": "कैनवास साफ करना।", + "headingMain": "एक त्रुटि का सामना करना पड़ा। प्रयत्न ", + "clearCanvasMessage": "यदि पुनः लोड करना काम नहीं करता है, तो प्रयास करें ", "clearCanvasCaveat": " इससे काम का नुकसान होगा ", - "trackedToSentry_pre": "पहचानकर्ता के साथ त्रुटि ", - "trackedToSentry_post": " हमारे सिस्टम पर नज़र रखी गई थी।", - "openIssueMessage_pre": "हम बहुत सतर्क थे कि त्रुटि पर आपकी दृश्य जानकारी शामिल न करें। यदि आपका दृश्य निजी नहीं है, तो कृपया हमारे बारे में विचार करें ", - "openIssueMessage_button": "बग ट्रैकर", - "openIssueMessage_post": " कृपया GitHub मुद्दे को कॉपी और पेस्ट करके नीचे दी गई जानकारी शामिल करें।", + "trackedToSentry": "पहचानकर्ता के साथ त्रुटि {{eventId}} हमारे सिस्टम पर नज़र रखी गई थी।", + "openIssueMessage": "हम बहुत सतर्क थे कि त्रुटि पर आपकी दृश्य जानकारी शामिल न करें। यदि आपका दृश्य निजी नहीं है, तो कृपया हमारे बारे में विचार करें कृपया GitHub मुद्दे को कॉपी और पेस्ट करके नीचे दी गई जानकारी शामिल करें।", "sceneContent": "दृश्य सामग्री:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "", "website": "मान्य URL प्रविष्ट करें" }, - "noteDescription": { - "pre": "संग्रह सम्मिलित करने हेतु प्रस्तुत करें ", - "link": "सार्वजनिक संग्रहालय", - "post": "अन्य वक्तियों को उनके चित्रकारी में उपयोग के लिये" - }, - "noteGuidelines": { - "pre": "संग्रह को पहले स्वीकृति आवश्यक कृपया यह पढ़ें ", - "link": "दिशा-निर्देश", - "post": "" - }, - "noteLicense": { - "pre": "", - "link": "", - "post": "" - }, + "noteDescription": "संग्रह सम्मिलित करने हेतु प्रस्तुत करें सार्वजनिक संग्रहालयअन्य वक्तियों को उनके चित्रकारी में उपयोग के लिये", + "noteGuidelines": "संग्रह को पहले स्वीकृति आवश्यक कृपया यह पढ़ें दिशा-निर्देश", + "noteLicense": "", "noteItems": "", "atleastOneLibItem": "", "republishWarning": "टिप्पणी: कुछ चुने हुवे आइटम पहले ही प्रकाशित/प्रस्तुत किए जा चुके हैं। किसी प्रकाशित संग्रह को अद्यतन करते समय या पहले से प्रस्तुत आइटम को पुन्हा प्रस्तुत करते समय, आप बस उसे केवल अद्यतन करें ।" }, "publishSuccessDialog": { "title": "", - "content": "", - "link": "" + "content": "" }, "confirmDialog": { "resetLibrary": "", diff --git a/src/locales/hu-HU.json b/src/locales/hu-HU.json index 472d13190..a54dde9f6 100644 --- a/src/locales/hu-HU.json +++ b/src/locales/hu-HU.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Tipp: próbáld meg a legtávolabbi elemeket közelebb hozni egy máshoz." }, "errorSplash": { - "headingMain_pre": "Hiba történt. Próbáld ", - "headingMain_button": "újratölteni az oldalt.", - "clearCanvasMessage": "Ha az újratöltés nem működik, próbáld ", - "clearCanvasMessage_button": "letörölni a vászont.", + "headingMain": "Hiba történt. Próbáld ", + "clearCanvasMessage": "Ha az újratöltés nem működik, próbáld ", "clearCanvasCaveat": " Ezzel az eddigi munka elveszik ", - "trackedToSentry_pre": "A hibakód azonosítóval ", - "trackedToSentry_post": " nyomon van követve a rendszerünkben.", - "openIssueMessage_pre": "Vigyáztunk arra, hogy a jelenthez tartozó információ ne jelenjen meg a hibaüzenetben. Ha a jeleneted nem bizalmas, kérjük add hozzá a ", - "openIssueMessage_button": "hibakövető rendszerünkhöz.", - "openIssueMessage_post": " Kérjük, másolja be az alábbi információkat a GitHub problémába.", + "trackedToSentry": "A hibakód azonosítóval {{eventId}} nyomon van követve a rendszerünkben.", + "openIssueMessage": "Vigyáztunk arra, hogy a jelenthez tartozó információ ne jelenjen meg a hibaüzenetben. Ha a jeleneted nem bizalmas, kérjük add hozzá a Kérjük, másolja be az alábbi információkat a GitHub problémába.", "sceneContent": "Jelenet tartalma:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Kötelező", "website": "Adj meg egy érvényes URL-t" }, - "noteDescription": { - "pre": "Küld be könyvtáradat, hogy bekerüljön a ", - "link": "nyilvános könyvtár tárolóba", - "post": "hogy mások is felhasználhassák a rajzaikban." - }, - "noteGuidelines": { - "pre": "A könyvtárat először manuálisan kell jóváhagyni. Kérjük, olvassa el a ", - "link": "segédletet", - "post": " benyújtása előtt. Szüksége lesz egy GitHub-fiókra a kommunikációhoz és a módosításokhoz, ha kérik, de ez nem feltétlenül szükséges." - }, - "noteLicense": { - "pre": "A beküldéssel elfogadja, hogy a könyvtár a következő alatt kerül közzétételre ", - "link": "MIT Licensz ", - "post": "ami röviden azt jelenti, hogy bárki korlátozás nélkül használhatja őket." - }, + "noteDescription": "Küld be könyvtáradat, hogy bekerüljön a nyilvános könyvtár tárolóbahogy mások is felhasználhassák a rajzaikban.", + "noteGuidelines": "A könyvtárat először manuálisan kell jóváhagyni. Kérjük, olvassa el a segédletet benyújtása előtt. Szüksége lesz egy GitHub-fiókra a kommunikációhoz és a módosításokhoz, ha kérik, de ez nem feltétlenül szükséges.", + "noteLicense": "A beküldéssel elfogadja, hogy a könyvtár a következő alatt kerül közzétételre MIT Licensz ami röviden azt jelenti, hogy bárki korlátozás nélkül használhatja őket.", "noteItems": "Minden könyvtárelemnek saját nevével kell rendelkeznie, hogy szűrhető legyen. A következő könyvtári tételek kerülnek bele:", "atleastOneLibItem": "A kezdéshez válassz ki legalább egy könyvtári elemet", "republishWarning": "" }, "publishSuccessDialog": { "title": "A könyvtár beküldve", - "content": "Köszönjük {{authorName}}. Könyvtáradat elküldtük felülvizsgálatra. Nyomon követheted az állapotot", - "link": "itt" + "content": "Köszönjük {{authorName}}. Könyvtáradat elküldtük felülvizsgálatra. Nyomon követheted az állapototitt" }, "confirmDialog": { "resetLibrary": "Könyvtár alaphelyzetbe állítása", diff --git a/src/locales/id-ID.json b/src/locales/id-ID.json index af909f24c..bb83459dc 100644 --- a/src/locales/id-ID.json +++ b/src/locales/id-ID.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Tip: coba pindahkan elemen-terjauh lebih dekat bersama." }, "errorSplash": { - "headingMain_pre": "Mengalami sebuah kesalahan. Cobalah ", - "headingMain_button": "muat ulang halaman.", - "clearCanvasMessage": "Jika memuat ulang tidak bekerja, cobalah ", - "clearCanvasMessage_button": "bersihkan canvas.", + "headingMain": "Mengalami sebuah kesalahan. Cobalah ", + "clearCanvasMessage": "Jika memuat ulang tidak bekerja, cobalah ", "clearCanvasCaveat": " Ini akan menghasilkan hilangnya pekerjaan ", - "trackedToSentry_pre": "Kesalahan dengan pengidentifikasi ", - "trackedToSentry_post": " dilacak di sistem kami.", - "openIssueMessage_pre": "Kami sangat berhati-hati untuk tidak menyertakan informasi pemandangan Anda pada kesalahan. Jika pemandangan Anda tidak bersifat pribadi, mohon pertimbangkan menindak lanjut pada ", - "openIssueMessage_button": "pelacak bug.", - "openIssueMessage_post": " Mohon sertakan informasi dibawah ini dengan menyalin dan menempelkan di Github issue.", + "trackedToSentry": "Kesalahan dengan pengidentifikasi {{eventId}} dilacak di sistem kami.", + "openIssueMessage": "Kami sangat berhati-hati untuk tidak menyertakan informasi pemandangan Anda pada kesalahan. Jika pemandangan Anda tidak bersifat pribadi, mohon pertimbangkan menindak lanjut pada Mohon sertakan informasi dibawah ini dengan menyalin dan menempelkan di Github issue.", "sceneContent": "Pemandangan konten:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Dibutuhkan", "website": "Masukkan URL valid" }, - "noteDescription": { - "pre": "Kirimkan pustaka Anda untuk disertakan di ", - "link": "repositori pustaka publik", - "post": "untuk orang lain menggunakannya dalam gambar mereka." - }, - "noteGuidelines": { - "pre": "Pustaka butuh disetujui secara manual terlebih dahulu. Baca ", - "link": "pedoman", - "post": " sebelum mengirim. Anda butuh akun GitHub untuk berkomunikasi dan membuat perubahan jika dibutuhkan, tetapi tidak wajib dibutukan." - }, - "noteLicense": { - "pre": "Dengan mengkirimkannya, Anda setuju pustaka akan diterbitkan dibawah ", - "link": "Lisensi MIT, ", - "post": "yang artinya siapa pun dapat menggunakannya tanpa batasan." - }, + "noteDescription": "Kirimkan pustaka Anda untuk disertakan di repositori pustaka publikuntuk orang lain menggunakannya dalam gambar mereka.", + "noteGuidelines": "Pustaka butuh disetujui secara manual terlebih dahulu. Baca pedoman sebelum mengirim. Anda butuh akun GitHub untuk berkomunikasi dan membuat perubahan jika dibutuhkan, tetapi tidak wajib dibutukan.", + "noteLicense": "Dengan mengkirimkannya, Anda setuju pustaka akan diterbitkan dibawah Lisensi MIT, yang artinya siapa pun dapat menggunakannya tanpa batasan.", "noteItems": "Setiap item pustaka harus memiliki nama, sehingga bisa disortir. Item pustaka di bawah ini akan dimasukan:", "atleastOneLibItem": "Pilih setidaknya satu item pustaka untuk mulai", "republishWarning": "Catatan: beberapa item yang dipilih telah ditandai sebagai sudah dipublikasikan/diserahkan. Anda hanya dapat menyerahkan kembali item-item ketika memperbarui pustaka atau pengumpulan." }, "publishSuccessDialog": { "title": "Pustaka telah dikirm", - "content": "Terima kasih {{authorName}}. pustaka Anda telah diserahkan untuk ditinjau ulang. Anda dapat cek statusnya", - "link": "di sini" + "content": "Terima kasih {{authorName}}. pustaka Anda telah diserahkan untuk ditinjau ulang. Anda dapat cek statusnyadi sini" }, "confirmDialog": { "resetLibrary": "Reset pustaka", diff --git a/src/locales/it-IT.json b/src/locales/it-IT.json index 3bf7f7749..12d215623 100644 --- a/src/locales/it-IT.json +++ b/src/locales/it-IT.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Suggerimento: prova a spostare gli elementi più lontani più vicini tra loro." }, "errorSplash": { - "headingMain_pre": "Si è verificato un errore. Provare ", - "headingMain_button": "ricaricando la pagina.", - "clearCanvasMessage": "Se ricaricare non funziona, prova ", - "clearCanvasMessage_button": "pulire la tela.", + "headingMain": "Si è verificato un errore. Provare ", + "clearCanvasMessage": "Se ricaricare non funziona, prova ", "clearCanvasCaveat": " Questo risulterà nella perdita del lavoro ", - "trackedToSentry_pre": "L'errore con identificativo ", - "trackedToSentry_post": " è stato tracciato nel nostro sistema.", - "openIssueMessage_pre": "Siamo stati molto cauti nel non includere informazioni della scena nell'errore. Se la tua scena non è privata, ti preghiamo di considerare la sua inclusione nel nostro ", - "openIssueMessage_button": "bug tracker.", - "openIssueMessage_post": " Per favore includi le informazioni riportate qui sotto copiandole e incollandole nella issue di GitHub.", + "trackedToSentry": "L'errore con identificativo {{eventId}} è stato tracciato nel nostro sistema.", + "openIssueMessage": "Siamo stati molto cauti nel non includere informazioni della scena nell'errore. Se la tua scena non è privata, ti preghiamo di considerare la sua inclusione nel nostro Per favore includi le informazioni riportate qui sotto copiandole e incollandole nella issue di GitHub.", "sceneContent": "Contenuto della scena:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Obbligatorio", "website": "Inserisci un URL valido" }, - "noteDescription": { - "pre": "Invia la tua libreria da includere nella ", - "link": "repository della libreria pubblica", - "post": "perché sia usata da altri nei loro disegni." - }, - "noteGuidelines": { - "pre": "La libreria dev'esser prima approvata manualmente. Sei pregato di leggere le ", - "link": "linee guida", - "post": " prima di inviarla. Necessiterai di un profilo di GitHub per comunicare ed effettuare modifiche se richiesto, ma non è strettamente necessario." - }, - "noteLicense": { - "pre": "Inviando, acconsenti che la libreria sarà pubblicata sotto la ", - "link": "Licenza MIT, ", - "post": "che in breve significa che chiunque possa usarla senza restrizioni." - }, + "noteDescription": "Invia la tua libreria da includere nella repository della libreria pubblicaperché sia usata da altri nei loro disegni.", + "noteGuidelines": "La libreria dev'esser prima approvata manualmente. Sei pregato di leggere le linee guida prima di inviarla. Necessiterai di un profilo di GitHub per comunicare ed effettuare modifiche se richiesto, ma non è strettamente necessario.", + "noteLicense": "Inviando, acconsenti che la libreria sarà pubblicata sotto la Licenza MIT, che in breve significa che chiunque possa usarla senza restrizioni.", "noteItems": "Ogni elemento della libreria deve avere il proprio nome, così che sia filtrabile. Gli elementi della seguente libreria saranno inclusi:", "atleastOneLibItem": "Sei pregato di selezionare almeno un elemento della libreria per iniziare", "republishWarning": "Nota: alcuni degli elementi selezionati sono contrassegnati come già pubblicati/presentati. È necessario reinviare gli elementi solo quando si aggiorna una libreria o una presentazione esistente." }, "publishSuccessDialog": { "title": "Libreria inviata", - "content": "Grazie {{authorName}}. La tua libreria è stata inviata per la revisione. Puoi monitorarne lo stato", - "link": "qui" + "content": "Grazie {{authorName}}. La tua libreria è stata inviata per la revisione. Puoi monitorarne lo statoqui" }, "confirmDialog": { "resetLibrary": "Ripristina la libreria", diff --git a/src/locales/ja-JP.json b/src/locales/ja-JP.json index f2686a883..fd4f2c2c7 100644 --- a/src/locales/ja-JP.json +++ b/src/locales/ja-JP.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "ヒント: 最も遠い要素をもう少し近づけてみてください。" }, "errorSplash": { - "headingMain_pre": "エラーが発生しました。もう一度やり直してください。 ", - "headingMain_button": "ページを再読み込みする。", - "clearCanvasMessage": "再読み込みがうまくいかない場合は、 ", - "clearCanvasMessage_button": "キャンバスを消去しています", + "headingMain": "エラーが発生しました。もう一度やり直してください。 ", + "clearCanvasMessage": "再読み込みがうまくいかない場合は、 ", "clearCanvasCaveat": " これにより作業が失われます ", - "trackedToSentry_pre": "識別子のエラー ", - "trackedToSentry_post": " が我々のシステムで追跡されました。", - "openIssueMessage_pre": "エラーに関するシーン情報を含めないように非常に慎重に設定しました。もしあなたのシーンがプライベートでない場合は、私たちのフォローアップを検討してください。 ", - "openIssueMessage_button": "バグ報告", - "openIssueMessage_post": " GitHub のIssueに以下の情報をコピーして貼り付けてください。", + "trackedToSentry": "識別子のエラー {{eventId}} が我々のシステムで追跡されました。", + "openIssueMessage": "エラーに関するシーン情報を含めないように非常に慎重に設定しました。もしあなたのシーンがプライベートでない場合は、私たちのフォローアップを検討してください。 GitHub のIssueに以下の情報をコピーして貼り付けてください。", "sceneContent": "シーンの内容:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "必須項目", "website": "有効な URL を入力してください" }, - "noteDescription": { - "pre": "以下に含めるライブラリを提出してください ", - "link": "公開ライブラリのリポジトリ", - "post": "他の人が作図に使えるようにするためです" - }, - "noteGuidelines": { - "pre": "最初にライブラリを手動で承認する必要があります。次をお読みください ", - "link": "ガイドライン", - "post": " 送信する前に、GitHubアカウントが必要になりますが、必須ではありません。" - }, - "noteLicense": { - "pre": "提出することにより、ライブラリが次の下で公開されることに同意します: ", - "link": "MIT ライセンス ", - "post": "つまり誰でも制限なく使えるということです" - }, + "noteDescription": "以下に含めるライブラリを提出してください 公開ライブラリのリポジトリ他の人が作図に使えるようにするためです", + "noteGuidelines": "最初にライブラリを手動で承認する必要があります。次をお読みください ガイドライン 送信する前に、GitHubアカウントが必要になりますが、必須ではありません。", + "noteLicense": "提出することにより、ライブラリが次の下で公開されることに同意します: MIT ライセンス つまり誰でも制限なく使えるということです", "noteItems": "各ライブラリ項目は、フィルタリングのために独自の名前を持つ必要があります。以下のライブラリアイテムが含まれます:", "atleastOneLibItem": "開始するには少なくとも1つのライブラリ項目を選択してください", "republishWarning": "注意: 選択された項目の中には、すでに公開/投稿済みと表示されているものがあります。既存のライブラリや投稿を更新する場合のみ、アイテムを再投稿してください。" }, "publishSuccessDialog": { "title": "ライブラリを送信しました", - "content": "{{authorName}} さん、ありがとうございます。あなたのライブラリはレビューのために提出されました。状況を追跡できます。", - "link": "こちら" + "content": "{{authorName}} さん、ありがとうございます。あなたのライブラリはレビューのために提出されました。状況を追跡できます。こちら" }, "confirmDialog": { "resetLibrary": "ライブラリをリセット", diff --git a/src/locales/kab-KAB.json b/src/locales/kab-KAB.json index 4a84074ed..364eed4ec 100644 --- a/src/locales/kab-KAB.json +++ b/src/locales/kab-KAB.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Tixidest: eɛreḍ ad tesqerbeḍ ciṭ iferdisen yembaɛaden." }, "errorSplash": { - "headingMain_pre": "Teḍra-d tuccḍa. Eɛreḍ ", - "headingMain_button": "asali n usebter tikkelt-nniḍen.", - "clearCanvasMessage": "Ma yella tulsa n usali ur tefri ara ugur, eɛreḍ ", - "clearCanvasMessage_button": "asfaḍ n teɣzut n usuneɣ.", + "headingMain": "Teḍra-d tuccḍa. Eɛreḍ ", + "clearCanvasMessage": "Ma yella tulsa n usali ur tefri ara ugur, eɛreḍ ", "clearCanvasCaveat": " Ayagi ad d-iglu s usṛuḥu n umahil ", - "trackedToSentry_pre": "Tuccḍa akked umesmagi ", - "trackedToSentry_post": " tettwasekles deg unagraw-nneɣ.", - "openIssueMessage_pre": "Nḥuder aṭas akken ur nseddu ara talɣut n usayes-inek (m) di tuccḍa. Ma yella asayes-inek (m) mačči d amaẓlay, ttxil-k (m) xemmem ad ḍefreḍ ", - "openIssueMessage_button": "afecku n weḍfar n yibugen.", - "openIssueMessage_post": " Ma ulac uɣilif seddu talɣut ukessar-agi s wenɣal akked usenṭeḍ di GitHub issue.", + "trackedToSentry": "Tuccḍa akked umesmagi {{eventId}} tettwasekles deg unagraw-nneɣ.", + "openIssueMessage": "Nḥuder aṭas akken ur nseddu ara talɣut n usayes-inek (m) di tuccḍa. Ma yella asayes-inek (m) mačči d amaẓlay, ttxil-k (m) xemmem ad ḍefreḍ Ma ulac uɣilif seddu talɣut ukessar-agi s wenɣal akked usenṭeḍ di GitHub issue.", "sceneContent": "Agbur n usayes:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Yettwasra", "website": "Sekcem URL ameɣtu" }, - "noteDescription": { - "pre": "Azen tamkarḍit-inek•inem akken ad teddu di ", - "link": "akaram azayez n temkarḍit", - "post": "i yimdanen-nniḍen ara isqedcen deg wunuɣen-nnsen." - }, - "noteGuidelines": { - "pre": "Tamkarḍit teḥwaǧ ad tettwaqbel s ufus qbel. Ma ulac uɣilif ɣer ", - "link": "iwellihen", - "post": " send ad tazneḍ. Tesriḍ amiḍan n GitHub akken ad tmmeslayeḍ yerna ad tgeḍ ibeddilen ma yelaq, maca mačči d ayen yettwaḥetmen." - }, - "noteLicense": { - "pre": "Mi tuzneḍ ad tqebleḍ akken tamkarḍit ad d-teffeɣ s ", - "link": "Turagt MIT, ", - "post": "ayen yebɣan ad d-yini belli yal yiwen izmer ad ten-iseqdec war tilist." - }, + "noteDescription": "Azen tamkarḍit-inek•inem akken ad teddu di akaram azayez n temkarḍiti yimdanen-nniḍen ara isqedcen deg wunuɣen-nnsen.", + "noteGuidelines": "Tamkarḍit teḥwaǧ ad tettwaqbel s ufus qbel. Ma ulac uɣilif ɣer iwellihen send ad tazneḍ. Tesriḍ amiḍan n GitHub akken ad tmmeslayeḍ yerna ad tgeḍ ibeddilen ma yelaq, maca mačči d ayen yettwaḥetmen.", + "noteLicense": "Mi tuzneḍ ad tqebleḍ akken tamkarḍit ad d-teffeɣ s Turagt MIT, ayen yebɣan ad d-yini belli yal yiwen izmer ad ten-iseqdec war tilist.", "noteItems": "Yal aferdis n temkarḍit isefk ad isɛu isem-is i yiman-is akken ad yili wamek ara yettusizdeg. Iferdisen-agi n temkarḍit ad ddun:", "atleastOneLibItem": "Ma ulac uɣilif fern ma drus yiwen n uferdis n temkarḍit akken ad tebduḍ", "republishWarning": "Tamawt: kra n yiferdisen yettwafernen ttwacerḍen ffeɣen-d/ttwaznen. Isefk ad talseḍ tuzzna n yiferdisen anagar mi ara tleqqemeḍ tamkarḍit neɣ tuzzna yellan." }, "publishSuccessDialog": { "title": "Tamkarḍit tettwazen", - "content": "Tanemmirt-ik•im {{authorName}}. Tamkarḍit-inek•inem tettwazen i weselken. Tzemreḍ ad tḍefreḍ aẓayer", - "link": "dagi" + "content": "Tanemmirt-ik•im {{authorName}}. Tamkarḍit-inek•inem tettwazen i weselken. Tzemreḍ ad tḍefreḍ aẓayerdagi" }, "confirmDialog": { "resetLibrary": "Ales awennez n temkarḍit", diff --git a/src/locales/kk-KZ.json b/src/locales/kk-KZ.json index 3096cb030..3718c7767 100644 --- a/src/locales/kk-KZ.json +++ b/src/locales/kk-KZ.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "" }, "errorSplash": { - "headingMain_pre": "", - "headingMain_button": "", + "headingMain": "", "clearCanvasMessage": "", - "clearCanvasMessage_button": "", "clearCanvasCaveat": "", - "trackedToSentry_pre": "", - "trackedToSentry_post": "", - "openIssueMessage_pre": "", - "openIssueMessage_button": "", - "openIssueMessage_post": "", + "trackedToSentry": "", + "openIssueMessage": "", "sceneContent": "" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "", "website": "" }, - "noteDescription": { - "pre": "", - "link": "", - "post": "" - }, - "noteGuidelines": { - "pre": "", - "link": "", - "post": "" - }, - "noteLicense": { - "pre": "", - "link": "", - "post": "" - }, + "noteDescription": "", + "noteGuidelines": "", + "noteLicense": "", "noteItems": "", "atleastOneLibItem": "", "republishWarning": "" }, "publishSuccessDialog": { "title": "", - "content": "", - "link": "" + "content": "" }, "confirmDialog": { "resetLibrary": "", diff --git a/src/locales/ko-KR.json b/src/locales/ko-KR.json index 5ba991600..20bdd4306 100644 --- a/src/locales/ko-KR.json +++ b/src/locales/ko-KR.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "팁: 멀리 있는 요소들을 좀 더 가까이로 붙여 보세요." }, "errorSplash": { - "headingMain_pre": "오류가 발생했습니다. ", - "headingMain_button": "페이지 새로고침", - "clearCanvasMessage": "새로고침으로 해결되지 않을 경우, ", - "clearCanvasMessage_button": "캔버스 비우기", + "headingMain": "오류가 발생했습니다. ", + "clearCanvasMessage": "새로고침으로 해결되지 않을 경우, ", "clearCanvasCaveat": " 작업 내용을 잃게 됩니다 ", - "trackedToSentry_pre": "오류 ", - "trackedToSentry_post": " 가 시스템에서 발견되었습니다.", - "openIssueMessage_pre": "저희는 화면 정보를 오류에 포함하지 않도록 매우 주의하고 있습니다. 혹시 화면에 민감한 내용이 없다면 이곳에 업로드를 고려해주세요.", - "openIssueMessage_button": "버그 트래커", - "openIssueMessage_post": " 아래 정보를 GitHub 이슈에 복사 및 붙여넣기해 주세요.", + "trackedToSentry": "오류 {{eventId}} 가 시스템에서 발견되었습니다.", + "openIssueMessage": "저희는 화면 정보를 오류에 포함하지 않도록 매우 주의하고 있습니다. 혹시 화면에 민감한 내용이 없다면 이곳에 업로드를 고려해주세요. 아래 정보를 GitHub 이슈에 복사 및 붙여넣기해 주세요.", "sceneContent": "화면 내용:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "필수사항", "website": "유효한 URL을 입력하세요" }, - "noteDescription": { - "pre": "당신의 라이브러리를 제출하여 ", - "link": "공개 라이브러리 저장소", - "post": "에서 다른 사람들의 그림에 사용할 수 있도록 하세요." - }, - "noteGuidelines": { - "pre": "라이브러리는 먼저 수동으로 승인되어야 합니다. 제출하기 전에 ", - "link": "가이드라인", - "post": "을 먼저 읽어보세요. 의견을 공유하거나 변경사항을 만들기 위해선 GitHub 계정이 필요하지만, 반드시 필요하진 않습니다." - }, - "noteLicense": { - "pre": "제출함으로써, 당신은 라이브러리가 ", - "link": "MIT 라이선스 ", - "post": "하에 배포됨을, 즉 아무나 제약 없이 사용할 수 있음에 동의합니다." - }, + "noteDescription": "당신의 라이브러리를 제출하여 공개 라이브러리 저장소에서 다른 사람들의 그림에 사용할 수 있도록 하세요.", + "noteGuidelines": "라이브러리는 먼저 수동으로 승인되어야 합니다. 제출하기 전에 가이드라인을 먼저 읽어보세요. 의견을 공유하거나 변경사항을 만들기 위해선 GitHub 계정이 필요하지만, 반드시 필요하진 않습니다.", + "noteLicense": "제출함으로써, 당신은 라이브러리가 MIT 라이선스 하에 배포됨을, 즉 아무나 제약 없이 사용할 수 있음에 동의합니다.", "noteItems": "각각의 라이브러리는 분류할 수 있도록 고유한 이름을 가져야 합니다. 다음의 라이브러리 항목이 포함됩니다:", "atleastOneLibItem": "최소한 하나의 라이브러리를 선택해주세요", "republishWarning": "참고: 선택된 항목의 일부는 이미 제출/게시되었습니다. 기존의 라이브러리나 제출물을 업데이트하는 경우에만 제출하세요." }, "publishSuccessDialog": { "title": "라이브러리 제출됨", - "content": "{{authorName}}님 감사합니다. 당신의 라이브러리가 심사를 위해 제출되었습니다. 진행 상황을", - "link": "여기에서 확인하실 수 있습니다." + "content": "{{authorName}}님 감사합니다. 당신의 라이브러리가 심사를 위해 제출되었습니다. 진행 상황을여기에서 확인하실 수 있습니다." }, "confirmDialog": { "resetLibrary": "라이브러리 리셋", diff --git a/src/locales/ku-TR.json b/src/locales/ku-TR.json index 248c16be4..1e2afe444 100644 --- a/src/locales/ku-TR.json +++ b/src/locales/ku-TR.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "زانیاری: هەوڵ بدە دوورترین توخمەکان کەمێک لە یەکتر نزیک بکەوە." }, "errorSplash": { - "headingMain_pre": "تووشی هەڵەیەک بوو. هەوڵ بدە ", - "headingMain_button": "دووبارە بارکردنی لاپەڕەکە.", - "clearCanvasMessage": "ئەگەر دووبارە بارکردنەوە کار ناکات، هەوڵبدە ", - "clearCanvasMessage_button": "خاوێنکردنەوەی تابلۆکە.", + "headingMain": "تووشی هەڵەیەک بوو. هەوڵ بدە ", + "clearCanvasMessage": "ئەگەر دووبارە بارکردنەوە کار ناکات، هەوڵبدە ", "clearCanvasCaveat": " ئەمە دەبێتە هۆی لەدەستدانی ئەوەی کە کردوتە ", - "trackedToSentry_pre": "هەڵەکە لەگەڵ ناسێنەری ", - "trackedToSentry_post": " لەسەر سیستەمەکەمان بەدواداچوونی بۆ کرا.", - "openIssueMessage_pre": "ئێمە زۆر وریا بووین کە زانیارییەکانی دیمەنەکەت لەسەر هەڵەکە نەخەینەڕوو. ئەگەر دیمەنەکەت تایبەت نییە، تکایە بیر لە بەدواداچوون بکەنەوە بۆ ئێمە ", - "openIssueMessage_button": "شوێنپێهەڵگری هەڵە.", - "openIssueMessage_post": " تکایە ئەم زانیارییانەی خوارەوە کۆپی بکە و لە بەشی کێشەکانی Github دایبنێ.", + "trackedToSentry": "هەڵەکە لەگەڵ ناسێنەری {{eventId}} لەسەر سیستەمەکەمان بەدواداچوونی بۆ کرا.", + "openIssueMessage": "ئێمە زۆر وریا بووین کە زانیارییەکانی دیمەنەکەت لەسەر هەڵەکە نەخەینەڕوو. ئەگەر دیمەنەکەت تایبەت نییە، تکایە بیر لە بەدواداچوون بکەنەوە بۆ ئێمە تکایە ئەم زانیارییانەی خوارەوە کۆپی بکە و لە بەشی کێشەکانی Github دایبنێ.", "sceneContent": "پێکهاتەی ناو دیمەنەکە:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "داواکراوە", "website": "URLێکی دروست تێبنووسە" }, - "noteDescription": { - "pre": "کتێبخانەکەت بنێرە بۆ ئەوەی بخرێتە ناو ", - "link": "کۆگای کتێبخانەی گشتی", - "post": "بۆ ئەوەی کەسانی تر لە وێنەکێشانەکانیاندا بەکاری بهێنن." - }, - "noteGuidelines": { - "pre": "کتێبخانەکە پێویستە سەرەتا بە دەست پەسەند بکرێت. تکایە بفەرمو بە خوێندنەوەی ", - "link": "ڕێنماییەکان", - "post": " پێش پێشکەشکردن. پێویستت بە ئەژمێری GitHub دەبێت بۆ پەیوەندیکردن و گۆڕانکاری ئەگەر داوای لێکرا، بەڵام بە توندی پێویست نییە." - }, - "noteLicense": { - "pre": "بە پێشکەشکردن، تۆ ڕەزامەندیت لەسەر بڵاوکردنەوەی کتێبخانەکە بەپێی ", - "link": "مۆڵەتی MIT، ", - "post": "کە بە کورتی مانای ئەوەیە کە هەرکەسێک دەتوانێت بە بێ سنوور بەکاری بهێنێت" - }, + "noteDescription": "کتێبخانەکەت بنێرە بۆ ئەوەی بخرێتە ناو کۆگای کتێبخانەی گشتیبۆ ئەوەی کەسانی تر لە وێنەکێشانەکانیاندا بەکاری بهێنن.", + "noteGuidelines": "کتێبخانەکە پێویستە سەرەتا بە دەست پەسەند بکرێت. تکایە بفەرمو بە خوێندنەوەی ڕێنماییەکان پێش پێشکەشکردن. پێویستت بە ئەژمێری GitHub دەبێت بۆ پەیوەندیکردن و گۆڕانکاری ئەگەر داوای لێکرا، بەڵام بە توندی پێویست نییە.", + "noteLicense": "بە پێشکەشکردن، تۆ ڕەزامەندیت لەسەر بڵاوکردنەوەی کتێبخانەکە بەپێی مۆڵەتی MIT، کە بە کورتی مانای ئەوەیە کە هەرکەسێک دەتوانێت بە بێ سنوور بەکاری بهێنێت", "noteItems": "هەر شتێکی کتێبخانە دەبێت ناوی تایبەتی خۆی هەبێت بۆ ئەوەی بتوانرێت فلتەر بکرێت. ئەم بابەتانەی کتێبخانانەی خوارەوە لەخۆدەگرێت:", "atleastOneLibItem": "تکایە بەلایەنی کەمەوە یەک بڕگەی کتێبخانە دیاریبکە بۆ دەستپێکردن", "republishWarning": "تێبینی: هەندێک لە ئایتمە دیاریکراوەکان نیشانکراون وەک ئەوەی پێشتر بڵاوکراونەتەوە/نێردراون. تەنها پێویستە شتەکان دووبارە پێشکەش بکەیتەوە لە کاتی نوێکردنەوەی کتێبخانەیەکی هەبوو یان پێشکەشکردن." }, "publishSuccessDialog": { "title": "کتێبخانە پێشکەش کرا", - "content": "سوپاس {{authorName}}. کتێبخانەکەت پێشکەش کراوە بۆ پێداچوونەوە. دەتوانیت بەدواداچوون بۆ دۆخەکە بکەیت", - "link": "لێرە" + "content": "سوپاس {{authorName}}. کتێبخانەکەت پێشکەش کراوە بۆ پێداچوونەوە. دەتوانیت بەدواداچوون بۆ دۆخەکە بکەیتلێرە" }, "confirmDialog": { "resetLibrary": "ڕێکخستنەوەی کتێبخانە", diff --git a/src/locales/lt-LT.json b/src/locales/lt-LT.json index 237fec359..20ce45a5e 100644 --- a/src/locales/lt-LT.json +++ b/src/locales/lt-LT.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "" }, "errorSplash": { - "headingMain_pre": "", - "headingMain_button": "", + "headingMain": "", "clearCanvasMessage": "", - "clearCanvasMessage_button": "", "clearCanvasCaveat": "", - "trackedToSentry_pre": "", - "trackedToSentry_post": "", - "openIssueMessage_pre": "", - "openIssueMessage_button": "", - "openIssueMessage_post": "", + "trackedToSentry": "", + "openIssueMessage": "", "sceneContent": "" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Privalomas", "website": "Įveskite teisingą nuorodą (URL)" }, - "noteDescription": { - "pre": "Pateik savo biblioteką, jog ji galėtų būti įtraukta į ", - "link": "", - "post": "jog kiti žmonės galėtų tai naudoti savo piešiniuose." - }, - "noteGuidelines": { - "pre": "Visų pirma, biblioteka turi būti rankiniu būdu patvirtinta. Prašome paskaityti ", - "link": "gairės", - "post": "" - }, - "noteLicense": { - "pre": "", - "link": "MIT licencija, ", - "post": "" - }, + "noteDescription": "Pateik savo biblioteką, jog ji galėtų būti įtraukta į jog kiti žmonės galėtų tai naudoti savo piešiniuose.", + "noteGuidelines": "Visų pirma, biblioteka turi būti rankiniu būdu patvirtinta. Prašome paskaityti gairės", + "noteLicense": "MIT licencija, ", "noteItems": "", "atleastOneLibItem": "", "republishWarning": "" }, "publishSuccessDialog": { "title": "Biblioteka pateikta", - "content": "Ačiū {{authorName}}. Tavo biblioteka buvo pateikta peržiūrai. Gali sekti būseną", - "link": "čia" + "content": "Ačiū {{authorName}}. Tavo biblioteka buvo pateikta peržiūrai. Gali sekti būsenąčia" }, "confirmDialog": { "resetLibrary": "Atstatyti biblioteką", diff --git a/src/locales/lv-LV.json b/src/locales/lv-LV.json index aaeee1795..c8c90d426 100644 --- a/src/locales/lv-LV.json +++ b/src/locales/lv-LV.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Ieteikums: mēģiniet satuvināt pašus tālākos elementus." }, "errorSplash": { - "headingMain_pre": "Notikusi kļūda. Mēģiniet ", - "headingMain_button": "pārlādēt lapu.", - "clearCanvasMessage": "Ja pārlādēšana nestrādā, mēģiniet ", - "clearCanvasMessage_button": "notīrot tāfeli.", + "headingMain": "Notikusi kļūda. Mēģiniet ", + "clearCanvasMessage": "Ja pārlādēšana nestrādā, mēģiniet ", "clearCanvasCaveat": " Tas novedīs pie darba zaudēšanas ", - "trackedToSentry_pre": "Kļūda ar kodu ", - "trackedToSentry_post": " tika noteikta mūsu sistēmā.", - "openIssueMessage_pre": "Mēs uzmanījāmies, lai neiekļautu jūsu ainas informāciju šajā kļūdā. Ja jūsu aina nav privāta, lūdzu ziņojiet par šo kļūdu mūsu ", - "openIssueMessage_button": "kļūdu uzskaitē.", - "openIssueMessage_post": " Lūdzu, miniet sekojošo informāciju to kopējot un ielīmējot jūsu ziņojumā platformā GitHub.", + "trackedToSentry": "Kļūda ar kodu {{eventId}} tika noteikta mūsu sistēmā.", + "openIssueMessage": "Mēs uzmanījāmies, lai neiekļautu jūsu ainas informāciju šajā kļūdā. Ja jūsu aina nav privāta, lūdzu ziņojiet par šo kļūdu mūsu Lūdzu, miniet sekojošo informāciju to kopējot un ielīmējot jūsu ziņojumā platformā GitHub.", "sceneContent": "Ainas saturs:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Obligāts", "website": "Ievadiet derīgu URL" }, - "noteDescription": { - "pre": "Iesniegt savu bibliotēku iekļaušanai ", - "link": "publiskajā bibliotēku datubāzē", - "post": ", lai citi to varētu izmantot savos zīmējumos." - }, - "noteGuidelines": { - "pre": "Šai bibliotēkai vispirms jātiek manuāli apstiprinātai. Lūdzu, izlasiet ", - "link": "norādījumus", - "post": " pirms iesniegšanas. Jums vajadzēs GitHub kontu, lai sazinātos un veiktu izmaiņas, ja tādas būs pieprasītas, bet tas nav absolūti nepieciešams." - }, - "noteLicense": { - "pre": "Iesniedzot bibliotēku, jūs piekrītat tās publicēšanai saskaņā ar ", - "link": "MIT Licenci, ", - "post": "kas īsumā nozīmē, ka jebkurš to varēs izmantot bez ierobežojumiem." - }, + "noteDescription": "Iesniegt savu bibliotēku iekļaušanai publiskajā bibliotēku datubāzē, lai citi to varētu izmantot savos zīmējumos.", + "noteGuidelines": "Šai bibliotēkai vispirms jātiek manuāli apstiprinātai. Lūdzu, izlasiet norādījumus pirms iesniegšanas. Jums vajadzēs GitHub kontu, lai sazinātos un veiktu izmaiņas, ja tādas būs pieprasītas, bet tas nav absolūti nepieciešams.", + "noteLicense": "Iesniedzot bibliotēku, jūs piekrītat tās publicēšanai saskaņā ar MIT Licenci, kas īsumā nozīmē, ka jebkurš to varēs izmantot bez ierobežojumiem.", "noteItems": "Katram bibliotēkas vienumam jābūt savam nosaukumam, lai to varētu atrast filtrējot. Tiks iekļauti sekojošie bibliotēkas vienumi:", "atleastOneLibItem": "Lūdzu, atlasiet vismaz vienu bibliotēkas vienumu, lai sāktu darbu", "republishWarning": "Ievēro: daži no atzīmētajiem objektiem jau atzīmēti kā publicēti vai iesniegti publicēšanai. Tos vajadzētu atkārtoti iesniegt tikai tad, ja vēlies labot esošo bibliotēku." }, "publishSuccessDialog": { "title": "Bibliotēka iesniegta", - "content": "Paldies, {{authorName}}! Jūsu bibliotēka iesniegta izskatīšanai. Jūs varat izsekot iesnieguma statusam", - "link": "šeit" + "content": "Paldies, {{authorName}}! Jūsu bibliotēka iesniegta izskatīšanai. Jūs varat izsekot iesnieguma statusamšeit" }, "confirmDialog": { "resetLibrary": "Atiestatīt bibliotēku", diff --git a/src/locales/mr-IN.json b/src/locales/mr-IN.json index 4bb8f9306..85e346d08 100644 --- a/src/locales/mr-IN.json +++ b/src/locales/mr-IN.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "टीप: दूर चा तत्व थोडं जवळ आणण्याचा प्रयत्न करावा." }, "errorSplash": { - "headingMain_pre": "त्रुटि आली. परत प्रयत्न करा ", - "headingMain_button": "ह्या पानाला पुनः लोड करा.", - "clearCanvasMessage": "रीलोडिंग होत नसल्यास, परत प्रयत्न करा ", - "clearCanvasMessage_button": "पटल स्वच्छ करित आहे.", + "headingMain": "त्रुटि आली. परत प्रयत्न करा ", + "clearCanvasMessage": "रीलोडिंग होत नसल्यास, परत प्रयत्न करा ", "clearCanvasCaveat": " त्यामुळे केलेल्या कामाचे नुकसान होईल ", - "trackedToSentry_pre": "त्रुटि क्रमांक के साथ त्रुटि ", - "trackedToSentry_post": " आमच्या प्रणाली नी निरीक्षण केले होते.", - "openIssueMessage_pre": "त्रुटीत तुमची दृश्य माहिती समाविष्ट न करण्यासाठी आम्ही खूप सावध होतो. तुमचा सीन खाजगी नसल्यास, कृपया आम्हाला पुढ च्या कारवाई साठी सम्पर्क साधा ", - "openIssueMessage_button": "त्रुटि व्यवस्थापन.", - "openIssueMessage_post": " कृपया गिटहब समस्येमध्ये कॉपी आणि पेस्ट करून खालिल माहिती समाविष्ट करा.", + "trackedToSentry": "त्रुटि क्रमांक के साथ त्रुटि {{eventId}} आमच्या प्रणाली नी निरीक्षण केले होते.", + "openIssueMessage": "त्रुटीत तुमची दृश्य माहिती समाविष्ट न करण्यासाठी आम्ही खूप सावध होतो. तुमचा सीन खाजगी नसल्यास, कृपया आम्हाला पुढ च्या कारवाई साठी सम्पर्क साधा कृपया गिटहब समस्येमध्ये कॉपी आणि पेस्ट करून खालिल माहिती समाविष्ट करा.", "sceneContent": "दृश्य विषय:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "आवश्यक आहे", "website": "वैध यू-आर-एल द्या" }, - "noteDescription": { - "pre": "समाविष्ट करण्या साठी तुमचा संग्रह ह्याचात जमा करा ", - "link": "सार्वजनिक संग्रहाचे कोठार", - "post": "इतर लोकांना त्यांच्या रेखाचित्रांमधे वापरण्यासाठी." - }, - "noteGuidelines": { - "pre": "संग्रहाला आधी स्वहस्ते स्वीकृती मिळणे आवश्यक आहे. कृपया हे वाचा ", - "link": "मार्गदर्शक तत्त्वे", - "post": " जमा करण्या पूर्वी, तुमच्या जवळ एक गिटहब खाते असणे आवश्यक आहे जे संवादा साठी आणिक बदल करण्या साठी लागेल, तरी हे सर्व अगदी आवश्यक नाही आहे." - }, - "noteLicense": { - "pre": "जमा करताना तुम्हीं सहमति दाखवतात आहे की संग्रह ह्याचा खाली प्रकाशित होईल ", - "link": "एम-आइ-टी परवाना, ", - "post": "ज्याचा थोडक्यात अर्थ कोणीही निर्बंधांशिवाय वापरू शकतो." - }, + "noteDescription": "समाविष्ट करण्या साठी तुमचा संग्रह ह्याचात जमा करा सार्वजनिक संग्रहाचे कोठारइतर लोकांना त्यांच्या रेखाचित्रांमधे वापरण्यासाठी.", + "noteGuidelines": "संग्रहाला आधी स्वहस्ते स्वीकृती मिळणे आवश्यक आहे. कृपया हे वाचा मार्गदर्शक तत्त्वे जमा करण्या पूर्वी, तुमच्या जवळ एक गिटहब खाते असणे आवश्यक आहे जे संवादा साठी आणिक बदल करण्या साठी लागेल, तरी हे सर्व अगदी आवश्यक नाही आहे.", + "noteLicense": "जमा करताना तुम्हीं सहमति दाखवतात आहे की संग्रह ह्याचा खाली प्रकाशित होईल एम-आइ-टी परवाना, ज्याचा थोडक्यात अर्थ कोणीही निर्बंधांशिवाय वापरू शकतो.", "noteItems": "प्रतैक संग्रहाचे नाव, नीट शोधनासाठी, असणे आवश्यक आहे. खाली दिलेल्या वस्तु समाविष्ट केल्या जातील:", "atleastOneLibItem": "सुरु करण्यासाठी, कृपया करून, कमित कमी एक वस्तु तरी निवडा", "republishWarning": "टीप: काही निवडक आयटम आधीच प्रकाशित/प्रस्तुत केलेले आहेत. विद्यमान लायब्ररी किंवा प्रस्तुतित आयटम अद्यावित करताना तुम्ही फक्त तो पुन्हा प्रस्तुत करा." }, "publishSuccessDialog": { "title": "संग्रह जमा केला", - "content": "धन्यवाद {{authorName}}. आपला संग्रह पुनरावलोकना साठी जमा झाला आहे. तुम्हीं स्थिति सारखी तपासू सकता", - "link": "इकडे" + "content": "धन्यवाद {{authorName}}. आपला संग्रह पुनरावलोकना साठी जमा झाला आहे. तुम्हीं स्थिति सारखी तपासू सकताइकडे" }, "confirmDialog": { "resetLibrary": "संग्रह पुनर्स्थित करा", diff --git a/src/locales/my-MM.json b/src/locales/my-MM.json index d0b64a389..50f8bb560 100644 --- a/src/locales/my-MM.json +++ b/src/locales/my-MM.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "မှတ်ချက်။ ။ဝေးကွာနေသော ပုံများ၊ စာများအား ပိုမိုနီးကပ်အောင်ရွှေ့ကြည့်ပါ။" }, "errorSplash": { - "headingMain_pre": "ချို့ယွင်းမှုဖြစ်ပေါ်ခဲ့သဖြင့် ထပ်မံကြိုးစားကြည့်ရန် ", - "headingMain_button": "စာမျက်နှာအား အသစ်ပြန်လည်ရယူပါ။", - "clearCanvasMessage": "အသစ်ပြန်လည်မရယူနိုင်ပါက ထပ်မံကြိုးစားကြည့်ရန်", - "clearCanvasMessage_button": "ကားချပ်အား ရှင်းလင်းပါ။", + "headingMain": "ချို့ယွင်းမှုဖြစ်ပေါ်ခဲ့သဖြင့် ထပ်မံကြိုးစားကြည့်ရန် ", + "clearCanvasMessage": "အသစ်ပြန်လည်မရယူနိုင်ပါက ထပ်မံကြိုးစားကြည့်ရန်", "clearCanvasCaveat": " ရေးဆွဲထားသည်များ ဆုံးရှုံးနိုင်သည် ", - "trackedToSentry_pre": "ချို့ယွင်းမှုသတ်မှတ်ချက် ", - "trackedToSentry_post": " အားစနစ်အတွင်းခြေရာကောက်ပြီးပါပြီ။", - "openIssueMessage_pre": "ချို့ယွင်းမှုမှတ်တမ်းတွင် အရေးကြီးအချက်အလက်များပါဝင်မှုမရှိစေရန်အထူးသတိပြုပါသည်။ မပါဝင်ပါက ဆက်လက်ဆောင်ရွက်ရန် ", - "openIssueMessage_button": "ချို့ယွင်းမှုအားခြေရာကောက်ပါ။", - "openIssueMessage_post": " အောက်ပါအချက်အလက်များအား Github တွင် Issue အနေဖြင့်ဖြည့်သွင်းဖော်ပြပေးပါ။", + "trackedToSentry": "ချို့ယွင်းမှုသတ်မှတ်ချက် {{eventId}} အားစနစ်အတွင်းခြေရာကောက်ပြီးပါပြီ။", + "openIssueMessage": "ချို့ယွင်းမှုမှတ်တမ်းတွင် အရေးကြီးအချက်အလက်များပါဝင်မှုမရှိစေရန်အထူးသတိပြုပါသည်။ မပါဝင်ပါက ဆက်လက်ဆောင်ရွက်ရန် အောက်ပါအချက်အလက်များအား Github တွင် Issue အနေဖြင့်ဖြည့်သွင်းဖော်ပြပေးပါ။", "sceneContent": "မြင်ကွင်းပါအချက်အလက်။ ။" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "", "website": "" }, - "noteDescription": { - "pre": "", - "link": "", - "post": "" - }, - "noteGuidelines": { - "pre": "", - "link": "", - "post": "" - }, - "noteLicense": { - "pre": "", - "link": "", - "post": "" - }, + "noteDescription": "", + "noteGuidelines": "", + "noteLicense": "", "noteItems": "", "atleastOneLibItem": "", "republishWarning": "" }, "publishSuccessDialog": { "title": "", - "content": "", - "link": "" + "content": "" }, "confirmDialog": { "resetLibrary": "", diff --git a/src/locales/nb-NO.json b/src/locales/nb-NO.json index 36c4fca79..66b197317 100644 --- a/src/locales/nb-NO.json +++ b/src/locales/nb-NO.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Tips: Prøv å flytte de ytterste elementene litt tettere sammen." }, "errorSplash": { - "headingMain_pre": "En feil oppsto. Prøv ", - "headingMain_button": "å laste siden på nytt.", - "clearCanvasMessage": "Om ny sidelasting ikke fungerer, prøv ", - "clearCanvasMessage_button": "å tømme lerretet.", + "headingMain": "En feil oppsto. Prøv ", + "clearCanvasMessage": "Om ny sidelasting ikke fungerer, prøv ", "clearCanvasCaveat": " Dette vil føre til tap av arbeid ", - "trackedToSentry_pre": "Feilen med identifikator ", - "trackedToSentry_post": " ble logget i vårt system.", - "openIssueMessage_pre": "Vi er veldig nøye med å ikke inkludere dine scene-opplysninger i feilen. Hvis din scene ikke er privat, vurder å følge opp i vårt ", - "openIssueMessage_button": "feilrapporteringssystem.", - "openIssueMessage_post": " Ta med opplysningene nedenfor ved å kopiere og lime inn i GitHub-saken.", + "trackedToSentry": "Feilen med identifikator {{eventId}} ble logget i vårt system.", + "openIssueMessage": "Vi er veldig nøye med å ikke inkludere dine scene-opplysninger i feilen. Hvis din scene ikke er privat, vurder å følge opp i vårt Ta med opplysningene nedenfor ved å kopiere og lime inn i GitHub-saken.", "sceneContent": "Scene-innhold:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Påkrevd", "website": "Angi en gyldig nettadresse" }, - "noteDescription": { - "pre": "Send inn biblioteket ditt som skal inkluderes i ", - "link": "kildekode for offentlig bibliotek", - "post": "for andre å bruke dem i tegninger." - }, - "noteGuidelines": { - "pre": "Biblioteket må godkjennes manuelt først. Les ", - "link": "retningslinjene", - "post": " før innsending. Du vil trenge en GitHub-konto for å kommunisere og gjøre endringer hvis ønsket, men det er ikke påkrevd." - }, - "noteLicense": { - "pre": "Ved å sende inn godtar du at biblioteket blir publisert under ", - "link": "MIT-lisens, ", - "post": "som kortfattet betyr at andre kan bruke dem uten begrensninger." - }, + "noteDescription": "Send inn biblioteket ditt som skal inkluderes i kildekode for offentlig bibliotekfor andre å bruke dem i tegninger.", + "noteGuidelines": "Biblioteket må godkjennes manuelt først. Les retningslinjene før innsending. Du vil trenge en GitHub-konto for å kommunisere og gjøre endringer hvis ønsket, men det er ikke påkrevd.", + "noteLicense": "Ved å sende inn godtar du at biblioteket blir publisert under MIT-lisens, som kortfattet betyr at andre kan bruke dem uten begrensninger.", "noteItems": "Hvert bibliotek må ha sitt eget navn, så det er filtrerbart. Følgende bibliotekselementer vil bli inkludert:", "atleastOneLibItem": "Vennligst velg minst ett bibliotek for å komme i gang", "republishWarning": "Merk: noen av de valgte elementene er merket som allerede publisert/sendt. Du bør kun sende inn elementer på nytt når du oppdaterer et eksisterende bibliotek eller innlevering." }, "publishSuccessDialog": { "title": "Bibliotek innsendt", - "content": "Takk {{authorName}}. Ditt bibliotek har blitt sendt inn for gjennomgang. Du kan spore statusen", - "link": "her" + "content": "Takk {{authorName}}. Ditt bibliotek har blitt sendt inn for gjennomgang. Du kan spore statusenher" }, "confirmDialog": { "resetLibrary": "Nullstill bibliotek", diff --git a/src/locales/nl-NL.json b/src/locales/nl-NL.json index 417882da1..e94b89f0e 100644 --- a/src/locales/nl-NL.json +++ b/src/locales/nl-NL.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Tip: beweeg de verste elementen iets dichter bij elkaar." }, "errorSplash": { - "headingMain_pre": "Fout opgetreden. Probeer ", - "headingMain_button": "de pagina opnieuw laden.", - "clearCanvasMessage": "Als herladen niet werkt, probeer ", - "clearCanvasMessage_button": "het canvas te wissen.", + "headingMain": "Fout opgetreden. Probeer ", + "clearCanvasMessage": "Als herladen niet werkt, probeer ", "clearCanvasCaveat": " Dit zal leiden tot verlies van je werk ", - "trackedToSentry_pre": "De fout met ID ", - "trackedToSentry_post": " was gevolgd op ons systeem.", - "openIssueMessage_pre": "We waren voorzichtig om je scène-informatie niet in de fout toe te voegen. Als je scène niet privé is, overweeg dan alstublieft het opvolgen op onze ", - "openIssueMessage_button": "bugtracker.", - "openIssueMessage_post": " Kopieer de informatie hieronder naar de GitHub issue.", + "trackedToSentry": "De fout met ID {{eventId}} was gevolgd op ons systeem.", + "openIssueMessage": "We waren voorzichtig om je scène-informatie niet in de fout toe te voegen. Als je scène niet privé is, overweeg dan alstublieft het opvolgen op onze Kopieer de informatie hieronder naar de GitHub issue.", "sceneContent": "Scène-inhoud:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Vereist", "website": "Vul een geldige URL in" }, - "noteDescription": { - "pre": "", - "link": "openbare repository", - "post": "" - }, - "noteGuidelines": { - "pre": "", - "link": "richtlijnen", - "post": "" - }, - "noteLicense": { - "pre": "", - "link": "MIT-licentie, ", - "post": "" - }, + "noteDescription": "openbare repository", + "noteGuidelines": "richtlijnen", + "noteLicense": "MIT-licentie, ", "noteItems": "", "atleastOneLibItem": "", "republishWarning": "" }, "publishSuccessDialog": { "title": "Bibliotheek ingediend", - "content": "", - "link": "Hier" + "content": "Hier" }, "confirmDialog": { "resetLibrary": "Reset bibliotheek", diff --git a/src/locales/nn-NO.json b/src/locales/nn-NO.json index fcdefab9d..755101171 100644 --- a/src/locales/nn-NO.json +++ b/src/locales/nn-NO.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Tips: prøv å flytte elementa som er lengst frå kvarandre, litt nærare kvarandre." }, "errorSplash": { - "headingMain_pre": "Ein feil oppstod. Prøv ", - "headingMain_button": "å laste sida på nytt.", - "clearCanvasMessage": "Om ny sidelasting ikkje fungerer, prøv ", - "clearCanvasMessage_button": "å tømme lerretet.", + "headingMain": "Ein feil oppstod. Prøv ", + "clearCanvasMessage": "Om ny sidelasting ikkje fungerer, prøv ", "clearCanvasCaveat": " Dette vil føre til tap av arbeid ", - "trackedToSentry_pre": "Feilen med identifikator ", - "trackedToSentry_post": " vart logga i systemet vårt.", - "openIssueMessage_pre": "Vi er veldig nøye med å ikkje inkludere scene-opplysingane dine i feilmeldinga. Viss scena di ikkje er privat kan du vurdere å følge opp i ", - "openIssueMessage_button": "feilrapporteringssystemet vårt.", - "openIssueMessage_post": " Ta med opplysingane nedanfor ved å kopiere og lime inn i GitHub-saka.", + "trackedToSentry": "Feilen med identifikator {{eventId}} vart logga i systemet vårt.", + "openIssueMessage": "Vi er veldig nøye med å ikkje inkludere scene-opplysingane dine i feilmeldinga. Viss scena di ikkje er privat kan du vurdere å følge opp i Ta med opplysingane nedanfor ved å kopiere og lime inn i GitHub-saka.", "sceneContent": "Scene-innhald:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Kravt", "website": "Fyll inn ein gyldig URL" }, - "noteDescription": { - "pre": "Send inn biblioteket ditt til inkludering i ", - "link": "den offentlege bibliotek-kjeldekoda", - "post": "slik at andre kan bruke det i teikningane deira." - }, - "noteGuidelines": { - "pre": "Biblioteket må godkjennast manuelt fyrst. Ver vennleg å lese ", - "link": "retningslinjene", - "post": " før du sender inn. Du kjem til å trenge ein GitHub-konto for å kommunisere og gjere endringar dersom kravt, men det er ikkje strengt naudsynt." - }, - "noteLicense": { - "pre": "Ved å sende inn godkjenner du at biblioteket vert publisert under ", - "link": "MIT-lisensen, ", - "post": "som kort sagt betyr at kven som helst kan bruke det utan avgrensingar." - }, + "noteDescription": "Send inn biblioteket ditt til inkludering i den offentlege bibliotek-kjeldekodaslik at andre kan bruke det i teikningane deira.", + "noteGuidelines": "Biblioteket må godkjennast manuelt fyrst. Ver vennleg å lese retningslinjene før du sender inn. Du kjem til å trenge ein GitHub-konto for å kommunisere og gjere endringar dersom kravt, men det er ikkje strengt naudsynt.", + "noteLicense": "Ved å sende inn godkjenner du at biblioteket vert publisert under MIT-lisensen, som kort sagt betyr at kven som helst kan bruke det utan avgrensingar.", "noteItems": "Kvart bibliotekselement må ha eit eige namn, slik at det er mogleg å filtrere. Dei følgande bibliotekselementa blir inkludert:", "atleastOneLibItem": "Ver vennleg å markere minst eitt bibliotekselement for å starte", "republishWarning": "" }, "publishSuccessDialog": { "title": "Bibliotek innsendt", - "content": "Tusen takk {{authorName}}! Biblioteket ditt har blitt sendt inn til gjennomgang. Du kan halde styr på status", - "link": "her" + "content": "Tusen takk {{authorName}}! Biblioteket ditt har blitt sendt inn til gjennomgang. Du kan halde styr på statusher" }, "confirmDialog": { "resetLibrary": "Tilbakestill bibliotek", diff --git a/src/locales/oc-FR.json b/src/locales/oc-FR.json index 3bc67225e..a15c33b93 100644 --- a/src/locales/oc-FR.json +++ b/src/locales/oc-FR.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Astúcia : ensajatz de sarrar los elements mai alonhats." }, "errorSplash": { - "headingMain_pre": "Una error s’es producha. Ensajatz ", - "headingMain_button": "recargament de la pagina.", - "clearCanvasMessage": "Se recargar fonciona pas, ensajatz ", - "clearCanvasMessage_button": "d’escafar los canabasses.", + "headingMain": "Una error s’es producha. Ensajatz ", + "clearCanvasMessage": "Se recargar fonciona pas, ensajatz ", "clearCanvasCaveat": " Menarà a una pèrda del trabalh ", - "trackedToSentry_pre": "Error amb l’identificant ", - "trackedToSentry_post": " es estada enregistrada sus nòstre sistèma.", - "openIssueMessage_pre": "Èrem plan prudents per inclure pas d’informacions de la scèna vòstra sus l’error. Se vòstra scèna es pas privada, volgatz considerar de perseguir sus nòstre ", - "openIssueMessage_button": "traçadors d’avarias.", - "openIssueMessage_post": " Volgatz inclure las informacions çai-jos en las copiant e pegant a l’issue GitHub.", + "trackedToSentry": "Error amb l’identificant {{eventId}} es estada enregistrada sus nòstre sistèma.", + "openIssueMessage": "Èrem plan prudents per inclure pas d’informacions de la scèna vòstra sus l’error. Se vòstra scèna es pas privada, volgatz considerar de perseguir sus nòstre Volgatz inclure las informacions çai-jos en las copiant e pegant a l’issue GitHub.", "sceneContent": "Contengut de la scèna :" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Requerit", "website": "Picatz una URL valida" }, - "noteDescription": { - "pre": "Enviatz vòstra bibliotèca per èsser compresa al ", - "link": "repertòri public de bibliotèca", - "post": "per que los autres l’utilizen dins lor dessenhs." - }, - "noteGuidelines": { - "pre": "Qualqu’un deu aprovar la bibliotèca manualament per començar. Volgatz legir las ", - "link": "linhas directrises", - "post": " abans de sometre. Vos farà mestièr un compte GitHub per comunicar e realizar de modificacions se demandadas, mas es pas complètament obligatòri." - }, - "noteLicense": { - "pre": "En sometent, acceptatz que la bibliotèca siá publicada sota la ", - "link": "Licéncia MIT, ", - "post": "que significa en brèu que qual que siá pòt l’utilizar sens cap de restriccion." - }, + "noteDescription": "Enviatz vòstra bibliotèca per èsser compresa al repertòri public de bibliotècaper que los autres l’utilizen dins lor dessenhs.", + "noteGuidelines": "Qualqu’un deu aprovar la bibliotèca manualament per començar. Volgatz legir las linhas directrises abans de sometre. Vos farà mestièr un compte GitHub per comunicar e realizar de modificacions se demandadas, mas es pas complètament obligatòri.", + "noteLicense": "En sometent, acceptatz que la bibliotèca siá publicada sota la Licéncia MIT, que significa en brèu que qual que siá pòt l’utilizar sens cap de restriccion.", "noteItems": "Cada element de bibliotèca deu aver un nom pròpri per èsser filtrable. Los elements de bibliotèca seguentas seràn incluses :", "atleastOneLibItem": "Volgatz seleccionar almens un element de bibliotèca per començar", "republishWarning": "Nòta : d’unes elements seleccionats son marcats ja coma publicats/enviats. Deuriatz sonque tornar enviar los elements pendent l’actualizacion d’una bibliotèca existenta o un mandadís." }, "publishSuccessDialog": { "title": "Bibliotèca somesa", - "content": "Mercés {{authorName}}. Vòstre bibliotèca es estada somesa per repassa. Podètz seguir l’avançament", - "link": "aquí" + "content": "Mercés {{authorName}}. Vòstre bibliotèca es estada somesa per repassa. Podètz seguir l’avançamentaquí" }, "confirmDialog": { "resetLibrary": "Reïnicializar la bibliotèca", diff --git a/src/locales/pa-IN.json b/src/locales/pa-IN.json index b18cc6f97..9131afd6d 100644 --- a/src/locales/pa-IN.json +++ b/src/locales/pa-IN.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "ਨੁਸਖਾ: ਸਭ ਤੋਂ ਦੂਰ ਸਥਿੱਤ ਐਲੀਮੈਂਟਾਂ ਨੂੰ ਥੋੜ੍ਹਾ ਜਿਹਾ ਨੇੜੇ ਲਿਆ ਕੇ ਦੇਖੋ।" }, "errorSplash": { - "headingMain_pre": "ਗਲਤੀ ਹੋਈ। ਇਹ ਕਰਕੇ ਦੇਖੋ ", - "headingMain_button": "ਪੰਨਾ ਮੁੜ-ਲੋਡ ਕਰੋ।", - "clearCanvasMessage": "ਜੇ ਮੁੜ-ਲੋਡ ਕਰਨਾ ਕੰਮ ਨਾ ਕਰੇ, ਤਾਂ ਇਹ ਕਰਕੇ ਦੇਖੋ ", - "clearCanvasMessage_button": "ਕੈਨਵਸ ਸਾਫ ਕਰੋ।", + "headingMain": "ਗਲਤੀ ਹੋਈ। ਇਹ ਕਰਕੇ ਦੇਖੋ ", + "clearCanvasMessage": "ਜੇ ਮੁੜ-ਲੋਡ ਕਰਨਾ ਕੰਮ ਨਾ ਕਰੇ, ਤਾਂ ਇਹ ਕਰਕੇ ਦੇਖੋ ", "clearCanvasCaveat": " ਇਹ ਸਾਰਾ ਕੰਮ ਗਵਾ ਦੇਵੇਗਾ ", - "trackedToSentry_pre": "ਗਲਤੀ ਸੂਚਕ ", - "trackedToSentry_post": " ਸਾਡੇ ਸਿਸਟਮ 'ਤੇ ਟਰੈਕ ਕੀਤਾ ਗਿਆ ਸੀ।", - "openIssueMessage_pre": "ਅਸੀਂ ਬੜੇ ਸਾਵਧਾਨ ਸੀ ਕਿ ਗਲਤੀ ਵਿੱਚ ਤੁਹਾਡੇ ਦ੍ਰਿਸ਼ ਦੀ ਜਾਣਕਾਰੀ ਸ਼ਾਮਲ ਨਾ ਕਰੀਏ। ਜੇ ਤੁਹਾਡਾ ਦ੍ਰਿਸ਼ ਨਿੱਜੀ ਨਹੀਂ ਹੈ ਤਾਂ ਇਸ 'ਤੇ ਸਾਡੇ ਨਾਲ ਸੰਪਰਕ ਕਰੋ ਜੀ ", - "openIssueMessage_button": "ਬੱਗ ਟਰੈਕਰ।", - "openIssueMessage_post": "ਹੇਠਾਂ ਦਿੱਤੀ ਜਾਣਕਾਰੀ ਨੂੰ ਕਾਪੀ ਕਰਕੇ ਗਿੱਟਹੱਬ ਮੁੱਦੇ ਵਿੱਚ ਪੇਸਟ ਕਰਕੇ ਸ਼ਾਮਲ ਕਰੋ ਜੀ।", + "trackedToSentry": "ਗਲਤੀ ਸੂਚਕ {{eventId}} ਸਾਡੇ ਸਿਸਟਮ 'ਤੇ ਟਰੈਕ ਕੀਤਾ ਗਿਆ ਸੀ।", + "openIssueMessage": "ਅਸੀਂ ਬੜੇ ਸਾਵਧਾਨ ਸੀ ਕਿ ਗਲਤੀ ਵਿੱਚ ਤੁਹਾਡੇ ਦ੍ਰਿਸ਼ ਦੀ ਜਾਣਕਾਰੀ ਸ਼ਾਮਲ ਨਾ ਕਰੀਏ। ਜੇ ਤੁਹਾਡਾ ਦ੍ਰਿਸ਼ ਨਿੱਜੀ ਨਹੀਂ ਹੈ ਤਾਂ ਇਸ 'ਤੇ ਸਾਡੇ ਨਾਲ ਸੰਪਰਕ ਕਰੋ ਜੀ ਹੇਠਾਂ ਦਿੱਤੀ ਜਾਣਕਾਰੀ ਨੂੰ ਕਾਪੀ ਕਰਕੇ ਗਿੱਟਹੱਬ ਮੁੱਦੇ ਵਿੱਚ ਪੇਸਟ ਕਰਕੇ ਸ਼ਾਮਲ ਕਰੋ ਜੀ।", "sceneContent": "ਦ੍ਰਿਸ਼ ਦੀ ਸਮੱਗਰੀ:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "ਲੋੜੀਂਦਾ", "website": "ਜਾਇਜ਼ URL ਭਰੋ" }, - "noteDescription": { - "pre": "", - "link": "ਜਨਤਕ ਲਾਇਬ੍ਰੇਰੀ ਦੀ ਰਿਪਾਜ਼ੀਟਰੀ", - "post": "" - }, - "noteGuidelines": { - "pre": "", - "link": "ਦਿਸ਼ਾ ਨਿਰਦੇਸ਼", - "post": "" - }, - "noteLicense": { - "pre": "", - "link": "MIT ਲਾਇਸੈਂਸ, ", - "post": "" - }, + "noteDescription": "ਜਨਤਕ ਲਾਇਬ੍ਰੇਰੀ ਦੀ ਰਿਪਾਜ਼ੀਟਰੀ", + "noteGuidelines": "ਦਿਸ਼ਾ ਨਿਰਦੇਸ਼", + "noteLicense": "MIT ਲਾਇਸੈਂਸ, ", "noteItems": "", "atleastOneLibItem": "", "republishWarning": "" }, "publishSuccessDialog": { "title": "", - "content": "", - "link": "ਇੱਥੇ" + "content": "ਇੱਥੇ" }, "confirmDialog": { "resetLibrary": "ਲਾਇਬ੍ਰੇਰੀ ਰੀਸੈੱਟ ਕਰੋ", diff --git a/src/locales/pl-PL.json b/src/locales/pl-PL.json index b3feba402..047e5ec6b 100644 --- a/src/locales/pl-PL.json +++ b/src/locales/pl-PL.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Wskazówka: spróbuj nieco zbliżyć najdalej wysunięte elementy." }, "errorSplash": { - "headingMain_pre": "Wystąpił błąd. Spróbuj ", - "headingMain_button": "odświeżyć stronę.", - "clearCanvasMessage": "Jeśli odświeżenie strony nie zadziałało, spróbuj ", - "clearCanvasMessage_button": "usunąć wszystko z dokumentu.", + "headingMain": "Wystąpił błąd. Spróbuj ", + "clearCanvasMessage": "Jeśli odświeżenie strony nie zadziałało, spróbuj ", "clearCanvasCaveat": " Pamiętaj tylko, że spowoduje to utratę całej twojej pracy ", - "trackedToSentry_pre": "Błąd o identyfikatorze ", - "trackedToSentry_post": " został zaraportowany w naszym systemie.", - "openIssueMessage_pre": "Szanujemy twoją prywatność i raport nie zawierał żadnych danych dotyczących tego nad czym pracowałeś, natomiast jeżeli jesteś w stanie podzielić się tym nad czym pracowałeś, prosimy o dodatkowy raport poprzez ", - "openIssueMessage_button": "nasze narzędzie do raportowania błędów.", - "openIssueMessage_post": " Prosimy o dołączenie poniższej informacji poprzez skopiowanie jej i umieszczenie jej w zgłoszeniu na portalu GitHub.", + "trackedToSentry": "Błąd o identyfikatorze {{eventId}} został zaraportowany w naszym systemie.", + "openIssueMessage": "Szanujemy twoją prywatność i raport nie zawierał żadnych danych dotyczących tego nad czym pracowałeś, natomiast jeżeli jesteś w stanie podzielić się tym nad czym pracowałeś, prosimy o dodatkowy raport poprzez Prosimy o dołączenie poniższej informacji poprzez skopiowanie jej i umieszczenie jej w zgłoszeniu na portalu GitHub.", "sceneContent": "Zawartość dokumentu:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Wymagane", "website": "Wprowadź prawidłowy adres URL" }, - "noteDescription": { - "pre": "", - "link": "", - "post": "dla innych osób do wykorzystania w swoich rysunkach." - }, - "noteGuidelines": { - "pre": "Biblioteka musi być najpierw zatwierdzona ręcznie. Przeczytaj ", - "link": "wytyczne", - "post": "" - }, - "noteLicense": { - "pre": "Wysyłając zgadzasz się, że biblioteka zostanie opublikowana pod ", - "link": "Licencja MIT, ", - "post": "w skrócie, każdy może z nich korzystać bez ograniczeń." - }, + "noteDescription": "dla innych osób do wykorzystania w swoich rysunkach.", + "noteGuidelines": "Biblioteka musi być najpierw zatwierdzona ręcznie. Przeczytaj wytyczne", + "noteLicense": "Wysyłając zgadzasz się, że biblioteka zostanie opublikowana pod Licencja MIT, w skrócie, każdy może z nich korzystać bez ograniczeń.", "noteItems": "", "atleastOneLibItem": "Proszę wybrać co najmniej jeden element biblioteki, by rozpocząć", "republishWarning": "" }, "publishSuccessDialog": { "title": "Biblioteka została przesłana", - "content": "Dziękujemy {{authorName}}. Twoja biblioteka została przesłana do sprawdzenia. Możesz śledzić jej stan", - "link": "tutaj" + "content": "Dziękujemy {{authorName}}. Twoja biblioteka została przesłana do sprawdzenia. Możesz śledzić jej stantutaj" }, "confirmDialog": { "resetLibrary": "Zresetuj Bibliotekę", diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json index f544cce10..bf4f7d445 100644 --- a/src/locales/pt-BR.json +++ b/src/locales/pt-BR.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Dica: tente aproximar um pouco os elementos mais distantes." }, "errorSplash": { - "headingMain_pre": "Foi encontrado um erro. Tente ", - "headingMain_button": "recarregar a página.", - "clearCanvasMessage": "Se recarregar a página não funcionar, tente ", - "clearCanvasMessage_button": "limpando a tela.", + "headingMain": "Foi encontrado um erro. Tente ", + "clearCanvasMessage": "Se recarregar a página não funcionar, tente ", "clearCanvasCaveat": " Isso resultará em perda de trabalho ", - "trackedToSentry_pre": "O erro com o identificador ", - "trackedToSentry_post": " foi rastreado no nosso sistema.", - "openIssueMessage_pre": "Fomos muito cautelosos para não incluir suas informações de cena no erro. Se sua cena não for privada, por favor, considere seguir nosso ", - "openIssueMessage_button": "rastreador de bugs.", - "openIssueMessage_post": " Por favor, inclua informações abaixo, copiando e colando para a issue do GitHub.", + "trackedToSentry": "O erro com o identificador {{eventId}} foi rastreado no nosso sistema.", + "openIssueMessage": "Fomos muito cautelosos para não incluir suas informações de cena no erro. Se sua cena não for privada, por favor, considere seguir nosso Por favor, inclua informações abaixo, copiando e colando para a issue do GitHub.", "sceneContent": "Conteúdo da cena:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Obrigatório", "website": "Informe uma URL válida" }, - "noteDescription": { - "pre": "Envie sua biblioteca para ser incluída no ", - "link": "repositório de biblioteca pública", - "post": "para outras pessoas usarem em seus desenhos." - }, - "noteGuidelines": { - "pre": "A biblioteca precisa ser aprovada manualmente primeiro. Por favor leia o ", - "link": "orientações", - "post": " antes de enviar. Você precisará de uma conta do GitHub para se comunicar e fazer alterações quando solicitado, mas não é estritamente necessário." - }, - "noteLicense": { - "pre": "Ao enviar, você concorda que a biblioteca será publicada sob a ", - "link": "Licença MIT, ", - "post": "o que, em suma, significa que qualquer pessoa pode utilizá-los sem restrições." - }, + "noteDescription": "Envie sua biblioteca para ser incluída no repositório de biblioteca públicapara outras pessoas usarem em seus desenhos.", + "noteGuidelines": "A biblioteca precisa ser aprovada manualmente primeiro. Por favor leia o orientações antes de enviar. Você precisará de uma conta do GitHub para se comunicar e fazer alterações quando solicitado, mas não é estritamente necessário.", + "noteLicense": "Ao enviar, você concorda que a biblioteca será publicada sob a Licença MIT, o que, em suma, significa que qualquer pessoa pode utilizá-los sem restrições.", "noteItems": "Cada item da biblioteca deve ter seu próprio nome para que seja filtrável. Os seguintes itens da biblioteca serão incluídos:", "atleastOneLibItem": "Por favor, selecione pelo menos um item da biblioteca para começar", "republishWarning": "Nota: alguns dos itens selecionados estão marcados como já publicado/enviado. Você só deve reenviar itens ao atualizar uma biblioteca existente ou submissão." }, "publishSuccessDialog": { "title": "Biblioteca enviada", - "content": "Obrigado {{authorName}}. Sua biblioteca foi enviada para análise. Você pode acompanhar o status", - "link": "aqui" + "content": "Obrigado {{authorName}}. Sua biblioteca foi enviada para análise. Você pode acompanhar o statusaqui" }, "confirmDialog": { "resetLibrary": "Redefinir biblioteca", diff --git a/src/locales/pt-PT.json b/src/locales/pt-PT.json index 96cb4168a..0070d162c 100644 --- a/src/locales/pt-PT.json +++ b/src/locales/pt-PT.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Dica: tente aproximar um pouco os elementos mais distantes." }, "errorSplash": { - "headingMain_pre": "Foi encontrado um erro. Tente ", - "headingMain_button": "recarregar a página.", - "clearCanvasMessage": "Se a recarga não funcionar, tente ", - "clearCanvasMessage_button": "a limpar a área de desenho.", + "headingMain": "Foi encontrado um erro. Tente ", + "clearCanvasMessage": "Se a recarga não funcionar, tente ", "clearCanvasCaveat": " Isso resultará em perda de trabalho ", - "trackedToSentry_pre": "O erro com o identificador ", - "trackedToSentry_post": " foi rastreado no nosso sistema.", - "openIssueMessage_pre": "Fomos muito cautelosos para não incluir suas informações de cena no erro. Se sua cena não for privada, por favor, considere seguir nosso ", - "openIssueMessage_button": "rastreador de bugs.", - "openIssueMessage_post": " Por favor, inclua informações abaixo, copiando e colando no relatório de erros no GitHub.", + "trackedToSentry": "O erro com o identificador {{eventId}} foi rastreado no nosso sistema.", + "openIssueMessage": "Fomos muito cautelosos para não incluir suas informações de cena no erro. Se sua cena não for privada, por favor, considere seguir nosso Por favor, inclua informações abaixo, copiando e colando no relatório de erros no GitHub.", "sceneContent": "Conteúdo da cena:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Obrigatório", "website": "Introduza um URL válido" }, - "noteDescription": { - "pre": "Envie a sua biblioteca para ser incluída no ", - "link": "repositório de bibliotecas públicas", - "post": "para outras pessoas a poderem usar nos seus próprios desenhos." - }, - "noteGuidelines": { - "pre": "A biblioteca precisa ser aprovada manualmente primeiro. Por favor, leia ", - "link": "orientações", - "post": " antes de enviar. Vai precisar de uma conta no GitHub para comunicar e fazer alterações se solicitado, mas não é estritamente necessária." - }, - "noteLicense": { - "pre": "Ao enviar, concorda que a biblioteca será publicada sob a ", - "link": "Licença MIT, ", - "post": "o que significa, de forma resumida, que qualquer pessoa pode utilizá-la sem restrições." - }, + "noteDescription": "Envie a sua biblioteca para ser incluída no repositório de bibliotecas públicaspara outras pessoas a poderem usar nos seus próprios desenhos.", + "noteGuidelines": "A biblioteca precisa ser aprovada manualmente primeiro. Por favor, leia orientações antes de enviar. Vai precisar de uma conta no GitHub para comunicar e fazer alterações se solicitado, mas não é estritamente necessária.", + "noteLicense": "Ao enviar, concorda que a biblioteca será publicada sob a Licença MIT, o que significa, de forma resumida, que qualquer pessoa pode utilizá-la sem restrições.", "noteItems": "Cada item da biblioteca deve ter o seu próprio nome para que este seja pesquisável com filtros. Os seguintes itens da biblioteca serão incluídos:", "atleastOneLibItem": "Por favor, seleccione pelo menos um item da biblioteca para começar", "republishWarning": "Nota: alguns dos itens seleccionados estão marcados como já publicados/enviados. Só deve reenviar itens ao actualizar uma biblioteca existente ou submissão." }, "publishSuccessDialog": { "title": "Biblioteca enviada", - "content": "Obrigado {{authorName}}. A sua biblioteca foi enviada para análise. Pode acompanhar o status", - "link": "aqui" + "content": "Obrigado {{authorName}}. A sua biblioteca foi enviada para análise. Pode acompanhar o statusaqui" }, "confirmDialog": { "resetLibrary": "Repor a biblioteca", diff --git a/src/locales/ro-RO.json b/src/locales/ro-RO.json index bfbda4bfd..ddcd2c59d 100644 --- a/src/locales/ro-RO.json +++ b/src/locales/ro-RO.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Sfat: încearcă să apropii puțin mai mult elementele cele mai îndepărtate." }, "errorSplash": { - "headingMain_pre": "A apărut o eroare. Încearcă ", - "headingMain_button": "să reîncarci pagina.", - "clearCanvasMessage": "Dacă reîncărcarea nu funcționează, încearcă ", - "clearCanvasMessage_button": "să golești pânza.", + "headingMain": "A apărut o eroare. Încearcă ", + "clearCanvasMessage": "Dacă reîncărcarea nu funcționează, încearcă ", "clearCanvasCaveat": " Acest lucru va duce la pierderea progresului ", - "trackedToSentry_pre": "Eroarea cu identificatorul ", - "trackedToSentry_post": " a fost urmărită în sistemul nostru.", - "openIssueMessage_pre": "Am luat măsuri de precauție pentru a nu include informații despre scenă în eroare. Dacă scena nu este privată, te rugăm să ne oferi mai multe informații în ", - "openIssueMessage_button": "monitorul nostru pentru erori.", - "openIssueMessage_post": " Te rugăm să incluzi informațiile de mai jos prin copierea și lipirea în problema GitHub.", + "trackedToSentry": "Eroarea cu identificatorul {{eventId}} a fost urmărită în sistemul nostru.", + "openIssueMessage": "Am luat măsuri de precauție pentru a nu include informații despre scenă în eroare. Dacă scena nu este privată, te rugăm să ne oferi mai multe informații în Te rugăm să incluzi informațiile de mai jos prin copierea și lipirea în problema GitHub.", "sceneContent": "Conținutul scenei:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Obligatoriu", "website": "Introdu un URL valid" }, - "noteDescription": { - "pre": "Trimite-ți biblioteca pentru fi inclus în ", - "link": "depozitul de biblioteci publice", - "post": "pentru utilizarea de către alte persoane în desenele lor." - }, - "noteGuidelines": { - "pre": "Biblioteca trebuie aprobată manual. Citește ", - "link": "orientările", - "post": " înainte de trimitere. Vei avea nevoie de un cont GitHub pentru a comunica și efectua modificări, dacă este cazul, însă nu este strict necesar." - }, - "noteLicense": { - "pre": "Prin trimiterea bibliotecii, ești de acord că aceasta va fi publicată sub ", - "link": "Licența MIT, ", - "post": "care, pe scurt, înseamnă că oricine o poate folosi fără restricții." - }, + "noteDescription": "Trimite-ți biblioteca pentru fi inclus în depozitul de biblioteci publicepentru utilizarea de către alte persoane în desenele lor.", + "noteGuidelines": "Biblioteca trebuie aprobată manual. Citește orientările înainte de trimitere. Vei avea nevoie de un cont GitHub pentru a comunica și efectua modificări, dacă este cazul, însă nu este strict necesar.", + "noteLicense": "Prin trimiterea bibliotecii, ești de acord că aceasta va fi publicată sub Licența MIT, care, pe scurt, înseamnă că oricine o poate folosi fără restricții.", "noteItems": "Fiecare element din bibliotecă trebuie să aibă propriul nume astfel încât să fie filtrabil. Următoarele elemente din bibliotecă vor fi incluse:", "atleastOneLibItem": "Selectează cel puțin un element din bibliotecă pentru a începe", "republishWarning": "Observație: unele dintre elementele selectate sunt marcate ca fiind deja publicate/trimise. Ar trebui să retrimiți elemente numai atunci când actualizezi o trimitere sau o bibliotecă existentă." }, "publishSuccessDialog": { "title": "Bibliotecă trimisă", - "content": "Îți mulțumim, {{authorName}}. Biblioteca ta a fost trimisă spre revizuire. Poți urmări starea", - "link": "aici" + "content": "Îți mulțumim, {{authorName}}. Biblioteca ta a fost trimisă spre revizuire. Poți urmări stareaaici" }, "confirmDialog": { "resetLibrary": "Resetare bibliotecă", diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json index 8ed74de6f..c4680a7f6 100644 --- a/src/locales/ru-RU.json +++ b/src/locales/ru-RU.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Совет: попробуйте сблизить элементы рисунка." }, "errorSplash": { - "headingMain_pre": "Возникла ошибка. Попробуйте ", - "headingMain_button": "перезагрузить страницу.", - "clearCanvasMessage": "Если перезагрузка страницы не помогла, попробуйте ", - "clearCanvasMessage_button": "очистить холст.", + "headingMain": "Возникла ошибка. Попробуйте ", + "clearCanvasMessage": "Если перезагрузка страницы не помогла, попробуйте ", "clearCanvasCaveat": " Текущая работа будет утеряна ", - "trackedToSentry_pre": "Ошибка с идентификатором ", - "trackedToSentry_post": " отслеживается в нашей системе.", - "openIssueMessage_pre": "Для безопасности информация о вашей сцене не включена в ошибку. Если в сцене нет ничего конфиденциального, пожалуйста следуйте нашим ", - "openIssueMessage_button": "баг трекере.", - "openIssueMessage_post": " Пожалуйста, приложите информацию ниже, скопировав и вставив её, в issue GitHub.", + "trackedToSentry": "Ошибка с идентификатором {{eventId}} отслеживается в нашей системе.", + "openIssueMessage": "Для безопасности информация о вашей сцене не включена в ошибку. Если в сцене нет ничего конфиденциального, пожалуйста следуйте нашим Пожалуйста, приложите информацию ниже, скопировав и вставив её, в issue GitHub.", "sceneContent": "Содержание сцены:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Обязательно", "website": "Введите допустимый URL-адрес" }, - "noteDescription": { - "pre": "Отправить вашу библиотеку для включения в ", - "link": "хранилище публичных библиотек", - "post": ", чтобы другие люди могли использовать объекты из вашей библиотеки в своих рисунках." - }, - "noteGuidelines": { - "pre": "Библиотека должна быть подтверждена вручную. Пожалуйста, прочтите ", - "link": "рекомендации", - "post": " перед отправкой. Вам понадобится учетная запись GitHub, чтобы общаться и вносить изменения при необходимости, но это не обязательно." - }, - "noteLicense": { - "pre": "Выполняя отправку, вы соглашаетесь с тем, что библиотека будет опубликована под ", - "link": "лицензией MIT, ", - "post": ", что, вкратце, означает, что каждый может использовать её без ограничений." - }, + "noteDescription": "Отправить вашу библиотеку для включения в хранилище публичных библиотек, чтобы другие люди могли использовать объекты из вашей библиотеки в своих рисунках.", + "noteGuidelines": "Библиотека должна быть подтверждена вручную. Пожалуйста, прочтите рекомендации перед отправкой. Вам понадобится учетная запись GitHub, чтобы общаться и вносить изменения при необходимости, но это не обязательно.", + "noteLicense": "Выполняя отправку, вы соглашаетесь с тем, что библиотека будет опубликована под лицензией MIT, , что, вкратце, означает, что каждый может использовать её без ограничений.", "noteItems": "Каждый объект в библиотеке должен иметь свое собственное имя, чтобы по нему можно было фильтровать. Следующие объекты библиотеки будут включены:", "atleastOneLibItem": "Пожалуйста, выберите хотя бы один объект в библиотеке, чтобы начать", "republishWarning": "Примечание: некоторые из выбранных элементов помечены как уже опубликованные/отправленные. Вы должны повторно отправить элементы только при обновлении существующей библиотеки или сдаче работы." }, "publishSuccessDialog": { "title": "Библиотека отправлена", - "content": "Благодарим вас, {{authorName}}. Ваша библиотека была отправлена на проверку. Вы можете отслеживать статус", - "link": "здесь" + "content": "Благодарим вас, {{authorName}}. Ваша библиотека была отправлена на проверку. Вы можете отслеживать статусздесь" }, "confirmDialog": { "resetLibrary": "Сброс библиотеки", diff --git a/src/locales/si-LK.json b/src/locales/si-LK.json index 786ff8004..60a3d2f1f 100644 --- a/src/locales/si-LK.json +++ b/src/locales/si-LK.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "" }, "errorSplash": { - "headingMain_pre": "", - "headingMain_button": "", + "headingMain": "", "clearCanvasMessage": "", - "clearCanvasMessage_button": "", "clearCanvasCaveat": "", - "trackedToSentry_pre": "", - "trackedToSentry_post": "", - "openIssueMessage_pre": "", - "openIssueMessage_button": "", - "openIssueMessage_post": "", + "trackedToSentry": "", + "openIssueMessage": "", "sceneContent": "" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "", "website": "" }, - "noteDescription": { - "pre": "", - "link": "", - "post": "" - }, - "noteGuidelines": { - "pre": "", - "link": "", - "post": "" - }, - "noteLicense": { - "pre": "", - "link": "", - "post": "" - }, + "noteDescription": "", + "noteGuidelines": "", + "noteLicense": "", "noteItems": "", "atleastOneLibItem": "", "republishWarning": "" }, "publishSuccessDialog": { "title": "", - "content": "", - "link": "" + "content": "" }, "confirmDialog": { "resetLibrary": "", diff --git a/src/locales/sk-SK.json b/src/locales/sk-SK.json index d3374aa5d..4e97ba73e 100644 --- a/src/locales/sk-SK.json +++ b/src/locales/sk-SK.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Tip: skúste presunúť najvzdialenejšie prvky bližšie k sebe." }, "errorSplash": { - "headingMain_pre": "Nastala chyba. Vyskúšajte ", - "headingMain_button": "obnoviť stránku.", - "clearCanvasMessage": "Ak obnovenie stránky nepomáha, vyskúšajte ", - "clearCanvasMessage_button": "vyčistiť plátno.", + "headingMain": "Nastala chyba. Vyskúšajte ", + "clearCanvasMessage": "Ak obnovenie stránky nepomáha, vyskúšajte ", "clearCanvasCaveat": " To bude mať za následok stratu práce ", - "trackedToSentry_pre": "Chyba s identifikátorom ", - "trackedToSentry_post": " bola zaznamenaná v našom systéme.", - "openIssueMessage_pre": "Boli sme veľmi opatrní, aby informácie vašej scény neboli v chybe zaznamenané. Ak vaša scéna nie je súkromná, prosím zvážte pokračovanie na naše ", - "openIssueMessage_button": "hlásenie chýb.", - "openIssueMessage_post": " Prosím zahrňte informácie nižšie pomocou kopírovania a prilepenia do GitHub issue.", + "trackedToSentry": "Chyba s identifikátorom {{eventId}} bola zaznamenaná v našom systéme.", + "openIssueMessage": "Boli sme veľmi opatrní, aby informácie vašej scény neboli v chybe zaznamenané. Ak vaša scéna nie je súkromná, prosím zvážte pokračovanie na naše Prosím zahrňte informácie nižšie pomocou kopírovania a prilepenia do GitHub issue.", "sceneContent": "Obsah scény:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Povinné", "website": "Zadajte platnú adresu URL" }, - "noteDescription": { - "pre": "Uverejnite vašu knižnicu vo ", - "link": "verejnom zozname knižníc", - "post": "aby ju aj ostatní mohli použiť v ich náčrtoch." - }, - "noteGuidelines": { - "pre": "Knižnica musí byť najprv manuálne schválená. Prosím prečítajte si ", - "link": "pokyny", - "post": " pred uverejnením. Budete potrebovať Github účet na komunikáciu a vykonanie zmien, ak budú potrebné, avšak nie je to úplne povinné." - }, - "noteLicense": { - "pre": "Potvrdením súhlasíte, že knižnica bude zverejnená s ", - "link": "MIT licenciou, ", - "post": "čo v skratke znamená, že ju môže použiť hocikto bez obmedzení." - }, + "noteDescription": "Uverejnite vašu knižnicu vo verejnom zozname knižnícaby ju aj ostatní mohli použiť v ich náčrtoch.", + "noteGuidelines": "Knižnica musí byť najprv manuálne schválená. Prosím prečítajte si pokyny pred uverejnením. Budete potrebovať Github účet na komunikáciu a vykonanie zmien, ak budú potrebné, avšak nie je to úplne povinné.", + "noteLicense": "Potvrdením súhlasíte, že knižnica bude zverejnená s MIT licenciou, čo v skratke znamená, že ju môže použiť hocikto bez obmedzení.", "noteItems": "Každá položka v knižnici musí mať svoje vlastné meno, aby sa dala vyhľadať. Súčasťou knižnice budú nasledujúce položky:", "atleastOneLibItem": "Začnite prosím zvolením aspoň jednej položky z knižnice", "republishWarning": "Poznámka: Niektoré z vybraných položiek sú už označené ako zverejnené. Ich znovu uverejnenie by ste mali vykovať iba vtedy ak aktualizujete už existujúcu knižnicu alebo požiadavku na uverejnenie." }, "publishSuccessDialog": { "title": "Knižnica uverejnená", - "content": "Ďakujeme vám {{authorName}}. Vaša knižnica bola uverejnená na posúdenie. Stav môžete skontrolovať", - "link": "tu" + "content": "Ďakujeme vám {{authorName}}. Vaša knižnica bola uverejnená na posúdenie. Stav môžete skontrolovaťtu" }, "confirmDialog": { "resetLibrary": "Obnoviť knižnicu", diff --git a/src/locales/sl-SI.json b/src/locales/sl-SI.json index 2e178d129..1b5957384 100644 --- a/src/locales/sl-SI.json +++ b/src/locales/sl-SI.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Nasvet: poskusite premakniti najbolj oddaljene elemente nekoliko bližje skupaj." }, "errorSplash": { - "headingMain_pre": "Prišlo je do napake. Poskusite ", - "headingMain_button": "ponovno naložiti stran.", - "clearCanvasMessage": "Če ponovno nalaganje ne deluje, poskusite ", - "clearCanvasMessage_button": "počistiti platno.", + "headingMain": "Prišlo je do napake. Poskusite ", + "clearCanvasMessage": "Če ponovno nalaganje ne deluje, poskusite ", "clearCanvasCaveat": " To bo povzročilo izgubo dela ", - "trackedToSentry_pre": "Napaka z identifikatorjem ", - "trackedToSentry_post": " smo zabeležili v naš sistem.", - "openIssueMessage_pre": "Zelo smo bili previdni, da v podatke o napaki nismo vključili vaših podatkov o sceni. Če vaša scena ni zasebna, vas prosimo, da napišete več podrobnosti na našem ", - "openIssueMessage_button": "sledilniku hroščev.", - "openIssueMessage_post": " Prosimo, vključite spodnje informacije tako, da jih kopirate in prilepite v GitHub vprašanje.", + "trackedToSentry": "Napaka z identifikatorjem {{eventId}} smo zabeležili v naš sistem.", + "openIssueMessage": "Zelo smo bili previdni, da v podatke o napaki nismo vključili vaših podatkov o sceni. Če vaša scena ni zasebna, vas prosimo, da napišete več podrobnosti na našem Prosimo, vključite spodnje informacije tako, da jih kopirate in prilepite v GitHub vprašanje.", "sceneContent": "Vsebina scene:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Obvezno", "website": "Vnesite veljaven URL" }, - "noteDescription": { - "pre": "Predložite svojo knjižnico, da bo vključena v ", - "link": "javno skladišče knjižnic,", - "post": "da jih drugi lahko uporabljajo v svojih risbah." - }, - "noteGuidelines": { - "pre": "Knjižnica mora biti najprej ročno odobrena. Prosimo vas, da pred oddajanjem preberete naše ", - "link": "smernice.", - "post": "Za komunikacijo in spreminjanje po potrebi boste potrebovali račun GitHub, vendar to ni obvezno." - }, - "noteLicense": { - "pre": "Z oddajo se strinjate, da bo knjižnica objavljena pod ", - "link": "licenco MIT, ", - "post": "kar na kratko pomeni, da jo lahko kdorkoli uporablja brez omejitev." - }, + "noteDescription": "Predložite svojo knjižnico, da bo vključena v javno skladišče knjižnic,da jih drugi lahko uporabljajo v svojih risbah.", + "noteGuidelines": "Knjižnica mora biti najprej ročno odobrena. Prosimo vas, da pred oddajanjem preberete naše smernice.Za komunikacijo in spreminjanje po potrebi boste potrebovali račun GitHub, vendar to ni obvezno.", + "noteLicense": "Z oddajo se strinjate, da bo knjižnica objavljena pod licenco MIT, kar na kratko pomeni, da jo lahko kdorkoli uporablja brez omejitev.", "noteItems": "Vsak element knjižnice mora imeti svoje ime, tako da ga je mogoče filtrirati. Vključeni bodo naslednji elementi knjižnice:", "atleastOneLibItem": "Za začetek izberite vsaj en element knjižnice", "republishWarning": "Opomba: nekateri izbrani predmeti so označeni kot že objavljeni/oddani. Elemente lahko znova oddate samo, ko posodabljate obstoječo knjižnico ali oddajo." }, "publishSuccessDialog": { "title": "Knjižnica oddana", - "content": "{{authorName}}, hvala. Vaša knjižnica je bila poslana v pregled. Stanje lahko spremljate", - "link": "tukaj" + "content": "{{authorName}}, hvala. Vaša knjižnica je bila poslana v pregled. Stanje lahko spremljatetukaj" }, "confirmDialog": { "resetLibrary": "Ponastavi knjižnico", diff --git a/src/locales/sv-SE.json b/src/locales/sv-SE.json index f61b63767..a3d98d7a7 100644 --- a/src/locales/sv-SE.json +++ b/src/locales/sv-SE.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Tips: prova att flytta de mest avlägsna elementen lite närmare varandra." }, "errorSplash": { - "headingMain_pre": "Ett fel uppstod. Försök ", - "headingMain_button": "med att läsa in sidan på nytt.", - "clearCanvasMessage": "Om omladdning inte fungerar, försök ", - "clearCanvasMessage_button": "rensa canvasen.", + "headingMain": "Ett fel uppstod. Försök ", + "clearCanvasMessage": "Om omladdning inte fungerar, försök ", "clearCanvasCaveat": " Detta kommer att leda till förlust av arbete ", - "trackedToSentry_pre": "Felet med identifieraren ", - "trackedToSentry_post": " spårades på vårt system.", - "openIssueMessage_pre": "Vi var mycket försiktiga med att inte inkludera din skissinformation om felet. Om din skiss inte är privat, vänligen överväga att följa upp på vår ", - "openIssueMessage_button": "buggspårare.", - "openIssueMessage_post": " Vänligen inkludera information nedan genom att kopiera och klistra in i GitHub-problemet.", + "trackedToSentry": "Felet med identifieraren {{eventId}} spårades på vårt system.", + "openIssueMessage": "Vi var mycket försiktiga med att inte inkludera din skissinformation om felet. Om din skiss inte är privat, vänligen överväga att följa upp på vår Vänligen inkludera information nedan genom att kopiera och klistra in i GitHub-problemet.", "sceneContent": "Skissinnehåll:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Obligatoriskt", "website": "Ange en giltig URL" }, - "noteDescription": { - "pre": "Skicka ditt bibliotek för att inkluderas i ", - "link": "det offentliga bibliotekets arkiv", - "post": "för andra människor att använda i sina skisser." - }, - "noteGuidelines": { - "pre": "Biblioteket måste godkännas manuellt först. Vänligen läs ", - "link": "riktlinjerna", - "post": " innan du skickar in. Du behöver ett GitHub-konto för att kommunicera och göra ändringar om så önskas, men det krävs inte." - }, - "noteLicense": { - "pre": "Genom att skicka in godkänner du att biblioteket kommer att publiceras under ", - "link": "MIT-licens, ", - "post": "vilket kort sagt betyder att vem som helst kan använda det utan restriktioner." - }, + "noteDescription": "Skicka ditt bibliotek för att inkluderas i det offentliga bibliotekets arkivför andra människor att använda i sina skisser.", + "noteGuidelines": "Biblioteket måste godkännas manuellt först. Vänligen läs riktlinjerna innan du skickar in. Du behöver ett GitHub-konto för att kommunicera och göra ändringar om så önskas, men det krävs inte.", + "noteLicense": "Genom att skicka in godkänner du att biblioteket kommer att publiceras under MIT-licens, vilket kort sagt betyder att vem som helst kan använda det utan restriktioner.", "noteItems": "Varje objekt måste ha sitt eget namn så att det är filtrerbart. Följande objekt kommer att inkluderas:", "atleastOneLibItem": "Välj minst ett biblioteksobjekt för att komma igång", "republishWarning": "Obs: några av de markerade objekten är redan markerade som publicerade/skickade. Du bör endast skicka objekt igen när du uppdaterar ett befintligt bibliotek eller inlämning." }, "publishSuccessDialog": { "title": "Bibliotek inskickat", - "content": "Tack {{authorName}}. Ditt bibliotek har skickats för granskning. Du kan följa status", - "link": "här" + "content": "Tack {{authorName}}. Ditt bibliotek har skickats för granskning. Du kan följa statushär" }, "confirmDialog": { "resetLibrary": "Återställ bibliotek", diff --git a/src/locales/ta-IN.json b/src/locales/ta-IN.json index 279dcdd3c..b82220f4a 100644 --- a/src/locales/ta-IN.json +++ b/src/locales/ta-IN.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "துணுக்குதவி: தூரத்திலுள்ள உறுப்புகளைப் நெருக்கமாக நகர்த்தப்பார்." }, "errorSplash": { - "headingMain_pre": "பிழையைச் சந்தித்தீரா. முயலவும் ", - "headingMain_button": "பக்கத்தை மீண்டுமேற்றுகிறது.", - "clearCanvasMessage": "மீண்டுமேற்றல் வேலைசெய்யவிட்டால், முயற்சி ", - "clearCanvasMessage_button": "கித்தானைத் துடைக்கிறது.", + "headingMain": "பிழையைச் சந்தித்தீரா. முயலவும் ", + "clearCanvasMessage": "மீண்டுமேற்றல் வேலைசெய்யவிட்டால், முயற்சி ", "clearCanvasCaveat": " இது வேலையை இழக்கக்கூடும் ", - "trackedToSentry_pre": "இனங்காணியில் பிழை ", - "trackedToSentry_post": " எங்கள் இயங்குதளத்தில் தடமறியப்பட்டது.", - "openIssueMessage_pre": "பிழையில் உம் காட்சி தகவலை உள்ளடக்காமலிருக்க நாங்கள் மிக எச்சரிக்கையாக இருந்தோம். உம் காட்சி தனிப்பட்டதில்லையெனில், பின்தொடர்வதற்கு பரிசீலிக்கவும் எங்கள் ", - "openIssueMessage_button": "பிழை தடமி.", - "openIssueMessage_post": " கீழுள்ள தகவலை நகலெடுத்து ஒட்டி GitHub சிக்கலுள் உள்ளடக்கவும்.", + "trackedToSentry": "இனங்காணியில் பிழை {{eventId}} எங்கள் இயங்குதளத்தில் தடமறியப்பட்டது.", + "openIssueMessage": "பிழையில் உம் காட்சி தகவலை உள்ளடக்காமலிருக்க நாங்கள் மிக எச்சரிக்கையாக இருந்தோம். உம் காட்சி தனிப்பட்டதில்லையெனில், பின்தொடர்வதற்கு பரிசீலிக்கவும் எங்கள் கீழுள்ள தகவலை நகலெடுத்து ஒட்டி GitHub சிக்கலுள் உள்ளடக்கவும்.", "sceneContent": "காட்சி உள்ளடக்கம்:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "தேவைப்டுகிறது", "website": "செல்லத்தக்க உரலியை உள்ளிடு" }, - "noteDescription": { - "pre": "உம் நூலகத்தைச் சமர்ப்பி உள்ளடக்குவதற்கு ", - "link": "பொது நூலக களஞ்சியத்தில்", - "post": "பிற மக்களவர்களின் சித்திரங்களில் பயன்படுத்த." - }, - "noteGuidelines": { - "pre": "நூலகம் முதலில் கைமுறையாக ஒப்புக்கொள்ளப்படவேண்டும். வாசிக்கவும் ", - "link": "வழிகாட்டுதல்களைச்", - "post": " சமர்ப்பிக்கும் முன்பு. கோரப்பட்டால் தொடர்புகொள்ள மற்றும் மாற்றங்கள் செய்ய உமக்கொரு GitHub கணக்கு தேவை, ஆனால் அது கண்டிப்பாக தேவையல்ல." - }, - "noteLicense": { - "pre": "சமர்ப்பிப்பதனால், நூலகம் இதனடியில் பிரசரிக்கப்பட ஏற்கிறீர்கள் ", - "link": "MIT உரிமம், ", - "post": "சுருக்கமாக எவருமிதைப் வரையறையின்றி பயன்படுத்தலாமென குறிக்கிறது." - }, + "noteDescription": "உம் நூலகத்தைச் சமர்ப்பி உள்ளடக்குவதற்கு பொது நூலக களஞ்சியத்தில்பிற மக்களவர்களின் சித்திரங்களில் பயன்படுத்த.", + "noteGuidelines": "நூலகம் முதலில் கைமுறையாக ஒப்புக்கொள்ளப்படவேண்டும். வாசிக்கவும் வழிகாட்டுதல்களைச் சமர்ப்பிக்கும் முன்பு. கோரப்பட்டால் தொடர்புகொள்ள மற்றும் மாற்றங்கள் செய்ய உமக்கொரு GitHub கணக்கு தேவை, ஆனால் அது கண்டிப்பாக தேவையல்ல.", + "noteLicense": "சமர்ப்பிப்பதனால், நூலகம் இதனடியில் பிரசரிக்கப்பட ஏற்கிறீர்கள் MIT உரிமம், சுருக்கமாக எவருமிதைப் வரையறையின்றி பயன்படுத்தலாமென குறிக்கிறது.", "noteItems": "வடிக்கட்டக்கூடியதாகவிருக்க ஒவ்வொரு நூலகவுருப்படிக்கும் சொந்த பெயர் இருக்கவேண்டும். பின்வரும் நூலகவுருப்படிகள் உள்ளடக்கப்படும்:", "atleastOneLibItem": "ஆரம்பிக்க ஒரு நூலக உருப்படியையாவது தேர்ந்தெடுக்கவும்", "republishWarning": "" }, "publishSuccessDialog": { "title": "நூலகம் சமர்ப்பிக்கப்பட்டது", - "content": "நன்றி {{authorName}}. உமது நூலகம் மதிப்பாய்விற்காக சமர்ப்பிக்கப்பட்டது. நிலையை நீங்கள் தடமறியலாம்", - "link": "இங்கே" + "content": "நன்றி {{authorName}}. உமது நூலகம் மதிப்பாய்விற்காக சமர்ப்பிக்கப்பட்டது. நிலையை நீங்கள் தடமறியலாம்இங்கே" }, "confirmDialog": { "resetLibrary": "நூலகத்தை அகரமாக்கு", diff --git a/src/locales/th-TH.json b/src/locales/th-TH.json index bd87b5189..3860af21d 100644 --- a/src/locales/th-TH.json +++ b/src/locales/th-TH.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "" }, "errorSplash": { - "headingMain_pre": "", - "headingMain_button": "กำลังรีโหลดหน้า", - "clearCanvasMessage": "ถ้าโหลดไม่ได้ ให้ลอง ", - "clearCanvasMessage_button": "เคลียร์ผืนผ้าใบ", + "headingMain": "", + "clearCanvasMessage": "ถ้าโหลดไม่ได้ ให้ลอง ", "clearCanvasCaveat": "", - "trackedToSentry_pre": "", - "trackedToSentry_post": "", - "openIssueMessage_pre": "", - "openIssueMessage_button": "", - "openIssueMessage_post": "", + "trackedToSentry": "", + "openIssueMessage": "", "sceneContent": "" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "", "website": "" }, - "noteDescription": { - "pre": "", - "link": "", - "post": "" - }, - "noteGuidelines": { - "pre": "", - "link": "", - "post": "" - }, - "noteLicense": { - "pre": "", - "link": "", - "post": "" - }, + "noteDescription": "", + "noteGuidelines": "", + "noteLicense": "", "noteItems": "", "atleastOneLibItem": "", "republishWarning": "" }, "publishSuccessDialog": { "title": "", - "content": "", - "link": "" + "content": "" }, "confirmDialog": { "resetLibrary": "", diff --git a/src/locales/tr-TR.json b/src/locales/tr-TR.json index 9bd473b00..a48a16408 100644 --- a/src/locales/tr-TR.json +++ b/src/locales/tr-TR.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "İpucu: En uzaktaki elemanları birbirine yakınlaştırmayı deneyin." }, "errorSplash": { - "headingMain_pre": "Hata oluştu. Lütfen ", - "headingMain_button": "sayfayı yenilemeyi deneyin.", - "clearCanvasMessage": "Yenileme sonrası sorun devam ediyorsa, lütfen ", - "clearCanvasMessage_button": "çizim alanını temizlemeyi deneyin.", + "headingMain": "Hata oluştu. Lütfen ", + "clearCanvasMessage": "Yenileme sonrası sorun devam ediyorsa, lütfen ", "clearCanvasCaveat": " Bu, yaptığınız değişiklikleri sıfırlayacak ", - "trackedToSentry_pre": "Tanımlayıcı ile ilgili hata ", - "trackedToSentry_post": " sistemimize yakalandı.", - "openIssueMessage_pre": "Sahne bilginizi hata mesajına yansıtmamak için oldukça dikkatli davrandık. Eğer sahneniz gizli değilse hatayı lütfen şuradan takip edin ", - "openIssueMessage_button": "hata takibi.", - "openIssueMessage_post": " Lütfen aşağıya GitHub sorununa kopyalayarak ve yapıştırarak bilgi ekleyin.", + "trackedToSentry": "Tanımlayıcı ile ilgili hata {{eventId}} sistemimize yakalandı.", + "openIssueMessage": "Sahne bilginizi hata mesajına yansıtmamak için oldukça dikkatli davrandık. Eğer sahneniz gizli değilse hatayı lütfen şuradan takip edin Lütfen aşağıya GitHub sorununa kopyalayarak ve yapıştırarak bilgi ekleyin.", "sceneContent": "Sahne içeriği:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Gerekli", "website": "Geçerli bir URL girin" }, - "noteDescription": { - "pre": "Submit your library to be included in the ", - "link": "genel kütüphane reposu", - "post": "diğer insanlar çizimlerinde kullanabilsin diye." - }, - "noteGuidelines": { - "pre": "Önce kütüphane elle onaylanmalı. şunu okuyun ", - "link": "yönergeler", - "post": " onaylamadan önce. gerekli olması halinde iletişim kurmak için ve değişiklik için Github hesabı gerekli, ama çok da illaki olmalı değil." - }, - "noteLicense": { - "pre": "Bunu onaylayarak, kütüğhanenin şu lisansla yayınlanmasını onaylıyorsunuz ", - "link": "MIT Lisans, ", - "post": "ki bu kısaca herkesin onu kısıtlama olmaksızın kullanabileceği anlamına gelmektedir." - }, + "noteDescription": "Submit your library to be included in the genel kütüphane reposudiğer insanlar çizimlerinde kullanabilsin diye.", + "noteGuidelines": "Önce kütüphane elle onaylanmalı. şunu okuyun yönergeler onaylamadan önce. gerekli olması halinde iletişim kurmak için ve değişiklik için Github hesabı gerekli, ama çok da illaki olmalı değil.", + "noteLicense": "Bunu onaylayarak, kütüğhanenin şu lisansla yayınlanmasını onaylıyorsunuz MIT Lisans, ki bu kısaca herkesin onu kısıtlama olmaksızın kullanabileceği anlamına gelmektedir.", "noteItems": "Her kütüphane kendi ismine sahip olmalı ki tarama yapabilelim. Şu kütüphane ögeleri dahil edilecek:", "atleastOneLibItem": "Lütfen başlamak için en az bir tane kütüphane ögesi seçin", "republishWarning": "Not: seçilen ögelerden bir kısmı zaten yayınlanmış/gönderilmiş. Yalnızca mevcut kütüphane ve gönderileri güncellerken yeniden gönderme işlemi yapmalısınız." }, "publishSuccessDialog": { "title": "Kütüphane gönderildi", - "content": "Teşekkürler {{authorName}}. Kütüphaneniz gözden geçirme için alındı. Durumu takip edebilirsiniz", - "link": "burada" + "content": "Teşekkürler {{authorName}}. Kütüphaneniz gözden geçirme için alındı. Durumu takip edebilirsinizburada" }, "confirmDialog": { "resetLibrary": "Kütüphaneyi sıfırla", diff --git a/src/locales/uk-UA.json b/src/locales/uk-UA.json index fbe77af76..1f8a70c76 100644 --- a/src/locales/uk-UA.json +++ b/src/locales/uk-UA.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Порада: спробуйте підсунути найвіддаленіші елементи ближче один до одного." }, "errorSplash": { - "headingMain_pre": "Сталася помилка. Спробуйте ", - "headingMain_button": "перезавантажити сторінку.", - "clearCanvasMessage": "Якщо перезавантаження не допоможе, спробуйте ", - "clearCanvasMessage_button": "очистити полотно.", + "headingMain": "Сталася помилка. Спробуйте ", + "clearCanvasMessage": "Якщо перезавантаження не допоможе, спробуйте ", "clearCanvasCaveat": " Це призведе до втрати роботи ", - "trackedToSentry_pre": "Помилка з ідентифікатором ", - "trackedToSentry_post": " було відслідковано в нашій системі.", - "openIssueMessage_pre": "Ми були дуже обережні, щоб не включати інформацію про ваші сцени в текст помилки. Якщо ваша сцена не була приватна, будь ласка, розгляньте можливість продовження на нашому ", - "openIssueMessage_button": "трекер помилок.", - "openIssueMessage_post": " Будь ласка, додайте інформацію нижче, скопіюючи і вставляючи у GitHub issue.", + "trackedToSentry": "Помилка з ідентифікатором {{eventId}} було відслідковано в нашій системі.", + "openIssueMessage": "Ми були дуже обережні, щоб не включати інформацію про ваші сцени в текст помилки. Якщо ваша сцена не була приватна, будь ласка, розгляньте можливість продовження на нашому Будь ласка, додайте інформацію нижче, скопіюючи і вставляючи у GitHub issue.", "sceneContent": "Вміст сцени:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "Обов’язково", "website": "Введіть дійсну URL-адресу" }, - "noteDescription": { - "pre": "Подати бібліотеку, щоб вона була включена до ", - "link": "публічного репозиторія бібліотек", - "post": "для інших людей, для використання у їхніх полотнах." - }, - "noteGuidelines": { - "pre": "Спочатку бібліотека повинна бути підтверджена. Будь ласка, прочитайте ", - "link": "настанови", - "post": " перед відправкою. Вам знадобиться обліковий запис на GitHub, щоб колаборувати та вносити зміни, але це не обов'язково." - }, - "noteLicense": { - "pre": "Публікуючи, ви погоджуєтеся, що бібліотека буде опублікована під ", - "link": "Ліцензія MIT, ", - "post": ", простими словами, це означає що нею зможе користуватися будь-хто без обмежень." - }, + "noteDescription": "Подати бібліотеку, щоб вона була включена до публічного репозиторія бібліотекдля інших людей, для використання у їхніх полотнах.", + "noteGuidelines": "Спочатку бібліотека повинна бути підтверджена. Будь ласка, прочитайте настанови перед відправкою. Вам знадобиться обліковий запис на GitHub, щоб колаборувати та вносити зміни, але це не обов'язково.", + "noteLicense": "Публікуючи, ви погоджуєтеся, що бібліотека буде опублікована під Ліцензія MIT, , простими словами, це означає що нею зможе користуватися будь-хто без обмежень.", "noteItems": "Кожен об'єкт в бібліотеці повинен мати назву, це потрібно для пошуку та фільтрування. Наступні об'єкти бібліотеки будуть включені:", "atleastOneLibItem": "Будь ласка, виберіть принаймні один елемент бібліотеки, щоб почати", "republishWarning": "Зауважте, деякі з вибраних елементів позначені як вже опубліковані/надіслані. Ви повинні повторно надсилати елементи тільки при оновленні вже опублікованої бібліотеки чи при публікації бібліотеки." }, "publishSuccessDialog": { "title": "Бібліотека відправлена", - "content": "Дякуємо, {{authorName}}. Ваша бібліотека була відправлена для розгляду. Ви можете відстежувати статус", - "link": "тут" + "content": "Дякуємо, {{authorName}}. Ваша бібліотека була відправлена для розгляду. Ви можете відстежувати статустут" }, "confirmDialog": { "resetLibrary": "Очистити бібліотеку", diff --git a/src/locales/vi-VN.json b/src/locales/vi-VN.json index dcc8c44df..1015b0393 100644 --- a/src/locales/vi-VN.json +++ b/src/locales/vi-VN.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "Mẹo: hãy thử di chuyển các elements nhất lại gần nhau hơn một chút." }, "errorSplash": { - "headingMain_pre": "", - "headingMain_button": "", - "clearCanvasMessage": "Nếu không tải lại được, hãy thử ", - "clearCanvasMessage_button": "dọn canvas.", + "headingMain": "", + "clearCanvasMessage": "Nếu không tải lại được, hãy thử ", "clearCanvasCaveat": " Điều này sẽ dẫn đến mất dữ liệu bạn đã làm ", - "trackedToSentry_pre": "", - "trackedToSentry_post": "", - "openIssueMessage_pre": "", - "openIssueMessage_button": "", - "openIssueMessage_post": "", + "trackedToSentry": "", + "openIssueMessage": "", "sceneContent": "" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "", "website": "" }, - "noteDescription": { - "pre": "", - "link": "", - "post": "" - }, - "noteGuidelines": { - "pre": "", - "link": "", - "post": "" - }, - "noteLicense": { - "pre": "", - "link": "", - "post": "" - }, + "noteDescription": "", + "noteGuidelines": "", + "noteLicense": "", "noteItems": "Từng món trong thư viện phải có tên riêng để có thể lọc. Các món thư viện sau đây sẽ thêm:", "atleastOneLibItem": "Vui lòng chọn ít nhất một món thư viện để bắt đầu", "republishWarning": "Lưu ý: một số món đã chọn được đánh dấu là đã xuất bản/đã gửi. Bạn chỉ nên gửi lại các món khi cập nhật thư viện hiện có hoặc gửi." }, "publishSuccessDialog": { "title": "", - "content": "", - "link": "" + "content": "" }, "confirmDialog": { "resetLibrary": "", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index d47ac5744..522cd8420 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "提示:尝试将最远的元素移动到和其它元素更近一些。" }, "errorSplash": { - "headingMain_pre": "遇到异常。请尝试", - "headingMain_button": "重新加载页面。", - "clearCanvasMessage": "如果重新加载页面无效, 请尝试", - "clearCanvasMessage_button": "清除画布。", + "headingMain": "遇到异常。请尝试", + "clearCanvasMessage": "如果重新加载页面无效, 请尝试", "clearCanvasCaveat": "这会造成当前工作丢失", - "trackedToSentry_pre": "带有标识符的错误", - "trackedToSentry_post": "已在我们的系统中跟踪", - "openIssueMessage_pre": "我们非常谨慎地处理错误信息,您的画布内容不会被包含在错误报告中。如果您的画布内容不需要保持私密,请考虑使用我们的", - "openIssueMessage_button": "错误追踪器。", - "openIssueMessage_post": " 请复制并粘贴以下信息到 GitHub Issue 中。", + "trackedToSentry": "带有标识符的错误{{eventId}}已在我们的系统中跟踪", + "openIssueMessage": "我们非常谨慎地处理错误信息,您的画布内容不会被包含在错误报告中。如果您的画布内容不需要保持私密,请考虑使用我们的 请复制并粘贴以下信息到 GitHub Issue 中。", "sceneContent": "画布内容:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "必填", "website": "输入一个有效的URL" }, - "noteDescription": { - "pre": "提交后,您的素材库将被包含在 ", - "link": "公共素材库广场", - "post": "以供其他人在绘图中使用。" - }, - "noteGuidelines": { - "pre": "提交的素材库需先经人工审核。在提交之前,请先阅读 ", - "link": "指南", - "post": " 。后续沟通和对库的修改需要 GitHub 账号,但这不是必须的。" - }, - "noteLicense": { - "pre": "提交即表明您已同意素材库将遵循 ", - "link": "MIT 许可证, ", - "post": "简而言之,任何人都可以不受限制地使用它们。" - }, + "noteDescription": "提交后,您的素材库将被包含在 公共素材库广场以供其他人在绘图中使用。", + "noteGuidelines": "提交的素材库需先经人工审核。在提交之前,请先阅读 指南 。后续沟通和对库的修改需要 GitHub 账号,但这不是必须的。", + "noteLicense": "提交即表明您已同意素材库将遵循 MIT 许可证, 简而言之,任何人都可以不受限制地使用它们。", "noteItems": "素材库中每个项目都有各自的名称以供筛选。以下项目将被包含:", "atleastOneLibItem": "请选择至少一个素材库以开始", "republishWarning": "注意:部分选中的项目已经发布或提交。请仅在更新已有或已提交的素材库时重复提交项目。" }, "publishSuccessDialog": { "title": "素材库已提交", - "content": "谢谢你 {{authorName}}。您的素材库已被提交审核。跟进此次提交的状态请点击", - "link": "此处" + "content": "谢谢你 {{authorName}}。您的素材库已被提交审核。跟进此次提交的状态请点击此处" }, "confirmDialog": { "resetLibrary": "重置素材库", diff --git a/src/locales/zh-HK.json b/src/locales/zh-HK.json index b89f8af73..527937163 100644 --- a/src/locales/zh-HK.json +++ b/src/locales/zh-HK.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "" }, "errorSplash": { - "headingMain_pre": "", - "headingMain_button": "", - "clearCanvasMessage": "如果重新整理頁面都係睇唔到,你可以", - "clearCanvasMessage_button": "清空畫布", + "headingMain": "", + "clearCanvasMessage": "如果重新整理頁面都係睇唔到,你可以", "clearCanvasCaveat": "(注意:呢個動作會直接丟棄你嘅作品,並且無法復原)", - "trackedToSentry_pre": "", - "trackedToSentry_post": "", - "openIssueMessage_pre": "", - "openIssueMessage_button": "", - "openIssueMessage_post": "", + "trackedToSentry": "", + "openIssueMessage": "", "sceneContent": "" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "", "website": "" }, - "noteDescription": { - "pre": "", - "link": "", - "post": "" - }, - "noteGuidelines": { - "pre": "", - "link": "", - "post": "" - }, - "noteLicense": { - "pre": "", - "link": "", - "post": "" - }, + "noteDescription": "", + "noteGuidelines": "", + "noteLicense": "", "noteItems": "", "atleastOneLibItem": "", "republishWarning": "" }, "publishSuccessDialog": { "title": "", - "content": "", - "link": "" + "content": "" }, "confirmDialog": { "resetLibrary": "", diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index fe8868bac..08965de95 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -264,16 +264,11 @@ "canvasTooBigTip": "提示:可嘗試將最遠的元素移動至較集中的位置" }, "errorSplash": { - "headingMain_pre": "發生錯誤,嘗試", - "headingMain_button": "重新載入頁面。", - "clearCanvasMessage": "若重新載入仍無法解決問題,嘗試", - "clearCanvasMessage_button": "清除 canvas。", + "headingMain": "發生錯誤,嘗試", + "clearCanvasMessage": "若重新載入仍無法解決問題,嘗試", "clearCanvasCaveat": "此動作將造成目前的作品被移除。", - "trackedToSentry_pre": "此錯誤與其識別碼", - "trackedToSentry_post": "將由系統記錄。", - "openIssueMessage_pre": "我們將謹慎處理,你的作品內容不會被包含在錯誤報告中。若你的作品不需保持私密,請考慮使用我們的", - "openIssueMessage_button": "bug tracker。", - "openIssueMessage_post": "請將下列資訊複製貼上至 GitHub issue 中。", + "trackedToSentry": "此錯誤與其識別碼{{eventId}}將由系統記錄。", + "openIssueMessage": "我們將謹慎處理,你的作品內容不會被包含在錯誤報告中。若你的作品不需保持私密,請考慮使用我們的請將下列資訊複製貼上至 GitHub issue 中。", "sceneContent": "作品內容:" }, "roomDialog": { @@ -353,29 +348,16 @@ "required": "必填", "website": "請輸入有效的 URL" }, - "noteDescription": { - "pre": "送出您的資料庫後將被包含於", - "link": "公開資料庫 repository", - "post": "以利他人在其繪圖中使用。" - }, - "noteGuidelines": { - "pre": "資料庫需先經人工審查。請閱讀", - "link": "說明文件", - "post": "再送出。若需溝通與修改時要透過 GitHub 帳號來進行,但並非強制需求。" - }, - "noteLicense": { - "pre": "送出即代表您同意此資料庫將發布時使用 ", - "link": "MIT 授權,", - "post": "簡單來說是指任何人都能不受限制的使用。" - }, + "noteDescription": "送出您的資料庫後將被包含於公開資料庫 repository以利他人在其繪圖中使用。", + "noteGuidelines": "資料庫需先經人工審查。請閱讀說明文件再送出。若需溝通與修改時要透過 GitHub 帳號來進行,但並非強制需求。", + "noteLicense": "送出即代表您同意此資料庫將發布時使用 MIT 授權,簡單來說是指任何人都能不受限制的使用。", "noteItems": "每個資料庫項目都有獨立的名稱故可篩選。會包含下列資料庫項目:", "atleastOneLibItem": "請選擇至少一項資料庫項目", "republishWarning": "注意:部分選取中的物件先前已發布/送出過。建議僅在要更新現存資料庫或已送出的物件時才重新送出這些物件。" }, "publishSuccessDialog": { "title": "資料庫已送出", - "content": "感謝 {{authorName}} 。您的資料庫已送出待審查。您可查看目前狀態", - "link": "在此" + "content": "感謝 {{authorName}} 。您的資料庫已送出待審查。您可查看目前狀態在此" }, "confirmDialog": { "resetLibrary": "重設資料庫",