mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-08-29 13:16:58 +02:00
Compare commits
173 Commits
preserve-a
...
dependabot
Author | SHA1 | Date | |
---|---|---|---|
![]() |
37ad451f67 | ||
![]() |
4d26993c8f | ||
![]() |
1e69609ce4 | ||
![]() |
f5379d1563 | ||
![]() |
c8f6e3faa8 | ||
![]() |
36bf17cf59 | ||
![]() |
75458c3192 | ||
![]() |
4cd25253bf | ||
![]() |
78e254fb30 | ||
![]() |
79bd3b8cda | ||
![]() |
55110bf1b8 | ||
![]() |
941b2d7042 | ||
![]() |
e9067de173 | ||
![]() |
fdc462ec01 | ||
![]() |
d1441afec9 | ||
![]() |
3298aaf0c7 | ||
![]() |
e9a224a0de | ||
![]() |
76cf560914 | ||
![]() |
6c1246ef77 | ||
![]() |
b477c2ad6b | ||
![]() |
4cb6f09559 | ||
![]() |
8636ef1017 | ||
![]() |
3a776f8795 | ||
![]() |
9929a2be6f | ||
![]() |
9cccac1458 | ||
![]() |
7eaf47c9d4 | ||
![]() |
ec4b3d913e | ||
![]() |
5390617c01 | ||
![]() |
0d1058a596 | ||
![]() |
c5869979c8 | ||
![]() |
6a6b9c90a7 | ||
![]() |
5c17751662 | ||
![]() |
898789b979 | ||
![]() |
7922ce129e | ||
![]() |
59ec1c6cee | ||
![]() |
933c6a2237 | ||
![]() |
cd61f31116 | ||
![]() |
b3052f0178 | ||
![]() |
a271e42af1 | ||
![]() |
836120c14b | ||
![]() |
da4fa91ffc | ||
![]() |
553b493f37 | ||
![]() |
59a1d192d2 | ||
![]() |
8b7302e89e | ||
![]() |
f9b7cfd8aa | ||
![]() |
2b4462c941 | ||
![]() |
43b13d8e3a | ||
![]() |
720f468f39 | ||
![]() |
33300d19f6 | ||
![]() |
5aed159991 | ||
![]() |
de1d221d1c | ||
![]() |
9a68dbffe2 | ||
![]() |
32d82219b1 | ||
![]() |
ba2c86fe1b | ||
![]() |
f1ae37c84b | ||
![]() |
ec350ba8b2 | ||
![]() |
46a61ad4df | ||
![]() |
f4b1a30bef | ||
![]() |
32aa79164b | ||
![]() |
b5fd904808 | ||
![]() |
8f8dd1105f | ||
![]() |
b914ad41fc | ||
![]() |
551c38f60b | ||
![]() |
38e8ae46c9 | ||
![]() |
ad0c4c4c78 | ||
![]() |
27cf5ed17e | ||
![]() |
fd946adbae | ||
![]() |
c37977af4b | ||
![]() |
a0d413ab4e | ||
![]() |
b67a2b4f65 | ||
![]() |
5a8dbe8030 | ||
![]() |
731093f631 | ||
![]() |
fe56975f19 | ||
![]() |
2d800feeeb | ||
![]() |
93cccd596a | ||
![]() |
45b592227d | ||
![]() |
b818df1098 | ||
![]() |
4359e2935d | ||
![]() |
3d9d398378 | ||
![]() |
0a5da0269f | ||
![]() |
08ce7c7fc3 | ||
![]() |
fe7fbff7f6 | ||
![]() |
501397cb61 | ||
![]() |
865d29388c | ||
![]() |
54c7ec416a | ||
![]() |
aca284057d | ||
![]() |
2820cd112e | ||
![]() |
426b5d9537 | ||
![]() |
e7d34677c6 | ||
![]() |
3d5356cb8e | ||
![]() |
46f5ce5ce0 | ||
![]() |
b00bd3d6c0 | ||
![]() |
91fc22182c | ||
![]() |
966ca2ffa6 | ||
![]() |
2b049b4a65 | ||
![]() |
339212e563 | ||
![]() |
f8b4bb66b4 | ||
![]() |
f4312bba5e | ||
![]() |
ac66665b64 | ||
![]() |
2b71a1f0bd | ||
![]() |
58845e450a | ||
![]() |
15d79f8fee | ||
![]() |
958ebeae61 | ||
![]() |
31f51ca53b | ||
![]() |
5abbf73050 | ||
![]() |
7cf766630d | ||
![]() |
59fccafeac | ||
![]() |
19a6996e6b | ||
![]() |
86c4f90910 | ||
![]() |
4d88112021 | ||
![]() |
de5c63e299 | ||
![]() |
da0853a121 | ||
![]() |
57cc4b6a29 | ||
![]() |
e2ddd7b27a | ||
![]() |
693de8501e | ||
![]() |
c6df6d444e | ||
![]() |
ad5692c5f8 | ||
![]() |
60ab3337af | ||
![]() |
dd847793d2 | ||
![]() |
6d6e9f0dd3 | ||
![]() |
0fe0d7ca6b | ||
![]() |
abcf1f1bae | ||
![]() |
7d0b03f754 | ||
![]() |
bd8931d3d1 | ||
![]() |
0d86c04939 | ||
![]() |
8436ebbf68 | ||
![]() |
824f94b3df | ||
![]() |
f9a8e686b2 | ||
![]() |
e442a44ba8 | ||
![]() |
f1fd29571a | ||
![]() |
6a482a7ba2 | ||
![]() |
bfea434a55 | ||
![]() |
acb22c5a64 | ||
![]() |
7cd1b621d1 | ||
![]() |
9c37b25bab | ||
![]() |
a8bb9a78ef | ||
![]() |
e4aff04061 | ||
![]() |
c5cadc7de3 | ||
![]() |
7dc0c0d96a | ||
![]() |
2c9c8c8e05 | ||
![]() |
b5d7ae57e5 | ||
![]() |
0f66ee3a41 | ||
![]() |
771372c66b | ||
![]() |
a7937681e9 | ||
![]() |
792f238d16 | ||
![]() |
ba16416c75 | ||
![]() |
6e0ac52a64 | ||
![]() |
5bc40402a6 | ||
![]() |
df14c69977 | ||
![]() |
1ea67ba93d | ||
![]() |
a7153d9d1d | ||
![]() |
e885057a71 | ||
![]() |
7efa081976 | ||
![]() |
5deb93a083 | ||
![]() |
e3908e6fe3 | ||
![]() |
fe3d0b5e8b | ||
![]() |
b6bb74d08d | ||
![]() |
c725f84334 | ||
![]() |
11a3380d83 | ||
![]() |
76a5bb060e | ||
![]() |
dac8dda4d4 | ||
![]() |
a1a62468a6 | ||
![]() |
ba3a723e99 | ||
![]() |
c5355c08cf | ||
![]() |
6102380051 | ||
![]() |
655e59a707 | ||
![]() |
d05745070b | ||
![]() |
88c313bf86 | ||
![]() |
a7705848ec | ||
![]() |
69e1bae8dd | ||
![]() |
d361757e4a | ||
![]() |
0ef202f2df | ||
![]() |
bbfd2b3cd3 |
43
.codesandbox/tasks.json
Normal file
43
.codesandbox/tasks.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
// These tasks will run in order when initializing your CodeSandbox project.
|
||||||
|
"setupTasks": [
|
||||||
|
{
|
||||||
|
"name": "Install Dependencies",
|
||||||
|
"command": "yarn install"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// These tasks can be run from CodeSandbox. Running one will open a log in the app.
|
||||||
|
"tasks": {
|
||||||
|
"build": {
|
||||||
|
"name": "Build",
|
||||||
|
"command": "yarn build",
|
||||||
|
"runAtStart": false
|
||||||
|
},
|
||||||
|
"fix": {
|
||||||
|
"name": "Fix",
|
||||||
|
"command": "yarn fix",
|
||||||
|
"runAtStart": false
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"name": "Prettify",
|
||||||
|
"command": "yarn prettier",
|
||||||
|
"runAtStart": false
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"name": "Start Excalidraw",
|
||||||
|
"command": "yarn start",
|
||||||
|
"runAtStart": true
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"name": "Run Tests",
|
||||||
|
"command": "yarn test",
|
||||||
|
"runAtStart": false
|
||||||
|
},
|
||||||
|
"install-deps": {
|
||||||
|
"name": "Install Dependencies",
|
||||||
|
"command": "yarn install",
|
||||||
|
"restartOn": { "files": ["yarn.lock"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -11,3 +11,12 @@ 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
37
.github/dependabot.yml
vendored
@@ -1,37 +0,0 @@
|
|||||||
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
|
|
2
.github/workflows/autorelease-excalidraw.yml
vendored
2
.github/workflows/autorelease-excalidraw.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Auto release @excalidraw/excalidraw-next
|
name: Auto release excalidraw next
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
2
.github/workflows/autorelease-preview.yml
vendored
2
.github/workflows/autorelease-preview.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Auto release preview @excalidraw/excalidraw-preview
|
name: Auto release excalidraw preview
|
||||||
on:
|
on:
|
||||||
issue_comment:
|
issue_comment:
|
||||||
types: [created, edited]
|
types: [created, edited]
|
||||||
|
15
.github/workflows/publish-docker.yml
vendored
15
.github/workflows/publish-docker.yml
vendored
@@ -10,11 +10,16 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Checkout repository
|
||||||
- uses: docker/build-push-action@v2
|
uses: actions/checkout@v3
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: excalidraw/excalidraw
|
- name: Build and push
|
||||||
tag_with_ref: true
|
uses: docker/build-push-action@v3
|
||||||
tag_with_sha: true
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: excalidraw/excalidraw:latest
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,11 +19,9 @@ 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
|
||||||
|
|
||||||
|
@@ -88,7 +88,7 @@ Try out [`@excalidraw/excalidraw`](https://www.npmjs.com/package/@excalidraw/exc
|
|||||||
|
|
||||||
### Code Sandbox
|
### Code Sandbox
|
||||||
|
|
||||||
- Go to https://codesandbox.io/s/github/excalidraw/excalidraw
|
- Go to https://codesandbox.io/p/github/excalidraw/excalidraw
|
||||||
- You may need to sign in with GitHub and reload the page
|
- You 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
Normal file
20
dev-docs/.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.docusaurus
|
||||||
|
.cache-loader
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
41
dev-docs/README.md
Normal file
41
dev-docs/README.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Website
|
||||||
|
|
||||||
|
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
Using SSH:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ USE_SSH=true yarn deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Not using SSH:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ GIT_USER=<Your GitHub username> yarn deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
3
dev-docs/babel.config.js
Normal file
3
dev-docs/babel.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
|
||||||
|
};
|
6
dev-docs/docs/codebase/overview.md
Normal file
6
dev-docs/docs/codebase/overview.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 1
|
||||||
|
title: Overview
|
||||||
|
---
|
||||||
|
|
||||||
|
In development. For now, refer to [excalidraw Readme](https://github.com/excalidraw/excalidraw/blob/master/README.md).
|
8
dev-docs/docs/get-started.md
Normal file
8
dev-docs/docs/get-started.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 1
|
||||||
|
title: Introduction
|
||||||
|
---
|
||||||
|
|
||||||
|
Want to integrate Excalidraw into your app? Head over to the [package docs](/docs/package/overview).
|
||||||
|
|
||||||
|
If you're looking into the Excalidraw codebase itself, start [here](/docs/codebase/overview).
|
6
dev-docs/docs/package/overview.md
Normal file
6
dev-docs/docs/package/overview.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 1
|
||||||
|
title: Overview
|
||||||
|
---
|
||||||
|
|
||||||
|
In development. For now, refer to [excalidraw package readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md).
|
121
dev-docs/docusaurus.config.js
Normal file
121
dev-docs/docusaurus.config.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Note: type annotations allow type checking and IDEs autocompletion
|
||||||
|
|
||||||
|
const lightCodeTheme = require("prism-react-renderer/themes/github");
|
||||||
|
const darkCodeTheme = require("prism-react-renderer/themes/dracula");
|
||||||
|
|
||||||
|
/** @type {import('@docusaurus/types').Config} */
|
||||||
|
const config = {
|
||||||
|
title: "Excalidraw developer docs",
|
||||||
|
tagline:
|
||||||
|
"For Excalidraw contributors or those integrating the Excalidraw editor",
|
||||||
|
url: "https://docs.excalidraw.com.com",
|
||||||
|
baseUrl: "/",
|
||||||
|
onBrokenLinks: "throw",
|
||||||
|
onBrokenMarkdownLinks: "warn",
|
||||||
|
favicon: "img/favicon.ico",
|
||||||
|
organizationName: "Excalidraw", // Usually your GitHub org/user name.
|
||||||
|
projectName: "excalidraw", // Usually your repo name.
|
||||||
|
|
||||||
|
// Even if you don't use internalization, you can use this field to set useful
|
||||||
|
// metadata like html lang. For example, if your site is Chinese, you may want
|
||||||
|
// to replace "en" with "zh-Hans".
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: "en",
|
||||||
|
locales: ["en"],
|
||||||
|
},
|
||||||
|
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
"classic",
|
||||||
|
/** @type {import('@docusaurus/preset-classic').Options} */
|
||||||
|
({
|
||||||
|
docs: {
|
||||||
|
sidebarPath: require.resolve("./sidebars.js"),
|
||||||
|
// Please change this to your repo.
|
||||||
|
editUrl: "https://github.com/excalidraw/docs/tree/master/",
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
customCss: require.resolve("./src/css/custom.css"),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
themeConfig:
|
||||||
|
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
|
||||||
|
({
|
||||||
|
navbar: {
|
||||||
|
title: "Excalidraw Docs",
|
||||||
|
logo: {
|
||||||
|
alt: "Excalidraw Logo",
|
||||||
|
src: "img/logo.svg",
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: "doc",
|
||||||
|
docId: "get-started",
|
||||||
|
position: "left",
|
||||||
|
label: "Get started",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: "https://blog.excalidraw.com",
|
||||||
|
label: "Blog",
|
||||||
|
position: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: "https://github.com/excalidraw/excalidraw",
|
||||||
|
label: "GitHub",
|
||||||
|
position: "right",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
style: "dark",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
title: "Docs",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: "Get Started",
|
||||||
|
to: "/docs/get-started",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Community",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: "Discord",
|
||||||
|
href: "https://discord.gg/UexuTaE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Twitter",
|
||||||
|
href: "https://twitter.com/excalidraw",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "More",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: "Blog",
|
||||||
|
to: "https://blog.excalidraw.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "GitHub",
|
||||||
|
to: "https://github.com/excalidraw/excalidraw",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
copyright: `Made with ❤️ Built with Docusaurus`,
|
||||||
|
},
|
||||||
|
prism: {
|
||||||
|
theme: lightCodeTheme,
|
||||||
|
darkTheme: darkCodeTheme,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
46
dev-docs/package.json
Normal file
46
dev-docs/package.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "docs",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"docusaurus": "docusaurus",
|
||||||
|
"start": "docusaurus start --port 3003",
|
||||||
|
"build": "docusaurus build",
|
||||||
|
"swizzle": "docusaurus swizzle",
|
||||||
|
"deploy": "docusaurus deploy",
|
||||||
|
"clear": "docusaurus clear",
|
||||||
|
"serve": "docusaurus serve",
|
||||||
|
"write-translations": "docusaurus write-translations",
|
||||||
|
"write-heading-ids": "docusaurus write-heading-ids",
|
||||||
|
"typecheck": "tsc"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@docusaurus/core": "2.0.0-rc.1",
|
||||||
|
"@docusaurus/preset-classic": "2.0.0-rc.1",
|
||||||
|
"@mdx-js/react": "^1.6.22",
|
||||||
|
"clsx": "^1.2.1",
|
||||||
|
"prism-react-renderer": "^1.3.5",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@docusaurus/module-type-aliases": "2.0.0-rc.1",
|
||||||
|
"@tsconfig/docusaurus": "^1.0.5",
|
||||||
|
"typescript": "^4.7.4"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.5%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.14"
|
||||||
|
}
|
||||||
|
}
|
31
dev-docs/sidebars.js
Normal file
31
dev-docs/sidebars.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Creating a sidebar enables you to:
|
||||||
|
- create an ordered group of docs
|
||||||
|
- render a sidebar for each doc of that group
|
||||||
|
- provide next/previous navigation
|
||||||
|
|
||||||
|
The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||||
|
|
||||||
|
Create as many sidebars as you want.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
|
||||||
|
const sidebars = {
|
||||||
|
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||||
|
tutorialSidebar: [{ type: "autogenerated", dirName: "." }],
|
||||||
|
|
||||||
|
// But you can create a sidebar manually
|
||||||
|
/*
|
||||||
|
tutorialSidebar: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Tutorial',
|
||||||
|
items: ['hello'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = sidebars;
|
62
dev-docs/src/components/Homepage/index.js
Normal file
62
dev-docs/src/components/Homepage/index.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import React from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
const FeatureList = [
|
||||||
|
{
|
||||||
|
title: "Learn how Excalidraw works",
|
||||||
|
Svg: require("@site/static/img/undraw_innovative.svg").default,
|
||||||
|
description: (
|
||||||
|
<>Want to contribute to Excalidraw but got lost in the codebase?</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Integrate Excalidraw",
|
||||||
|
Svg: require("@site/static/img/undraw_blank_canvas.svg").default,
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Want to build your own app powered by Excalidraw by don't know where to
|
||||||
|
start?
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Help us improve",
|
||||||
|
Svg: require("@site/static/img/undraw_add_files.svg").default,
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Are the docs missing something? Anything you had trouble understanding
|
||||||
|
or needs an explanation? Come contribute to the docs to make them even
|
||||||
|
better!
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function Feature({ Svg, title, description }) {
|
||||||
|
return (
|
||||||
|
<div className={clsx("col col--4")}>
|
||||||
|
<div className="text--center">
|
||||||
|
<Svg className={styles.featureSvg} role="img" />
|
||||||
|
</div>
|
||||||
|
<div className="text--center padding-horiz--md">
|
||||||
|
<h3>{title}</h3>
|
||||||
|
<p>{description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HomepageFeatures() {
|
||||||
|
return (
|
||||||
|
<section className={styles.features}>
|
||||||
|
<div className="container">
|
||||||
|
<div className="row">
|
||||||
|
{FeatureList.map((props, idx) => (
|
||||||
|
<Feature key={idx} {...props} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
70
dev-docs/src/components/Homepage/index.tsx
Normal file
70
dev-docs/src/components/Homepage/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import React from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
type FeatureItem = {
|
||||||
|
title: string;
|
||||||
|
Svg: React.ComponentType<React.ComponentProps<"svg">>;
|
||||||
|
description: JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FeatureList: FeatureItem[] = [
|
||||||
|
{
|
||||||
|
title: "Easy to Use",
|
||||||
|
Svg: require("@site/static/img/undraw_docusaurus_mountain.svg").default,
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Docusaurus was designed from the ground up to be easily installed and
|
||||||
|
used to get your website up and running quickly.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Focus on What Matters",
|
||||||
|
Svg: require("@site/static/img/undraw_docusaurus_tree.svg").default,
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Docusaurus lets you focus on your docs, and we'll do the chores. Go
|
||||||
|
ahead and move your docs into the <code>docs</code> directory.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Powered by React",
|
||||||
|
Svg: require("@site/static/img/undraw_docusaurus_react.svg").default,
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Extend or customize your website layout by reusing React. Docusaurus can
|
||||||
|
be extended while reusing the same header and footer.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function Feature({ title, Svg, description }: FeatureItem) {
|
||||||
|
return (
|
||||||
|
<div className={clsx("col col--4")}>
|
||||||
|
<div className="text--center">
|
||||||
|
<Svg className={styles.featureSvg} role="img" />
|
||||||
|
</div>
|
||||||
|
<div className="text--center padding-horiz--md">
|
||||||
|
<h3>{title}</h3>
|
||||||
|
<p>{description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HomepageFeatures(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<section className={styles.features}>
|
||||||
|
<div className="container">
|
||||||
|
<div className="row">
|
||||||
|
{FeatureList.map((props, idx) => (
|
||||||
|
<Feature key={idx} {...props} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
11
dev-docs/src/components/Homepage/styles.module.css
Normal file
11
dev-docs/src/components/Homepage/styles.module.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.features {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2rem 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featureSvg {
|
||||||
|
height: 200px;
|
||||||
|
width: 200px;
|
||||||
|
}
|
43
dev-docs/src/css/custom.css
Normal file
43
dev-docs/src/css/custom.css
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Any CSS included here will be global. The classic template
|
||||||
|
* bundles Infima by default. Infima is a CSS framework designed to
|
||||||
|
* work well for content-centric websites.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* You can override the default Infima variables here. */
|
||||||
|
:root {
|
||||||
|
--ifm-color-primary: #6965db;
|
||||||
|
--ifm-color-primary-dark: #5b57d1;
|
||||||
|
--ifm-color-primary-darker: #5b57d1;
|
||||||
|
--ifm-color-primary-darkest: #4a47b1;
|
||||||
|
--ifm-color-primary-light: #5b57d1;
|
||||||
|
--ifm-color-primary-lighter: #5b57d1;
|
||||||
|
--ifm-color-primary-lightest: #5b57d1;
|
||||||
|
--ifm-code-font-size: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--ifm-color-primary: #5650f0;
|
||||||
|
--ifm-color-primary-dark: #4b46d8;
|
||||||
|
--ifm-color-primary-darker: #4b46d8;
|
||||||
|
--ifm-color-primary-darkest: #3e39be;
|
||||||
|
--ifm-color-primary-light: #3f3d64;
|
||||||
|
--ifm-color-primary-lighter: #3f3d64;
|
||||||
|
--ifm-color-primary-lightest: #3f3d64;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docusaurus-highlight-code-line {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
display: block;
|
||||||
|
margin: 0 calc(-1 * var(--ifm-pre-padding));
|
||||||
|
padding: 0 var(--ifm-pre-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .docusaurus-highlight-code-line {
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .navbar__logo {
|
||||||
|
filter: invert(93%) hue-rotate(180deg);
|
||||||
|
}
|
42
dev-docs/src/pages/index.js
Normal file
42
dev-docs/src/pages/index.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import Layout from "@theme/Layout";
|
||||||
|
import Link from "@docusaurus/Link";
|
||||||
|
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||||
|
import styles from "./index.module.css";
|
||||||
|
import HomepageFeatures from "@site/src/components/Homepage";
|
||||||
|
|
||||||
|
function HomepageHeader() {
|
||||||
|
const { siteConfig } = useDocusaurusContext();
|
||||||
|
return (
|
||||||
|
<header className={clsx("hero hero--primary", styles.heroBanner)}>
|
||||||
|
<div className="container">
|
||||||
|
<h1 className="hero__title">{siteConfig.title}</h1>
|
||||||
|
<p className="hero__subtitle">{siteConfig.tagline}</p>
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<Link
|
||||||
|
className="button button--secondary button--lg"
|
||||||
|
to="/docs/get-started"
|
||||||
|
>
|
||||||
|
Get started
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const { siteConfig } = useDocusaurusContext();
|
||||||
|
return (
|
||||||
|
<Layout
|
||||||
|
title={`Hello from ${siteConfig.title}`}
|
||||||
|
description="Description will go into a meta tag in <head />"
|
||||||
|
>
|
||||||
|
<HomepageHeader />
|
||||||
|
<main>
|
||||||
|
<HomepageFeatures />
|
||||||
|
</main>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
27
dev-docs/src/pages/index.module.css
Normal file
27
dev-docs/src/pages/index.module.css
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* CSS files with the .module.css suffix will be treated as CSS modules
|
||||||
|
* and scoped locally.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.heroBanner {
|
||||||
|
padding: 4rem 0;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .heroBanner {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 996px) {
|
||||||
|
.heroBanner {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
42
dev-docs/src/pages/index.tsx
Normal file
42
dev-docs/src/pages/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import Layout from "@theme/Layout";
|
||||||
|
import Link from "@docusaurus/Link";
|
||||||
|
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||||
|
import styles from "./index.module.css";
|
||||||
|
import HomepageFeatures from "@site/src/components/Homepage";
|
||||||
|
|
||||||
|
function HomepageHeader() {
|
||||||
|
const { siteConfig } = useDocusaurusContext();
|
||||||
|
return (
|
||||||
|
<header className={clsx("hero hero--primary", styles.heroBanner)}>
|
||||||
|
<div className="container">
|
||||||
|
<h1 className="hero__title">{siteConfig.title}</h1>
|
||||||
|
<p className="hero__subtitle">{siteConfig.tagline}</p>
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<Link
|
||||||
|
className="button button--secondary button--lg"
|
||||||
|
to="/docs/get-started"
|
||||||
|
>
|
||||||
|
Get started
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const { siteConfig } = useDocusaurusContext();
|
||||||
|
return (
|
||||||
|
<Layout
|
||||||
|
title={`Hello from ${siteConfig.title}`}
|
||||||
|
description="Description will go into a meta tag in <head />"
|
||||||
|
>
|
||||||
|
<HomepageHeader />
|
||||||
|
<main>
|
||||||
|
<HomepageFeatures />
|
||||||
|
</main>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
7
dev-docs/src/pages/markdown-page.md
Normal file
7
dev-docs/src/pages/markdown-page.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Markdown page example
|
||||||
|
---
|
||||||
|
|
||||||
|
# Markdown page example
|
||||||
|
|
||||||
|
You don't need React to write simple standalone pages.
|
0
dev-docs/static/.nojekyll
Normal file
0
dev-docs/static/.nojekyll
Normal file
BIN
dev-docs/static/img/docusaurus.png
Normal file
BIN
dev-docs/static/img/docusaurus.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
BIN
dev-docs/static/img/favicon.ico
Normal file
BIN
dev-docs/static/img/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
4
dev-docs/static/img/logo.svg
Normal file
4
dev-docs/static/img/logo.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg viewBox="0 0 80 180" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2">
|
||||||
|
<path d="M22.197 150.382c-4.179-3.359-10.618-9.051-15.702-13.946l-4.01-3.813.734-5.009c.396-2.732 1.13-8.083 1.582-11.839.508-3.757 1.017-7.286 1.186-7.798.226-.683 0-1.025-.621-1.025-1.073 0-1.13.285 1.807-9.107a617.602 617.602 0 0 1 2.203-7.229c.113-.398.565-.569 1.073-.398.508.227.791.683.621 1.081-.169.455.113.911.565 1.082.621.227.565.683-.395 2.333-1.525 2.562-5.422 24.419-5.648 31.477-.17 5.009-.17 5.066 1.92 7.912 2.033 2.789 6.721 7.001 13.951 12.351 2.033 1.537 4.067 3.245 4.631 3.814.848 1.024 1.243.74 8.36-6.887 4.123-4.383 8.698-8.88 10.166-10.018l2.711-2.049-2.089-4.44c-1.13-2.391-5.705-11.612-10.223-20.377-9.433-18.442-7.513-16.678-18.47-16.849l-7.117-.056-2.372-2.733c-2.485-2.903-2.824-3.984-1.638-5.805.452-.627.791-1.651.791-2.277 0-1.025.395-1.196 2.655-1.309 1.412-.057 2.711-.228 2.88-.399.17-.171.396-3.7.565-7.855l.226-7.513-3.784-8.197C2.485 39.844 0 33.583 0 31.533c0-1.081.226-1.992.452-1.992.565 0 .565.057 23.553 48.382 10.675 22.426 20.785 43.544 22.479 47.016 1.695 3.472 3.22 6.659 3.333 7.115.113.512-3.785 4.439-9.998 9.961-5.591 5.008-10.505 9.562-10.957 10.074-1.299 1.594-3.219 1.082-6.665-1.707Zm1.921-65.458c-2.599-5.066-2.712-5.123-9.828-5.464-6.27-.342-6.383-.285-6.383.911 0 .683-.226 1.593-.508 2.049-.339.512-.113 1.423.678 2.675l1.242 1.935h5.649c3.106.057 6.664.285 7.907.512 1.243.228 2.316.342 2.429.285.113-.057-.452-1.366-1.186-2.903Zm-4.745-9.107c-.452-1.195-1.638-3.7-2.598-5.578-1.581-3.188-1.751-3.301-2.146-1.992-.226.797-.396 3.13-.452 5.236-.057 4.155-.17 4.098 4.575 4.383l1.525.057-.904-2.106Z" style="fill-rule:nonzero;stroke:#000;stroke-width:2px" transform="matrix(1.01351 0 0 -1 9.088 166.517)" />
|
||||||
|
<path d="M23.892 136.835c-1.017-.74-1.299-1.48-1.299-3.358 0-2.22.169-2.562 1.694-3.188 1.525-.626 1.92-.569 3.671.626 2.316 1.594 2.373 1.992.678 4.554-1.468 2.22-2.937 2.618-4.744 1.366Zm3.219-2.049c.904-1.594.339-2.789-1.355-2.789-1.525 0-2.203 1.536-1.356 3.073.678 1.253 1.977 1.139 2.711-.284ZM59.306 124.028c0-.285-.339-.569-.735-.569-.339 0-1.299-1.594-2.033-3.529-2.259-5.92-24.852-50.943-24.908-49.52 0 .74-.339 1.252-.904 1.252-.791 0-.904-.456-.565-2.675.339-2.562.113-3.131-7.907-18.841-4.519-8.936-9.376-18.271-10.788-20.775-1.469-2.619-2.598-5.465-2.711-6.66-.17-2.049.056-2.334 4.97-6.603 2.824-2.504 6.439-5.635 8.02-7.058C28.862 2.504 32.194-.114 33.098.057c1.356.228 22.31 22.369 22.367 23.622 0 .569-1.017 9.221-2.259 19.238-2.147 17.076-4.18 37.055-3.954 38.99.169 1.196-.678 7.229-1.299 9.847-.509 2.05-.283 2.903 3.784 12.238 2.372 5.521 5.479 12.295 6.834 15.027 1.299 2.732 2.429 5.123 2.429 5.294 0 .17-.395.284-.847.284-.452 0-.847-.228-.847-.569ZM46.315 81.509c.621-3.984 1.864-13.547 2.767-21.231 1.751-14.116 3.785-29.769 4.349-33.753.339-1.993.113-2.391-3.558-6.489-6.382-7.229-13.16-14.344-15.476-16.165l-2.146-1.708-11.014 10.359C11.07 21.971 10.223 22.939 10.844 24.077c.339.626 3.22 5.92 6.383 11.725 3.163 5.806 7.342 13.547 9.263 17.19 1.977 3.7 3.784 6.887 4.123 7.058.395.228.508-5.521.395-17.759-.226-18.271-.169-18.328 1.638-17.929.226 0 .396 9.221.396 20.434v20.377l5.93 11.953c3.276 6.603 5.987 11.896 6.1 11.84.113-.058.678-3.416 1.243-7.457Z" style="fill-rule:nonzero;stroke:#000;stroke-width:2px" transform="matrix(1.01351 0 0 -1 9.088 166.517)" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
1
dev-docs/static/img/undraw_add_files.svg
Normal file
1
dev-docs/static/img/undraw_add_files.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.7 KiB |
1
dev-docs/static/img/undraw_blank_canvas.svg
Normal file
1
dev-docs/static/img/undraw_blank_canvas.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 12 KiB |
1
dev-docs/static/img/undraw_innovative.svg
Normal file
1
dev-docs/static/img/undraw_innovative.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.4 KiB |
7
dev-docs/tsconfig.json
Normal file
7
dev-docs/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
// This file is not used in compilation. It is here just for a nice editor experience.
|
||||||
|
"extends": "@tsconfig/docusaurus/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "."
|
||||||
|
}
|
||||||
|
}
|
7489
dev-docs/yarn.lock
Normal file
7489
dev-docs/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@@ -23,32 +23,33 @@
|
|||||||
"@sentry/integrations": "6.2.5",
|
"@sentry/integrations": "6.2.5",
|
||||||
"@testing-library/jest-dom": "5.16.2",
|
"@testing-library/jest-dom": "5.16.2",
|
||||||
"@testing-library/react": "12.1.5",
|
"@testing-library/react": "12.1.5",
|
||||||
"@tldraw/vec": "1.4.3",
|
"@tldraw/vec": "1.7.1",
|
||||||
"@types/jest": "27.4.0",
|
"@types/jest": "27.4.0",
|
||||||
"@types/pica": "5.1.3",
|
"@types/pica": "5.1.3",
|
||||||
"@types/react": "17.0.39",
|
"@types/react": "18.0.15",
|
||||||
"@types/react-dom": "17.0.11",
|
"@types/react-dom": "18.0.6",
|
||||||
"@types/socket.io-client": "1.4.36",
|
"@types/socket.io-client": "1.4.36",
|
||||||
"browser-fs-access": "0.29.1",
|
"browser-fs-access": "0.29.1",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"fake-indexeddb": "3.1.7",
|
"fake-indexeddb": "3.1.7",
|
||||||
"firebase": "8.3.3",
|
"firebase": "8.3.3",
|
||||||
"i18next-browser-languagedetector": "6.1.2",
|
"i18next-browser-languagedetector": "6.1.4",
|
||||||
"idb-keyval": "6.0.3",
|
"idb-keyval": "6.0.3",
|
||||||
"image-blob-reduce": "3.0.1",
|
"image-blob-reduce": "4.1.0",
|
||||||
"jotai": "1.6.4",
|
"jotai": "1.6.4",
|
||||||
"lodash.throttle": "4.1.1",
|
"lodash.throttle": "4.1.1",
|
||||||
"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.0.16",
|
"perfect-freehand": "1.2.0",
|
||||||
|
"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": "17.0.2",
|
"react": "18.2.0",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "18.2.0",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"roughjs": "4.5.2",
|
"roughjs": "4.5.2",
|
||||||
"sass": "1.51.0",
|
"sass": "1.51.0",
|
||||||
@@ -59,11 +60,11 @@
|
|||||||
"@excalidraw/eslint-config": "1.0.0",
|
"@excalidraw/eslint-config": "1.0.0",
|
||||||
"@excalidraw/prettier-config": "1.0.2",
|
"@excalidraw/prettier-config": "1.0.2",
|
||||||
"@types/chai": "4.3.0",
|
"@types/chai": "4.3.0",
|
||||||
"@types/lodash.throttle": "4.1.6",
|
"@types/lodash.throttle": "4.1.7",
|
||||||
"@types/pako": "1.0.3",
|
"@types/pako": "1.0.3",
|
||||||
"@types/resize-observer-browser": "0.1.6",
|
"@types/resize-observer-browser": "0.1.7",
|
||||||
"chai": "4.3.6",
|
"chai": "4.3.6",
|
||||||
"dotenv": "10.0.0",
|
"dotenv": "16.0.1",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
"eslint-plugin-prettier": "3.3.1",
|
"eslint-plugin-prettier": "3.3.1",
|
||||||
"husky": "7.0.4",
|
"husky": "7.0.4",
|
||||||
@@ -71,10 +72,7 @@
|
|||||||
"lint-staged": "12.3.7",
|
"lint-staged": "12.3.7",
|
||||||
"pepjs": "0.5.3",
|
"pepjs": "0.5.3",
|
||||||
"prettier": "2.6.2",
|
"prettier": "2.6.2",
|
||||||
"rewire": "5.0.0"
|
"rewire": "6.0.0"
|
||||||
},
|
|
||||||
"resolutions": {
|
|
||||||
"@typescript-eslint/typescript-estree": "5.10.2"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -94,7 +92,8 @@
|
|||||||
"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": "yarn build:app && yarn build:version",
|
"build:prebuild": "node ./scripts/prebuild.js",
|
||||||
|
"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",
|
||||||
@@ -112,6 +111,8 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -52,6 +52,25 @@
|
|||||||
content="Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
content="Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!------------------------------------------------------------------------->
|
||||||
|
<!-- to minimize white flash on load when user has dark mode enabled -->
|
||||||
|
<script>
|
||||||
|
try {
|
||||||
|
//
|
||||||
|
const theme = window.localStorage.getItem("excalidraw-theme");
|
||||||
|
if (theme === "dark") {
|
||||||
|
document.documentElement.classList.add("dark");
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
html.dark {
|
||||||
|
background-color: #121212;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!------------------------------------------------------------------------->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Redirect Excalidraw+ users which have auto-redirect enabled.
|
// Redirect Excalidraw+ users which have auto-redirect enabled.
|
||||||
//
|
//
|
||||||
@@ -98,6 +117,22 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<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.
|
||||||
@@ -139,7 +174,7 @@
|
|||||||
width: 1px;
|
width: 1px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
clip: rect(1px, 1px, 1px, 1px);
|
clip: rect(1px, 1px, 1px, 1px);
|
||||||
white-space: nowrap; /* added line */
|
white-space: nowrap;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -17,11 +17,23 @@
|
|||||||
* See https://goo.gl/2aRDsh
|
* See https://goo.gl/2aRDsh
|
||||||
*/
|
*/
|
||||||
|
|
||||||
importScripts("/workbox/workbox-sw.js");
|
// in dev, `process` is undefined because this file is not compiled until build
|
||||||
|
const IS_DEVELOPMENT =
|
||||||
|
typeof process === "undefined" || process.env.NODE_ENV !== "production";
|
||||||
|
|
||||||
workbox.setConfig({
|
if (IS_DEVELOPMENT) {
|
||||||
modulePathPrefix: "/workbox/",
|
importScripts(
|
||||||
});
|
"https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js",
|
||||||
|
);
|
||||||
|
workbox.setConfig({
|
||||||
|
debug: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
importScripts("/workbox/workbox-sw.js");
|
||||||
|
workbox.setConfig({
|
||||||
|
modulePathPrefix: "/workbox/",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
self.addEventListener("message", (event) => {
|
self.addEventListener("message", (event) => {
|
||||||
if (event.data && event.data.type === "SKIP_WAITING") {
|
if (event.data && event.data.type === "SKIP_WAITING") {
|
||||||
@@ -30,14 +42,17 @@ self.addEventListener("message", (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
workbox.core.clientsClaim();
|
workbox.core.clientsClaim();
|
||||||
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
|
|
||||||
|
|
||||||
workbox.routing.registerNavigationRoute(
|
if (!IS_DEVELOPMENT) {
|
||||||
workbox.precaching.getCacheKeyForURL("./index.html"),
|
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
|
||||||
{
|
|
||||||
blacklist: [/^\/_/, /\/[^/?]+\.[^/]+$/],
|
workbox.routing.registerNavigationRoute(
|
||||||
},
|
workbox.precaching.getCacheKeyForURL("./index.html"),
|
||||||
);
|
{
|
||||||
|
blacklist: [/^\/_/, /\/[^/?]+\.[^/]+$/],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Cache relevant font files
|
// Cache relevant font files
|
||||||
workbox.routing.registerRoute(
|
workbox.routing.registerRoute(
|
@@ -5,22 +5,25 @@ 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`);
|
execSync(`yarn --cwd ${excalidrawDir} publish --tag ${tag}`);
|
||||||
console.info("Published 🎉");
|
console.info(`Published ${pkg.name}@${tag}🎉`);
|
||||||
core.setOutput(
|
core.setOutput(
|
||||||
"result",
|
"result",
|
||||||
`**Preview version has been shipped** :rocket:
|
`**Preview version has been shipped** :rocket:
|
||||||
You can use [@excalidraw/excalidraw-preview@${pkg.version}](https://www.npmjs.com/package/@excalidraw/excalidraw-preview/v/${pkg.version}) for testing!`,
|
You can use [@excalidraw/excalidraw@${pkg.version}](https://www.npmjs.com/package/@excalidraw/excalidraw/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:!");
|
||||||
@@ -51,27 +54,19 @@ 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();
|
||||||
});
|
});
|
||||||
|
21
scripts/buildDocs.js
Normal file
21
scripts/buildDocs.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const { exec } = require("child_process");
|
||||||
|
|
||||||
|
// get files changed between prev and head commit
|
||||||
|
exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
|
||||||
|
if (error || stderr) {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const changedFiles = stdout.trim().split("\n");
|
||||||
|
|
||||||
|
const docFiles = changedFiles.filter((file) => {
|
||||||
|
return file.indexOf("docs") >= 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!docFiles.length) {
|
||||||
|
console.info("Skipping building docs as no valid diff found");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
// Exit code 1 to build the docs in ignoredBuildStep
|
||||||
|
process.exit(1);
|
||||||
|
});
|
@@ -15,6 +15,7 @@ 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",
|
||||||
@@ -23,6 +24,7 @@ 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",
|
||||||
@@ -36,6 +38,7 @@ const crowdinMap = {
|
|||||||
"ru-RU": "en-ru",
|
"ru-RU": "en-ru",
|
||||||
"si-LK": "en-silk",
|
"si-LK": "en-silk",
|
||||||
"sk-SK": "en-sk",
|
"sk-SK": "en-sk",
|
||||||
|
"sl-SI": "en-sl",
|
||||||
"sv-SE": "en-sv",
|
"sv-SE": "en-sv",
|
||||||
"ta-IN": "en-ta",
|
"ta-IN": "en-ta",
|
||||||
"tr-TR": "en-tr",
|
"tr-TR": "en-tr",
|
||||||
@@ -47,6 +50,8 @@ const crowdinMap = {
|
|||||||
"lv-LV": "en-lv",
|
"lv-LV": "en-lv",
|
||||||
"cs-CZ": "en-cs",
|
"cs-CZ": "en-cs",
|
||||||
"kk-KZ": "en-kk",
|
"kk-KZ": "en-kk",
|
||||||
|
"vi-vn": "en-vi",
|
||||||
|
"mr-in": "en-mr",
|
||||||
};
|
};
|
||||||
|
|
||||||
const flags = {
|
const flags = {
|
||||||
@@ -62,6 +67,7 @@ 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": "🇭🇺",
|
||||||
@@ -71,6 +77,7 @@ 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": "🇲🇲",
|
||||||
@@ -86,6 +93,7 @@ const flags = {
|
|||||||
"ru-RU": "🇷🇺",
|
"ru-RU": "🇷🇺",
|
||||||
"si-LK": "🇱🇰",
|
"si-LK": "🇱🇰",
|
||||||
"sk-SK": "🇸🇰",
|
"sk-SK": "🇸🇰",
|
||||||
|
"sl-SI": "🇸🇮",
|
||||||
"sv-SE": "🇸🇪",
|
"sv-SE": "🇸🇪",
|
||||||
"ta-IN": "🇮🇳",
|
"ta-IN": "🇮🇳",
|
||||||
"tr-TR": "🇹🇷",
|
"tr-TR": "🇹🇷",
|
||||||
@@ -93,6 +101,9 @@ const flags = {
|
|||||||
"zh-CN": "🇨🇳",
|
"zh-CN": "🇨🇳",
|
||||||
"zh-HK": "🇭🇰",
|
"zh-HK": "🇭🇰",
|
||||||
"zh-TW": "🇹🇼",
|
"zh-TW": "🇹🇼",
|
||||||
|
"eu-ES": "🇪🇦",
|
||||||
|
"vi-VN": "🇻🇳",
|
||||||
|
"mr-IN": "🇮🇳",
|
||||||
};
|
};
|
||||||
|
|
||||||
const languages = {
|
const languages = {
|
||||||
@@ -133,6 +144,7 @@ const languages = {
|
|||||||
"ru-RU": "Русский",
|
"ru-RU": "Русский",
|
||||||
"si-LK": "සිංහල",
|
"si-LK": "සිංහල",
|
||||||
"sk-SK": "Slovenčina",
|
"sk-SK": "Slovenčina",
|
||||||
|
"sl-SI": "Slovenščina",
|
||||||
"sv-SE": "Svenska",
|
"sv-SE": "Svenska",
|
||||||
"ta-IN": "Tamil",
|
"ta-IN": "Tamil",
|
||||||
"tr-TR": "Türkçe",
|
"tr-TR": "Türkçe",
|
||||||
@@ -140,6 +152,8 @@ const languages = {
|
|||||||
"zh-CN": "简体中文",
|
"zh-CN": "简体中文",
|
||||||
"zh-HK": "繁體中文 (香港)",
|
"zh-HK": "繁體中文 (香港)",
|
||||||
"zh-TW": "繁體中文",
|
"zh-TW": "繁體中文",
|
||||||
|
"vi-VN": "Tiếng Việt",
|
||||||
|
"mr-IN": "मराठी",
|
||||||
};
|
};
|
||||||
|
|
||||||
const percentages = fs.readFileSync(
|
const percentages = fs.readFileSync(
|
||||||
|
21
scripts/prebuild.js
Normal file
21
scripts/prebuild.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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();
|
37
scripts/prerelease.js
Normal file
37
scripts/prerelease.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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);
|
@@ -1,39 +1,44 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const util = require("util");
|
const { execSync } = require("child_process");
|
||||||
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 pkg = require(excalidrawPackage);
|
||||||
|
|
||||||
const updatePackageVersion = (nextVersion) => {
|
const originalReadMe = fs.readFileSync(`${excalidrawDir}/README.md`, "utf8");
|
||||||
const pkg = require(excalidrawPackage);
|
|
||||||
pkg.version = nextVersion;
|
const updateReadme = () => {
|
||||||
const content = `${JSON.stringify(pkg, null, 2)}\n`;
|
const excalidrawIndex = originalReadMe.indexOf("### Excalidraw");
|
||||||
fs.writeFileSync(excalidrawPackage, content, "utf-8");
|
|
||||||
|
// remove note for stable readme
|
||||||
|
const data = originalReadMe.slice(excalidrawIndex);
|
||||||
|
|
||||||
|
// update readme
|
||||||
|
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
|
||||||
};
|
};
|
||||||
|
|
||||||
const release = async (nextVersion) => {
|
const publish = () => {
|
||||||
try {
|
try {
|
||||||
updateReadme();
|
execSync(`yarn --frozen-lockfile`);
|
||||||
await updateChangelog(nextVersion);
|
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
|
||||||
updatePackageVersion(nextVersion);
|
execSync(`yarn run build:umd`, { cwd: excalidrawDir });
|
||||||
await exec(`git add -u`);
|
execSync(`yarn --cwd ${excalidrawDir} publish`);
|
||||||
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 nextVersion = process.argv.slice(2)[0];
|
const release = () => {
|
||||||
if (!nextVersion) {
|
updateReadme();
|
||||||
console.error("Pass the next version to release!");
|
console.info("Note for stable readme removed");
|
||||||
process.exit(1);
|
|
||||||
}
|
publish();
|
||||||
release(nextVersion);
|
console.info(`Published ${pkg.version}!`);
|
||||||
|
|
||||||
|
// revert readme after release
|
||||||
|
fs.writeFileSync(`${excalidrawDir}/README.md`, originalReadMe, "utf8");
|
||||||
|
console.info("Readme reverted");
|
||||||
|
};
|
||||||
|
|
||||||
|
release();
|
||||||
|
@@ -1,27 +0,0 @@
|
|||||||
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;
|
|
@@ -42,7 +42,7 @@ export const actionAddToLibrary = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
toastMessage: t("toast.addedToLibrary"),
|
toast: { message: t("toast.addedToLibrary") },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
@@ -2,7 +2,7 @@ import { ColorPicker } from "../components/ColorPicker";
|
|||||||
import { eraser, zoomIn, zoomOut } from "../components/icons";
|
import { eraser, zoomIn, zoomOut } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { DarkModeToggle } from "../components/DarkModeToggle";
|
import { DarkModeToggle } from "../components/DarkModeToggle";
|
||||||
import { THEME, ZOOM_STEP } from "../constants";
|
import { MIN_ZOOM, THEME, ZOOM_STEP } from "../constants";
|
||||||
import { getCommonBounds, getNonDeletedElements } from "../element";
|
import { getCommonBounds, getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@@ -206,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, ZOOM_STEP),
|
Math.max(zoomAdjustedToSteps, MIN_ZOOM),
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
return clampedZoomValueToFitElements as NormalizedZoomValue;
|
return clampedZoomValueToFitElements as NormalizedZoomValue;
|
||||||
|
@@ -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.code === CODES.X,
|
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.X,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionCopyAsSvg = register({
|
export const actionCopyAsSvg = register({
|
||||||
@@ -107,14 +107,16 @@ export const actionCopyAsPng = register({
|
|||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
toastMessage: t("toast.copyToClipboardAsPng", {
|
toast: {
|
||||||
exportSelection: selectedElements.length
|
message: t("toast.copyToClipboardAsPng", {
|
||||||
? t("toast.selection")
|
exportSelection: selectedElements.length
|
||||||
: t("toast.canvas"),
|
? t("toast.selection")
|
||||||
exportColorScheme: appState.exportWithDarkMode
|
: t("toast.canvas"),
|
||||||
? t("buttons.darkMode")
|
exportColorScheme: appState.exportWithDarkMode
|
||||||
: t("buttons.lightMode"),
|
? t("buttons.darkMode")
|
||||||
}),
|
: t("buttons.lightMode"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
|
@@ -128,12 +128,15 @@ const duplicateElements = (
|
|||||||
{
|
{
|
||||||
...appState,
|
...appState,
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
selectedElementIds: newElements.reduce((acc, element) => {
|
selectedElementIds: newElements.reduce(
|
||||||
if (!isBoundToContainer(element)) {
|
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||||
acc[element.id] = true;
|
if (!isBoundToContainer(element)) {
|
||||||
}
|
acc[element.id] = true;
|
||||||
return acc;
|
}
|
||||||
}, {} as any),
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getNonDeletedElements(finalElements),
|
getNonDeletedElements(finalElements),
|
||||||
),
|
),
|
||||||
|
@@ -144,13 +144,15 @@ export const actionSaveToActiveFile = register({
|
|||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
fileHandle,
|
fileHandle,
|
||||||
toastMessage: fileHandleExists
|
toast: fileHandleExists
|
||||||
? fileHandle?.name
|
? {
|
||||||
? t("toast.fileSavedToFilename").replace(
|
message: fileHandle?.name
|
||||||
"{filename}",
|
? t("toast.fileSavedToFilename").replace(
|
||||||
`"${fileHandle.name}"`,
|
"{filename}",
|
||||||
)
|
`"${fileHandle.name}"`,
|
||||||
: t("toast.fileSaved")
|
)
|
||||||
|
: t("toast.fileSaved"),
|
||||||
|
}
|
||||||
: null,
|
: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -242,7 +244,7 @@ export const actionLoadScene = register({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
|
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
|
||||||
PanelComponent: ({ updateData, appState }) => (
|
PanelComponent: ({ updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={load}
|
icon={load}
|
||||||
|
@@ -13,7 +13,7 @@ import {
|
|||||||
maybeBindLinearElement,
|
maybeBindLinearElement,
|
||||||
bindOrUnbindLinearElement,
|
bindOrUnbindLinearElement,
|
||||||
} from "../element/binding";
|
} from "../element/binding";
|
||||||
import { isBindingElement } from "../element/typeChecks";
|
import { isBindingElement, isLinearElement } from "../element/typeChecks";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
|
|
||||||
export const actionFinalize = register({
|
export const actionFinalize = register({
|
||||||
@@ -181,6 +181,11 @@ export const actionFinalize = register({
|
|||||||
[multiPointElement.id]: true,
|
[multiPointElement.id]: true,
|
||||||
}
|
}
|
||||||
: appState.selectedElementIds,
|
: appState.selectedElementIds,
|
||||||
|
// To select the linear element when user has finished mutipoint editing
|
||||||
|
selectedLinearElement:
|
||||||
|
multiPointElement && isLinearElement(multiPointElement)
|
||||||
|
? new LinearElementEditor(multiPointElement, scene)
|
||||||
|
: appState.selectedLinearElement,
|
||||||
pendingImageElementId: null,
|
pendingImageElementId: null,
|
||||||
},
|
},
|
||||||
commitToHistory: appState.activeTool.type === "freedraw",
|
commitToHistory: appState.activeTool.type === "freedraw",
|
||||||
|
@@ -6,10 +6,14 @@ 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 { LinearElementEditor } from "../element/linearElementEditor";
|
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
|
import {
|
||||||
|
getElementAbsoluteCoords,
|
||||||
|
getElementPointsCoords,
|
||||||
|
} from "../element/bounds";
|
||||||
|
import { isLinearElement } from "../element/typeChecks";
|
||||||
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
|
||||||
const enableActionFlipHorizontal = (
|
const enableActionFlipHorizontal = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
@@ -118,13 +122,6 @@ 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),
|
||||||
@@ -132,7 +129,6 @@ 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
|
||||||
@@ -146,30 +142,51 @@ const flipElement = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let finalOffsetX = 0;
|
||||||
|
if (isLinearElement(element) && element.points.length < 3) {
|
||||||
|
finalOffsetX =
|
||||||
|
element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
|
||||||
|
element.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
let initialPointsCoords;
|
||||||
if (isLinearElement(element)) {
|
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 {
|
||||||
// calculate new x-coord for transformation
|
const elWidth = initialPointsCoords
|
||||||
newNCoordsX = usingNWHandle ? element.x + 2 * width : element.x - 2 * width;
|
? initialPointsCoords[2] - initialPointsCoords[0]
|
||||||
|
: 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),
|
||||||
true,
|
false,
|
||||||
element,
|
element,
|
||||||
usingNWHandle ? "nw" : "ne",
|
usingNWHandle ? "nw" : "ne",
|
||||||
false,
|
true,
|
||||||
newNCoordsX,
|
usingNWHandle ? startPoint[0] + elWidth : startPoint[0] - elWidth,
|
||||||
nHandle[1],
|
startPoint[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)
|
||||||
@@ -186,9 +203,34 @@ 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) => {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { CODES, KEYS } from "../keys";
|
import { 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.code === CODES.G,
|
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
@@ -189,7 +189,9 @@ export const actionUngroup = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.code === CODES.G,
|
event.shiftKey &&
|
||||||
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
|
event.key === KEYS.G.toUpperCase(),
|
||||||
contextItemLabel: "labels.ungroup",
|
contextItemLabel: "labels.ungroup",
|
||||||
contextItemPredicate: (elements, appState) =>
|
contextItemPredicate: (elements, appState) =>
|
||||||
getSelectedGroupIds(appState).length > 0,
|
getSelectedGroupIds(appState).length > 0,
|
||||||
|
49
src/actions/actionLinearEditor.ts
Normal file
49
src/actions/actionLinearEditor.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { getNonDeletedElements } from "../element";
|
||||||
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
import { isLinearElement } from "../element/typeChecks";
|
||||||
|
import { ExcalidrawLinearElement } from "../element/types";
|
||||||
|
import { getSelectedElements } from "../scene";
|
||||||
|
import { register } from "./register";
|
||||||
|
|
||||||
|
export const actionToggleLinearEditor = register({
|
||||||
|
name: "toggleLinearEditor",
|
||||||
|
trackEvent: {
|
||||||
|
category: "element",
|
||||||
|
},
|
||||||
|
contextItemPredicate: (elements, appState) => {
|
||||||
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
perform(elements, appState, _, app) {
|
||||||
|
const selectedElement = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
true,
|
||||||
|
)[0] as ExcalidrawLinearElement;
|
||||||
|
|
||||||
|
const editingLinearElement =
|
||||||
|
appState.editingLinearElement?.elementId === selectedElement.id
|
||||||
|
? null
|
||||||
|
: new LinearElementEditor(selectedElement, app.scene);
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
editingLinearElement,
|
||||||
|
},
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
contextItemLabel: (elements, appState) => {
|
||||||
|
const selectedElement = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
true,
|
||||||
|
)[0] as ExcalidrawLinearElement;
|
||||||
|
return appState.editingLinearElement?.elementId === selectedElement.id
|
||||||
|
? "labels.lineEditor.exit"
|
||||||
|
: "labels.lineEditor.edit";
|
||||||
|
},
|
||||||
|
});
|
@@ -4,7 +4,7 @@ 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 { CODES, KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { HelpIcon } from "../components/HelpIcon";
|
import { HelpIcon } from "../components/HelpIcon";
|
||||||
|
|
||||||
export const actionToggleCanvasMenu = register({
|
export const actionToggleCanvasMenu = register({
|
||||||
@@ -67,7 +67,7 @@ export const actionFullScreen = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
keyTest: (event) => event.code === CODES.F && !event[KEYS.CTRL_OR_CMD],
|
keyTest: (event) => event.key === KEYS.F && !event[KEYS.CTRL_OR_CMD],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionShortcuts = register({
|
export const actionShortcuts = register({
|
||||||
|
@@ -2,29 +2,43 @@ import { KEYS } from "../keys";
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { selectGroupsForSelectedElements } from "../groups";
|
import { selectGroupsForSelectedElements } from "../groups";
|
||||||
import { getNonDeletedElements, isTextElement } from "../element";
|
import { getNonDeletedElements, isTextElement } from "../element";
|
||||||
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
import { isLinearElement } from "../element/typeChecks";
|
||||||
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
|
||||||
export const actionSelectAll = register({
|
export const actionSelectAll = register({
|
||||||
name: "selectAll",
|
name: "selectAll",
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, value, app) => {
|
||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const selectedElementIds = elements.reduce(
|
||||||
|
(map: Record<ExcalidrawElement["id"], true>, element) => {
|
||||||
|
if (
|
||||||
|
!element.isDeleted &&
|
||||||
|
!(isTextElement(element) && element.containerId) &&
|
||||||
|
!element.locked
|
||||||
|
) {
|
||||||
|
map[element.id] = true;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: selectGroupsForSelectedElements(
|
appState: selectGroupsForSelectedElements(
|
||||||
{
|
{
|
||||||
...appState,
|
...appState,
|
||||||
|
selectedLinearElement:
|
||||||
|
// single linear element selected
|
||||||
|
Object.keys(selectedElementIds).length === 1 &&
|
||||||
|
isLinearElement(elements[0])
|
||||||
|
? new LinearElementEditor(elements[0], app.scene)
|
||||||
|
: null,
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
selectedElementIds: elements.reduce((map, element) => {
|
selectedElementIds,
|
||||||
if (
|
|
||||||
!element.isDeleted &&
|
|
||||||
!(isTextElement(element) && element.containerId) &&
|
|
||||||
element.locked === false
|
|
||||||
) {
|
|
||||||
map[element.id] = true;
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}, {} as any),
|
|
||||||
},
|
},
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
),
|
),
|
||||||
|
@@ -36,7 +36,7 @@ export const actionCopyStyles = register({
|
|||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
toastMessage: t("toast.copyStyles"),
|
toast: { message: t("toast.copyStyles") },
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
|
@@ -17,16 +17,19 @@ export const actionToggleLock = register({
|
|||||||
|
|
||||||
const operation = getOperation(selectedElements);
|
const operation = getOperation(selectedElements);
|
||||||
const selectedElementsMap = arrayToMap(selectedElements);
|
const selectedElementsMap = arrayToMap(selectedElements);
|
||||||
|
const lock = operation === "lock";
|
||||||
return {
|
return {
|
||||||
elements: elements.map((element) => {
|
elements: elements.map((element) => {
|
||||||
if (!selectedElementsMap.has(element.id)) {
|
if (!selectedElementsMap.has(element.id)) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
return newElementWith(element, { locked: operation === "lock" });
|
return newElementWith(element, { locked: lock });
|
||||||
}),
|
}),
|
||||||
appState,
|
appState: {
|
||||||
|
...appState,
|
||||||
|
selectedLinearElement: lock ? null : appState.selectedLinearElement,
|
||||||
|
},
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@@ -85,3 +85,4 @@ 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";
|
||||||
|
@@ -137,7 +137,6 @@ export class ActionManager {
|
|||||||
*/
|
*/
|
||||||
renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => {
|
renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => {
|
||||||
const canvasActions = this.app.props.UIOptions.canvasActions;
|
const canvasActions = this.app.props.UIOptions.canvasActions;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.actions[name] &&
|
this.actions[name] &&
|
||||||
"PanelComponent" in this.actions[name] &&
|
"PanelComponent" in this.actions[name] &&
|
||||||
@@ -147,6 +146,7 @@ export class ActionManager {
|
|||||||
) {
|
) {
|
||||||
const action = this.actions[name];
|
const action = this.actions[name];
|
||||||
const PanelComponent = action.PanelComponent!;
|
const PanelComponent = action.PanelComponent!;
|
||||||
|
PanelComponent.displayName = "PanelComponent";
|
||||||
const elements = this.getElementsIncludingDeleted();
|
const elements = this.getElementsIncludingDeleted();
|
||||||
const appState = this.getAppState();
|
const appState = this.getAppState();
|
||||||
const updateData = (formState?: any) => {
|
const updateData = (formState?: any) => {
|
||||||
|
@@ -111,7 +111,8 @@ export type ActionName =
|
|||||||
| "hyperlink"
|
| "hyperlink"
|
||||||
| "eraser"
|
| "eraser"
|
||||||
| "bindText"
|
| "bindText"
|
||||||
| "toggleLock";
|
| "toggleLock"
|
||||||
|
| "toggleLinearEditor";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
|
296
src/appState.ts
296
src/appState.ts
@@ -57,8 +57,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
fileHandle: null,
|
fileHandle: null,
|
||||||
gridSize: null,
|
gridSize: null,
|
||||||
isBindingEnabled: true,
|
isBindingEnabled: true,
|
||||||
isLibraryOpen: false,
|
isSidebarDocked: false,
|
||||||
isLibraryMenuDocked: false,
|
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isResizing: false,
|
isResizing: false,
|
||||||
isRotating: false,
|
isRotating: false,
|
||||||
@@ -67,6 +66,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
name: `${t("labels.untitled")}-${getDateTime()}`,
|
name: `${t("labels.untitled")}-${getDateTime()}`,
|
||||||
openMenu: null,
|
openMenu: null,
|
||||||
openPopup: null,
|
openPopup: null,
|
||||||
|
openSidebar: null,
|
||||||
pasteDialog: { shown: false, data: null },
|
pasteDialog: { shown: false, data: null },
|
||||||
previousSelectedElementIds: {},
|
previousSelectedElementIds: {},
|
||||||
resizingElement: null,
|
resizingElement: null,
|
||||||
@@ -81,7 +81,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
showStats: false,
|
showStats: false,
|
||||||
startBoundElement: null,
|
startBoundElement: null,
|
||||||
suggestedBindings: [],
|
suggestedBindings: [],
|
||||||
toastMessage: null,
|
toast: null,
|
||||||
viewBackgroundColor: oc.white,
|
viewBackgroundColor: oc.white,
|
||||||
zenModeEnabled: false,
|
zenModeEnabled: false,
|
||||||
zoom: {
|
zoom: {
|
||||||
@@ -90,6 +90,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
viewModeEnabled: false,
|
viewModeEnabled: false,
|
||||||
pendingImageElementId: null,
|
pendingImageElementId: null,
|
||||||
showHyperlinkPopup: false,
|
showHyperlinkPopup: false,
|
||||||
|
selectedLinearElement: null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -101,228 +102,91 @@ 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 a text file */
|
/** whether to keep when exporting to file/database */
|
||||||
text: boolean;
|
export: 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)({
|
||||||
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 },
|
||||||
browser: true,
|
currentItemEndArrowhead: { browser: true, export: false, server: false },
|
||||||
text: false,
|
currentItemFillStyle: { browser: true, export: false, server: false },
|
||||||
image: false,
|
currentItemFontFamily: { browser: true, export: false, server: false },
|
||||||
server: false,
|
currentItemFontSize: { browser: true, export: 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: {
|
currentItemLinearStrokeSharpness: {
|
||||||
browser: true,
|
browser: true,
|
||||||
text: false,
|
export: 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 },
|
||||||
|
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 },
|
||||||
|
showHelpDialog: { browser: false, export: false, server: false },
|
||||||
|
showStats: { browser: true, export: false, server: false },
|
||||||
|
startBoundElement: { browser: false, export: false, server: false },
|
||||||
|
suggestedBindings: { browser: false, export: false, server: false },
|
||||||
|
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 "image" | "text" | "browser" | "server",
|
ExportType extends "export" | "browser" | "server",
|
||||||
>(
|
>(
|
||||||
appState: Partial<AppState>,
|
appState: Partial<AppState>,
|
||||||
exportType: ExportType,
|
exportType: ExportType,
|
||||||
@@ -349,12 +213,8 @@ export const clearAppStateForLocalStorage = (appState: Partial<AppState>) => {
|
|||||||
return _clearAppStateForStorage(appState, "browser");
|
return _clearAppStateForStorage(appState, "browser");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanAppStateForTextExport = (appState: Partial<AppState>) => {
|
export const cleanAppStateForExport = (appState: Partial<AppState>) => {
|
||||||
return _clearAppStateForStorage(appState, "text");
|
return _clearAppStateForStorage(appState, "export");
|
||||||
};
|
|
||||||
|
|
||||||
export const cleanAppStateForImageExport = (appState: Partial<AppState>) => {
|
|
||||||
return _clearAppStateForStorage(appState, "image");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clearAppStateForDatabase = (appState: Partial<AppState>) => {
|
export const clearAppStateForDatabase = (appState: Partial<AppState>) => {
|
||||||
|
@@ -26,17 +26,17 @@ import { ToolButton } from "./ToolButton";
|
|||||||
import { hasStrokeColor } from "../scene/comparisons";
|
import { hasStrokeColor } from "../scene/comparisons";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks";
|
import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { actionToggleZenMode } from "../actions";
|
||||||
|
|
||||||
export const SelectedShapeActions = ({
|
export const SelectedShapeActions = ({
|
||||||
appState,
|
appState,
|
||||||
elements,
|
elements,
|
||||||
renderAction,
|
renderAction,
|
||||||
activeTool,
|
|
||||||
}: {
|
}: {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
renderAction: ActionManager["renderAction"];
|
renderAction: ActionManager["renderAction"];
|
||||||
activeTool: AppState["activeTool"]["type"];
|
|
||||||
}) => {
|
}) => {
|
||||||
const targetElements = getTargetElements(
|
const targetElements = getTargetElements(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
@@ -56,13 +56,13 @@ export const SelectedShapeActions = ({
|
|||||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||||
|
|
||||||
const showFillIcons =
|
const showFillIcons =
|
||||||
hasBackground(activeTool) ||
|
hasBackground(appState.activeTool.type) ||
|
||||||
targetElements.some(
|
targetElements.some(
|
||||||
(element) =>
|
(element) =>
|
||||||
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
||||||
);
|
);
|
||||||
const showChangeBackgroundIcons =
|
const showChangeBackgroundIcons =
|
||||||
hasBackground(activeTool) ||
|
hasBackground(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => hasBackground(element.type));
|
targetElements.some((element) => hasBackground(element.type));
|
||||||
|
|
||||||
const showLinkIcon =
|
const showLinkIcon =
|
||||||
@@ -79,23 +79,23 @@ export const SelectedShapeActions = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panelColumn">
|
<div className="panelColumn">
|
||||||
{((hasStrokeColor(activeTool) &&
|
{((hasStrokeColor(appState.activeTool.type) &&
|
||||||
activeTool !== "image" &&
|
appState.activeTool.type !== "image" &&
|
||||||
commonSelectedType !== "image") ||
|
commonSelectedType !== "image") ||
|
||||||
targetElements.some((element) => hasStrokeColor(element.type))) &&
|
targetElements.some((element) => hasStrokeColor(element.type))) &&
|
||||||
renderAction("changeStrokeColor")}
|
renderAction("changeStrokeColor")}
|
||||||
{showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
|
{showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
|
||||||
{showFillIcons && renderAction("changeFillStyle")}
|
{showFillIcons && renderAction("changeFillStyle")}
|
||||||
|
|
||||||
{(hasStrokeWidth(activeTool) ||
|
{(hasStrokeWidth(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => hasStrokeWidth(element.type))) &&
|
targetElements.some((element) => hasStrokeWidth(element.type))) &&
|
||||||
renderAction("changeStrokeWidth")}
|
renderAction("changeStrokeWidth")}
|
||||||
|
|
||||||
{(activeTool === "freedraw" ||
|
{(appState.activeTool.type === "freedraw" ||
|
||||||
targetElements.some((element) => element.type === "freedraw")) &&
|
targetElements.some((element) => element.type === "freedraw")) &&
|
||||||
renderAction("changeStrokeShape")}
|
renderAction("changeStrokeShape")}
|
||||||
|
|
||||||
{(hasStrokeStyle(activeTool) ||
|
{(hasStrokeStyle(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => hasStrokeStyle(element.type))) && (
|
targetElements.some((element) => hasStrokeStyle(element.type))) && (
|
||||||
<>
|
<>
|
||||||
{renderAction("changeStrokeStyle")}
|
{renderAction("changeStrokeStyle")}
|
||||||
@@ -103,12 +103,12 @@ export const SelectedShapeActions = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(canChangeSharpness(activeTool) ||
|
{(canChangeSharpness(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => canChangeSharpness(element.type))) && (
|
targetElements.some((element) => canChangeSharpness(element.type))) && (
|
||||||
<>{renderAction("changeSharpness")}</>
|
<>{renderAction("changeSharpness")}</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(hasText(activeTool) ||
|
{(hasText(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => hasText(element.type))) && (
|
targetElements.some((element) => hasText(element.type))) && (
|
||||||
<>
|
<>
|
||||||
{renderAction("changeFontSize")}
|
{renderAction("changeFontSize")}
|
||||||
@@ -123,7 +123,7 @@ export const SelectedShapeActions = ({
|
|||||||
(element) =>
|
(element) =>
|
||||||
hasBoundTextElement(element) || isBoundToContainer(element),
|
hasBoundTextElement(element) || isBoundToContainer(element),
|
||||||
) && renderAction("changeVerticalAlign")}
|
) && renderAction("changeVerticalAlign")}
|
||||||
{(canHaveArrowheads(activeTool) ||
|
{(canHaveArrowheads(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => canHaveArrowheads(element.type))) && (
|
targetElements.some((element) => canHaveArrowheads(element.type))) && (
|
||||||
<>{renderAction("changeArrowhead")}</>
|
<>{renderAction("changeArrowhead")}</>
|
||||||
)}
|
)}
|
||||||
@@ -271,3 +271,45 @@ export const ZoomActions = ({
|
|||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const UndoRedoActions = ({
|
||||||
|
renderAction,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
renderAction: ActionManager["renderAction"];
|
||||||
|
className?: string;
|
||||||
|
}) => (
|
||||||
|
<div className={`undo-redo-buttons ${className}`}>
|
||||||
|
{renderAction("undo", { size: "small" })}
|
||||||
|
{renderAction("redo", { size: "small" })}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ExitZenModeAction = ({
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,12 @@
|
|||||||
import React from "react";
|
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { AppState } from "../types";
|
|
||||||
|
|
||||||
export const BackgroundPickerAndDarkModeToggle = ({
|
export const BackgroundPickerAndDarkModeToggle = ({
|
||||||
appState,
|
|
||||||
setAppState,
|
|
||||||
actionManager,
|
actionManager,
|
||||||
showThemeBtn,
|
|
||||||
}: {
|
}: {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
appState: AppState;
|
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
|
||||||
showThemeBtn: boolean;
|
|
||||||
}) => (
|
}) => (
|
||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex" }}>
|
||||||
{actionManager.renderAction("changeViewBackgroundColor")}
|
{actionManager.renderAction("changeViewBackgroundColor")}
|
||||||
{showThemeBtn && actionManager.renderAction("toggleTheme")}
|
{actionManager.renderAction("toggleTheme")}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -4,6 +4,7 @@ import "./Card.scss";
|
|||||||
|
|
||||||
export const Card: React.FC<{
|
export const Card: React.FC<{
|
||||||
color: keyof OpenColor | "primary";
|
color: keyof OpenColor | "primary";
|
||||||
|
children?: React.ReactNode;
|
||||||
}> = ({ children, color }) => {
|
}> = ({ children, color }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@@ -8,6 +8,7 @@ export const CheckboxItem: React.FC<{
|
|||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange: (checked: boolean, event: React.MouseEvent) => void;
|
onChange: (checked: boolean, event: React.MouseEvent) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
}> = ({ children, checked, onChange, className }) => {
|
}> = ({ children, checked, onChange, className }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@@ -18,13 +18,15 @@
|
|||||||
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-6;
|
background-color: $oc-green-6;
|
||||||
color: $oc-white;
|
color: $oc-white;
|
||||||
font-size: 0.7em;
|
font-size: 0.6em;
|
||||||
font-family: var(--ui-font);
|
font-family: "Cascadia";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -28,7 +28,7 @@ const CollabButton = ({
|
|||||||
aria-label={t("labels.liveCollaboration")}
|
aria-label={t("labels.liveCollaboration")}
|
||||||
showAriaLabel={useDevice().isMobile}
|
showAriaLabel={useDevice().isMobile}
|
||||||
>
|
>
|
||||||
{collaboratorCount > 0 && (
|
{isCollaborating && (
|
||||||
<div className="CollabButton-collaborators">{collaboratorCount}</div>
|
<div className="CollabButton-collaborators">{collaboratorCount}</div>
|
||||||
)}
|
)}
|
||||||
</ToolButton>
|
</ToolButton>
|
||||||
|
@@ -343,6 +343,8 @@ const ColorInput = React.forwardRef(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColorInput.displayName = "ColorInput";
|
||||||
|
|
||||||
export const ColorPicker = ({
|
export const ColorPicker = ({
|
||||||
type,
|
type,
|
||||||
color,
|
color,
|
||||||
|
@@ -85,6 +85,7 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
<button
|
<button
|
||||||
className="Modal__close"
|
className="Modal__close"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
title={t("buttons.close")}
|
||||||
aria-label={t("buttons.close")}
|
aria-label={t("buttons.close")}
|
||||||
>
|
>
|
||||||
{useDevice().isMobile ? back : close}
|
{useDevice().isMobile ? back : close}
|
||||||
|
106
src/components/Footer.tsx
Normal file
106
src/components/Footer.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { ActionManager } from "../actions/manager";
|
||||||
|
import { AppState, ExcalidrawProps } from "../types";
|
||||||
|
import {
|
||||||
|
ExitZenModeAction,
|
||||||
|
FinalizeAction,
|
||||||
|
UndoRedoActions,
|
||||||
|
ZoomActions,
|
||||||
|
} from "./Actions";
|
||||||
|
import { useDevice } from "./App";
|
||||||
|
import { Island } from "./Island";
|
||||||
|
import { Section } from "./Section";
|
||||||
|
import Stack from "./Stack";
|
||||||
|
|
||||||
|
const Footer = ({
|
||||||
|
appState,
|
||||||
|
actionManager,
|
||||||
|
renderCustomFooter,
|
||||||
|
showExitZenModeBtn,
|
||||||
|
}: {
|
||||||
|
appState: AppState;
|
||||||
|
actionManager: ActionManager;
|
||||||
|
renderCustomFooter?: ExcalidrawProps["renderFooter"];
|
||||||
|
showExitZenModeBtn: boolean;
|
||||||
|
}) => {
|
||||||
|
const device = useDevice();
|
||||||
|
const showFinalize =
|
||||||
|
!appState.viewModeEnabled && appState.multiElement && device.isTouchScreen;
|
||||||
|
return (
|
||||||
|
<footer
|
||||||
|
role="contentinfo"
|
||||||
|
className="layer-ui__wrapper__footer App-menu App-menu_bottom"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={clsx("layer-ui__wrapper__footer-left zen-mode-transition", {
|
||||||
|
"layer-ui__wrapper__footer-left--transition-left":
|
||||||
|
appState.zenModeEnabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Stack.Col gap={2}>
|
||||||
|
<Section heading="canvasActions">
|
||||||
|
<Island padding={1}>
|
||||||
|
<ZoomActions
|
||||||
|
renderAction={actionManager.renderAction}
|
||||||
|
zoom={appState.zoom}
|
||||||
|
/>
|
||||||
|
</Island>
|
||||||
|
{!appState.viewModeEnabled && (
|
||||||
|
<>
|
||||||
|
<UndoRedoActions
|
||||||
|
renderAction={actionManager.renderAction}
|
||||||
|
className={clsx("zen-mode-transition", {
|
||||||
|
"layer-ui__wrapper__footer-left--transition-bottom":
|
||||||
|
appState.zenModeEnabled,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={clsx("eraser-buttons zen-mode-transition", {
|
||||||
|
"layer-ui__wrapper__footer-left--transition-left":
|
||||||
|
appState.zenModeEnabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{actionManager.renderAction("eraser", { size: "small" })}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{showFinalize && (
|
||||||
|
<FinalizeAction
|
||||||
|
renderAction={actionManager.renderAction}
|
||||||
|
className={clsx("zen-mode-transition", {
|
||||||
|
"layer-ui__wrapper__footer-left--transition-left":
|
||||||
|
appState.zenModeEnabled,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Section>
|
||||||
|
</Stack.Col>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"layer-ui__wrapper__footer-center zen-mode-transition",
|
||||||
|
{
|
||||||
|
"layer-ui__wrapper__footer-left--transition-bottom":
|
||||||
|
appState.zenModeEnabled,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{renderCustomFooter?.(false, appState)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={clsx("layer-ui__wrapper__footer-right zen-mode-transition", {
|
||||||
|
"transition-right disable-pointerEvents": appState.zenModeEnabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{actionManager.renderAction("toggleShortcuts")}
|
||||||
|
</div>
|
||||||
|
<ExitZenModeAction
|
||||||
|
actionManager={actionManager}
|
||||||
|
showExitZenModeBtn={showExitZenModeBtn}
|
||||||
|
/>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
@@ -3,7 +3,7 @@ import { NonDeletedExcalidrawElement } from "../element/types";
|
|||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
|
|
||||||
import "./HintViewer.scss";
|
import "./HintViewer.scss";
|
||||||
import { AppState } from "../types";
|
import { AppState, Device } from "../types";
|
||||||
import {
|
import {
|
||||||
isImageElement,
|
isImageElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
@@ -17,13 +17,19 @@ interface HintViewerProps {
|
|||||||
appState: AppState;
|
appState: AppState;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
|
device: Device;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getHints = ({ appState, elements, isMobile }: HintViewerProps) => {
|
const getHints = ({
|
||||||
|
appState,
|
||||||
|
elements,
|
||||||
|
isMobile,
|
||||||
|
device,
|
||||||
|
}: HintViewerProps) => {
|
||||||
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
||||||
const multiMode = appState.multiElement !== null;
|
const multiMode = appState.multiElement !== null;
|
||||||
|
|
||||||
if (appState.isLibraryOpen) {
|
if (appState.openSidebar === "library" && !device.canDeviceFitSidebar) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,11 +117,13 @@ export const HintViewer = ({
|
|||||||
appState,
|
appState,
|
||||||
elements,
|
elements,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
device,
|
||||||
}: HintViewerProps) => {
|
}: HintViewerProps) => {
|
||||||
let hint = getHints({
|
let hint = getHints({
|
||||||
appState,
|
appState,
|
||||||
elements,
|
elements,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
device,
|
||||||
});
|
});
|
||||||
if (!hint) {
|
if (!hint) {
|
||||||
return null;
|
return null;
|
||||||
|
@@ -58,6 +58,7 @@ const ExportButton: React.FC<{
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
title: string;
|
title: string;
|
||||||
shade?: number;
|
shade?: number;
|
||||||
|
children?: React.ReactNode;
|
||||||
}> = ({ children, title, onClick, color, shade = 6 }) => {
|
}> = ({ children, title, onClick, color, shade = 6 }) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -170,7 +171,9 @@ const ImageExportModal = ({
|
|||||||
<Stack.Row gap={2}>
|
<Stack.Row gap={2}>
|
||||||
{actionManager.renderAction("changeExportScale")}
|
{actionManager.renderAction("changeExportScale")}
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
<p style={{ marginLeft: "1em", userSelect: "none" }}>Scale</p>
|
<p style={{ marginLeft: "1em", userSelect: "none" }}>
|
||||||
|
{t("buttons.scale")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@@ -2,10 +2,12 @@ import React, { useEffect, useState } from "react";
|
|||||||
|
|
||||||
import { LoadingMessage } from "./LoadingMessage";
|
import { LoadingMessage } from "./LoadingMessage";
|
||||||
import { defaultLang, Language, languages, setLanguage } from "../i18n";
|
import { defaultLang, Language, languages, setLanguage } from "../i18n";
|
||||||
|
import { Theme } from "../element/types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
langCode: Language["code"];
|
langCode: Language["code"];
|
||||||
children: React.ReactElement;
|
children: React.ReactElement;
|
||||||
|
theme?: Theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InitializeApp = (props: Props) => {
|
export const InitializeApp = (props: Props) => {
|
||||||
@@ -21,5 +23,5 @@ export const InitializeApp = (props: Props) => {
|
|||||||
updateLang();
|
updateLang();
|
||||||
}, [props.langCode]);
|
}, [props.langCode]);
|
||||||
|
|
||||||
return loading ? <LoadingMessage /> : props.children;
|
return loading ? <LoadingMessage theme={props.theme} /> : props.children;
|
||||||
};
|
};
|
||||||
|
@@ -1,48 +1,6 @@
|
|||||||
@import "open-color/open-color";
|
@import "open-color/open-color";
|
||||||
@import "../css/variables.module";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.layer-ui__sidebar {
|
|
||||||
position: absolute;
|
|
||||||
top: var(--sat);
|
|
||||||
bottom: var(--sab);
|
|
||||||
right: var(--sar);
|
|
||||||
z-index: 5;
|
|
||||||
|
|
||||||
box-shadow: var(--shadow-island);
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
margin: var(--space-factor);
|
|
||||||
width: calc(#{$right-sidebar-width} - var(--space-factor) * 2);
|
|
||||||
|
|
||||||
.Island {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ToolIcon__icon {
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ToolIcon__icon__close {
|
|
||||||
.Modal__close {
|
|
||||||
width: calc(var(--space-factor) * 7);
|
|
||||||
height: calc(var(--space-factor) * 7);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.Island {
|
|
||||||
--padding: 0;
|
|
||||||
background-color: var(--island-bg-color);
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
padding: calc(var(--padding) * var(--space-factor));
|
|
||||||
position: relative;
|
|
||||||
transition: box-shadow 0.5s ease-in-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.layer-ui__wrapper.animate {
|
.layer-ui__wrapper.animate {
|
||||||
transition: width 0.1s ease-in-out;
|
transition: width 0.1s ease-in-out;
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import React, { useCallback } from "react";
|
import React from "react";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { CLASSES, LIBRARY_SIDEBAR_WIDTH } from "../constants";
|
import { CLASSES, LIBRARY_SIDEBAR_WIDTH } from "../constants";
|
||||||
import { exportCanvas } from "../data";
|
import { exportCanvas } from "../data";
|
||||||
import { isTextElement, showSelectedShapeActions } from "../element";
|
import { isTextElement, showSelectedShapeActions } from "../element";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { Language, t } from "../i18n";
|
import { Language, t } from "../i18n";
|
||||||
import { calculateScrollCenter, getSelectedElements } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
import { ExportType } from "../scene/types";
|
import { ExportType } from "../scene/types";
|
||||||
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
|
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
|
||||||
import { muteFSAbortError } from "../utils";
|
import { muteFSAbortError } from "../utils";
|
||||||
import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
|
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
||||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||||
import CollabButton from "./CollabButton";
|
import CollabButton from "./CollabButton";
|
||||||
import { ErrorDialog } from "./ErrorDialog";
|
import { ErrorDialog } from "./ErrorDialog";
|
||||||
@@ -26,7 +26,7 @@ import { Section } from "./Section";
|
|||||||
import { HelpDialog } from "./HelpDialog";
|
import { HelpDialog } from "./HelpDialog";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { UserList } from "./UserList";
|
import { UserList } from "./UserList";
|
||||||
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
import Library from "../data/library";
|
||||||
import { JSONExportDialog } from "./JSONExportDialog";
|
import { JSONExportDialog } from "./JSONExportDialog";
|
||||||
import { LibraryButton } from "./LibraryButton";
|
import { LibraryButton } from "./LibraryButton";
|
||||||
import { isImageFileHandle } from "../data/blob";
|
import { isImageFileHandle } from "../data/blob";
|
||||||
@@ -39,6 +39,10 @@ import { trackEvent } from "../analytics";
|
|||||||
import { useDevice } from "../components/App";
|
import { useDevice } from "../components/App";
|
||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
import { actionToggleStats } from "../actions/actionToggleStats";
|
import { actionToggleStats } from "../actions/actionToggleStats";
|
||||||
|
import Footer from "./Footer";
|
||||||
|
import { hostSidebarCountersAtom } from "./Sidebar/Sidebar";
|
||||||
|
import { jotaiScope } from "../jotai";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@@ -51,16 +55,13 @@ interface LayerUIProps {
|
|||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
onPenModeToggle: () => void;
|
onPenModeToggle: () => void;
|
||||||
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||||
zenModeEnabled: boolean;
|
|
||||||
showExitZenModeBtn: boolean;
|
showExitZenModeBtn: boolean;
|
||||||
showThemeBtn: boolean;
|
|
||||||
toggleZenMode: () => void;
|
|
||||||
langCode: Language["code"];
|
langCode: Language["code"];
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
||||||
renderCustomFooter?: ExcalidrawProps["renderFooter"];
|
renderCustomFooter?: ExcalidrawProps["renderFooter"];
|
||||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||||
viewModeEnabled: boolean;
|
renderCustomSidebar?: ExcalidrawProps["renderSidebar"];
|
||||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
UIOptions: AppProps["UIOptions"];
|
UIOptions: AppProps["UIOptions"];
|
||||||
focusContainer: () => void;
|
focusContainer: () => void;
|
||||||
@@ -73,21 +74,18 @@ const LayerUI = ({
|
|||||||
appState,
|
appState,
|
||||||
files,
|
files,
|
||||||
setAppState,
|
setAppState,
|
||||||
canvas,
|
|
||||||
elements,
|
elements,
|
||||||
|
canvas,
|
||||||
onCollabButtonClick,
|
onCollabButtonClick,
|
||||||
onLockToggle,
|
onLockToggle,
|
||||||
onPenModeToggle,
|
onPenModeToggle,
|
||||||
onInsertElements,
|
onInsertElements,
|
||||||
zenModeEnabled,
|
|
||||||
showExitZenModeBtn,
|
showExitZenModeBtn,
|
||||||
showThemeBtn,
|
|
||||||
toggleZenMode,
|
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
renderCustomFooter,
|
renderCustomFooter,
|
||||||
renderCustomStats,
|
renderCustomStats,
|
||||||
viewModeEnabled,
|
renderCustomSidebar,
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
UIOptions,
|
UIOptions,
|
||||||
focusContainer,
|
focusContainer,
|
||||||
@@ -171,7 +169,7 @@ const LayerUI = ({
|
|||||||
<Section
|
<Section
|
||||||
heading="canvasActions"
|
heading="canvasActions"
|
||||||
className={clsx("zen-mode-transition", {
|
className={clsx("zen-mode-transition", {
|
||||||
"transition-left": zenModeEnabled,
|
"transition-left": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* the zIndex ensures this menu has higher stacking order,
|
{/* the zIndex ensures this menu has higher stacking order,
|
||||||
@@ -192,7 +190,7 @@ const LayerUI = ({
|
|||||||
<Section
|
<Section
|
||||||
heading="canvasActions"
|
heading="canvasActions"
|
||||||
className={clsx("zen-mode-transition", {
|
className={clsx("zen-mode-transition", {
|
||||||
"transition-left": zenModeEnabled,
|
"transition-left": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* the zIndex ensures this menu has higher stacking order,
|
{/* the zIndex ensures this menu has higher stacking order,
|
||||||
@@ -214,12 +212,7 @@ const LayerUI = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
<BackgroundPickerAndDarkModeToggle
|
<BackgroundPickerAndDarkModeToggle actionManager={actionManager} />
|
||||||
actionManager={actionManager}
|
|
||||||
appState={appState}
|
|
||||||
setAppState={setAppState}
|
|
||||||
showThemeBtn={showThemeBtn}
|
|
||||||
/>
|
|
||||||
{appState.fileHandle && (
|
{appState.fileHandle && (
|
||||||
<>{actionManager.renderAction("saveToActiveFile")}</>
|
<>{actionManager.renderAction("saveToActiveFile")}</>
|
||||||
)}
|
)}
|
||||||
@@ -232,7 +225,7 @@ const LayerUI = ({
|
|||||||
<Section
|
<Section
|
||||||
heading="selectedShapeActions"
|
heading="selectedShapeActions"
|
||||||
className={clsx("zen-mode-transition", {
|
className={clsx("zen-mode-transition", {
|
||||||
"transition-left": zenModeEnabled,
|
"transition-left": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Island
|
<Island
|
||||||
@@ -249,48 +242,11 @@ const LayerUI = ({
|
|||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
renderAction={actionManager.renderAction}
|
renderAction={actionManager.renderAction}
|
||||||
activeTool={appState.activeTool.type}
|
|
||||||
/>
|
/>
|
||||||
</Island>
|
</Island>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
|
|
||||||
const closeLibrary = useCallback(() => {
|
|
||||||
const isDialogOpen = !!document.querySelector(".Dialog");
|
|
||||||
|
|
||||||
// Prevent closing if any dialog is open
|
|
||||||
if (isDialogOpen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setAppState({ isLibraryOpen: false });
|
|
||||||
}, [setAppState]);
|
|
||||||
|
|
||||||
const deselectItems = useCallback(() => {
|
|
||||||
setAppState({
|
|
||||||
selectedElementIds: {},
|
|
||||||
selectedGroupIds: {},
|
|
||||||
});
|
|
||||||
}, [setAppState]);
|
|
||||||
|
|
||||||
const libraryMenu = appState.isLibraryOpen ? (
|
|
||||||
<LibraryMenu
|
|
||||||
pendingElements={getSelectedElements(elements, appState, true)}
|
|
||||||
onClose={closeLibrary}
|
|
||||||
onInsertLibraryItems={(libraryItems) => {
|
|
||||||
onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
|
|
||||||
}}
|
|
||||||
onAddToLibrary={deselectItems}
|
|
||||||
setAppState={setAppState}
|
|
||||||
libraryReturnUrl={libraryReturnUrl}
|
|
||||||
focusContainer={focusContainer}
|
|
||||||
library={library}
|
|
||||||
theme={appState.theme}
|
|
||||||
files={files}
|
|
||||||
id={id}
|
|
||||||
appState={appState}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const renderFixedSideContainer = () => {
|
const renderFixedSideContainer = () => {
|
||||||
const shouldRenderSelectedShapeActions = showSelectedShapeActions(
|
const shouldRenderSelectedShapeActions = showSelectedShapeActions(
|
||||||
appState,
|
appState,
|
||||||
@@ -302,32 +258,34 @@ const LayerUI = ({
|
|||||||
<div className="App-menu App-menu_top">
|
<div className="App-menu App-menu_top">
|
||||||
<Stack.Col
|
<Stack.Col
|
||||||
gap={4}
|
gap={4}
|
||||||
className={clsx({ "disable-pointerEvents": zenModeEnabled })}
|
className={clsx({
|
||||||
|
"disable-pointerEvents": appState.zenModeEnabled,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{viewModeEnabled
|
{appState.viewModeEnabled
|
||||||
? renderViewModeCanvasActions()
|
? renderViewModeCanvasActions()
|
||||||
: renderCanvasActions()}
|
: renderCanvasActions()}
|
||||||
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
{!viewModeEnabled && (
|
{!appState.viewModeEnabled && (
|
||||||
<Section heading="shapes">
|
<Section heading="shapes">
|
||||||
{(heading) => (
|
{(heading: React.ReactNode) => (
|
||||||
<Stack.Col gap={4} align="start">
|
<Stack.Col gap={4} align="start">
|
||||||
<Stack.Row
|
<Stack.Row
|
||||||
gap={1}
|
gap={1}
|
||||||
className={clsx("App-toolbar-container", {
|
className={clsx("App-toolbar-container", {
|
||||||
"zen-mode": zenModeEnabled,
|
"zen-mode": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<PenModeButton
|
<PenModeButton
|
||||||
zenModeEnabled={zenModeEnabled}
|
zenModeEnabled={appState.zenModeEnabled}
|
||||||
checked={appState.penMode}
|
checked={appState.penMode}
|
||||||
onChange={onPenModeToggle}
|
onChange={onPenModeToggle}
|
||||||
title={t("toolBar.penMode")}
|
title={t("toolBar.penMode")}
|
||||||
penDetected={appState.penDetected}
|
penDetected={appState.penDetected}
|
||||||
/>
|
/>
|
||||||
<LockButton
|
<LockButton
|
||||||
zenModeEnabled={zenModeEnabled}
|
zenModeEnabled={appState.zenModeEnabled}
|
||||||
checked={appState.activeTool.locked}
|
checked={appState.activeTool.locked}
|
||||||
onChange={() => onLockToggle()}
|
onChange={() => onLockToggle()}
|
||||||
title={t("toolBar.lock")}
|
title={t("toolBar.lock")}
|
||||||
@@ -335,13 +293,14 @@ const LayerUI = ({
|
|||||||
<Island
|
<Island
|
||||||
padding={1}
|
padding={1}
|
||||||
className={clsx("App-toolbar", {
|
className={clsx("App-toolbar", {
|
||||||
"zen-mode": zenModeEnabled,
|
"zen-mode": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<HintViewer
|
<HintViewer
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
isMobile={device.isMobile}
|
isMobile={device.isMobile}
|
||||||
|
device={device}
|
||||||
/>
|
/>
|
||||||
{heading}
|
{heading}
|
||||||
<Stack.Row gap={1}>
|
<Stack.Row gap={1}>
|
||||||
@@ -371,7 +330,7 @@ const LayerUI = ({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"layer-ui__wrapper__top-right zen-mode-transition",
|
"layer-ui__wrapper__top-right zen-mode-transition",
|
||||||
{
|
{
|
||||||
"transition-right": zenModeEnabled,
|
"transition-right": appState.zenModeEnabled,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -386,99 +345,24 @@ const LayerUI = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderBottomAppMenu = () => {
|
const renderSidebars = () => {
|
||||||
return (
|
return appState.openSidebar === "customSidebar" ? (
|
||||||
<footer
|
renderCustomSidebar?.() || null
|
||||||
role="contentinfo"
|
) : appState.openSidebar === "library" ? (
|
||||||
className="layer-ui__wrapper__footer App-menu App-menu_bottom"
|
<LibraryMenu
|
||||||
>
|
appState={appState}
|
||||||
<div
|
onInsertElements={onInsertElements}
|
||||||
className={clsx(
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
"layer-ui__wrapper__footer-left zen-mode-transition",
|
focusContainer={focusContainer}
|
||||||
{
|
library={library}
|
||||||
"layer-ui__wrapper__footer-left--transition-left": zenModeEnabled,
|
id={id}
|
||||||
},
|
/>
|
||||||
)}
|
) : null;
|
||||||
>
|
|
||||||
<Stack.Col gap={2}>
|
|
||||||
<Section heading="canvasActions">
|
|
||||||
<Island padding={1}>
|
|
||||||
<ZoomActions
|
|
||||||
renderAction={actionManager.renderAction}
|
|
||||||
zoom={appState.zoom}
|
|
||||||
/>
|
|
||||||
</Island>
|
|
||||||
{!viewModeEnabled && (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={clsx("undo-redo-buttons zen-mode-transition", {
|
|
||||||
"layer-ui__wrapper__footer-left--transition-bottom":
|
|
||||||
zenModeEnabled,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{actionManager.renderAction("undo", { size: "small" })}
|
|
||||||
{actionManager.renderAction("redo", { size: "small" })}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={clsx("eraser-buttons zen-mode-transition", {
|
|
||||||
"layer-ui__wrapper__footer-left--transition-left":
|
|
||||||
zenModeEnabled,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{actionManager.renderAction("eraser", { size: "small" })}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!viewModeEnabled &&
|
|
||||||
appState.multiElement &&
|
|
||||||
device.isTouchScreen && (
|
|
||||||
<div
|
|
||||||
className={clsx("finalize-button zen-mode-transition", {
|
|
||||||
"layer-ui__wrapper__footer-left--transition-left":
|
|
||||||
zenModeEnabled,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{actionManager.renderAction("finalize", { size: "small" })}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Section>
|
|
||||||
</Stack.Col>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
"layer-ui__wrapper__footer-center zen-mode-transition",
|
|
||||||
{
|
|
||||||
"layer-ui__wrapper__footer-left--transition-bottom":
|
|
||||||
zenModeEnabled,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{renderCustomFooter?.(false, appState)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
"layer-ui__wrapper__footer-right zen-mode-transition",
|
|
||||||
{
|
|
||||||
"transition-right disable-pointerEvents": zenModeEnabled,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{actionManager.renderAction("toggleShortcuts")}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className={clsx("disable-zen-mode", {
|
|
||||||
"disable-zen-mode--visible": showExitZenModeBtn,
|
|
||||||
})}
|
|
||||||
onClick={toggleZenMode}
|
|
||||||
>
|
|
||||||
{t("buttons.exitZenMode")}
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const dialogs = (
|
const [hostSidebarCounters] = useAtom(hostSidebarCountersAtom, jotaiScope);
|
||||||
|
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
{appState.isLoading && <LoadingMessage delay={250} />}
|
{appState.isLoading && <LoadingMessage delay={250} />}
|
||||||
{appState.errorMessage && (
|
{appState.errorMessage && (
|
||||||
@@ -506,87 +390,80 @@ const LayerUI = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
{device.isMobile && (
|
||||||
);
|
<MobileMenu
|
||||||
|
appState={appState}
|
||||||
|
elements={elements}
|
||||||
|
actionManager={actionManager}
|
||||||
|
renderJSONExportDialog={renderJSONExportDialog}
|
||||||
|
renderImageExportDialog={renderImageExportDialog}
|
||||||
|
setAppState={setAppState}
|
||||||
|
onCollabButtonClick={onCollabButtonClick}
|
||||||
|
onLockToggle={() => onLockToggle()}
|
||||||
|
onPenModeToggle={onPenModeToggle}
|
||||||
|
canvas={canvas}
|
||||||
|
isCollaborating={isCollaborating}
|
||||||
|
renderCustomFooter={renderCustomFooter}
|
||||||
|
onImageAction={onImageAction}
|
||||||
|
renderTopRightUI={renderTopRightUI}
|
||||||
|
renderCustomStats={renderCustomStats}
|
||||||
|
renderSidebars={renderSidebars}
|
||||||
|
device={device}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
const renderStats = () => {
|
{!device.isMobile && (
|
||||||
if (!appState.showStats) {
|
<>
|
||||||
return null;
|
<div
|
||||||
}
|
className={clsx("layer-ui__wrapper", {
|
||||||
return (
|
"disable-pointerEvents":
|
||||||
<Stats
|
appState.draggingElement ||
|
||||||
appState={appState}
|
appState.resizingElement ||
|
||||||
setAppState={setAppState}
|
(appState.editingElement &&
|
||||||
elements={elements}
|
!isTextElement(appState.editingElement)),
|
||||||
onClose={() => {
|
})}
|
||||||
actionManager.executeAction(actionToggleStats);
|
style={
|
||||||
}}
|
((appState.openSidebar === "library" &&
|
||||||
renderCustomStats={renderCustomStats}
|
appState.isSidebarDocked) ||
|
||||||
/>
|
hostSidebarCounters.docked) &&
|
||||||
);
|
device.canDeviceFitSidebar
|
||||||
};
|
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
|
||||||
|
: {}
|
||||||
return device.isMobile ? (
|
}
|
||||||
<>
|
|
||||||
{dialogs}
|
|
||||||
<MobileMenu
|
|
||||||
appState={appState}
|
|
||||||
elements={elements}
|
|
||||||
actionManager={actionManager}
|
|
||||||
libraryMenu={libraryMenu}
|
|
||||||
renderJSONExportDialog={renderJSONExportDialog}
|
|
||||||
renderImageExportDialog={renderImageExportDialog}
|
|
||||||
setAppState={setAppState}
|
|
||||||
onCollabButtonClick={onCollabButtonClick}
|
|
||||||
onLockToggle={() => onLockToggle()}
|
|
||||||
onPenModeToggle={onPenModeToggle}
|
|
||||||
canvas={canvas}
|
|
||||||
isCollaborating={isCollaborating}
|
|
||||||
renderCustomFooter={renderCustomFooter}
|
|
||||||
viewModeEnabled={viewModeEnabled}
|
|
||||||
showThemeBtn={showThemeBtn}
|
|
||||||
onImageAction={onImageAction}
|
|
||||||
renderTopRightUI={renderTopRightUI}
|
|
||||||
renderStats={renderStats}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={clsx("layer-ui__wrapper", {
|
|
||||||
"disable-pointerEvents":
|
|
||||||
appState.draggingElement ||
|
|
||||||
appState.resizingElement ||
|
|
||||||
(appState.editingElement &&
|
|
||||||
!isTextElement(appState.editingElement)),
|
|
||||||
})}
|
|
||||||
style={
|
|
||||||
appState.isLibraryOpen &&
|
|
||||||
appState.isLibraryMenuDocked &&
|
|
||||||
device.canDeviceFitSidebar
|
|
||||||
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{dialogs}
|
|
||||||
{renderFixedSideContainer()}
|
|
||||||
{renderBottomAppMenu()}
|
|
||||||
{renderStats()}
|
|
||||||
{appState.scrolledOutside && (
|
|
||||||
<button
|
|
||||||
className="scroll-back-to-content"
|
|
||||||
onClick={() => {
|
|
||||||
setAppState({
|
|
||||||
...calculateScrollCenter(elements, appState, canvas),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t("buttons.scrollBackToContent")}
|
{renderFixedSideContainer()}
|
||||||
</button>
|
<Footer
|
||||||
)}
|
appState={appState}
|
||||||
</div>
|
actionManager={actionManager}
|
||||||
{appState.isLibraryOpen && (
|
renderCustomFooter={renderCustomFooter}
|
||||||
<div className="layer-ui__sidebar">{libraryMenu}</div>
|
showExitZenModeBtn={showExitZenModeBtn}
|
||||||
|
/>
|
||||||
|
{appState.showStats && (
|
||||||
|
<Stats
|
||||||
|
appState={appState}
|
||||||
|
setAppState={setAppState}
|
||||||
|
elements={elements}
|
||||||
|
onClose={() => {
|
||||||
|
actionManager.executeAction(actionToggleStats);
|
||||||
|
}}
|
||||||
|
renderCustomStats={renderCustomStats}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{appState.scrolledOutside && (
|
||||||
|
<button
|
||||||
|
className="scroll-back-to-content"
|
||||||
|
onClick={() => {
|
||||||
|
setAppState({
|
||||||
|
...calculateScrollCenter(elements, appState, canvas),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("buttons.scrollBackToContent")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{renderSidebars()}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -605,8 +482,12 @@ const areEqual = (prev: LayerUIProps, next: LayerUIProps) => {
|
|||||||
const nextAppState = getNecessaryObj(next.appState);
|
const nextAppState = getNecessaryObj(next.appState);
|
||||||
|
|
||||||
const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
|
const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
prev.renderCustomFooter === next.renderCustomFooter &&
|
prev.renderCustomFooter === next.renderCustomFooter &&
|
||||||
|
prev.renderTopRightUI === next.renderTopRightUI &&
|
||||||
|
prev.renderCustomStats === next.renderCustomStats &&
|
||||||
|
prev.renderCustomSidebar === next.renderCustomSidebar &&
|
||||||
prev.langCode === next.langCode &&
|
prev.langCode === next.langCode &&
|
||||||
prev.elements === next.elements &&
|
prev.elements === next.elements &&
|
||||||
prev.files === next.files &&
|
prev.files === next.files &&
|
||||||
|
@@ -40,10 +40,10 @@ export const LibraryButton: React.FC<{
|
|||||||
document
|
document
|
||||||
.querySelector(".layer-ui__wrapper")
|
.querySelector(".layer-ui__wrapper")
|
||||||
?.classList.remove("animate");
|
?.classList.remove("animate");
|
||||||
const nextState = event.target.checked;
|
const isOpen = event.target.checked;
|
||||||
setAppState({ isLibraryOpen: nextState });
|
setAppState({ openSidebar: isOpen ? "library" : null });
|
||||||
// track only openings
|
// track only openings
|
||||||
if (nextState) {
|
if (isOpen) {
|
||||||
trackEvent(
|
trackEvent(
|
||||||
"library",
|
"library",
|
||||||
"toggleLibrary (open)",
|
"toggleLibrary (open)",
|
||||||
@@ -51,7 +51,7 @@ export const LibraryButton: React.FC<{
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
checked={appState.isLibraryOpen}
|
checked={appState.openSidebar === "library"}
|
||||||
aria-label={capitalizeString(t("toolBar.library"))}
|
aria-label={capitalizeString(t("toolBar.library"))}
|
||||||
aria-keyshortcuts="0"
|
aria-keyshortcuts="0"
|
||||||
/>
|
/>
|
||||||
|
@@ -1,10 +1,16 @@
|
|||||||
@import "open-color/open-color";
|
@import "open-color/open-color";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
|
.layer-ui__library-sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.layer-ui__library {
|
.layer-ui__library {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
justify-content: center;
|
|
||||||
|
flex: 1 1 auto;
|
||||||
|
|
||||||
.layer-ui__library-header {
|
.layer-ui__library-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -23,16 +29,100 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.layer-ui__sidebar {
|
.layer-ui__sidebar {
|
||||||
.layer-ui__library {
|
|
||||||
padding: 0;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.library-menu-items-container {
|
.library-menu-items-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.library-actions {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
margin-right: auto;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
button .library-actions-counter {
|
||||||
|
position: absolute;
|
||||||
|
right: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
padding: 1px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--remove {
|
||||||
|
background-color: $oc-red-7;
|
||||||
|
&:hover {
|
||||||
|
background-color: $oc-red-8;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: $oc-red-9;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
color: $oc-white;
|
||||||
|
}
|
||||||
|
.library-actions-counter {
|
||||||
|
color: $oc-red-7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--export {
|
||||||
|
background-color: $oc-lime-5;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $oc-lime-7;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: $oc-lime-8;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
color: $oc-white;
|
||||||
|
}
|
||||||
|
.library-actions-counter {
|
||||||
|
color: $oc-lime-5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--publish {
|
||||||
|
background-color: $oc-cyan-6;
|
||||||
|
&:hover {
|
||||||
|
background-color: $oc-cyan-7;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: $oc-cyan-9;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
color: $oc-white;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
margin-left: -0.2em;
|
||||||
|
margin-right: 1.1em;
|
||||||
|
color: $oc-white;
|
||||||
|
font-size: 0.86em;
|
||||||
|
}
|
||||||
|
.library-actions-counter {
|
||||||
|
color: $oc-cyan-6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--load {
|
||||||
|
background-color: $oc-blue-6;
|
||||||
|
&:hover {
|
||||||
|
background-color: $oc-blue-7;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: $oc-blue-9;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
color: $oc-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.layer-ui__library-message {
|
.layer-ui__library-message {
|
||||||
padding: 2em 4em;
|
padding: 2em 4em;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
|
@@ -6,30 +6,31 @@ import {
|
|||||||
RefObject,
|
RefObject,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
import Library, { libraryItemsAtom } from "../data/library";
|
import Library, {
|
||||||
|
distributeLibraryItemsOnSquareGrid,
|
||||||
|
libraryItemsAtom,
|
||||||
|
} from "../data/library";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
import {
|
import { LibraryItems, LibraryItem, AppState, ExcalidrawProps } from "../types";
|
||||||
LibraryItems,
|
|
||||||
LibraryItem,
|
|
||||||
AppState,
|
|
||||||
BinaryFiles,
|
|
||||||
ExcalidrawProps,
|
|
||||||
} from "../types";
|
|
||||||
import { Dialog } from "./Dialog";
|
|
||||||
import { Island } from "./Island";
|
|
||||||
import PublishLibrary from "./PublishLibrary";
|
|
||||||
import { ToolButton } from "./ToolButton";
|
|
||||||
|
|
||||||
import "./LibraryMenu.scss";
|
import "./LibraryMenu.scss";
|
||||||
import LibraryMenuItems from "./LibraryMenuItems";
|
import LibraryMenuItems from "./LibraryMenuItems";
|
||||||
import { EVENT } from "../constants";
|
import { EVENT, VERSIONS } from "../constants";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { jotaiScope } from "../jotai";
|
import { jotaiScope } from "../jotai";
|
||||||
import Spinner from "./Spinner";
|
import Spinner from "./Spinner";
|
||||||
import { useDevice } from "./App";
|
import {
|
||||||
|
useDevice,
|
||||||
|
useExcalidrawElements,
|
||||||
|
useExcalidrawSetAppState,
|
||||||
|
} from "./App";
|
||||||
|
import { Sidebar } from "./Sidebar/Sidebar";
|
||||||
|
import { getSelectedElements } from "../scene";
|
||||||
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
|
import { LibraryMenuHeader } from "./LibraryMenuHeaderContent";
|
||||||
|
|
||||||
const useOnClickOutside = (
|
const useOnClickOutside = (
|
||||||
ref: RefObject<HTMLElement>,
|
ref: RefObject<HTMLElement>,
|
||||||
@@ -59,112 +60,45 @@ const useOnClickOutside = (
|
|||||||
}, [ref, cb]);
|
}, [ref, cb]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSelectedItems = (
|
|
||||||
libraryItems: LibraryItems,
|
|
||||||
selectedItems: LibraryItem["id"][],
|
|
||||||
) => libraryItems.filter((item) => selectedItems.includes(item.id));
|
|
||||||
|
|
||||||
const LibraryMenuWrapper = forwardRef<
|
const LibraryMenuWrapper = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
{ children: React.ReactNode }
|
{ children: React.ReactNode }
|
||||||
>(({ children }, ref) => {
|
>(({ children }, ref) => {
|
||||||
return (
|
return (
|
||||||
<Island padding={1} ref={ref} className="layer-ui__library">
|
<div ref={ref} className="layer-ui__library">
|
||||||
{children}
|
{children}
|
||||||
</Island>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const LibraryMenu = ({
|
export const LibraryMenuContent = ({
|
||||||
onClose,
|
|
||||||
onInsertLibraryItems,
|
onInsertLibraryItems,
|
||||||
pendingElements,
|
pendingElements,
|
||||||
onAddToLibrary,
|
onAddToLibrary,
|
||||||
theme,
|
|
||||||
setAppState,
|
setAppState,
|
||||||
files,
|
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
focusContainer,
|
|
||||||
library,
|
library,
|
||||||
id,
|
id,
|
||||||
appState,
|
appState,
|
||||||
|
selectedItems,
|
||||||
|
onSelectItems,
|
||||||
}: {
|
}: {
|
||||||
pendingElements: LibraryItem["elements"];
|
pendingElements: LibraryItem["elements"];
|
||||||
onClose: () => void;
|
|
||||||
onInsertLibraryItems: (libraryItems: LibraryItems) => void;
|
onInsertLibraryItems: (libraryItems: LibraryItems) => void;
|
||||||
onAddToLibrary: () => void;
|
onAddToLibrary: () => void;
|
||||||
theme: AppState["theme"];
|
|
||||||
files: BinaryFiles;
|
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
focusContainer: () => void;
|
|
||||||
library: Library;
|
library: Library;
|
||||||
id: string;
|
id: string;
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
|
selectedItems: LibraryItem["id"][];
|
||||||
|
onSelectItems: (id: LibraryItem["id"][]) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const referrer =
|
||||||
|
libraryReturnUrl || window.location.origin + window.location.pathname;
|
||||||
const device = useDevice();
|
|
||||||
|
|
||||||
useOnClickOutside(
|
|
||||||
ref,
|
|
||||||
useCallback(
|
|
||||||
(event) => {
|
|
||||||
// If click on the library icon, do nothing.
|
|
||||||
if ((event.target as Element).closest(".ToolIcon__library")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!appState.isLibraryMenuDocked || !device.canDeviceFitSidebar) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onClose, appState.isLibraryMenuDocked, device.canDeviceFitSidebar],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if (
|
|
||||||
event.key === KEYS.ESCAPE &&
|
|
||||||
(!appState.isLibraryMenuDocked || !device.canDeviceFitSidebar)
|
|
||||||
) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener(EVENT.KEYDOWN, handleKeyDown);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
|
|
||||||
};
|
|
||||||
}, [onClose, appState.isLibraryMenuDocked, device.canDeviceFitSidebar]);
|
|
||||||
|
|
||||||
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
|
|
||||||
const [showPublishLibraryDialog, setShowPublishLibraryDialog] =
|
|
||||||
useState(false);
|
|
||||||
const [publishLibSuccess, setPublishLibSuccess] = useState<null | {
|
|
||||||
url: string;
|
|
||||||
authorName: string;
|
|
||||||
}>(null);
|
|
||||||
|
|
||||||
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
||||||
|
|
||||||
const removeFromLibrary = useCallback(
|
|
||||||
async (libraryItems: LibraryItems) => {
|
|
||||||
const nextItems = libraryItems.filter(
|
|
||||||
(item) => !selectedItems.includes(item.id),
|
|
||||||
);
|
|
||||||
library.setLibrary(nextItems).catch(() => {
|
|
||||||
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
|
|
||||||
});
|
|
||||||
setSelectedItems([]);
|
|
||||||
},
|
|
||||||
[library, setAppState, selectedItems, setSelectedItems],
|
|
||||||
);
|
|
||||||
|
|
||||||
const resetLibrary = useCallback(() => {
|
|
||||||
library.resetLibrary();
|
|
||||||
focusContainer();
|
|
||||||
}, [library, focusContainer]);
|
|
||||||
|
|
||||||
const addToLibrary = useCallback(
|
const addToLibrary = useCallback(
|
||||||
async (elements: LibraryItem["elements"], libraryItems: LibraryItems) => {
|
async (elements: LibraryItem["elements"], libraryItems: LibraryItems) => {
|
||||||
trackEvent("element", "addToLibrary", "ui");
|
trackEvent("element", "addToLibrary", "ui");
|
||||||
@@ -190,60 +124,12 @@ export const LibraryMenu = ({
|
|||||||
[onAddToLibrary, library, setAppState],
|
[onAddToLibrary, library, setAppState],
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderPublishSuccess = useCallback(() => {
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
onCloseRequest={() => setPublishLibSuccess(null)}
|
|
||||||
title={t("publishSuccessDialog.title")}
|
|
||||||
className="publish-library-success"
|
|
||||||
small={true}
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
{t("publishSuccessDialog.content", {
|
|
||||||
authorName: publishLibSuccess!.authorName,
|
|
||||||
})}{" "}
|
|
||||||
<a
|
|
||||||
href={publishLibSuccess?.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{t("publishSuccessDialog.link")}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<ToolButton
|
|
||||||
type="button"
|
|
||||||
title={t("buttons.close")}
|
|
||||||
aria-label={t("buttons.close")}
|
|
||||||
label={t("buttons.close")}
|
|
||||||
onClick={() => setPublishLibSuccess(null)}
|
|
||||||
data-testid="publish-library-success-close"
|
|
||||||
className="publish-library-success-close"
|
|
||||||
/>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}, [setPublishLibSuccess, publishLibSuccess]);
|
|
||||||
|
|
||||||
const onPublishLibSuccess = useCallback(
|
|
||||||
(data, libraryItems: LibraryItems) => {
|
|
||||||
setShowPublishLibraryDialog(false);
|
|
||||||
setPublishLibSuccess({ url: data.url, authorName: data.authorName });
|
|
||||||
const nextLibItems = libraryItems.slice();
|
|
||||||
nextLibItems.forEach((libItem) => {
|
|
||||||
if (selectedItems.includes(libItem.id)) {
|
|
||||||
libItem.status = "published";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
library.setLibrary(nextLibItems);
|
|
||||||
},
|
|
||||||
[setShowPublishLibraryDialog, setPublishLibSuccess, selectedItems, library],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
libraryItemsData.status === "loading" &&
|
libraryItemsData.status === "loading" &&
|
||||||
!libraryItemsData.isInitialized
|
!libraryItemsData.isInitialized
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<LibraryMenuWrapper ref={ref}>
|
<LibraryMenuWrapper>
|
||||||
<div className="layer-ui__library-message">
|
<div className="layer-ui__library-message">
|
||||||
<Spinner size="2em" />
|
<Spinner size="2em" />
|
||||||
<span>{t("labels.libraryLoadingMessage")}</span>
|
<span>{t("labels.libraryLoadingMessage")}</span>
|
||||||
@@ -253,51 +139,168 @@ export const LibraryMenu = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LibraryMenuWrapper ref={ref}>
|
<LibraryMenuWrapper>
|
||||||
{showPublishLibraryDialog && (
|
|
||||||
<PublishLibrary
|
|
||||||
onClose={() => setShowPublishLibraryDialog(false)}
|
|
||||||
libraryItems={getSelectedItems(
|
|
||||||
libraryItemsData.libraryItems,
|
|
||||||
selectedItems,
|
|
||||||
)}
|
|
||||||
appState={appState}
|
|
||||||
onSuccess={(data) =>
|
|
||||||
onPublishLibSuccess(data, libraryItemsData.libraryItems)
|
|
||||||
}
|
|
||||||
onError={(error) => window.alert(error)}
|
|
||||||
updateItemsInStorage={() =>
|
|
||||||
library.setLibrary(libraryItemsData.libraryItems)
|
|
||||||
}
|
|
||||||
onRemove={(id: string) =>
|
|
||||||
setSelectedItems(selectedItems.filter((_id) => _id !== id))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{publishLibSuccess && renderPublishSuccess()}
|
|
||||||
<LibraryMenuItems
|
<LibraryMenuItems
|
||||||
isLoading={libraryItemsData.status === "loading"}
|
isLoading={libraryItemsData.status === "loading"}
|
||||||
libraryItems={libraryItemsData.libraryItems}
|
libraryItems={libraryItemsData.libraryItems}
|
||||||
onRemoveFromLibrary={() =>
|
|
||||||
removeFromLibrary(libraryItemsData.libraryItems)
|
|
||||||
}
|
|
||||||
onAddToLibrary={(elements) =>
|
onAddToLibrary={(elements) =>
|
||||||
addToLibrary(elements, libraryItemsData.libraryItems)
|
addToLibrary(elements, libraryItemsData.libraryItems)
|
||||||
}
|
}
|
||||||
onInsertLibraryItems={onInsertLibraryItems}
|
onInsertLibraryItems={onInsertLibraryItems}
|
||||||
pendingElements={pendingElements}
|
pendingElements={pendingElements}
|
||||||
setAppState={setAppState}
|
|
||||||
appState={appState}
|
|
||||||
libraryReturnUrl={libraryReturnUrl}
|
|
||||||
library={library}
|
|
||||||
theme={theme}
|
|
||||||
files={files}
|
|
||||||
id={id}
|
|
||||||
selectedItems={selectedItems}
|
selectedItems={selectedItems}
|
||||||
onSelectItems={(ids) => setSelectedItems(ids)}
|
onSelectItems={onSelectItems}
|
||||||
onPublish={() => setShowPublishLibraryDialog(true)}
|
|
||||||
resetLibrary={resetLibrary}
|
|
||||||
/>
|
/>
|
||||||
|
<a
|
||||||
|
className="library-menu-browse-button"
|
||||||
|
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
|
||||||
|
window.name || "_blank"
|
||||||
|
}&referrer=${referrer}&useHash=true&token=${id}&theme=${
|
||||||
|
appState.theme
|
||||||
|
}&version=${VERSIONS.excalidrawLibrary}`}
|
||||||
|
target="_excalidraw_libraries"
|
||||||
|
>
|
||||||
|
{t("labels.libraries")}
|
||||||
|
</a>
|
||||||
</LibraryMenuWrapper>
|
</LibraryMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const LibraryMenu: React.FC<{
|
||||||
|
appState: AppState;
|
||||||
|
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||||
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
|
focusContainer: () => void;
|
||||||
|
library: Library;
|
||||||
|
id: string;
|
||||||
|
}> = ({
|
||||||
|
appState,
|
||||||
|
onInsertElements,
|
||||||
|
libraryReturnUrl,
|
||||||
|
focusContainer,
|
||||||
|
library,
|
||||||
|
id,
|
||||||
|
}) => {
|
||||||
|
const setAppState = useExcalidrawSetAppState();
|
||||||
|
const elements = useExcalidrawElements();
|
||||||
|
const device = useDevice();
|
||||||
|
|
||||||
|
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
|
||||||
|
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const closeLibrary = useCallback(() => {
|
||||||
|
const isDialogOpen = !!document.querySelector(".Dialog");
|
||||||
|
|
||||||
|
// Prevent closing if any dialog is open
|
||||||
|
if (isDialogOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAppState({ openSidebar: null });
|
||||||
|
}, [setAppState]);
|
||||||
|
|
||||||
|
useOnClickOutside(
|
||||||
|
ref,
|
||||||
|
useCallback(
|
||||||
|
(event) => {
|
||||||
|
// If click on the library icon, do nothing so that LibraryButton
|
||||||
|
// can toggle library menu
|
||||||
|
if ((event.target as Element).closest(".ToolIcon__library")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!appState.isSidebarDocked || !device.canDeviceFitSidebar) {
|
||||||
|
closeLibrary();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[closeLibrary, appState.isSidebarDocked, device.canDeviceFitSidebar],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (
|
||||||
|
event.key === KEYS.ESCAPE &&
|
||||||
|
(!appState.isSidebarDocked || !device.canDeviceFitSidebar)
|
||||||
|
) {
|
||||||
|
closeLibrary();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener(EVENT.KEYDOWN, handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [closeLibrary, appState.isSidebarDocked, device.canDeviceFitSidebar]);
|
||||||
|
|
||||||
|
const deselectItems = useCallback(() => {
|
||||||
|
setAppState({
|
||||||
|
selectedElementIds: {},
|
||||||
|
selectedGroupIds: {},
|
||||||
|
});
|
||||||
|
}, [setAppState]);
|
||||||
|
|
||||||
|
const removeFromLibrary = useCallback(
|
||||||
|
async (libraryItems: LibraryItems) => {
|
||||||
|
const nextItems = libraryItems.filter(
|
||||||
|
(item) => !selectedItems.includes(item.id),
|
||||||
|
);
|
||||||
|
library.setLibrary(nextItems).catch(() => {
|
||||||
|
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
|
||||||
|
});
|
||||||
|
setSelectedItems([]);
|
||||||
|
},
|
||||||
|
[library, setAppState, selectedItems, setSelectedItems],
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetLibrary = useCallback(() => {
|
||||||
|
library.resetLibrary();
|
||||||
|
focusContainer();
|
||||||
|
}, [library, focusContainer]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sidebar
|
||||||
|
__isInternal
|
||||||
|
// necessary to remount when switching between internal
|
||||||
|
// and custom (host app) sidebar, so that the `props.onClose`
|
||||||
|
// is colled correctly
|
||||||
|
key="library"
|
||||||
|
className="layer-ui__library-sidebar"
|
||||||
|
onDock={(docked) => {
|
||||||
|
trackEvent(
|
||||||
|
"library",
|
||||||
|
`toggleLibraryDock (${docked ? "dock" : "undock"})`,
|
||||||
|
`sidebar (${device.isMobile ? "mobile" : "desktop"})`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<Sidebar.Header className="layer-ui__library-header">
|
||||||
|
<LibraryMenuHeader
|
||||||
|
appState={appState}
|
||||||
|
setAppState={setAppState}
|
||||||
|
selectedItems={selectedItems}
|
||||||
|
onSelectItems={setSelectedItems}
|
||||||
|
library={library}
|
||||||
|
onRemoveFromLibrary={() =>
|
||||||
|
removeFromLibrary(libraryItemsData.libraryItems)
|
||||||
|
}
|
||||||
|
resetLibrary={resetLibrary}
|
||||||
|
/>
|
||||||
|
</Sidebar.Header>
|
||||||
|
<LibraryMenuContent
|
||||||
|
pendingElements={getSelectedElements(elements, appState, true)}
|
||||||
|
onInsertLibraryItems={(libraryItems) => {
|
||||||
|
onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
|
||||||
|
}}
|
||||||
|
onAddToLibrary={deselectItems}
|
||||||
|
setAppState={setAppState}
|
||||||
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
|
library={library}
|
||||||
|
id={id}
|
||||||
|
appState={appState}
|
||||||
|
selectedItems={selectedItems}
|
||||||
|
onSelectItems={setSelectedItems}
|
||||||
|
/>
|
||||||
|
</Sidebar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
258
src/components/LibraryMenuHeaderContent.tsx
Normal file
258
src/components/LibraryMenuHeaderContent.tsx
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import React, { useCallback, useState } from "react";
|
||||||
|
import { saveLibraryAsJSON } from "../data/json";
|
||||||
|
import Library, { libraryItemsAtom } from "../data/library";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { AppState, LibraryItem, LibraryItems } from "../types";
|
||||||
|
import { exportToFileIcon, load, publishIcon, trash } from "./icons";
|
||||||
|
import { ToolButton } from "./ToolButton";
|
||||||
|
import { Tooltip } from "./Tooltip";
|
||||||
|
import { fileOpen } from "../data/filesystem";
|
||||||
|
import { muteFSAbortError } from "../utils";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { jotaiScope } from "../jotai";
|
||||||
|
import ConfirmDialog from "./ConfirmDialog";
|
||||||
|
import PublishLibrary from "./PublishLibrary";
|
||||||
|
import { Dialog } from "./Dialog";
|
||||||
|
|
||||||
|
const getSelectedItems = (
|
||||||
|
libraryItems: LibraryItems,
|
||||||
|
selectedItems: LibraryItem["id"][],
|
||||||
|
) => libraryItems.filter((item) => selectedItems.includes(item.id));
|
||||||
|
|
||||||
|
export const LibraryMenuHeader: React.FC<{
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
selectedItems: LibraryItem["id"][];
|
||||||
|
library: Library;
|
||||||
|
onRemoveFromLibrary: () => void;
|
||||||
|
resetLibrary: () => void;
|
||||||
|
onSelectItems: (items: LibraryItem["id"][]) => void;
|
||||||
|
appState: AppState;
|
||||||
|
}> = ({
|
||||||
|
setAppState,
|
||||||
|
selectedItems,
|
||||||
|
library,
|
||||||
|
onRemoveFromLibrary,
|
||||||
|
resetLibrary,
|
||||||
|
onSelectItems,
|
||||||
|
appState,
|
||||||
|
}) => {
|
||||||
|
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
||||||
|
|
||||||
|
const renderRemoveLibAlert = useCallback(() => {
|
||||||
|
const content = selectedItems.length
|
||||||
|
? t("alerts.removeItemsFromsLibrary", { count: selectedItems.length })
|
||||||
|
: t("alerts.resetLibrary");
|
||||||
|
const title = selectedItems.length
|
||||||
|
? t("confirmDialog.removeItemsFromLib")
|
||||||
|
: t("confirmDialog.resetLibrary");
|
||||||
|
return (
|
||||||
|
<ConfirmDialog
|
||||||
|
onConfirm={() => {
|
||||||
|
if (selectedItems.length) {
|
||||||
|
onRemoveFromLibrary();
|
||||||
|
} else {
|
||||||
|
resetLibrary();
|
||||||
|
}
|
||||||
|
setShowRemoveLibAlert(false);
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setShowRemoveLibAlert(false);
|
||||||
|
}}
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
<p>{content}</p>
|
||||||
|
</ConfirmDialog>
|
||||||
|
);
|
||||||
|
}, [selectedItems, onRemoveFromLibrary, resetLibrary]);
|
||||||
|
|
||||||
|
const [showRemoveLibAlert, setShowRemoveLibAlert] = useState(false);
|
||||||
|
|
||||||
|
const itemsSelected = !!selectedItems.length;
|
||||||
|
const items = itemsSelected
|
||||||
|
? libraryItemsData.libraryItems.filter((item) =>
|
||||||
|
selectedItems.includes(item.id),
|
||||||
|
)
|
||||||
|
: libraryItemsData.libraryItems;
|
||||||
|
const resetLabel = itemsSelected
|
||||||
|
? t("buttons.remove")
|
||||||
|
: t("buttons.resetLibrary");
|
||||||
|
|
||||||
|
const [showPublishLibraryDialog, setShowPublishLibraryDialog] =
|
||||||
|
useState(false);
|
||||||
|
const [publishLibSuccess, setPublishLibSuccess] = useState<null | {
|
||||||
|
url: string;
|
||||||
|
authorName: string;
|
||||||
|
}>(null);
|
||||||
|
const renderPublishSuccess = useCallback(() => {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
onCloseRequest={() => setPublishLibSuccess(null)}
|
||||||
|
title={t("publishSuccessDialog.title")}
|
||||||
|
className="publish-library-success"
|
||||||
|
small={true}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{t("publishSuccessDialog.content", {
|
||||||
|
authorName: publishLibSuccess!.authorName,
|
||||||
|
})}{" "}
|
||||||
|
<a
|
||||||
|
href={publishLibSuccess?.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{t("publishSuccessDialog.link")}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<ToolButton
|
||||||
|
type="button"
|
||||||
|
title={t("buttons.close")}
|
||||||
|
aria-label={t("buttons.close")}
|
||||||
|
label={t("buttons.close")}
|
||||||
|
onClick={() => setPublishLibSuccess(null)}
|
||||||
|
data-testid="publish-library-success-close"
|
||||||
|
className="publish-library-success-close"
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}, [setPublishLibSuccess, publishLibSuccess]);
|
||||||
|
|
||||||
|
const onPublishLibSuccess = useCallback(
|
||||||
|
(data: { url: string; authorName: string }, libraryItems: LibraryItems) => {
|
||||||
|
setShowPublishLibraryDialog(false);
|
||||||
|
setPublishLibSuccess({ url: data.url, authorName: data.authorName });
|
||||||
|
const nextLibItems = libraryItems.slice();
|
||||||
|
nextLibItems.forEach((libItem) => {
|
||||||
|
if (selectedItems.includes(libItem.id)) {
|
||||||
|
libItem.status = "published";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
library.setLibrary(nextLibItems);
|
||||||
|
},
|
||||||
|
[setShowPublishLibraryDialog, setPublishLibSuccess, selectedItems, library],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onLibraryImport = async () => {
|
||||||
|
try {
|
||||||
|
await library.updateLibrary({
|
||||||
|
libraryItems: fileOpen({
|
||||||
|
description: "Excalidraw library files",
|
||||||
|
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
|
||||||
|
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
|
||||||
|
/*
|
||||||
|
extensions: [".json", ".excalidrawlib"],
|
||||||
|
*/
|
||||||
|
}),
|
||||||
|
merge: true,
|
||||||
|
openLibraryMenu: true,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.name === "AbortError") {
|
||||||
|
console.warn(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAppState({ errorMessage: t("errors.importLibraryError") });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLibraryExport = async () => {
|
||||||
|
const libraryItems = itemsSelected
|
||||||
|
? items
|
||||||
|
: await library.getLatestLibrary();
|
||||||
|
saveLibraryAsJSON(libraryItems)
|
||||||
|
.catch(muteFSAbortError)
|
||||||
|
.catch((error) => {
|
||||||
|
setAppState({ errorMessage: error.message });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="library-actions">
|
||||||
|
{showRemoveLibAlert && renderRemoveLibAlert()}
|
||||||
|
{showPublishLibraryDialog && (
|
||||||
|
<PublishLibrary
|
||||||
|
onClose={() => setShowPublishLibraryDialog(false)}
|
||||||
|
libraryItems={getSelectedItems(
|
||||||
|
libraryItemsData.libraryItems,
|
||||||
|
selectedItems,
|
||||||
|
)}
|
||||||
|
appState={appState}
|
||||||
|
onSuccess={(data) =>
|
||||||
|
onPublishLibSuccess(data, libraryItemsData.libraryItems)
|
||||||
|
}
|
||||||
|
onError={(error) => window.alert(error)}
|
||||||
|
updateItemsInStorage={() =>
|
||||||
|
library.setLibrary(libraryItemsData.libraryItems)
|
||||||
|
}
|
||||||
|
onRemove={(id: string) =>
|
||||||
|
onSelectItems(selectedItems.filter((_id) => _id !== id))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{publishLibSuccess && renderPublishSuccess()}
|
||||||
|
{!itemsSelected && (
|
||||||
|
<ToolButton
|
||||||
|
key="import"
|
||||||
|
type="button"
|
||||||
|
title={t("buttons.load")}
|
||||||
|
aria-label={t("buttons.load")}
|
||||||
|
icon={load}
|
||||||
|
onClick={onLibraryImport}
|
||||||
|
className="library-actions--load"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!!items.length && (
|
||||||
|
<>
|
||||||
|
<ToolButton
|
||||||
|
key="export"
|
||||||
|
type="button"
|
||||||
|
title={t("buttons.export")}
|
||||||
|
aria-label={t("buttons.export")}
|
||||||
|
icon={exportToFileIcon}
|
||||||
|
onClick={onLibraryExport}
|
||||||
|
className="library-actions--export"
|
||||||
|
>
|
||||||
|
{selectedItems.length > 0 && (
|
||||||
|
<span className="library-actions-counter">
|
||||||
|
{selectedItems.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton
|
||||||
|
key="reset"
|
||||||
|
type="button"
|
||||||
|
title={resetLabel}
|
||||||
|
aria-label={resetLabel}
|
||||||
|
icon={trash}
|
||||||
|
onClick={() => setShowRemoveLibAlert(true)}
|
||||||
|
className="library-actions--remove"
|
||||||
|
>
|
||||||
|
{selectedItems.length > 0 && (
|
||||||
|
<span className="library-actions-counter">
|
||||||
|
{selectedItems.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</ToolButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{itemsSelected && (
|
||||||
|
<Tooltip label={t("hints.publishLibrary")}>
|
||||||
|
<ToolButton
|
||||||
|
type="button"
|
||||||
|
aria-label={t("buttons.publishLibrary")}
|
||||||
|
label={t("buttons.publishLibrary")}
|
||||||
|
icon={publishIcon}
|
||||||
|
className="library-actions--publish"
|
||||||
|
onClick={() => setShowPublishLibraryDialog(true)}
|
||||||
|
>
|
||||||
|
<label>{t("buttons.publishLibrary")}</label>
|
||||||
|
{selectedItems.length > 0 && (
|
||||||
|
<span className="library-actions-counter">
|
||||||
|
{selectedItems.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</ToolButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@@ -5,96 +5,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0.5rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
.library-actions {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
margin-right: auto;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
button .library-actions-counter {
|
|
||||||
position: absolute;
|
|
||||||
right: 2px;
|
|
||||||
bottom: 2px;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
padding: 1px;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--remove {
|
|
||||||
background-color: $oc-red-7;
|
|
||||||
&:hover {
|
|
||||||
background-color: $oc-red-8;
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background-color: $oc-red-9;
|
|
||||||
}
|
|
||||||
svg {
|
|
||||||
color: $oc-white;
|
|
||||||
}
|
|
||||||
.library-actions-counter {
|
|
||||||
color: $oc-red-7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--export {
|
|
||||||
background-color: $oc-lime-5;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $oc-lime-7;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: $oc-lime-8;
|
|
||||||
}
|
|
||||||
svg {
|
|
||||||
color: $oc-white;
|
|
||||||
}
|
|
||||||
.library-actions-counter {
|
|
||||||
color: $oc-lime-5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--publish {
|
|
||||||
background-color: $oc-cyan-6;
|
|
||||||
&:hover {
|
|
||||||
background-color: $oc-cyan-7;
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background-color: $oc-cyan-9;
|
|
||||||
}
|
|
||||||
svg {
|
|
||||||
color: $oc-white;
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
margin-left: -0.2em;
|
|
||||||
margin-right: 1.1em;
|
|
||||||
color: $oc-white;
|
|
||||||
font-size: 0.86em;
|
|
||||||
}
|
|
||||||
.library-actions-counter {
|
|
||||||
color: $oc-cyan-6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--load {
|
|
||||||
background-color: $oc-blue-6;
|
|
||||||
&:hover {
|
|
||||||
background-color: $oc-blue-7;
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background-color: $oc-blue-9;
|
|
||||||
}
|
|
||||||
svg {
|
|
||||||
color: $oc-white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&__items {
|
&__items {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
@@ -1,226 +1,35 @@
|
|||||||
import { chunk } from "lodash";
|
import React, { useState } from "react";
|
||||||
import React, { useCallback, useState } from "react";
|
import { serializeLibraryAsJSON } from "../data/json";
|
||||||
import { saveLibraryAsJSON, serializeLibraryAsJSON } from "../data/json";
|
|
||||||
import Library from "../data/library";
|
|
||||||
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import {
|
import { LibraryItem, LibraryItems } from "../types";
|
||||||
AppState,
|
import { arrayToMap, chunk } from "../utils";
|
||||||
BinaryFiles,
|
|
||||||
ExcalidrawProps,
|
|
||||||
LibraryItem,
|
|
||||||
LibraryItems,
|
|
||||||
} from "../types";
|
|
||||||
import { arrayToMap, muteFSAbortError } from "../utils";
|
|
||||||
import { useDevice } from "./App";
|
|
||||||
import ConfirmDialog from "./ConfirmDialog";
|
|
||||||
import { close, exportToFileIcon, load, publishIcon, trash } from "./icons";
|
|
||||||
import { LibraryUnit } from "./LibraryUnit";
|
import { LibraryUnit } from "./LibraryUnit";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { ToolButton } from "./ToolButton";
|
|
||||||
import { Tooltip } from "./Tooltip";
|
|
||||||
|
|
||||||
import "./LibraryMenuItems.scss";
|
import "./LibraryMenuItems.scss";
|
||||||
import { MIME_TYPES, VERSIONS } from "../constants";
|
import { MIME_TYPES } from "../constants";
|
||||||
import Spinner from "./Spinner";
|
import Spinner from "./Spinner";
|
||||||
import { fileOpen } from "../data/filesystem";
|
|
||||||
|
|
||||||
import { SidebarLockButton } from "./SidebarLockButton";
|
const CELLS_PER_ROW = 4;
|
||||||
import { trackEvent } from "../analytics";
|
|
||||||
|
|
||||||
const LibraryMenuItems = ({
|
const LibraryMenuItems = ({
|
||||||
isLoading,
|
isLoading,
|
||||||
libraryItems,
|
libraryItems,
|
||||||
onRemoveFromLibrary,
|
|
||||||
onAddToLibrary,
|
onAddToLibrary,
|
||||||
onInsertLibraryItems,
|
onInsertLibraryItems,
|
||||||
pendingElements,
|
pendingElements,
|
||||||
theme,
|
|
||||||
setAppState,
|
|
||||||
appState,
|
|
||||||
libraryReturnUrl,
|
|
||||||
library,
|
|
||||||
files,
|
|
||||||
id,
|
|
||||||
selectedItems,
|
selectedItems,
|
||||||
onSelectItems,
|
onSelectItems,
|
||||||
onPublish,
|
|
||||||
resetLibrary,
|
|
||||||
}: {
|
}: {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
libraryItems: LibraryItems;
|
libraryItems: LibraryItems;
|
||||||
pendingElements: LibraryItem["elements"];
|
pendingElements: LibraryItem["elements"];
|
||||||
onRemoveFromLibrary: () => void;
|
|
||||||
onInsertLibraryItems: (libraryItems: LibraryItems) => void;
|
onInsertLibraryItems: (libraryItems: LibraryItems) => void;
|
||||||
onAddToLibrary: (elements: LibraryItem["elements"]) => void;
|
onAddToLibrary: (elements: LibraryItem["elements"]) => void;
|
||||||
theme: AppState["theme"];
|
|
||||||
files: BinaryFiles;
|
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
|
||||||
appState: AppState;
|
|
||||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
|
||||||
library: Library;
|
|
||||||
id: string;
|
|
||||||
selectedItems: LibraryItem["id"][];
|
selectedItems: LibraryItem["id"][];
|
||||||
onSelectItems: (id: LibraryItem["id"][]) => void;
|
onSelectItems: (id: LibraryItem["id"][]) => void;
|
||||||
onPublish: () => void;
|
|
||||||
resetLibrary: () => void;
|
|
||||||
}) => {
|
}) => {
|
||||||
const renderRemoveLibAlert = useCallback(() => {
|
|
||||||
const content = selectedItems.length
|
|
||||||
? t("alerts.removeItemsFromsLibrary", { count: selectedItems.length })
|
|
||||||
: t("alerts.resetLibrary");
|
|
||||||
const title = selectedItems.length
|
|
||||||
? t("confirmDialog.removeItemsFromLib")
|
|
||||||
: t("confirmDialog.resetLibrary");
|
|
||||||
return (
|
|
||||||
<ConfirmDialog
|
|
||||||
onConfirm={() => {
|
|
||||||
if (selectedItems.length) {
|
|
||||||
onRemoveFromLibrary();
|
|
||||||
} else {
|
|
||||||
resetLibrary();
|
|
||||||
}
|
|
||||||
setShowRemoveLibAlert(false);
|
|
||||||
}}
|
|
||||||
onCancel={() => {
|
|
||||||
setShowRemoveLibAlert(false);
|
|
||||||
}}
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
<p>{content}</p>
|
|
||||||
</ConfirmDialog>
|
|
||||||
);
|
|
||||||
}, [selectedItems, onRemoveFromLibrary, resetLibrary]);
|
|
||||||
|
|
||||||
const [showRemoveLibAlert, setShowRemoveLibAlert] = useState(false);
|
|
||||||
const device = useDevice();
|
|
||||||
const renderLibraryActions = () => {
|
|
||||||
const itemsSelected = !!selectedItems.length;
|
|
||||||
const items = itemsSelected
|
|
||||||
? libraryItems.filter((item) => selectedItems.includes(item.id))
|
|
||||||
: libraryItems;
|
|
||||||
const resetLabel = itemsSelected
|
|
||||||
? t("buttons.remove")
|
|
||||||
: t("buttons.resetLibrary");
|
|
||||||
return (
|
|
||||||
<div className="library-actions">
|
|
||||||
{!itemsSelected && (
|
|
||||||
<ToolButton
|
|
||||||
key="import"
|
|
||||||
type="button"
|
|
||||||
title={t("buttons.load")}
|
|
||||||
aria-label={t("buttons.load")}
|
|
||||||
icon={load}
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
await library.updateLibrary({
|
|
||||||
libraryItems: fileOpen({
|
|
||||||
description: "Excalidraw library files",
|
|
||||||
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
|
|
||||||
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
|
|
||||||
/*
|
|
||||||
extensions: [".json", ".excalidrawlib"],
|
|
||||||
*/
|
|
||||||
}),
|
|
||||||
merge: true,
|
|
||||||
openLibraryMenu: true,
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error?.name === "AbortError") {
|
|
||||||
console.warn(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setAppState({ errorMessage: t("errors.importLibraryError") });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="library-actions--load"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!!items.length && (
|
|
||||||
<>
|
|
||||||
<ToolButton
|
|
||||||
key="export"
|
|
||||||
type="button"
|
|
||||||
title={t("buttons.export")}
|
|
||||||
aria-label={t("buttons.export")}
|
|
||||||
icon={exportToFileIcon}
|
|
||||||
onClick={async () => {
|
|
||||||
const libraryItems = itemsSelected
|
|
||||||
? items
|
|
||||||
: await library.getLatestLibrary();
|
|
||||||
saveLibraryAsJSON(libraryItems)
|
|
||||||
.catch(muteFSAbortError)
|
|
||||||
.catch((error) => {
|
|
||||||
setAppState({ errorMessage: error.message });
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="library-actions--export"
|
|
||||||
>
|
|
||||||
{selectedItems.length > 0 && (
|
|
||||||
<span className="library-actions-counter">
|
|
||||||
{selectedItems.length}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</ToolButton>
|
|
||||||
<ToolButton
|
|
||||||
key="reset"
|
|
||||||
type="button"
|
|
||||||
title={resetLabel}
|
|
||||||
aria-label={resetLabel}
|
|
||||||
icon={trash}
|
|
||||||
onClick={() => setShowRemoveLibAlert(true)}
|
|
||||||
className="library-actions--remove"
|
|
||||||
>
|
|
||||||
{selectedItems.length > 0 && (
|
|
||||||
<span className="library-actions-counter">
|
|
||||||
{selectedItems.length}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</ToolButton>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{itemsSelected && (
|
|
||||||
<Tooltip label={t("hints.publishLibrary")}>
|
|
||||||
<ToolButton
|
|
||||||
type="button"
|
|
||||||
aria-label={t("buttons.publishLibrary")}
|
|
||||||
label={t("buttons.publishLibrary")}
|
|
||||||
icon={publishIcon}
|
|
||||||
className="library-actions--publish"
|
|
||||||
onClick={onPublish}
|
|
||||||
>
|
|
||||||
{!device.isMobile && <label>{t("buttons.publishLibrary")}</label>}
|
|
||||||
{selectedItems.length > 0 && (
|
|
||||||
<span className="library-actions-counter">
|
|
||||||
{selectedItems.length}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</ToolButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{device.isMobile && (
|
|
||||||
<div className="library-menu-browse-button--mobile">
|
|
||||||
<a
|
|
||||||
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
|
|
||||||
window.name || "_blank"
|
|
||||||
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
|
|
||||||
VERSIONS.excalidrawLibrary
|
|
||||||
}`}
|
|
||||||
target="_excalidraw_libraries"
|
|
||||||
>
|
|
||||||
{t("labels.libraries")}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CELLS_PER_ROW = device.isMobile && !device.isSmScreen ? 6 : 4;
|
|
||||||
|
|
||||||
const referrer =
|
|
||||||
libraryReturnUrl || window.location.origin + window.location.pathname;
|
|
||||||
|
|
||||||
const [lastSelectedItem, setLastSelectedItem] = useState<
|
const [lastSelectedItem, setLastSelectedItem] = useState<
|
||||||
LibraryItem["id"] | null
|
LibraryItem["id"] | null
|
||||||
>(null);
|
>(null);
|
||||||
@@ -297,7 +106,6 @@ const LibraryMenuItems = ({
|
|||||||
<Stack.Col key={params.key}>
|
<Stack.Col key={params.key}>
|
||||||
<LibraryUnit
|
<LibraryUnit
|
||||||
elements={params.item?.elements}
|
elements={params.item?.elements}
|
||||||
files={files}
|
|
||||||
isPending={!params.item?.id && !!params.item?.elements}
|
isPending={!params.item?.id && !!params.item?.elements}
|
||||||
onClick={params.onClick || (() => {})}
|
onClick={params.onClick || (() => {})}
|
||||||
id={params.item?.id || null}
|
id={params.item?.id || null}
|
||||||
@@ -373,56 +181,21 @@ const LibraryMenuItems = ({
|
|||||||
(item) => item.status === "published",
|
(item) => item.status === "published",
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderLibraryHeader = () => {
|
return (
|
||||||
return (
|
<div
|
||||||
<>
|
className="library-menu-items-container"
|
||||||
<div className="layer-ui__library-header" key="library-header">
|
style={
|
||||||
{renderLibraryActions()}
|
publishedItems.length || unpublishedItems.length
|
||||||
{device.canDeviceFitSidebar && (
|
? {
|
||||||
<>
|
flex: "1 1 0",
|
||||||
<div className="layer-ui__sidebar-lock-button">
|
overflowY: "auto",
|
||||||
<SidebarLockButton
|
}
|
||||||
checked={appState.isLibraryMenuDocked}
|
: {
|
||||||
onChange={() => {
|
marginBottom: "2rem",
|
||||||
document
|
flex: 0,
|
||||||
.querySelector(".layer-ui__wrapper")
|
}
|
||||||
?.classList.add("animate");
|
}
|
||||||
const nextState = !appState.isLibraryMenuDocked;
|
>
|
||||||
setAppState({
|
|
||||||
isLibraryMenuDocked: nextState,
|
|
||||||
});
|
|
||||||
trackEvent(
|
|
||||||
"library",
|
|
||||||
`toggleLibraryDock (${nextState ? "dock" : "undock"})`,
|
|
||||||
`sidebar (${device.isMobile ? "mobile" : "desktop"})`,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!device.isMobile && (
|
|
||||||
<div className="ToolIcon__icon__close">
|
|
||||||
<button
|
|
||||||
className="Modal__close"
|
|
||||||
onClick={() =>
|
|
||||||
setAppState({
|
|
||||||
isLibraryOpen: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
aria-label={t("buttons.close")}
|
|
||||||
>
|
|
||||||
{close}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderLibraryMenuItems = () => {
|
|
||||||
return (
|
|
||||||
<Stack.Col
|
<Stack.Col
|
||||||
className="library-menu-items-container__items"
|
className="library-menu-items-container__items"
|
||||||
align="start"
|
align="start"
|
||||||
@@ -494,8 +267,8 @@ const LibraryMenuItems = ({
|
|||||||
|
|
||||||
<>
|
<>
|
||||||
{(publishedItems.length > 0 ||
|
{(publishedItems.length > 0 ||
|
||||||
(!device.isMobile &&
|
pendingElements.length > 0 ||
|
||||||
(pendingElements.length > 0 || unpublishedItems.length > 0))) && (
|
unpublishedItems.length > 0) && (
|
||||||
<div className="separator">{t("labels.excalidrawLib")}</div>
|
<div className="separator">{t("labels.excalidrawLib")}</div>
|
||||||
)}
|
)}
|
||||||
{publishedItems.length > 0 ? (
|
{publishedItems.length > 0 ? (
|
||||||
@@ -517,41 +290,6 @@ const LibraryMenuItems = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderLibraryFooter = () => {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
className="library-menu-browse-button"
|
|
||||||
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
|
|
||||||
window.name || "_blank"
|
|
||||||
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
|
|
||||||
VERSIONS.excalidrawLibrary
|
|
||||||
}`}
|
|
||||||
target="_excalidraw_libraries"
|
|
||||||
>
|
|
||||||
{t("labels.libraries")}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="library-menu-items-container"
|
|
||||||
style={
|
|
||||||
device.isMobile
|
|
||||||
? {
|
|
||||||
minHeight: "200px",
|
|
||||||
maxHeight: "70vh",
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{showRemoveLibAlert && renderRemoveLibAlert()}
|
|
||||||
{renderLibraryHeader()}
|
|
||||||
{renderLibraryMenuItems()}
|
|
||||||
{!device.isMobile && renderLibraryFooter()}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -3,7 +3,7 @@ import oc from "open-color";
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useDevice } from "../components/App";
|
import { useDevice } from "../components/App";
|
||||||
import { exportToSvg } from "../scene/export";
|
import { exportToSvg } from "../scene/export";
|
||||||
import { BinaryFiles, LibraryItem } from "../types";
|
import { LibraryItem } from "../types";
|
||||||
import "./LibraryUnit.scss";
|
import "./LibraryUnit.scss";
|
||||||
import { CheckboxItem } from "./CheckboxItem";
|
import { CheckboxItem } from "./CheckboxItem";
|
||||||
|
|
||||||
@@ -23,7 +23,6 @@ const PLUS_ICON = (
|
|||||||
export const LibraryUnit = ({
|
export const LibraryUnit = ({
|
||||||
id,
|
id,
|
||||||
elements,
|
elements,
|
||||||
files,
|
|
||||||
isPending,
|
isPending,
|
||||||
onClick,
|
onClick,
|
||||||
selected,
|
selected,
|
||||||
@@ -32,7 +31,6 @@ export const LibraryUnit = ({
|
|||||||
}: {
|
}: {
|
||||||
id: LibraryItem["id"] | /** for pending item */ null;
|
id: LibraryItem["id"] | /** for pending item */ null;
|
||||||
elements?: LibraryItem["elements"];
|
elements?: LibraryItem["elements"];
|
||||||
files: BinaryFiles;
|
|
||||||
isPending?: boolean;
|
isPending?: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
@@ -56,7 +54,7 @@ export const LibraryUnit = ({
|
|||||||
exportBackground: false,
|
exportBackground: false,
|
||||||
viewBackgroundColor: oc.white,
|
viewBackgroundColor: oc.white,
|
||||||
},
|
},
|
||||||
files,
|
null,
|
||||||
);
|
);
|
||||||
node.innerHTML = svg.outerHTML;
|
node.innerHTML = svg.outerHTML;
|
||||||
})();
|
})();
|
||||||
@@ -64,7 +62,7 @@ export const LibraryUnit = ({
|
|||||||
return () => {
|
return () => {
|
||||||
node.innerHTML = "";
|
node.innerHTML = "";
|
||||||
};
|
};
|
||||||
}, [elements, files]);
|
}, [elements]);
|
||||||
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const isMobile = useDevice().isMobile;
|
const isMobile = useDevice().isMobile;
|
||||||
|
@@ -1,8 +1,14 @@
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import Spinner from "./Spinner";
|
import Spinner from "./Spinner";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { THEME } from "../constants";
|
||||||
|
import { Theme } from "../element/types";
|
||||||
|
|
||||||
export const LoadingMessage: React.FC<{ delay?: number }> = ({ delay }) => {
|
export const LoadingMessage: React.FC<{ delay?: number; theme?: Theme }> = ({
|
||||||
|
delay,
|
||||||
|
theme,
|
||||||
|
}) => {
|
||||||
const [isWaiting, setIsWaiting] = useState(!!delay);
|
const [isWaiting, setIsWaiting] = useState(!!delay);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -20,7 +26,11 @@ export const LoadingMessage: React.FC<{ delay?: number }> = ({ delay }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="LoadingMessage">
|
<div
|
||||||
|
className={clsx("LoadingMessage", {
|
||||||
|
"LoadingMessage--dark": theme === THEME.DARK,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { AppState } from "../types";
|
import { AppState, Device, ExcalidrawProps } from "../types";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
@@ -18,6 +18,8 @@ import { UserList } from "./UserList";
|
|||||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||||
import { LibraryButton } from "./LibraryButton";
|
import { LibraryButton } from "./LibraryButton";
|
||||||
import { PenModeButton } from "./PenModeButton";
|
import { PenModeButton } from "./PenModeButton";
|
||||||
|
import { Stats } from "./Stats";
|
||||||
|
import { actionToggleStats } from "../actions";
|
||||||
|
|
||||||
type MobileMenuProps = {
|
type MobileMenuProps = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
@@ -26,7 +28,6 @@ type MobileMenuProps = {
|
|||||||
renderImageExportDialog: () => React.ReactNode;
|
renderImageExportDialog: () => React.ReactNode;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
libraryMenu: JSX.Element | null;
|
|
||||||
onCollabButtonClick?: () => void;
|
onCollabButtonClick?: () => void;
|
||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
onPenModeToggle: () => void;
|
onPenModeToggle: () => void;
|
||||||
@@ -36,20 +37,19 @@ type MobileMenuProps = {
|
|||||||
isMobile: boolean,
|
isMobile: boolean,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => JSX.Element | null;
|
) => JSX.Element | null;
|
||||||
viewModeEnabled: boolean;
|
|
||||||
showThemeBtn: boolean;
|
|
||||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||||
renderTopRightUI?: (
|
renderTopRightUI?: (
|
||||||
isMobile: boolean,
|
isMobile: boolean,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => JSX.Element | null;
|
) => JSX.Element | null;
|
||||||
renderStats: () => JSX.Element | null;
|
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||||
|
renderSidebars: () => JSX.Element | null;
|
||||||
|
device: Device;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MobileMenu = ({
|
export const MobileMenu = ({
|
||||||
appState,
|
appState,
|
||||||
elements,
|
elements,
|
||||||
libraryMenu,
|
|
||||||
actionManager,
|
actionManager,
|
||||||
renderJSONExportDialog,
|
renderJSONExportDialog,
|
||||||
renderImageExportDialog,
|
renderImageExportDialog,
|
||||||
@@ -60,17 +60,17 @@ export const MobileMenu = ({
|
|||||||
canvas,
|
canvas,
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
renderCustomFooter,
|
renderCustomFooter,
|
||||||
viewModeEnabled,
|
|
||||||
showThemeBtn,
|
|
||||||
onImageAction,
|
onImageAction,
|
||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
renderStats,
|
renderCustomStats,
|
||||||
|
renderSidebars,
|
||||||
|
device,
|
||||||
}: MobileMenuProps) => {
|
}: MobileMenuProps) => {
|
||||||
const renderToolbar = () => {
|
const renderToolbar = () => {
|
||||||
return (
|
return (
|
||||||
<FixedSideContainer side="top" className="App-top-bar">
|
<FixedSideContainer side="top" className="App-top-bar">
|
||||||
<Section heading="shapes">
|
<Section heading="shapes">
|
||||||
{(heading) => (
|
{(heading: React.ReactNode) => (
|
||||||
<Stack.Col gap={4} align="center">
|
<Stack.Col gap={4} align="center">
|
||||||
<Stack.Row gap={1} className="App-toolbar-container">
|
<Stack.Row gap={1} className="App-toolbar-container">
|
||||||
<Island padding={1} className="App-toolbar">
|
<Island padding={1} className="App-toolbar">
|
||||||
@@ -109,11 +109,15 @@ export const MobileMenu = ({
|
|||||||
penDetected={appState.penDetected}
|
penDetected={appState.penDetected}
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
{libraryMenu}
|
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
<HintViewer appState={appState} elements={elements} isMobile={true} />
|
<HintViewer
|
||||||
|
appState={appState}
|
||||||
|
elements={elements}
|
||||||
|
isMobile={true}
|
||||||
|
device={device}
|
||||||
|
/>
|
||||||
</FixedSideContainer>
|
</FixedSideContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -121,11 +125,10 @@ export const MobileMenu = ({
|
|||||||
const renderAppToolbar = () => {
|
const renderAppToolbar = () => {
|
||||||
// Render eraser conditionally in mobile
|
// Render eraser conditionally in mobile
|
||||||
const showEraser =
|
const showEraser =
|
||||||
!appState.viewModeEnabled &&
|
|
||||||
!appState.editingElement &&
|
!appState.editingElement &&
|
||||||
getSelectedElements(elements, appState).length === 0;
|
getSelectedElements(elements, appState).length === 0;
|
||||||
|
|
||||||
if (viewModeEnabled) {
|
if (appState.viewModeEnabled) {
|
||||||
return (
|
return (
|
||||||
<div className="App-toolbar-content">
|
<div className="App-toolbar-content">
|
||||||
{actionManager.renderAction("toggleCanvasMenu")}
|
{actionManager.renderAction("toggleCanvasMenu")}
|
||||||
@@ -140,18 +143,18 @@ export const MobileMenu = ({
|
|||||||
|
|
||||||
{actionManager.renderAction("undo")}
|
{actionManager.renderAction("undo")}
|
||||||
{actionManager.renderAction("redo")}
|
{actionManager.renderAction("redo")}
|
||||||
{showEraser && actionManager.renderAction("eraser")}
|
{showEraser
|
||||||
|
? actionManager.renderAction("eraser")
|
||||||
{actionManager.renderAction(
|
: actionManager.renderAction(
|
||||||
appState.multiElement ? "finalize" : "duplicateSelection",
|
appState.multiElement ? "finalize" : "duplicateSelection",
|
||||||
)}
|
)}
|
||||||
{actionManager.renderAction("deleteSelectedElements")}
|
{actionManager.renderAction("deleteSelectedElements")}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderCanvasActions = () => {
|
const renderCanvasActions = () => {
|
||||||
if (viewModeEnabled) {
|
if (appState.viewModeEnabled) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderJSONExportDialog()}
|
{renderJSONExportDialog()}
|
||||||
@@ -172,21 +175,25 @@ export const MobileMenu = ({
|
|||||||
onClick={onCollabButtonClick}
|
onClick={onCollabButtonClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{
|
{<BackgroundPickerAndDarkModeToggle actionManager={actionManager} />}
|
||||||
<BackgroundPickerAndDarkModeToggle
|
|
||||||
actionManager={actionManager}
|
|
||||||
appState={appState}
|
|
||||||
setAppState={setAppState}
|
|
||||||
showThemeBtn={showThemeBtn}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!viewModeEnabled && renderToolbar()}
|
{renderSidebars()}
|
||||||
{renderStats()}
|
{!appState.viewModeEnabled && renderToolbar()}
|
||||||
|
{!appState.openMenu && appState.showStats && (
|
||||||
|
<Stats
|
||||||
|
appState={appState}
|
||||||
|
setAppState={setAppState}
|
||||||
|
elements={elements}
|
||||||
|
onClose={() => {
|
||||||
|
actionManager.executeAction(actionToggleStats);
|
||||||
|
}}
|
||||||
|
renderCustomStats={renderCustomStats}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className="App-bottom-bar"
|
className="App-bottom-bar"
|
||||||
style={{
|
style={{
|
||||||
@@ -216,31 +223,32 @@ export const MobileMenu = ({
|
|||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
) : appState.openMenu === "shape" &&
|
) : appState.openMenu === "shape" &&
|
||||||
!viewModeEnabled &&
|
!appState.viewModeEnabled &&
|
||||||
showSelectedShapeActions(appState, elements) ? (
|
showSelectedShapeActions(appState, elements) ? (
|
||||||
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
||||||
<SelectedShapeActions
|
<SelectedShapeActions
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
renderAction={actionManager.renderAction}
|
renderAction={actionManager.renderAction}
|
||||||
activeTool={appState.activeTool.type}
|
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
) : null}
|
) : null}
|
||||||
<footer className="App-toolbar">
|
<footer className="App-toolbar">
|
||||||
{renderAppToolbar()}
|
{renderAppToolbar()}
|
||||||
{appState.scrolledOutside && !appState.openMenu && (
|
{appState.scrolledOutside &&
|
||||||
<button
|
!appState.openMenu &&
|
||||||
className="scroll-back-to-content"
|
appState.openSidebar !== "library" && (
|
||||||
onClick={() => {
|
<button
|
||||||
setAppState({
|
className="scroll-back-to-content"
|
||||||
...calculateScrollCenter(elements, appState, canvas),
|
onClick={() => {
|
||||||
});
|
setAppState({
|
||||||
}}
|
...calculateScrollCenter(elements, appState, canvas),
|
||||||
>
|
});
|
||||||
{t("buttons.scrollBackToContent")}
|
}}
|
||||||
</button>
|
>
|
||||||
)}
|
{t("buttons.scrollBackToContent")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</footer>
|
</footer>
|
||||||
</Island>
|
</Island>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -8,7 +8,7 @@ import { useExcalidrawContainer, useDevice } from "./App";
|
|||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { THEME } from "../constants";
|
import { THEME } from "../constants";
|
||||||
|
|
||||||
export const Modal = (props: {
|
export const Modal: React.FC<{
|
||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
@@ -16,7 +16,7 @@ export const Modal = (props: {
|
|||||||
labelledBy: string;
|
labelledBy: string;
|
||||||
theme?: AppState["theme"];
|
theme?: AppState["theme"];
|
||||||
closeOnClickOutside?: boolean;
|
closeOnClickOutside?: boolean;
|
||||||
}) => {
|
}> = (props) => {
|
||||||
const { theme = THEME.LIGHT, closeOnClickOutside = true } = props;
|
const { theme = THEME.LIGHT, closeOnClickOutside = true } = props;
|
||||||
const modalRoot = useBodyRoot(theme);
|
const modalRoot = useBodyRoot(theme);
|
||||||
|
|
||||||
|
@@ -46,7 +46,7 @@ const ChartPreviewBtn = (props: {
|
|||||||
},
|
},
|
||||||
null, // files
|
null, // files
|
||||||
);
|
);
|
||||||
|
previewNode.replaceChildren();
|
||||||
previewNode.appendChild(svg);
|
previewNode.appendChild(svg);
|
||||||
|
|
||||||
if (props.selected) {
|
if (props.selected) {
|
||||||
@@ -55,7 +55,7 @@ const ChartPreviewBtn = (props: {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
previewNode.removeChild(svg);
|
previewNode.replaceChildren();
|
||||||
};
|
};
|
||||||
}, [props.spreadsheet, props.chartType, props.selected]);
|
}, [props.spreadsheet, props.chartType, props.selected]);
|
||||||
|
|
||||||
|
@@ -2,5 +2,6 @@
|
|||||||
.popover {
|
.popover {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
padding: 5px 0 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -69,12 +69,26 @@ export const Popover = ({
|
|||||||
if (fitInViewport && popoverRef.current) {
|
if (fitInViewport && popoverRef.current) {
|
||||||
const element = popoverRef.current;
|
const element = popoverRef.current;
|
||||||
const { x, y, width, height } = element.getBoundingClientRect();
|
const { x, y, width, height } = element.getBoundingClientRect();
|
||||||
|
|
||||||
|
//Position correctly when clicked on rightmost part or the bottom part of viewport
|
||||||
if (x + width - offsetLeft > viewportWidth) {
|
if (x + width - offsetLeft > viewportWidth) {
|
||||||
element.style.left = `${viewportWidth - width}px`;
|
element.style.left = `${viewportWidth - width - 10}px`;
|
||||||
}
|
}
|
||||||
if (y + height - offsetTop > viewportHeight) {
|
if (y + height - offsetTop > viewportHeight) {
|
||||||
element.style.top = `${viewportHeight - height}px`;
|
element.style.top = `${viewportHeight - height}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Resize to fit viewport on smaller screens
|
||||||
|
if (height >= viewportHeight) {
|
||||||
|
element.style.height = `${viewportHeight - 20}px`;
|
||||||
|
element.style.top = "10px";
|
||||||
|
element.style.overflowY = "scroll";
|
||||||
|
}
|
||||||
|
if (width >= viewportWidth) {
|
||||||
|
element.style.width = `${viewportWidth}px`;
|
||||||
|
element.style.left = "0px";
|
||||||
|
element.style.overflowX = "scroll";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [fitInViewport, viewportWidth, viewportHeight, offsetLeft, offsetTop]);
|
}, [fitInViewport, viewportWidth, viewportHeight, offsetLeft, offsetTop]);
|
||||||
|
|
||||||
|
@@ -2,12 +2,11 @@ import React from "react";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useExcalidrawContainer } from "./App";
|
import { useExcalidrawContainer } from "./App";
|
||||||
|
|
||||||
interface SectionProps extends React.HTMLProps<HTMLElement> {
|
export const Section: React.FC<{
|
||||||
heading: string;
|
heading: string;
|
||||||
children: React.ReactNode | ((header: React.ReactNode) => React.ReactNode);
|
children?: React.ReactNode | ((heading: React.ReactNode) => React.ReactNode);
|
||||||
}
|
className?: string;
|
||||||
|
}> = ({ heading, children, ...props }) => {
|
||||||
export const Section = ({ heading, children, ...props }: SectionProps) => {
|
|
||||||
const { id } = useExcalidrawContainer();
|
const { id } = useExcalidrawContainer();
|
||||||
const header = (
|
const header = (
|
||||||
<h2 className="visually-hidden" id={`${id}-${heading}-title`}>
|
<h2 className="visually-hidden" id={`${id}-${heading}-title`}>
|
||||||
|
89
src/components/Sidebar/Sidebar.scss
Normal file
89
src/components/Sidebar/Sidebar.scss
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
@import "open-color/open-color";
|
||||||
|
@import "../../css/variables.module";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.layer-ui__sidebar {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--sat);
|
||||||
|
bottom: var(--sab);
|
||||||
|
right: var(--sar);
|
||||||
|
z-index: 5;
|
||||||
|
|
||||||
|
box-shadow: var(--shadow-island);
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
margin: var(--space-factor);
|
||||||
|
width: calc(#{$right-sidebar-width} - var(--space-factor) * 2);
|
||||||
|
|
||||||
|
padding: 0.5rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.Island {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToolIcon__icon {
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToolIcon__icon__close {
|
||||||
|
.Modal__close {
|
||||||
|
width: calc(var(--space-factor) * 7);
|
||||||
|
height: calc(var(--space-factor) * 7);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Island {
|
||||||
|
--padding: 0;
|
||||||
|
background-color: var(--island-bg-color);
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
padding: calc(var(--padding) * var(--space-factor));
|
||||||
|
position: relative;
|
||||||
|
transition: box-shadow 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-ui__sidebar__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
margin: 2px 0 15px 0;
|
||||||
|
&:empty {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
// 2px from the left to account for focus border of left-most button
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-ui__sidebar__header__buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-ui__sidebar-dock-button {
|
||||||
|
@include toolbarButtonColorStates;
|
||||||
|
margin-right: 0.2rem;
|
||||||
|
|
||||||
|
.ToolIcon_type_floating .ToolIcon__icon {
|
||||||
|
width: calc(var(--space-factor) * 7);
|
||||||
|
height: calc(var(--space-factor) * 7);
|
||||||
|
svg {
|
||||||
|
// mirror
|
||||||
|
transform: scale(-1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToolIcon_type_checkbox {
|
||||||
|
&:not(.ToolIcon_toggle_opaque):checked + .ToolIcon__icon {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
355
src/components/Sidebar/Sidebar.test.tsx
Normal file
355
src/components/Sidebar/Sidebar.test.tsx
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Excalidraw, Sidebar } from "../../packages/excalidraw/index";
|
||||||
|
import {
|
||||||
|
act,
|
||||||
|
fireEvent,
|
||||||
|
queryAllByTestId,
|
||||||
|
queryByTestId,
|
||||||
|
render,
|
||||||
|
waitFor,
|
||||||
|
withExcalidrawDimensions,
|
||||||
|
} from "../../tests/test-utils";
|
||||||
|
|
||||||
|
describe("Sidebar", () => {
|
||||||
|
it("should render custom sidebar", async () => {
|
||||||
|
const { container } = await render(
|
||||||
|
<Excalidraw
|
||||||
|
initialData={{ appState: { openSidebar: "customSidebar" } }}
|
||||||
|
renderSidebar={() => (
|
||||||
|
<Sidebar>
|
||||||
|
<div id="test-sidebar-content">42</div>
|
||||||
|
</Sidebar>
|
||||||
|
)}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const node = container.querySelector("#test-sidebar-content");
|
||||||
|
expect(node).not.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render custom sidebar header", async () => {
|
||||||
|
const { container } = await render(
|
||||||
|
<Excalidraw
|
||||||
|
initialData={{ appState: { openSidebar: "customSidebar" } }}
|
||||||
|
renderSidebar={() => (
|
||||||
|
<Sidebar>
|
||||||
|
<Sidebar.Header>
|
||||||
|
<div id="test-sidebar-header-content">42</div>
|
||||||
|
</Sidebar.Header>
|
||||||
|
</Sidebar>
|
||||||
|
)}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const node = container.querySelector("#test-sidebar-header-content");
|
||||||
|
expect(node).not.toBe(null);
|
||||||
|
// make sure we don't render the default fallback header,
|
||||||
|
// just the custom one
|
||||||
|
expect(queryAllByTestId(container, "sidebar-header").length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render only one sidebar and prefer the custom one", async () => {
|
||||||
|
const { container } = await render(
|
||||||
|
<Excalidraw
|
||||||
|
initialData={{ appState: { openSidebar: "customSidebar" } }}
|
||||||
|
renderSidebar={() => (
|
||||||
|
<Sidebar>
|
||||||
|
<div id="test-sidebar-content">42</div>
|
||||||
|
</Sidebar>
|
||||||
|
)}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// make sure the custom sidebar is rendered
|
||||||
|
const node = container.querySelector("#test-sidebar-content");
|
||||||
|
expect(node).not.toBe(null);
|
||||||
|
|
||||||
|
// make sure only one sidebar is rendered
|
||||||
|
const sidebars = container.querySelectorAll(".layer-ui__sidebar");
|
||||||
|
expect(sidebars.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should always render custom sidebar with close button & close on click", async () => {
|
||||||
|
const onClose = jest.fn();
|
||||||
|
const CustomExcalidraw = () => {
|
||||||
|
return (
|
||||||
|
<Excalidraw
|
||||||
|
initialData={{ appState: { openSidebar: "customSidebar" } }}
|
||||||
|
renderSidebar={() => (
|
||||||
|
<Sidebar className="test-sidebar" onClose={onClose}>
|
||||||
|
hello
|
||||||
|
</Sidebar>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { container } = await render(<CustomExcalidraw />);
|
||||||
|
|
||||||
|
const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
|
||||||
|
expect(sidebar).not.toBe(null);
|
||||||
|
const closeButton = queryByTestId(sidebar!, "sidebar-close");
|
||||||
|
expect(closeButton).not.toBe(null);
|
||||||
|
|
||||||
|
fireEvent.click(closeButton!.querySelector("button")!);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(container.querySelector<HTMLElement>(".test-sidebar")).toBe(null);
|
||||||
|
expect(onClose).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render custom sidebar with dock (irrespective of onDock prop)", async () => {
|
||||||
|
const CustomExcalidraw = () => {
|
||||||
|
return (
|
||||||
|
<Excalidraw
|
||||||
|
initialData={{ appState: { openSidebar: "customSidebar" } }}
|
||||||
|
renderSidebar={() => (
|
||||||
|
<Sidebar className="test-sidebar">hello</Sidebar>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { container } = await render(<CustomExcalidraw />);
|
||||||
|
|
||||||
|
// should show dock button when the sidebar fits to be docked
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
await withExcalidrawDimensions({ width: 1920, height: 1080 }, () => {
|
||||||
|
const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
|
||||||
|
expect(sidebar).not.toBe(null);
|
||||||
|
const closeButton = queryByTestId(sidebar!, "sidebar-dock");
|
||||||
|
expect(closeButton).not.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// should not show dock button when the sidebar does not fit to be docked
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
await withExcalidrawDimensions({ width: 400, height: 1080 }, () => {
|
||||||
|
const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
|
||||||
|
expect(sidebar).not.toBe(null);
|
||||||
|
const closeButton = queryByTestId(sidebar!, "sidebar-dock");
|
||||||
|
expect(closeButton).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support controlled docking", async () => {
|
||||||
|
let _setDockable: (dockable: boolean) => void = null!;
|
||||||
|
|
||||||
|
const CustomExcalidraw = () => {
|
||||||
|
const [dockable, setDockable] = React.useState(false);
|
||||||
|
_setDockable = setDockable;
|
||||||
|
return (
|
||||||
|
<Excalidraw
|
||||||
|
initialData={{ appState: { openSidebar: "customSidebar" } }}
|
||||||
|
renderSidebar={() => (
|
||||||
|
<Sidebar
|
||||||
|
className="test-sidebar"
|
||||||
|
docked={false}
|
||||||
|
dockable={dockable}
|
||||||
|
>
|
||||||
|
hello
|
||||||
|
</Sidebar>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { container } = await render(<CustomExcalidraw />);
|
||||||
|
|
||||||
|
await withExcalidrawDimensions({ width: 1920, height: 1080 }, async () => {
|
||||||
|
// should not show dock button when `dockable` is `false`
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
_setDockable(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
|
||||||
|
expect(sidebar).not.toBe(null);
|
||||||
|
const closeButton = queryByTestId(sidebar!, "sidebar-dock");
|
||||||
|
expect(closeButton).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// should show dock button when `dockable` is `true`, even if `docked`
|
||||||
|
// prop is set
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
_setDockable(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
|
||||||
|
expect(sidebar).not.toBe(null);
|
||||||
|
const closeButton = queryByTestId(sidebar!, "sidebar-dock");
|
||||||
|
expect(closeButton).not.toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support controlled docking", async () => {
|
||||||
|
let _setDocked: (docked?: boolean) => void = null!;
|
||||||
|
|
||||||
|
const CustomExcalidraw = () => {
|
||||||
|
const [docked, setDocked] = React.useState<boolean | undefined>();
|
||||||
|
_setDocked = setDocked;
|
||||||
|
return (
|
||||||
|
<Excalidraw
|
||||||
|
initialData={{ appState: { openSidebar: "customSidebar" } }}
|
||||||
|
renderSidebar={() => (
|
||||||
|
<Sidebar className="test-sidebar" docked={docked}>
|
||||||
|
hello
|
||||||
|
</Sidebar>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { container } = await render(<CustomExcalidraw />);
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
|
|
||||||
|
await withExcalidrawDimensions({ width: 1920, height: 1080 }, async () => {
|
||||||
|
const dockButton = await waitFor(() => {
|
||||||
|
const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
|
||||||
|
expect(sidebar).not.toBe(null);
|
||||||
|
const dockBotton = queryByTestId(sidebar!, "sidebar-dock");
|
||||||
|
expect(dockBotton).not.toBe(null);
|
||||||
|
return dockBotton!;
|
||||||
|
});
|
||||||
|
|
||||||
|
const dockButtonInput = dockButton.querySelector("input")!;
|
||||||
|
|
||||||
|
// should not show dock button when `dockable` is `false`
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
expect(h.state.isSidebarDocked).toBe(false);
|
||||||
|
|
||||||
|
fireEvent.click(dockButtonInput);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.state.isSidebarDocked).toBe(true);
|
||||||
|
expect(dockButtonInput).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(dockButtonInput);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.state.isSidebarDocked).toBe(false);
|
||||||
|
expect(dockButtonInput).not.toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
// shouldn't update `appState.isSidebarDocked` when the sidebar
|
||||||
|
// is controlled (`docked` prop is set), as host apps should handle
|
||||||
|
// the state themselves
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
_setDocked(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(dockButtonInput).toBeChecked();
|
||||||
|
expect(h.state.isSidebarDocked).toBe(false);
|
||||||
|
expect(dockButtonInput).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(dockButtonInput);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.state.isSidebarDocked).toBe(false);
|
||||||
|
expect(dockButtonInput).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
// the `appState.isSidebarDocked` should remain untouched when
|
||||||
|
// `props.docked` is set to `false`, and user toggles
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
_setDocked(false);
|
||||||
|
h.setState({ isSidebarDocked: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.state.isSidebarDocked).toBe(true);
|
||||||
|
expect(dockButtonInput).not.toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(dockButtonInput);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(dockButtonInput).not.toBeChecked();
|
||||||
|
expect(h.state.isSidebarDocked).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should toggle sidebar using props.toggleMenu()", async () => {
|
||||||
|
const { container } = await render(
|
||||||
|
<Excalidraw
|
||||||
|
renderSidebar={() => (
|
||||||
|
<Sidebar>
|
||||||
|
<div id="test-sidebar-content">42</div>
|
||||||
|
</Sidebar>
|
||||||
|
)}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// sidebar isn't rendered initially
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
await waitFor(() => {
|
||||||
|
const node = container.querySelector("#test-sidebar-content");
|
||||||
|
expect(node).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// toggle sidebar on
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
expect(window.h.app.toggleMenu("customSidebar")).toBe(true);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const node = container.querySelector("#test-sidebar-content");
|
||||||
|
expect(node).not.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// toggle sidebar off
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
expect(window.h.app.toggleMenu("customSidebar")).toBe(false);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const node = container.querySelector("#test-sidebar-content");
|
||||||
|
expect(node).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// force-toggle sidebar off (=> still hidden)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
expect(window.h.app.toggleMenu("customSidebar", false)).toBe(false);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const node = container.querySelector("#test-sidebar-content");
|
||||||
|
expect(node).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// force-toggle sidebar on
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
expect(window.h.app.toggleMenu("customSidebar", true)).toBe(true);
|
||||||
|
expect(window.h.app.toggleMenu("customSidebar", true)).toBe(true);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const node = container.querySelector("#test-sidebar-content");
|
||||||
|
expect(node).not.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// toggle library (= hide custom sidebar)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
expect(window.h.app.toggleMenu("library")).toBe(true);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const node = container.querySelector("#test-sidebar-content");
|
||||||
|
expect(node).toBe(null);
|
||||||
|
|
||||||
|
// make sure only one sidebar is rendered
|
||||||
|
const sidebars = container.querySelectorAll(".layer-ui__sidebar");
|
||||||
|
expect(sidebars.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
139
src/components/Sidebar/Sidebar.tsx
Normal file
139
src/components/Sidebar/Sidebar.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import {
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
forwardRef,
|
||||||
|
} from "react";
|
||||||
|
import { Island } from ".././Island";
|
||||||
|
import { atom, useAtom } from "jotai";
|
||||||
|
import { jotaiScope } from "../../jotai";
|
||||||
|
import {
|
||||||
|
SidebarPropsContext,
|
||||||
|
SidebarProps,
|
||||||
|
SidebarPropsContextValue,
|
||||||
|
} from "./common";
|
||||||
|
|
||||||
|
import { SidebarHeaderComponents } from "./SidebarHeader";
|
||||||
|
|
||||||
|
import "./Sidebar.scss";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { useExcalidrawSetAppState } from "../App";
|
||||||
|
import { updateObject } from "../../utils";
|
||||||
|
|
||||||
|
/** using a counter instead of boolean to handle race conditions where
|
||||||
|
* the host app may render (mount/unmount) multiple different sidebar */
|
||||||
|
export const hostSidebarCountersAtom = atom({ rendered: 0, docked: 0 });
|
||||||
|
|
||||||
|
export const Sidebar = Object.assign(
|
||||||
|
forwardRef(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
children,
|
||||||
|
onClose,
|
||||||
|
onDock,
|
||||||
|
docked,
|
||||||
|
dockable = true,
|
||||||
|
className,
|
||||||
|
__isInternal,
|
||||||
|
}: SidebarProps<{
|
||||||
|
// NOTE sidebars we use internally inside the editor must have this flag set.
|
||||||
|
// It indicates that this sidebar should have lower precedence over host
|
||||||
|
// sidebars, if both are open.
|
||||||
|
/** @private internal */
|
||||||
|
__isInternal?: boolean;
|
||||||
|
}>,
|
||||||
|
ref: React.ForwardedRef<HTMLDivElement>,
|
||||||
|
) => {
|
||||||
|
const [hostSidebarCounters, setHostSidebarCounters] = useAtom(
|
||||||
|
hostSidebarCountersAtom,
|
||||||
|
jotaiScope,
|
||||||
|
);
|
||||||
|
|
||||||
|
const setAppState = useExcalidrawSetAppState();
|
||||||
|
|
||||||
|
const [isDockedFallback, setIsDockedFallback] = useState(docked ?? false);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (docked === undefined) {
|
||||||
|
// ugly hack to get initial state out of AppState without subscribing
|
||||||
|
// to it as a whole (once we have granular subscriptions, we'll move
|
||||||
|
// to that)
|
||||||
|
//
|
||||||
|
// NOTE this means that is updated `state.isSidebarDocked` changes outside
|
||||||
|
// of this compoent, it won't be reflected here. Currently doesn't happen.
|
||||||
|
setAppState((state) => {
|
||||||
|
setIsDockedFallback(state.isSidebarDocked);
|
||||||
|
// bail from update
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [setAppState, docked]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!__isInternal) {
|
||||||
|
setHostSidebarCounters((s) => ({
|
||||||
|
rendered: s.rendered + 1,
|
||||||
|
docked: isDockedFallback ? s.docked + 1 : s.docked,
|
||||||
|
}));
|
||||||
|
return () => {
|
||||||
|
setHostSidebarCounters((s) => ({
|
||||||
|
rendered: s.rendered - 1,
|
||||||
|
docked: isDockedFallback ? s.docked - 1 : s.docked,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [__isInternal, setHostSidebarCounters, isDockedFallback]);
|
||||||
|
|
||||||
|
const onCloseRef = useRef(onClose);
|
||||||
|
onCloseRef.current = onClose;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
onCloseRef.current?.();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const headerPropsRef = useRef<SidebarPropsContextValue>({});
|
||||||
|
headerPropsRef.current.onClose = () => {
|
||||||
|
setAppState({ openSidebar: null });
|
||||||
|
};
|
||||||
|
headerPropsRef.current.onDock = (isDocked) => {
|
||||||
|
if (docked === undefined) {
|
||||||
|
setAppState({ isSidebarDocked: isDocked });
|
||||||
|
setIsDockedFallback(isDocked);
|
||||||
|
}
|
||||||
|
onDock?.(isDocked);
|
||||||
|
};
|
||||||
|
// renew the ref object if the following props change since we want to
|
||||||
|
// rerender. We can't pass down as component props manually because
|
||||||
|
// the <Sidebar.Header/> can be rendered upsream.
|
||||||
|
headerPropsRef.current = updateObject(headerPropsRef.current, {
|
||||||
|
docked: docked ?? isDockedFallback,
|
||||||
|
dockable,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hostSidebarCounters.rendered > 0 && __isInternal) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Island
|
||||||
|
padding={2}
|
||||||
|
className={clsx("layer-ui__sidebar", className)}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<SidebarPropsContext.Provider value={headerPropsRef.current}>
|
||||||
|
<SidebarHeaderComponents.Context>
|
||||||
|
<SidebarHeaderComponents.Component __isFallback />
|
||||||
|
{children}
|
||||||
|
</SidebarHeaderComponents.Context>
|
||||||
|
</SidebarPropsContext.Provider>
|
||||||
|
</Island>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
{
|
||||||
|
Header: SidebarHeaderComponents.Component,
|
||||||
|
},
|
||||||
|
);
|
95
src/components/Sidebar/SidebarHeader.tsx
Normal file
95
src/components/Sidebar/SidebarHeader.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { t } from "../../i18n";
|
||||||
|
import { useDevice } from "../App";
|
||||||
|
import { SidebarPropsContext } from "./common";
|
||||||
|
import { close } from "../icons";
|
||||||
|
import { withUpstreamOverride } from "../hoc/withUpstreamOverride";
|
||||||
|
import { Tooltip } from "../Tooltip";
|
||||||
|
|
||||||
|
const SIDE_LIBRARY_TOGGLE_ICON = (
|
||||||
|
<svg viewBox="0 0 24 24" fill="#ffffff">
|
||||||
|
<path d="M19 22H5a3 3 0 01-3-3V5a3 3 0 013-3h14a3 3 0 013 3v14a3 3 0 01-3 3zm0-18h-9v16h9a1.01 1.01 0 001-1V5a1.01 1.01 0 00-1-1z"></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SidebarDockButton = (props: {
|
||||||
|
checked: boolean;
|
||||||
|
onChange?(): void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="layer-ui__sidebar-dock-button" data-testid="sidebar-dock">
|
||||||
|
<Tooltip label={t("labels.sidebarLock")}>
|
||||||
|
<label
|
||||||
|
className={clsx(
|
||||||
|
"ToolIcon ToolIcon__lock ToolIcon_type_floating",
|
||||||
|
`ToolIcon_size_medium`,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="ToolIcon_type_checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
onChange={props.onChange}
|
||||||
|
checked={props.checked}
|
||||||
|
aria-label={t("labels.sidebarLock")}
|
||||||
|
/>{" "}
|
||||||
|
<div className="ToolIcon__icon" tabIndex={0}>
|
||||||
|
{SIDE_LIBRARY_TOGGLE_ICON}
|
||||||
|
</div>{" "}
|
||||||
|
</label>{" "}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _SidebarHeader: React.FC<{
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}> = ({ children, className }) => {
|
||||||
|
const device = useDevice();
|
||||||
|
const props = useContext(SidebarPropsContext);
|
||||||
|
|
||||||
|
const renderDockButton = !!(device.canDeviceFitSidebar && props.dockable);
|
||||||
|
const renderCloseButton = !!props.onClose;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx("layer-ui__sidebar__header", className)}
|
||||||
|
data-testid="sidebar-header"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{(renderDockButton || renderCloseButton) && (
|
||||||
|
<div className="layer-ui__sidebar__header__buttons">
|
||||||
|
{renderDockButton && (
|
||||||
|
<SidebarDockButton
|
||||||
|
checked={!!props.docked}
|
||||||
|
onChange={() => {
|
||||||
|
document
|
||||||
|
.querySelector(".layer-ui__wrapper")
|
||||||
|
?.classList.add("animate");
|
||||||
|
|
||||||
|
props.onDock?.(!props.docked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{renderCloseButton && (
|
||||||
|
<div className="ToolIcon__icon__close" data-testid="sidebar-close">
|
||||||
|
<button
|
||||||
|
className="Modal__close"
|
||||||
|
onClick={props.onClose}
|
||||||
|
aria-label={t("buttons.close")}
|
||||||
|
>
|
||||||
|
{close}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Context, Component] = withUpstreamOverride(_SidebarHeader);
|
||||||
|
|
||||||
|
/** @private */
|
||||||
|
export const SidebarHeaderComponents = { Context, Component };
|
22
src/components/Sidebar/common.ts
Normal file
22
src/components/Sidebar/common.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export type SidebarProps<P = {}> = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
/**
|
||||||
|
* Called on sidebar close (either by user action or by the editor).
|
||||||
|
*/
|
||||||
|
onClose?: () => void | boolean;
|
||||||
|
/** if not supplied, sidebar won't be dockable */
|
||||||
|
onDock?: (docked: boolean) => void;
|
||||||
|
docked?: boolean;
|
||||||
|
dockable?: boolean;
|
||||||
|
className?: string;
|
||||||
|
} & P;
|
||||||
|
|
||||||
|
export type SidebarPropsContextValue = Pick<
|
||||||
|
SidebarProps,
|
||||||
|
"onClose" | "onDock" | "docked" | "dockable"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const SidebarPropsContext =
|
||||||
|
React.createContext<SidebarPropsContextValue>({});
|
@@ -1,22 +0,0 @@
|
|||||||
@import "../css/variables.module";
|
|
||||||
|
|
||||||
.excalidraw {
|
|
||||||
.layer-ui__sidebar-lock-button {
|
|
||||||
@include toolbarButtonColorStates;
|
|
||||||
margin-right: 0.2rem;
|
|
||||||
}
|
|
||||||
.ToolIcon_type_floating .side_lock_icon {
|
|
||||||
width: calc(var(--space-factor) * 7);
|
|
||||||
height: calc(var(--space-factor) * 7);
|
|
||||||
svg {
|
|
||||||
// mirror
|
|
||||||
transform: scale(-1, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ToolIcon_type_checkbox {
|
|
||||||
&:not(.ToolIcon_toggle_opaque):checked + .side_lock_icon {
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,46 +0,0 @@
|
|||||||
import "./ToolIcon.scss";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { ToolButtonSize } from "./ToolButton";
|
|
||||||
import { t } from "../i18n";
|
|
||||||
import { Tooltip } from "./Tooltip";
|
|
||||||
|
|
||||||
import "./SidebarLockButton.scss";
|
|
||||||
|
|
||||||
type SidebarLockIconProps = {
|
|
||||||
checked: boolean;
|
|
||||||
onChange?(): void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_SIZE: ToolButtonSize = "medium";
|
|
||||||
|
|
||||||
const SIDE_LIBRARY_TOGGLE_ICON = (
|
|
||||||
<svg viewBox="0 0 24 24" fill="#ffffff">
|
|
||||||
<path d="M19 22H5a3 3 0 01-3-3V5a3 3 0 013-3h14a3 3 0 013 3v14a3 3 0 01-3 3zm0-18h-9v16h9a1.01 1.01 0 001-1V5a1.01 1.01 0 00-1-1z"></path>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const SidebarLockButton = (props: SidebarLockIconProps) => {
|
|
||||||
return (
|
|
||||||
<Tooltip label={t("labels.sidebarLock")}>
|
|
||||||
<label
|
|
||||||
className={clsx(
|
|
||||||
"ToolIcon ToolIcon__lock ToolIcon_type_floating",
|
|
||||||
`ToolIcon_size_${DEFAULT_SIZE}`,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
className="ToolIcon_type_checkbox"
|
|
||||||
type="checkbox"
|
|
||||||
onChange={props.onChange}
|
|
||||||
checked={props.checked}
|
|
||||||
aria-label={t("labels.sidebarLock")}
|
|
||||||
/>{" "}
|
|
||||||
<div className="ToolIcon__icon side_lock_icon" tabIndex={0}>
|
|
||||||
{SIDE_LIBRARY_TOGGLE_ICON}
|
|
||||||
</div>{" "}
|
|
||||||
</label>{" "}
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -2,7 +2,6 @@ import React from "react";
|
|||||||
import { getCommonBounds } from "../element/bounds";
|
import { getCommonBounds } from "../element/bounds";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useDevice } from "../components/App";
|
|
||||||
import { getTargetElements } from "../scene";
|
import { getTargetElements } from "../scene";
|
||||||
import { AppState, ExcalidrawProps } from "../types";
|
import { AppState, ExcalidrawProps } from "../types";
|
||||||
import { close } from "./icons";
|
import { close } from "./icons";
|
||||||
@@ -16,13 +15,10 @@ export const Stats = (props: {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
renderCustomStats: ExcalidrawProps["renderCustomStats"];
|
renderCustomStats: ExcalidrawProps["renderCustomStats"];
|
||||||
}) => {
|
}) => {
|
||||||
const device = useDevice();
|
|
||||||
const boundingBox = getCommonBounds(props.elements);
|
const boundingBox = getCommonBounds(props.elements);
|
||||||
const selectedElements = getTargetElements(props.elements, props.appState);
|
const selectedElements = getTargetElements(props.elements, props.appState);
|
||||||
const selectedBoundingBox = getCommonBounds(selectedElements);
|
const selectedBoundingBox = getCommonBounds(selectedElements);
|
||||||
if (device.isMobile && props.appState.openMenu) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div className="Stats">
|
<div className="Stats">
|
||||||
<Island padding={2}>
|
<Island padding={2}>
|
||||||
|
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.Toast {
|
.Toast {
|
||||||
|
$closeButtonSize: 1.2rem;
|
||||||
|
$closeButtonPadding: 0.4rem;
|
||||||
|
|
||||||
animation: fade-in 0.5s;
|
animation: fade-in 0.5s;
|
||||||
background-color: var(--button-gray-1);
|
background-color: var(--button-gray-1);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -15,11 +18,24 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
z-index: 999999;
|
z-index: 999999;
|
||||||
}
|
|
||||||
|
|
||||||
.Toast__message {
|
.Toast__message {
|
||||||
color: var(--popup-text-color);
|
padding: 0 $closeButtonSize + ($closeButtonPadding);
|
||||||
white-space: pre-wrap;
|
color: var(--popup-text-color);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: $closeButtonPadding;
|
||||||
|
|
||||||
|
.ToolIcon__icon {
|
||||||
|
width: $closeButtonSize;
|
||||||
|
height: $closeButtonSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
|
@@ -1,34 +1,59 @@
|
|||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import { TOAST_TIMEOUT } from "../constants";
|
import { close } from "./icons";
|
||||||
import "./Toast.scss";
|
import "./Toast.scss";
|
||||||
|
import { ToolButton } from "./ToolButton";
|
||||||
|
|
||||||
|
const DEFAULT_TOAST_TIMEOUT = 5000;
|
||||||
|
|
||||||
export const Toast = ({
|
export const Toast = ({
|
||||||
message,
|
message,
|
||||||
clearToast,
|
onClose,
|
||||||
|
closable = false,
|
||||||
|
// To prevent autoclose, pass duration as Infinity
|
||||||
|
duration = DEFAULT_TOAST_TIMEOUT,
|
||||||
}: {
|
}: {
|
||||||
message: string;
|
message: string;
|
||||||
clearToast: () => void;
|
onClose: () => void;
|
||||||
|
closable?: boolean;
|
||||||
|
duration?: number;
|
||||||
}) => {
|
}) => {
|
||||||
const timerRef = useRef<number>(0);
|
const timerRef = useRef<number>(0);
|
||||||
|
const shouldAutoClose = duration !== Infinity;
|
||||||
const scheduleTimeout = useCallback(
|
const scheduleTimeout = useCallback(() => {
|
||||||
() =>
|
if (!shouldAutoClose) {
|
||||||
(timerRef.current = window.setTimeout(() => clearToast(), TOAST_TIMEOUT)),
|
return;
|
||||||
[clearToast],
|
}
|
||||||
);
|
timerRef.current = window.setTimeout(() => onClose(), duration);
|
||||||
|
}, [onClose, duration, shouldAutoClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!shouldAutoClose) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
scheduleTimeout();
|
scheduleTimeout();
|
||||||
return () => clearTimeout(timerRef.current);
|
return () => clearTimeout(timerRef.current);
|
||||||
}, [scheduleTimeout, message]);
|
}, [scheduleTimeout, message, duration, shouldAutoClose]);
|
||||||
|
|
||||||
|
const onMouseEnter = shouldAutoClose
|
||||||
|
? () => clearTimeout(timerRef?.current)
|
||||||
|
: undefined;
|
||||||
|
const onMouseLeave = shouldAutoClose ? scheduleTimeout : undefined;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="Toast"
|
className="Toast"
|
||||||
onMouseEnter={() => clearTimeout(timerRef?.current)}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={scheduleTimeout}
|
onMouseLeave={onMouseLeave}
|
||||||
>
|
>
|
||||||
<p className="Toast__message">{message}</p>
|
<p className="Toast__message">{message}</p>
|
||||||
|
{closable && (
|
||||||
|
<ToolButton
|
||||||
|
icon={close}
|
||||||
|
aria-label="close"
|
||||||
|
type="icon"
|
||||||
|
onClick={onClose}
|
||||||
|
className="close"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user