Compare commits
	
		
			110 Commits
		
	
	
		
			vscode-rea
			...
			dwelle/exp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | fa9631617f | ||
|   | 0314e81396 | ||
|   | 8d5d68e589 | ||
|   | 6c15d9948b | ||
|   | e8fba43cf6 | ||
|   | 2e5c798c71 | ||
|   | 8c298336fc | ||
|   | 7f91cdc0c9 | ||
|   | 6334bd832f | ||
|   | 4d26993c8f | ||
|   | 1e69609ce4 | ||
|   | f5379d1563 | ||
|   | c8f6e3faa8 | ||
|   | 36bf17cf59 | ||
|   | 75458c3192 | ||
|   | 4cd25253bf | ||
|   | 78e254fb30 | ||
|   | 79bd3b8cda | ||
|   | 55110bf1b8 | ||
|   | 941b2d7042 | ||
|   | e9067de173 | ||
|   | fdc462ec01 | ||
|   | d1441afec9 | ||
|   | 3298aaf0c7 | ||
|   | e9a224a0de | ||
|   | 76cf560914 | ||
|   | 6c1246ef77 | ||
|   | b477c2ad6b | ||
|   | 4cb6f09559 | ||
|   | 8636ef1017 | ||
|   | 3a776f8795 | ||
|   | 9929a2be6f | ||
|   | 9cccac1458 | ||
|   | 7eaf47c9d4 | ||
|   | ec4b3d913e | ||
|   | 5390617c01 | ||
|   | 0d1058a596 | ||
|   | c5869979c8 | ||
|   | 6a6b9c90a7 | ||
|   | 5c17751662 | ||
|   | 898789b979 | ||
|   | 7922ce129e | ||
|   | 59ec1c6cee | ||
|   | 933c6a2237 | ||
|   | cd61f31116 | ||
|   | b3052f0178 | ||
|   | a271e42af1 | ||
|   | 836120c14b | ||
|   | da4fa91ffc | ||
|   | 553b493f37 | ||
|   | 59a1d192d2 | ||
|   | 8b7302e89e | ||
|   | f9b7cfd8aa | ||
|   | 2b4462c941 | ||
|   | 43b13d8e3a | ||
|   | 720f468f39 | ||
|   | 33300d19f6 | ||
|   | 5aed159991 | ||
|   | de1d221d1c | ||
|   | 9a68dbffe2 | ||
|   | 32d82219b1 | ||
|   | ba2c86fe1b | ||
|   | f1ae37c84b | ||
|   | ec350ba8b2 | ||
|   | 46a61ad4df | ||
|   | f4b1a30bef | ||
|   | 32aa79164b | ||
|   | b5fd904808 | ||
|   | 8f8dd1105f | ||
|   | b914ad41fc | ||
|   | 551c38f60b | ||
|   | 38e8ae46c9 | ||
|   | ad0c4c4c78 | ||
|   | 27cf5ed17e | ||
|   | fd946adbae | ||
|   | c37977af4b | ||
|   | a0d413ab4e | ||
|   | b67a2b4f65 | ||
|   | 5a8dbe8030 | ||
|   | 731093f631 | ||
|   | fe56975f19 | ||
|   | 2d800feeeb | ||
|   | 93cccd596a | ||
|   | 45b592227d | ||
|   | b818df1098 | ||
|   | 4359e2935d | ||
|   | 3d9d398378 | ||
|   | 0a5da0269f | ||
|   | 08ce7c7fc3 | ||
|   | fe7fbff7f6 | ||
|   | 501397cb61 | ||
|   | 865d29388c | ||
|   | 54c7ec416a | ||
|   | aca284057d | ||
|   | 2820cd112e | ||
|   | 426b5d9537 | ||
|   | e7d34677c6 | ||
|   | 3d5356cb8e | ||
|   | 46f5ce5ce0 | ||
|   | b00bd3d6c0 | ||
|   | 91fc22182c | ||
|   | 966ca2ffa6 | ||
|   | 2b049b4a65 | ||
|   | 339212e563 | ||
|   | f8b4bb66b4 | ||
|   | f4312bba5e | ||
|   | ac66665b64 | ||
|   | 2b71a1f0bd | ||
|   | 58845e450a | ||
|   | 15d79f8fee | 
							
								
								
									
										43
									
								
								.codesandbox/tasks.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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"] } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										15
									
								
								.github/workflows/publish-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -10,11 +10,16 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|  | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: docker/build-push-action@v2 | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v3 | ||||
|       - name: Login to DockerHub | ||||
|         uses: docker/login-action@v2 | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKER_USERNAME }} | ||||
|           password: ${{ secrets.DOCKER_PASSWORD }} | ||||
|           repository: excalidraw/excalidraw | ||||
|           tag_with_ref: true | ||||
|           tag_with_sha: true | ||||
|       - name: Build and push | ||||
|         uses: docker/build-push-action@v3 | ||||
|         with: | ||||
|           context: . | ||||
|           push: true | ||||
|           tags: excalidraw/excalidraw:latest | ||||
|   | ||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -19,11 +19,9 @@ logs | ||||
| node_modules | ||||
| npm-debug.log* | ||||
| package-lock.json | ||||
| static | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| src/packages/excalidraw/types | ||||
| src/packages/excalidraw/example/public/bundle.js | ||||
| src/packages/excalidraw/example/public/excalidraw-assets-dev | ||||
| src/packages/excalidraw/example/public/excalidraw.development.js | ||||
|  | ||||
|   | ||||
| @@ -34,7 +34,7 @@ Last but not least, we're thankful to these companies for offering their service | ||||
|  | ||||
| ## 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/) • [VS Code](https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor) | ||||
| [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/) • | ||||
|  | ||||
| ## Documentation | ||||
|  | ||||
| @@ -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! | ||||
|  | ||||
|   | ||||
							
								
								
									
										20
									
								
								dev-docs/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,20 @@ | ||||
| # Dependencies | ||||
| /node_modules | ||||
|  | ||||
| # Production | ||||
| /build | ||||
|  | ||||
| # Generated files | ||||
| .docusaurus | ||||
| .cache-loader | ||||
|  | ||||
| # Misc | ||||
| .DS_Store | ||||
| .env.local | ||||
| .env.development.local | ||||
| .env.test.local | ||||
| .env.production.local | ||||
|  | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
							
								
								
									
										41
									
								
								dev-docs/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,41 @@ | ||||
| # Website | ||||
|  | ||||
| This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. | ||||
|  | ||||
| ### Installation | ||||
|  | ||||
| ``` | ||||
| $ yarn | ||||
| ``` | ||||
|  | ||||
| ### Local Development | ||||
|  | ||||
| ``` | ||||
| $ yarn start | ||||
| ``` | ||||
|  | ||||
| This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. | ||||
|  | ||||
| ### Build | ||||
|  | ||||
| ``` | ||||
| $ yarn build | ||||
| ``` | ||||
|  | ||||
| This command generates static content into the `build` directory and can be served using any static contents hosting service. | ||||
|  | ||||
| ### Deployment | ||||
|  | ||||
| Using SSH: | ||||
|  | ||||
| ``` | ||||
| $ USE_SSH=true yarn deploy | ||||
| ``` | ||||
|  | ||||
| Not using SSH: | ||||
|  | ||||
| ``` | ||||
| $ GIT_USER=<Your GitHub username> yarn deploy | ||||
| ``` | ||||
|  | ||||
| If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. | ||||
							
								
								
									
										3
									
								
								dev-docs/babel.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| module.exports = { | ||||
|   presets: [require.resolve("@docusaurus/core/lib/babel/preset")], | ||||
| }; | ||||
							
								
								
									
										6
									
								
								dev-docs/docs/codebase/overview.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| --- | ||||
| sidebar_position: 1 | ||||
| title: Overview | ||||
| --- | ||||
|  | ||||
| In development. For now, refer to [excalidraw Readme](https://github.com/excalidraw/excalidraw/blob/master/README.md). | ||||
							
								
								
									
										8
									
								
								dev-docs/docs/get-started.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | ||||
| --- | ||||
| sidebar_position: 1 | ||||
| title: Introduction | ||||
| --- | ||||
|  | ||||
| Want to integrate Excalidraw into your app? Head over to the [package docs](/docs/package/overview). | ||||
|  | ||||
| If you're looking into the Excalidraw codebase itself, start [here](/docs/codebase/overview). | ||||
							
								
								
									
										6
									
								
								dev-docs/docs/package/overview.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| --- | ||||
| sidebar_position: 1 | ||||
| title: Overview | ||||
| --- | ||||
|  | ||||
| In development. For now, refer to [excalidraw package readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md). | ||||
							
								
								
									
										121
									
								
								dev-docs/docusaurus.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,121 @@ | ||||
| // @ts-check | ||||
| // Note: type annotations allow type checking and IDEs autocompletion | ||||
|  | ||||
| const lightCodeTheme = require("prism-react-renderer/themes/github"); | ||||
| const darkCodeTheme = require("prism-react-renderer/themes/dracula"); | ||||
|  | ||||
| /** @type {import('@docusaurus/types').Config} */ | ||||
| const config = { | ||||
|   title: "Excalidraw developer docs", | ||||
|   tagline: | ||||
|     "For Excalidraw contributors or those integrating the Excalidraw editor", | ||||
|   url: "https://docs.excalidraw.com.com", | ||||
|   baseUrl: "/", | ||||
|   onBrokenLinks: "throw", | ||||
|   onBrokenMarkdownLinks: "warn", | ||||
|   favicon: "img/favicon.ico", | ||||
|   organizationName: "Excalidraw", // Usually your GitHub org/user name. | ||||
|   projectName: "excalidraw", // Usually your repo name. | ||||
|  | ||||
|   // Even if you don't use internalization, you can use this field to set useful | ||||
|   // metadata like html lang. For example, if your site is Chinese, you may want | ||||
|   // to replace "en" with "zh-Hans". | ||||
|   i18n: { | ||||
|     defaultLocale: "en", | ||||
|     locales: ["en"], | ||||
|   }, | ||||
|  | ||||
|   presets: [ | ||||
|     [ | ||||
|       "classic", | ||||
|       /** @type {import('@docusaurus/preset-classic').Options} */ | ||||
|       ({ | ||||
|         docs: { | ||||
|           sidebarPath: require.resolve("./sidebars.js"), | ||||
|           // Please change this to your repo. | ||||
|           editUrl: "https://github.com/excalidraw/docs/tree/master/", | ||||
|         }, | ||||
|         theme: { | ||||
|           customCss: require.resolve("./src/css/custom.css"), | ||||
|         }, | ||||
|       }), | ||||
|     ], | ||||
|   ], | ||||
|  | ||||
|   themeConfig: | ||||
|     /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ | ||||
|     ({ | ||||
|       navbar: { | ||||
|         title: "Excalidraw Docs", | ||||
|         logo: { | ||||
|           alt: "Excalidraw Logo", | ||||
|           src: "img/logo.svg", | ||||
|         }, | ||||
|         items: [ | ||||
|           { | ||||
|             type: "doc", | ||||
|             docId: "get-started", | ||||
|             position: "left", | ||||
|             label: "Get started", | ||||
|           }, | ||||
|           { | ||||
|             to: "https://blog.excalidraw.com", | ||||
|             label: "Blog", | ||||
|             position: "left", | ||||
|           }, | ||||
|           { | ||||
|             to: "https://github.com/excalidraw/excalidraw", | ||||
|             label: "GitHub", | ||||
|             position: "right", | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|       footer: { | ||||
|         style: "dark", | ||||
|         links: [ | ||||
|           { | ||||
|             title: "Docs", | ||||
|             items: [ | ||||
|               { | ||||
|                 label: "Get Started", | ||||
|                 to: "/docs/get-started", | ||||
|               }, | ||||
|             ], | ||||
|           }, | ||||
|           { | ||||
|             title: "Community", | ||||
|             items: [ | ||||
|               { | ||||
|                 label: "Discord", | ||||
|                 href: "https://discord.gg/UexuTaE", | ||||
|               }, | ||||
|               { | ||||
|                 label: "Twitter", | ||||
|                 href: "https://twitter.com/excalidraw", | ||||
|               }, | ||||
|             ], | ||||
|           }, | ||||
|           { | ||||
|             title: "More", | ||||
|             items: [ | ||||
|               { | ||||
|                 label: "Blog", | ||||
|                 to: "https://blog.excalidraw.com", | ||||
|               }, | ||||
|               { | ||||
|                 label: "GitHub", | ||||
|                 to: "https://github.com/excalidraw/excalidraw", | ||||
|               }, | ||||
|             ], | ||||
|           }, | ||||
|         ], | ||||
|         copyright: `Made with ❤️ Built with Docusaurus`, | ||||
|       }, | ||||
|       prism: { | ||||
|         theme: lightCodeTheme, | ||||
|         darkTheme: darkCodeTheme, | ||||
|       }, | ||||
|     }), | ||||
| }; | ||||
|  | ||||
| module.exports = config; | ||||
							
								
								
									
										46
									
								
								dev-docs/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,46 @@ | ||||
| { | ||||
|   "name": "docs", | ||||
|   "version": "0.0.0", | ||||
|   "private": true, | ||||
|   "scripts": { | ||||
|     "docusaurus": "docusaurus", | ||||
|     "start": "docusaurus start --port 3003", | ||||
|     "build": "docusaurus build", | ||||
|     "swizzle": "docusaurus swizzle", | ||||
|     "deploy": "docusaurus deploy", | ||||
|     "clear": "docusaurus clear", | ||||
|     "serve": "docusaurus serve", | ||||
|     "write-translations": "docusaurus write-translations", | ||||
|     "write-heading-ids": "docusaurus write-heading-ids", | ||||
|     "typecheck": "tsc" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@docusaurus/core": "2.0.0-rc.1", | ||||
|     "@docusaurus/preset-classic": "2.0.0-rc.1", | ||||
|     "@mdx-js/react": "^1.6.22", | ||||
|     "clsx": "^1.2.1", | ||||
|     "prism-react-renderer": "^1.3.5", | ||||
|     "react": "^17.0.2", | ||||
|     "react-dom": "^17.0.2" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@docusaurus/module-type-aliases": "2.0.0-rc.1", | ||||
|     "@tsconfig/docusaurus": "^1.0.5", | ||||
|     "typescript": "^4.7.4" | ||||
|   }, | ||||
|   "browserslist": { | ||||
|     "production": [ | ||||
|       ">0.5%", | ||||
|       "not dead", | ||||
|       "not op_mini all" | ||||
|     ], | ||||
|     "development": [ | ||||
|       "last 1 chrome version", | ||||
|       "last 1 firefox version", | ||||
|       "last 1 safari version" | ||||
|     ] | ||||
|   }, | ||||
|   "engines": { | ||||
|     "node": ">=16.14" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										31
									
								
								dev-docs/sidebars.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,31 @@ | ||||
| /** | ||||
|  * Creating a sidebar enables you to: | ||||
|  - create an ordered group of docs | ||||
|  - render a sidebar for each doc of that group | ||||
|  - provide next/previous navigation | ||||
|  | ||||
|  The sidebars can be generated from the filesystem, or explicitly defined here. | ||||
|  | ||||
|  Create as many sidebars as you want. | ||||
|  */ | ||||
|  | ||||
| // @ts-check | ||||
|  | ||||
| /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ | ||||
| const sidebars = { | ||||
|   // By default, Docusaurus generates a sidebar from the docs folder structure | ||||
|   tutorialSidebar: [{ type: "autogenerated", dirName: "." }], | ||||
|  | ||||
|   // But you can create a sidebar manually | ||||
|   /* | ||||
|   tutorialSidebar: [ | ||||
|     { | ||||
|       type: 'category', | ||||
|       label: 'Tutorial', | ||||
|       items: ['hello'], | ||||
|     }, | ||||
|   ], | ||||
|    */ | ||||
| }; | ||||
|  | ||||
| module.exports = sidebars; | ||||
							
								
								
									
										62
									
								
								dev-docs/src/components/Homepage/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,62 @@ | ||||
| import React from "react"; | ||||
| import clsx from "clsx"; | ||||
| import styles from "./styles.module.css"; | ||||
|  | ||||
| const FeatureList = [ | ||||
|   { | ||||
|     title: "Learn how Excalidraw works", | ||||
|     Svg: require("@site/static/img/undraw_innovative.svg").default, | ||||
|     description: ( | ||||
|       <>Want to contribute to Excalidraw but got lost in the codebase?</> | ||||
|     ), | ||||
|   }, | ||||
|   { | ||||
|     title: "Integrate Excalidraw", | ||||
|     Svg: require("@site/static/img/undraw_blank_canvas.svg").default, | ||||
|     description: ( | ||||
|       <> | ||||
|         Want to build your own app powered by Excalidraw by don't know where to | ||||
|         start? | ||||
|       </> | ||||
|     ), | ||||
|   }, | ||||
|   { | ||||
|     title: "Help us improve", | ||||
|     Svg: require("@site/static/img/undraw_add_files.svg").default, | ||||
|     description: ( | ||||
|       <> | ||||
|         Are the docs missing something? Anything you had trouble understanding | ||||
|         or needs an explanation? Come contribute to the docs to make them even | ||||
|         better! | ||||
|       </> | ||||
|     ), | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| function Feature({ Svg, title, description }) { | ||||
|   return ( | ||||
|     <div className={clsx("col col--4")}> | ||||
|       <div className="text--center"> | ||||
|         <Svg className={styles.featureSvg} role="img" /> | ||||
|       </div> | ||||
|       <div className="text--center padding-horiz--md"> | ||||
|         <h3>{title}</h3> | ||||
|         <p>{description}</p> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export default function HomepageFeatures() { | ||||
|   return ( | ||||
|     <section className={styles.features}> | ||||
|       <div className="container"> | ||||
|         <div className="row"> | ||||
|           {FeatureList.map((props, idx) => ( | ||||
|             <Feature key={idx} {...props} /> | ||||
|           ))} | ||||
|         </div> | ||||
|       </div> | ||||
|     </section> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										70
									
								
								dev-docs/src/components/Homepage/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,70 @@ | ||||
| import React from "react"; | ||||
| import clsx from "clsx"; | ||||
| import styles from "./styles.module.css"; | ||||
|  | ||||
| type FeatureItem = { | ||||
|   title: string; | ||||
|   Svg: React.ComponentType<React.ComponentProps<"svg">>; | ||||
|   description: JSX.Element; | ||||
| }; | ||||
|  | ||||
| const FeatureList: FeatureItem[] = [ | ||||
|   { | ||||
|     title: "Easy to Use", | ||||
|     Svg: require("@site/static/img/undraw_docusaurus_mountain.svg").default, | ||||
|     description: ( | ||||
|       <> | ||||
|         Docusaurus was designed from the ground up to be easily installed and | ||||
|         used to get your website up and running quickly. | ||||
|       </> | ||||
|     ), | ||||
|   }, | ||||
|   { | ||||
|     title: "Focus on What Matters", | ||||
|     Svg: require("@site/static/img/undraw_docusaurus_tree.svg").default, | ||||
|     description: ( | ||||
|       <> | ||||
|         Docusaurus lets you focus on your docs, and we'll do the chores. Go | ||||
|         ahead and move your docs into the <code>docs</code> directory. | ||||
|       </> | ||||
|     ), | ||||
|   }, | ||||
|   { | ||||
|     title: "Powered by React", | ||||
|     Svg: require("@site/static/img/undraw_docusaurus_react.svg").default, | ||||
|     description: ( | ||||
|       <> | ||||
|         Extend or customize your website layout by reusing React. Docusaurus can | ||||
|         be extended while reusing the same header and footer. | ||||
|       </> | ||||
|     ), | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| function Feature({ title, Svg, description }: FeatureItem) { | ||||
|   return ( | ||||
|     <div className={clsx("col col--4")}> | ||||
|       <div className="text--center"> | ||||
|         <Svg className={styles.featureSvg} role="img" /> | ||||
|       </div> | ||||
|       <div className="text--center padding-horiz--md"> | ||||
|         <h3>{title}</h3> | ||||
|         <p>{description}</p> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export default function HomepageFeatures(): JSX.Element { | ||||
|   return ( | ||||
|     <section className={styles.features}> | ||||
|       <div className="container"> | ||||
|         <div className="row"> | ||||
|           {FeatureList.map((props, idx) => ( | ||||
|             <Feature key={idx} {...props} /> | ||||
|           ))} | ||||
|         </div> | ||||
|       </div> | ||||
|     </section> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										11
									
								
								dev-docs/src/components/Homepage/styles.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| .features { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   padding: 2rem 0; | ||||
|   width: 100%; | ||||
| } | ||||
|  | ||||
| .featureSvg { | ||||
|   height: 200px; | ||||
|   width: 200px; | ||||
| } | ||||
							
								
								
									
										43
									
								
								dev-docs/src/css/custom.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,43 @@ | ||||
| /** | ||||
|  * Any CSS included here will be global. The classic template | ||||
|  * bundles Infima by default. Infima is a CSS framework designed to | ||||
|  * work well for content-centric websites. | ||||
|  */ | ||||
|  | ||||
| /* You can override the default Infima variables here. */ | ||||
| :root { | ||||
|   --ifm-color-primary: #6965db; | ||||
|   --ifm-color-primary-dark: #5b57d1; | ||||
|   --ifm-color-primary-darker: #5b57d1; | ||||
|   --ifm-color-primary-darkest: #4a47b1; | ||||
|   --ifm-color-primary-light: #5b57d1; | ||||
|   --ifm-color-primary-lighter: #5b57d1; | ||||
|   --ifm-color-primary-lightest: #5b57d1; | ||||
|   --ifm-code-font-size: 95%; | ||||
| } | ||||
|  | ||||
| /* For readability concerns, you should choose a lighter palette in dark mode. */ | ||||
| [data-theme="dark"] { | ||||
|   --ifm-color-primary: #5650f0; | ||||
|   --ifm-color-primary-dark: #4b46d8; | ||||
|   --ifm-color-primary-darker: #4b46d8; | ||||
|   --ifm-color-primary-darkest: #3e39be; | ||||
|   --ifm-color-primary-light: #3f3d64; | ||||
|   --ifm-color-primary-lighter: #3f3d64; | ||||
|   --ifm-color-primary-lightest: #3f3d64; | ||||
| } | ||||
|  | ||||
| .docusaurus-highlight-code-line { | ||||
|   background-color: rgba(0, 0, 0, 0.1); | ||||
|   display: block; | ||||
|   margin: 0 calc(-1 * var(--ifm-pre-padding)); | ||||
|   padding: 0 var(--ifm-pre-padding); | ||||
| } | ||||
|  | ||||
| [data-theme="dark"] .docusaurus-highlight-code-line { | ||||
|   background-color: rgba(0, 0, 0, 0.3); | ||||
| } | ||||
|  | ||||
| [data-theme="dark"] .navbar__logo { | ||||
|   filter: invert(93%) hue-rotate(180deg); | ||||
| } | ||||
							
								
								
									
										42
									
								
								dev-docs/src/pages/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,42 @@ | ||||
| import React from "react"; | ||||
| import clsx from "clsx"; | ||||
| import Layout from "@theme/Layout"; | ||||
| import Link from "@docusaurus/Link"; | ||||
| import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; | ||||
| import styles from "./index.module.css"; | ||||
| import HomepageFeatures from "@site/src/components/Homepage"; | ||||
|  | ||||
| function HomepageHeader() { | ||||
|   const { siteConfig } = useDocusaurusContext(); | ||||
|   return ( | ||||
|     <header className={clsx("hero hero--primary", styles.heroBanner)}> | ||||
|       <div className="container"> | ||||
|         <h1 className="hero__title">{siteConfig.title}</h1> | ||||
|         <p className="hero__subtitle">{siteConfig.tagline}</p> | ||||
|         <div className={styles.buttons}> | ||||
|           <Link | ||||
|             className="button button--secondary button--lg" | ||||
|             to="/docs/get-started" | ||||
|           > | ||||
|             Get started | ||||
|           </Link> | ||||
|         </div> | ||||
|       </div> | ||||
|     </header> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export default function Home() { | ||||
|   const { siteConfig } = useDocusaurusContext(); | ||||
|   return ( | ||||
|     <Layout | ||||
|       title={`Hello from ${siteConfig.title}`} | ||||
|       description="Description will go into a meta tag in <head />" | ||||
|     > | ||||
|       <HomepageHeader /> | ||||
|       <main> | ||||
|         <HomepageFeatures /> | ||||
|       </main> | ||||
|     </Layout> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										27
									
								
								dev-docs/src/pages/index.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,27 @@ | ||||
| /** | ||||
|  * CSS files with the .module.css suffix will be treated as CSS modules | ||||
|  * and scoped locally. | ||||
|  */ | ||||
|  | ||||
| .heroBanner { | ||||
|   padding: 4rem 0; | ||||
|   text-align: center; | ||||
|   position: relative; | ||||
|   overflow: hidden; | ||||
| } | ||||
|  | ||||
| [data-theme="dark"] .heroBanner { | ||||
|   color: #fff; | ||||
| } | ||||
|  | ||||
| @media screen and (max-width: 996px) { | ||||
|   .heroBanner { | ||||
|     padding: 2rem; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .buttons { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
| } | ||||
							
								
								
									
										42
									
								
								dev-docs/src/pages/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,42 @@ | ||||
| import React from "react"; | ||||
| import clsx from "clsx"; | ||||
| import Layout from "@theme/Layout"; | ||||
| import Link from "@docusaurus/Link"; | ||||
| import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; | ||||
| import styles from "./index.module.css"; | ||||
| import HomepageFeatures from "@site/src/components/Homepage"; | ||||
|  | ||||
| function HomepageHeader() { | ||||
|   const { siteConfig } = useDocusaurusContext(); | ||||
|   return ( | ||||
|     <header className={clsx("hero hero--primary", styles.heroBanner)}> | ||||
|       <div className="container"> | ||||
|         <h1 className="hero__title">{siteConfig.title}</h1> | ||||
|         <p className="hero__subtitle">{siteConfig.tagline}</p> | ||||
|         <div className={styles.buttons}> | ||||
|           <Link | ||||
|             className="button button--secondary button--lg" | ||||
|             to="/docs/get-started" | ||||
|           > | ||||
|             Get started | ||||
|           </Link> | ||||
|         </div> | ||||
|       </div> | ||||
|     </header> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export default function Home() { | ||||
|   const { siteConfig } = useDocusaurusContext(); | ||||
|   return ( | ||||
|     <Layout | ||||
|       title={`Hello from ${siteConfig.title}`} | ||||
|       description="Description will go into a meta tag in <head />" | ||||
|     > | ||||
|       <HomepageHeader /> | ||||
|       <main> | ||||
|         <HomepageFeatures /> | ||||
|       </main> | ||||
|     </Layout> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										7
									
								
								dev-docs/src/pages/markdown-page.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | ||||
| --- | ||||
| title: Markdown page example | ||||
| --- | ||||
|  | ||||
| # Markdown page example | ||||
|  | ||||
| You don't need React to write simple standalone pages. | ||||
							
								
								
									
										0
									
								
								dev-docs/static/.nojekyll
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								dev-docs/static/img/docusaurus.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 5.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								dev-docs/static/img/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 26 KiB | 
							
								
								
									
										4
									
								
								dev-docs/static/img/logo.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| <svg viewBox="0 0 80 180" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"> | ||||
| 	<path d="M22.197 150.382c-4.179-3.359-10.618-9.051-15.702-13.946l-4.01-3.813.734-5.009c.396-2.732 1.13-8.083 1.582-11.839.508-3.757 1.017-7.286 1.186-7.798.226-.683 0-1.025-.621-1.025-1.073 0-1.13.285 1.807-9.107a617.602 617.602 0 0 1 2.203-7.229c.113-.398.565-.569 1.073-.398.508.227.791.683.621 1.081-.169.455.113.911.565 1.082.621.227.565.683-.395 2.333-1.525 2.562-5.422 24.419-5.648 31.477-.17 5.009-.17 5.066 1.92 7.912 2.033 2.789 6.721 7.001 13.951 12.351 2.033 1.537 4.067 3.245 4.631 3.814.848 1.024 1.243.74 8.36-6.887 4.123-4.383 8.698-8.88 10.166-10.018l2.711-2.049-2.089-4.44c-1.13-2.391-5.705-11.612-10.223-20.377-9.433-18.442-7.513-16.678-18.47-16.849l-7.117-.056-2.372-2.733c-2.485-2.903-2.824-3.984-1.638-5.805.452-.627.791-1.651.791-2.277 0-1.025.395-1.196 2.655-1.309 1.412-.057 2.711-.228 2.88-.399.17-.171.396-3.7.565-7.855l.226-7.513-3.784-8.197C2.485 39.844 0 33.583 0 31.533c0-1.081.226-1.992.452-1.992.565 0 .565.057 23.553 48.382 10.675 22.426 20.785 43.544 22.479 47.016 1.695 3.472 3.22 6.659 3.333 7.115.113.512-3.785 4.439-9.998 9.961-5.591 5.008-10.505 9.562-10.957 10.074-1.299 1.594-3.219 1.082-6.665-1.707Zm1.921-65.458c-2.599-5.066-2.712-5.123-9.828-5.464-6.27-.342-6.383-.285-6.383.911 0 .683-.226 1.593-.508 2.049-.339.512-.113 1.423.678 2.675l1.242 1.935h5.649c3.106.057 6.664.285 7.907.512 1.243.228 2.316.342 2.429.285.113-.057-.452-1.366-1.186-2.903Zm-4.745-9.107c-.452-1.195-1.638-3.7-2.598-5.578-1.581-3.188-1.751-3.301-2.146-1.992-.226.797-.396 3.13-.452 5.236-.057 4.155-.17 4.098 4.575 4.383l1.525.057-.904-2.106Z" style="fill-rule:nonzero;stroke:#000;stroke-width:2px" transform="matrix(1.01351 0 0 -1 9.088 166.517)" /> | ||||
| 	<path d="M23.892 136.835c-1.017-.74-1.299-1.48-1.299-3.358 0-2.22.169-2.562 1.694-3.188 1.525-.626 1.92-.569 3.671.626 2.316 1.594 2.373 1.992.678 4.554-1.468 2.22-2.937 2.618-4.744 1.366Zm3.219-2.049c.904-1.594.339-2.789-1.355-2.789-1.525 0-2.203 1.536-1.356 3.073.678 1.253 1.977 1.139 2.711-.284ZM59.306 124.028c0-.285-.339-.569-.735-.569-.339 0-1.299-1.594-2.033-3.529-2.259-5.92-24.852-50.943-24.908-49.52 0 .74-.339 1.252-.904 1.252-.791 0-.904-.456-.565-2.675.339-2.562.113-3.131-7.907-18.841-4.519-8.936-9.376-18.271-10.788-20.775-1.469-2.619-2.598-5.465-2.711-6.66-.17-2.049.056-2.334 4.97-6.603 2.824-2.504 6.439-5.635 8.02-7.058C28.862 2.504 32.194-.114 33.098.057c1.356.228 22.31 22.369 22.367 23.622 0 .569-1.017 9.221-2.259 19.238-2.147 17.076-4.18 37.055-3.954 38.99.169 1.196-.678 7.229-1.299 9.847-.509 2.05-.283 2.903 3.784 12.238 2.372 5.521 5.479 12.295 6.834 15.027 1.299 2.732 2.429 5.123 2.429 5.294 0 .17-.395.284-.847.284-.452 0-.847-.228-.847-.569ZM46.315 81.509c.621-3.984 1.864-13.547 2.767-21.231 1.751-14.116 3.785-29.769 4.349-33.753.339-1.993.113-2.391-3.558-6.489-6.382-7.229-13.16-14.344-15.476-16.165l-2.146-1.708-11.014 10.359C11.07 21.971 10.223 22.939 10.844 24.077c.339.626 3.22 5.92 6.383 11.725 3.163 5.806 7.342 13.547 9.263 17.19 1.977 3.7 3.784 6.887 4.123 7.058.395.228.508-5.521.395-17.759-.226-18.271-.169-18.328 1.638-17.929.226 0 .396 9.221.396 20.434v20.377l5.93 11.953c3.276 6.603 5.987 11.896 6.1 11.84.113-.058.678-3.416 1.243-7.457Z" style="fill-rule:nonzero;stroke:#000;stroke-width:2px" transform="matrix(1.01351 0 0 -1 9.088 166.517)" /> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 3.4 KiB | 
							
								
								
									
										1
									
								
								dev-docs/static/img/undraw_add_files.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 5.7 KiB | 
							
								
								
									
										1
									
								
								dev-docs/static/img/undraw_blank_canvas.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 12 KiB | 
							
								
								
									
										1
									
								
								dev-docs/static/img/undraw_innovative.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 5.4 KiB | 
							
								
								
									
										7
									
								
								dev-docs/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | ||||
| { | ||||
|   // This file is not used in compilation. It is here just for a nice editor experience. | ||||
|   "extends": "@tsconfig/docusaurus/tsconfig.json", | ||||
|   "compilerOptions": { | ||||
|     "baseUrl": "." | ||||
|   } | ||||
| } | ||||
							
								
								
									
										7489
									
								
								dev-docs/yarn.lock
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										14
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -26,8 +26,8 @@ | ||||
|     "@tldraw/vec": "1.7.1", | ||||
|     "@types/jest": "27.4.0", | ||||
|     "@types/pica": "5.1.3", | ||||
|     "@types/react": "17.0.39", | ||||
|     "@types/react-dom": "17.0.11", | ||||
|     "@types/react": "18.0.15", | ||||
|     "@types/react-dom": "18.0.6", | ||||
|     "@types/socket.io-client": "1.4.36", | ||||
|     "browser-fs-access": "0.29.1", | ||||
|     "clsx": "1.1.1", | ||||
| @@ -41,14 +41,15 @@ | ||||
|     "nanoid": "3.3.3", | ||||
|     "open-color": "1.9.1", | ||||
|     "pako": "1.0.11", | ||||
|     "perfect-freehand": "1.0.16", | ||||
|     "perfect-freehand": "1.2.0", | ||||
|     "pica": "7.1.1", | ||||
|     "png-chunk-text": "1.0.0", | ||||
|     "png-chunks-encode": "1.0.0", | ||||
|     "png-chunks-extract": "1.0.0", | ||||
|     "points-on-curve": "0.2.0", | ||||
|     "pwacompat": "2.0.17", | ||||
|     "react": "17.0.2", | ||||
|     "react-dom": "17.0.2", | ||||
|     "react": "18.2.0", | ||||
|     "react-dom": "18.2.0", | ||||
|     "react-scripts": "4.0.3", | ||||
|     "roughjs": "4.5.2", | ||||
|     "sass": "1.51.0", | ||||
| @@ -73,9 +74,6 @@ | ||||
|     "prettier": "2.6.2", | ||||
|     "rewire": "6.0.0" | ||||
|   }, | ||||
|   "resolutions": { | ||||
|     "@typescript-eslint/typescript-estree": "5.10.2" | ||||
|   }, | ||||
|   "engines": { | ||||
|     "node": ">=14.0.0" | ||||
|   }, | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								public/Assistant-Bold.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/Assistant-Medium.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/Assistant-Regular.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/Assistant-SemiBold.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -11,3 +11,28 @@ | ||||
|   src: url("Cascadia.woff2"); | ||||
|   font-display: swap; | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
|   font-family: "Assistant"; | ||||
|   src: url("Assistant-Regular.woff2"); | ||||
|   font-display: swap; | ||||
|   font-weight: 400; | ||||
| } | ||||
| @font-face { | ||||
|   font-family: "Assistant"; | ||||
|   src: url("Assistant-Medium.woff2"); | ||||
|   font-display: swap; | ||||
|   font-weight: 500; | ||||
| } | ||||
| @font-face { | ||||
|   font-family: "Assistant"; | ||||
|   src: url("Assistant-SemiBold.woff2"); | ||||
|   font-display: swap; | ||||
|   font-weight: 600; | ||||
| } | ||||
| @font-face { | ||||
|   font-family: "Assistant"; | ||||
|   src: url("Assistant-Bold.woff2"); | ||||
|   font-display: swap; | ||||
|   font-weight: 700; | ||||
| } | ||||
|   | ||||
| @@ -8,49 +8,76 @@ | ||||
|       content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no" | ||||
|     /> | ||||
|     <meta name="referrer" content="origin" /> | ||||
|  | ||||
|     <meta name="mobile-web-app-capable" content="yes" /> | ||||
|     <meta name="theme-color" content="#121212" /> | ||||
|  | ||||
|     <meta name="theme-color" content="#000" /> | ||||
|     <!-- Primary Meta Tags --> | ||||
|     <meta | ||||
|       name="title" | ||||
|       content="Excalidraw — Collaborative whiteboarding made easy" | ||||
|     /> | ||||
|     <meta | ||||
|       name="description" | ||||
|       content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them." | ||||
|     /> | ||||
|     <meta name="image" content="https://excalidraw.com/og-general-v1.png" /> | ||||
|  | ||||
|     <!-- Open Graph / Facebook --> | ||||
|     <meta property="og:site_name" content="Excalidraw" /> | ||||
|     <meta property="og:type" content="website" /> | ||||
|     <meta property="og:url" content="https://excalidraw.com" /> | ||||
|     <meta | ||||
|       property="og:title" | ||||
|       content="Excalidraw — Collaborative whiteboarding made easy" | ||||
|     /> | ||||
|     <meta property="og:image:alt" content="Excalidraw logo" /> | ||||
|     <meta | ||||
|       property="og:description" | ||||
|       content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them." | ||||
|     /> | ||||
|     <meta property="og:image" content="https://excalidraw.com/og-fb-v1.png" /> | ||||
|  | ||||
|     <!-- Twitter --> | ||||
|     <meta property="twitter:card" content="summary_large_image" /> | ||||
|     <meta property="twitter:site" content="@excalidraw" /> | ||||
|     <meta property="twitter:url" content="https://excalidraw.com" /> | ||||
|     <meta | ||||
|       property="twitter:title" | ||||
|       content="Excalidraw — Collaborative whiteboarding made easy" | ||||
|     /> | ||||
|     <meta | ||||
|       property="twitter:description" | ||||
|       content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them." | ||||
|     /> | ||||
|     <meta | ||||
|       property="twitter:image" | ||||
|       content="https://excalidraw.com/og-twitter-v1.png" | ||||
|     /> | ||||
|  | ||||
|     <!-- General tags --> | ||||
|     <meta | ||||
|       name="description" | ||||
|       content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them." | ||||
|     /> | ||||
|     <meta name="image" content="og-image.png" /> | ||||
|  | ||||
|     <!-- OpenGraph tags --> | ||||
|     <meta property="og:url" content="https://excalidraw.com" /> | ||||
|     <meta property="og:site_name" content="Excalidraw" /> | ||||
|     <meta property="og:type" content="website" /> | ||||
|     <meta property="og:title" content="Excalidraw" /> | ||||
|     <meta | ||||
|       property="og:description" | ||||
|       content="Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them." | ||||
|     /> | ||||
|     <!-- OG tags require an absolute url for images --> | ||||
|     <meta | ||||
|       property="og:image" | ||||
|       name="twitter:image" | ||||
|       content="https://excalidraw.com/og-image.png" | ||||
|     /> | ||||
|     <meta | ||||
|       property="og:image:secure_url" | ||||
|       name="twitter:image" | ||||
|       content="https://excalidraw.com/og-image.png" | ||||
|     /> | ||||
|     <meta property="og:image:width" content="1280" /> | ||||
|     <meta property="og:image:height" content="669" /> | ||||
|     <meta property="og:image:alt" content="Excalidraw logo with byline." /> | ||||
|  | ||||
|     <!-- Twitter Card tags --> | ||||
|     <meta name="twitter:card" content="summary_large_image" /> | ||||
|     <meta name="twitter:title" content="Excalidraw" /> | ||||
|     <meta | ||||
|       name="twitter:description" | ||||
|       content="Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them." | ||||
|     /> | ||||
|     <!-------------------------------------------------------------------------> | ||||
|     <!--   to minimize white flash on load when user has dark mode enabled   --> | ||||
|     <script> | ||||
|       try { | ||||
|         // | ||||
|         const theme = window.localStorage.getItem("excalidraw-theme"); | ||||
|         if (theme === "dark") { | ||||
|           document.documentElement.classList.add("dark"); | ||||
|         } | ||||
|       } catch {} | ||||
|     </script> | ||||
|     <style> | ||||
|       html.dark { | ||||
|         background-color: #121212; | ||||
|         color: #fff; | ||||
|       } | ||||
|     </style> | ||||
|     <!-------------------------------------------------------------------------> | ||||
|  | ||||
|     <script> | ||||
|       // Redirect Excalidraw+ users which have auto-redirect enabled. | ||||
| @@ -98,7 +125,7 @@ | ||||
|     /> | ||||
|  | ||||
|     <link rel="stylesheet" href="fonts.css" type="text/css" /> | ||||
|     <% if (process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD === "true") { %> | ||||
|     <% if (process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD==="true" ) { %> | ||||
|     <script> | ||||
|       { | ||||
|         const _WebSocket = window.WebSocket; | ||||
| @@ -139,8 +166,8 @@ | ||||
|       body, | ||||
|       html { | ||||
|         margin: 0; | ||||
|         --ui-font: system-ui, BlinkMacSystemFont, -apple-system, Segoe UI, | ||||
|           Roboto, Helvetica, Arial, sans-serif; | ||||
|         --ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system, | ||||
|           Segoe UI, Roboto, Helvetica, Arial, sans-serif; | ||||
|         font-family: var(--ui-font); | ||||
|         -webkit-text-size-adjust: 100%; | ||||
|  | ||||
| @@ -155,7 +182,7 @@ | ||||
|         width: 1px; | ||||
|         overflow: hidden; | ||||
|         clip: rect(1px, 1px, 1px, 1px); | ||||
|         white-space: nowrap; /* added line */ | ||||
|         white-space: nowrap; | ||||
|         user-select: none; | ||||
|       } | ||||
|  | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								public/og-fb-v1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 26 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/og-general-v1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 26 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/og-twitter-v1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 27 KiB | 
| @@ -1,3 +1,9 @@ | ||||
| User-agent: Twitterbot | ||||
| Disallow: | ||||
|  | ||||
| User-agent: facebookexternalhit | ||||
| Disallow: | ||||
|  | ||||
| user-agent: * | ||||
| Allow: /$ | ||||
| Disallow: / | ||||
|   | ||||
							
								
								
									
										21
									
								
								scripts/buildDocs.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| const { exec } = require("child_process"); | ||||
|  | ||||
| // get files changed between prev and head commit | ||||
| exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => { | ||||
|   if (error || stderr) { | ||||
|     console.error(error); | ||||
|     process.exit(1); | ||||
|   } | ||||
|   const changedFiles = stdout.trim().split("\n"); | ||||
|  | ||||
|   const docFiles = changedFiles.filter((file) => { | ||||
|     return file.indexOf("docs") >= 0; | ||||
|   }); | ||||
|  | ||||
|   if (!docFiles.length) { | ||||
|     console.info("Skipping building docs as no valid diff found"); | ||||
|     process.exit(0); | ||||
|   } | ||||
|   // Exit code 1 to build the docs in ignoredBuildStep | ||||
|   process.exit(1); | ||||
| }); | ||||
| @@ -15,6 +15,7 @@ const crowdinMap = { | ||||
|   "fa-IR": "en-fa", | ||||
|   "fi-FI": "en-fi", | ||||
|   "fr-FR": "en-fr", | ||||
|   "gl-ES": "en-gl", | ||||
|   "he-IL": "en-he", | ||||
|   "hi-IN": "en-hi", | ||||
|   "hu-HU": "en-hu", | ||||
| @@ -23,6 +24,7 @@ const crowdinMap = { | ||||
|   "ja-JP": "en-ja", | ||||
|   "kab-KAB": "en-kab", | ||||
|   "ko-KR": "en-ko", | ||||
|   "ku-TR": "en-ku", | ||||
|   "my-MM": "en-my", | ||||
|   "nb-NO": "en-nb", | ||||
|   "nl-NL": "en-nl", | ||||
| @@ -65,6 +67,7 @@ const flags = { | ||||
|   "fa-IR": "🇮🇷", | ||||
|   "fi-FI": "🇫🇮", | ||||
|   "fr-FR": "🇫🇷", | ||||
|   "gl-ES": "🇪🇸", | ||||
|   "he-IL": "🇮🇱", | ||||
|   "hi-IN": "🇮🇳", | ||||
|   "hu-HU": "🇭🇺", | ||||
| @@ -74,6 +77,7 @@ const flags = { | ||||
|   "kab-KAB": "🏳", | ||||
|   "kk-KZ": "🇰🇿", | ||||
|   "ko-KR": "🇰🇷", | ||||
|   "ku-TR": "🏳", | ||||
|   "lt-LT": "🇱🇹", | ||||
|   "lv-LV": "🇱🇻", | ||||
|   "my-MM": "🇲🇲", | ||||
|   | ||||
| @@ -1,11 +1,12 @@ | ||||
| const fs = require("fs"); | ||||
| const path = require("path"); | ||||
|  | ||||
| // for development purposes we want to have the service-worker.js file | ||||
| // accessible from the public folder. On build though, we need to compile it | ||||
| // and CRA expects that file to be in src/ folder. | ||||
| const moveServiceWorkerScript = () => { | ||||
|   const oldPath = "./public/service-worker.js"; | ||||
|   const newPath = "./src/service-worker.js"; | ||||
|   const oldPath = path.resolve(__dirname, "../public/service-worker.js"); | ||||
|   const newPath = path.resolve(__dirname, "../src/service-worker.js"); | ||||
|  | ||||
|   fs.rename(oldPath, newPath, (error) => { | ||||
|     if (error) { | ||||
|   | ||||
| @@ -60,7 +60,7 @@ export const actionAlignTop = register({ | ||||
|     <ToolButton | ||||
|       hidden={!enableActionGroup(elements, appState)} | ||||
|       type="button" | ||||
|       icon={<AlignTopIcon theme={appState.theme} />} | ||||
|       icon={AlignTopIcon} | ||||
|       onClick={() => updateData(null)} | ||||
|       title={`${t("labels.alignTop")} — ${getShortcutKey( | ||||
|         "CtrlOrCmd+Shift+Up", | ||||
| @@ -90,7 +90,7 @@ export const actionAlignBottom = register({ | ||||
|     <ToolButton | ||||
|       hidden={!enableActionGroup(elements, appState)} | ||||
|       type="button" | ||||
|       icon={<AlignBottomIcon theme={appState.theme} />} | ||||
|       icon={AlignBottomIcon} | ||||
|       onClick={() => updateData(null)} | ||||
|       title={`${t("labels.alignBottom")} — ${getShortcutKey( | ||||
|         "CtrlOrCmd+Shift+Down", | ||||
| @@ -120,7 +120,7 @@ export const actionAlignLeft = register({ | ||||
|     <ToolButton | ||||
|       hidden={!enableActionGroup(elements, appState)} | ||||
|       type="button" | ||||
|       icon={<AlignLeftIcon theme={appState.theme} />} | ||||
|       icon={AlignLeftIcon} | ||||
|       onClick={() => updateData(null)} | ||||
|       title={`${t("labels.alignLeft")} — ${getShortcutKey( | ||||
|         "CtrlOrCmd+Shift+Left", | ||||
| @@ -151,7 +151,7 @@ export const actionAlignRight = register({ | ||||
|     <ToolButton | ||||
|       hidden={!enableActionGroup(elements, appState)} | ||||
|       type="button" | ||||
|       icon={<AlignRightIcon theme={appState.theme} />} | ||||
|       icon={AlignRightIcon} | ||||
|       onClick={() => updateData(null)} | ||||
|       title={`${t("labels.alignRight")} — ${getShortcutKey( | ||||
|         "CtrlOrCmd+Shift+Right", | ||||
| @@ -180,7 +180,7 @@ export const actionAlignVerticallyCentered = register({ | ||||
|     <ToolButton | ||||
|       hidden={!enableActionGroup(elements, appState)} | ||||
|       type="button" | ||||
|       icon={<CenterVerticallyIcon theme={appState.theme} />} | ||||
|       icon={CenterVerticallyIcon} | ||||
|       onClick={() => updateData(null)} | ||||
|       title={t("labels.centerVertically")} | ||||
|       aria-label={t("labels.centerVertically")} | ||||
| @@ -206,7 +206,7 @@ export const actionAlignHorizontallyCentered = register({ | ||||
|     <ToolButton | ||||
|       hidden={!enableActionGroup(elements, appState)} | ||||
|       type="button" | ||||
|       icon={<CenterHorizontallyIcon theme={appState.theme} />} | ||||
|       icon={CenterHorizontallyIcon} | ||||
|       onClick={() => updateData(null)} | ||||
|       title={t("labels.centerHorizontally")} | ||||
|       aria-label={t("labels.centerHorizontally")} | ||||
|   | ||||
| @@ -1,8 +1,13 @@ | ||||
| import { ColorPicker } from "../components/ColorPicker"; | ||||
| import { eraser, zoomIn, zoomOut } from "../components/icons"; | ||||
| import { | ||||
|   eraser, | ||||
|   MoonIcon, | ||||
|   SunIcon, | ||||
|   ZoomInIcon, | ||||
|   ZoomOutIcon, | ||||
| } from "../components/icons"; | ||||
| import { ToolButton } from "../components/ToolButton"; | ||||
| import { DarkModeToggle } from "../components/DarkModeToggle"; | ||||
| import { THEME, ZOOM_STEP } from "../constants"; | ||||
| import { MIN_ZOOM, THEME, ZOOM_STEP } from "../constants"; | ||||
| import { getCommonBounds, getNonDeletedElements } from "../element"; | ||||
| import { ExcalidrawElement } from "../element/types"; | ||||
| import { t } from "../i18n"; | ||||
| @@ -18,6 +23,8 @@ import { newElementWith } from "../element/mutateElement"; | ||||
| import { getDefaultAppState, isEraserActive } from "../appState"; | ||||
| import ClearCanvas from "../components/ClearCanvas"; | ||||
| import clsx from "clsx"; | ||||
| import MenuItem from "../components/MenuItem"; | ||||
| import { getShortcutFromShortcutName } from "./shortcuts"; | ||||
|  | ||||
| export const actionChangeViewBackgroundColor = register({ | ||||
|   name: "changeViewBackgroundColor", | ||||
| @@ -103,13 +110,13 @@ export const actionZoomIn = register({ | ||||
|   PanelComponent: ({ updateData }) => ( | ||||
|     <ToolButton | ||||
|       type="button" | ||||
|       icon={zoomIn} | ||||
|       className="zoom-in-button zoom-button" | ||||
|       icon={ZoomInIcon} | ||||
|       title={`${t("buttons.zoomIn")} — ${getShortcutKey("CtrlOrCmd++")}`} | ||||
|       aria-label={t("buttons.zoomIn")} | ||||
|       onClick={() => { | ||||
|         updateData(null); | ||||
|       }} | ||||
|       size="small" | ||||
|     /> | ||||
|   ), | ||||
|   keyTest: (event) => | ||||
| @@ -139,13 +146,13 @@ export const actionZoomOut = register({ | ||||
|   PanelComponent: ({ updateData }) => ( | ||||
|     <ToolButton | ||||
|       type="button" | ||||
|       icon={zoomOut} | ||||
|       className="zoom-out-button zoom-button" | ||||
|       icon={ZoomOutIcon} | ||||
|       title={`${t("buttons.zoomOut")} — ${getShortcutKey("CtrlOrCmd+-")}`} | ||||
|       aria-label={t("buttons.zoomOut")} | ||||
|       onClick={() => { | ||||
|         updateData(null); | ||||
|       }} | ||||
|       size="small" | ||||
|     /> | ||||
|   ), | ||||
|   keyTest: (event) => | ||||
| @@ -176,13 +183,12 @@ export const actionResetZoom = register({ | ||||
|     <Tooltip label={t("buttons.resetZoom")} style={{ height: "100%" }}> | ||||
|       <ToolButton | ||||
|         type="button" | ||||
|         className="reset-zoom-button" | ||||
|         className="reset-zoom-button zoom-button" | ||||
|         title={t("buttons.resetZoom")} | ||||
|         aria-label={t("buttons.resetZoom")} | ||||
|         onClick={() => { | ||||
|           updateData(null); | ||||
|         }} | ||||
|         size="small" | ||||
|       > | ||||
|         {(appState.zoom.value * 100).toFixed(0)}% | ||||
|       </ToolButton> | ||||
| @@ -206,7 +212,7 @@ const zoomValueToFitBoundsOnViewport = ( | ||||
|   const zoomAdjustedToSteps = | ||||
|     Math.floor(smallestZoomValue / ZOOM_STEP) * ZOOM_STEP; | ||||
|   const clampedZoomValueToFitElements = Math.min( | ||||
|     Math.max(zoomAdjustedToSteps, ZOOM_STEP), | ||||
|     Math.max(zoomAdjustedToSteps, MIN_ZOOM), | ||||
|     1, | ||||
|   ); | ||||
|   return clampedZoomValueToFitElements as NormalizedZoomValue; | ||||
| @@ -288,14 +294,19 @@ export const actionToggleTheme = register({ | ||||
|     }; | ||||
|   }, | ||||
|   PanelComponent: ({ appState, updateData }) => ( | ||||
|     <div style={{ marginInlineStart: "0.25rem" }}> | ||||
|       <DarkModeToggle | ||||
|         value={appState.theme} | ||||
|         onChange={(theme) => { | ||||
|           updateData(theme); | ||||
|         }} | ||||
|       /> | ||||
|     </div> | ||||
|     <MenuItem | ||||
|       label={ | ||||
|         appState.theme === "dark" | ||||
|           ? t("buttons.lightMode") | ||||
|           : t("buttons.darkMode") | ||||
|       } | ||||
|       onClick={() => { | ||||
|         updateData(appState.theme === THEME.LIGHT ? THEME.DARK : THEME.LIGHT); | ||||
|       }} | ||||
|       icon={appState.theme === "dark" ? SunIcon : MoonIcon} | ||||
|       dataTestId="toggle-dark-mode" | ||||
|       shortcut={getShortcutFromShortcutName("toggleTheme")} | ||||
|     /> | ||||
|   ), | ||||
|   keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D, | ||||
| }); | ||||
|   | ||||
| @@ -36,7 +36,7 @@ export const actionCut = register({ | ||||
|     return actionDeleteSelected.perform(elements, appState); | ||||
|   }, | ||||
|   contextItemLabel: "labels.cut", | ||||
|   keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.X, | ||||
|   keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.X, | ||||
| }); | ||||
|  | ||||
| export const actionCopyAsSvg = register({ | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import { isSomeElementSelected } from "../scene"; | ||||
| import { KEYS } from "../keys"; | ||||
| import { ToolButton } from "../components/ToolButton"; | ||||
| import { trash } from "../components/icons"; | ||||
| import { t } from "../i18n"; | ||||
| import { register } from "./register"; | ||||
| import { getNonDeletedElements } from "../element"; | ||||
| @@ -13,6 +12,7 @@ import { LinearElementEditor } from "../element/linearElementEditor"; | ||||
| import { fixBindingsAfterDeletion } from "../element/binding"; | ||||
| import { isBoundToContainer } from "../element/typeChecks"; | ||||
| import { updateActiveTool } from "../utils"; | ||||
| import { TrashIcon } from "../components/icons"; | ||||
|  | ||||
| const deleteSelectedElements = ( | ||||
|   elements: readonly ExcalidrawElement[], | ||||
| @@ -72,13 +72,22 @@ export const actionDeleteSelected = register({ | ||||
|       if (!element) { | ||||
|         return false; | ||||
|       } | ||||
|       if ( | ||||
|         // case: no point selected → delete whole element | ||||
|         selectedPointsIndices == null || | ||||
|         // case: deleting last remaining point | ||||
|         element.points.length < 2 | ||||
|       ) { | ||||
|         const nextElements = elements.filter((el) => el.id !== element.id); | ||||
|       // case: no point selected → do nothing, as deleting the whole element | ||||
|       // is most likely a mistake, where you wanted to delete a specific point | ||||
|       // but failed to select it (or you thought it's selected, while it was | ||||
|       // only in a hover state) | ||||
|       if (selectedPointsIndices == null) { | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|       // case: deleting last remaining point | ||||
|       if (element.points.length < 2) { | ||||
|         const nextElements = elements.map((el) => { | ||||
|           if (el.id === element.id) { | ||||
|             return newElementWith(el, { isDeleted: true }); | ||||
|           } | ||||
|           return el; | ||||
|         }); | ||||
|         const nextAppState = handleGroupEditingState(appState, nextElements); | ||||
|  | ||||
|         return { | ||||
| @@ -149,7 +158,7 @@ export const actionDeleteSelected = register({ | ||||
|   PanelComponent: ({ elements, appState, updateData }) => ( | ||||
|     <ToolButton | ||||
|       type="button" | ||||
|       icon={trash} | ||||
|       icon={TrashIcon} | ||||
|       title={t("labels.delete")} | ||||
|       aria-label={t("labels.delete")} | ||||
|       onClick={() => updateData(null)} | ||||
|   | ||||
| @@ -56,7 +56,7 @@ export const distributeHorizontally = register({ | ||||
|     <ToolButton | ||||
|       hidden={!enableActionGroup(elements, appState)} | ||||
|       type="button" | ||||
|       icon={<DistributeHorizontallyIcon theme={appState.theme} />} | ||||
|       icon={DistributeHorizontallyIcon} | ||||
|       onClick={() => updateData(null)} | ||||
|       title={`${t("labels.distributeHorizontally")} — ${getShortcutKey( | ||||
|         "Alt+H", | ||||
| @@ -86,7 +86,7 @@ export const distributeVertically = register({ | ||||
|     <ToolButton | ||||
|       hidden={!enableActionGroup(elements, appState)} | ||||
|       type="button" | ||||
|       icon={<DistributeVerticallyIcon theme={appState.theme} />} | ||||
|       icon={DistributeVerticallyIcon} | ||||
|       onClick={() => updateData(null)} | ||||
|       title={`${t("labels.distributeVertically")} — ${getShortcutKey("Alt+V")}`} | ||||
|       aria-label={t("labels.distributeVertically")} | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import { ExcalidrawElement } from "../element/types"; | ||||
| import { duplicateElement, getNonDeletedElements } from "../element"; | ||||
| import { getSelectedElements, isSomeElementSelected } from "../scene"; | ||||
| import { ToolButton } from "../components/ToolButton"; | ||||
| import { clone } from "../components/icons"; | ||||
| import { t } from "../i18n"; | ||||
| import { arrayToMap, getShortcutKey } from "../utils"; | ||||
| import { LinearElementEditor } from "../element/linearElementEditor"; | ||||
| @@ -19,6 +18,7 @@ import { ActionResult } from "./types"; | ||||
| import { GRID_SIZE } from "../constants"; | ||||
| import { bindTextToShapeAfterDuplication } from "../element/textElement"; | ||||
| import { isBoundToContainer } from "../element/typeChecks"; | ||||
| import { DuplicateIcon } from "../components/icons"; | ||||
|  | ||||
| export const actionDuplicateSelection = register({ | ||||
|   name: "duplicateSelection", | ||||
| @@ -49,7 +49,7 @@ export const actionDuplicateSelection = register({ | ||||
|   PanelComponent: ({ elements, appState, updateData }) => ( | ||||
|     <ToolButton | ||||
|       type="button" | ||||
|       icon={clone} | ||||
|       icon={DuplicateIcon} | ||||
|       title={`${t("labels.duplicateSelection")} — ${getShortcutKey( | ||||
|         "CtrlOrCmd+D", | ||||
|       )}`} | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { load, questionCircle, saveAs } from "../components/icons"; | ||||
| import { LoadIcon, questionCircle, saveAs } from "../components/icons"; | ||||
| import { ProjectName } from "../components/ProjectName"; | ||||
| import { ToolButton } from "../components/ToolButton"; | ||||
| import "../components/ToolIcon.scss"; | ||||
| @@ -19,6 +19,8 @@ import { ActiveFile } from "../components/ActiveFile"; | ||||
| import { isImageFileHandle } from "../data/blob"; | ||||
| import { nativeFileSystemSupported } from "../data/filesystem"; | ||||
| import { Theme } from "../element/types"; | ||||
| import MenuItem from "../components/MenuItem"; | ||||
| import { getShortcutFromShortcutName } from "./shortcuts"; | ||||
|  | ||||
| export const actionChangeProjectName = register({ | ||||
|   name: "changeProjectName", | ||||
| @@ -87,6 +89,28 @@ export const actionChangeExportScale = register({ | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| export const actionChangeExportPadding = register({ | ||||
|   name: "changeExportPadding", | ||||
|   trackEvent: { category: "export", action: "togglePadding" }, | ||||
|   perform: (_elements, appState, value) => { | ||||
|     return { | ||||
|       appState: { | ||||
|         ...appState, | ||||
|         exportPadding: value ? DEFAULT_EXPORT_PADDING : 0, | ||||
|       }, | ||||
|       commitToHistory: false, | ||||
|     }; | ||||
|   }, | ||||
|   PanelComponent: ({ appState, updateData }) => ( | ||||
|     <CheckboxItem | ||||
|       checked={!!appState.exportPadding} | ||||
|       onChange={(checked) => updateData(checked)} | ||||
|     > | ||||
|       {"Padding"} | ||||
|     </CheckboxItem> | ||||
|   ), | ||||
| }); | ||||
|  | ||||
| export const actionChangeExportBackground = register({ | ||||
|   name: "changeExportBackground", | ||||
|   trackEvent: { category: "export", action: "toggleBackground" }, | ||||
| @@ -244,15 +268,13 @@ export const actionLoadScene = register({ | ||||
|     } | ||||
|   }, | ||||
|   keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O, | ||||
|   PanelComponent: ({ updateData, appState }) => ( | ||||
|     <ToolButton | ||||
|       type="button" | ||||
|       icon={load} | ||||
|       title={t("buttons.load")} | ||||
|       aria-label={t("buttons.load")} | ||||
|       showAriaLabel={useDevice().isMobile} | ||||
|   PanelComponent: ({ updateData }) => ( | ||||
|     <MenuItem | ||||
|       label={t("buttons.load")} | ||||
|       icon={LoadIcon} | ||||
|       onClick={updateData} | ||||
|       data-testid="load-button" | ||||
|       dataTestId="load-button" | ||||
|       shortcut={getShortcutFromShortcutName("loadScene")} | ||||
|     /> | ||||
|   ), | ||||
| }); | ||||
|   | ||||
| @@ -13,7 +13,7 @@ import { | ||||
|   maybeBindLinearElement, | ||||
|   bindOrUnbindLinearElement, | ||||
| } from "../element/binding"; | ||||
| import { isBindingElement } from "../element/typeChecks"; | ||||
| import { isBindingElement, isLinearElement } from "../element/typeChecks"; | ||||
| import { AppState } from "../types"; | ||||
|  | ||||
| export const actionFinalize = register({ | ||||
| @@ -181,6 +181,11 @@ export const actionFinalize = register({ | ||||
|                 [multiPointElement.id]: true, | ||||
|               } | ||||
|             : appState.selectedElementIds, | ||||
|         // To select the linear element when user has finished mutipoint editing | ||||
|         selectedLinearElement: | ||||
|           multiPointElement && isLinearElement(multiPointElement) | ||||
|             ? new LinearElementEditor(multiPointElement, scene) | ||||
|             : appState.selectedLinearElement, | ||||
|         pendingImageElementId: null, | ||||
|       }, | ||||
|       commitToHistory: appState.activeTool.type === "freedraw", | ||||
|   | ||||
| @@ -6,10 +6,14 @@ import { ExcalidrawElement, NonDeleted } from "../element/types"; | ||||
| import { normalizeAngle, resizeSingleElement } from "../element/resizeElements"; | ||||
| import { AppState } from "../types"; | ||||
| import { getTransformHandles } from "../element/transformHandles"; | ||||
| import { isFreeDrawElement, isLinearElement } from "../element/typeChecks"; | ||||
| import { updateBoundElements } from "../element/binding"; | ||||
| import { LinearElementEditor } from "../element/linearElementEditor"; | ||||
| import { arrayToMap } from "../utils"; | ||||
| import { | ||||
|   getElementAbsoluteCoords, | ||||
|   getElementPointsCoords, | ||||
| } from "../element/bounds"; | ||||
| import { isLinearElement } from "../element/typeChecks"; | ||||
| import { LinearElementEditor } from "../element/linearElementEditor"; | ||||
|  | ||||
| const enableActionFlipHorizontal = ( | ||||
|   elements: readonly ExcalidrawElement[], | ||||
| @@ -118,13 +122,6 @@ const flipElement = ( | ||||
|   const height = element.height; | ||||
|   const originalAngle = normalizeAngle(element.angle); | ||||
|  | ||||
|   let finalOffsetX = 0; | ||||
|   if (isLinearElement(element) || isFreeDrawElement(element)) { | ||||
|     finalOffsetX = | ||||
|       element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 - | ||||
|       element.width; | ||||
|   } | ||||
|  | ||||
|   // Rotate back to zero, if necessary | ||||
|   mutateElement(element, { | ||||
|     angle: normalizeAngle(0), | ||||
| @@ -132,7 +129,6 @@ const flipElement = ( | ||||
|   // Flip unrotated by pulling TransformHandle to opposite side | ||||
|   const transformHandles = getTransformHandles(element, appState.zoom); | ||||
|   let usingNWHandle = true; | ||||
|   let newNCoordsX = 0; | ||||
|   let nHandle = transformHandles.nw; | ||||
|   if (!nHandle) { | ||||
|     // Use ne handle instead | ||||
| @@ -146,30 +142,51 @@ const flipElement = ( | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   let finalOffsetX = 0; | ||||
|   if (isLinearElement(element) && element.points.length < 3) { | ||||
|     finalOffsetX = | ||||
|       element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 - | ||||
|       element.width; | ||||
|   } | ||||
|  | ||||
|   let initialPointsCoords; | ||||
|   if (isLinearElement(element)) { | ||||
|     initialPointsCoords = getElementPointsCoords( | ||||
|       element, | ||||
|       element.points, | ||||
|       element.strokeSharpness, | ||||
|     ); | ||||
|   } | ||||
|   const initialElementAbsoluteCoords = getElementAbsoluteCoords(element); | ||||
|  | ||||
|   if (isLinearElement(element) && element.points.length < 3) { | ||||
|     for (let index = 1; index < element.points.length; index++) { | ||||
|       LinearElementEditor.movePoints(element, [ | ||||
|         { index, point: [-element.points[index][0], element.points[index][1]] }, | ||||
|         { | ||||
|           index, | ||||
|           point: [-element.points[index][0], element.points[index][1]], | ||||
|         }, | ||||
|       ]); | ||||
|     } | ||||
|     LinearElementEditor.normalizePoints(element); | ||||
|   } else { | ||||
|     // calculate new x-coord for transformation | ||||
|     newNCoordsX = usingNWHandle ? element.x + 2 * width : element.x - 2 * width; | ||||
|     const elWidth = initialPointsCoords | ||||
|       ? initialPointsCoords[2] - initialPointsCoords[0] | ||||
|       : initialElementAbsoluteCoords[2] - initialElementAbsoluteCoords[0]; | ||||
|  | ||||
|     const startPoint = initialPointsCoords | ||||
|       ? [initialPointsCoords[0], initialPointsCoords[1]] | ||||
|       : [initialElementAbsoluteCoords[0], initialElementAbsoluteCoords[1]]; | ||||
|  | ||||
|     resizeSingleElement( | ||||
|       new Map().set(element.id, element), | ||||
|       true, | ||||
|       false, | ||||
|       element, | ||||
|       usingNWHandle ? "nw" : "ne", | ||||
|       false, | ||||
|       newNCoordsX, | ||||
|       nHandle[1], | ||||
|       true, | ||||
|       usingNWHandle ? startPoint[0] + elWidth : startPoint[0] - elWidth, | ||||
|       startPoint[1], | ||||
|     ); | ||||
|     // fix the size to account for handle sizes | ||||
|     mutateElement(element, { | ||||
|       width, | ||||
|       height, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   // Rotate by (360 degrees - original angle) | ||||
| @@ -186,9 +203,34 @@ const flipElement = ( | ||||
|   mutateElement(element, { | ||||
|     x: originalX + finalOffsetX, | ||||
|     y: originalY, | ||||
|     width, | ||||
|     height, | ||||
|   }); | ||||
|  | ||||
|   updateBoundElements(element); | ||||
|  | ||||
|   if (initialPointsCoords && isLinearElement(element)) { | ||||
|     // Adjusting origin because when a beizer curve path exceeds min/max points it offsets the origin. | ||||
|     // There's still room for improvement since when the line roughness is > 1 | ||||
|     // we still have a small offset of the origin when fliipping the element. | ||||
|     const finalPointsCoords = getElementPointsCoords( | ||||
|       element, | ||||
|       element.points, | ||||
|       element.strokeSharpness, | ||||
|     ); | ||||
|  | ||||
|     const topLeftCoordsDiff = initialPointsCoords[0] - finalPointsCoords[0]; | ||||
|     const topRightCoordDiff = initialPointsCoords[2] - finalPointsCoords[2]; | ||||
|  | ||||
|     const coordsDiff = topLeftCoordsDiff + topRightCoordDiff; | ||||
|  | ||||
|     mutateElement(element, { | ||||
|       x: element.x + coordsDiff * 0.5, | ||||
|       y: element.y, | ||||
|       width, | ||||
|       height, | ||||
|     }); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| const rotateElement = (element: ExcalidrawElement, rotationAngle: number) => { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { CODES, KEYS } from "../keys"; | ||||
| import { KEYS } from "../keys"; | ||||
| import { t } from "../i18n"; | ||||
| import { arrayToMap, getShortcutKey } from "../utils"; | ||||
| import { register } from "./register"; | ||||
| @@ -132,7 +132,7 @@ export const actionGroup = register({ | ||||
|   contextItemPredicate: (elements, appState) => | ||||
|     enableActionGroup(elements, appState), | ||||
|   keyTest: (event) => | ||||
|     !event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.code === CODES.G, | ||||
|     !event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G, | ||||
|   PanelComponent: ({ elements, appState, updateData }) => ( | ||||
|     <ToolButton | ||||
|       hidden={!enableActionGroup(elements, appState)} | ||||
| @@ -189,7 +189,9 @@ export const actionUngroup = register({ | ||||
|     }; | ||||
|   }, | ||||
|   keyTest: (event) => | ||||
|     event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.code === CODES.G, | ||||
|     event.shiftKey && | ||||
|     event[KEYS.CTRL_OR_CMD] && | ||||
|     event.key === KEYS.G.toUpperCase(), | ||||
|   contextItemLabel: "labels.ungroup", | ||||
|   contextItemPredicate: (elements, appState) => | ||||
|     getSelectedGroupIds(appState).length > 0, | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { Action, ActionResult } from "./types"; | ||||
| import { undo, redo } from "../components/icons"; | ||||
| import { UndoIcon, RedoIcon } from "../components/icons"; | ||||
| import { ToolButton } from "../components/ToolButton"; | ||||
| import { t } from "../i18n"; | ||||
| import History, { HistoryEntry } from "../history"; | ||||
| @@ -72,7 +72,7 @@ export const createUndoAction: ActionCreator = (history) => ({ | ||||
|   PanelComponent: ({ updateData, data }) => ( | ||||
|     <ToolButton | ||||
|       type="button" | ||||
|       icon={undo} | ||||
|       icon={UndoIcon} | ||||
|       aria-label={t("buttons.undo")} | ||||
|       onClick={updateData} | ||||
|       size={data?.size || "medium"} | ||||
| @@ -94,7 +94,7 @@ export const createRedoAction: ActionCreator = (history) => ({ | ||||
|   PanelComponent: ({ updateData, data }) => ( | ||||
|     <ToolButton | ||||
|       type="button" | ||||
|       icon={redo} | ||||
|       icon={RedoIcon} | ||||
|       aria-label={t("buttons.redo")} | ||||
|       onClick={updateData} | ||||
|       size={data?.size || "medium"} | ||||
|   | ||||
							
								
								
									
										49
									
								
								src/actions/actionLinearEditor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,49 @@ | ||||
| import { getNonDeletedElements } from "../element"; | ||||
| import { LinearElementEditor } from "../element/linearElementEditor"; | ||||
| import { isLinearElement } from "../element/typeChecks"; | ||||
| import { ExcalidrawLinearElement } from "../element/types"; | ||||
| import { getSelectedElements } from "../scene"; | ||||
| import { register } from "./register"; | ||||
|  | ||||
| export const actionToggleLinearEditor = register({ | ||||
|   name: "toggleLinearEditor", | ||||
|   trackEvent: { | ||||
|     category: "element", | ||||
|   }, | ||||
|   contextItemPredicate: (elements, appState) => { | ||||
|     const selectedElements = getSelectedElements(elements, appState); | ||||
|     if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) { | ||||
|       return true; | ||||
|     } | ||||
|     return false; | ||||
|   }, | ||||
|   perform(elements, appState, _, app) { | ||||
|     const selectedElement = getSelectedElements( | ||||
|       getNonDeletedElements(elements), | ||||
|       appState, | ||||
|       true, | ||||
|     )[0] as ExcalidrawLinearElement; | ||||
|  | ||||
|     const editingLinearElement = | ||||
|       appState.editingLinearElement?.elementId === selectedElement.id | ||||
|         ? null | ||||
|         : new LinearElementEditor(selectedElement, app.scene); | ||||
|     return { | ||||
|       appState: { | ||||
|         ...appState, | ||||
|         editingLinearElement, | ||||
|       }, | ||||
|       commitToHistory: false, | ||||
|     }; | ||||
|   }, | ||||
|   contextItemLabel: (elements, appState) => { | ||||
|     const selectedElement = getSelectedElements( | ||||
|       getNonDeletedElements(elements), | ||||
|       appState, | ||||
|       true, | ||||
|     )[0] as ExcalidrawLinearElement; | ||||
|     return appState.editingLinearElement?.elementId === selectedElement.id | ||||
|       ? "labels.lineEditor.exit" | ||||
|       : "labels.lineEditor.edit"; | ||||
|   }, | ||||
| }); | ||||
| @@ -1,11 +1,12 @@ | ||||
| import { menu, palette } from "../components/icons"; | ||||
| import { HamburgerMenuIcon, HelpIcon, palette } from "../components/icons"; | ||||
| import { ToolButton } from "../components/ToolButton"; | ||||
| import { t } from "../i18n"; | ||||
| import { showSelectedShapeActions, getNonDeletedElements } from "../element"; | ||||
| import { register } from "./register"; | ||||
| import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils"; | ||||
| import { CODES, KEYS } from "../keys"; | ||||
| import { HelpIcon } from "../components/HelpIcon"; | ||||
| import { KEYS } from "../keys"; | ||||
| import { HelpButton } from "../components/HelpButton"; | ||||
| import MenuItem from "../components/MenuItem"; | ||||
|  | ||||
| export const actionToggleCanvasMenu = register({ | ||||
|   name: "toggleCanvasMenu", | ||||
| @@ -20,7 +21,7 @@ export const actionToggleCanvasMenu = register({ | ||||
|   PanelComponent: ({ appState, updateData }) => ( | ||||
|     <ToolButton | ||||
|       type="button" | ||||
|       icon={menu} | ||||
|       icon={HamburgerMenuIcon} | ||||
|       aria-label={t("buttons.menu")} | ||||
|       onClick={updateData} | ||||
|       selected={appState.openMenu === "canvas"} | ||||
| @@ -67,26 +68,35 @@ export const actionFullScreen = register({ | ||||
|       commitToHistory: false, | ||||
|     }; | ||||
|   }, | ||||
|   keyTest: (event) => event.code === CODES.F && !event[KEYS.CTRL_OR_CMD], | ||||
|   keyTest: (event) => event.key === KEYS.F && !event[KEYS.CTRL_OR_CMD], | ||||
| }); | ||||
|  | ||||
| export const actionShortcuts = register({ | ||||
|   name: "toggleShortcuts", | ||||
|   trackEvent: { category: "menu", action: "toggleHelpDialog" }, | ||||
|   perform: (_elements, appState, _, { focusContainer }) => { | ||||
|     if (appState.showHelpDialog) { | ||||
|     if (appState.openDialog === "help") { | ||||
|       focusContainer(); | ||||
|     } | ||||
|     return { | ||||
|       appState: { | ||||
|         ...appState, | ||||
|         showHelpDialog: !appState.showHelpDialog, | ||||
|         openDialog: appState.openDialog === "help" ? null : "help", | ||||
|       }, | ||||
|       commitToHistory: false, | ||||
|     }; | ||||
|   }, | ||||
|   PanelComponent: ({ updateData }) => ( | ||||
|     <HelpIcon title={t("helpDialog.title")} onClick={updateData} /> | ||||
|   ), | ||||
|   PanelComponent: ({ updateData, isInHamburgerMenu }) => | ||||
|     isInHamburgerMenu ? ( | ||||
|       <MenuItem | ||||
|         label={t("helpDialog.title")} | ||||
|         dataTestId="help-menu-item" | ||||
|         icon={HelpIcon} | ||||
|         onClick={updateData} | ||||
|         shortcut="?" | ||||
|       /> | ||||
|     ) : ( | ||||
|       <HelpButton title={t("helpDialog.title")} onClick={updateData} /> | ||||
|     ), | ||||
|   keyTest: (event) => event.key === KEYS.QUESTION_MARK, | ||||
| }); | ||||
|   | ||||
| @@ -2,37 +2,41 @@ import { AppState } from "../../src/types"; | ||||
| import { ButtonIconSelect } from "../components/ButtonIconSelect"; | ||||
| import { ColorPicker } from "../components/ColorPicker"; | ||||
| import { IconPicker } from "../components/IconPicker"; | ||||
| // TODO barnabasmolnar/editor-redesign | ||||
| // TextAlignTopIcon, TextAlignBottomIcon,TextAlignMiddleIcon, | ||||
| // ArrowHead icons | ||||
| import { | ||||
|   ArrowheadArrowIcon, | ||||
|   ArrowheadBarIcon, | ||||
|   ArrowheadDotIcon, | ||||
|   ArrowheadTriangleIcon, | ||||
|   ArrowheadNoneIcon, | ||||
|   EdgeRoundIcon, | ||||
|   EdgeSharpIcon, | ||||
|   FillCrossHatchIcon, | ||||
|   FillHachureIcon, | ||||
|   FillSolidIcon, | ||||
|   FontFamilyCodeIcon, | ||||
|   FontFamilyHandDrawnIcon, | ||||
|   FontFamilyNormalIcon, | ||||
|   FontSizeExtraLargeIcon, | ||||
|   FontSizeLargeIcon, | ||||
|   FontSizeMediumIcon, | ||||
|   FontSizeSmallIcon, | ||||
|   SloppinessArchitectIcon, | ||||
|   SloppinessArtistIcon, | ||||
|   SloppinessCartoonistIcon, | ||||
|   StrokeStyleDashedIcon, | ||||
|   StrokeStyleDottedIcon, | ||||
|   StrokeStyleSolidIcon, | ||||
|   StrokeWidthIcon, | ||||
|   TextAlignCenterIcon, | ||||
|   TextAlignLeftIcon, | ||||
|   TextAlignRightIcon, | ||||
|   TextAlignTopIcon, | ||||
|   TextAlignBottomIcon, | ||||
|   TextAlignMiddleIcon, | ||||
|   FillHachureIcon, | ||||
|   FillCrossHatchIcon, | ||||
|   FillSolidIcon, | ||||
|   SloppinessArchitectIcon, | ||||
|   SloppinessArtistIcon, | ||||
|   SloppinessCartoonistIcon, | ||||
|   StrokeWidthBaseIcon, | ||||
|   StrokeWidthBoldIcon, | ||||
|   StrokeWidthExtraBoldIcon, | ||||
|   FontSizeSmallIcon, | ||||
|   FontSizeMediumIcon, | ||||
|   FontSizeLargeIcon, | ||||
|   FontSizeExtraLargeIcon, | ||||
|   EdgeSharpIcon, | ||||
|   EdgeRoundIcon, | ||||
|   FreedrawIcon, | ||||
|   FontFamilyNormalIcon, | ||||
|   FontFamilyCodeIcon, | ||||
|   TextAlignLeftIcon, | ||||
|   TextAlignCenterIcon, | ||||
|   TextAlignRightIcon, | ||||
| } from "../components/icons"; | ||||
| import { | ||||
|   DEFAULT_FONT_FAMILY, | ||||
| @@ -307,17 +311,17 @@ export const actionChangeFillStyle = register({ | ||||
|           { | ||||
|             value: "hachure", | ||||
|             text: t("labels.hachure"), | ||||
|             icon: <FillHachureIcon theme={appState.theme} />, | ||||
|             icon: FillHachureIcon, | ||||
|           }, | ||||
|           { | ||||
|             value: "cross-hatch", | ||||
|             text: t("labels.crossHatch"), | ||||
|             icon: <FillCrossHatchIcon theme={appState.theme} />, | ||||
|             icon: FillCrossHatchIcon, | ||||
|           }, | ||||
|           { | ||||
|             value: "solid", | ||||
|             text: t("labels.solid"), | ||||
|             icon: <FillSolidIcon theme={appState.theme} />, | ||||
|             icon: FillSolidIcon, | ||||
|           }, | ||||
|         ]} | ||||
|         group="fill" | ||||
| @@ -358,17 +362,17 @@ export const actionChangeStrokeWidth = register({ | ||||
|           { | ||||
|             value: 1, | ||||
|             text: t("labels.thin"), | ||||
|             icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={2} />, | ||||
|             icon: StrokeWidthBaseIcon, | ||||
|           }, | ||||
|           { | ||||
|             value: 2, | ||||
|             text: t("labels.bold"), | ||||
|             icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={6} />, | ||||
|             icon: StrokeWidthBoldIcon, | ||||
|           }, | ||||
|           { | ||||
|             value: 4, | ||||
|             text: t("labels.extraBold"), | ||||
|             icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={10} />, | ||||
|             icon: StrokeWidthExtraBoldIcon, | ||||
|           }, | ||||
|         ]} | ||||
|         value={getFormValue( | ||||
| @@ -407,17 +411,17 @@ export const actionChangeSloppiness = register({ | ||||
|           { | ||||
|             value: 0, | ||||
|             text: t("labels.architect"), | ||||
|             icon: <SloppinessArchitectIcon theme={appState.theme} />, | ||||
|             icon: SloppinessArchitectIcon, | ||||
|           }, | ||||
|           { | ||||
|             value: 1, | ||||
|             text: t("labels.artist"), | ||||
|             icon: <SloppinessArtistIcon theme={appState.theme} />, | ||||
|             icon: SloppinessArtistIcon, | ||||
|           }, | ||||
|           { | ||||
|             value: 2, | ||||
|             text: t("labels.cartoonist"), | ||||
|             icon: <SloppinessCartoonistIcon theme={appState.theme} />, | ||||
|             icon: SloppinessCartoonistIcon, | ||||
|           }, | ||||
|         ]} | ||||
|         value={getFormValue( | ||||
| @@ -455,17 +459,17 @@ export const actionChangeStrokeStyle = register({ | ||||
|           { | ||||
|             value: "solid", | ||||
|             text: t("labels.strokeStyle_solid"), | ||||
|             icon: <StrokeStyleSolidIcon theme={appState.theme} />, | ||||
|             icon: StrokeWidthBaseIcon, | ||||
|           }, | ||||
|           { | ||||
|             value: "dashed", | ||||
|             text: t("labels.strokeStyle_dashed"), | ||||
|             icon: <StrokeStyleDashedIcon theme={appState.theme} />, | ||||
|             icon: StrokeStyleDashedIcon, | ||||
|           }, | ||||
|           { | ||||
|             value: "dotted", | ||||
|             text: t("labels.strokeStyle_dotted"), | ||||
|             icon: <StrokeStyleDottedIcon theme={appState.theme} />, | ||||
|             icon: StrokeStyleDottedIcon, | ||||
|           }, | ||||
|         ]} | ||||
|         value={getFormValue( | ||||
| @@ -535,25 +539,25 @@ export const actionChangeFontSize = register({ | ||||
|           { | ||||
|             value: 16, | ||||
|             text: t("labels.small"), | ||||
|             icon: <FontSizeSmallIcon theme={appState.theme} />, | ||||
|             icon: FontSizeSmallIcon, | ||||
|             testId: "fontSize-small", | ||||
|           }, | ||||
|           { | ||||
|             value: 20, | ||||
|             text: t("labels.medium"), | ||||
|             icon: <FontSizeMediumIcon theme={appState.theme} />, | ||||
|             icon: FontSizeMediumIcon, | ||||
|             testId: "fontSize-medium", | ||||
|           }, | ||||
|           { | ||||
|             value: 28, | ||||
|             text: t("labels.large"), | ||||
|             icon: <FontSizeLargeIcon theme={appState.theme} />, | ||||
|             icon: FontSizeLargeIcon, | ||||
|             testId: "fontSize-large", | ||||
|           }, | ||||
|           { | ||||
|             value: 36, | ||||
|             text: t("labels.veryLarge"), | ||||
|             icon: <FontSizeExtraLargeIcon theme={appState.theme} />, | ||||
|             icon: FontSizeExtraLargeIcon, | ||||
|             testId: "fontSize-veryLarge", | ||||
|           }, | ||||
|         ]} | ||||
| @@ -658,17 +662,17 @@ export const actionChangeFontFamily = register({ | ||||
|       { | ||||
|         value: FONT_FAMILY.Virgil, | ||||
|         text: t("labels.handDrawn"), | ||||
|         icon: <FontFamilyHandDrawnIcon theme={appState.theme} />, | ||||
|         icon: FreedrawIcon, | ||||
|       }, | ||||
|       { | ||||
|         value: FONT_FAMILY.Helvetica, | ||||
|         text: t("labels.normal"), | ||||
|         icon: <FontFamilyNormalIcon theme={appState.theme} />, | ||||
|         icon: FontFamilyNormalIcon, | ||||
|       }, | ||||
|       { | ||||
|         value: FONT_FAMILY.Cascadia, | ||||
|         text: t("labels.code"), | ||||
|         icon: <FontFamilyCodeIcon theme={appState.theme} />, | ||||
|         icon: FontFamilyCodeIcon, | ||||
|       }, | ||||
|     ]; | ||||
|  | ||||
| @@ -739,17 +743,17 @@ export const actionChangeTextAlign = register({ | ||||
|             { | ||||
|               value: "left", | ||||
|               text: t("labels.left"), | ||||
|               icon: <TextAlignLeftIcon theme={appState.theme} />, | ||||
|               icon: TextAlignLeftIcon, | ||||
|             }, | ||||
|             { | ||||
|               value: "center", | ||||
|               text: t("labels.center"), | ||||
|               icon: <TextAlignCenterIcon theme={appState.theme} />, | ||||
|               icon: TextAlignCenterIcon, | ||||
|             }, | ||||
|             { | ||||
|               value: "right", | ||||
|               text: t("labels.right"), | ||||
|               icon: <TextAlignRightIcon theme={appState.theme} />, | ||||
|               icon: TextAlignRightIcon, | ||||
|             }, | ||||
|           ]} | ||||
|           value={getFormValue( | ||||
| @@ -882,12 +886,12 @@ export const actionChangeSharpness = register({ | ||||
|           { | ||||
|             value: "sharp", | ||||
|             text: t("labels.sharp"), | ||||
|             icon: <EdgeSharpIcon theme={appState.theme} />, | ||||
|             icon: EdgeSharpIcon, | ||||
|           }, | ||||
|           { | ||||
|             value: "round", | ||||
|             text: t("labels.round"), | ||||
|             icon: <EdgeRoundIcon theme={appState.theme} />, | ||||
|             icon: EdgeRoundIcon, | ||||
|           }, | ||||
|         ]} | ||||
|         value={getFormValue( | ||||
| @@ -949,42 +953,38 @@ export const actionChangeArrowhead = register({ | ||||
|     return ( | ||||
|       <fieldset> | ||||
|         <legend>{t("labels.arrowheads")}</legend> | ||||
|         <div className="iconSelectList"> | ||||
|         <div className="iconSelectList buttonList"> | ||||
|           <IconPicker | ||||
|             label="arrowhead_start" | ||||
|             options={[ | ||||
|               { | ||||
|                 value: null, | ||||
|                 text: t("labels.arrowhead_none"), | ||||
|                 icon: <ArrowheadNoneIcon theme={appState.theme} />, | ||||
|                 icon: ArrowheadNoneIcon, | ||||
|                 keyBinding: "q", | ||||
|               }, | ||||
|               { | ||||
|                 value: "arrow", | ||||
|                 text: t("labels.arrowhead_arrow"), | ||||
|                 icon: ( | ||||
|                   <ArrowheadArrowIcon theme={appState.theme} flip={!isRTL} /> | ||||
|                 ), | ||||
|                 icon: <ArrowheadArrowIcon flip={!isRTL} />, | ||||
|                 keyBinding: "w", | ||||
|               }, | ||||
|               { | ||||
|                 value: "bar", | ||||
|                 text: t("labels.arrowhead_bar"), | ||||
|                 icon: <ArrowheadBarIcon theme={appState.theme} flip={!isRTL} />, | ||||
|                 icon: <ArrowheadBarIcon flip={!isRTL} />, | ||||
|                 keyBinding: "e", | ||||
|               }, | ||||
|               { | ||||
|                 value: "dot", | ||||
|                 text: t("labels.arrowhead_dot"), | ||||
|                 icon: <ArrowheadDotIcon theme={appState.theme} flip={!isRTL} />, | ||||
|                 icon: <ArrowheadDotIcon flip={!isRTL} />, | ||||
|                 keyBinding: "r", | ||||
|               }, | ||||
|               { | ||||
|                 value: "triangle", | ||||
|                 text: t("labels.arrowhead_triangle"), | ||||
|                 icon: ( | ||||
|                   <ArrowheadTriangleIcon theme={appState.theme} flip={!isRTL} /> | ||||
|                 ), | ||||
|                 icon: <ArrowheadTriangleIcon flip={!isRTL} />, | ||||
|                 keyBinding: "t", | ||||
|               }, | ||||
|             ]} | ||||
| @@ -1007,34 +1007,30 @@ export const actionChangeArrowhead = register({ | ||||
|                 value: null, | ||||
|                 text: t("labels.arrowhead_none"), | ||||
|                 keyBinding: "q", | ||||
|                 icon: <ArrowheadNoneIcon theme={appState.theme} />, | ||||
|                 icon: ArrowheadNoneIcon, | ||||
|               }, | ||||
|               { | ||||
|                 value: "arrow", | ||||
|                 text: t("labels.arrowhead_arrow"), | ||||
|                 keyBinding: "w", | ||||
|                 icon: ( | ||||
|                   <ArrowheadArrowIcon theme={appState.theme} flip={isRTL} /> | ||||
|                 ), | ||||
|                 icon: <ArrowheadArrowIcon flip={isRTL} />, | ||||
|               }, | ||||
|               { | ||||
|                 value: "bar", | ||||
|                 text: t("labels.arrowhead_bar"), | ||||
|                 keyBinding: "e", | ||||
|                 icon: <ArrowheadBarIcon theme={appState.theme} flip={isRTL} />, | ||||
|                 icon: <ArrowheadBarIcon flip={isRTL} />, | ||||
|               }, | ||||
|               { | ||||
|                 value: "dot", | ||||
|                 text: t("labels.arrowhead_dot"), | ||||
|                 keyBinding: "r", | ||||
|                 icon: <ArrowheadDotIcon theme={appState.theme} flip={isRTL} />, | ||||
|                 icon: <ArrowheadDotIcon flip={isRTL} />, | ||||
|               }, | ||||
|               { | ||||
|                 value: "triangle", | ||||
|                 text: t("labels.arrowhead_triangle"), | ||||
|                 icon: ( | ||||
|                   <ArrowheadTriangleIcon theme={appState.theme} flip={isRTL} /> | ||||
|                 ), | ||||
|                 icon: <ArrowheadTriangleIcon flip={isRTL} />, | ||||
|                 keyBinding: "t", | ||||
|               }, | ||||
|             ]} | ||||
|   | ||||
| @@ -3,32 +3,42 @@ import { register } from "./register"; | ||||
| import { selectGroupsForSelectedElements } from "../groups"; | ||||
| import { getNonDeletedElements, isTextElement } from "../element"; | ||||
| import { ExcalidrawElement } from "../element/types"; | ||||
| import { isLinearElement } from "../element/typeChecks"; | ||||
| import { LinearElementEditor } from "../element/linearElementEditor"; | ||||
|  | ||||
| export const actionSelectAll = register({ | ||||
|   name: "selectAll", | ||||
|   trackEvent: { category: "canvas" }, | ||||
|   perform: (elements, appState) => { | ||||
|   perform: (elements, appState, value, app) => { | ||||
|     if (appState.editingLinearElement) { | ||||
|       return false; | ||||
|     } | ||||
|     const selectedElementIds = elements.reduce( | ||||
|       (map: Record<ExcalidrawElement["id"], true>, element) => { | ||||
|         if ( | ||||
|           !element.isDeleted && | ||||
|           !(isTextElement(element) && element.containerId) && | ||||
|           !element.locked | ||||
|         ) { | ||||
|           map[element.id] = true; | ||||
|         } | ||||
|         return map; | ||||
|       }, | ||||
|       {}, | ||||
|     ); | ||||
|  | ||||
|     return { | ||||
|       appState: selectGroupsForSelectedElements( | ||||
|         { | ||||
|           ...appState, | ||||
|           selectedLinearElement: | ||||
|             // single linear element selected | ||||
|             Object.keys(selectedElementIds).length === 1 && | ||||
|             isLinearElement(elements[0]) | ||||
|               ? new LinearElementEditor(elements[0], app.scene) | ||||
|               : null, | ||||
|           editingGroupId: null, | ||||
|           selectedElementIds: elements.reduce( | ||||
|             (map: Record<ExcalidrawElement["id"], true>, element) => { | ||||
|               if ( | ||||
|                 !element.isDeleted && | ||||
|                 !(isTextElement(element) && element.containerId) && | ||||
|                 !element.locked | ||||
|               ) { | ||||
|                 map[element.id] = true; | ||||
|               } | ||||
|               return map; | ||||
|             }, | ||||
|             {}, | ||||
|           ), | ||||
|           selectedElementIds, | ||||
|         }, | ||||
|         getNonDeletedElements(elements), | ||||
|       ), | ||||
|   | ||||
| @@ -17,16 +17,19 @@ export const actionToggleLock = register({ | ||||
|  | ||||
|     const operation = getOperation(selectedElements); | ||||
|     const selectedElementsMap = arrayToMap(selectedElements); | ||||
|  | ||||
|     const lock = operation === "lock"; | ||||
|     return { | ||||
|       elements: elements.map((element) => { | ||||
|         if (!selectedElementsMap.has(element.id)) { | ||||
|           return element; | ||||
|         } | ||||
|  | ||||
|         return newElementWith(element, { locked: operation === "lock" }); | ||||
|         return newElementWith(element, { locked: lock }); | ||||
|       }), | ||||
|       appState, | ||||
|       appState: { | ||||
|         ...appState, | ||||
|         selectedLinearElement: lock ? null : appState.selectedLinearElement, | ||||
|       }, | ||||
|       commitToHistory: true, | ||||
|     }; | ||||
|   }, | ||||
|   | ||||
| @@ -10,10 +10,10 @@ import { t } from "../i18n"; | ||||
| import { getShortcutKey } from "../utils"; | ||||
| import { register } from "./register"; | ||||
| import { | ||||
|   SendBackwardIcon, | ||||
|   BringToFrontIcon, | ||||
|   SendToBackIcon, | ||||
|   BringForwardIcon, | ||||
|   BringToFrontIcon, | ||||
|   SendBackwardIcon, | ||||
|   SendToBackIcon, | ||||
| } from "../components/icons"; | ||||
|  | ||||
| export const actionSendBackward = register({ | ||||
| @@ -39,7 +39,7 @@ export const actionSendBackward = register({ | ||||
|       onClick={() => updateData(null)} | ||||
|       title={`${t("labels.sendBackward")} — ${getShortcutKey("CtrlOrCmd+[")}`} | ||||
|     > | ||||
|       <SendBackwardIcon theme={appState.theme} /> | ||||
|       {SendBackwardIcon} | ||||
|     </button> | ||||
|   ), | ||||
| }); | ||||
| @@ -67,7 +67,7 @@ export const actionBringForward = register({ | ||||
|       onClick={() => updateData(null)} | ||||
|       title={`${t("labels.bringForward")} — ${getShortcutKey("CtrlOrCmd+]")}`} | ||||
|     > | ||||
|       <BringForwardIcon theme={appState.theme} /> | ||||
|       {BringForwardIcon} | ||||
|     </button> | ||||
|   ), | ||||
| }); | ||||
| @@ -102,7 +102,7 @@ export const actionSendToBack = register({ | ||||
|           : getShortcutKey("CtrlOrCmd+Shift+[") | ||||
|       }`} | ||||
|     > | ||||
|       <SendToBackIcon theme={appState.theme} /> | ||||
|       {SendToBackIcon} | ||||
|     </button> | ||||
|   ), | ||||
| }); | ||||
| @@ -138,7 +138,7 @@ export const actionBringToFront = register({ | ||||
|           : getShortcutKey("CtrlOrCmd+Shift+]") | ||||
|       }`} | ||||
|     > | ||||
|       <BringToFrontIcon theme={appState.theme} /> | ||||
|       {BringToFrontIcon} | ||||
|     </button> | ||||
|   ), | ||||
| }); | ||||
|   | ||||
| @@ -85,3 +85,4 @@ export { actionToggleStats } from "./actionToggleStats"; | ||||
| export { actionUnbindText, actionBindText } from "./actionBoundText"; | ||||
| export { actionLink } from "../element/Hyperlink"; | ||||
| export { actionToggleLock } from "./actionToggleLock"; | ||||
| export { actionToggleLinearEditor } from "./actionLinearEditor"; | ||||
|   | ||||
| @@ -135,7 +135,11 @@ export class ActionManager { | ||||
|   /** | ||||
|    * @param data additional data sent to the PanelComponent | ||||
|    */ | ||||
|   renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => { | ||||
|   renderAction = ( | ||||
|     name: ActionName, | ||||
|     data?: PanelComponentProps["data"], | ||||
|     isInHamburgerMenu = false, | ||||
|   ) => { | ||||
|     const canvasActions = this.app.props.UIOptions.canvasActions; | ||||
|  | ||||
|     if ( | ||||
| @@ -147,6 +151,7 @@ export class ActionManager { | ||||
|     ) { | ||||
|       const action = this.actions[name]; | ||||
|       const PanelComponent = action.PanelComponent!; | ||||
|       PanelComponent.displayName = "PanelComponent"; | ||||
|       const elements = this.getElementsIncludingDeleted(); | ||||
|       const appState = this.getAppState(); | ||||
|       const updateData = (formState?: any) => { | ||||
| @@ -169,6 +174,7 @@ export class ActionManager { | ||||
|           updateData={updateData} | ||||
|           appProps={this.app.props} | ||||
|           data={data} | ||||
|           isInHamburgerMenu={isInHamburgerMenu} | ||||
|         /> | ||||
|       ); | ||||
|     } | ||||
|   | ||||
| @@ -3,36 +3,45 @@ import { isDarwin } from "../keys"; | ||||
| import { getShortcutKey } from "../utils"; | ||||
| import { ActionName } from "./types"; | ||||
|  | ||||
| export type ShortcutName = SubtypeOf< | ||||
|   ActionName, | ||||
|   | "cut" | ||||
|   | "copy" | ||||
|   | "paste" | ||||
|   | "copyStyles" | ||||
|   | "pasteStyles" | ||||
|   | "selectAll" | ||||
|   | "deleteSelectedElements" | ||||
|   | "duplicateSelection" | ||||
|   | "sendBackward" | ||||
|   | "bringForward" | ||||
|   | "sendToBack" | ||||
|   | "bringToFront" | ||||
|   | "copyAsPng" | ||||
|   | "copyAsSvg" | ||||
|   | "group" | ||||
|   | "ungroup" | ||||
|   | "gridMode" | ||||
|   | "zenMode" | ||||
|   | "stats" | ||||
|   | "addToLibrary" | ||||
|   | "viewMode" | ||||
|   | "flipHorizontal" | ||||
|   | "flipVertical" | ||||
|   | "hyperlink" | ||||
|   | "toggleLock" | ||||
| >; | ||||
| export type ShortcutName = | ||||
|   | SubtypeOf< | ||||
|       ActionName, | ||||
|       | "toggleTheme" | ||||
|       | "loadScene" | ||||
|       | "cut" | ||||
|       | "copy" | ||||
|       | "paste" | ||||
|       | "copyStyles" | ||||
|       | "pasteStyles" | ||||
|       | "selectAll" | ||||
|       | "deleteSelectedElements" | ||||
|       | "duplicateSelection" | ||||
|       | "sendBackward" | ||||
|       | "bringForward" | ||||
|       | "sendToBack" | ||||
|       | "bringToFront" | ||||
|       | "copyAsPng" | ||||
|       | "copyAsSvg" | ||||
|       | "group" | ||||
|       | "ungroup" | ||||
|       | "gridMode" | ||||
|       | "zenMode" | ||||
|       | "stats" | ||||
|       | "addToLibrary" | ||||
|       | "viewMode" | ||||
|       | "flipHorizontal" | ||||
|       | "flipVertical" | ||||
|       | "hyperlink" | ||||
|       | "toggleLock" | ||||
|     > | ||||
|   | "saveScene" | ||||
|   | "imageExport"; | ||||
|  | ||||
| const shortcutMap: Record<ShortcutName, string[]> = { | ||||
|   toggleTheme: [getShortcutKey("Shift+Alt+D")], | ||||
|   saveScene: [getShortcutKey("CtrlOrCmd+S")], | ||||
|   loadScene: [getShortcutKey("CtrlOrCmd+O")], | ||||
|   imageExport: [getShortcutKey("CtrlOrCmd+Shift+E")], | ||||
|   cut: [getShortcutKey("CtrlOrCmd+X")], | ||||
|   copy: [getShortcutKey("CtrlOrCmd+C")], | ||||
|   paste: [getShortcutKey("CtrlOrCmd+V")], | ||||
|   | ||||
| @@ -68,6 +68,7 @@ export type ActionName = | ||||
|   | "finalize" | ||||
|   | "changeProjectName" | ||||
|   | "changeExportBackground" | ||||
|   | "changeExportPadding" | ||||
|   | "changeExportEmbedScene" | ||||
|   | "changeExportScale" | ||||
|   | "saveToActiveFile" | ||||
| @@ -111,7 +112,8 @@ export type ActionName = | ||||
|   | "hyperlink" | ||||
|   | "eraser" | ||||
|   | "bindText" | ||||
|   | "toggleLock"; | ||||
|   | "toggleLock" | ||||
|   | "toggleLinearEditor"; | ||||
|  | ||||
| export type PanelComponentProps = { | ||||
|   elements: readonly ExcalidrawElement[]; | ||||
| @@ -123,7 +125,9 @@ export type PanelComponentProps = { | ||||
|  | ||||
| export interface Action { | ||||
|   name: ActionName; | ||||
|   PanelComponent?: React.FC<PanelComponentProps>; | ||||
|   PanelComponent?: React.FC< | ||||
|     PanelComponentProps & { isInHamburgerMenu: boolean } | ||||
|   >; | ||||
|   perform: ActionFn; | ||||
|   keyPriority?: number; | ||||
|   keyTest?: ( | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import oc from "open-color"; | ||||
| import { | ||||
|   DEFAULT_EXPORT_PADDING, | ||||
|   DEFAULT_FONT_FAMILY, | ||||
|   DEFAULT_FONT_SIZE, | ||||
|   DEFAULT_TEXT_ALIGN, | ||||
| @@ -19,6 +20,7 @@ export const getDefaultAppState = (): Omit< | ||||
|   "offsetTop" | "offsetLeft" | "width" | "height" | ||||
| > => { | ||||
|   return { | ||||
|     showWelcomeScreen: false, | ||||
|     theme: THEME.LIGHT, | ||||
|     collaborators: new Map(), | ||||
|     currentChartType: "bar", | ||||
| @@ -54,11 +56,11 @@ export const getDefaultAppState = (): Omit< | ||||
|     exportScale: defaultExportScale, | ||||
|     exportEmbedScene: false, | ||||
|     exportWithDarkMode: false, | ||||
|     exportPadding: DEFAULT_EXPORT_PADDING, | ||||
|     fileHandle: null, | ||||
|     gridSize: null, | ||||
|     isBindingEnabled: true, | ||||
|     isLibraryOpen: false, | ||||
|     isLibraryMenuDocked: false, | ||||
|     isSidebarDocked: false, | ||||
|     isLoading: false, | ||||
|     isResizing: false, | ||||
|     isRotating: false, | ||||
| @@ -67,6 +69,8 @@ export const getDefaultAppState = (): Omit< | ||||
|     name: `${t("labels.untitled")}-${getDateTime()}`, | ||||
|     openMenu: null, | ||||
|     openPopup: null, | ||||
|     openSidebar: null, | ||||
|     openDialog: null, | ||||
|     pasteDialog: { shown: false, data: null }, | ||||
|     previousSelectedElementIds: {}, | ||||
|     resizingElement: null, | ||||
| @@ -77,7 +81,6 @@ export const getDefaultAppState = (): Omit< | ||||
|     selectedGroupIds: {}, | ||||
|     selectionElement: null, | ||||
|     shouldCacheIgnoreZoom: false, | ||||
|     showHelpDialog: false, | ||||
|     showStats: false, | ||||
|     startBoundElement: null, | ||||
|     suggestedBindings: [], | ||||
| @@ -90,6 +93,7 @@ export const getDefaultAppState = (): Omit< | ||||
|     viewModeEnabled: false, | ||||
|     pendingImageElementId: null, | ||||
|     showHyperlinkPopup: false, | ||||
|     selectedLinearElement: null, | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| @@ -109,6 +113,7 @@ const APP_STATE_STORAGE_CONF = (< | ||||
|   T extends Record<keyof AppState, Values>, | ||||
| >(config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }) => | ||||
|   config)({ | ||||
|   showWelcomeScreen: { browser: true, export: false, server: false }, | ||||
|   theme: { browser: true, export: false, server: false }, | ||||
|   collaborators: { browser: false, export: false, server: false }, | ||||
|   currentChartType: { browser: true, export: false, server: false }, | ||||
| @@ -142,13 +147,13 @@ const APP_STATE_STORAGE_CONF = (< | ||||
|   exportBackground: { browser: true, export: false, server: false }, | ||||
|   exportEmbedScene: { browser: true, export: false, server: false }, | ||||
|   exportScale: { browser: true, export: false, server: false }, | ||||
|   exportPadding: { browser: true, export: false, server: false }, | ||||
|   exportWithDarkMode: { browser: true, export: false, server: false }, | ||||
|   fileHandle: { browser: false, export: false, server: false }, | ||||
|   gridSize: { browser: true, export: true, server: true }, | ||||
|   height: { browser: false, export: false, server: false }, | ||||
|   isBindingEnabled: { browser: false, export: false, server: false }, | ||||
|   isLibraryOpen: { browser: true, export: false, server: false }, | ||||
|   isLibraryMenuDocked: { browser: true, export: false, server: false }, | ||||
|   isSidebarDocked: { 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 }, | ||||
| @@ -159,6 +164,8 @@ const APP_STATE_STORAGE_CONF = (< | ||||
|   offsetTop: { browser: false, export: false, server: false }, | ||||
|   openMenu: { browser: true, export: false, server: false }, | ||||
|   openPopup: { browser: false, export: false, server: false }, | ||||
|   openSidebar: { browser: true, export: false, server: false }, | ||||
|   openDialog: { browser: false, export: false, server: false }, | ||||
|   pasteDialog: { browser: false, export: false, server: false }, | ||||
|   previousSelectedElementIds: { browser: true, export: false, server: false }, | ||||
|   resizingElement: { browser: false, export: false, server: false }, | ||||
| @@ -169,7 +176,6 @@ const APP_STATE_STORAGE_CONF = (< | ||||
|   selectedGroupIds: { browser: true, export: false, server: false }, | ||||
|   selectionElement: { browser: false, export: false, server: false }, | ||||
|   shouldCacheIgnoreZoom: { browser: true, export: false, server: false }, | ||||
|   showHelpDialog: { browser: false, export: false, server: false }, | ||||
|   showStats: { browser: true, export: false, server: false }, | ||||
|   startBoundElement: { browser: false, export: false, server: false }, | ||||
|   suggestedBindings: { browser: false, export: false, server: false }, | ||||
| @@ -181,6 +187,7 @@ const APP_STATE_STORAGE_CONF = (< | ||||
|   viewModeEnabled: { browser: false, export: false, server: false }, | ||||
|   pendingImageElementId: { browser: false, export: false, server: false }, | ||||
|   showHyperlinkPopup: { browser: false, export: false, server: false }, | ||||
|   selectedLinearElement: { browser: true, export: false, server: false }, | ||||
| }); | ||||
|  | ||||
| const _clearAppStateForStorage = < | ||||
|   | ||||
| @@ -11,27 +11,18 @@ export const getClientColors = (clientId: string, appState: AppState) => { | ||||
|   // Naive way of getting an integer out of the clientId | ||||
|   const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0); | ||||
|  | ||||
|   // Skip transparent background. | ||||
|   const backgrounds = colors.elementBackground.slice(1); | ||||
|   const strokes = colors.elementStroke.slice(1); | ||||
|   // Skip transparent & gray colors | ||||
|   const backgrounds = colors.elementBackground.slice(3); | ||||
|   const strokes = colors.elementStroke.slice(3); | ||||
|   return { | ||||
|     background: backgrounds[sum % backgrounds.length], | ||||
|     stroke: strokes[sum % strokes.length], | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export const getClientInitials = (username?: string | null) => { | ||||
|   if (!username) { | ||||
| export const getClientInitials = (userName?: string | null) => { | ||||
|   if (!userName) { | ||||
|     return "?"; | ||||
|   } | ||||
|   const names = username.trim().split(" "); | ||||
|  | ||||
|   if (names.length < 2) { | ||||
|     return names[0].substring(0, 2).toUpperCase(); | ||||
|   } | ||||
|  | ||||
|   const firstName = names[0]; | ||||
|   const lastName = names[names.length - 1]; | ||||
|  | ||||
|   return (firstName[0] + lastName[0]).toUpperCase(); | ||||
|   return userName.trim()[0].toUpperCase(); | ||||
| }; | ||||
|   | ||||
							
								
								
									
										92
									
								
								src/components/Actions.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,92 @@ | ||||
| .zoom-actions, | ||||
| .undo-redo-buttons { | ||||
|   background-color: var(--island-bg-color); | ||||
|   border-radius: var(--border-radius-lg); | ||||
| } | ||||
|  | ||||
| .zoom-button, | ||||
| .undo-redo-buttons button { | ||||
|   border: 1px solid var(--default-border-color) !important; | ||||
|   border-radius: 0 !important; | ||||
|   background-color: transparent !important; | ||||
|   font-size: 0.875rem !important; | ||||
|   width: var(--lg-button-size); | ||||
|   height: var(--lg-button-size); | ||||
|   svg { | ||||
|     width: var(--lg-icon-size) !important; | ||||
|     height: var(--lg-icon-size) !important; | ||||
|   } | ||||
|  | ||||
|   .ToolIcon__icon { | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .reset-zoom-button { | ||||
|   border-left: 0 !important; | ||||
|   border-right: 0 !important; | ||||
|   padding: 0 0.625rem !important; | ||||
|   width: 3.75rem !important; | ||||
|   justify-content: center; | ||||
|   color: var(--text-primary-color); | ||||
| } | ||||
|  | ||||
| .zoom-out-button { | ||||
|   border-top-left-radius: var(--border-radius-lg) !important; | ||||
|   border-bottom-left-radius: var(--border-radius-lg) !important; | ||||
|  | ||||
|   :root[dir="rtl"] & { | ||||
|     transform: scaleX(-1); | ||||
|   } | ||||
|  | ||||
|   .ToolIcon__icon { | ||||
|     border-top-right-radius: 0 !important; | ||||
|     border-bottom-right-radius: 0 !important; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .zoom-in-button { | ||||
|   border-top-right-radius: var(--border-radius-lg) !important; | ||||
|   border-bottom-right-radius: var(--border-radius-lg) !important; | ||||
|  | ||||
|   :root[dir="rtl"] & { | ||||
|     transform: scaleX(-1); | ||||
|   } | ||||
|  | ||||
|   .ToolIcon__icon { | ||||
|     border-top-left-radius: 0 !important; | ||||
|     border-bottom-left-radius: 0 !important; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .undo-redo-buttons { | ||||
|   .undo-button-container button { | ||||
|     border-top-left-radius: var(--border-radius-lg) !important; | ||||
|     border-bottom-left-radius: var(--border-radius-lg) !important; | ||||
|     border-right: 0 !important; | ||||
|  | ||||
|     :root[dir="rtl"] & { | ||||
|       transform: scaleX(-1); | ||||
|     } | ||||
|  | ||||
|     .ToolIcon__icon { | ||||
|       border-top-right-radius: 0 !important; | ||||
|       border-bottom-right-radius: 0 !important; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .redo-button-container button { | ||||
|     border-top-right-radius: var(--border-radius-lg) !important; | ||||
|     border-bottom-right-radius: var(--border-radius-lg) !important; | ||||
|  | ||||
|     :root[dir="rtl"] & { | ||||
|       transform: scaleX(-1); | ||||
|     } | ||||
|  | ||||
|     .ToolIcon__icon { | ||||
|       border-top-left-radius: 0 !important; | ||||
|       border-bottom-left-radius: 0 !important; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -26,17 +26,19 @@ import { ToolButton } from "./ToolButton"; | ||||
| import { hasStrokeColor } from "../scene/comparisons"; | ||||
| import { trackEvent } from "../analytics"; | ||||
| import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks"; | ||||
| import clsx from "clsx"; | ||||
| import { actionToggleZenMode } from "../actions"; | ||||
| import "./Actions.scss"; | ||||
| import { Tooltip } from "./Tooltip"; | ||||
|  | ||||
| export const SelectedShapeActions = ({ | ||||
|   appState, | ||||
|   elements, | ||||
|   renderAction, | ||||
|   activeTool, | ||||
| }: { | ||||
|   appState: AppState; | ||||
|   elements: readonly ExcalidrawElement[]; | ||||
|   renderAction: ActionManager["renderAction"]; | ||||
|   activeTool: AppState["activeTool"]["type"]; | ||||
| }) => { | ||||
|   const targetElements = getTargetElements( | ||||
|     getNonDeletedElements(elements), | ||||
| @@ -56,13 +58,13 @@ export const SelectedShapeActions = ({ | ||||
|   const isRTL = document.documentElement.getAttribute("dir") === "rtl"; | ||||
|  | ||||
|   const showFillIcons = | ||||
|     hasBackground(activeTool) || | ||||
|     hasBackground(appState.activeTool.type) || | ||||
|     targetElements.some( | ||||
|       (element) => | ||||
|         hasBackground(element.type) && !isTransparent(element.backgroundColor), | ||||
|     ); | ||||
|   const showChangeBackgroundIcons = | ||||
|     hasBackground(activeTool) || | ||||
|     hasBackground(appState.activeTool.type) || | ||||
|     targetElements.some((element) => hasBackground(element.type)); | ||||
|  | ||||
|   const showLinkIcon = | ||||
| @@ -79,23 +81,27 @@ export const SelectedShapeActions = ({ | ||||
|  | ||||
|   return ( | ||||
|     <div className="panelColumn"> | ||||
|       {((hasStrokeColor(activeTool) && | ||||
|         activeTool !== "image" && | ||||
|         commonSelectedType !== "image") || | ||||
|         targetElements.some((element) => hasStrokeColor(element.type))) && | ||||
|         renderAction("changeStrokeColor")} | ||||
|       {showChangeBackgroundIcons && renderAction("changeBackgroundColor")} | ||||
|       <div> | ||||
|         {((hasStrokeColor(appState.activeTool.type) && | ||||
|           appState.activeTool.type !== "image" && | ||||
|           commonSelectedType !== "image") || | ||||
|           targetElements.some((element) => hasStrokeColor(element.type))) && | ||||
|           renderAction("changeStrokeColor")} | ||||
|       </div> | ||||
|       {showChangeBackgroundIcons && ( | ||||
|         <div>{renderAction("changeBackgroundColor")}</div> | ||||
|       )} | ||||
|       {showFillIcons && renderAction("changeFillStyle")} | ||||
|  | ||||
|       {(hasStrokeWidth(activeTool) || | ||||
|       {(hasStrokeWidth(appState.activeTool.type) || | ||||
|         targetElements.some((element) => hasStrokeWidth(element.type))) && | ||||
|         renderAction("changeStrokeWidth")} | ||||
|  | ||||
|       {(activeTool === "freedraw" || | ||||
|       {(appState.activeTool.type === "freedraw" || | ||||
|         targetElements.some((element) => element.type === "freedraw")) && | ||||
|         renderAction("changeStrokeShape")} | ||||
|  | ||||
|       {(hasStrokeStyle(activeTool) || | ||||
|       {(hasStrokeStyle(appState.activeTool.type) || | ||||
|         targetElements.some((element) => hasStrokeStyle(element.type))) && ( | ||||
|         <> | ||||
|           {renderAction("changeStrokeStyle")} | ||||
| @@ -103,12 +109,12 @@ export const SelectedShapeActions = ({ | ||||
|         </> | ||||
|       )} | ||||
|  | ||||
|       {(canChangeSharpness(activeTool) || | ||||
|       {(canChangeSharpness(appState.activeTool.type) || | ||||
|         targetElements.some((element) => canChangeSharpness(element.type))) && ( | ||||
|         <>{renderAction("changeSharpness")}</> | ||||
|       )} | ||||
|  | ||||
|       {(hasText(activeTool) || | ||||
|       {(hasText(appState.activeTool.type) || | ||||
|         targetElements.some((element) => hasText(element.type))) && ( | ||||
|         <> | ||||
|           {renderAction("changeFontSize")} | ||||
| @@ -123,7 +129,7 @@ export const SelectedShapeActions = ({ | ||||
|         (element) => | ||||
|           hasBoundTextElement(element) || isBoundToContainer(element), | ||||
|       ) && renderAction("changeVerticalAlign")} | ||||
|       {(canHaveArrowheads(activeTool) || | ||||
|       {(canHaveArrowheads(appState.activeTool.type) || | ||||
|         targetElements.some((element) => canHaveArrowheads(element.type))) && ( | ||||
|         <>{renderAction("changeArrowhead")}</> | ||||
|       )} | ||||
| @@ -163,7 +169,16 @@ export const SelectedShapeActions = ({ | ||||
|             )} | ||||
|             {targetElements.length > 2 && | ||||
|               renderAction("distributeHorizontally")} | ||||
|             <div className="iconRow"> | ||||
|             {/* breaks the row ˇˇ */} | ||||
|             <div style={{ flexBasis: "100%", height: 0 }} /> | ||||
|             <div | ||||
|               style={{ | ||||
|                 display: "flex", | ||||
|                 flexWrap: "wrap", | ||||
|                 gap: ".5rem", | ||||
|                 marginTop: "-0.5rem", | ||||
|               }} | ||||
|             > | ||||
|               {renderAction("alignTop")} | ||||
|               {renderAction("alignVerticallyCentered")} | ||||
|               {renderAction("alignBottom")} | ||||
| @@ -203,25 +218,26 @@ export const ShapesSwitcher = ({ | ||||
|   appState: AppState; | ||||
| }) => ( | ||||
|   <> | ||||
|     {SHAPES.map(({ value, icon, key }, index) => { | ||||
|     {SHAPES.map(({ value, icon, key, fillable }, index) => { | ||||
|       const numberKey = value === "eraser" ? 0 : index + 1; | ||||
|       const label = t(`toolBar.${value}`); | ||||
|       const letter = key && (typeof key === "string" ? key : key[0]); | ||||
|       const shortcut = letter | ||||
|         ? `${capitalizeString(letter)} ${t("helpDialog.or")} ${index + 1}` | ||||
|         : `${index + 1}`; | ||||
|         ? `${capitalizeString(letter)} ${t("helpDialog.or")} ${numberKey}` | ||||
|         : `${numberKey}`; | ||||
|       return ( | ||||
|         <ToolButton | ||||
|           className="Shape" | ||||
|           className={clsx("Shape", { fillable })} | ||||
|           key={value} | ||||
|           type="radio" | ||||
|           icon={icon} | ||||
|           checked={activeTool.type === value} | ||||
|           name="editor-current-shape" | ||||
|           title={`${capitalizeString(label)} — ${shortcut}`} | ||||
|           keyBindingLabel={`${index + 1}`} | ||||
|           keyBindingLabel={`${numberKey}`} | ||||
|           aria-label={capitalizeString(label)} | ||||
|           aria-keyshortcuts={shortcut} | ||||
|           data-testid={value} | ||||
|           data-testid={`toolbar-${value}`} | ||||
|           onPointerDown={({ pointerType }) => { | ||||
|             if (!appState.penDetected && pointerType === "pen") { | ||||
|               setAppState({ | ||||
| @@ -263,11 +279,57 @@ export const ZoomActions = ({ | ||||
|   renderAction: ActionManager["renderAction"]; | ||||
|   zoom: Zoom; | ||||
| }) => ( | ||||
|   <Stack.Col gap={1}> | ||||
|     <Stack.Row gap={1} align="center"> | ||||
|   <Stack.Col gap={1} className="zoom-actions"> | ||||
|     <Stack.Row align="center"> | ||||
|       {renderAction("zoomOut")} | ||||
|       {renderAction("zoomIn")} | ||||
|       {renderAction("resetZoom")} | ||||
|       {renderAction("zoomIn")} | ||||
|     </Stack.Row> | ||||
|   </Stack.Col> | ||||
| ); | ||||
|  | ||||
| export const UndoRedoActions = ({ | ||||
|   renderAction, | ||||
|   className, | ||||
| }: { | ||||
|   renderAction: ActionManager["renderAction"]; | ||||
|   className?: string; | ||||
| }) => ( | ||||
|   <div className={`undo-redo-buttons ${className}`}> | ||||
|     <div className="undo-button-container"> | ||||
|       <Tooltip label={t("buttons.undo")}>{renderAction("undo")}</Tooltip> | ||||
|     </div> | ||||
|     <div className="redo-button-container"> | ||||
|       <Tooltip label={t("buttons.redo")}> {renderAction("redo")}</Tooltip> | ||||
|     </div> | ||||
|   </div> | ||||
| ); | ||||
|  | ||||
| export const ExitZenModeAction = ({ | ||||
|   actionManager, | ||||
|   showExitZenModeBtn, | ||||
| }: { | ||||
|   actionManager: ActionManager; | ||||
|   showExitZenModeBtn: boolean; | ||||
| }) => ( | ||||
|   <button | ||||
|     className={clsx("disable-zen-mode", { | ||||
|       "disable-zen-mode--visible": showExitZenModeBtn, | ||||
|     })} | ||||
|     onClick={() => actionManager.executeAction(actionToggleZenMode)} | ||||
|   > | ||||
|     {t("buttons.exitZenMode")} | ||||
|   </button> | ||||
| ); | ||||
|  | ||||
| export const FinalizeAction = ({ | ||||
|   renderAction, | ||||
|   className, | ||||
| }: { | ||||
|   renderAction: ActionManager["renderAction"]; | ||||
|   className?: string; | ||||
| }) => ( | ||||
|   <div className={`finalize-button ${className}`}> | ||||
|     {renderAction("finalize", { size: "small" })} | ||||
|   </div> | ||||
| ); | ||||
|   | ||||
| @@ -1,9 +1,11 @@ | ||||
| import Stack from "../components/Stack"; | ||||
| import { ToolButton } from "../components/ToolButton"; | ||||
| import { save, file } from "../components/icons"; | ||||
| // TODO barnabasmolnar/editor-redesign | ||||
| // this icon is not great | ||||
| import { getShortcutFromShortcutName } from "../actions/shortcuts"; | ||||
| import { save } from "../components/icons"; | ||||
| import { t } from "../i18n"; | ||||
|  | ||||
| import "./ActiveFile.scss"; | ||||
| import MenuItem from "./MenuItem"; | ||||
|  | ||||
| type ActiveFileProps = { | ||||
|   fileName?: string; | ||||
| @@ -11,18 +13,11 @@ type ActiveFileProps = { | ||||
| }; | ||||
|  | ||||
| export const ActiveFile = ({ fileName, onSave }: ActiveFileProps) => ( | ||||
|   <Stack.Row className="ActiveFile" gap={1} align="center"> | ||||
|     <span className="ActiveFile__fileName"> | ||||
|       {file} | ||||
|       <span>{fileName}</span> | ||||
|     </span> | ||||
|     <ToolButton | ||||
|       type="icon" | ||||
|       icon={save} | ||||
|       title={t("buttons.save")} | ||||
|       aria-label={t("buttons.save")} | ||||
|       onClick={onSave} | ||||
|       data-testid="save-button" | ||||
|     /> | ||||
|   </Stack.Row> | ||||
|   <MenuItem | ||||
|     label={`${t("buttons.save")}`} | ||||
|     shortcut={getShortcutFromShortcutName("saveScene")} | ||||
|     dataTestId="save-button" | ||||
|     onClick={onSave} | ||||
|     icon={save} | ||||
|   /> | ||||
| ); | ||||
|   | ||||
| @@ -2,16 +2,19 @@ | ||||
|  | ||||
| .excalidraw { | ||||
|   .Avatar { | ||||
|     width: 2.5rem; | ||||
|     height: 2.5rem; | ||||
|     border-radius: 1.25rem; | ||||
|     width: 1.25rem; | ||||
|     height: 1.25rem; | ||||
|     border-radius: 100%; | ||||
|     outline: 2px solid var(--avatar-border-color); | ||||
|     outline-offset: 2px; | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     color: $oc-white; | ||||
|     cursor: pointer; | ||||
|     font-size: 0.8rem; | ||||
|     font-size: 0.625rem; | ||||
|     font-weight: 500; | ||||
|     line-height: 1; | ||||
|  | ||||
|     &-img { | ||||
|       width: 100%; | ||||
|   | ||||
| @@ -11,13 +11,11 @@ type AvatarProps = { | ||||
|   src?: string; | ||||
| }; | ||||
|  | ||||
| export const Avatar = ({ color, border, onClick, name, src }: AvatarProps) => { | ||||
| export const Avatar = ({ color, onClick, name, src }: AvatarProps) => { | ||||
|   const shortName = getClientInitials(name); | ||||
|   const [error, setError] = useState(false); | ||||
|   const loadImg = !error && src; | ||||
|   const style = loadImg | ||||
|     ? undefined | ||||
|     : { background: color, border: `1px solid ${border}` }; | ||||
|   const style = loadImg ? undefined : { background: color }; | ||||
|   return ( | ||||
|     <div className="Avatar" style={style} onClick={onClick}> | ||||
|       {loadImg ? ( | ||||
|   | ||||
| @@ -1,20 +0,0 @@ | ||||
| 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<any, AppState>["setState"]; | ||||
|   showThemeBtn: boolean; | ||||
| }) => ( | ||||
|   <div style={{ display: "flex" }}> | ||||
|     {actionManager.renderAction("changeViewBackgroundColor")} | ||||
|     {showThemeBtn && actionManager.renderAction("toggleTheme")} | ||||
|   </div> | ||||
| ); | ||||
| @@ -4,6 +4,7 @@ import "./Card.scss"; | ||||
|  | ||||
| export const Card: React.FC<{ | ||||
|   color: keyof OpenColor | "primary"; | ||||
|   children?: React.ReactNode; | ||||
| }> = ({ children, color }) => { | ||||
|   return ( | ||||
|     <div | ||||
|   | ||||
| @@ -64,6 +64,8 @@ | ||||
|  | ||||
|       color: #{$oc-blue-7}; | ||||
|  | ||||
|       border: 0; | ||||
|  | ||||
|       &:focus { | ||||
|         box-shadow: 0 0 0 3px #{$oc-blue-7}; | ||||
|       } | ||||
|   | ||||
| @@ -8,6 +8,7 @@ export const CheckboxItem: React.FC<{ | ||||
|   checked: boolean; | ||||
|   onChange: (checked: boolean, event: React.MouseEvent) => void; | ||||
|   className?: string; | ||||
|   children?: React.ReactNode; | ||||
| }> = ({ children, checked, onChange, className }) => { | ||||
|   return ( | ||||
|     <div | ||||
|   | ||||
| @@ -1,10 +1,9 @@ | ||||
| import { useState } from "react"; | ||||
| import { t } from "../i18n"; | ||||
| import { useDevice } from "./App"; | ||||
| import { trash } from "./icons"; | ||||
| import { ToolButton } from "./ToolButton"; | ||||
| import { TrashIcon } from "./icons"; | ||||
|  | ||||
| import ConfirmDialog from "./ConfirmDialog"; | ||||
| import MenuItem from "./MenuItem"; | ||||
|  | ||||
| const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => { | ||||
|   const [showDialog, setShowDialog] = useState(false); | ||||
| @@ -14,14 +13,11 @@ const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => { | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <ToolButton | ||||
|         type="button" | ||||
|         icon={trash} | ||||
|         title={t("buttons.clearReset")} | ||||
|         aria-label={t("buttons.clearReset")} | ||||
|         showAriaLabel={useDevice().isMobile} | ||||
|       <MenuItem | ||||
|         label={t("buttons.clearReset")} | ||||
|         icon={TrashIcon} | ||||
|         onClick={toggleDialog} | ||||
|         data-testid="clear-canvas-button" | ||||
|         dataTestId="clear-canvas-button" | ||||
|       /> | ||||
|  | ||||
|       {showDialog && ( | ||||
|   | ||||
| @@ -1,6 +1,51 @@ | ||||
| @import "../css/variables.module"; | ||||
|  | ||||
| .excalidraw { | ||||
|   .collab-button { | ||||
|     @include outlineButtonStyles; | ||||
|     width: var(--lg-button-size); | ||||
|     height: var(--lg-button-size); | ||||
|  | ||||
|     svg { | ||||
|       width: var(--lg-icon-size); | ||||
|       height: var(--lg-icon-size); | ||||
|     } | ||||
|     background-color: var(--color-primary); | ||||
|     border-color: var(--color-primary); | ||||
|     color: white; | ||||
|     flex-shrink: 0; | ||||
|  | ||||
|     &:hover { | ||||
|       background-color: var(--color-primary-darker); | ||||
|       border-color: var(--color-primary-darker); | ||||
|     } | ||||
|  | ||||
|     &:active { | ||||
|       background-color: var(--color-primary-darker); | ||||
|     } | ||||
|  | ||||
|     &.active { | ||||
|       background-color: #0fb884; | ||||
|       border-color: #0fb884; | ||||
|  | ||||
|       svg { | ||||
|         color: #fff; | ||||
|       } | ||||
|  | ||||
|       &:hover, | ||||
|       &:active { | ||||
|         background-color: #0fb884; | ||||
|         border-color: #0fb884; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &.theme--dark { | ||||
|     .collab-button { | ||||
|       color: var(--color-gray-90); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .CollabButton.is-collaborating { | ||||
|     background-color: var(--button-special-active-bg-color); | ||||
|  | ||||
| @@ -24,9 +69,9 @@ | ||||
|     bottom: -5px; | ||||
|     padding: 3px; | ||||
|     border-radius: 50%; | ||||
|     background-color: $oc-green-6; | ||||
|     color: $oc-white; | ||||
|     font-size: 0.6em; | ||||
|     background-color: $oc-green-2; | ||||
|     color: $oc-green-9; | ||||
|     font-size: 0.6rem; | ||||
|     font-family: "Cascadia"; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,37 +1,47 @@ | ||||
| import clsx from "clsx"; | ||||
| import { ToolButton } from "./ToolButton"; | ||||
| import { t } from "../i18n"; | ||||
| import { useDevice } from "../components/App"; | ||||
| import { users } from "./icons"; | ||||
| import { UsersIcon } from "./icons"; | ||||
|  | ||||
| import "./CollabButton.scss"; | ||||
| import MenuItem from "./MenuItem"; | ||||
| import clsx from "clsx"; | ||||
|  | ||||
| const CollabButton = ({ | ||||
|   isCollaborating, | ||||
|   collaboratorCount, | ||||
|   onClick, | ||||
|   isInHamburgerMenu = true, | ||||
| }: { | ||||
|   isCollaborating: boolean; | ||||
|   collaboratorCount: number; | ||||
|   onClick: () => void; | ||||
|   isInHamburgerMenu?: boolean; | ||||
| }) => { | ||||
|   return ( | ||||
|     <> | ||||
|       <ToolButton | ||||
|         className={clsx("CollabButton", { | ||||
|           "is-collaborating": isCollaborating, | ||||
|         })} | ||||
|         onClick={onClick} | ||||
|         icon={users} | ||||
|         type="button" | ||||
|         title={t("labels.liveCollaboration")} | ||||
|         aria-label={t("labels.liveCollaboration")} | ||||
|         showAriaLabel={useDevice().isMobile} | ||||
|       > | ||||
|         {isCollaborating && ( | ||||
|           <div className="CollabButton-collaborators">{collaboratorCount}</div> | ||||
|         )} | ||||
|       </ToolButton> | ||||
|       {isInHamburgerMenu ? ( | ||||
|         <MenuItem | ||||
|           label={t("labels.liveCollaboration")} | ||||
|           dataTestId="collab-button" | ||||
|           icon={UsersIcon} | ||||
|           onClick={onClick} | ||||
|           isCollaborating={isCollaborating} | ||||
|         /> | ||||
|       ) : ( | ||||
|         <button | ||||
|           className={clsx("collab-button", { active: isCollaborating })} | ||||
|           type="button" | ||||
|           onClick={onClick} | ||||
|           style={{ position: "relative" }} | ||||
|           title={t("labels.liveCollaboration")} | ||||
|         > | ||||
|           {UsersIcon} | ||||
|           {collaboratorCount > 0 && ( | ||||
|             <div className="CollabButton-collaborators"> | ||||
|               {collaboratorCount} | ||||
|             </div> | ||||
|           )} | ||||
|         </button> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -21,6 +21,23 @@ | ||||
|     display: grid; | ||||
|     grid-template-columns: auto 1fr; | ||||
|     align-items: center; | ||||
|     column-gap: 0.5rem; | ||||
|   } | ||||
|  | ||||
|   .color-picker-control-container + .popover { | ||||
|     position: static; | ||||
|   } | ||||
|  | ||||
|   .color-picker-popover-container { | ||||
|     margin-top: -0.25rem; | ||||
|  | ||||
|     :root[dir="ltr"] & { | ||||
|       margin-left: 0.5rem; | ||||
|     } | ||||
|  | ||||
|     :root[dir="rtl"] & { | ||||
|       margin-left: -3rem; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .color-picker-triangle { | ||||
| @@ -30,20 +47,29 @@ | ||||
|     border-width: 0 9px 10px; | ||||
|     border-color: transparent transparent var(--popup-bg-color); | ||||
|     position: absolute; | ||||
|     top: -10px; | ||||
|     top: 10px; | ||||
|  | ||||
|     :root[dir="ltr"] & { | ||||
|       left: 12px; | ||||
|       transform: rotate(270deg); | ||||
|       left: -14px; | ||||
|     } | ||||
|  | ||||
|     :root[dir="rtl"] & { | ||||
|       right: 12px; | ||||
|       transform: rotate(90deg); | ||||
|       right: -14px; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .color-picker-triangle-shadow { | ||||
|     border-color: transparent transparent transparentize($oc-black, 0.9); | ||||
|     top: -11px; | ||||
|  | ||||
|     :root[dir="ltr"] & { | ||||
|       left: -14px; | ||||
|     } | ||||
|  | ||||
|     :root[dir="rtl"] & { | ||||
|       right: -16px; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .color-picker-content--default { | ||||
| @@ -119,16 +145,21 @@ | ||||
|   } | ||||
|  | ||||
|   .color-picker-hash { | ||||
|     background: var(--input-border-color); | ||||
|     height: 1.875rem; | ||||
|     width: 1.875rem; | ||||
|     height: var(--default-button-size); | ||||
|     flex-shrink: 0; | ||||
|     padding: 0.5rem 0.5rem 0.5rem 0.75rem; | ||||
|     border: 1px solid var(--default-border-color); | ||||
|     border-right: 0; | ||||
|     box-sizing: border-box; | ||||
|  | ||||
|     :root[dir="ltr"] & { | ||||
|       border-radius: 4px 0 0 4px; | ||||
|       border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg); | ||||
|     } | ||||
|  | ||||
|     :root[dir="rtl"] & { | ||||
|       border-radius: 0 4px 4px 0; | ||||
|       border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0; | ||||
|       border-right: 1px solid var(--default-border-color); | ||||
|       border-left: 0; | ||||
|     } | ||||
|  | ||||
|     color: var(--input-label-color); | ||||
| @@ -138,81 +169,64 @@ | ||||
|     position: relative; | ||||
|   } | ||||
|  | ||||
|   .color-input-container:focus-within .color-picker-hash { | ||||
|     box-shadow: 0 0 0 2px var(--focus-highlight-color); | ||||
|   } | ||||
|  | ||||
|   .color-input-container:focus-within .color-picker-hash::before, | ||||
|   .color-input-container:focus-within .color-picker-hash::after { | ||||
|     content: ""; | ||||
|     width: 1px; | ||||
|     height: 100%; | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|   } | ||||
|  | ||||
|   .color-input-container:focus-within .color-picker-hash::before { | ||||
|     background: var(--input-border-color); | ||||
|  | ||||
|     :root[dir="ltr"] & { | ||||
|       right: -1px; | ||||
|     } | ||||
|  | ||||
|     :root[dir="rtl"] & { | ||||
|       left: -1px; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .color-input-container:focus-within .color-picker-hash::after { | ||||
|     background: var(--input-bg-color); | ||||
|  | ||||
|     :root[dir="ltr"] & { | ||||
|       right: -2px; | ||||
|     } | ||||
|  | ||||
|     :root[dir="rtl"] & { | ||||
|       left: -2px; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .color-input-container { | ||||
|     display: flex; | ||||
|  | ||||
|     &:focus-within { | ||||
|       box-shadow: 0 0 0 1px var(--color-primary-darkest); | ||||
|       border-radius: var(--border-radius-lg); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .color-picker-input { | ||||
|     width: 11ch; /* length of `transparent` */ | ||||
|     box-sizing: border-box; | ||||
|     width: 100%; | ||||
|     margin: 0; | ||||
|     font-size: 1rem; | ||||
|     background-color: var(--input-bg-color); | ||||
|     font-size: 0.875rem; | ||||
|     background-color: transparent; | ||||
|     color: var(--text-primary-color); | ||||
|     border: 0; | ||||
|     outline: none; | ||||
|     height: 1.75em; | ||||
|     box-shadow: var(--input-border-color) 0 0 0 1px inset; | ||||
|     height: var(--default-button-size); | ||||
|     border: 1px solid var(--default-border-color); | ||||
|     border-left: 0; | ||||
|     letter-spacing: 0.4px; | ||||
|  | ||||
|     :root[dir="ltr"] & { | ||||
|       border-radius: 0 4px 4px 0; | ||||
|       border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0; | ||||
|     } | ||||
|  | ||||
|     :root[dir="rtl"] & { | ||||
|       border-radius: 4px 0 0 4px; | ||||
|       border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg); | ||||
|       border-left: 1px solid var(--default-border-color); | ||||
|       border-right: 0; | ||||
|     } | ||||
|  | ||||
|     float: left; | ||||
|     padding: 1px; | ||||
|     padding-inline-start: 0.5em; | ||||
|     padding: 0.5rem; | ||||
|     padding-left: 0.25rem; | ||||
|     appearance: none; | ||||
|  | ||||
|     &:focus-visible { | ||||
|       box-shadow: none; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .color-picker-label-swatch-container { | ||||
|     border: 1px solid var(--default-border-color); | ||||
|     border-radius: var(--border-radius-lg); | ||||
|     width: var(--default-button-size); | ||||
|     height: var(--default-button-size); | ||||
|     box-sizing: border-box; | ||||
|     overflow: hidden; | ||||
|   } | ||||
|  | ||||
|   .color-picker-label-swatch { | ||||
|     height: 1.875rem; | ||||
|     width: 1.875rem; | ||||
|     margin-inline-end: 0.25rem; | ||||
|     border: 1px solid $oc-gray-3; | ||||
|     position: relative; | ||||
|     @include outlineButtonStyles; | ||||
|     background-color: var(--swatch-color) !important; | ||||
|     overflow: hidden; | ||||
|     background-color: transparent !important; | ||||
|     position: relative; | ||||
|     filter: var(--theme-filter); | ||||
|     border: 0 !important; | ||||
|  | ||||
|     &:after { | ||||
|       content: ""; | ||||
|   | ||||
| @@ -343,6 +343,8 @@ const ColorInput = React.forwardRef( | ||||
|   }, | ||||
| ); | ||||
|  | ||||
| ColorInput.displayName = "ColorInput"; | ||||
|  | ||||
| export const ColorPicker = ({ | ||||
|   type, | ||||
|   color, | ||||
| @@ -363,17 +365,20 @@ export const ColorPicker = ({ | ||||
|   appState: AppState; | ||||
| }) => { | ||||
|   const pickerButton = React.useRef<HTMLButtonElement>(null); | ||||
|   const coords = pickerButton.current?.getBoundingClientRect(); | ||||
|  | ||||
|   return ( | ||||
|     <div> | ||||
|       <div className="color-picker-control-container"> | ||||
|         <button | ||||
|           className="color-picker-label-swatch" | ||||
|           aria-label={label} | ||||
|           style={color ? { "--swatch-color": color } : undefined} | ||||
|           onClick={() => setActive(!isActive)} | ||||
|           ref={pickerButton} | ||||
|         /> | ||||
|         <div className="color-picker-label-swatch-container"> | ||||
|           <button | ||||
|             className="color-picker-label-swatch" | ||||
|             aria-label={label} | ||||
|             style={color ? { "--swatch-color": color } : undefined} | ||||
|             onClick={() => setActive(!isActive)} | ||||
|             ref={pickerButton} | ||||
|           /> | ||||
|         </div> | ||||
|         <ColorInput | ||||
|           color={color} | ||||
|           label={label} | ||||
| @@ -384,27 +389,37 @@ export const ColorPicker = ({ | ||||
|       </div> | ||||
|       <React.Suspense fallback=""> | ||||
|         {isActive ? ( | ||||
|           <Popover | ||||
|             onCloseRequest={(event) => | ||||
|               event.target !== pickerButton.current && setActive(false) | ||||
|             } | ||||
|           <div | ||||
|             className="color-picker-popover-container" | ||||
|             style={{ | ||||
|               position: "fixed", | ||||
|               top: coords?.top, | ||||
|               left: coords?.right, | ||||
|               zIndex: 1, | ||||
|             }} | ||||
|           > | ||||
|             <Picker | ||||
|               colors={colors[type]} | ||||
|               color={color || null} | ||||
|               onChange={(changedColor) => { | ||||
|                 onChange(changedColor); | ||||
|               }} | ||||
|               onClose={() => { | ||||
|                 setActive(false); | ||||
|                 pickerButton.current?.focus(); | ||||
|               }} | ||||
|               label={label} | ||||
|               showInput={false} | ||||
|               type={type} | ||||
|               elements={elements} | ||||
|             /> | ||||
|           </Popover> | ||||
|             <Popover | ||||
|               onCloseRequest={(event) => | ||||
|                 event.target !== pickerButton.current && setActive(false) | ||||
|               } | ||||
|             > | ||||
|               <Picker | ||||
|                 colors={colors[type]} | ||||
|                 color={color || null} | ||||
|                 onChange={(changedColor) => { | ||||
|                   onChange(changedColor); | ||||
|                 }} | ||||
|                 onClose={() => { | ||||
|                   setActive(false); | ||||
|                   pickerButton.current?.focus(); | ||||
|                 }} | ||||
|                 label={label} | ||||
|                 showInput={false} | ||||
|                 type={type} | ||||
|                 elements={elements} | ||||
|               /> | ||||
|             </Popover> | ||||
|           </div> | ||||
|         ) : null} | ||||
|       </React.Suspense> | ||||
|     </div> | ||||
|   | ||||
| @@ -4,34 +4,8 @@ | ||||
|   .confirm-dialog { | ||||
|     &-buttons { | ||||
|       display: flex; | ||||
|       padding: 0.2rem 0; | ||||
|       column-gap: 0.5rem; | ||||
|       justify-content: flex-end; | ||||
|     } | ||||
|     .ToolIcon__icon { | ||||
|       min-width: 2.5rem; | ||||
|       width: auto; | ||||
|       font-size: 1rem; | ||||
|     } | ||||
|  | ||||
|     .ToolIcon_type_button { | ||||
|       margin-left: 0.8rem; | ||||
|       padding: 0 0.5rem; | ||||
|     } | ||||
|  | ||||
|     &__content { | ||||
|       font-size: 1rem; | ||||
|     } | ||||
|  | ||||
|     &--confirm.ToolIcon_type_button { | ||||
|       background-color: $oc-red-6; | ||||
|  | ||||
|       &:hover { | ||||
|         background-color: $oc-red-8; | ||||
|       } | ||||
|  | ||||
|       .ToolIcon__icon { | ||||
|         color: $oc-white; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,11 @@ | ||||
| import { t } from "../i18n"; | ||||
| import { Dialog, DialogProps } from "./Dialog"; | ||||
| import { ToolButton } from "./ToolButton"; | ||||
|  | ||||
| import "./ConfirmDialog.scss"; | ||||
| import DialogActionButton from "./DialogActionButton"; | ||||
| import { isMenuOpenAtom } from "./App"; | ||||
| import { isDropdownOpenAtom } from "./App"; | ||||
| import { useSetAtom } from "jotai"; | ||||
|  | ||||
| interface Props extends Omit<DialogProps, "onCloseRequest"> { | ||||
|   onConfirm: () => void; | ||||
| @@ -20,6 +23,10 @@ const ConfirmDialog = (props: Props) => { | ||||
|     className = "", | ||||
|     ...rest | ||||
|   } = props; | ||||
|  | ||||
|   const setIsMenuOpen = useSetAtom(isMenuOpenAtom); | ||||
|   const setIsDropdownOpen = useSetAtom(isDropdownOpenAtom); | ||||
|  | ||||
|   return ( | ||||
|     <Dialog | ||||
|       onCloseRequest={onCancel} | ||||
| @@ -29,21 +36,22 @@ const ConfirmDialog = (props: Props) => { | ||||
|     > | ||||
|       {children} | ||||
|       <div className="confirm-dialog-buttons"> | ||||
|         <ToolButton | ||||
|           type="button" | ||||
|           title={cancelText} | ||||
|           aria-label={cancelText} | ||||
|         <DialogActionButton | ||||
|           label={cancelText} | ||||
|           onClick={onCancel} | ||||
|           className="confirm-dialog--cancel" | ||||
|           onClick={() => { | ||||
|             setIsMenuOpen(false); | ||||
|             setIsDropdownOpen(false); | ||||
|             onCancel(); | ||||
|           }} | ||||
|         /> | ||||
|         <ToolButton | ||||
|           type="button" | ||||
|           title={confirmText} | ||||
|           aria-label={confirmText} | ||||
|         <DialogActionButton | ||||
|           label={confirmText} | ||||
|           onClick={onConfirm} | ||||
|           className="confirm-dialog--confirm" | ||||
|           onClick={() => { | ||||
|             setIsMenuOpen(false); | ||||
|             setIsDropdownOpen(false); | ||||
|             onConfirm(); | ||||
|           }} | ||||
|           actionType="danger" | ||||
|         /> | ||||
|       </div> | ||||
|     </Dialog> | ||||
|   | ||||
| @@ -7,68 +7,11 @@ | ||||
|   } | ||||
|  | ||||
|   .Dialog__title { | ||||
|     display: grid; | ||||
|     align-items: center; | ||||
|     margin-top: 0; | ||||
|     grid-template-columns: 1fr calc(var(--space-factor) * 7); | ||||
|     grid-gap: var(--metric); | ||||
|     padding: calc(var(--space-factor) * 2); | ||||
|     text-align: center; | ||||
|     font-variant: small-caps; | ||||
|     font-size: 1.2em; | ||||
|   } | ||||
|  | ||||
|   .Dialog__titleContent { | ||||
|     flex: 1; | ||||
|   } | ||||
|  | ||||
|   .Dialog .Modal__close { | ||||
|     color: var(--icon-fill-color); | ||||
|     margin: 0; | ||||
|   } | ||||
|  | ||||
|   .Dialog__content { | ||||
|     padding: 0 16px 16px; | ||||
|   } | ||||
|  | ||||
|   @include isMobile { | ||||
|     .Dialog { | ||||
|       --metric: calc(var(--space-factor) * 4); | ||||
|       --inset-left: #{"max(var(--metric), var(--sal))"}; | ||||
|       --inset-right: #{"max(var(--metric), var(--sar))"}; | ||||
|     } | ||||
|  | ||||
|     .Dialog__title { | ||||
|       grid-template-columns: calc(var(--space-factor) * 7) 1fr calc( | ||||
|           var(--space-factor) * 7 | ||||
|         ); | ||||
|       position: sticky; | ||||
|       top: 0; | ||||
|       padding: calc(var(--space-factor) * 2); | ||||
|       background: var(--island-bg-color); | ||||
|       font-size: 1.25em; | ||||
|  | ||||
|       box-sizing: border-box; | ||||
|       border-bottom: 1px solid var(--button-gray-2); | ||||
|       z-index: 1; | ||||
|     } | ||||
|  | ||||
|     .Dialog__titleContent { | ||||
|       text-align: center; | ||||
|     } | ||||
|  | ||||
|     .Dialog .Island { | ||||
|       width: 100vw; | ||||
|       height: 100%; | ||||
|       box-sizing: border-box; | ||||
|       overflow-y: auto; | ||||
|       padding-left: #{"max(calc(var(--padding) * var(--space-factor)), var(--sal))"}; | ||||
|       padding-right: #{"max(calc(var(--padding) * var(--space-factor)), var(--sar))"}; | ||||
|       padding-bottom: #{"max(calc(var(--padding) * var(--space-factor)), var(--sab))"}; | ||||
|     } | ||||
|  | ||||
|     .Dialog .Modal__close { | ||||
|       order: -1; | ||||
|     } | ||||
|     text-align: left; | ||||
|     font-size: 1.25rem; | ||||
|     border-bottom: 1px solid var(--dialog-border-color); | ||||
|     padding: 0 0 0.75rem; | ||||
|     margin-bottom: 1.5rem; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -5,11 +5,13 @@ import { t } from "../i18n"; | ||||
| import { useExcalidrawContainer, useDevice } from "../components/App"; | ||||
| import { KEYS } from "../keys"; | ||||
| import "./Dialog.scss"; | ||||
| import { back, close } from "./icons"; | ||||
| import { back, CloseIcon } from "./icons"; | ||||
| import { Island } from "./Island"; | ||||
| import { Modal } from "./Modal"; | ||||
| import { AppState } from "../types"; | ||||
| import { queryFocusableElements } from "../utils"; | ||||
| import { isMenuOpenAtom, isDropdownOpenAtom } from "./App"; | ||||
| import { useSetAtom } from "jotai"; | ||||
|  | ||||
| export interface DialogProps { | ||||
|   children: React.ReactNode; | ||||
| @@ -65,7 +67,12 @@ export const Dialog = (props: DialogProps) => { | ||||
|     return () => islandNode.removeEventListener("keydown", handleKeyDown); | ||||
|   }, [islandNode, props.autofocus]); | ||||
|  | ||||
|   const setIsMenuOpen = useSetAtom(isMenuOpenAtom); | ||||
|   const setIsDropdownOpen = useSetAtom(isDropdownOpenAtom); | ||||
|  | ||||
|   const onClose = () => { | ||||
|     setIsMenuOpen(false); | ||||
|     setIsDropdownOpen(false); | ||||
|     (lastActiveElement as HTMLElement).focus(); | ||||
|     props.onCloseRequest(); | ||||
|   }; | ||||
| @@ -85,9 +92,10 @@ export const Dialog = (props: DialogProps) => { | ||||
|           <button | ||||
|             className="Modal__close" | ||||
|             onClick={onClose} | ||||
|             title={t("buttons.close")} | ||||
|             aria-label={t("buttons.close")} | ||||
|           > | ||||
|             {useDevice().isMobile ? back : close} | ||||
|             {useDevice().isMobile ? back : CloseIcon} | ||||
|           </button> | ||||
|         </h2> | ||||
|         <div className="Dialog__content">{props.children}</div> | ||||
|   | ||||
							
								
								
									
										47
									
								
								src/components/DialogActionButton.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,47 @@ | ||||
| .excalidraw { | ||||
|   .Dialog__action-button { | ||||
|     position: relative; | ||||
|     display: flex; | ||||
|     column-gap: 0.5rem; | ||||
|     align-items: center; | ||||
|     padding: 0.5rem 1.5rem; | ||||
|     border: 1px solid var(--default-border-color); | ||||
|     background-color: transparent; | ||||
|     height: 3rem; | ||||
|     border-radius: var(--border-radius-lg); | ||||
|     letter-spacing: 0.4px; | ||||
|     color: inherit; | ||||
|     font-family: inherit; | ||||
|     font-size: 0.875rem; | ||||
|     font-weight: 600; | ||||
|     user-select: none; | ||||
|  | ||||
|     svg { | ||||
|       display: block; | ||||
|       width: 1rem; | ||||
|       height: 1rem; | ||||
|     } | ||||
|  | ||||
|     &--danger { | ||||
|       background-color: var(--color-danger); | ||||
|       border-color: var(--color-danger); | ||||
|       color: #fff; | ||||
|     } | ||||
|  | ||||
|     &--primary { | ||||
|       background-color: var(--color-primary); | ||||
|       border-color: var(--color-primary); | ||||
|       color: #fff; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &.theme--dark { | ||||
|     .Dialog__action-button--danger { | ||||
|       color: var(--color-gray-100); | ||||
|     } | ||||
|  | ||||
|     .Dialog__action-button--primary { | ||||
|       color: var(--color-gray-100); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										46
									
								
								src/components/DialogActionButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,46 @@ | ||||
| import clsx from "clsx"; | ||||
| import { ReactNode } from "react"; | ||||
| import "./DialogActionButton.scss"; | ||||
| import Spinner from "./Spinner"; | ||||
|  | ||||
| interface DialogActionButtonProps { | ||||
|   label: string; | ||||
|   children?: ReactNode; | ||||
|   actionType?: "primary" | "danger"; | ||||
|   isLoading?: boolean; | ||||
| } | ||||
|  | ||||
| const DialogActionButton = ({ | ||||
|   label, | ||||
|   onClick, | ||||
|   className, | ||||
|   children, | ||||
|   actionType, | ||||
|   type = "button", | ||||
|   isLoading, | ||||
|   ...rest | ||||
| }: DialogActionButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => { | ||||
|   const cs = actionType ? `Dialog__action-button--${actionType}` : ""; | ||||
|  | ||||
|   return ( | ||||
|     <button | ||||
|       className={clsx("Dialog__action-button", cs, className)} | ||||
|       type={type} | ||||
|       aria-label={label} | ||||
|       onClick={onClick} | ||||
|       {...rest} | ||||
|     > | ||||
|       {children && ( | ||||
|         <div style={isLoading ? { visibility: "hidden" } : {}}>{children}</div> | ||||
|       )} | ||||
|       <div style={isLoading ? { visibility: "hidden" } : {}}>{label}</div> | ||||
|       {isLoading && ( | ||||
|         <div style={{ position: "absolute", inset: 0 }}> | ||||
|           <Spinner /> | ||||
|         </div> | ||||
|       )} | ||||
|     </button> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default DialogActionButton; | ||||
							
								
								
									
										19
									
								
								src/components/EncryptedIcon.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | ||||
| import { t } from "../i18n"; | ||||
| import { shield } from "./icons"; | ||||
| import { Tooltip } from "./Tooltip"; | ||||
|  | ||||
| const EncryptedIcon = () => ( | ||||
|   <a | ||||
|     className="encrypted-icon tooltip" | ||||
|     href="https://blog.excalidraw.com/end-to-end-encryption/" | ||||
|     target="_blank" | ||||
|     rel="noopener noreferrer" | ||||
|     aria-label={t("encrypted.link")} | ||||
|   > | ||||
|     <Tooltip label={t("encrypted.tooltip")} long={true}> | ||||
|       {shield} | ||||
|     </Tooltip> | ||||
|   </a> | ||||
| ); | ||||
|  | ||||
| export default EncryptedIcon; | ||||
| @@ -91,6 +91,8 @@ | ||||
|   } | ||||
|  | ||||
|   button.ExportDialog-imageExportButton { | ||||
|     border: 0; | ||||
|  | ||||
|     width: 5rem; | ||||
|     height: 5rem; | ||||
|     margin: 0 0.2em; | ||||
|   | ||||
| @@ -9,9 +9,10 @@ | ||||
|   } | ||||
|  | ||||
|   .FixedSideContainer_side_top { | ||||
|     left: var(--space-factor); | ||||
|     top: var(--space-factor); | ||||
|     right: var(--space-factor); | ||||
|     left: 1rem; | ||||
|     top: 1rem; | ||||
|     right: 1rem; | ||||
|     bottom: 1rem; | ||||
|     z-index: 2; | ||||
|   } | ||||
|  | ||||
|   | ||||
							
								
								
									
										109
									
								
								src/components/Footer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,109 @@ | ||||
| import clsx from "clsx"; | ||||
| import { ActionManager } from "../actions/manager"; | ||||
| import { t } from "../i18n"; | ||||
| import { AppState, ExcalidrawProps } from "../types"; | ||||
| import { | ||||
|   ExitZenModeAction, | ||||
|   FinalizeAction, | ||||
|   UndoRedoActions, | ||||
|   ZoomActions, | ||||
| } from "./Actions"; | ||||
| import { useDevice } from "./App"; | ||||
| import { WelcomeScreenHelpArrow } from "./icons"; | ||||
| import { Section } from "./Section"; | ||||
| import Stack from "./Stack"; | ||||
| import WelcomeScreenDecor from "./WelcomeScreenDecor"; | ||||
|  | ||||
| const Footer = ({ | ||||
|   appState, | ||||
|   actionManager, | ||||
|   renderCustomFooter, | ||||
|   showExitZenModeBtn, | ||||
|   renderWelcomeScreen, | ||||
| }: { | ||||
|   appState: AppState; | ||||
|   actionManager: ActionManager; | ||||
|   renderCustomFooter?: ExcalidrawProps["renderFooter"]; | ||||
|   showExitZenModeBtn: boolean; | ||||
|   renderWelcomeScreen: boolean; | ||||
| }) => { | ||||
|   const device = useDevice(); | ||||
|   const showFinalize = | ||||
|     !appState.viewModeEnabled && appState.multiElement && device.isTouchScreen; | ||||
|   return ( | ||||
|     <footer | ||||
|       role="contentinfo" | ||||
|       className="layer-ui__wrapper__footer App-menu App-menu_bottom" | ||||
|     > | ||||
|       <div | ||||
|         className={clsx("layer-ui__wrapper__footer-left zen-mode-transition", { | ||||
|           "layer-ui__wrapper__footer-left--transition-left": | ||||
|             appState.zenModeEnabled, | ||||
|         })} | ||||
|       > | ||||
|         <Stack.Col gap={2}> | ||||
|           <Section heading="canvasActions"> | ||||
|             <ZoomActions | ||||
|               renderAction={actionManager.renderAction} | ||||
|               zoom={appState.zoom} | ||||
|             /> | ||||
|  | ||||
|             {!appState.viewModeEnabled && ( | ||||
|               <UndoRedoActions | ||||
|                 renderAction={actionManager.renderAction} | ||||
|                 className={clsx("zen-mode-transition", { | ||||
|                   "layer-ui__wrapper__footer-left--transition-bottom": | ||||
|                     appState.zenModeEnabled, | ||||
|                 })} | ||||
|               /> | ||||
|             )} | ||||
|             {showFinalize && ( | ||||
|               <FinalizeAction | ||||
|                 renderAction={actionManager.renderAction} | ||||
|                 className={clsx("zen-mode-transition", { | ||||
|                   "layer-ui__wrapper__footer-left--transition-left": | ||||
|                     appState.zenModeEnabled, | ||||
|                 })} | ||||
|               /> | ||||
|             )} | ||||
|           </Section> | ||||
|         </Stack.Col> | ||||
|       </div> | ||||
|       <div | ||||
|         className={clsx( | ||||
|           "layer-ui__wrapper__footer-center zen-mode-transition", | ||||
|           { | ||||
|             "layer-ui__wrapper__footer-left--transition-bottom": | ||||
|               appState.zenModeEnabled, | ||||
|           }, | ||||
|         )} | ||||
|       > | ||||
|         {renderCustomFooter?.(false, appState)} | ||||
|       </div> | ||||
|       <div | ||||
|         className={clsx("layer-ui__wrapper__footer-right zen-mode-transition", { | ||||
|           "transition-right disable-pointerEvents": appState.zenModeEnabled, | ||||
|         })} | ||||
|       > | ||||
|         <div style={{ position: "relative" }}> | ||||
|           <WelcomeScreenDecor | ||||
|             shouldRender={renderWelcomeScreen && !appState.isLoading} | ||||
|           > | ||||
|             <div className="virgil WelcomeScreen-decor WelcomeScreen-decor--help-pointer"> | ||||
|               <div>{t("welcomeScreen.helpHints")}</div> | ||||
|               {WelcomeScreenHelpArrow} | ||||
|             </div> | ||||
|           </WelcomeScreenDecor> | ||||
|  | ||||
|           {actionManager.renderAction("toggleShortcuts")} | ||||
|         </div> | ||||
|       </div> | ||||
|       <ExitZenModeAction | ||||
|         actionManager={actionManager} | ||||
|         showExitZenModeBtn={showExitZenModeBtn} | ||||
|       /> | ||||
|     </footer> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default Footer; | ||||
| @@ -1,13 +1,13 @@ | ||||
| import { questionCircle } from "../components/icons"; | ||||
| import { HelpIcon } from "./icons"; | ||||
| 
 | ||||
| type HelpIconProps = { | ||||
| type HelpButtonProps = { | ||||
|   title?: string; | ||||
|   name?: string; | ||||
|   id?: string; | ||||
|   onClick?(): void; | ||||
| }; | ||||
| 
 | ||||
| export const HelpIcon = (props: HelpIconProps) => ( | ||||
| export const HelpButton = (props: HelpButtonProps) => ( | ||||
|   <button | ||||
|     className="help-icon" | ||||
|     onClick={props.onClick} | ||||
| @@ -15,6 +15,6 @@ export const HelpIcon = (props: HelpIconProps) => ( | ||||
|     title={`${props.title} — ?`} | ||||
|     aria-label={props.title} | ||||
|   > | ||||
|     {questionCircle} | ||||
|     {HelpIcon} | ||||
|   </button> | ||||
| ); | ||||
| @@ -1,56 +1,115 @@ | ||||
| @import "../css/variables.module"; | ||||
|  | ||||
| .excalidraw { | ||||
|   .HelpDialog h3 { | ||||
|     border-bottom: 1px solid var(--button-gray-2); | ||||
|     padding-bottom: 4px; | ||||
|   } | ||||
|   .HelpDialog { | ||||
|     .Modal__content { | ||||
|       max-width: 960px; | ||||
|     } | ||||
|  | ||||
|   .HelpDialog--island { | ||||
|     border: 1px solid var(--button-gray-2); | ||||
|     margin-bottom: 16px; | ||||
|   } | ||||
|     h3 { | ||||
|       margin: 1.5rem 0; | ||||
|       font-weight: bold; | ||||
|       font-size: 1.125rem; | ||||
|     } | ||||
|  | ||||
|   .HelpDialog--island-title { | ||||
|     margin: 0; | ||||
|     padding: 4px; | ||||
|     background-color: var(--button-gray-1); | ||||
|     text-align: center; | ||||
|   } | ||||
|     &__header { | ||||
|       display: flex; | ||||
|       flex-wrap: wrap; | ||||
|       gap: 0.75rem; | ||||
|     } | ||||
|  | ||||
|   .HelpDialog--shortcut { | ||||
|     border-top: 1px solid var(--button-gray-2); | ||||
|   } | ||||
|     &__btn { | ||||
|       display: flex; | ||||
|       column-gap: 0.5rem; | ||||
|       align-items: center; | ||||
|       border: 1px solid var(--default-border-color); | ||||
|       padding: 0.625rem 1rem; | ||||
|       border-radius: var(--border-radius-lg); | ||||
|       color: var(--text-primary-color); | ||||
|       font-weight: 600; | ||||
|       font-size: 0.75rem; | ||||
|       letter-spacing: 0.4px; | ||||
|  | ||||
|   .HelpDialog--key { | ||||
|     word-break: keep-all; | ||||
|     border: 1px solid var(--button-gray-2); | ||||
|     padding: 2px 8px; | ||||
|     margin: auto 4px; | ||||
|     background-color: var(--button-gray-1); | ||||
|     border-radius: 2px; | ||||
|     font-size: 0.8em; | ||||
|     min-height: 26px; | ||||
|     box-sizing: border-box; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     font-family: inherit; | ||||
|   } | ||||
|       &:hover { | ||||
|         text-decoration: none; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|   .HelpDialog--header { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     justify-content: space-evenly; | ||||
|     margin-bottom: 32px; | ||||
|     padding-bottom: 16px; | ||||
|   } | ||||
|     &__link-icon { | ||||
|       line-height: 0; | ||||
|       svg { | ||||
|         width: 1rem; | ||||
|         height: 1rem; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|   .HelpDialog--btn { | ||||
|     border: 1px solid var(--link-color); | ||||
|     padding: 8px 32px; | ||||
|     border-radius: 4px; | ||||
|   } | ||||
|   .HelpDialog--btn:hover { | ||||
|     text-decoration: none; | ||||
|     &__islands-container { | ||||
|       display: grid; | ||||
|       @media screen and (min-width: 1024px) { | ||||
|         grid-template-columns: 1fr 1fr; | ||||
|       } | ||||
|       grid-column-gap: 1.5rem; | ||||
|       grid-row-gap: 2rem; | ||||
|     } | ||||
|  | ||||
|     @media screen and (min-width: 1024px) { | ||||
|       &__island--tools { | ||||
|         grid-area: 1 / 1 / 2 / 2; | ||||
|       } | ||||
|       &__island--view { | ||||
|         grid-area: 2 / 1 / 3 / 2; | ||||
|       } | ||||
|       &__island--editor { | ||||
|         grid-area: 1 / 2 / 3 / 3; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     &__island { | ||||
|       h4 { | ||||
|         font-size: 1rem; | ||||
|         font-weight: bold; | ||||
|         margin: 0; | ||||
|         margin-bottom: 0.625rem; | ||||
|       } | ||||
|  | ||||
|       &-content { | ||||
|         border: 1px solid var(--dialog-border-color); | ||||
|         border-radius: var(--border-radius-lg); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     &__shortcut { | ||||
|       border-bottom: 1px solid var(--dialog-border-color); | ||||
|       padding: 0.375rem 0.75rem; | ||||
|       display: flex; | ||||
|       justify-content: space-between; | ||||
|       align-items: center; | ||||
|       font-size: 0.875rem; | ||||
|       column-gap: 0.5rem; | ||||
|  | ||||
|       &:last-child { | ||||
|         border-bottom: none; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     &__key-container { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       column-gap: 0.25rem; | ||||
|       flex-shrink: 0; | ||||
|     } | ||||
|  | ||||
|     &__key { | ||||
|       display: flex; | ||||
|       box-sizing: border-box; | ||||
|       font-size: 0.625rem; | ||||
|       background-color: var(--color-primary-light); | ||||
|       border-radius: var(--border-radius-md); | ||||
|       padding: 0.5rem; | ||||
|       word-break: keep-all; | ||||
|       align-items: center; | ||||
|       font-family: inherit; | ||||
|       line-height: 1; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -4,32 +4,36 @@ import { isDarwin, isWindows } from "../keys"; | ||||
| import { Dialog } from "./Dialog"; | ||||
| import { getShortcutKey } from "../utils"; | ||||
| import "./HelpDialog.scss"; | ||||
| import { ExternalLinkIcon } from "./icons"; | ||||
|  | ||||
| const Header = () => ( | ||||
|   <div className="HelpDialog--header"> | ||||
|   <div className="HelpDialog__header"> | ||||
|     <a | ||||
|       className="HelpDialog--btn" | ||||
|       className="HelpDialog__btn" | ||||
|       href="https://github.com/excalidraw/excalidraw#documentation" | ||||
|       target="_blank" | ||||
|       rel="noopener noreferrer" | ||||
|     > | ||||
|       {t("helpDialog.documentation")} | ||||
|       <div className="HelpDialog__link-icon">{ExternalLinkIcon}</div> | ||||
|     </a> | ||||
|     <a | ||||
|       className="HelpDialog--btn" | ||||
|       className="HelpDialog__btn" | ||||
|       href="https://blog.excalidraw.com" | ||||
|       target="_blank" | ||||
|       rel="noopener noreferrer" | ||||
|     > | ||||
|       {t("helpDialog.blog")} | ||||
|       <div className="HelpDialog__link-icon">{ExternalLinkIcon}</div> | ||||
|     </a> | ||||
|     <a | ||||
|       className="HelpDialog--btn" | ||||
|       className="HelpDialog__btn" | ||||
|       href="https://github.com/excalidraw/excalidraw/issues" | ||||
|       target="_blank" | ||||
|       rel="noopener noreferrer" | ||||
|     > | ||||
|       {t("helpDialog.github")} | ||||
|       <div className="HelpDialog__link-icon">{ExternalLinkIcon}</div> | ||||
|     </a> | ||||
|   </div> | ||||
| ); | ||||
| @@ -37,88 +41,61 @@ const Header = () => ( | ||||
| const Section = (props: { title: string; children: React.ReactNode }) => ( | ||||
|   <> | ||||
|     <h3>{props.title}</h3> | ||||
|     {props.children} | ||||
|     <div className="HelpDialog__islands-container">{props.children}</div> | ||||
|   </> | ||||
| ); | ||||
|  | ||||
| const Columns = (props: { children: React.ReactNode }) => ( | ||||
|   <div | ||||
|     style={{ | ||||
|       display: "flex", | ||||
|       flexDirection: "row", | ||||
|       flexWrap: "wrap", | ||||
|       justifyContent: "space-between", | ||||
|     }} | ||||
|   > | ||||
|     {props.children} | ||||
|   </div> | ||||
| ); | ||||
|  | ||||
| const Column = (props: { children: React.ReactNode }) => ( | ||||
|   <div style={{ width: "49%" }}>{props.children}</div> | ||||
| ); | ||||
|  | ||||
| const ShortcutIsland = (props: { | ||||
|   caption: string; | ||||
|   children: React.ReactNode; | ||||
|   className?: string; | ||||
| }) => ( | ||||
|   <div className="HelpDialog--island"> | ||||
|     <h3 className="HelpDialog--island-title">{props.caption}</h3> | ||||
|     {props.children} | ||||
|   <div className={`HelpDialog__island ${props.className}`}> | ||||
|     <h4 className="HelpDialog__island-title">{props.caption}</h4> | ||||
|     <div className="HelpDialog__island-content">{props.children}</div> | ||||
|   </div> | ||||
| ); | ||||
|  | ||||
| const Shortcut = (props: { | ||||
| function* intersperse(as: JSX.Element[][], delim: string | null) { | ||||
|   let first = true; | ||||
|   for (const x of as) { | ||||
|     if (!first) { | ||||
|       yield delim; | ||||
|     } | ||||
|     first = false; | ||||
|     yield x; | ||||
|   } | ||||
| } | ||||
|  | ||||
| const Shortcut = ({ | ||||
|   label, | ||||
|   shortcuts, | ||||
|   isOr = true, | ||||
| }: { | ||||
|   label: string; | ||||
|   shortcuts: string[]; | ||||
|   isOr: boolean; | ||||
|   isOr?: boolean; | ||||
| }) => { | ||||
|   const splitShortcutKeys = shortcuts.map((shortcut) => { | ||||
|     const keys = shortcut.endsWith("++") | ||||
|       ? [...shortcut.slice(0, -2).split("+"), "+"] | ||||
|       : shortcut.split("+"); | ||||
|  | ||||
|     return keys.map((key) => <ShortcutKey key={key}>{key}</ShortcutKey>); | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <div className="HelpDialog--shortcut"> | ||||
|       <div | ||||
|         style={{ | ||||
|           display: "flex", | ||||
|           margin: "0", | ||||
|           padding: "4px 8px", | ||||
|           alignItems: "center", | ||||
|         }} | ||||
|       > | ||||
|         <div | ||||
|           style={{ | ||||
|             lineHeight: 1.4, | ||||
|           }} | ||||
|         > | ||||
|           {props.label} | ||||
|         </div> | ||||
|         <div | ||||
|           style={{ | ||||
|             display: "flex", | ||||
|             flex: "0 0 auto", | ||||
|             justifyContent: "flex-end", | ||||
|             marginInlineStart: "auto", | ||||
|             minWidth: "30%", | ||||
|           }} | ||||
|         > | ||||
|           {props.shortcuts.map((shortcut, index) => ( | ||||
|             <React.Fragment key={index}> | ||||
|               <ShortcutKey>{shortcut}</ShortcutKey> | ||||
|               {props.isOr && | ||||
|                 index !== props.shortcuts.length - 1 && | ||||
|                 t("helpDialog.or")} | ||||
|             </React.Fragment> | ||||
|           ))} | ||||
|         </div> | ||||
|     <div className="HelpDialog__shortcut"> | ||||
|       <div>{label}</div> | ||||
|       <div className="HelpDialog__key-container"> | ||||
|         {[...intersperse(splitShortcutKeys, isOr ? t("helpDialog.or") : null)]} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| Shortcut.defaultProps = { | ||||
|   isOr: true, | ||||
| }; | ||||
|  | ||||
| const ShortcutKey = (props: { children: React.ReactNode }) => ( | ||||
|   <kbd className="HelpDialog--key" {...props} /> | ||||
|   <kbd className="HelpDialog__key" {...props} /> | ||||
| ); | ||||
|  | ||||
| export const HelpDialog = ({ onClose }: { onClose?: () => void }) => { | ||||
| @@ -137,286 +114,276 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => { | ||||
|       > | ||||
|         <Header /> | ||||
|         <Section title={t("helpDialog.shortcuts")}> | ||||
|           <Columns> | ||||
|             <Column> | ||||
|               <ShortcutIsland caption={t("helpDialog.tools")}> | ||||
|                 <Shortcut | ||||
|                   label={t("toolBar.selection")} | ||||
|                   shortcuts={["V", "1"]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("toolBar.rectangle")} | ||||
|                   shortcuts={["R", "2"]} | ||||
|                 /> | ||||
|                 <Shortcut label={t("toolBar.diamond")} shortcuts={["D", "3"]} /> | ||||
|                 <Shortcut label={t("toolBar.ellipse")} shortcuts={["O", "4"]} /> | ||||
|                 <Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} /> | ||||
|                 <Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} /> | ||||
|                 <Shortcut | ||||
|                   label={t("toolBar.freedraw")} | ||||
|                   shortcuts={["Shift + P", "X", "7"]} | ||||
|                 /> | ||||
|                 <Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} /> | ||||
|                 <Shortcut label={t("toolBar.image")} shortcuts={["9"]} /> | ||||
|                 <Shortcut label={t("toolBar.library")} shortcuts={["0"]} /> | ||||
|                 <Shortcut | ||||
|                   label={t("toolBar.eraser")} | ||||
|                   shortcuts={[getShortcutKey("E")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("helpDialog.editSelectedShape")} | ||||
|                   shortcuts={[ | ||||
|                     getShortcutKey("Enter"), | ||||
|                     t("helpDialog.doubleClick"), | ||||
|                   ]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("helpDialog.textNewLine")} | ||||
|                   shortcuts={[ | ||||
|                     getShortcutKey("Enter"), | ||||
|                     getShortcutKey("Shift+Enter"), | ||||
|                   ]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("helpDialog.textFinish")} | ||||
|                   shortcuts={[ | ||||
|                     getShortcutKey("Esc"), | ||||
|                     getShortcutKey("CtrlOrCmd+Enter"), | ||||
|                   ]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("helpDialog.curvedArrow")} | ||||
|                   shortcuts={[ | ||||
|                     "A", | ||||
|                     t("helpDialog.click"), | ||||
|                     t("helpDialog.click"), | ||||
|                     t("helpDialog.click"), | ||||
|                   ]} | ||||
|                   isOr={false} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("helpDialog.curvedLine")} | ||||
|                   shortcuts={[ | ||||
|                     "L", | ||||
|                     t("helpDialog.click"), | ||||
|                     t("helpDialog.click"), | ||||
|                     t("helpDialog.click"), | ||||
|                   ]} | ||||
|                   isOr={false} | ||||
|                 /> | ||||
|                 <Shortcut label={t("toolBar.lock")} shortcuts={["Q"]} /> | ||||
|                 <Shortcut | ||||
|                   label={t("helpDialog.preventBinding")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("toolBar.link")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+K")]} | ||||
|                 /> | ||||
|               </ShortcutIsland> | ||||
|               <ShortcutIsland caption={t("helpDialog.view")}> | ||||
|                 <Shortcut | ||||
|                   label={t("buttons.zoomIn")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd++")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("buttons.zoomOut")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+-")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("buttons.resetZoom")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+0")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("helpDialog.zoomToFit")} | ||||
|                   shortcuts={["Shift+1"]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("helpDialog.zoomToSelection")} | ||||
|                   shortcuts={["Shift+2"]} | ||||
|                 /> | ||||
|                 <Shortcut label={t("buttons.fullScreen")} shortcuts={["F"]} /> | ||||
|                 <Shortcut | ||||
|                   label={t("buttons.zenMode")} | ||||
|                   shortcuts={[getShortcutKey("Alt+Z")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.showGrid")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+'")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.viewMode")} | ||||
|                   shortcuts={[getShortcutKey("Alt+R")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.toggleTheme")} | ||||
|                   shortcuts={[getShortcutKey("Alt+Shift+D")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("stats.title")} | ||||
|                   shortcuts={[getShortcutKey("Alt+/")]} | ||||
|                 /> | ||||
|               </ShortcutIsland> | ||||
|             </Column> | ||||
|             <Column> | ||||
|               <ShortcutIsland caption={t("helpDialog.editor")}> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.selectAll")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+A")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.multiSelect")} | ||||
|                   shortcuts={[getShortcutKey(`Shift+${t("helpDialog.click")}`)]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("helpDialog.deepSelect")} | ||||
|                   shortcuts={[ | ||||
|                     getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`), | ||||
|                   ]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("helpDialog.deepBoxSelect")} | ||||
|                   shortcuts={[ | ||||
|                     getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`), | ||||
|                   ]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.moveCanvas")} | ||||
|                   shortcuts={[ | ||||
|                     getShortcutKey(`Space+${t("helpDialog.drag")}`), | ||||
|                     getShortcutKey(`Wheel+${t("helpDialog.drag")}`), | ||||
|                   ]} | ||||
|                   isOr={true} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.cut")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+X")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.copy")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+C")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.paste")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+V")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.copyAsPng")} | ||||
|                   shortcuts={[getShortcutKey("Shift+Alt+C")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.copyStyles")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.pasteStyles")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+Alt+V")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.delete")} | ||||
|                   shortcuts={[getShortcutKey("Del")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.sendToBack")} | ||||
|                   shortcuts={[ | ||||
|                     isDarwin | ||||
|                       ? getShortcutKey("CtrlOrCmd+Alt+[") | ||||
|                       : getShortcutKey("CtrlOrCmd+Shift+["), | ||||
|                   ]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.bringToFront")} | ||||
|                   shortcuts={[ | ||||
|                     isDarwin | ||||
|                       ? getShortcutKey("CtrlOrCmd+Alt+]") | ||||
|                       : getShortcutKey("CtrlOrCmd+Shift+]"), | ||||
|                   ]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.sendBackward")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+[")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.bringForward")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+]")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.alignTop")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Up")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.alignBottom")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Down")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.alignLeft")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Left")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.alignRight")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Right")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.duplicateSelection")} | ||||
|                   shortcuts={[ | ||||
|                     getShortcutKey("CtrlOrCmd+D"), | ||||
|                     getShortcutKey(`Alt+${t("helpDialog.drag")}`), | ||||
|                   ]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("helpDialog.toggleElementLock")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+Shift+L")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("buttons.undo")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+Z")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("buttons.redo")} | ||||
|                   shortcuts={ | ||||
|                     isWindows | ||||
|                       ? [ | ||||
|                           getShortcutKey("CtrlOrCmd+Y"), | ||||
|                           getShortcutKey("CtrlOrCmd+Shift+Z"), | ||||
|                         ] | ||||
|                       : [getShortcutKey("CtrlOrCmd+Shift+Z")] | ||||
|                   } | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.group")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+G")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.ungroup")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.flipHorizontal")} | ||||
|                   shortcuts={[getShortcutKey("Shift+H")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.flipVertical")} | ||||
|                   shortcuts={[getShortcutKey("Shift+V")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.showStroke")} | ||||
|                   shortcuts={[getShortcutKey("S")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.showBackground")} | ||||
|                   shortcuts={[getShortcutKey("G")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.decreaseFontSize")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+Shift+<")]} | ||||
|                 /> | ||||
|                 <Shortcut | ||||
|                   label={t("labels.increaseFontSize")} | ||||
|                   shortcuts={[getShortcutKey("CtrlOrCmd+Shift+>")]} | ||||
|                 /> | ||||
|               </ShortcutIsland> | ||||
|             </Column> | ||||
|           </Columns> | ||||
|           <ShortcutIsland | ||||
|             className="HelpDialog__island--tools" | ||||
|             caption={t("helpDialog.tools")} | ||||
|           > | ||||
|             <Shortcut label={t("toolBar.selection")} shortcuts={["V", "1"]} /> | ||||
|             <Shortcut label={t("toolBar.rectangle")} shortcuts={["R", "2"]} /> | ||||
|             <Shortcut label={t("toolBar.diamond")} shortcuts={["D", "3"]} /> | ||||
|             <Shortcut label={t("toolBar.ellipse")} shortcuts={["O", "4"]} /> | ||||
|             <Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} /> | ||||
|             <Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} /> | ||||
|             <Shortcut | ||||
|               label={t("toolBar.freedraw")} | ||||
|               shortcuts={["Shift + P", "X", "7"]} | ||||
|             /> | ||||
|             <Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} /> | ||||
|             <Shortcut label={t("toolBar.image")} shortcuts={["9"]} /> | ||||
|             <Shortcut label={t("toolBar.library")} shortcuts={["0"]} /> | ||||
|             <Shortcut | ||||
|               label={t("toolBar.eraser")} | ||||
|               shortcuts={[getShortcutKey("E")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("helpDialog.editSelectedShape")} | ||||
|               shortcuts={[getShortcutKey("Enter"), t("helpDialog.doubleClick")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("helpDialog.textNewLine")} | ||||
|               shortcuts={[ | ||||
|                 getShortcutKey("Enter"), | ||||
|                 getShortcutKey("Shift+Enter"), | ||||
|               ]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("helpDialog.textFinish")} | ||||
|               shortcuts={[ | ||||
|                 getShortcutKey("Esc"), | ||||
|                 getShortcutKey("CtrlOrCmd+Enter"), | ||||
|               ]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("helpDialog.curvedArrow")} | ||||
|               shortcuts={[ | ||||
|                 "A", | ||||
|                 t("helpDialog.click"), | ||||
|                 t("helpDialog.click"), | ||||
|                 t("helpDialog.click"), | ||||
|               ]} | ||||
|               isOr={false} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("helpDialog.curvedLine")} | ||||
|               shortcuts={[ | ||||
|                 "L", | ||||
|                 t("helpDialog.click"), | ||||
|                 t("helpDialog.click"), | ||||
|                 t("helpDialog.click"), | ||||
|               ]} | ||||
|               isOr={false} | ||||
|             /> | ||||
|             <Shortcut label={t("toolBar.lock")} shortcuts={["Q"]} /> | ||||
|             <Shortcut | ||||
|               label={t("helpDialog.preventBinding")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("toolBar.link")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+K")]} | ||||
|             /> | ||||
|           </ShortcutIsland> | ||||
|           <ShortcutIsland | ||||
|             className="HelpDialog__island--view" | ||||
|             caption={t("helpDialog.view")} | ||||
|           > | ||||
|             <Shortcut | ||||
|               label={t("buttons.zoomIn")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd++")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("buttons.zoomOut")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+-")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("buttons.resetZoom")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+0")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("helpDialog.zoomToFit")} | ||||
|               shortcuts={["Shift+1"]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("helpDialog.zoomToSelection")} | ||||
|               shortcuts={["Shift+2"]} | ||||
|             /> | ||||
|             <Shortcut label={t("buttons.fullScreen")} shortcuts={["F"]} /> | ||||
|             <Shortcut | ||||
|               label={t("buttons.zenMode")} | ||||
|               shortcuts={[getShortcutKey("Alt+Z")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.showGrid")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+'")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.viewMode")} | ||||
|               shortcuts={[getShortcutKey("Alt+R")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.toggleTheme")} | ||||
|               shortcuts={[getShortcutKey("Alt+Shift+D")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("stats.title")} | ||||
|               shortcuts={[getShortcutKey("Alt+/")]} | ||||
|             /> | ||||
|           </ShortcutIsland> | ||||
|           <ShortcutIsland | ||||
|             className="HelpDialog__island--editor" | ||||
|             caption={t("helpDialog.editor")} | ||||
|           > | ||||
|             <Shortcut | ||||
|               label={t("labels.selectAll")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+A")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.multiSelect")} | ||||
|               shortcuts={[getShortcutKey(`Shift+${t("helpDialog.click")}`)]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("helpDialog.deepSelect")} | ||||
|               shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`)]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("helpDialog.deepBoxSelect")} | ||||
|               shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`)]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.moveCanvas")} | ||||
|               shortcuts={[ | ||||
|                 getShortcutKey(`Space+${t("helpDialog.drag")}`), | ||||
|                 getShortcutKey(`Wheel+${t("helpDialog.drag")}`), | ||||
|               ]} | ||||
|               isOr={true} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.cut")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+X")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.copy")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+C")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.paste")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+V")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.copyAsPng")} | ||||
|               shortcuts={[getShortcutKey("Shift+Alt+C")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.copyStyles")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.pasteStyles")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+Alt+V")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.delete")} | ||||
|               shortcuts={[getShortcutKey("Del")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.sendToBack")} | ||||
|               shortcuts={[ | ||||
|                 isDarwin | ||||
|                   ? getShortcutKey("CtrlOrCmd+Alt+[") | ||||
|                   : getShortcutKey("CtrlOrCmd+Shift+["), | ||||
|               ]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.bringToFront")} | ||||
|               shortcuts={[ | ||||
|                 isDarwin | ||||
|                   ? getShortcutKey("CtrlOrCmd+Alt+]") | ||||
|                   : getShortcutKey("CtrlOrCmd+Shift+]"), | ||||
|               ]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.sendBackward")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+[")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.bringForward")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+]")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.alignTop")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Up")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.alignBottom")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Down")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.alignLeft")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Left")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.alignRight")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Right")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.duplicateSelection")} | ||||
|               shortcuts={[ | ||||
|                 getShortcutKey("CtrlOrCmd+D"), | ||||
|                 getShortcutKey(`Alt+${t("helpDialog.drag")}`), | ||||
|               ]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("helpDialog.toggleElementLock")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+Shift+L")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("buttons.undo")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+Z")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("buttons.redo")} | ||||
|               shortcuts={ | ||||
|                 isWindows | ||||
|                   ? [ | ||||
|                       getShortcutKey("CtrlOrCmd+Y"), | ||||
|                       getShortcutKey("CtrlOrCmd+Shift+Z"), | ||||
|                     ] | ||||
|                   : [getShortcutKey("CtrlOrCmd+Shift+Z")] | ||||
|               } | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.group")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+G")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.ungroup")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.flipHorizontal")} | ||||
|               shortcuts={[getShortcutKey("Shift+H")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.flipVertical")} | ||||
|               shortcuts={[getShortcutKey("Shift+V")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.showStroke")} | ||||
|               shortcuts={[getShortcutKey("S")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.showBackground")} | ||||
|               shortcuts={[getShortcutKey("G")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.decreaseFontSize")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+Shift+<")]} | ||||
|             /> | ||||
|             <Shortcut | ||||
|               label={t("labels.increaseFontSize")} | ||||
|               shortcuts={[getShortcutKey("CtrlOrCmd+Shift+>")]} | ||||
|             /> | ||||
|           </ShortcutIsland> | ||||
|         </Section> | ||||
|       </Dialog> | ||||
|     </> | ||||
|   | ||||
| @@ -14,20 +14,24 @@ $wide-viewport-width: 1000px; | ||||
|     top: 100%; | ||||
|     max-width: 100%; | ||||
|     width: 100%; | ||||
|     margin-top: 6px; | ||||
|     margin-top: 0.5rem; | ||||
|     text-align: center; | ||||
|     color: $oc-gray-6; | ||||
|     font-size: 0.8rem; | ||||
|     color: var(--color-gray-40); | ||||
|     font-size: 0.75rem; | ||||
|  | ||||
|     @include isMobile { | ||||
|       position: static; | ||||
|       padding-right: 2em; | ||||
|       padding-right: 2rem; | ||||
|     } | ||||
|  | ||||
|     > span { | ||||
|       padding: 0.2rem 0.4rem; | ||||
|       background-color: var(--overlay-bg-color); | ||||
|       border-radius: 4px; | ||||
|       padding: 0.25rem; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &.theme--dark { | ||||
|     .HintViewer { | ||||
|       color: var(--color-gray-60); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { NonDeletedExcalidrawElement } from "../element/types"; | ||||
| import { getSelectedElements } from "../scene"; | ||||
|  | ||||
| import "./HintViewer.scss"; | ||||
| import { AppState } from "../types"; | ||||
| import { AppState, Device } from "../types"; | ||||
| import { | ||||
|   isImageElement, | ||||
|   isLinearElement, | ||||
| @@ -17,13 +17,19 @@ interface HintViewerProps { | ||||
|   appState: AppState; | ||||
|   elements: readonly NonDeletedExcalidrawElement[]; | ||||
|   isMobile: boolean; | ||||
|   device: Device; | ||||
| } | ||||
|  | ||||
| const getHints = ({ appState, elements, isMobile }: HintViewerProps) => { | ||||
| const getHints = ({ | ||||
|   appState, | ||||
|   elements, | ||||
|   isMobile, | ||||
|   device, | ||||
| }: HintViewerProps) => { | ||||
|   const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState; | ||||
|   const multiMode = appState.multiElement !== null; | ||||
|  | ||||
|   if (appState.isLibraryOpen) { | ||||
|   if (appState.openSidebar === "library" && !device.canDeviceFitSidebar) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
| @@ -111,11 +117,13 @@ export const HintViewer = ({ | ||||
|   appState, | ||||
|   elements, | ||||
|   isMobile, | ||||
|   device, | ||||
| }: HintViewerProps) => { | ||||
|   let hint = getHints({ | ||||
|     appState, | ||||
|     elements, | ||||
|     isMobile, | ||||
|     device, | ||||
|   }); | ||||
|   if (!hint) { | ||||
|     return null; | ||||
|   | ||||
| @@ -10,7 +10,8 @@ | ||||
|   .picker { | ||||
|     background: var(--popup-bg-color); | ||||
|     border: 0 solid transparentize($oc-white, 0.75); | ||||
|     box-shadow: transparentize($oc-black, 0.75) 0 1px 4px; | ||||
|     // ˇˇ yeah, i dunno, open to suggestions here :D | ||||
|     box-shadow: rgb(0 0 0 / 25%) 2px 2px 4px 2px; | ||||
|     border-radius: 4px; | ||||
|     position: absolute; | ||||
|   } | ||||
| @@ -46,7 +47,6 @@ | ||||
|       margin: 0; | ||||
|       width: 36px; | ||||
|       height: 18px; | ||||
|       opacity: 0.6; | ||||
|       pointer-events: none; | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import { Popover } from "./Popover"; | ||||
| import "./IconPicker.scss"; | ||||
| import { isArrowKey, KEYS } from "../keys"; | ||||
| import { getLanguage } from "../i18n"; | ||||
| import clsx from "clsx"; | ||||
|  | ||||
| function Picker<T>({ | ||||
|   options, | ||||
| @@ -102,7 +103,9 @@ function Picker<T>({ | ||||
|       <div className="picker-content" ref={rGallery}> | ||||
|         {options.map((option, i) => ( | ||||
|           <button | ||||
|             className="picker-option" | ||||
|             className={clsx("picker-option", { | ||||
|               active: value === option.value, | ||||
|             })} | ||||
|             onClick={(event) => { | ||||
|               (event.currentTarget as HTMLButtonElement).focus(); | ||||
|               onChange(option.value); | ||||
| @@ -150,7 +153,7 @@ export function IconPicker<T>({ | ||||
|   const isRTL = getLanguage().rtl; | ||||
|  | ||||
|   return ( | ||||
|     <label className={"picker-container"}> | ||||
|     <div> | ||||
|       <button | ||||
|         name={group} | ||||
|         className={isActive ? "active" : ""} | ||||
| @@ -184,6 +187,6 @@ export function IconPicker<T>({ | ||||
|           </> | ||||
|         ) : null} | ||||
|       </React.Suspense> | ||||
|     </label> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -5,14 +5,12 @@ import { canvasToBlob } from "../data/blob"; | ||||
| import { NonDeletedExcalidrawElement } from "../element/types"; | ||||
| import { CanvasError } from "../errors"; | ||||
| import { t } from "../i18n"; | ||||
| import { useDevice } from "./App"; | ||||
| import { getSelectedElements, isSomeElementSelected } from "../scene"; | ||||
| import { exportToCanvas } from "../scene/export"; | ||||
| import { AppState, BinaryFiles } from "../types"; | ||||
| import { Dialog } from "./Dialog"; | ||||
| import { clipboard, exportImage } from "./icons"; | ||||
| import { clipboard } from "./icons"; | ||||
| import Stack from "./Stack"; | ||||
| import { ToolButton } from "./ToolButton"; | ||||
| import "./ExportDialog.scss"; | ||||
| import OpenColor from "open-color"; | ||||
| import { CheckboxItem } from "./CheckboxItem"; | ||||
| @@ -58,6 +56,7 @@ const ExportButton: React.FC<{ | ||||
|   onClick: () => void; | ||||
|   title: string; | ||||
|   shade?: number; | ||||
|   children?: React.ReactNode; | ||||
| }> = ({ children, title, onClick, color, shade = 6 }) => { | ||||
|   return ( | ||||
|     <button | ||||
| @@ -80,7 +79,6 @@ const ImageExportModal = ({ | ||||
|   elements, | ||||
|   appState, | ||||
|   files, | ||||
|   exportPadding = DEFAULT_EXPORT_PADDING, | ||||
|   actionManager, | ||||
|   onExportToPng, | ||||
|   onExportToSvg, | ||||
| @@ -89,7 +87,6 @@ const ImageExportModal = ({ | ||||
|   appState: AppState; | ||||
|   elements: readonly NonDeletedExcalidrawElement[]; | ||||
|   files: BinaryFiles; | ||||
|   exportPadding?: number; | ||||
|   actionManager: ActionManager; | ||||
|   onExportToPng: ExportCB; | ||||
|   onExportToSvg: ExportCB; | ||||
| @@ -117,7 +114,7 @@ const ImageExportModal = ({ | ||||
|     exportToCanvas(exportedElements, appState, files, { | ||||
|       exportBackground, | ||||
|       viewBackgroundColor, | ||||
|       exportPadding, | ||||
|       exportPadding: appState.exportPadding, | ||||
|     }) | ||||
|       .then((canvas) => { | ||||
|         // if converting to blob fails, there's some problem that will | ||||
| @@ -135,7 +132,6 @@ const ImageExportModal = ({ | ||||
|     files, | ||||
|     exportedElements, | ||||
|     exportBackground, | ||||
|     exportPadding, | ||||
|     viewBackgroundColor, | ||||
|   ]); | ||||
|  | ||||
| @@ -152,8 +148,10 @@ const ImageExportModal = ({ | ||||
|             // dunno why this is needed, but when the items wrap it creates | ||||
|             // an overflow | ||||
|             overflow: "hidden", | ||||
|             gap: ".6rem", | ||||
|           }} | ||||
|         > | ||||
|           {actionManager.renderAction("changeExportPadding")} | ||||
|           {actionManager.renderAction("changeExportBackground")} | ||||
|           {someElementIsSelected && ( | ||||
|             <CheckboxItem | ||||
| @@ -220,48 +218,34 @@ const ImageExportModal = ({ | ||||
| export const ImageExportDialog = ({ | ||||
|   elements, | ||||
|   appState, | ||||
|   setAppState, | ||||
|   files, | ||||
|   exportPadding = DEFAULT_EXPORT_PADDING, | ||||
|   actionManager, | ||||
|   onExportToPng, | ||||
|   onExportToSvg, | ||||
|   onExportToClipboard, | ||||
| }: { | ||||
|   appState: AppState; | ||||
|   setAppState: React.Component<any, AppState>["setState"]; | ||||
|   elements: readonly NonDeletedExcalidrawElement[]; | ||||
|   files: BinaryFiles; | ||||
|   exportPadding?: number; | ||||
|   actionManager: ActionManager; | ||||
|   onExportToPng: ExportCB; | ||||
|   onExportToSvg: ExportCB; | ||||
|   onExportToClipboard: ExportCB; | ||||
| }) => { | ||||
|   const [modalIsShown, setModalIsShown] = useState(false); | ||||
|  | ||||
|   const handleClose = React.useCallback(() => { | ||||
|     setModalIsShown(false); | ||||
|   }, []); | ||||
|     setAppState({ openDialog: null }); | ||||
|   }, [setAppState]); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <ToolButton | ||||
|         onClick={() => { | ||||
|           setModalIsShown(true); | ||||
|         }} | ||||
|         data-testid="image-export-button" | ||||
|         icon={exportImage} | ||||
|         type="button" | ||||
|         aria-label={t("buttons.exportImage")} | ||||
|         showAriaLabel={useDevice().isMobile} | ||||
|         title={t("buttons.exportImage")} | ||||
|       /> | ||||
|       {modalIsShown && ( | ||||
|       {appState.openDialog === "imageExport" && ( | ||||
|         <Dialog onCloseRequest={handleClose} title={t("buttons.exportImage")}> | ||||
|           <ImageExportModal | ||||
|             elements={elements} | ||||
|             appState={appState} | ||||
|             files={files} | ||||
|             exportPadding={exportPadding} | ||||
|             actionManager={actionManager} | ||||
|             onExportToPng={onExportToPng} | ||||
|             onExportToSvg={onExportToSvg} | ||||
|   | ||||