Compare commits

..

5 Commits

Author SHA1 Message Date
pomdtr
8984d8f19a don't clean export options on load 2022-06-29 10:23:48 +00:00
Achille Lacoin
b80706cd4a Update appState.ts 2022-06-28 17:53:01 +02:00
pomdtr
cf34cbdd30 fix export.test.tsx 2022-06-28 08:16:14 +00:00
pomdtr
6ead3ff839 fix export snapshot 2022-06-28 08:12:38 +00:00
pomdtr
d7f0d4ee21 [feat] serialize export options when embedding scene in an image 2022-06-27 20:31:05 +00:00
322 changed files with 15591 additions and 30840 deletions

View File

@@ -1,43 +0,0 @@
{
// 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"] }
}
}
}

View File

@@ -11,12 +11,3 @@ REACT_APP_WS_SERVER_URL=http://localhost:3002
REACT_APP_PORTAL_URL= REACT_APP_PORTAL_URL=
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}' REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
# put these in your .env.local, or make sure you don't commit!
# must be lowercase `true` when turned on
#
# whether to enable Service Workers in development
REACT_APP_DEV_ENABLE_SW=
# whether to disable live reload / HMR. Usuaully what you want to do when
# debugging Service Workers.
REACT_APP_DEV_DISABLE_LIVE_RELOAD=

37
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: sunday
time: "01:00"
reviewers:
- lipis
assignees:
- lipis
open-pull-requests-limit: 20
- package-ecosystem: npm
directory: /src/packages/excalidraw/
schedule:
interval: weekly
day: sunday
time: "01:00"
reviewers:
- ad1992
assignees:
- ad1992
open-pull-requests-limit: 20
- package-ecosystem: npm
directory: /src/packages/utils/
schedule:
interval: weekly
day: sunday
time: "01:00"
reviewers:
- ad1992
assignees:
- ad1992
open-pull-requests-limit: 20

View File

@@ -1,4 +1,4 @@
name: Auto release excalidraw next name: Auto release @excalidraw/excalidraw-next
on: on:
push: push:
branches: branches:

View File

@@ -1,4 +1,4 @@
name: Auto release excalidraw preview name: Auto release preview @excalidraw/excalidraw-preview
on: on:
issue_comment: issue_comment:
types: [created, edited] types: [created, edited]

View File

@@ -10,16 +10,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - uses: actions/checkout@v2
uses: actions/checkout@v3 - uses: docker/build-push-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push repository: excalidraw/excalidraw
uses: docker/build-push-action@v3 tag_with_ref: true
with: tag_with_sha: true
context: .
push: true
tags: excalidraw/excalidraw:latest

2
.gitignore vendored
View File

@@ -19,9 +19,11 @@ logs
node_modules node_modules
npm-debug.log* npm-debug.log*
package-lock.json package-lock.json
static
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
src/packages/excalidraw/types src/packages/excalidraw/types
src/packages/excalidraw/example/public/bundle.js src/packages/excalidraw/example/public/bundle.js
src/packages/excalidraw/example/public/excalidraw-assets-dev src/packages/excalidraw/example/public/excalidraw-assets-dev
src/packages/excalidraw/example/public/excalidraw.development.js src/packages/excalidraw/example/public/excalidraw.development.js

View File

@@ -88,7 +88,7 @@ Try out [`@excalidraw/excalidraw`](https://www.npmjs.com/package/@excalidraw/exc
### Code Sandbox ### Code Sandbox
- Go to https://codesandbox.io/p/github/excalidraw/excalidraw - Go to https://codesandbox.io/s/github/excalidraw/excalidraw
- You may need to sign in with GitHub and reload the page - You may need to sign in with GitHub and reload the page
- You can start coding instantly, and even send PRs from there! - You can start coding instantly, and even send PRs from there!

20
dev-docs/.gitignore vendored
View File

@@ -1,20 +0,0 @@
# 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*

View File

@@ -1,41 +0,0 @@
# 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.

View File

@@ -1,3 +0,0 @@
module.exports = {
presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
};

View File

@@ -1,6 +0,0 @@
---
sidebar_position: 1
title: Overview
---
In development. For now, refer to [excalidraw Readme](https://github.com/excalidraw/excalidraw/blob/master/README.md).

View File

@@ -1,8 +0,0 @@
---
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).

View File

@@ -1,6 +0,0 @@
---
sidebar_position: 1
title: Overview
---
In development. For now, refer to [excalidraw package readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md).

View File

@@ -1,121 +0,0 @@
// @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;

View File

@@ -1,46 +0,0 @@
{
"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"
}
}

View File

@@ -1,31 +0,0 @@
/**
* 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;

View File

@@ -1,62 +0,0 @@
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>
);
}

View File

@@ -1,70 +0,0 @@
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&apos;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>
);
}

View File

@@ -1,11 +0,0 @@
.features {
display: flex;
align-items: center;
padding: 2rem 0;
width: 100%;
}
.featureSvg {
height: 200px;
width: 200px;
}

View File

@@ -1,43 +0,0 @@
/**
* 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);
}

View File

@@ -1,42 +0,0 @@
import React from "react";
import clsx from "clsx";
import Layout from "@theme/Layout";
import Link from "@docusaurus/Link";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import styles from "./index.module.css";
import HomepageFeatures from "@site/src/components/Homepage";
function HomepageHeader() {
const { siteConfig } = useDocusaurusContext();
return (
<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>
);
}

View File

@@ -1,27 +0,0 @@
/**
* 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;
}

View File

@@ -1,42 +0,0 @@
import React from "react";
import clsx from "clsx";
import Layout from "@theme/Layout";
import Link from "@docusaurus/Link";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import styles from "./index.module.css";
import HomepageFeatures from "@site/src/components/Homepage";
function HomepageHeader() {
const { siteConfig } = useDocusaurusContext();
return (
<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>
);
}

View File

@@ -1,7 +0,0 @@
---
title: Markdown page example
---
# Markdown page example
You don't need React to write simple standalone pages.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -1,4 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 3.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -1,7 +0,0 @@
{
// This file is not used in compilation. It is here just for a nice editor experience.
"extends": "@tsconfig/docusaurus/tsconfig.json",
"compilerOptions": {
"baseUrl": "."
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -23,17 +23,17 @@
"@sentry/integrations": "6.2.5", "@sentry/integrations": "6.2.5",
"@testing-library/jest-dom": "5.16.2", "@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.5", "@testing-library/react": "12.1.5",
"@tldraw/vec": "1.7.1", "@tldraw/vec": "1.4.3",
"@types/jest": "27.4.0", "@types/jest": "27.4.0",
"@types/pica": "5.1.3", "@types/pica": "5.1.3",
"@types/react": "18.0.15", "@types/react": "17.0.39",
"@types/react-dom": "18.0.6", "@types/react-dom": "17.0.11",
"@types/socket.io-client": "1.4.36", "@types/socket.io-client": "1.4.36",
"browser-fs-access": "0.29.1", "browser-fs-access": "0.29.1",
"clsx": "1.1.1", "clsx": "1.1.1",
"fake-indexeddb": "3.1.7", "fake-indexeddb": "3.1.7",
"firebase": "8.3.3", "firebase": "8.3.3",
"i18next-browser-languagedetector": "6.1.4", "i18next-browser-languagedetector": "6.1.2",
"idb-keyval": "6.0.3", "idb-keyval": "6.0.3",
"image-blob-reduce": "3.0.1", "image-blob-reduce": "3.0.1",
"jotai": "1.6.4", "jotai": "1.6.4",
@@ -41,15 +41,14 @@
"nanoid": "3.3.3", "nanoid": "3.3.3",
"open-color": "1.9.1", "open-color": "1.9.1",
"pako": "1.0.11", "pako": "1.0.11",
"perfect-freehand": "1.2.0", "perfect-freehand": "1.0.16",
"pica": "7.1.1",
"png-chunk-text": "1.0.0", "png-chunk-text": "1.0.0",
"png-chunks-encode": "1.0.0", "png-chunks-encode": "1.0.0",
"png-chunks-extract": "1.0.0", "png-chunks-extract": "1.0.0",
"points-on-curve": "0.2.0", "points-on-curve": "0.2.0",
"pwacompat": "2.0.17", "pwacompat": "2.0.17",
"react": "18.2.0", "react": "17.0.2",
"react-dom": "18.2.0", "react-dom": "17.0.2",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"roughjs": "4.5.2", "roughjs": "4.5.2",
"sass": "1.51.0", "sass": "1.51.0",
@@ -60,11 +59,11 @@
"@excalidraw/eslint-config": "1.0.0", "@excalidraw/eslint-config": "1.0.0",
"@excalidraw/prettier-config": "1.0.2", "@excalidraw/prettier-config": "1.0.2",
"@types/chai": "4.3.0", "@types/chai": "4.3.0",
"@types/lodash.throttle": "4.1.7", "@types/lodash.throttle": "4.1.6",
"@types/pako": "1.0.3", "@types/pako": "1.0.3",
"@types/resize-observer-browser": "0.1.7", "@types/resize-observer-browser": "0.1.6",
"chai": "4.3.6", "chai": "4.3.6",
"dotenv": "16.0.1", "dotenv": "10.0.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "3.3.1", "eslint-plugin-prettier": "3.3.1",
"husky": "7.0.4", "husky": "7.0.4",
@@ -72,7 +71,10 @@
"lint-staged": "12.3.7", "lint-staged": "12.3.7",
"pepjs": "0.5.3", "pepjs": "0.5.3",
"prettier": "2.6.2", "prettier": "2.6.2",
"rewire": "6.0.0" "rewire": "5.0.0"
},
"resolutions": {
"@typescript-eslint/typescript-estree": "5.10.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -92,8 +94,7 @@
"build:app:docker": "REACT_APP_DISABLE_SENTRY=true react-scripts build", "build:app:docker": "REACT_APP_DISABLE_SENTRY=true react-scripts build",
"build:app": "REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build", "build:app": "REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
"build:version": "node ./scripts/build-version.js", "build:version": "node ./scripts/build-version.js",
"build:prebuild": "node ./scripts/prebuild.js", "build": "yarn build:app && yarn build:version",
"build": "yarn build:prebuild && yarn build:app && yarn build:version",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"fix:code": "yarn test:code --fix", "fix:code": "yarn test:code --fix",
"fix:other": "yarn prettier --write", "fix:other": "yarn prettier --write",
@@ -111,8 +112,6 @@
"test:typecheck": "tsc", "test:typecheck": "tsc",
"test:update": "yarn test:app --updateSnapshot --watchAll=false", "test:update": "yarn test:app --updateSnapshot --watchAll=false",
"test": "yarn test:app", "test": "yarn test:app",
"autorelease": "node scripts/autorelease.js", "autorelease": "node scripts/autorelease.js"
"prerelease": "node scripts/prerelease.js",
"release": "node scripts/release.js"
} }
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -11,28 +11,3 @@
src: url("Cascadia.woff2"); src: url("Cascadia.woff2");
font-display: swap; 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;
}

View File

@@ -8,76 +8,49 @@
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no" 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="referrer" content="origin" />
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#121212" />
<!-- Primary Meta Tags --> <meta name="theme-color" content="#000" />
<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 --> <!-- General tags -->
<meta <meta
name="description" name="description"
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them." 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 -->
<!-- to minimize white flash on load when user has dark mode enabled --> <meta property="og:url" content="https://excalidraw.com" />
<script> <meta property="og:site_name" content="Excalidraw" />
try { <meta property="og:type" content="website" />
// <meta property="og:title" content="Excalidraw" />
const theme = window.localStorage.getItem("excalidraw-theme"); <meta
if (theme === "dark") { property="og:description"
document.documentElement.classList.add("dark"); content="Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
} />
} catch {} <!-- OG tags require an absolute url for images -->
</script> <meta
<style> property="og:image"
html.dark { name="twitter:image"
background-color: #121212; content="https://excalidraw.com/og-image.png"
color: #fff; />
} <meta
</style> 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."
/>
<script> <script>
// Redirect Excalidraw+ users which have auto-redirect enabled. // Redirect Excalidraw+ users which have auto-redirect enabled.
@@ -125,22 +98,6 @@
/> />
<link rel="stylesheet" href="fonts.css" type="text/css" /> <link rel="stylesheet" href="fonts.css" type="text/css" />
<% if (process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD==="true" ) { %>
<script>
{
const _WebSocket = window.WebSocket;
window.WebSocket = function (url) {
if (/ws:\/\/localhost:.+?\/sockjs-node/.test(url)) {
console.info(
"[!!!] Live reload is disabled via process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
);
} else {
return new _WebSocket(url);
}
};
}
</script>
<% } %>
<script> <script>
window.EXCALIDRAW_ASSET_PATH = "/"; window.EXCALIDRAW_ASSET_PATH = "/";
// setting this so that libraries installation reuses this window tab. // setting this so that libraries installation reuses this window tab.
@@ -166,8 +123,8 @@
body, body,
html { html {
margin: 0; margin: 0;
--ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system, --ui-font: system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
Segoe UI, Roboto, Helvetica, Arial, sans-serif; Roboto, Helvetica, Arial, sans-serif;
font-family: var(--ui-font); font-family: var(--ui-font);
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
@@ -182,7 +139,7 @@
width: 1px; width: 1px;
overflow: hidden; overflow: hidden;
clip: rect(1px, 1px, 1px, 1px); clip: rect(1px, 1px, 1px, 1px);
white-space: nowrap; white-space: nowrap; /* added line */
user-select: none; user-select: none;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -1,9 +1,3 @@
User-agent: Twitterbot
Disallow:
User-agent: facebookexternalhit
Disallow:
user-agent: * user-agent: *
Allow: /$ Allow: /$
Disallow: / Disallow: /

View File

@@ -5,25 +5,22 @@ const core = require("@actions/core");
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`; const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`; const excalidrawPackage = `${excalidrawDir}/package.json`;
const pkg = require(excalidrawPackage); const pkg = require(excalidrawPackage);
const isPreview = process.argv.slice(2)[0] === "preview";
const getShortCommitHash = () => { const getShortCommitHash = () => {
return execSync("git rev-parse --short HEAD").toString().trim(); return execSync("git rev-parse --short HEAD").toString().trim();
}; };
const publish = () => { const publish = () => {
const tag = isPreview ? "preview" : "next";
try { try {
execSync(`yarn --frozen-lockfile`); execSync(`yarn --frozen-lockfile`);
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir }); execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
execSync(`yarn run build:umd`, { cwd: excalidrawDir }); execSync(`yarn run build:umd`, { cwd: excalidrawDir });
execSync(`yarn --cwd ${excalidrawDir} publish --tag ${tag}`); execSync(`yarn --cwd ${excalidrawDir} publish`);
console.info(`Published ${pkg.name}@${tag}🎉`); console.info("Published 🎉");
core.setOutput( core.setOutput(
"result", "result",
`**Preview version has been shipped** :rocket: `**Preview version has been shipped** :rocket:
You can use [@excalidraw/excalidraw@${pkg.version}](https://www.npmjs.com/package/@excalidraw/excalidraw/v/${pkg.version}) for testing!`, You can use [@excalidraw/excalidraw-preview@${pkg.version}](https://www.npmjs.com/package/@excalidraw/excalidraw-preview/v/${pkg.version}) for testing!`,
); );
} catch (error) { } catch (error) {
core.setOutput("result", "package couldn't be published :warning:!"); core.setOutput("result", "package couldn't be published :warning:!");
@@ -54,19 +51,27 @@ exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
} }
// update package.json // update package.json
pkg.name = "@excalidraw/excalidraw-next";
let version = `${pkg.version}-${getShortCommitHash()}`; let version = `${pkg.version}-${getShortCommitHash()}`;
// update readme // update readme
let data = fs.readFileSync(`${excalidrawDir}/README_NEXT.md`, "utf8");
const isPreview = process.argv.slice(2)[0] === "preview";
if (isPreview) { if (isPreview) {
// use pullNumber-commithash as the version for preview // use pullNumber-commithash as the version for preview
const pullRequestNumber = process.argv.slice(3)[0]; const pullRequestNumber = process.argv.slice(3)[0];
version = `${pkg.version}-${pullRequestNumber}-${getShortCommitHash()}`; version = `${pkg.version}-${pullRequestNumber}-${getShortCommitHash()}`;
// replace "excalidraw-next" with "excalidraw-preview"
pkg.name = "@excalidraw/excalidraw-preview";
data = data.replace(/excalidraw-next/g, "excalidraw-preview");
data = data.trim();
} }
pkg.version = version; pkg.version = version;
fs.writeFileSync(excalidrawPackage, JSON.stringify(pkg, null, 2), "utf8"); fs.writeFileSync(excalidrawPackage, JSON.stringify(pkg, null, 2), "utf8");
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
console.info("Publish in progress..."); console.info("Publish in progress...");
publish(); publish();
}); });

View File

@@ -1,21 +0,0 @@
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);
});

View File

@@ -15,7 +15,6 @@ const crowdinMap = {
"fa-IR": "en-fa", "fa-IR": "en-fa",
"fi-FI": "en-fi", "fi-FI": "en-fi",
"fr-FR": "en-fr", "fr-FR": "en-fr",
"gl-ES": "en-gl",
"he-IL": "en-he", "he-IL": "en-he",
"hi-IN": "en-hi", "hi-IN": "en-hi",
"hu-HU": "en-hu", "hu-HU": "en-hu",
@@ -24,7 +23,6 @@ const crowdinMap = {
"ja-JP": "en-ja", "ja-JP": "en-ja",
"kab-KAB": "en-kab", "kab-KAB": "en-kab",
"ko-KR": "en-ko", "ko-KR": "en-ko",
"ku-TR": "en-ku",
"my-MM": "en-my", "my-MM": "en-my",
"nb-NO": "en-nb", "nb-NO": "en-nb",
"nl-NL": "en-nl", "nl-NL": "en-nl",
@@ -38,7 +36,6 @@ const crowdinMap = {
"ru-RU": "en-ru", "ru-RU": "en-ru",
"si-LK": "en-silk", "si-LK": "en-silk",
"sk-SK": "en-sk", "sk-SK": "en-sk",
"sl-SI": "en-sl",
"sv-SE": "en-sv", "sv-SE": "en-sv",
"ta-IN": "en-ta", "ta-IN": "en-ta",
"tr-TR": "en-tr", "tr-TR": "en-tr",
@@ -50,8 +47,6 @@ const crowdinMap = {
"lv-LV": "en-lv", "lv-LV": "en-lv",
"cs-CZ": "en-cs", "cs-CZ": "en-cs",
"kk-KZ": "en-kk", "kk-KZ": "en-kk",
"vi-vn": "en-vi",
"mr-in": "en-mr",
}; };
const flags = { const flags = {
@@ -67,7 +62,6 @@ const flags = {
"fa-IR": "🇮🇷", "fa-IR": "🇮🇷",
"fi-FI": "🇫🇮", "fi-FI": "🇫🇮",
"fr-FR": "🇫🇷", "fr-FR": "🇫🇷",
"gl-ES": "🇪🇸",
"he-IL": "🇮🇱", "he-IL": "🇮🇱",
"hi-IN": "🇮🇳", "hi-IN": "🇮🇳",
"hu-HU": "🇭🇺", "hu-HU": "🇭🇺",
@@ -77,7 +71,6 @@ const flags = {
"kab-KAB": "🏳", "kab-KAB": "🏳",
"kk-KZ": "🇰🇿", "kk-KZ": "🇰🇿",
"ko-KR": "🇰🇷", "ko-KR": "🇰🇷",
"ku-TR": "🏳",
"lt-LT": "🇱🇹", "lt-LT": "🇱🇹",
"lv-LV": "🇱🇻", "lv-LV": "🇱🇻",
"my-MM": "🇲🇲", "my-MM": "🇲🇲",
@@ -93,7 +86,6 @@ const flags = {
"ru-RU": "🇷🇺", "ru-RU": "🇷🇺",
"si-LK": "🇱🇰", "si-LK": "🇱🇰",
"sk-SK": "🇸🇰", "sk-SK": "🇸🇰",
"sl-SI": "🇸🇮",
"sv-SE": "🇸🇪", "sv-SE": "🇸🇪",
"ta-IN": "🇮🇳", "ta-IN": "🇮🇳",
"tr-TR": "🇹🇷", "tr-TR": "🇹🇷",
@@ -101,9 +93,6 @@ const flags = {
"zh-CN": "🇨🇳", "zh-CN": "🇨🇳",
"zh-HK": "🇭🇰", "zh-HK": "🇭🇰",
"zh-TW": "🇹🇼", "zh-TW": "🇹🇼",
"eu-ES": "🇪🇦",
"vi-VN": "🇻🇳",
"mr-IN": "🇮🇳",
}; };
const languages = { const languages = {
@@ -144,7 +133,6 @@ const languages = {
"ru-RU": "Русский", "ru-RU": "Русский",
"si-LK": "සිංහල", "si-LK": "සිංහල",
"sk-SK": "Slovenčina", "sk-SK": "Slovenčina",
"sl-SI": "Slovenščina",
"sv-SE": "Svenska", "sv-SE": "Svenska",
"ta-IN": "Tamil", "ta-IN": "Tamil",
"tr-TR": "Türkçe", "tr-TR": "Türkçe",
@@ -152,8 +140,6 @@ const languages = {
"zh-CN": "简体中文", "zh-CN": "简体中文",
"zh-HK": "繁體中文 (香港)", "zh-HK": "繁體中文 (香港)",
"zh-TW": "繁體中文", "zh-TW": "繁體中文",
"vi-VN": "Tiếng Việt",
"mr-IN": "मराठी",
}; };
const percentages = fs.readFileSync( const percentages = fs.readFileSync(

View File

@@ -1,21 +0,0 @@
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 = path.resolve(__dirname, "../public/service-worker.js");
const newPath = path.resolve(__dirname, "../src/service-worker.js");
fs.rename(oldPath, newPath, (error) => {
if (error) {
throw error;
}
console.info("public/service-worker.js moved to src/");
});
};
// -----------------------------------------------------------------------------
moveServiceWorkerScript();

View File

@@ -1,37 +0,0 @@
const fs = require("fs");
const util = require("util");
const exec = util.promisify(require("child_process").exec);
const updateChangelog = require("./updateChangelog");
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
const updatePackageVersion = (nextVersion) => {
const pkg = require(excalidrawPackage);
pkg.version = nextVersion;
const content = `${JSON.stringify(pkg, null, 2)}\n`;
fs.writeFileSync(excalidrawPackage, content, "utf-8");
};
const prerelease = async (nextVersion) => {
try {
await updateChangelog(nextVersion);
updatePackageVersion(nextVersion);
await exec(`git add -u`);
await exec(
`git commit -m "docs: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
);
console.info("Done!");
} catch (error) {
console.error(error);
process.exit(1);
}
};
const nextVersion = process.argv.slice(2)[0];
if (!nextVersion) {
console.error("Pass the next version to release!");
process.exit(1);
}
prerelease(nextVersion);

View File

@@ -1,44 +1,39 @@
const fs = require("fs"); const fs = require("fs");
const { execSync } = require("child_process"); const util = require("util");
const exec = util.promisify(require("child_process").exec);
const updateReadme = require("./updateReadme");
const updateChangelog = require("./updateChangelog");
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`; const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`; const excalidrawPackage = `${excalidrawDir}/package.json`;
const updatePackageVersion = (nextVersion) => {
const pkg = require(excalidrawPackage); const pkg = require(excalidrawPackage);
pkg.version = nextVersion;
const originalReadMe = fs.readFileSync(`${excalidrawDir}/README.md`, "utf8"); const content = `${JSON.stringify(pkg, null, 2)}\n`;
fs.writeFileSync(excalidrawPackage, content, "utf-8");
const updateReadme = () => {
const excalidrawIndex = originalReadMe.indexOf("### Excalidraw");
// remove note for stable readme
const data = originalReadMe.slice(excalidrawIndex);
// update readme
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
}; };
const publish = () => { const release = async (nextVersion) => {
try { try {
execSync(`yarn --frozen-lockfile`); updateReadme();
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir }); await updateChangelog(nextVersion);
execSync(`yarn run build:umd`, { cwd: excalidrawDir }); updatePackageVersion(nextVersion);
execSync(`yarn --cwd ${excalidrawDir} publish`); await exec(`git add -u`);
await exec(
`git commit -m "docs: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
);
/* eslint-disable no-console */
console.log("Done!");
} catch (error) { } catch (error) {
console.error(error); console.error(error);
process.exit(1); process.exit(1);
} }
}; };
const release = () => { const nextVersion = process.argv.slice(2)[0];
updateReadme(); if (!nextVersion) {
console.info("Note for stable readme removed"); console.error("Pass the next version to release!");
process.exit(1);
publish(); }
console.info(`Published ${pkg.version}!`); release(nextVersion);
// revert readme after release
fs.writeFileSync(`${excalidrawDir}/README.md`, originalReadMe, "utf8");
console.info("Readme reverted");
};
release();

27
scripts/updateReadme.js Normal file
View File

@@ -0,0 +1,27 @@
const fs = require("fs");
const updateReadme = () => {
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
let data = fs.readFileSync(`${excalidrawDir}/README_NEXT.md`, "utf8");
// remove note for unstable release
data = data.replace(
/<!-- unstable-readme-start-->[\s\S]*?<!-- unstable-readme-end-->/,
"",
);
// replace "excalidraw-next" with "excalidraw"
data = data.replace(/excalidraw-next/g, "excalidraw");
data = data.trim();
const demoIndex = data.indexOf("### Demo");
const excalidrawNextNote =
"#### Note\n\n**If you don't want to wait for the next stable release and try out the unreleased changes you can use [@excalidraw/excalidraw-next](https://www.npmjs.com/package/@excalidraw/excalidraw-next).**\n\n";
// Add excalidraw next note to try out for unreleased changes
data = data.slice(0, demoIndex) + excalidrawNextNote + data.slice(demoIndex);
// update readme
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
};
module.exports = updateReadme;

View File

@@ -42,7 +42,7 @@ export const actionAddToLibrary = register({
commitToHistory: false, commitToHistory: false,
appState: { appState: {
...appState, ...appState,
toast: { message: t("toast.addedToLibrary") }, toastMessage: t("toast.addedToLibrary"),
}, },
}; };
}) })

View File

@@ -60,7 +60,7 @@ export const actionAlignTop = register({
<ToolButton <ToolButton
hidden={!enableActionGroup(elements, appState)} hidden={!enableActionGroup(elements, appState)}
type="button" type="button"
icon={AlignTopIcon} icon={<AlignTopIcon theme={appState.theme} />}
onClick={() => updateData(null)} onClick={() => updateData(null)}
title={`${t("labels.alignTop")}${getShortcutKey( title={`${t("labels.alignTop")}${getShortcutKey(
"CtrlOrCmd+Shift+Up", "CtrlOrCmd+Shift+Up",
@@ -90,7 +90,7 @@ export const actionAlignBottom = register({
<ToolButton <ToolButton
hidden={!enableActionGroup(elements, appState)} hidden={!enableActionGroup(elements, appState)}
type="button" type="button"
icon={AlignBottomIcon} icon={<AlignBottomIcon theme={appState.theme} />}
onClick={() => updateData(null)} onClick={() => updateData(null)}
title={`${t("labels.alignBottom")}${getShortcutKey( title={`${t("labels.alignBottom")}${getShortcutKey(
"CtrlOrCmd+Shift+Down", "CtrlOrCmd+Shift+Down",
@@ -120,7 +120,7 @@ export const actionAlignLeft = register({
<ToolButton <ToolButton
hidden={!enableActionGroup(elements, appState)} hidden={!enableActionGroup(elements, appState)}
type="button" type="button"
icon={AlignLeftIcon} icon={<AlignLeftIcon theme={appState.theme} />}
onClick={() => updateData(null)} onClick={() => updateData(null)}
title={`${t("labels.alignLeft")}${getShortcutKey( title={`${t("labels.alignLeft")}${getShortcutKey(
"CtrlOrCmd+Shift+Left", "CtrlOrCmd+Shift+Left",
@@ -151,7 +151,7 @@ export const actionAlignRight = register({
<ToolButton <ToolButton
hidden={!enableActionGroup(elements, appState)} hidden={!enableActionGroup(elements, appState)}
type="button" type="button"
icon={AlignRightIcon} icon={<AlignRightIcon theme={appState.theme} />}
onClick={() => updateData(null)} onClick={() => updateData(null)}
title={`${t("labels.alignRight")}${getShortcutKey( title={`${t("labels.alignRight")}${getShortcutKey(
"CtrlOrCmd+Shift+Right", "CtrlOrCmd+Shift+Right",
@@ -180,7 +180,7 @@ export const actionAlignVerticallyCentered = register({
<ToolButton <ToolButton
hidden={!enableActionGroup(elements, appState)} hidden={!enableActionGroup(elements, appState)}
type="button" type="button"
icon={CenterVerticallyIcon} icon={<CenterVerticallyIcon theme={appState.theme} />}
onClick={() => updateData(null)} onClick={() => updateData(null)}
title={t("labels.centerVertically")} title={t("labels.centerVertically")}
aria-label={t("labels.centerVertically")} aria-label={t("labels.centerVertically")}
@@ -206,7 +206,7 @@ export const actionAlignHorizontallyCentered = register({
<ToolButton <ToolButton
hidden={!enableActionGroup(elements, appState)} hidden={!enableActionGroup(elements, appState)}
type="button" type="button"
icon={CenterHorizontallyIcon} icon={<CenterHorizontallyIcon theme={appState.theme} />}
onClick={() => updateData(null)} onClick={() => updateData(null)}
title={t("labels.centerHorizontally")} title={t("labels.centerHorizontally")}
aria-label={t("labels.centerHorizontally")} aria-label={t("labels.centerHorizontally")}

View File

@@ -1,13 +1,8 @@
import { ColorPicker } from "../components/ColorPicker"; import { ColorPicker } from "../components/ColorPicker";
import { import { eraser, zoomIn, zoomOut } from "../components/icons";
eraser,
MoonIcon,
SunIcon,
ZoomInIcon,
ZoomOutIcon,
} from "../components/icons";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { MIN_ZOOM, THEME, ZOOM_STEP } from "../constants"; import { DarkModeToggle } from "../components/DarkModeToggle";
import { THEME, ZOOM_STEP } from "../constants";
import { getCommonBounds, getNonDeletedElements } from "../element"; import { getCommonBounds, getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
@@ -23,8 +18,6 @@ import { newElementWith } from "../element/mutateElement";
import { getDefaultAppState, isEraserActive } from "../appState"; import { getDefaultAppState, isEraserActive } from "../appState";
import ClearCanvas from "../components/ClearCanvas"; import ClearCanvas from "../components/ClearCanvas";
import clsx from "clsx"; import clsx from "clsx";
import MenuItem from "../components/MenuItem";
import { getShortcutFromShortcutName } from "./shortcuts";
export const actionChangeViewBackgroundColor = register({ export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor", name: "changeViewBackgroundColor",
@@ -110,13 +103,13 @@ export const actionZoomIn = register({
PanelComponent: ({ updateData }) => ( PanelComponent: ({ updateData }) => (
<ToolButton <ToolButton
type="button" type="button"
className="zoom-in-button zoom-button" icon={zoomIn}
icon={ZoomInIcon}
title={`${t("buttons.zoomIn")}${getShortcutKey("CtrlOrCmd++")}`} title={`${t("buttons.zoomIn")}${getShortcutKey("CtrlOrCmd++")}`}
aria-label={t("buttons.zoomIn")} aria-label={t("buttons.zoomIn")}
onClick={() => { onClick={() => {
updateData(null); updateData(null);
}} }}
size="small"
/> />
), ),
keyTest: (event) => keyTest: (event) =>
@@ -146,13 +139,13 @@ export const actionZoomOut = register({
PanelComponent: ({ updateData }) => ( PanelComponent: ({ updateData }) => (
<ToolButton <ToolButton
type="button" type="button"
className="zoom-out-button zoom-button" icon={zoomOut}
icon={ZoomOutIcon}
title={`${t("buttons.zoomOut")}${getShortcutKey("CtrlOrCmd+-")}`} title={`${t("buttons.zoomOut")}${getShortcutKey("CtrlOrCmd+-")}`}
aria-label={t("buttons.zoomOut")} aria-label={t("buttons.zoomOut")}
onClick={() => { onClick={() => {
updateData(null); updateData(null);
}} }}
size="small"
/> />
), ),
keyTest: (event) => keyTest: (event) =>
@@ -183,12 +176,13 @@ export const actionResetZoom = register({
<Tooltip label={t("buttons.resetZoom")} style={{ height: "100%" }}> <Tooltip label={t("buttons.resetZoom")} style={{ height: "100%" }}>
<ToolButton <ToolButton
type="button" type="button"
className="reset-zoom-button zoom-button" className="reset-zoom-button"
title={t("buttons.resetZoom")} title={t("buttons.resetZoom")}
aria-label={t("buttons.resetZoom")} aria-label={t("buttons.resetZoom")}
onClick={() => { onClick={() => {
updateData(null); updateData(null);
}} }}
size="small"
> >
{(appState.zoom.value * 100).toFixed(0)}% {(appState.zoom.value * 100).toFixed(0)}%
</ToolButton> </ToolButton>
@@ -212,7 +206,7 @@ const zoomValueToFitBoundsOnViewport = (
const zoomAdjustedToSteps = const zoomAdjustedToSteps =
Math.floor(smallestZoomValue / ZOOM_STEP) * ZOOM_STEP; Math.floor(smallestZoomValue / ZOOM_STEP) * ZOOM_STEP;
const clampedZoomValueToFitElements = Math.min( const clampedZoomValueToFitElements = Math.min(
Math.max(zoomAdjustedToSteps, MIN_ZOOM), Math.max(zoomAdjustedToSteps, ZOOM_STEP),
1, 1,
); );
return clampedZoomValueToFitElements as NormalizedZoomValue; return clampedZoomValueToFitElements as NormalizedZoomValue;
@@ -294,19 +288,14 @@ export const actionToggleTheme = register({
}; };
}, },
PanelComponent: ({ appState, updateData }) => ( PanelComponent: ({ appState, updateData }) => (
<MenuItem <div style={{ marginInlineStart: "0.25rem" }}>
label={ <DarkModeToggle
appState.theme === "dark" value={appState.theme}
? t("buttons.lightMode") onChange={(theme) => {
: t("buttons.darkMode") updateData(theme);
}
onClick={() => {
updateData(appState.theme === THEME.LIGHT ? THEME.DARK : THEME.LIGHT);
}} }}
icon={appState.theme === "dark" ? SunIcon : MoonIcon}
dataTestId="toggle-dark-mode"
shortcut={getShortcutFromShortcutName("toggleTheme")}
/> />
</div>
), ),
keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D, keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D,
}); });

View File

@@ -36,7 +36,7 @@ export const actionCut = register({
return actionDeleteSelected.perform(elements, appState); return actionDeleteSelected.perform(elements, appState);
}, },
contextItemLabel: "labels.cut", contextItemLabel: "labels.cut",
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.X, keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.X,
}); });
export const actionCopyAsSvg = register({ export const actionCopyAsSvg = register({
@@ -107,8 +107,7 @@ export const actionCopyAsPng = register({
return { return {
appState: { appState: {
...appState, ...appState,
toast: { toastMessage: t("toast.copyToClipboardAsPng", {
message: t("toast.copyToClipboardAsPng", {
exportSelection: selectedElements.length exportSelection: selectedElements.length
? t("toast.selection") ? t("toast.selection")
: t("toast.canvas"), : t("toast.canvas"),
@@ -117,7 +116,6 @@ export const actionCopyAsPng = register({
: t("buttons.lightMode"), : t("buttons.lightMode"),
}), }),
}, },
},
commitToHistory: false, commitToHistory: false,
}; };
} catch (error: any) { } catch (error: any) {

View File

@@ -1,6 +1,7 @@
import { isSomeElementSelected } from "../scene"; import { isSomeElementSelected } from "../scene";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { trash } from "../components/icons";
import { t } from "../i18n"; import { t } from "../i18n";
import { register } from "./register"; import { register } from "./register";
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
@@ -12,7 +13,6 @@ import { LinearElementEditor } from "../element/linearElementEditor";
import { fixBindingsAfterDeletion } from "../element/binding"; import { fixBindingsAfterDeletion } from "../element/binding";
import { isBoundToContainer } from "../element/typeChecks"; import { isBoundToContainer } from "../element/typeChecks";
import { updateActiveTool } from "../utils"; import { updateActiveTool } from "../utils";
import { TrashIcon } from "../components/icons";
const deleteSelectedElements = ( const deleteSelectedElements = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
@@ -72,22 +72,13 @@ export const actionDeleteSelected = register({
if (!element) { if (!element) {
return false; return false;
} }
// case: no point selected → do nothing, as deleting the whole element if (
// is most likely a mistake, where you wanted to delete a specific point // case: no point selected delete whole element
// but failed to select it (or you thought it's selected, while it was selectedPointsIndices == null ||
// only in a hover state)
if (selectedPointsIndices == null) {
return false;
}
// case: deleting last remaining point // case: deleting last remaining point
if (element.points.length < 2) { element.points.length < 2
const nextElements = elements.map((el) => { ) {
if (el.id === element.id) { const nextElements = elements.filter((el) => el.id !== element.id);
return newElementWith(el, { isDeleted: true });
}
return el;
});
const nextAppState = handleGroupEditingState(appState, nextElements); const nextAppState = handleGroupEditingState(appState, nextElements);
return { return {
@@ -158,7 +149,7 @@ export const actionDeleteSelected = register({
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton <ToolButton
type="button" type="button"
icon={TrashIcon} icon={trash}
title={t("labels.delete")} title={t("labels.delete")}
aria-label={t("labels.delete")} aria-label={t("labels.delete")}
onClick={() => updateData(null)} onClick={() => updateData(null)}

View File

@@ -56,7 +56,7 @@ export const distributeHorizontally = register({
<ToolButton <ToolButton
hidden={!enableActionGroup(elements, appState)} hidden={!enableActionGroup(elements, appState)}
type="button" type="button"
icon={DistributeHorizontallyIcon} icon={<DistributeHorizontallyIcon theme={appState.theme} />}
onClick={() => updateData(null)} onClick={() => updateData(null)}
title={`${t("labels.distributeHorizontally")}${getShortcutKey( title={`${t("labels.distributeHorizontally")}${getShortcutKey(
"Alt+H", "Alt+H",
@@ -86,7 +86,7 @@ export const distributeVertically = register({
<ToolButton <ToolButton
hidden={!enableActionGroup(elements, appState)} hidden={!enableActionGroup(elements, appState)}
type="button" type="button"
icon={DistributeVerticallyIcon} icon={<DistributeVerticallyIcon theme={appState.theme} />}
onClick={() => updateData(null)} onClick={() => updateData(null)}
title={`${t("labels.distributeVertically")}${getShortcutKey("Alt+V")}`} title={`${t("labels.distributeVertically")}${getShortcutKey("Alt+V")}`}
aria-label={t("labels.distributeVertically")} aria-label={t("labels.distributeVertically")}

View File

@@ -4,6 +4,7 @@ import { ExcalidrawElement } from "../element/types";
import { duplicateElement, getNonDeletedElements } from "../element"; import { duplicateElement, getNonDeletedElements } from "../element";
import { getSelectedElements, isSomeElementSelected } from "../scene"; import { getSelectedElements, isSomeElementSelected } from "../scene";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { clone } from "../components/icons";
import { t } from "../i18n"; import { t } from "../i18n";
import { arrayToMap, getShortcutKey } from "../utils"; import { arrayToMap, getShortcutKey } from "../utils";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
@@ -18,7 +19,6 @@ import { ActionResult } from "./types";
import { GRID_SIZE } from "../constants"; import { GRID_SIZE } from "../constants";
import { bindTextToShapeAfterDuplication } from "../element/textElement"; import { bindTextToShapeAfterDuplication } from "../element/textElement";
import { isBoundToContainer } from "../element/typeChecks"; import { isBoundToContainer } from "../element/typeChecks";
import { DuplicateIcon } from "../components/icons";
export const actionDuplicateSelection = register({ export const actionDuplicateSelection = register({
name: "duplicateSelection", name: "duplicateSelection",
@@ -49,7 +49,7 @@ export const actionDuplicateSelection = register({
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton <ToolButton
type="button" type="button"
icon={DuplicateIcon} icon={clone}
title={`${t("labels.duplicateSelection")}${getShortcutKey( title={`${t("labels.duplicateSelection")}${getShortcutKey(
"CtrlOrCmd+D", "CtrlOrCmd+D",
)}`} )}`}
@@ -128,15 +128,12 @@ const duplicateElements = (
{ {
...appState, ...appState,
selectedGroupIds: {}, selectedGroupIds: {},
selectedElementIds: newElements.reduce( selectedElementIds: newElements.reduce((acc, element) => {
(acc: Record<ExcalidrawElement["id"], true>, element) => {
if (!isBoundToContainer(element)) { if (!isBoundToContainer(element)) {
acc[element.id] = true; acc[element.id] = true;
} }
return acc; return acc;
}, }, {} as any),
{},
),
}, },
getNonDeletedElements(finalElements), getNonDeletedElements(finalElements),
), ),

View File

@@ -1,4 +1,4 @@
import { LoadIcon, questionCircle, saveAs } from "../components/icons"; import { load, questionCircle, saveAs } from "../components/icons";
import { ProjectName } from "../components/ProjectName"; import { ProjectName } from "../components/ProjectName";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import "../components/ToolIcon.scss"; import "../components/ToolIcon.scss";
@@ -19,8 +19,6 @@ import { ActiveFile } from "../components/ActiveFile";
import { isImageFileHandle } from "../data/blob"; import { isImageFileHandle } from "../data/blob";
import { nativeFileSystemSupported } from "../data/filesystem"; import { nativeFileSystemSupported } from "../data/filesystem";
import { Theme } from "../element/types"; import { Theme } from "../element/types";
import MenuItem from "../components/MenuItem";
import { getShortcutFromShortcutName } from "./shortcuts";
export const actionChangeProjectName = register({ export const actionChangeProjectName = register({
name: "changeProjectName", name: "changeProjectName",
@@ -146,15 +144,13 @@ export const actionSaveToActiveFile = register({
appState: { appState: {
...appState, ...appState,
fileHandle, fileHandle,
toast: fileHandleExists toastMessage: fileHandleExists
? { ? fileHandle?.name
message: fileHandle?.name
? t("toast.fileSavedToFilename").replace( ? t("toast.fileSavedToFilename").replace(
"{filename}", "{filename}",
`"${fileHandle.name}"`, `"${fileHandle.name}"`,
) )
: t("toast.fileSaved"), : t("toast.fileSaved")
}
: null, : null,
}, },
}; };
@@ -246,13 +242,15 @@ export const actionLoadScene = register({
} }
}, },
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O, keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
PanelComponent: ({ updateData }) => ( PanelComponent: ({ updateData, appState }) => (
<MenuItem <ToolButton
label={t("buttons.load")} type="button"
icon={LoadIcon} icon={load}
title={t("buttons.load")}
aria-label={t("buttons.load")}
showAriaLabel={useDevice().isMobile}
onClick={updateData} onClick={updateData}
dataTestId="load-button" data-testid="load-button"
shortcut={getShortcutFromShortcutName("loadScene")}
/> />
), ),
}); });

View File

@@ -13,7 +13,7 @@ import {
maybeBindLinearElement, maybeBindLinearElement,
bindOrUnbindLinearElement, bindOrUnbindLinearElement,
} from "../element/binding"; } from "../element/binding";
import { isBindingElement, isLinearElement } from "../element/typeChecks"; import { isBindingElement } from "../element/typeChecks";
import { AppState } from "../types"; import { AppState } from "../types";
export const actionFinalize = register({ export const actionFinalize = register({
@@ -181,11 +181,6 @@ export const actionFinalize = register({
[multiPointElement.id]: true, [multiPointElement.id]: true,
} }
: appState.selectedElementIds, : appState.selectedElementIds,
// To select the linear element when user has finished mutipoint editing
selectedLinearElement:
multiPointElement && isLinearElement(multiPointElement)
? new LinearElementEditor(multiPointElement, scene)
: appState.selectedLinearElement,
pendingImageElementId: null, pendingImageElementId: null,
}, },
commitToHistory: appState.activeTool.type === "freedraw", commitToHistory: appState.activeTool.type === "freedraw",

View File

@@ -6,14 +6,10 @@ import { ExcalidrawElement, NonDeleted } from "../element/types";
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements"; import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
import { AppState } from "../types"; import { AppState } from "../types";
import { getTransformHandles } from "../element/transformHandles"; import { getTransformHandles } from "../element/transformHandles";
import { isFreeDrawElement, isLinearElement } from "../element/typeChecks";
import { updateBoundElements } from "../element/binding"; import { updateBoundElements } from "../element/binding";
import { arrayToMap } from "../utils";
import {
getElementAbsoluteCoords,
getElementPointsCoords,
} from "../element/bounds";
import { isLinearElement } from "../element/typeChecks";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { arrayToMap } from "../utils";
const enableActionFlipHorizontal = ( const enableActionFlipHorizontal = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
@@ -122,6 +118,13 @@ const flipElement = (
const height = element.height; const height = element.height;
const originalAngle = normalizeAngle(element.angle); 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 // Rotate back to zero, if necessary
mutateElement(element, { mutateElement(element, {
angle: normalizeAngle(0), angle: normalizeAngle(0),
@@ -129,6 +132,7 @@ const flipElement = (
// Flip unrotated by pulling TransformHandle to opposite side // Flip unrotated by pulling TransformHandle to opposite side
const transformHandles = getTransformHandles(element, appState.zoom); const transformHandles = getTransformHandles(element, appState.zoom);
let usingNWHandle = true; let usingNWHandle = true;
let newNCoordsX = 0;
let nHandle = transformHandles.nw; let nHandle = transformHandles.nw;
if (!nHandle) { if (!nHandle) {
// Use ne handle instead // Use ne handle instead
@@ -142,51 +146,30 @@ 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)) { 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++) { for (let index = 1; index < element.points.length; index++) {
LinearElementEditor.movePoints(element, [ 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); LinearElementEditor.normalizePoints(element);
} else { } else {
const elWidth = initialPointsCoords // calculate new x-coord for transformation
? initialPointsCoords[2] - initialPointsCoords[0] newNCoordsX = usingNWHandle ? element.x + 2 * width : element.x - 2 * width;
: initialElementAbsoluteCoords[2] - initialElementAbsoluteCoords[0];
const startPoint = initialPointsCoords
? [initialPointsCoords[0], initialPointsCoords[1]]
: [initialElementAbsoluteCoords[0], initialElementAbsoluteCoords[1]];
resizeSingleElement( resizeSingleElement(
new Map().set(element.id, element), new Map().set(element.id, element),
false, true,
element, element,
usingNWHandle ? "nw" : "ne", usingNWHandle ? "nw" : "ne",
true, false,
usingNWHandle ? startPoint[0] + elWidth : startPoint[0] - elWidth, newNCoordsX,
startPoint[1], nHandle[1],
); );
// fix the size to account for handle sizes
mutateElement(element, {
width,
height,
});
} }
// Rotate by (360 degrees - original angle) // Rotate by (360 degrees - original angle)
@@ -203,34 +186,9 @@ const flipElement = (
mutateElement(element, { mutateElement(element, {
x: originalX + finalOffsetX, x: originalX + finalOffsetX,
y: originalY, y: originalY,
width,
height,
}); });
updateBoundElements(element); 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) => { const rotateElement = (element: ExcalidrawElement, rotationAngle: number) => {

View File

@@ -1,4 +1,4 @@
import { KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { t } from "../i18n"; import { t } from "../i18n";
import { arrayToMap, getShortcutKey } from "../utils"; import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
@@ -132,7 +132,7 @@ export const actionGroup = register({
contextItemPredicate: (elements, appState) => contextItemPredicate: (elements, appState) =>
enableActionGroup(elements, appState), enableActionGroup(elements, appState),
keyTest: (event) => keyTest: (event) =>
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G, !event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.code === CODES.G,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton <ToolButton
hidden={!enableActionGroup(elements, appState)} hidden={!enableActionGroup(elements, appState)}
@@ -189,9 +189,7 @@ export const actionUngroup = register({
}; };
}, },
keyTest: (event) => keyTest: (event) =>
event.shiftKey && event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.code === CODES.G,
event[KEYS.CTRL_OR_CMD] &&
event.key === KEYS.G.toUpperCase(),
contextItemLabel: "labels.ungroup", contextItemLabel: "labels.ungroup",
contextItemPredicate: (elements, appState) => contextItemPredicate: (elements, appState) =>
getSelectedGroupIds(appState).length > 0, getSelectedGroupIds(appState).length > 0,

View File

@@ -1,5 +1,5 @@
import { Action, ActionResult } from "./types"; import { Action, ActionResult } from "./types";
import { UndoIcon, RedoIcon } from "../components/icons"; import { undo, redo } from "../components/icons";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n"; import { t } from "../i18n";
import History, { HistoryEntry } from "../history"; import History, { HistoryEntry } from "../history";
@@ -72,7 +72,7 @@ export const createUndoAction: ActionCreator = (history) => ({
PanelComponent: ({ updateData, data }) => ( PanelComponent: ({ updateData, data }) => (
<ToolButton <ToolButton
type="button" type="button"
icon={UndoIcon} icon={undo}
aria-label={t("buttons.undo")} aria-label={t("buttons.undo")}
onClick={updateData} onClick={updateData}
size={data?.size || "medium"} size={data?.size || "medium"}
@@ -94,7 +94,7 @@ export const createRedoAction: ActionCreator = (history) => ({
PanelComponent: ({ updateData, data }) => ( PanelComponent: ({ updateData, data }) => (
<ToolButton <ToolButton
type="button" type="button"
icon={RedoIcon} icon={redo}
aria-label={t("buttons.redo")} aria-label={t("buttons.redo")}
onClick={updateData} onClick={updateData}
size={data?.size || "medium"} size={data?.size || "medium"}

View File

@@ -1,49 +0,0 @@
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";
},
});

View File

@@ -1,12 +1,11 @@
import { HamburgerMenuIcon, HelpIcon, palette } from "../components/icons"; import { menu, palette } from "../components/icons";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n"; import { t } from "../i18n";
import { showSelectedShapeActions, getNonDeletedElements } from "../element"; import { showSelectedShapeActions, getNonDeletedElements } from "../element";
import { register } from "./register"; import { register } from "./register";
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils"; import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
import { KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { HelpButton } from "../components/HelpButton"; import { HelpIcon } from "../components/HelpIcon";
import MenuItem from "../components/MenuItem";
export const actionToggleCanvasMenu = register({ export const actionToggleCanvasMenu = register({
name: "toggleCanvasMenu", name: "toggleCanvasMenu",
@@ -21,7 +20,7 @@ export const actionToggleCanvasMenu = register({
PanelComponent: ({ appState, updateData }) => ( PanelComponent: ({ appState, updateData }) => (
<ToolButton <ToolButton
type="button" type="button"
icon={HamburgerMenuIcon} icon={menu}
aria-label={t("buttons.menu")} aria-label={t("buttons.menu")}
onClick={updateData} onClick={updateData}
selected={appState.openMenu === "canvas"} selected={appState.openMenu === "canvas"}
@@ -68,35 +67,26 @@ export const actionFullScreen = register({
commitToHistory: false, commitToHistory: false,
}; };
}, },
keyTest: (event) => event.key === KEYS.F && !event[KEYS.CTRL_OR_CMD], keyTest: (event) => event.code === CODES.F && !event[KEYS.CTRL_OR_CMD],
}); });
export const actionShortcuts = register({ export const actionShortcuts = register({
name: "toggleShortcuts", name: "toggleShortcuts",
trackEvent: { category: "menu", action: "toggleHelpDialog" }, trackEvent: { category: "menu", action: "toggleHelpDialog" },
perform: (_elements, appState, _, { focusContainer }) => { perform: (_elements, appState, _, { focusContainer }) => {
if (appState.openDialog === "help") { if (appState.showHelpDialog) {
focusContainer(); focusContainer();
} }
return { return {
appState: { appState: {
...appState, ...appState,
openDialog: appState.openDialog === "help" ? null : "help", showHelpDialog: !appState.showHelpDialog,
}, },
commitToHistory: false, commitToHistory: false,
}; };
}, },
PanelComponent: ({ updateData, isInHamburgerMenu }) => PanelComponent: ({ updateData }) => (
isInHamburgerMenu ? ( <HelpIcon title={t("helpDialog.title")} onClick={updateData} />
<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, keyTest: (event) => event.key === KEYS.QUESTION_MARK,
}); });

View File

@@ -2,41 +2,37 @@ import { AppState } from "../../src/types";
import { ButtonIconSelect } from "../components/ButtonIconSelect"; import { ButtonIconSelect } from "../components/ButtonIconSelect";
import { ColorPicker } from "../components/ColorPicker"; import { ColorPicker } from "../components/ColorPicker";
import { IconPicker } from "../components/IconPicker"; import { IconPicker } from "../components/IconPicker";
// TODO barnabasmolnar/editor-redesign
// TextAlignTopIcon, TextAlignBottomIcon,TextAlignMiddleIcon,
// ArrowHead icons
import { import {
ArrowheadArrowIcon, ArrowheadArrowIcon,
ArrowheadBarIcon, ArrowheadBarIcon,
ArrowheadDotIcon, ArrowheadDotIcon,
ArrowheadTriangleIcon, ArrowheadTriangleIcon,
ArrowheadNoneIcon, ArrowheadNoneIcon,
StrokeStyleDashedIcon, EdgeRoundIcon,
StrokeStyleDottedIcon, EdgeSharpIcon,
TextAlignTopIcon,
TextAlignBottomIcon,
TextAlignMiddleIcon,
FillHachureIcon,
FillCrossHatchIcon, FillCrossHatchIcon,
FillHachureIcon,
FillSolidIcon, FillSolidIcon,
FontFamilyCodeIcon,
FontFamilyHandDrawnIcon,
FontFamilyNormalIcon,
FontSizeExtraLargeIcon,
FontSizeLargeIcon,
FontSizeMediumIcon,
FontSizeSmallIcon,
SloppinessArchitectIcon, SloppinessArchitectIcon,
SloppinessArtistIcon, SloppinessArtistIcon,
SloppinessCartoonistIcon, SloppinessCartoonistIcon,
StrokeWidthBaseIcon, StrokeStyleDashedIcon,
StrokeWidthBoldIcon, StrokeStyleDottedIcon,
StrokeWidthExtraBoldIcon, StrokeStyleSolidIcon,
FontSizeSmallIcon, StrokeWidthIcon,
FontSizeMediumIcon,
FontSizeLargeIcon,
FontSizeExtraLargeIcon,
EdgeSharpIcon,
EdgeRoundIcon,
FreedrawIcon,
FontFamilyNormalIcon,
FontFamilyCodeIcon,
TextAlignLeftIcon,
TextAlignCenterIcon, TextAlignCenterIcon,
TextAlignLeftIcon,
TextAlignRightIcon, TextAlignRightIcon,
TextAlignTopIcon,
TextAlignBottomIcon,
TextAlignMiddleIcon,
} from "../components/icons"; } from "../components/icons";
import { import {
DEFAULT_FONT_FAMILY, DEFAULT_FONT_FAMILY,
@@ -311,17 +307,17 @@ export const actionChangeFillStyle = register({
{ {
value: "hachure", value: "hachure",
text: t("labels.hachure"), text: t("labels.hachure"),
icon: FillHachureIcon, icon: <FillHachureIcon theme={appState.theme} />,
}, },
{ {
value: "cross-hatch", value: "cross-hatch",
text: t("labels.crossHatch"), text: t("labels.crossHatch"),
icon: FillCrossHatchIcon, icon: <FillCrossHatchIcon theme={appState.theme} />,
}, },
{ {
value: "solid", value: "solid",
text: t("labels.solid"), text: t("labels.solid"),
icon: FillSolidIcon, icon: <FillSolidIcon theme={appState.theme} />,
}, },
]} ]}
group="fill" group="fill"
@@ -362,17 +358,17 @@ export const actionChangeStrokeWidth = register({
{ {
value: 1, value: 1,
text: t("labels.thin"), text: t("labels.thin"),
icon: StrokeWidthBaseIcon, icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={2} />,
}, },
{ {
value: 2, value: 2,
text: t("labels.bold"), text: t("labels.bold"),
icon: StrokeWidthBoldIcon, icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={6} />,
}, },
{ {
value: 4, value: 4,
text: t("labels.extraBold"), text: t("labels.extraBold"),
icon: StrokeWidthExtraBoldIcon, icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={10} />,
}, },
]} ]}
value={getFormValue( value={getFormValue(
@@ -411,17 +407,17 @@ export const actionChangeSloppiness = register({
{ {
value: 0, value: 0,
text: t("labels.architect"), text: t("labels.architect"),
icon: SloppinessArchitectIcon, icon: <SloppinessArchitectIcon theme={appState.theme} />,
}, },
{ {
value: 1, value: 1,
text: t("labels.artist"), text: t("labels.artist"),
icon: SloppinessArtistIcon, icon: <SloppinessArtistIcon theme={appState.theme} />,
}, },
{ {
value: 2, value: 2,
text: t("labels.cartoonist"), text: t("labels.cartoonist"),
icon: SloppinessCartoonistIcon, icon: <SloppinessCartoonistIcon theme={appState.theme} />,
}, },
]} ]}
value={getFormValue( value={getFormValue(
@@ -459,17 +455,17 @@ export const actionChangeStrokeStyle = register({
{ {
value: "solid", value: "solid",
text: t("labels.strokeStyle_solid"), text: t("labels.strokeStyle_solid"),
icon: StrokeWidthBaseIcon, icon: <StrokeStyleSolidIcon theme={appState.theme} />,
}, },
{ {
value: "dashed", value: "dashed",
text: t("labels.strokeStyle_dashed"), text: t("labels.strokeStyle_dashed"),
icon: StrokeStyleDashedIcon, icon: <StrokeStyleDashedIcon theme={appState.theme} />,
}, },
{ {
value: "dotted", value: "dotted",
text: t("labels.strokeStyle_dotted"), text: t("labels.strokeStyle_dotted"),
icon: StrokeStyleDottedIcon, icon: <StrokeStyleDottedIcon theme={appState.theme} />,
}, },
]} ]}
value={getFormValue( value={getFormValue(
@@ -539,25 +535,25 @@ export const actionChangeFontSize = register({
{ {
value: 16, value: 16,
text: t("labels.small"), text: t("labels.small"),
icon: FontSizeSmallIcon, icon: <FontSizeSmallIcon theme={appState.theme} />,
testId: "fontSize-small", testId: "fontSize-small",
}, },
{ {
value: 20, value: 20,
text: t("labels.medium"), text: t("labels.medium"),
icon: FontSizeMediumIcon, icon: <FontSizeMediumIcon theme={appState.theme} />,
testId: "fontSize-medium", testId: "fontSize-medium",
}, },
{ {
value: 28, value: 28,
text: t("labels.large"), text: t("labels.large"),
icon: FontSizeLargeIcon, icon: <FontSizeLargeIcon theme={appState.theme} />,
testId: "fontSize-large", testId: "fontSize-large",
}, },
{ {
value: 36, value: 36,
text: t("labels.veryLarge"), text: t("labels.veryLarge"),
icon: FontSizeExtraLargeIcon, icon: <FontSizeExtraLargeIcon theme={appState.theme} />,
testId: "fontSize-veryLarge", testId: "fontSize-veryLarge",
}, },
]} ]}
@@ -662,17 +658,17 @@ export const actionChangeFontFamily = register({
{ {
value: FONT_FAMILY.Virgil, value: FONT_FAMILY.Virgil,
text: t("labels.handDrawn"), text: t("labels.handDrawn"),
icon: FreedrawIcon, icon: <FontFamilyHandDrawnIcon theme={appState.theme} />,
}, },
{ {
value: FONT_FAMILY.Helvetica, value: FONT_FAMILY.Helvetica,
text: t("labels.normal"), text: t("labels.normal"),
icon: FontFamilyNormalIcon, icon: <FontFamilyNormalIcon theme={appState.theme} />,
}, },
{ {
value: FONT_FAMILY.Cascadia, value: FONT_FAMILY.Cascadia,
text: t("labels.code"), text: t("labels.code"),
icon: FontFamilyCodeIcon, icon: <FontFamilyCodeIcon theme={appState.theme} />,
}, },
]; ];
@@ -743,17 +739,17 @@ export const actionChangeTextAlign = register({
{ {
value: "left", value: "left",
text: t("labels.left"), text: t("labels.left"),
icon: TextAlignLeftIcon, icon: <TextAlignLeftIcon theme={appState.theme} />,
}, },
{ {
value: "center", value: "center",
text: t("labels.center"), text: t("labels.center"),
icon: TextAlignCenterIcon, icon: <TextAlignCenterIcon theme={appState.theme} />,
}, },
{ {
value: "right", value: "right",
text: t("labels.right"), text: t("labels.right"),
icon: TextAlignRightIcon, icon: <TextAlignRightIcon theme={appState.theme} />,
}, },
]} ]}
value={getFormValue( value={getFormValue(
@@ -886,12 +882,12 @@ export const actionChangeSharpness = register({
{ {
value: "sharp", value: "sharp",
text: t("labels.sharp"), text: t("labels.sharp"),
icon: EdgeSharpIcon, icon: <EdgeSharpIcon theme={appState.theme} />,
}, },
{ {
value: "round", value: "round",
text: t("labels.round"), text: t("labels.round"),
icon: EdgeRoundIcon, icon: <EdgeRoundIcon theme={appState.theme} />,
}, },
]} ]}
value={getFormValue( value={getFormValue(
@@ -953,38 +949,42 @@ export const actionChangeArrowhead = register({
return ( return (
<fieldset> <fieldset>
<legend>{t("labels.arrowheads")}</legend> <legend>{t("labels.arrowheads")}</legend>
<div className="iconSelectList buttonList"> <div className="iconSelectList">
<IconPicker <IconPicker
label="arrowhead_start" label="arrowhead_start"
options={[ options={[
{ {
value: null, value: null,
text: t("labels.arrowhead_none"), text: t("labels.arrowhead_none"),
icon: ArrowheadNoneIcon, icon: <ArrowheadNoneIcon theme={appState.theme} />,
keyBinding: "q", keyBinding: "q",
}, },
{ {
value: "arrow", value: "arrow",
text: t("labels.arrowhead_arrow"), text: t("labels.arrowhead_arrow"),
icon: <ArrowheadArrowIcon flip={!isRTL} />, icon: (
<ArrowheadArrowIcon theme={appState.theme} flip={!isRTL} />
),
keyBinding: "w", keyBinding: "w",
}, },
{ {
value: "bar", value: "bar",
text: t("labels.arrowhead_bar"), text: t("labels.arrowhead_bar"),
icon: <ArrowheadBarIcon flip={!isRTL} />, icon: <ArrowheadBarIcon theme={appState.theme} flip={!isRTL} />,
keyBinding: "e", keyBinding: "e",
}, },
{ {
value: "dot", value: "dot",
text: t("labels.arrowhead_dot"), text: t("labels.arrowhead_dot"),
icon: <ArrowheadDotIcon flip={!isRTL} />, icon: <ArrowheadDotIcon theme={appState.theme} flip={!isRTL} />,
keyBinding: "r", keyBinding: "r",
}, },
{ {
value: "triangle", value: "triangle",
text: t("labels.arrowhead_triangle"), text: t("labels.arrowhead_triangle"),
icon: <ArrowheadTriangleIcon flip={!isRTL} />, icon: (
<ArrowheadTriangleIcon theme={appState.theme} flip={!isRTL} />
),
keyBinding: "t", keyBinding: "t",
}, },
]} ]}
@@ -1007,30 +1007,34 @@ export const actionChangeArrowhead = register({
value: null, value: null,
text: t("labels.arrowhead_none"), text: t("labels.arrowhead_none"),
keyBinding: "q", keyBinding: "q",
icon: ArrowheadNoneIcon, icon: <ArrowheadNoneIcon theme={appState.theme} />,
}, },
{ {
value: "arrow", value: "arrow",
text: t("labels.arrowhead_arrow"), text: t("labels.arrowhead_arrow"),
keyBinding: "w", keyBinding: "w",
icon: <ArrowheadArrowIcon flip={isRTL} />, icon: (
<ArrowheadArrowIcon theme={appState.theme} flip={isRTL} />
),
}, },
{ {
value: "bar", value: "bar",
text: t("labels.arrowhead_bar"), text: t("labels.arrowhead_bar"),
keyBinding: "e", keyBinding: "e",
icon: <ArrowheadBarIcon flip={isRTL} />, icon: <ArrowheadBarIcon theme={appState.theme} flip={isRTL} />,
}, },
{ {
value: "dot", value: "dot",
text: t("labels.arrowhead_dot"), text: t("labels.arrowhead_dot"),
keyBinding: "r", keyBinding: "r",
icon: <ArrowheadDotIcon flip={isRTL} />, icon: <ArrowheadDotIcon theme={appState.theme} flip={isRTL} />,
}, },
{ {
value: "triangle", value: "triangle",
text: t("labels.arrowhead_triangle"), text: t("labels.arrowhead_triangle"),
icon: <ArrowheadTriangleIcon flip={isRTL} />, icon: (
<ArrowheadTriangleIcon theme={appState.theme} flip={isRTL} />
),
keyBinding: "t", keyBinding: "t",
}, },
]} ]}

View File

@@ -2,43 +2,29 @@ import { KEYS } from "../keys";
import { register } from "./register"; import { register } from "./register";
import { selectGroupsForSelectedElements } from "../groups"; import { selectGroupsForSelectedElements } from "../groups";
import { getNonDeletedElements, isTextElement } from "../element"; import { getNonDeletedElements, isTextElement } from "../element";
import { ExcalidrawElement } from "../element/types";
import { isLinearElement } from "../element/typeChecks";
import { LinearElementEditor } from "../element/linearElementEditor";
export const actionSelectAll = register({ export const actionSelectAll = register({
name: "selectAll", name: "selectAll",
trackEvent: { category: "canvas" }, trackEvent: { category: "canvas" },
perform: (elements, appState, value, app) => { perform: (elements, appState) => {
if (appState.editingLinearElement) { if (appState.editingLinearElement) {
return false; return false;
} }
const selectedElementIds = elements.reduce(
(map: Record<ExcalidrawElement["id"], true>, element) => {
if (
!element.isDeleted &&
!(isTextElement(element) && element.containerId) &&
!element.locked
) {
map[element.id] = true;
}
return map;
},
{},
);
return { return {
appState: selectGroupsForSelectedElements( appState: selectGroupsForSelectedElements(
{ {
...appState, ...appState,
selectedLinearElement:
// single linear element selected
Object.keys(selectedElementIds).length === 1 &&
isLinearElement(elements[0])
? new LinearElementEditor(elements[0], app.scene)
: null,
editingGroupId: null, editingGroupId: null,
selectedElementIds, selectedElementIds: elements.reduce((map, element) => {
if (
!element.isDeleted &&
!(isTextElement(element) && element.containerId) &&
element.locked === false
) {
map[element.id] = true;
}
return map;
}, {} as any),
}, },
getNonDeletedElements(elements), getNonDeletedElements(elements),
), ),

View File

@@ -36,7 +36,7 @@ export const actionCopyStyles = register({
return { return {
appState: { appState: {
...appState, ...appState,
toast: { message: t("toast.copyStyles") }, toastMessage: t("toast.copyStyles"),
}, },
commitToHistory: false, commitToHistory: false,
}; };

View File

@@ -17,19 +17,16 @@ export const actionToggleLock = register({
const operation = getOperation(selectedElements); const operation = getOperation(selectedElements);
const selectedElementsMap = arrayToMap(selectedElements); const selectedElementsMap = arrayToMap(selectedElements);
const lock = operation === "lock";
return { return {
elements: elements.map((element) => { elements: elements.map((element) => {
if (!selectedElementsMap.has(element.id)) { if (!selectedElementsMap.has(element.id)) {
return element; return element;
} }
return newElementWith(element, { locked: lock }); return newElementWith(element, { locked: operation === "lock" });
}), }),
appState: { appState,
...appState,
selectedLinearElement: lock ? null : appState.selectedLinearElement,
},
commitToHistory: true, commitToHistory: true,
}; };
}, },

View File

@@ -10,10 +10,10 @@ import { t } from "../i18n";
import { getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
import { import {
BringForwardIcon,
BringToFrontIcon,
SendBackwardIcon, SendBackwardIcon,
BringToFrontIcon,
SendToBackIcon, SendToBackIcon,
BringForwardIcon,
} from "../components/icons"; } from "../components/icons";
export const actionSendBackward = register({ export const actionSendBackward = register({
@@ -39,7 +39,7 @@ export const actionSendBackward = register({
onClick={() => updateData(null)} onClick={() => updateData(null)}
title={`${t("labels.sendBackward")}${getShortcutKey("CtrlOrCmd+[")}`} title={`${t("labels.sendBackward")}${getShortcutKey("CtrlOrCmd+[")}`}
> >
{SendBackwardIcon} <SendBackwardIcon theme={appState.theme} />
</button> </button>
), ),
}); });
@@ -67,7 +67,7 @@ export const actionBringForward = register({
onClick={() => updateData(null)} onClick={() => updateData(null)}
title={`${t("labels.bringForward")}${getShortcutKey("CtrlOrCmd+]")}`} title={`${t("labels.bringForward")}${getShortcutKey("CtrlOrCmd+]")}`}
> >
{BringForwardIcon} <BringForwardIcon theme={appState.theme} />
</button> </button>
), ),
}); });
@@ -102,7 +102,7 @@ export const actionSendToBack = register({
: getShortcutKey("CtrlOrCmd+Shift+[") : getShortcutKey("CtrlOrCmd+Shift+[")
}`} }`}
> >
{SendToBackIcon} <SendToBackIcon theme={appState.theme} />
</button> </button>
), ),
}); });
@@ -138,7 +138,7 @@ export const actionBringToFront = register({
: getShortcutKey("CtrlOrCmd+Shift+]") : getShortcutKey("CtrlOrCmd+Shift+]")
}`} }`}
> >
{BringToFrontIcon} <BringToFrontIcon theme={appState.theme} />
</button> </button>
), ),
}); });

View File

@@ -85,4 +85,3 @@ export { actionToggleStats } from "./actionToggleStats";
export { actionUnbindText, actionBindText } from "./actionBoundText"; export { actionUnbindText, actionBindText } from "./actionBoundText";
export { actionLink } from "../element/Hyperlink"; export { actionLink } from "../element/Hyperlink";
export { actionToggleLock } from "./actionToggleLock"; export { actionToggleLock } from "./actionToggleLock";
export { actionToggleLinearEditor } from "./actionLinearEditor";

View File

@@ -135,11 +135,7 @@ export class ActionManager {
/** /**
* @param data additional data sent to the PanelComponent * @param data additional data sent to the PanelComponent
*/ */
renderAction = ( renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => {
name: ActionName,
data?: PanelComponentProps["data"],
isInHamburgerMenu = false,
) => {
const canvasActions = this.app.props.UIOptions.canvasActions; const canvasActions = this.app.props.UIOptions.canvasActions;
if ( if (
@@ -151,7 +147,6 @@ export class ActionManager {
) { ) {
const action = this.actions[name]; const action = this.actions[name];
const PanelComponent = action.PanelComponent!; const PanelComponent = action.PanelComponent!;
PanelComponent.displayName = "PanelComponent";
const elements = this.getElementsIncludingDeleted(); const elements = this.getElementsIncludingDeleted();
const appState = this.getAppState(); const appState = this.getAppState();
const updateData = (formState?: any) => { const updateData = (formState?: any) => {
@@ -174,7 +169,6 @@ export class ActionManager {
updateData={updateData} updateData={updateData}
appProps={this.app.props} appProps={this.app.props}
data={data} data={data}
isInHamburgerMenu={isInHamburgerMenu}
/> />
); );
} }

View File

@@ -3,11 +3,8 @@ import { isDarwin } from "../keys";
import { getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
import { ActionName } from "./types"; import { ActionName } from "./types";
export type ShortcutName = export type ShortcutName = SubtypeOf<
| SubtypeOf<
ActionName, ActionName,
| "toggleTheme"
| "loadScene"
| "cut" | "cut"
| "copy" | "copy"
| "paste" | "paste"
@@ -33,15 +30,9 @@ export type ShortcutName =
| "flipVertical" | "flipVertical"
| "hyperlink" | "hyperlink"
| "toggleLock" | "toggleLock"
> >;
| "saveScene"
| "imageExport";
const shortcutMap: Record<ShortcutName, string[]> = { 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")], cut: [getShortcutKey("CtrlOrCmd+X")],
copy: [getShortcutKey("CtrlOrCmd+C")], copy: [getShortcutKey("CtrlOrCmd+C")],
paste: [getShortcutKey("CtrlOrCmd+V")], paste: [getShortcutKey("CtrlOrCmd+V")],

View File

@@ -111,8 +111,7 @@ export type ActionName =
| "hyperlink" | "hyperlink"
| "eraser" | "eraser"
| "bindText" | "bindText"
| "toggleLock" | "toggleLock";
| "toggleLinearEditor";
export type PanelComponentProps = { export type PanelComponentProps = {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
@@ -124,9 +123,7 @@ export type PanelComponentProps = {
export interface Action { export interface Action {
name: ActionName; name: ActionName;
PanelComponent?: React.FC< PanelComponent?: React.FC<PanelComponentProps>;
PanelComponentProps & { isInHamburgerMenu: boolean }
>;
perform: ActionFn; perform: ActionFn;
keyPriority?: number; keyPriority?: number;
keyTest?: ( keyTest?: (

View File

@@ -19,7 +19,6 @@ export const getDefaultAppState = (): Omit<
"offsetTop" | "offsetLeft" | "width" | "height" "offsetTop" | "offsetLeft" | "width" | "height"
> => { > => {
return { return {
showWelcomeScreen: false,
theme: THEME.LIGHT, theme: THEME.LIGHT,
collaborators: new Map(), collaborators: new Map(),
currentChartType: "bar", currentChartType: "bar",
@@ -58,7 +57,8 @@ export const getDefaultAppState = (): Omit<
fileHandle: null, fileHandle: null,
gridSize: null, gridSize: null,
isBindingEnabled: true, isBindingEnabled: true,
isSidebarDocked: false, isLibraryOpen: false,
isLibraryMenuDocked: false,
isLoading: false, isLoading: false,
isResizing: false, isResizing: false,
isRotating: false, isRotating: false,
@@ -67,8 +67,6 @@ export const getDefaultAppState = (): Omit<
name: `${t("labels.untitled")}-${getDateTime()}`, name: `${t("labels.untitled")}-${getDateTime()}`,
openMenu: null, openMenu: null,
openPopup: null, openPopup: null,
openSidebar: null,
openDialog: null,
pasteDialog: { shown: false, data: null }, pasteDialog: { shown: false, data: null },
previousSelectedElementIds: {}, previousSelectedElementIds: {},
resizingElement: null, resizingElement: null,
@@ -79,10 +77,11 @@ export const getDefaultAppState = (): Omit<
selectedGroupIds: {}, selectedGroupIds: {},
selectionElement: null, selectionElement: null,
shouldCacheIgnoreZoom: false, shouldCacheIgnoreZoom: false,
showHelpDialog: false,
showStats: false, showStats: false,
startBoundElement: null, startBoundElement: null,
suggestedBindings: [], suggestedBindings: [],
toast: null, toastMessage: null,
viewBackgroundColor: oc.white, viewBackgroundColor: oc.white,
zenModeEnabled: false, zenModeEnabled: false,
zoom: { zoom: {
@@ -91,7 +90,6 @@ export const getDefaultAppState = (): Omit<
viewModeEnabled: false, viewModeEnabled: false,
pendingImageElementId: null, pendingImageElementId: null,
showHyperlinkPopup: false, showHyperlinkPopup: false,
selectedLinearElement: null,
}; };
}; };
@@ -103,92 +101,228 @@ const APP_STATE_STORAGE_CONF = (<
Values extends { Values extends {
/** whether to keep when storing to browser storage (localStorage/IDB) */ /** whether to keep when storing to browser storage (localStorage/IDB) */
browser: boolean; browser: boolean;
/** whether to keep when exporting to file/database */ /** whether to keep when exporting to a text file */
export: boolean; text: boolean;
/** whether to keep when exporting to an image file */
image: boolean;
/** server (shareLink/collab/...) */ /** server (shareLink/collab/...) */
server: boolean; server: boolean;
}, },
T extends Record<keyof AppState, Values>, T extends Record<keyof AppState, Values>,
>(config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }) => >(config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }) =>
config)({ config)({
showWelcomeScreen: { browser: true, export: false, server: false }, theme: { browser: true, text: false, image: false, server: false },
theme: { browser: true, export: false, server: false }, collaborators: { browser: false, text: false, image: false, server: false },
collaborators: { browser: false, export: false, server: false }, currentChartType: { browser: true, text: false, image: false, server: false },
currentChartType: { browser: true, export: false, server: false }, currentItemBackgroundColor: {
currentItemBackgroundColor: { browser: true, export: false, server: false },
currentItemEndArrowhead: { browser: true, export: false, server: false },
currentItemFillStyle: { browser: true, export: false, server: false },
currentItemFontFamily: { browser: true, export: false, server: false },
currentItemFontSize: { browser: true, export: false, server: false },
currentItemLinearStrokeSharpness: {
browser: true, browser: true,
export: false, text: false,
image: false,
server: false,
},
currentItemEndArrowhead: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemFillStyle: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemFontFamily: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemFontSize: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemLinearStrokeSharpness: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemOpacity: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemRoughness: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStartArrowhead: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStrokeColor: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStrokeSharpness: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStrokeStyle: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStrokeWidth: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemTextAlign: {
browser: true,
text: false,
image: false,
server: false,
},
cursorButton: { browser: true, text: false, image: false, server: false },
draggingElement: { browser: false, text: false, image: false, server: false },
editingElement: { browser: false, text: false, image: false, server: false },
editingGroupId: { browser: true, text: false, image: false, server: false },
editingLinearElement: {
browser: false,
text: false,
image: false,
server: false,
},
activeTool: { browser: true, text: false, image: false, server: false },
penMode: { browser: true, text: false, image: false, server: false },
penDetected: { browser: true, text: false, image: false, server: false },
errorMessage: { browser: false, text: false, image: false, server: false },
exportBackground: { browser: true, text: false, image: true, server: false },
exportEmbedScene: { browser: true, text: false, image: true, server: false },
exportScale: { browser: true, text: false, image: true, server: false },
exportWithDarkMode: {
browser: true,
text: false,
image: true,
server: false,
},
fileHandle: { browser: false, text: false, image: false, server: false },
gridSize: { browser: true, text: true, image: true, server: true },
height: { browser: false, text: false, image: false, server: false },
isBindingEnabled: {
browser: false,
text: false,
image: false,
server: false,
},
isLibraryOpen: { browser: true, text: false, image: false, server: false },
isLibraryMenuDocked: {
browser: true,
text: false,
image: false,
server: false,
},
isLoading: { browser: false, text: false, image: false, server: false },
isResizing: { browser: false, text: false, image: false, server: false },
isRotating: { browser: false, text: false, image: false, server: false },
lastPointerDownWith: {
browser: true,
text: false,
image: false,
server: false,
},
multiElement: { browser: false, text: false, image: false, server: false },
name: { browser: true, text: false, image: false, server: false },
offsetLeft: { browser: false, text: false, image: false, server: false },
offsetTop: { browser: false, text: false, image: false, server: false },
openMenu: { browser: true, text: false, image: false, server: false },
openPopup: { browser: false, text: false, image: false, server: false },
pasteDialog: { browser: false, text: false, image: false, server: false },
previousSelectedElementIds: {
browser: true,
text: false,
image: false,
server: false,
},
resizingElement: { browser: false, text: false, image: false, server: false },
scrolledOutside: { browser: true, text: false, image: false, server: false },
scrollX: { browser: true, text: false, image: false, server: false },
scrollY: { browser: true, text: false, image: false, server: false },
selectedElementIds: {
browser: true,
text: false,
image: false,
server: false,
},
selectedGroupIds: { browser: true, text: false, image: false, server: false },
selectionElement: {
browser: false,
text: false,
image: false,
server: false,
},
shouldCacheIgnoreZoom: {
browser: true,
text: false,
image: false,
server: false,
},
showHelpDialog: { browser: false, text: false, image: false, server: false },
showStats: { browser: true, text: false, image: false, server: false },
startBoundElement: {
browser: false,
text: false,
image: false,
server: false,
},
suggestedBindings: {
browser: false,
text: false,
image: false,
server: false,
},
toastMessage: { browser: false, text: false, image: false, server: false },
viewBackgroundColor: {
browser: true,
text: true,
image: true,
server: true,
},
width: { browser: false, text: false, image: false, server: false },
zenModeEnabled: { browser: true, text: false, image: false, server: false },
zoom: { browser: true, text: false, image: false, server: false },
viewModeEnabled: { browser: false, text: false, image: false, server: false },
pendingImageElementId: {
browser: false,
text: false,
image: false,
server: false,
},
showHyperlinkPopup: {
browser: false,
text: false,
image: false,
server: false, server: false,
}, },
currentItemOpacity: { browser: true, export: false, server: false },
currentItemRoughness: { browser: true, export: false, server: false },
currentItemStartArrowhead: { browser: true, export: false, server: false },
currentItemStrokeColor: { browser: true, export: false, server: false },
currentItemStrokeSharpness: { browser: true, export: false, server: false },
currentItemStrokeStyle: { browser: true, export: false, server: false },
currentItemStrokeWidth: { browser: true, export: false, server: false },
currentItemTextAlign: { browser: true, export: false, server: false },
cursorButton: { browser: true, export: false, server: false },
draggingElement: { browser: false, export: false, server: false },
editingElement: { browser: false, export: false, server: false },
editingGroupId: { browser: true, export: false, server: false },
editingLinearElement: { browser: false, export: false, server: false },
activeTool: { browser: true, export: false, server: false },
penMode: { browser: true, export: false, server: false },
penDetected: { browser: true, export: false, server: false },
errorMessage: { browser: false, export: false, server: false },
exportBackground: { browser: true, export: false, server: false },
exportEmbedScene: { browser: true, export: false, server: false },
exportScale: { 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 },
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 },
lastPointerDownWith: { browser: true, export: false, server: false },
multiElement: { browser: false, export: false, server: false },
name: { browser: true, export: false, server: false },
offsetLeft: { browser: false, export: false, server: false },
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 },
scrolledOutside: { browser: true, export: false, server: false },
scrollX: { browser: true, export: false, server: false },
scrollY: { browser: true, export: false, server: false },
selectedElementIds: { browser: true, export: false, server: false },
selectedGroupIds: { browser: true, export: false, server: false },
selectionElement: { browser: false, export: false, server: false },
shouldCacheIgnoreZoom: { browser: true, 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 },
toast: { browser: false, export: false, server: false },
viewBackgroundColor: { browser: true, export: true, server: true },
width: { browser: false, export: false, server: false },
zenModeEnabled: { browser: true, export: false, server: false },
zoom: { browser: true, export: false, server: false },
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 = < const _clearAppStateForStorage = <
ExportType extends "export" | "browser" | "server", ExportType extends "image" | "text" | "browser" | "server",
>( >(
appState: Partial<AppState>, appState: Partial<AppState>,
exportType: ExportType, exportType: ExportType,
@@ -215,8 +349,12 @@ export const clearAppStateForLocalStorage = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "browser"); return _clearAppStateForStorage(appState, "browser");
}; };
export const cleanAppStateForExport = (appState: Partial<AppState>) => { export const cleanAppStateForTextExport = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "export"); return _clearAppStateForStorage(appState, "text");
};
export const cleanAppStateForImageExport = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "image");
}; };
export const clearAppStateForDatabase = (appState: Partial<AppState>) => { export const clearAppStateForDatabase = (appState: Partial<AppState>) => {

View File

@@ -11,18 +11,27 @@ export const getClientColors = (clientId: string, appState: AppState) => {
// Naive way of getting an integer out of the clientId // Naive way of getting an integer out of the clientId
const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0); const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0);
// Skip transparent & gray colors // Skip transparent background.
const backgrounds = colors.elementBackground.slice(3); const backgrounds = colors.elementBackground.slice(1);
const strokes = colors.elementStroke.slice(3); const strokes = colors.elementStroke.slice(1);
return { return {
background: backgrounds[sum % backgrounds.length], background: backgrounds[sum % backgrounds.length],
stroke: strokes[sum % strokes.length], stroke: strokes[sum % strokes.length],
}; };
}; };
export const getClientInitials = (userName?: string | null) => { export const getClientInitials = (username?: string | null) => {
if (!userName) { if (!username) {
return "?"; return "?";
} }
return userName.trim()[0].toUpperCase(); 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();
}; };

View File

@@ -1,92 +0,0 @@
.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;
}
}
}

View File

@@ -26,19 +26,17 @@ import { ToolButton } from "./ToolButton";
import { hasStrokeColor } from "../scene/comparisons"; import { hasStrokeColor } from "../scene/comparisons";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks"; import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks";
import clsx from "clsx";
import { actionToggleZenMode } from "../actions";
import "./Actions.scss";
import { Tooltip } from "./Tooltip";
export const SelectedShapeActions = ({ export const SelectedShapeActions = ({
appState, appState,
elements, elements,
renderAction, renderAction,
activeTool,
}: { }: {
appState: AppState; appState: AppState;
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
renderAction: ActionManager["renderAction"]; renderAction: ActionManager["renderAction"];
activeTool: AppState["activeTool"]["type"];
}) => { }) => {
const targetElements = getTargetElements( const targetElements = getTargetElements(
getNonDeletedElements(elements), getNonDeletedElements(elements),
@@ -58,13 +56,13 @@ export const SelectedShapeActions = ({
const isRTL = document.documentElement.getAttribute("dir") === "rtl"; const isRTL = document.documentElement.getAttribute("dir") === "rtl";
const showFillIcons = const showFillIcons =
hasBackground(appState.activeTool.type) || hasBackground(activeTool) ||
targetElements.some( targetElements.some(
(element) => (element) =>
hasBackground(element.type) && !isTransparent(element.backgroundColor), hasBackground(element.type) && !isTransparent(element.backgroundColor),
); );
const showChangeBackgroundIcons = const showChangeBackgroundIcons =
hasBackground(appState.activeTool.type) || hasBackground(activeTool) ||
targetElements.some((element) => hasBackground(element.type)); targetElements.some((element) => hasBackground(element.type));
const showLinkIcon = const showLinkIcon =
@@ -81,27 +79,23 @@ export const SelectedShapeActions = ({
return ( return (
<div className="panelColumn"> <div className="panelColumn">
<div> {((hasStrokeColor(activeTool) &&
{((hasStrokeColor(appState.activeTool.type) && activeTool !== "image" &&
appState.activeTool.type !== "image" &&
commonSelectedType !== "image") || commonSelectedType !== "image") ||
targetElements.some((element) => hasStrokeColor(element.type))) && targetElements.some((element) => hasStrokeColor(element.type))) &&
renderAction("changeStrokeColor")} renderAction("changeStrokeColor")}
</div> {showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
{showChangeBackgroundIcons && (
<div>{renderAction("changeBackgroundColor")}</div>
)}
{showFillIcons && renderAction("changeFillStyle")} {showFillIcons && renderAction("changeFillStyle")}
{(hasStrokeWidth(appState.activeTool.type) || {(hasStrokeWidth(activeTool) ||
targetElements.some((element) => hasStrokeWidth(element.type))) && targetElements.some((element) => hasStrokeWidth(element.type))) &&
renderAction("changeStrokeWidth")} renderAction("changeStrokeWidth")}
{(appState.activeTool.type === "freedraw" || {(activeTool === "freedraw" ||
targetElements.some((element) => element.type === "freedraw")) && targetElements.some((element) => element.type === "freedraw")) &&
renderAction("changeStrokeShape")} renderAction("changeStrokeShape")}
{(hasStrokeStyle(appState.activeTool.type) || {(hasStrokeStyle(activeTool) ||
targetElements.some((element) => hasStrokeStyle(element.type))) && ( targetElements.some((element) => hasStrokeStyle(element.type))) && (
<> <>
{renderAction("changeStrokeStyle")} {renderAction("changeStrokeStyle")}
@@ -109,12 +103,12 @@ export const SelectedShapeActions = ({
</> </>
)} )}
{(canChangeSharpness(appState.activeTool.type) || {(canChangeSharpness(activeTool) ||
targetElements.some((element) => canChangeSharpness(element.type))) && ( targetElements.some((element) => canChangeSharpness(element.type))) && (
<>{renderAction("changeSharpness")}</> <>{renderAction("changeSharpness")}</>
)} )}
{(hasText(appState.activeTool.type) || {(hasText(activeTool) ||
targetElements.some((element) => hasText(element.type))) && ( targetElements.some((element) => hasText(element.type))) && (
<> <>
{renderAction("changeFontSize")} {renderAction("changeFontSize")}
@@ -129,7 +123,7 @@ export const SelectedShapeActions = ({
(element) => (element) =>
hasBoundTextElement(element) || isBoundToContainer(element), hasBoundTextElement(element) || isBoundToContainer(element),
) && renderAction("changeVerticalAlign")} ) && renderAction("changeVerticalAlign")}
{(canHaveArrowheads(appState.activeTool.type) || {(canHaveArrowheads(activeTool) ||
targetElements.some((element) => canHaveArrowheads(element.type))) && ( targetElements.some((element) => canHaveArrowheads(element.type))) && (
<>{renderAction("changeArrowhead")}</> <>{renderAction("changeArrowhead")}</>
)} )}
@@ -169,16 +163,7 @@ export const SelectedShapeActions = ({
)} )}
{targetElements.length > 2 && {targetElements.length > 2 &&
renderAction("distributeHorizontally")} renderAction("distributeHorizontally")}
{/* breaks the row ˇˇ */} <div className="iconRow">
<div style={{ flexBasis: "100%", height: 0 }} />
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: ".5rem",
marginTop: "-0.5rem",
}}
>
{renderAction("alignTop")} {renderAction("alignTop")}
{renderAction("alignVerticallyCentered")} {renderAction("alignVerticallyCentered")}
{renderAction("alignBottom")} {renderAction("alignBottom")}
@@ -218,26 +203,25 @@ export const ShapesSwitcher = ({
appState: AppState; appState: AppState;
}) => ( }) => (
<> <>
{SHAPES.map(({ value, icon, key, fillable }, index) => { {SHAPES.map(({ value, icon, key }, index) => {
const numberKey = value === "eraser" ? 0 : index + 1;
const label = t(`toolBar.${value}`); const label = t(`toolBar.${value}`);
const letter = key && (typeof key === "string" ? key : key[0]); const letter = key && (typeof key === "string" ? key : key[0]);
const shortcut = letter const shortcut = letter
? `${capitalizeString(letter)} ${t("helpDialog.or")} ${numberKey}` ? `${capitalizeString(letter)} ${t("helpDialog.or")} ${index + 1}`
: `${numberKey}`; : `${index + 1}`;
return ( return (
<ToolButton <ToolButton
className={clsx("Shape", { fillable })} className="Shape"
key={value} key={value}
type="radio" type="radio"
icon={icon} icon={icon}
checked={activeTool.type === value} checked={activeTool.type === value}
name="editor-current-shape" name="editor-current-shape"
title={`${capitalizeString(label)}${shortcut}`} title={`${capitalizeString(label)}${shortcut}`}
keyBindingLabel={`${numberKey}`} keyBindingLabel={`${index + 1}`}
aria-label={capitalizeString(label)} aria-label={capitalizeString(label)}
aria-keyshortcuts={shortcut} aria-keyshortcuts={shortcut}
data-testid={`toolbar-${value}`} data-testid={value}
onPointerDown={({ pointerType }) => { onPointerDown={({ pointerType }) => {
if (!appState.penDetected && pointerType === "pen") { if (!appState.penDetected && pointerType === "pen") {
setAppState({ setAppState({
@@ -279,57 +263,11 @@ export const ZoomActions = ({
renderAction: ActionManager["renderAction"]; renderAction: ActionManager["renderAction"];
zoom: Zoom; zoom: Zoom;
}) => ( }) => (
<Stack.Col gap={1} className="zoom-actions"> <Stack.Col gap={1}>
<Stack.Row align="center"> <Stack.Row gap={1} align="center">
{renderAction("zoomOut")} {renderAction("zoomOut")}
{renderAction("resetZoom")}
{renderAction("zoomIn")} {renderAction("zoomIn")}
{renderAction("resetZoom")}
</Stack.Row> </Stack.Row>
</Stack.Col> </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>
);

View File

@@ -1,11 +1,9 @@
// TODO barnabasmolnar/editor-redesign import Stack from "../components/Stack";
// this icon is not great import { ToolButton } from "../components/ToolButton";
import { getShortcutFromShortcutName } from "../actions/shortcuts"; import { save, file } from "../components/icons";
import { save } from "../components/icons";
import { t } from "../i18n"; import { t } from "../i18n";
import "./ActiveFile.scss"; import "./ActiveFile.scss";
import MenuItem from "./MenuItem";
type ActiveFileProps = { type ActiveFileProps = {
fileName?: string; fileName?: string;
@@ -13,11 +11,18 @@ type ActiveFileProps = {
}; };
export const ActiveFile = ({ fileName, onSave }: ActiveFileProps) => ( export const ActiveFile = ({ fileName, onSave }: ActiveFileProps) => (
<MenuItem <Stack.Row className="ActiveFile" gap={1} align="center">
label={`${t("buttons.save")}`} <span className="ActiveFile__fileName">
shortcut={getShortcutFromShortcutName("saveScene")} {file}
dataTestId="save-button" <span>{fileName}</span>
onClick={onSave} </span>
<ToolButton
type="icon"
icon={save} icon={save}
title={t("buttons.save")}
aria-label={t("buttons.save")}
onClick={onSave}
data-testid="save-button"
/> />
</Stack.Row>
); );

File diff suppressed because it is too large Load Diff

View File

@@ -2,19 +2,16 @@
.excalidraw { .excalidraw {
.Avatar { .Avatar {
width: 1.25rem; width: 2.5rem;
height: 1.25rem; height: 2.5rem;
border-radius: 100%; border-radius: 1.25rem;
outline: 2px solid var(--avatar-border-color);
outline-offset: 2px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
color: $oc-white; color: $oc-white;
cursor: pointer; cursor: pointer;
font-size: 0.625rem; font-size: 0.8rem;
font-weight: 500; font-weight: 500;
line-height: 1;
&-img { &-img {
width: 100%; width: 100%;

View File

@@ -11,11 +11,13 @@ type AvatarProps = {
src?: string; src?: string;
}; };
export const Avatar = ({ color, onClick, name, src }: AvatarProps) => { export const Avatar = ({ color, border, onClick, name, src }: AvatarProps) => {
const shortName = getClientInitials(name); const shortName = getClientInitials(name);
const [error, setError] = useState(false); const [error, setError] = useState(false);
const loadImg = !error && src; const loadImg = !error && src;
const style = loadImg ? undefined : { background: color }; const style = loadImg
? undefined
: { background: color, border: `1px solid ${border}` };
return ( return (
<div className="Avatar" style={style} onClick={onClick}> <div className="Avatar" style={style} onClick={onClick}>
{loadImg ? ( {loadImg ? (

View File

@@ -0,0 +1,20 @@
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>
);

View File

@@ -4,7 +4,6 @@ import "./Card.scss";
export const Card: React.FC<{ export const Card: React.FC<{
color: keyof OpenColor | "primary"; color: keyof OpenColor | "primary";
children?: React.ReactNode;
}> = ({ children, color }) => { }> = ({ children, color }) => {
return ( return (
<div <div

View File

@@ -64,8 +64,6 @@
color: #{$oc-blue-7}; color: #{$oc-blue-7};
border: 0;
&:focus { &:focus {
box-shadow: 0 0 0 3px #{$oc-blue-7}; box-shadow: 0 0 0 3px #{$oc-blue-7};
} }

View File

@@ -8,7 +8,6 @@ export const CheckboxItem: React.FC<{
checked: boolean; checked: boolean;
onChange: (checked: boolean, event: React.MouseEvent) => void; onChange: (checked: boolean, event: React.MouseEvent) => void;
className?: string; className?: string;
children?: React.ReactNode;
}> = ({ children, checked, onChange, className }) => { }> = ({ children, checked, onChange, className }) => {
return ( return (
<div <div

View File

@@ -1,9 +1,10 @@
import { useState } from "react"; import { useState } from "react";
import { t } from "../i18n"; import { t } from "../i18n";
import { TrashIcon } from "./icons"; import { useDevice } from "./App";
import { trash } from "./icons";
import { ToolButton } from "./ToolButton";
import ConfirmDialog from "./ConfirmDialog"; import ConfirmDialog from "./ConfirmDialog";
import MenuItem from "./MenuItem";
const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => { const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => {
const [showDialog, setShowDialog] = useState(false); const [showDialog, setShowDialog] = useState(false);
@@ -13,11 +14,14 @@ const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => {
return ( return (
<> <>
<MenuItem <ToolButton
label={t("buttons.clearReset")} type="button"
icon={TrashIcon} icon={trash}
title={t("buttons.clearReset")}
aria-label={t("buttons.clearReset")}
showAriaLabel={useDevice().isMobile}
onClick={toggleDialog} onClick={toggleDialog}
dataTestId="clear-canvas-button" data-testid="clear-canvas-button"
/> />
{showDialog && ( {showDialog && (

View File

@@ -1,51 +1,6 @@
@import "../css/variables.module"; @import "../css/variables.module";
.excalidraw { .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 { .CollabButton.is-collaborating {
background-color: var(--button-special-active-bg-color); background-color: var(--button-special-active-bg-color);
@@ -63,15 +18,13 @@
left: -5px; left: -5px;
} }
min-width: 1em; min-width: 1em;
min-height: 1em;
line-height: 1;
position: absolute; position: absolute;
bottom: -5px; bottom: -5px;
padding: 3px; padding: 3px;
border-radius: 50%; border-radius: 50%;
background-color: $oc-green-2; background-color: $oc-green-6;
color: $oc-green-9; color: $oc-white;
font-size: 0.6rem; font-size: 0.7em;
font-family: "Cascadia"; font-family: var(--ui-font);
} }
} }

View File

@@ -1,47 +1,37 @@
import clsx from "clsx";
import { ToolButton } from "./ToolButton";
import { t } from "../i18n"; import { t } from "../i18n";
import { UsersIcon } from "./icons"; import { useDevice } from "../components/App";
import { users } from "./icons";
import "./CollabButton.scss"; import "./CollabButton.scss";
import MenuItem from "./MenuItem";
import clsx from "clsx";
const CollabButton = ({ const CollabButton = ({
isCollaborating, isCollaborating,
collaboratorCount, collaboratorCount,
onClick, onClick,
isInHamburgerMenu = true,
}: { }: {
isCollaborating: boolean; isCollaborating: boolean;
collaboratorCount: number; collaboratorCount: number;
onClick: () => void; onClick: () => void;
isInHamburgerMenu?: boolean;
}) => { }) => {
return ( return (
<> <>
{isInHamburgerMenu ? ( <ToolButton
<MenuItem className={clsx("CollabButton", {
label={t("labels.liveCollaboration")} "is-collaborating": isCollaborating,
dataTestId="collab-button" })}
icon={UsersIcon}
onClick={onClick} onClick={onClick}
isCollaborating={isCollaborating} icon={users}
/>
) : (
<button
className={clsx("collab-button", { active: isCollaborating })}
type="button" type="button"
onClick={onClick}
style={{ position: "relative" }}
title={t("labels.liveCollaboration")} title={t("labels.liveCollaboration")}
aria-label={t("labels.liveCollaboration")}
showAriaLabel={useDevice().isMobile}
> >
{UsersIcon}
{collaboratorCount > 0 && ( {collaboratorCount > 0 && (
<div className="CollabButton-collaborators"> <div className="CollabButton-collaborators">{collaboratorCount}</div>
{collaboratorCount}
</div>
)}
</button>
)} )}
</ToolButton>
</> </>
); );
}; };

View File

@@ -21,23 +21,6 @@
display: grid; display: grid;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
align-items: center; 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 { .color-picker-triangle {
@@ -47,29 +30,20 @@
border-width: 0 9px 10px; border-width: 0 9px 10px;
border-color: transparent transparent var(--popup-bg-color); border-color: transparent transparent var(--popup-bg-color);
position: absolute; position: absolute;
top: 10px; top: -10px;
:root[dir="ltr"] & { :root[dir="ltr"] & {
transform: rotate(270deg); left: 12px;
left: -14px;
} }
:root[dir="rtl"] & { :root[dir="rtl"] & {
transform: rotate(90deg); right: 12px;
right: -14px;
} }
} }
.color-picker-triangle-shadow { .color-picker-triangle-shadow {
border-color: transparent transparent transparentize($oc-black, 0.9); 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 { .color-picker-content--default {
@@ -145,21 +119,16 @@
} }
.color-picker-hash { .color-picker-hash {
height: var(--default-button-size); background: var(--input-border-color);
flex-shrink: 0; height: 1.875rem;
padding: 0.5rem 0.5rem 0.5rem 0.75rem; width: 1.875rem;
border: 1px solid var(--default-border-color);
border-right: 0;
box-sizing: border-box;
:root[dir="ltr"] & { :root[dir="ltr"] & {
border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg); border-radius: 4px 0 0 4px;
} }
:root[dir="rtl"] & { :root[dir="rtl"] & {
border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0; border-radius: 0 4px 4px 0;
border-right: 1px solid var(--default-border-color);
border-left: 0;
} }
color: var(--input-label-color); color: var(--input-label-color);
@@ -169,64 +138,81 @@
position: relative; position: relative;
} }
.color-input-container { .color-input-container:focus-within .color-picker-hash {
display: flex; box-shadow: 0 0 0 2px var(--focus-highlight-color);
&:focus-within {
box-shadow: 0 0 0 1px var(--color-primary-darkest);
border-radius: var(--border-radius-lg);
}
} }
.color-picker-input { .color-input-container:focus-within .color-picker-hash::before,
box-sizing: border-box; .color-input-container:focus-within .color-picker-hash::after {
width: 100%; content: "";
margin: 0; width: 1px;
font-size: 0.875rem; height: 100%;
background-color: transparent; position: absolute;
color: var(--text-primary-color); top: 0;
border: 0; }
outline: none;
height: var(--default-button-size); .color-input-container:focus-within .color-picker-hash::before {
border: 1px solid var(--default-border-color); background: var(--input-border-color);
border-left: 0;
letter-spacing: 0.4px;
:root[dir="ltr"] & { :root[dir="ltr"] & {
border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0; right: -1px;
} }
:root[dir="rtl"] & { :root[dir="rtl"] & {
border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg); left: -1px;
border-left: 1px solid var(--default-border-color); }
border-right: 0;
} }
padding: 0.5rem; .color-input-container:focus-within .color-picker-hash::after {
padding-left: 0.25rem; background: var(--input-bg-color);
:root[dir="ltr"] & {
right: -2px;
}
:root[dir="rtl"] & {
left: -2px;
}
}
.color-input-container {
display: flex;
}
.color-picker-input {
width: 11ch; /* length of `transparent` */
margin: 0;
font-size: 1rem;
background-color: var(--input-bg-color);
color: var(--text-primary-color);
border: 0;
outline: none;
height: 1.75em;
box-shadow: var(--input-border-color) 0 0 0 1px inset;
:root[dir="ltr"] & {
border-radius: 0 4px 4px 0;
}
:root[dir="rtl"] & {
border-radius: 4px 0 0 4px;
}
float: left;
padding: 1px;
padding-inline-start: 0.5em;
appearance: none; 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 { .color-picker-label-swatch {
@include outlineButtonStyles; height: 1.875rem;
background-color: var(--swatch-color) !important; width: 1.875rem;
overflow: hidden; margin-inline-end: 0.25rem;
border: 1px solid $oc-gray-3;
position: relative; position: relative;
overflow: hidden;
background-color: transparent !important;
filter: var(--theme-filter); filter: var(--theme-filter);
border: 0 !important;
&:after { &:after {
content: ""; content: "";

View File

@@ -343,8 +343,6 @@ const ColorInput = React.forwardRef(
}, },
); );
ColorInput.displayName = "ColorInput";
export const ColorPicker = ({ export const ColorPicker = ({
type, type,
color, color,
@@ -365,12 +363,10 @@ export const ColorPicker = ({
appState: AppState; appState: AppState;
}) => { }) => {
const pickerButton = React.useRef<HTMLButtonElement>(null); const pickerButton = React.useRef<HTMLButtonElement>(null);
const coords = pickerButton.current?.getBoundingClientRect();
return ( return (
<div> <div>
<div className="color-picker-control-container"> <div className="color-picker-control-container">
<div className="color-picker-label-swatch-container">
<button <button
className="color-picker-label-swatch" className="color-picker-label-swatch"
aria-label={label} aria-label={label}
@@ -378,7 +374,6 @@ export const ColorPicker = ({
onClick={() => setActive(!isActive)} onClick={() => setActive(!isActive)}
ref={pickerButton} ref={pickerButton}
/> />
</div>
<ColorInput <ColorInput
color={color} color={color}
label={label} label={label}
@@ -389,15 +384,6 @@ export const ColorPicker = ({
</div> </div>
<React.Suspense fallback=""> <React.Suspense fallback="">
{isActive ? ( {isActive ? (
<div
className="color-picker-popover-container"
style={{
position: "fixed",
top: coords?.top,
left: coords?.right,
zIndex: 1,
}}
>
<Popover <Popover
onCloseRequest={(event) => onCloseRequest={(event) =>
event.target !== pickerButton.current && setActive(false) event.target !== pickerButton.current && setActive(false)
@@ -419,7 +405,6 @@ export const ColorPicker = ({
elements={elements} elements={elements}
/> />
</Popover> </Popover>
</div>
) : null} ) : null}
</React.Suspense> </React.Suspense>
</div> </div>

View File

@@ -4,8 +4,34 @@
.confirm-dialog { .confirm-dialog {
&-buttons { &-buttons {
display: flex; display: flex;
column-gap: 0.5rem; padding: 0.2rem 0;
justify-content: flex-end; 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;
}
}
} }
} }

View File

@@ -1,11 +1,8 @@
import { t } from "../i18n"; import { t } from "../i18n";
import { Dialog, DialogProps } from "./Dialog"; import { Dialog, DialogProps } from "./Dialog";
import { ToolButton } from "./ToolButton";
import "./ConfirmDialog.scss"; 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"> { interface Props extends Omit<DialogProps, "onCloseRequest"> {
onConfirm: () => void; onConfirm: () => void;
@@ -23,10 +20,6 @@ const ConfirmDialog = (props: Props) => {
className = "", className = "",
...rest ...rest
} = props; } = props;
const setIsMenuOpen = useSetAtom(isMenuOpenAtom);
const setIsDropdownOpen = useSetAtom(isDropdownOpenAtom);
return ( return (
<Dialog <Dialog
onCloseRequest={onCancel} onCloseRequest={onCancel}
@@ -36,22 +29,21 @@ const ConfirmDialog = (props: Props) => {
> >
{children} {children}
<div className="confirm-dialog-buttons"> <div className="confirm-dialog-buttons">
<DialogActionButton <ToolButton
type="button"
title={cancelText}
aria-label={cancelText}
label={cancelText} label={cancelText}
onClick={() => { onClick={onCancel}
setIsMenuOpen(false); className="confirm-dialog--cancel"
setIsDropdownOpen(false);
onCancel();
}}
/> />
<DialogActionButton <ToolButton
type="button"
title={confirmText}
aria-label={confirmText}
label={confirmText} label={confirmText}
onClick={() => { onClick={onConfirm}
setIsMenuOpen(false); className="confirm-dialog--confirm"
setIsDropdownOpen(false);
onConfirm();
}}
actionType="danger"
/> />
</div> </div>
</Dialog> </Dialog>

View File

@@ -7,11 +7,68 @@
} }
.Dialog__title { .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; margin: 0;
text-align: left; }
font-size: 1.25rem;
border-bottom: 1px solid var(--dialog-border-color); .Dialog__content {
padding: 0 0 0.75rem; padding: 0 16px 16px;
margin-bottom: 1.5rem; }
@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;
}
} }
} }

View File

@@ -5,13 +5,11 @@ import { t } from "../i18n";
import { useExcalidrawContainer, useDevice } from "../components/App"; import { useExcalidrawContainer, useDevice } from "../components/App";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import "./Dialog.scss"; import "./Dialog.scss";
import { back, CloseIcon } from "./icons"; import { back, close } from "./icons";
import { Island } from "./Island"; import { Island } from "./Island";
import { Modal } from "./Modal"; import { Modal } from "./Modal";
import { AppState } from "../types"; import { AppState } from "../types";
import { queryFocusableElements } from "../utils"; import { queryFocusableElements } from "../utils";
import { isMenuOpenAtom, isDropdownOpenAtom } from "./App";
import { useSetAtom } from "jotai";
export interface DialogProps { export interface DialogProps {
children: React.ReactNode; children: React.ReactNode;
@@ -67,12 +65,7 @@ export const Dialog = (props: DialogProps) => {
return () => islandNode.removeEventListener("keydown", handleKeyDown); return () => islandNode.removeEventListener("keydown", handleKeyDown);
}, [islandNode, props.autofocus]); }, [islandNode, props.autofocus]);
const setIsMenuOpen = useSetAtom(isMenuOpenAtom);
const setIsDropdownOpen = useSetAtom(isDropdownOpenAtom);
const onClose = () => { const onClose = () => {
setIsMenuOpen(false);
setIsDropdownOpen(false);
(lastActiveElement as HTMLElement).focus(); (lastActiveElement as HTMLElement).focus();
props.onCloseRequest(); props.onCloseRequest();
}; };
@@ -92,10 +85,9 @@ export const Dialog = (props: DialogProps) => {
<button <button
className="Modal__close" className="Modal__close"
onClick={onClose} onClick={onClose}
title={t("buttons.close")}
aria-label={t("buttons.close")} aria-label={t("buttons.close")}
> >
{useDevice().isMobile ? back : CloseIcon} {useDevice().isMobile ? back : close}
</button> </button>
</h2> </h2>
<div className="Dialog__content">{props.children}</div> <div className="Dialog__content">{props.children}</div>

View File

@@ -1,47 +0,0 @@
.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);
}
}
}

View File

@@ -1,46 +0,0 @@
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;

View File

@@ -1,19 +0,0 @@
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;

View File

@@ -91,8 +91,6 @@
} }
button.ExportDialog-imageExportButton { button.ExportDialog-imageExportButton {
border: 0;
width: 5rem; width: 5rem;
height: 5rem; height: 5rem;
margin: 0 0.2em; margin: 0 0.2em;

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