This commit is contained in:
Panayiotis Lipiridis 2021-02-01 14:14:00 +02:00
commit 704986042d
118 changed files with 1834 additions and 1354 deletions

View File

@ -6,7 +6,7 @@ on:
- master - master
jobs: jobs:
build-docker: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View File

@ -13,10 +13,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Setup Node.js 12.x - name: Setup Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- name: Install dependencies - name: Install dependencies
run: | run: |

View File

@ -1,9 +1,11 @@
name: Cancel name: Cancel previous runs
on: [push]
on: push
jobs: jobs:
cancel: cancel:
name: "Cancel Previous Runs"
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 3 timeout-minutes: 3
steps: steps:
- uses: styfle/cancel-workflow-action@0.6.0 - uses: styfle/cancel-workflow-action@0.6.0

View File

@ -1,10 +1,6 @@
name: Lint name: Lint
on: on: push
push:
branches:
- master
pull_request:
jobs: jobs:
lint: lint:
@ -13,10 +9,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Setup Node.js 12.x - name: Setup Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- name: Install and lint - name: Install and lint
run: | run: |
@ -24,5 +20,3 @@ jobs:
npm run test:other npm run test:other
npm run test:code npm run test:code
npm run test:typecheck npm run test:typecheck
env:
CI: true

View File

@ -14,18 +14,18 @@ jobs:
with: with:
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }} token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
- name: Setup Node.js 12.x - name: Setup Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- name: Create report file - name: Create report file
run: | run: |
npm run locales-coverage npm run locales-coverage
FILE_CHANGED=$(git diff src/locales/percentages.json) FILE_CHANGED=$(git diff src/locales/percentages.json)
if [ ! -z "${FILE_CHANGED}" ]; then if [ ! -z "${FILE_CHANGED}" ]; then
git config --global user.name 'Kostas Bariotis' git config --global user.name 'Excalidraw Bot'
git config --global user.email 'konmpar@gmail.com' git config --global user.email 'bot@excalidraw.com'
git add src/locales/percentages.json git add src/locales/percentages.json
git commit -am "Auto commit: Calculate translation coverage" git commit -am "Auto commit: Calculate translation coverage"
git push git push

View File

@ -10,6 +10,7 @@ on:
jobs: jobs:
main: main:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: amannn/action-semantic-pull-request@v3.0.0 - uses: amannn/action-semantic-pull-request@v3.0.0
env: env:

View File

@ -8,13 +8,14 @@ on:
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1.0.0 - uses: actions/checkout@v1.0.0
- name: Setup Node.js 12.x - name: Setup Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- name: Install and build - name: Install and build
run: | run: |

View File

@ -1,10 +1,6 @@
name: Tests name: Tests
on: on: push
push:
branches:
- master
pull_request:
jobs: jobs:
test: test:
@ -13,14 +9,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Setup Node.js 12.x - name: Setup Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 14.x
- name: Install and test - name: Install and test
run: | run: |
npm ci npm ci
npm run test:app npm run test:app
env:
CI: true

195
package-lock.json generated
View File

@ -4,9 +4,9 @@
"lockfileVersion": 1, "lockfileVersion": 1,
"dependencies": { "dependencies": {
"@apidevtools/json-schema-ref-parser": { "@apidevtools/json-schema-ref-parser": {
"version": "9.0.6", "version": "9.0.7",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz", "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.7.tgz",
"integrity": "sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg==", "integrity": "sha512-QdwOGF1+eeyFh+17v2Tz626WX0nucd1iKOm6JUTUvCZdbolblCOOQCxGrQPY0f7jEhn36PiAWqZnsC2r5vmUWg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@jsdevtools/ono": "^7.1.3", "@jsdevtools/ono": "^7.1.3",
@ -1308,9 +1308,9 @@
"integrity": "sha512-Jj2xW+8+8XPfWGkv9HPv/uR+Qrmq37NPYT352wf7MvE9LrstpLVmFg3LqG6MCRr5miLAom5sen2gZ+iOhVDeRA==" "integrity": "sha512-Jj2xW+8+8XPfWGkv9HPv/uR+Qrmq37NPYT352wf7MvE9LrstpLVmFg3LqG6MCRr5miLAom5sen2gZ+iOhVDeRA=="
}, },
"@firebase/app": { "@firebase/app": {
"version": "0.6.13", "version": "0.6.14",
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.13.tgz", "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.14.tgz",
"integrity": "sha512-xGrJETzvCb89VYbGSHFHCW7O/y067HRxT7MGehUE1xMxdPVBDNayHnxEuKwzfGvXAjVmajXBKFlKxaCWpgSjCQ==", "integrity": "sha512-ZQKuiJ+fzr4tULgWoXbW+AZVTGsejOkSrlQ+zx78WiGKIubpFJLklnP3S0oYr/1nHzr4vaKuM4G8IL1Wv/+MpQ==",
"requires": { "requires": {
"@firebase/app-types": "0.6.1", "@firebase/app-types": "0.6.1",
"@firebase/component": "0.1.21", "@firebase/component": "0.1.21",
@ -1334,9 +1334,9 @@
"integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==" "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg=="
}, },
"@firebase/auth": { "@firebase/auth": {
"version": "0.16.1", "version": "0.16.2",
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.16.1.tgz", "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.16.2.tgz",
"integrity": "sha512-7juD7D/kaxNti/xa5G+ZGJJs+bdJUWOW0MlNBtXwiG+TjMh69EDmwJnQmmc9h/32QVvXt1qo1OGWOoMMpF/2Gg==", "integrity": "sha512-68TlDL0yh3kF8PiCzI8m8RWd/bf/xCLUsdz1NZ2Dwea0sp6e2WAhu0sem1GfhwuEwL+Ns4jCdX7qbe/OQlkVEA==",
"requires": { "requires": {
"@firebase/auth-types": "0.10.1" "@firebase/auth-types": "0.10.1"
} }
@ -1368,9 +1368,9 @@
} }
}, },
"@firebase/database": { "@firebase/database": {
"version": "0.8.3", "version": "0.9.1",
"resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.8.3.tgz", "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.9.1.tgz",
"integrity": "sha512-i29rr3kcPltIkA8La9M1lgsSxx9bfu5lCQ0T+tbJptZ3UpqpcL1NzCcZa24cJjiLgq3HQNPyLvUvCtcPSFDlRg==", "integrity": "sha512-JdxgNvniSZiAx+lrdAQxkCZOTv+UfdmhRm9JA4RTs4XOpvwzmRtJTAIGBn+9CWXUAkWkjt5CYHLmYysD7NGj6g==",
"requires": { "requires": {
"@firebase/auth-interop-types": "0.1.5", "@firebase/auth-interop-types": "0.1.5",
"@firebase/component": "0.1.21", "@firebase/component": "0.1.21",
@ -1405,9 +1405,9 @@
} }
}, },
"@firebase/firestore": { "@firebase/firestore": {
"version": "2.1.2", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-2.1.2.tgz", "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-2.1.4.tgz",
"integrity": "sha512-8yUdBLLr6UhE+IjPR+fxLBD0bDnEqF9GalohfURZeLQPaL3b+LtqqGCLvvXC4MKT0lJAHOV8J9LA6rHj8vI0/Q==", "integrity": "sha512-chSOvJyVoS7HmH7YOyqQP66wMwmsYNo2nPbFkrmQM/fRGXntNxXD1Greu1uts2hNyNeDLNrFHW5y7PlE3LAbwQ==",
"requires": { "requires": {
"@firebase/component": "0.1.21", "@firebase/component": "0.1.21",
"@firebase/firestore-types": "2.1.0", "@firebase/firestore-types": "2.1.0",
@ -2663,125 +2663,70 @@
} }
}, },
"@sentry/browser": { "@sentry/browser": {
"version": "5.30.0", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.30.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.0.1.tgz",
"integrity": "sha512-rOb58ZNVJWh1VuMuBG1mL9r54nZqKeaIlwSlvzJfc89vyfd7n6tQ1UXMN383QBz/MS5H5z44Hy5eE+7pCrYAfw==", "integrity": "sha512-iP8Bqxj4Ye8CXA4ja77buPZfXsKiZYUgHFzBQxVMihTHA8ZZLgBMPLQI6uFfHuJJW+1/yLzOf8BhvF2zknAebg==",
"requires": { "requires": {
"@sentry/core": "5.30.0", "@sentry/core": "6.0.1",
"@sentry/types": "5.30.0", "@sentry/types": "6.0.1",
"@sentry/utils": "5.30.0", "@sentry/utils": "6.0.1",
"tslib": "^1.9.3" "tslib": "^1.9.3"
},
"dependencies": {
"@sentry/types": {
"version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz",
"integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw=="
},
"@sentry/utils": {
"version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz",
"integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==",
"requires": {
"@sentry/types": "5.30.0",
"tslib": "^1.9.3"
}
}
} }
}, },
"@sentry/core": { "@sentry/core": {
"version": "5.30.0", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.30.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.0.1.tgz",
"integrity": "sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==", "integrity": "sha512-EoxgodyClasI8PA4GyU8Cp88W3R5ebpiLsE7fCcBcOU0DOBRkO8GAZ5IzfCDtYDJ50c9npivum5Oyj2wf8CXYw==",
"requires": { "requires": {
"@sentry/hub": "5.30.0", "@sentry/hub": "6.0.1",
"@sentry/minimal": "5.30.0", "@sentry/minimal": "6.0.1",
"@sentry/types": "5.30.0", "@sentry/types": "6.0.1",
"@sentry/utils": "5.30.0", "@sentry/utils": "6.0.1",
"tslib": "^1.9.3" "tslib": "^1.9.3"
},
"dependencies": {
"@sentry/types": {
"version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz",
"integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw=="
},
"@sentry/utils": {
"version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz",
"integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==",
"requires": {
"@sentry/types": "5.30.0",
"tslib": "^1.9.3"
}
}
} }
}, },
"@sentry/hub": { "@sentry/hub": {
"version": "5.30.0", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.30.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.0.1.tgz",
"integrity": "sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ==", "integrity": "sha512-pGckNdhKcr7qYVXgSgA/QVGArATcmQu54YFAR5xTnkWVHpAwNmh0fc4CJCc4JBwS/LXSU1Y0nYiLQduVfnv8Cg==",
"requires": { "requires": {
"@sentry/types": "5.30.0", "@sentry/types": "6.0.1",
"@sentry/utils": "5.30.0", "@sentry/utils": "6.0.1",
"tslib": "^1.9.3" "tslib": "^1.9.3"
},
"dependencies": {
"@sentry/types": {
"version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz",
"integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw=="
},
"@sentry/utils": {
"version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz",
"integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==",
"requires": {
"@sentry/types": "5.30.0",
"tslib": "^1.9.3"
}
}
} }
}, },
"@sentry/integrations": { "@sentry/integrations": {
"version": "5.30.0", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-5.30.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-6.0.1.tgz",
"integrity": "sha512-Fqh4ALLoQWdd+1ih0iBduANWFyNmFWMxwvBu3V/wLDRi8OcquI0lEzWai1InzTJTiNhRHPnhuU++l/vkO0OCww==", "integrity": "sha512-5HGwKW0otSVXSLAJ9ezqlux4AYdeX6ElzQgpm6roWEBXEWf/5OyD0n+M3+yHq4NdQXk2kkfL/0DCyNdy8zZX2Q==",
"requires": { "requires": {
"@sentry/types": "5.30.0", "@sentry/types": "6.0.1",
"@sentry/utils": "5.30.0", "@sentry/utils": "6.0.1",
"localforage": "1.8.1", "localforage": "1.8.1",
"tslib": "^1.9.3" "tslib": "^1.9.3"
} }
}, },
"@sentry/minimal": { "@sentry/minimal": {
"version": "5.30.0", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.30.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.0.1.tgz",
"integrity": "sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw==", "integrity": "sha512-TQ/M5A+OsxtQJ8dzHwrclxKXpJNdQeM1PUoYhff4BvsOXJScvZb7+Yn0OUEQXEc9pSMNt62tnQy4ct80iAMTHw==",
"requires": { "requires": {
"@sentry/hub": "5.30.0", "@sentry/hub": "6.0.1",
"@sentry/types": "5.30.0", "@sentry/types": "6.0.1",
"tslib": "^1.9.3" "tslib": "^1.9.3"
},
"dependencies": {
"@sentry/types": {
"version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz",
"integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw=="
}
} }
}, },
"@sentry/types": { "@sentry/types": {
"version": "5.30.0", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.0.1.tgz",
"integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw==" "integrity": "sha512-cEoe19vtam75Tf6eWmaobfbeV8XwBdr5FJoSVTomzcSsEiP2FHGOEhlE7kVBigzeH5Lri0aibiW6BDi1hIqHdg=="
}, },
"@sentry/utils": { "@sentry/utils": {
"version": "5.30.0", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.0.1.tgz",
"integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==", "integrity": "sha512-bjGuBYnG6fulZ8mLhPGBxttNu96DCN6d7Glw2sfLf4aurn1kjJ/58hP2c8dH0OqWO5e+rGYTsZ5Dr5kqVKNGTg==",
"requires": { "requires": {
"@sentry/types": "5.30.0", "@sentry/types": "6.0.1",
"tslib": "^1.9.3" "tslib": "^1.9.3"
} }
}, },
@ -5155,10 +5100,10 @@
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
}, },
"browser-nativefs": { "browser-fs-access": {
"version": "0.12.0", "version": "0.13.0",
"resolved": "https://registry.npmjs.org/browser-nativefs/-/browser-nativefs-0.12.0.tgz", "resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.13.0.tgz",
"integrity": "sha512-ZCHJcQI6bBm9YjB+6wMT1nWg+/mnWnz7r3gJ8sx7RjgLtWROFq+BuD12cAncD6y45MIbUqFM8eMKXoHXOxSFxA==" "integrity": "sha512-qP8zFVhRQThxYgBXdlFHbzIrWb1us0G5kL2ZL0vW4BO5llKE4qBAcQsQrw4KN+6vjw8sKeWaGWJtzijfRT4N0Q=="
}, },
"browser-process-hrtime": { "browser-process-hrtime": {
"version": "1.0.0", "version": "1.0.0",
@ -7987,9 +7932,9 @@
} }
}, },
"eslint-config-prettier": { "eslint-config-prettier": {
"version": "7.1.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.1.0.tgz", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz",
"integrity": "sha512-9sm5/PxaFG7qNJvJzTROMM1Bk1ozXVTKI0buKOyb0Bsr1hrwi0H/TzxF/COtf1uxikIK8SwhX7K6zg78jAzbeA==", "integrity": "sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg==",
"dev": true "dev": true
}, },
"eslint-config-react-app": { "eslint-config-react-app": {
@ -9067,16 +9012,16 @@
} }
}, },
"firebase": { "firebase": {
"version": "8.2.3", "version": "8.2.5",
"resolved": "https://registry.npmjs.org/firebase/-/firebase-8.2.3.tgz", "resolved": "https://registry.npmjs.org/firebase/-/firebase-8.2.5.tgz",
"integrity": "sha512-WdbcGSiLxiW/kGZT+EyqD9z3Md7kR35+k9qMjDn/twiIrm6Hh7Qi/Z69cqxhKW6+4uK5ghXIF28CjK67OyD9Qw==", "integrity": "sha512-x9KUJR8PvqLUNzNKWHjAnO7rJVgK546G0F+vjlJTNl+J/8oFTdWh8X4PvYda0z0XM68A2Y9xPGf3blz5qHCn0A==",
"requires": { "requires": {
"@firebase/analytics": "0.6.2", "@firebase/analytics": "0.6.2",
"@firebase/app": "0.6.13", "@firebase/app": "0.6.14",
"@firebase/app-types": "0.6.1", "@firebase/app-types": "0.6.1",
"@firebase/auth": "0.16.1", "@firebase/auth": "0.16.2",
"@firebase/database": "0.8.3", "@firebase/database": "0.9.1",
"@firebase/firestore": "2.1.2", "@firebase/firestore": "2.1.4",
"@firebase/functions": "0.6.1", "@firebase/functions": "0.6.1",
"@firebase/installations": "0.4.19", "@firebase/installations": "0.4.19",
"@firebase/messaging": "0.7.3", "@firebase/messaging": "0.7.3",
@ -9088,9 +9033,9 @@
} }
}, },
"firebase-tools": { "firebase-tools": {
"version": "9.2.1", "version": "9.2.2",
"resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-9.2.1.tgz", "resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-9.2.2.tgz",
"integrity": "sha512-sD4wfB5hs/8IKXV6AJOmkpvXf/St7gVc9QeW4Qz21PG7CkirgRf6FqcYkPKtBcro4wfj48dihnYx/IO1+XPTGg==", "integrity": "sha512-AFjf7S9NjEM+u8ZByJEKASxRG1g+LLg/A0CrzA3V91P92MN+8cyrCigEs7mCdtFknLaShrCgzROyo/OEwd4xdA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@google-cloud/pubsub": "^2.7.0", "@google-cloud/pubsub": "^2.7.0",
@ -11562,9 +11507,9 @@
}, },
"dependencies": { "dependencies": {
"ip-regex": { "ip-regex": {
"version": "4.2.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.2.0.tgz", "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz",
"integrity": "sha512-n5cDDeTWWRwK1EBoWwRti+8nP4NbytBBY0pldmnIkq6Z55KNFmWofh4rl9dPZpj+U/nVq7gweR3ylrvMt4YZ5A==", "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==",
"dev": true "dev": true
} }
} }

View File

@ -19,17 +19,17 @@
] ]
}, },
"dependencies": { "dependencies": {
"@sentry/browser": "5.30.0", "@sentry/browser": "6.0.1",
"@sentry/integrations": "5.30.0", "@sentry/integrations": "6.0.1",
"@testing-library/jest-dom": "5.11.9", "@testing-library/jest-dom": "5.11.9",
"@testing-library/react": "11.2.3", "@testing-library/react": "11.2.3",
"@types/jest": "26.0.20", "@types/jest": "26.0.20",
"@types/react": "17.0.0", "@types/react": "17.0.0",
"@types/react-dom": "17.0.0", "@types/react-dom": "17.0.0",
"@types/socket.io-client": "1.4.35", "@types/socket.io-client": "1.4.35",
"browser-nativefs": "0.12.0", "browser-fs-access": "0.13.0",
"clsx": "1.1.1", "clsx": "1.1.1",
"firebase": "8.2.3", "firebase": "8.2.5",
"i18next-browser-languagedetector": "6.0.1", "i18next-browser-languagedetector": "6.0.1",
"lodash.throttle": "4.1.1", "lodash.throttle": "4.1.1",
"nanoid": "3.1.20", "nanoid": "3.1.20",
@ -51,9 +51,9 @@
"devDependencies": { "devDependencies": {
"@types/lodash.throttle": "4.1.6", "@types/lodash.throttle": "4.1.6",
"@types/pako": "1.0.1", "@types/pako": "1.0.1",
"eslint-config-prettier": "7.1.0", "eslint-config-prettier": "7.2.0",
"eslint-plugin-prettier": "3.3.1", "eslint-plugin-prettier": "3.3.1",
"firebase-tools": "9.2.1", "firebase-tools": "9.2.2",
"husky": "4.3.8", "husky": "4.3.8",
"jest-canvas-mock": "2.3.0", "jest-canvas-mock": "2.3.0",
"lint-staged": "10.5.3", "lint-staged": "10.5.3",
@ -73,7 +73,7 @@
"jest": { "jest": {
"resetMocks": false, "resetMocks": false,
"transformIgnorePatterns": [ "transformIgnorePatterns": [
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-nativefs)/)" "node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access)/)"
] ]
}, },
"name": "excalidraw", "name": "excalidraw",
@ -81,7 +81,7 @@
"scripts": { "scripts": {
"build": "npm run build:app && npm run build:version", "build": "npm run build:app && npm run build:version",
"build-node": "node ./scripts/build-node.js", "build-node": "node ./scripts/build-node.js",
"build:app": "REACT_APP_GIT_SHA=$NOW_GITHUB_COMMIT_SHA react-scripts build", "build:app": "REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
"build:app:docker": "REACT_APP_DISABLE_SENTRY=true react-scripts build", "build:app:docker": "REACT_APP_DISABLE_SENTRY=true react-scripts build",
"build:version": "node ./scripts/build-version.js", "build:version": "node ./scripts/build-version.js",
"eject": "react-scripts eject", "eject": "react-scripts eject",

View File

@ -17,6 +17,5 @@ export const actionAddToLibrary = register({
}); });
return false; return false;
}, },
contextMenuOrder: 6,
contextItemLabel: "labels.addToLibrary", contextItemLabel: "labels.addToLibrary",
}); });

View File

@ -3,6 +3,7 @@ import { getDefaultAppState } from "../appState";
import { ColorPicker } from "../components/ColorPicker"; import { ColorPicker } from "../components/ColorPicker";
import { resetZoom, trash, zoomIn, zoomOut } from "../components/icons"; import { resetZoom, trash, zoomIn, zoomOut } from "../components/icons";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { ZOOM_STEP } from "../constants";
import { getCommonBounds, getNonDeletedElements } from "../element"; import { getCommonBounds, getNonDeletedElements } from "../element";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
@ -75,8 +76,6 @@ export const actionClearCanvas = register({
), ),
}); });
const ZOOM_STEP = 0.1;
export const actionZoomIn = register({ export const actionZoomIn = register({
name: "zoomIn", name: "zoomIn",
perform: (_elements, appState) => { perform: (_elements, appState) => {

View File

@ -0,0 +1,114 @@
import { CODES, KEYS } from "../keys";
import { register } from "./register";
import { copyToClipboard } from "../clipboard";
import { actionDeleteSelected } from "./actionDeleteSelected";
import { getSelectedElements } from "../scene/selection";
import { exportCanvas } from "../data/index";
import { getNonDeletedElements } from "../element";
import { t } from "../i18n";
export const actionCopy = register({
name: "copy",
perform: (elements, appState) => {
copyToClipboard(getNonDeletedElements(elements), appState);
return {
commitToHistory: false,
};
},
contextItemLabel: "labels.copy",
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.C,
});
export const actionCut = register({
name: "cut",
perform: (elements, appState, data, app) => {
actionCopy.perform(elements, appState, data, app);
return actionDeleteSelected.perform(elements, appState, data, app);
},
contextItemLabel: "labels.cut",
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.X,
});
export const actionCopyAsSvg = register({
name: "copyAsSvg",
perform: async (elements, appState, _data, app) => {
if (!app.canvas) {
return {
commitToHistory: false,
};
}
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
try {
await exportCanvas(
"clipboard-svg",
selectedElements.length
? selectedElements
: getNonDeletedElements(elements),
appState,
app.canvas,
appState,
);
return {
commitToHistory: false,
};
} catch (error) {
console.error(error);
return {
appState: {
...appState,
errorMessage: error.message,
},
commitToHistory: false,
};
}
},
contextItemLabel: "labels.copyAsSvg",
});
export const actionCopyAsPng = register({
name: "copyAsPng",
perform: async (elements, appState, _data, app) => {
if (!app.canvas) {
return {
commitToHistory: false,
};
}
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
try {
await exportCanvas(
"clipboard",
selectedElements.length
? selectedElements
: getNonDeletedElements(elements),
appState,
app.canvas,
appState,
);
return {
appState: {
...appState,
toastMessage: t("toast.copyToClipboardAsPng"),
},
commitToHistory: false,
};
} catch (error) {
console.error(error);
return {
appState: {
...appState,
errorMessage: error.message,
},
commitToHistory: false,
};
}
},
contextItemLabel: "labels.copyAsPng",
keyTest: (event) => event.code === CODES.C && event.altKey && event.shiftKey,
});

View File

@ -136,7 +136,6 @@ export const actionDeleteSelected = register({
}; };
}, },
contextItemLabel: "labels.delete", contextItemLabel: "labels.delete",
contextMenuOrder: 999999,
keyTest: (event) => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE, keyTest: (event) => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton <ToolButton

View File

@ -125,7 +125,6 @@ export const actionGroup = register({
commitToHistory: true, commitToHistory: true,
}; };
}, },
contextMenuOrder: 4,
contextItemLabel: "labels.group", contextItemLabel: "labels.group",
contextItemPredicate: (elements, appState) => contextItemPredicate: (elements, appState) =>
enableActionGroup(elements, appState), enableActionGroup(elements, appState),
@ -174,7 +173,6 @@ 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.code === CODES.G,
contextMenuOrder: 5,
contextItemLabel: "labels.ungroup", contextItemLabel: "labels.ungroup",
contextItemPredicate: (elements, appState) => contextItemPredicate: (elements, appState) =>
getSelectedGroupIds(appState).length > 0, getSelectedGroupIds(appState).length > 0,

View File

@ -6,7 +6,7 @@ import { t } from "../i18n";
import { SceneHistory, HistoryEntry } from "../history"; import { SceneHistory, HistoryEntry } from "../history";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types"; import { AppState } from "../types";
import { KEYS } from "../keys"; import { isWindows, KEYS } from "../keys";
import { getElementMap } from "../element"; import { getElementMap } from "../element";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { fixBindingsAfterDeletion } from "../element/binding"; import { fixBindingsAfterDeletion } from "../element/binding";
@ -59,16 +59,16 @@ const writeData = (
return { commitToHistory }; return { commitToHistory };
}; };
const testUndo = (shift: boolean) => (event: KeyboardEvent) =>
event[KEYS.CTRL_OR_CMD] && /z/i.test(event.key) && event.shiftKey === shift;
type ActionCreator = (history: SceneHistory) => Action; type ActionCreator = (history: SceneHistory) => Action;
export const createUndoAction: ActionCreator = (history) => ({ export const createUndoAction: ActionCreator = (history) => ({
name: "undo", name: "undo",
perform: (elements, appState) => perform: (elements, appState) =>
writeData(elements, appState, () => history.undoOnce()), writeData(elements, appState, () => history.undoOnce()),
keyTest: testUndo(false), keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] &&
event.key.toLowerCase() === KEYS.Z &&
!event.shiftKey,
PanelComponent: ({ updateData }) => ( PanelComponent: ({ updateData }) => (
<ToolButton <ToolButton
type="button" type="button"
@ -84,7 +84,11 @@ export const createRedoAction: ActionCreator = (history) => ({
name: "redo", name: "redo",
perform: (elements, appState) => perform: (elements, appState) =>
writeData(elements, appState, () => history.redoOnce()), writeData(elements, appState, () => history.redoOnce()),
keyTest: testUndo(true), keyTest: (event) =>
(event[KEYS.CTRL_OR_CMD] &&
event.shiftKey &&
event.key.toLowerCase() === KEYS.Z) ||
(isWindows && event.ctrlKey && !event.shiftKey && event.key === KEYS.Y),
PanelComponent: ({ updateData }) => ( PanelComponent: ({ updateData }) => (
<ToolButton <ToolButton
type="button" type="button"

View File

@ -74,7 +74,7 @@ export const actionShortcuts = register({
return { return {
appState: { appState: {
...appState, ...appState,
showHelpDialog: true, showHelpDialog: !appState.showHelpDialog,
}, },
commitToHistory: false, commitToHistory: false,
}; };

View File

@ -34,7 +34,6 @@ export const actionCopyStyles = register({
contextItemLabel: "labels.copyStyles", contextItemLabel: "labels.copyStyles",
keyTest: (event) => keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.C, event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.C,
contextMenuOrder: 0,
}); });
export const actionPasteStyles = register({ export const actionPasteStyles = register({
@ -74,5 +73,4 @@ export const actionPasteStyles = register({
contextItemLabel: "labels.pasteStyles", contextItemLabel: "labels.pasteStyles",
keyTest: (event) => keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V, event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
contextMenuOrder: 1,
}); });

View File

@ -0,0 +1,20 @@
import { CODES, KEYS } from "../keys";
import { register } from "./register";
import { GRID_SIZE } from "../constants";
import { AppState } from "../types";
export const actionToggleGridMode = register({
name: "gridMode",
perform(elements, appState) {
return {
appState: {
...appState,
gridSize: this.checked!(appState) ? null : GRID_SIZE,
},
commitToHistory: false,
};
},
checked: (appState: AppState) => appState.gridSize !== null,
contextItemLabel: "labels.gridMode",
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.QUOTE,
});

View File

@ -0,0 +1,16 @@
import { register } from "./register";
export const actionToggleStats = register({
name: "stats",
perform(elements, appState) {
return {
appState: {
...appState,
showStats: !this.checked!(appState),
},
commitToHistory: false,
};
},
checked: (appState) => appState.showStats,
contextItemLabel: "stats.title",
});

View File

@ -0,0 +1,19 @@
import { CODES, KEYS } from "../keys";
import { register } from "./register";
export const actionToggleZenMode = register({
name: "zenMode",
perform(elements, appState) {
return {
appState: {
...appState,
zenModeEnabled: !this.checked!(appState),
},
commitToHistory: false,
};
},
checked: (appState) => appState.zenModeEnabled,
contextItemLabel: "buttons.zenMode",
keyTest: (event) =>
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.Z,
});

View File

@ -65,3 +65,15 @@ export {
distributeHorizontally, distributeHorizontally,
distributeVertically, distributeVertically,
} from "./actionDistribute"; } from "./actionDistribute";
export {
actionCopy,
actionCut,
actionCopyAsPng,
actionCopyAsSvg,
} from "./actionClipboard";
export { actionToggleGridMode } from "./actionToggleGridMode";
export { actionToggleZenMode } from "./actionToggleZenMode";
export { actionToggleStats } from "./actionToggleStats";

View File

@ -3,14 +3,15 @@ import {
Action, Action,
ActionsManagerInterface, ActionsManagerInterface,
UpdaterFn, UpdaterFn,
ActionFilterFn,
ActionName, ActionName,
ActionResult, ActionResult,
} from "./types"; } from "./types";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types"; import { AppState } from "../types";
import { t } from "../i18n";
import { ShortcutName } from "./shortcuts"; // This is the <App> component, but for now we don't care about anything but its
// `canvas` state.
type App = { canvas: HTMLCanvasElement | null };
export class ActionManager implements ActionsManagerInterface { export class ActionManager implements ActionsManagerInterface {
actions = {} as ActionsManagerInterface["actions"]; actions = {} as ActionsManagerInterface["actions"];
@ -18,13 +19,14 @@ export class ActionManager implements ActionsManagerInterface {
updater: (actionResult: ActionResult | Promise<ActionResult>) => void; updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
getAppState: () => Readonly<AppState>; getAppState: () => Readonly<AppState>;
getElementsIncludingDeleted: () => readonly ExcalidrawElement[]; getElementsIncludingDeleted: () => readonly ExcalidrawElement[];
app: App;
constructor( constructor(
updater: UpdaterFn, updater: UpdaterFn,
getAppState: () => AppState, getAppState: () => AppState,
getElementsIncludingDeleted: () => readonly ExcalidrawElement[], getElementsIncludingDeleted: () => readonly ExcalidrawElement[],
app: App,
) { ) {
this.updater = (actionResult) => { this.updater = (actionResult) => {
if (actionResult && "then" in actionResult) { if (actionResult && "then" in actionResult) {
@ -37,6 +39,7 @@ export class ActionManager implements ActionsManagerInterface {
}; };
this.getAppState = getAppState; this.getAppState = getAppState;
this.getElementsIncludingDeleted = getElementsIncludingDeleted; this.getElementsIncludingDeleted = getElementsIncludingDeleted;
this.app = app;
} }
registerAction(action: Action) { registerAction(action: Action) {
@ -70,6 +73,7 @@ export class ActionManager implements ActionsManagerInterface {
this.getElementsIncludingDeleted(), this.getElementsIncludingDeleted(),
this.getAppState(), this.getAppState(),
null, null,
this.app,
), ),
); );
return true; return true;
@ -81,43 +85,11 @@ export class ActionManager implements ActionsManagerInterface {
this.getElementsIncludingDeleted(), this.getElementsIncludingDeleted(),
this.getAppState(), this.getAppState(),
null, null,
this.app,
), ),
); );
} }
getContextMenuItems(actionFilter: ActionFilterFn = (action) => action) {
return Object.values(this.actions)
.filter(actionFilter)
.filter((action) => "contextItemLabel" in action)
.filter((action) =>
action.contextItemPredicate
? action.contextItemPredicate(
this.getElementsIncludingDeleted(),
this.getAppState(),
)
: true,
)
.sort(
(a, b) =>
(a.contextMenuOrder !== undefined ? a.contextMenuOrder : 999) -
(b.contextMenuOrder !== undefined ? b.contextMenuOrder : 999),
)
.map((action) => ({
// take last bit of the label "labels.<shortcutName>"
shortcutName: action.contextItemLabel?.split(".").pop() as ShortcutName,
label: action.contextItemLabel ? t(action.contextItemLabel) : "",
action: () => {
this.updater(
action.perform(
this.getElementsIncludingDeleted(),
this.getAppState(),
null,
),
);
},
}));
}
// Id is an attribute that we can use to pass in data like keys. // Id is an attribute that we can use to pass in data like keys.
// This is needed for dynamically generated action components // This is needed for dynamically generated action components
// like the user list. We can use this key to extract more // like the user list. We can use this key to extract more
@ -132,6 +104,7 @@ export class ActionManager implements ActionsManagerInterface {
this.getElementsIncludingDeleted(), this.getElementsIncludingDeleted(),
this.getAppState(), this.getAppState(),
formState, formState,
this.app,
), ),
); );
}; };

View File

@ -9,7 +9,7 @@ export type ShortcutName =
| "copyStyles" | "copyStyles"
| "pasteStyles" | "pasteStyles"
| "selectAll" | "selectAll"
| "delete" | "deleteSelectedElements"
| "duplicateSelection" | "duplicateSelection"
| "sendBackward" | "sendBackward"
| "bringForward" | "bringForward"
@ -31,7 +31,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
copyStyles: [getShortcutKey("CtrlOrCmd+Alt+C")], copyStyles: [getShortcutKey("CtrlOrCmd+Alt+C")],
pasteStyles: [getShortcutKey("CtrlOrCmd+Alt+V")], pasteStyles: [getShortcutKey("CtrlOrCmd+Alt+V")],
selectAll: [getShortcutKey("CtrlOrCmd+A")], selectAll: [getShortcutKey("CtrlOrCmd+A")],
delete: [getShortcutKey("Del")], deleteSelectedElements: [getShortcutKey("Del")],
duplicateSelection: [ duplicateSelection: [
getShortcutKey("CtrlOrCmd+D"), getShortcutKey("CtrlOrCmd+D"),
getShortcutKey(`Alt+${t("helpDialog.drag")}`), getShortcutKey(`Alt+${t("helpDialog.drag")}`),

View File

@ -16,12 +16,18 @@ type ActionFn = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>, appState: Readonly<AppState>,
formData: any, formData: any,
app: { canvas: HTMLCanvasElement | null },
) => ActionResult | Promise<ActionResult>; ) => ActionResult | Promise<ActionResult>;
export type UpdaterFn = (res: ActionResult) => void; export type UpdaterFn = (res: ActionResult) => void;
export type ActionFilterFn = (action: Action) => void; export type ActionFilterFn = (action: Action) => void;
export type ActionName = export type ActionName =
| "copy"
| "cut"
| "paste"
| "copyAsPng"
| "copyAsSvg"
| "sendBackward" | "sendBackward"
| "bringForward" | "bringForward"
| "sendToBack" | "sendToBack"
@ -29,6 +35,9 @@ export type ActionName =
| "copyStyles" | "copyStyles"
| "selectAll" | "selectAll"
| "pasteStyles" | "pasteStyles"
| "gridMode"
| "zenMode"
| "stats"
| "changeStrokeColor" | "changeStrokeColor"
| "changeBackgroundColor" | "changeBackgroundColor"
| "changeFillStyle" | "changeFillStyle"
@ -93,19 +102,16 @@ export interface Action {
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
) => boolean; ) => boolean;
contextItemLabel?: string; contextItemLabel?: string;
contextMenuOrder?: number;
contextItemPredicate?: ( contextItemPredicate?: (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
) => boolean; ) => boolean;
checked?: (appState: Readonly<AppState>) => boolean;
} }
export interface ActionsManagerInterface { export interface ActionsManagerInterface {
actions: Record<ActionName, Action>; actions: Record<ActionName, Action>;
registerAction: (action: Action) => void; registerAction: (action: Action) => void;
handleKeyDown: (event: KeyboardEvent) => boolean; handleKeyDown: (event: KeyboardEvent) => boolean;
getContextMenuItems: (
actionFilter: ActionFilterFn,
) => { label: string; action: () => void }[];
renderAction: (name: ActionName) => React.ReactElement | null; renderAction: (name: ActionName) => React.ReactElement | null;
} }

View File

@ -1,5 +1,6 @@
export const trackEvent = export const trackEvent =
process.env.REACT_APP_GOOGLE_ANALYTICS_ID && typeof process !== "undefined" &&
process.env?.REACT_APP_GOOGLE_ANALYTICS_ID &&
typeof window !== "undefined" && typeof window !== "undefined" &&
window.gtag window.gtag
? (category: string, name: string, label?: string, value?: number) => { ? (category: string, name: string, label?: string, value?: number) => {
@ -9,7 +10,7 @@ export const trackEvent =
value, value,
}); });
} }
: typeof process !== "undefined" && process?.env?.JEST_WORKER_ID : typeof process !== "undefined" && process.env?.JEST_WORKER_ID
? (category: string, name: string, label?: string, value?: number) => {} ? (category: string, name: string, label?: string, value?: number) => {}
: (category: string, name: string, label?: string, value?: number) => { : (category: string, name: string, label?: string, value?: number) => {
// Uncomment the next line to track locally // Uncomment the next line to track locally

View File

@ -5,7 +5,7 @@ import {
DEFAULT_TEXT_ALIGN, DEFAULT_TEXT_ALIGN,
} from "./constants"; } from "./constants";
import { t } from "./i18n"; import { t } from "./i18n";
import { AppState, FlooredNumber, NormalizedZoomValue } from "./types"; import { AppState, NormalizedZoomValue } from "./types";
import { getDateTime } from "./utils"; import { getDateTime } from "./utils";
export const getDefaultAppState = (): Omit< export const getDefaultAppState = (): Omit<
@ -56,8 +56,8 @@ export const getDefaultAppState = (): Omit<
previousSelectedElementIds: {}, previousSelectedElementIds: {},
resizingElement: null, resizingElement: null,
scrolledOutside: false, scrolledOutside: false,
scrollX: 0 as FlooredNumber, scrollX: 0,
scrollY: 0 as FlooredNumber, scrollY: 0,
selectedElementIds: {}, selectedElementIds: {},
selectedGroupIds: {}, selectedGroupIds: {},
selectionElement: null, selectionElement: null,

View File

@ -3,7 +3,28 @@ import React from "react";
import { RoughCanvas } from "roughjs/bin/canvas"; import { RoughCanvas } from "roughjs/bin/canvas";
import rough from "roughjs/bin/rough"; import rough from "roughjs/bin/rough";
import "../actions"; import "../actions";
import { actionDeleteSelected, actionFinalize } from "../actions"; import {
actionAddToLibrary,
actionBringForward,
actionBringToFront,
actionCopy,
actionCopyAsPng,
actionCopyAsSvg,
actionCopyStyles,
actionCut,
actionDeleteSelected,
actionDuplicateSelection,
actionFinalize,
actionGroup,
actionPasteStyles,
actionSelectAll,
actionSendBackward,
actionSendToBack,
actionToggleGridMode,
actionToggleStats,
actionToggleZenMode,
actionUngroup,
} from "../actions";
import { createRedoAction, createUndoAction } from "../actions/actionHistory"; import { createRedoAction, createUndoAction } from "../actions/actionHistory";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { actions } from "../actions/register"; import { actions } from "../actions/register";
@ -18,7 +39,6 @@ import {
} from "../clipboard"; } from "../clipboard";
import { import {
APP_NAME, APP_NAME,
CANVAS_ONLY_ACTIONS,
CURSOR_TYPE, CURSOR_TYPE,
DEFAULT_VERTICAL_ALIGN, DEFAULT_VERTICAL_ALIGN,
DRAGGING_THRESHOLD, DRAGGING_THRESHOLD,
@ -26,15 +46,15 @@ import {
ELEMENT_TRANSLATE_AMOUNT, ELEMENT_TRANSLATE_AMOUNT,
ENV, ENV,
EVENT, EVENT,
GRID_SIZE,
LINE_CONFIRM_THRESHOLD, LINE_CONFIRM_THRESHOLD,
MIME_TYPES, MIME_TYPES,
POINTER_BUTTON, POINTER_BUTTON,
TAP_TWICE_TIMEOUT, TAP_TWICE_TIMEOUT,
TEXT_TO_CENTER_SNAP_THRESHOLD, TEXT_TO_CENTER_SNAP_THRESHOLD,
TOUCH_CTX_MENU_TIMEOUT, TOUCH_CTX_MENU_TIMEOUT,
ZOOM_STEP,
} from "../constants"; } from "../constants";
import { exportCanvas, loadFromBlob } from "../data"; import { loadFromBlob } from "../data";
import { isValidLibrary } from "../data/json"; import { isValidLibrary } from "../data/json";
import { Library } from "../data/library"; import { Library } from "../data/library";
import { restore } from "../data/restore"; import { restore } from "../data/restore";
@ -127,7 +147,6 @@ import {
getSelectedElements, getSelectedElements,
isOverScrollBars, isOverScrollBars,
isSomeElementSelected, isSomeElementSelected,
normalizeScroll,
} from "../scene"; } from "../scene";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { SceneState, ScrollBars } from "../scene/types"; import { SceneState, ScrollBars } from "../scene/types";
@ -155,6 +174,7 @@ import {
viewportCoordsToSceneCoords, viewportCoordsToSceneCoords,
withBatchedUpdates, withBatchedUpdates,
} from "../utils"; } from "../utils";
import { isMobile } from "../is-mobile";
import ContextMenu from "./ContextMenu"; import ContextMenu from "./ContextMenu";
import LayerUI from "./LayerUI"; import LayerUI from "./LayerUI";
import { Stats } from "./Stats"; import { Stats } from "./Stats";
@ -248,6 +268,7 @@ export type ExcalidrawImperativeAPI = {
}; };
setScrollToCenter: InstanceType<typeof App>["setScrollToCenter"]; setScrollToCenter: InstanceType<typeof App>["setScrollToCenter"];
getSceneElements: InstanceType<typeof App>["getSceneElements"]; getSceneElements: InstanceType<typeof App>["getSceneElements"];
getAppState: () => InstanceType<typeof App>["state"];
readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>; readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>;
ready: true; ready: true;
}; };
@ -298,6 +319,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}, },
setScrollToCenter: this.setScrollToCenter, setScrollToCenter: this.setScrollToCenter,
getSceneElements: this.getSceneElements, getSceneElements: this.getSceneElements,
getAppState: () => this.state,
} as const; } as const;
if (typeof excalidrawRef === "function") { if (typeof excalidrawRef === "function") {
excalidrawRef(api); excalidrawRef(api);
@ -312,6 +334,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.syncActionResult, this.syncActionResult,
() => this.state, () => this.state,
() => this.scene.getElementsIncludingDeleted(), () => this.scene.getElementsIncludingDeleted(),
this,
); );
this.actionManager.registerAll(actions); this.actionManager.registerAll(actions);
@ -906,44 +929,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
copyToClipboard(this.scene.getElements(), this.state); copyToClipboard(this.scene.getElements(), this.state);
}; };
private copyToClipboardAsPng = async () => {
const elements = this.scene.getElements();
const selectedElements = getSelectedElements(elements, this.state);
try {
await exportCanvas(
"clipboard",
selectedElements.length ? selectedElements : elements,
this.state,
this.canvas!,
this.state,
);
this.setState({ toastMessage: t("toast.copyToClipboardAsPng") });
} catch (error) {
console.error(error);
this.setState({ errorMessage: error.message });
}
};
private copyToClipboardAsSvg = async () => {
const selectedElements = getSelectedElements(
this.scene.getElements(),
this.state,
);
try {
await exportCanvas(
"clipboard-svg",
selectedElements.length ? selectedElements : this.scene.getElements(),
this.state,
this.canvas!,
this.state,
);
} catch (error) {
console.error(error);
this.setState({ errorMessage: error.message });
}
};
private static resetTapTwice() { private static resetTapTwice() {
didTapTwice = false; didTapTwice = false;
} }
@ -1146,24 +1131,18 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}; };
toggleZenMode = () => { toggleZenMode = () => {
this.setState({ this.actionManager.executeAction(actionToggleZenMode);
zenModeEnabled: !this.state.zenModeEnabled,
});
}; };
toggleGridMode = () => { toggleGridMode = () => {
this.setState({ this.actionManager.executeAction(actionToggleGridMode);
gridSize: this.state.gridSize ? null : GRID_SIZE,
});
}; };
toggleStats = () => { toggleStats = () => {
if (!this.state.showStats) { if (!this.state.showStats) {
trackEvent("dialog", "stats"); trackEvent("dialog", "stats");
} }
this.setState({ this.actionManager.executeAction(actionToggleStats);
showStats: !this.state.showStats,
});
}; };
setScrollToCenter = (remoteElements: readonly ExcalidrawElement[]) => { setScrollToCenter = (remoteElements: readonly ExcalidrawElement[]) => {
@ -1253,23 +1232,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}); });
} }
if (!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.Z) {
this.toggleZenMode();
}
if (event[KEYS.CTRL_OR_CMD] && event.code === CODES.QUOTE) {
this.toggleGridMode();
}
if (event[KEYS.CTRL_OR_CMD]) { if (event[KEYS.CTRL_OR_CMD]) {
this.setState({ isBindingEnabled: false }); this.setState({ isBindingEnabled: false });
} }
if (event.code === CODES.C && event.altKey && event.shiftKey) {
this.copyToClipboardAsPng();
event.preventDefault();
return;
}
if (this.actionManager.handleKeyDown(event)) { if (this.actionManager.handleKeyDown(event)) {
return; return;
} }
@ -1778,8 +1744,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const scaleFactor = distance / gesture.initialDistance; const scaleFactor = distance / gesture.initialDistance;
this.setState(({ zoom, scrollX, scrollY, offsetLeft, offsetTop }) => ({ this.setState(({ zoom, scrollX, scrollY, offsetLeft, offsetTop }) => ({
scrollX: normalizeScroll(scrollX + deltaX / zoom.value), scrollX: scrollX + deltaX / zoom.value,
scrollY: normalizeScroll(scrollY + deltaY / zoom.value), scrollY: scrollY + deltaY / zoom.value,
zoom: getNewZoom( zoom: getNewZoom(
getNormalizedZoom(initialScale * scaleFactor), getNormalizedZoom(initialScale * scaleFactor),
zoom, zoom,
@ -2190,12 +2156,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} }
this.setState({ this.setState({
scrollX: normalizeScroll( scrollX: this.state.scrollX - deltaX / this.state.zoom.value,
this.state.scrollX - deltaX / this.state.zoom.value, scrollY: this.state.scrollY - deltaY / this.state.zoom.value,
),
scrollY: normalizeScroll(
this.state.scrollY - deltaY / this.state.zoom.value,
),
}); });
}); });
const teardown = withBatchedUpdates( const teardown = withBatchedUpdates(
@ -3009,9 +2971,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const x = event.clientX; const x = event.clientX;
const dx = x - pointerDownState.lastCoords.x; const dx = x - pointerDownState.lastCoords.x;
this.setState({ this.setState({
scrollX: normalizeScroll( scrollX: this.state.scrollX - dx / this.state.zoom.value,
this.state.scrollX - dx / this.state.zoom.value,
),
}); });
pointerDownState.lastCoords.x = x; pointerDownState.lastCoords.x = x;
return true; return true;
@ -3021,9 +2981,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const y = event.clientY; const y = event.clientY;
const dy = y - pointerDownState.lastCoords.y; const dy = y - pointerDownState.lastCoords.y;
this.setState({ this.setState({
scrollY: normalizeScroll( scrollY: this.state.scrollY - dy / this.state.zoom.value,
this.state.scrollY - dy / this.state.zoom.value,
),
}); });
pointerDownState.lastCoords.y = y; pointerDownState.lastCoords.y = y;
return true; return true;
@ -3616,52 +3574,56 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.state, this.state,
); );
const maybeGroupAction = actionGroup.contextItemPredicate!(
this.actionManager.getElementsIncludingDeleted(),
this.actionManager.getAppState(),
);
const maybeUngroupAction = actionUngroup.contextItemPredicate!(
this.actionManager.getElementsIncludingDeleted(),
this.actionManager.getAppState(),
);
const separator = "separator";
const _isMobile = isMobile();
const elements = this.scene.getElements(); const elements = this.scene.getElements();
const element = this.getElementAtPosition(x, y); const element = this.getElementAtPosition(x, y);
if (!element) { if (!element) {
ContextMenu.push({ ContextMenu.push({
options: [ options: [
_isMobile &&
navigator.clipboard && { navigator.clipboard && {
shortcutName: "paste", name: "paste",
label: t("labels.paste"), perform: (elements, appStates) => {
action: () => this.pasteFromClipboard(null), this.pasteFromClipboard(null);
return {
commitToHistory: false,
};
}, },
contextItemLabel: "labels.paste",
},
_isMobile && navigator.clipboard && separator,
probablySupportsClipboardBlob && probablySupportsClipboardBlob &&
elements.length > 0 && { elements.length > 0 &&
shortcutName: "copyAsPng", actionCopyAsPng,
label: t("labels.copyAsPng"),
action: this.copyToClipboardAsPng,
},
probablySupportsClipboardWriteText && probablySupportsClipboardWriteText &&
elements.length > 0 && { elements.length > 0 &&
shortcutName: "copyAsSvg", actionCopyAsSvg,
label: t("labels.copyAsSvg"), ((probablySupportsClipboardBlob && elements.length > 0) ||
action: this.copyToClipboardAsSvg, (probablySupportsClipboardWriteText && elements.length > 0)) &&
}, separator,
...this.actionManager.getContextMenuItems((action) => actionSelectAll,
CANVAS_ONLY_ACTIONS.includes(action.name), separator,
), actionToggleGridMode,
{ actionToggleZenMode,
checked: this.state.gridSize !== null, actionToggleStats,
shortcutName: "gridMode",
label: t("labels.gridMode"),
action: this.toggleGridMode,
},
{
checked: this.state.zenModeEnabled,
shortcutName: "zenMode",
label: t("buttons.zenMode"),
action: this.toggleZenMode,
},
{
checked: this.state.showStats,
shortcutName: "stats",
label: t("stats.title"),
action: this.toggleStats,
},
], ],
top: clientY, top: clientY,
left: clientX, left: clientX,
actionManager: this.actionManager,
appState: this.state,
}); });
return; return;
} }
@ -3672,37 +3634,43 @@ class App extends React.Component<ExcalidrawProps, AppState> {
ContextMenu.push({ ContextMenu.push({
options: [ options: [
{ _isMobile && actionCut,
shortcutName: "cut", _isMobile && navigator.clipboard && actionCopy,
label: t("labels.cut"), _isMobile &&
action: this.cutAll,
},
navigator.clipboard && { navigator.clipboard && {
shortcutName: "copy", name: "paste",
label: t("labels.copy"), perform: (elements, appStates) => {
action: this.copyAll, this.pasteFromClipboard(null);
return {
commitToHistory: false,
};
}, },
navigator.clipboard && { contextItemLabel: "labels.paste",
shortcutName: "paste",
label: t("labels.paste"),
action: () => this.pasteFromClipboard(null),
}, },
probablySupportsClipboardBlob && { _isMobile && separator,
shortcutName: "copyAsPng", probablySupportsClipboardBlob && actionCopyAsPng,
label: t("labels.copyAsPng"), probablySupportsClipboardWriteText && actionCopyAsSvg,
action: this.copyToClipboardAsPng, separator,
}, actionCopyStyles,
probablySupportsClipboardWriteText && { actionPasteStyles,
shortcutName: "copyAsSvg", separator,
label: t("labels.copyAsSvg"), maybeGroupAction && actionGroup,
action: this.copyToClipboardAsSvg, maybeUngroupAction && actionUngroup,
}, (maybeGroupAction || maybeUngroupAction) && separator,
...this.actionManager.getContextMenuItems( actionAddToLibrary,
(action) => !CANVAS_ONLY_ACTIONS.includes(action.name), separator,
), actionSendBackward,
actionBringForward,
actionSendToBack,
actionBringToFront,
separator,
actionDuplicateSelection,
actionDeleteSelected,
], ],
top: clientY, top: clientY,
left: clientX, left: clientX,
actionManager: this.actionManager,
appState: this.state,
}); });
}; };
@ -3733,9 +3701,15 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}, 1000); }, 1000);
} }
let newZoom = this.state.zoom.value - delta / 100;
// increase zoom steps the more zoomed-in we are (applies to >100% only)
newZoom += Math.log10(Math.max(1, this.state.zoom.value)) * -sign;
// round to nearest step
newZoom = Math.round(newZoom * ZOOM_STEP * 100) / (ZOOM_STEP * 100);
this.setState(({ zoom, offsetLeft, offsetTop }) => ({ this.setState(({ zoom, offsetLeft, offsetTop }) => ({
zoom: getNewZoom( zoom: getNewZoom(
getNormalizedZoom(zoom.value - delta / 100), getNormalizedZoom(newZoom),
zoom, zoom,
{ left: offsetLeft, top: offsetTop }, { left: offsetLeft, top: offsetTop },
{ {
@ -3758,14 +3732,14 @@ class App extends React.Component<ExcalidrawProps, AppState> {
if (event.shiftKey) { if (event.shiftKey) {
this.setState(({ zoom, scrollX }) => ({ this.setState(({ zoom, scrollX }) => ({
// on Mac, shift+wheel tends to result in deltaX // on Mac, shift+wheel tends to result in deltaX
scrollX: normalizeScroll(scrollX - (deltaY || deltaX) / zoom.value), scrollX: scrollX - (deltaY || deltaX) / zoom.value,
})); }));
return; return;
} }
this.setState(({ zoom, scrollX, scrollY }) => ({ this.setState(({ zoom, scrollX, scrollY }) => ({
scrollX: normalizeScroll(scrollX - deltaX / zoom.value), scrollX: scrollX - deltaX / zoom.value,
scrollY: normalizeScroll(scrollY - deltaY / zoom.value), scrollY: scrollY - deltaY / zoom.value,
})); }));
}); });

View File

@ -1,4 +1,4 @@
@import "../css/_variables"; @import "../css/variables.module";
.excalidraw { .excalidraw {
.Avatar { .Avatar {

View File

@ -1,4 +1,4 @@
@import "../css/_variables"; @import "../css/variables.module";
.excalidraw { .excalidraw {
.CollabButton.is-collaborating { .CollabButton.is-collaborating {

View File

@ -1,4 +1,4 @@
@import "../css/_variables"; @import "../css/variables.module";
.excalidraw { .excalidraw {
.color-picker { .color-picker {

View File

@ -1,4 +1,4 @@
@import "../css/_variables"; @import "../css/variables.module";
.excalidraw { .excalidraw {
.context-menu { .context-menu {
@ -9,9 +9,10 @@
list-style: none; list-style: none;
user-select: none; user-select: none;
margin: -0.25rem 0 0 0.125rem; margin: -0.25rem 0 0 0.125rem;
padding: 0.25rem 0; padding: 0.5rem 0;
background-color: var(--popup-secondary-bg-color); background-color: var(--popup-secondary-bg-color);
border: 1px solid var(--button-gray-3); border: 1px solid var(--button-gray-3);
cursor: default;
} }
.context-menu button { .context-menu button {
@ -88,4 +89,9 @@
} }
} }
} }
.context-menu-option-separator {
border: none;
border-top: 1px solid $oc-gray-5;
}
} }

View File

@ -2,28 +2,36 @@ import React from "react";
import { render, unmountComponentAtNode } from "react-dom"; import { render, unmountComponentAtNode } from "react-dom";
import clsx from "clsx"; import clsx from "clsx";
import { Popover } from "./Popover"; import { Popover } from "./Popover";
import { t } from "../i18n";
import "./ContextMenu.scss"; import "./ContextMenu.scss";
import { import {
getShortcutFromShortcutName, getShortcutFromShortcutName,
ShortcutName, ShortcutName,
} from "../actions/shortcuts"; } from "../actions/shortcuts";
import { Action } from "../actions/types";
import { ActionManager } from "../actions/manager";
import { AppState } from "../types";
type ContextMenuOption = { type ContextMenuOption = "separator" | Action;
checked?: boolean;
shortcutName: ShortcutName;
label: string;
action(): void;
};
type Props = { type ContextMenuProps = {
options: ContextMenuOption[]; options: ContextMenuOption[];
onCloseRequest?(): void; onCloseRequest?(): void;
top: number; top: number;
left: number; left: number;
actionManager: ActionManager;
appState: Readonly<AppState>;
}; };
const ContextMenu = ({ options, onCloseRequest, top, left }: Props) => { const ContextMenu = ({
options,
onCloseRequest,
top,
left,
actionManager,
appState,
}: ContextMenuProps) => {
const isDarkTheme = !!document const isDarkTheme = !!document
.querySelector(".excalidraw") .querySelector(".excalidraw")
?.classList.contains("Appearance_dark"); ?.classList.contains("Appearance_dark");
@ -43,23 +51,34 @@ const ContextMenu = ({ options, onCloseRequest, top, left }: Props) => {
className="context-menu" className="context-menu"
onContextMenu={(event) => event.preventDefault()} onContextMenu={(event) => event.preventDefault()}
> >
{options.map(({ action, checked, shortcutName, label }, idx) => ( {options.map((option, idx) => {
<li data-testid={shortcutName} key={idx} onClick={onCloseRequest}> if (option === "separator") {
return <hr key={idx} className="context-menu-option-separator" />;
}
const actionName = option.name;
const label = option.contextItemLabel
? t(option.contextItemLabel)
: "";
return (
<li key={idx} data-testid={actionName} onClick={onCloseRequest}>
<button <button
className={`context-menu-option className={clsx("context-menu-option", {
${shortcutName === "delete" ? "dangerous" : ""} dangerous: actionName === "deleteSelectedElements",
${checked ? "checkmark" : ""}`} checkmark: option.checked?.(appState),
onClick={action} })}
onClick={() => actionManager.executeAction(option)}
> >
<div className="context-menu-option__label">{label}</div> <div className="context-menu-option__label">{label}</div>
<kbd className="context-menu-option__shortcut"> <kbd className="context-menu-option__shortcut">
{shortcutName {actionName
? getShortcutFromShortcutName(shortcutName) ? getShortcutFromShortcutName(actionName as ShortcutName)
: ""} : ""}
</kbd> </kbd>
</button> </button>
</li> </li>
))} );
})}
</ul> </ul>
</Popover> </Popover>
</div> </div>
@ -78,8 +97,10 @@ const getContextMenuNode = (): HTMLDivElement => {
type ContextMenuParams = { type ContextMenuParams = {
options: (ContextMenuOption | false | null | undefined)[]; options: (ContextMenuOption | false | null | undefined)[];
top: number; top: ContextMenuProps["top"];
left: number; left: ContextMenuProps["left"];
actionManager: ContextMenuProps["actionManager"];
appState: Readonly<AppState>;
}; };
const handleClose = () => { const handleClose = () => {
@ -101,6 +122,8 @@ export default {
left={params.left} left={params.left}
options={options} options={options}
onCloseRequest={handleClose} onCloseRequest={handleClose}
actionManager={params.actionManager}
appState={params.appState}
/>, />,
getContextMenuNode(), getContextMenuNode(),
); );

View File

@ -1,4 +1,4 @@
@import "../css/_variables"; @import "../css/variables.module";
.excalidraw { .excalidraw {
.Dialog { .Dialog {

View File

@ -1,5 +1,6 @@
import clsx from "clsx"; import clsx from "clsx";
import React, { useCallback, useEffect, useState } from "react"; import React, { useEffect } from "react";
import { useCallbackRefState } from "../hooks/useCallbackRefState";
import { t } from "../i18n"; import { t } from "../i18n";
import useIsMobile from "../is-mobile"; import useIsMobile from "../is-mobile";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
@ -8,14 +9,6 @@ import { back, close } from "./icons";
import { Island } from "./Island"; import { Island } from "./Island";
import { Modal } from "./Modal"; import { Modal } from "./Modal";
const useRefState = <T,>() => {
const [refValue, setRefValue] = useState<T | null>(null);
const refCallback = useCallback((value: T) => {
setRefValue(value);
}, []);
return [refValue, refCallback] as const;
};
export const Dialog = (props: { export const Dialog = (props: {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
@ -24,7 +17,7 @@ export const Dialog = (props: {
title: React.ReactNode; title: React.ReactNode;
autofocus?: boolean; autofocus?: boolean;
}) => { }) => {
const [islandNode, setIslandNode] = useRefState<HTMLDivElement>(); const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
useEffect(() => { useEffect(() => {
if (!islandNode) { if (!islandNode) {

View File

@ -1,4 +1,4 @@
@import "../css/_variables"; @import "../css/variables.module";
.excalidraw { .excalidraw {
.ExportDialog__preview { .ExportDialog__preview {

View File

@ -1,4 +1,4 @@
@import "../css/_variables"; @import "../css/variables.module";
.excalidraw { .excalidraw {
.HelpDialog h3 { .HelpDialog h3 {

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { t } from "../i18n"; import { t } from "../i18n";
import { isDarwin } from "../keys"; import { isDarwin, isWindows } from "../keys";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import { getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
import "./HelpDialog.scss"; import "./HelpDialog.scss";
@ -328,7 +328,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
/> />
<Shortcut <Shortcut
label={t("buttons.redo")} label={t("buttons.redo")}
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Z")]} shortcuts={
isWindows
? [
getShortcutKey("CtrlOrCmd+Y"),
getShortcutKey("CtrlOrCmd+Shift+Z"),
]
: [getShortcutKey("CtrlOrCmd+Shift+Z")]
}
/> />
<Shortcut <Shortcut
label={t("labels.group")} label={t("labels.group")}

View File

@ -1,4 +1,4 @@
@import "../css/_variables"; @import "../css/variables.module";
// this is loosely based on the longest hint text // this is loosely based on the longest hint text
$wide-viewport-width: 1000px; $wide-viewport-width: 1000px;

View File

@ -1,4 +1,4 @@
@import "../css/_variables"; @import "../css/variables.module";
.excalidraw { .excalidraw {
.picker-container { .picker-container {

View File

@ -1,4 +1,4 @@
@import "../css/_variables"; @import "../css/variables.module";
.excalidraw { .excalidraw {
.Modal { .Modal {

View File

@ -1,4 +1,4 @@
@import "../css/_variables"; @import "../css/variables.module";
.excalidraw { .excalidraw {
.PasteChartDialog { .PasteChartDialog {

View File

@ -1,4 +1,4 @@
@import "../css/_variables"; @import "../css/variables.module";
.excalidraw { .excalidraw {
.Stats { .Stats {

View File

@ -1,4 +1,4 @@
@import "../css/_variables.scss"; @import "../css/variables.module";
.excalidraw { .excalidraw {
.TextInput { .TextInput {

View File

@ -1,4 +1,4 @@
@import "../css/_variables"; @import "../css/variables.module";
.excalidraw { .excalidraw {
.Toast { .Toast {

View File

@ -1,5 +1,5 @@
@import "open-color/open-color.scss"; @import "open-color/open-color.scss";
@import "../css/variables"; @import "../css/variables.module";
.excalidraw { .excalidraw {
.ToolIcon { .ToolIcon {

View File

@ -1,4 +1,4 @@
@import "../css/_variables"; @import "../css/variables.module";
.excalidraw { .excalidraw {
.Tooltip { .Tooltip {
position: relative; position: relative;

View File

@ -91,3 +91,5 @@ export const TITLE_TIMEOUT = 10000;
export const TOAST_TIMEOUT = 5000; export const TOAST_TIMEOUT = 5000;
export const TOUCH_CTX_MENU_TIMEOUT = 500; export const TOUCH_CTX_MENU_TIMEOUT = 500;
export const VERSION_TIMEOUT = 15000; export const VERSION_TIMEOUT = 15000;
export const ZOOM_STEP = 0.1;

View File

@ -0,0 +1,42 @@
import React from "react";
export const createInverseContext = <T extends unknown = null>(
initialValue: T,
) => {
const Context = React.createContext(initialValue) as React.Context<T> & {
_updateProviderValue?: (value: T) => void;
};
class InverseConsumer extends React.Component {
state = { value: initialValue };
constructor(props: any) {
super(props);
Context._updateProviderValue = (value: T) => this.setState({ value });
}
render() {
return (
<Context.Provider value={this.state.value}>
{this.props.children}
</Context.Provider>
);
}
}
class InverseProvider extends React.Component<{ value: T }> {
componentDidMount() {
Context._updateProviderValue?.(this.props.value);
}
componentDidUpdate() {
Context._updateProviderValue?.(this.props.value);
}
render() {
return <Context.Consumer>{() => this.props.children}</Context.Consumer>;
}
}
return {
Context,
Consumer: InverseConsumer,
Provider: InverseProvider,
};
};

View File

@ -1,4 +1,4 @@
@import "./_variables"; @import "./variables.module";
@import "./theme"; @import "./theme";
.excalidraw { .excalidraw {

View File

@ -2,3 +2,7 @@
// Keep up to date with is-mobile.tsx // Keep up to date with is-mobile.tsx
$is-mobile-query: "(max-width: 600px), (max-height: 500px) and (max-width: 1000px)"; $is-mobile-query: "(max-width: 600px), (max-height: 500px) and (max-width: 1000px)";
:export {
isMobileQuery: unquote($is-mobile-query);
}

View File

@ -1,4 +1,4 @@
import { fileSave } from "browser-nativefs"; import { fileSave } from "browser-fs-access";
import { import {
copyCanvasToClipboardAsPng, copyCanvasToClipboardAsPng,
copyTextToSystemClipboard, copyTextToSystemClipboard,

View File

@ -1,4 +1,4 @@
import { fileOpen, fileSave } from "browser-nativefs"; import { fileOpen, fileSave } from "browser-fs-access";
import { cleanAppStateForExport } from "../appState"; import { cleanAppStateForExport } from "../appState";
import { MIME_TYPES } from "../constants"; import { MIME_TYPES } from "../constants";
import { clearElementsForExport } from "../element"; import { clearElementsForExport } from "../element";

View File

@ -6,10 +6,11 @@ import { APP_NAME, ENV, EVENT } from "../../constants";
import { ImportedDataState } from "../../data/types"; import { ImportedDataState } from "../../data/types";
import { ExcalidrawElement } from "../../element/types"; import { ExcalidrawElement } from "../../element/types";
import { import {
getElementMap,
getSceneVersion, getSceneVersion,
getSyncableElements, getSyncableElements,
} from "../../packages/excalidraw/index"; } from "../../packages/excalidraw/index";
import { AppState, Collaborator, Gesture } from "../../types"; import { Collaborator, Gesture } from "../../types";
import { resolvablePromise, withBatchedUpdates } from "../../utils"; import { resolvablePromise, withBatchedUpdates } from "../../utils";
import { import {
INITIAL_SCENE_UPDATE_TIMEOUT, INITIAL_SCENE_UPDATE_TIMEOUT,
@ -31,6 +32,7 @@ import {
} from "../data/localStorage"; } from "../data/localStorage";
import Portal from "./Portal"; import Portal from "./Portal";
import RoomDialog from "./RoomDialog"; import RoomDialog from "./RoomDialog";
import { createInverseContext } from "../../createInverseContext";
interface CollabState { interface CollabState {
isCollaborating: boolean; isCollaborating: boolean;
@ -56,17 +58,21 @@ type ReconciledElements = readonly ExcalidrawElement[] & {
}; };
interface Props { interface Props {
children: (collab: CollabAPI) => React.ReactNode; excalidrawAPI: ExcalidrawImperativeAPI;
// NOTE not type-safe because the refObject may in fact not be initialized
// with ExcalidrawImperativeAPI yet
excalidrawRef: React.MutableRefObject<ExcalidrawImperativeAPI>;
} }
const {
Context: CollabContext,
Consumer: CollabContextConsumer,
Provider: CollabContextProvider,
} = createInverseContext<{ api: CollabAPI | null }>({ api: null });
export { CollabContext, CollabContextConsumer };
class CollabWrapper extends PureComponent<Props, CollabState> { class CollabWrapper extends PureComponent<Props, CollabState> {
portal: Portal; portal: Portal;
excalidrawAPI: Props["excalidrawAPI"];
private socketInitializationTimer?: NodeJS.Timeout; private socketInitializationTimer?: NodeJS.Timeout;
private excalidrawRef: Props["excalidrawRef"];
excalidrawAppState?: AppState;
private lastBroadcastedOrReceivedSceneVersion: number = -1; private lastBroadcastedOrReceivedSceneVersion: number = -1;
private collaborators = new Map<string, Collaborator>(); private collaborators = new Map<string, Collaborator>();
@ -80,7 +86,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
activeRoomLink: "", activeRoomLink: "",
}; };
this.portal = new Portal(this); this.portal = new Portal(this);
this.excalidrawRef = props.excalidrawRef; this.excalidrawAPI = props.excalidrawAPI;
} }
componentDidMount() { componentDidMount() {
@ -142,7 +148,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
saveCollabRoomToFirebase = async ( saveCollabRoomToFirebase = async (
syncableElements: ExcalidrawElement[] = getSyncableElements( syncableElements: ExcalidrawElement[] = getSyncableElements(
this.excalidrawRef.current!.getSceneElementsIncludingDeleted(), this.excalidrawAPI.getSceneElementsIncludingDeleted(),
), ),
) => { ) => {
try { try {
@ -154,13 +160,13 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
openPortal = async () => { openPortal = async () => {
window.history.pushState({}, APP_NAME, await generateCollaborationLink()); window.history.pushState({}, APP_NAME, await generateCollaborationLink());
const elements = this.excalidrawRef.current!.getSceneElements(); const elements = this.excalidrawAPI.getSceneElements();
// remove deleted elements from elements array & history to ensure we don't // remove deleted elements from elements array & history to ensure we don't
// expose potentially sensitive user data in case user manually deletes // expose potentially sensitive user data in case user manually deletes
// existing elements (or clears scene), which would otherwise be persisted // existing elements (or clears scene), which would otherwise be persisted
// to database even if deleted before creating the room. // to database even if deleted before creating the room.
this.excalidrawRef.current!.history.clear(); this.excalidrawAPI.history.clear();
this.excalidrawRef.current!.updateScene({ this.excalidrawAPI.updateScene({
elements, elements,
commitToHistory: true, commitToHistory: true,
}); });
@ -175,7 +181,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
private destroySocketClient = () => { private destroySocketClient = () => {
this.collaborators = new Map(); this.collaborators = new Map();
this.excalidrawRef.current!.updateScene({ this.excalidrawAPI.updateScene({
collaborators: this.collaborators, collaborators: this.collaborators,
}); });
this.setState({ this.setState({
@ -265,7 +271,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
user.selectedElementIds = selectedElementIds; user.selectedElementIds = selectedElementIds;
user.username = username; user.username = username;
collaborators.set(socketId, user); collaborators.set(socketId, user);
this.excalidrawRef.current!.updateScene({ this.excalidrawAPI.updateScene({
collaborators, collaborators,
}); });
break; break;
@ -300,7 +306,55 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
private reconcileElements = ( private reconcileElements = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
): ReconciledElements => { ): ReconciledElements => {
const newElements = this.portal.reconcileElements(elements); const currentElements = this.getSceneElementsIncludingDeleted();
// create a map of ids so we don't have to iterate
// over the array more than once.
const localElementMap = getElementMap(currentElements);
const appState = this.excalidrawAPI.getAppState();
// Reconcile
const newElements: readonly ExcalidrawElement[] = elements
.reduce((elements, element) => {
// if the remote element references one that's currently
// edited on local, skip it (it'll be added in the next step)
if (
element.id === appState.editingElement?.id ||
element.id === appState.resizingElement?.id ||
element.id === appState.draggingElement?.id
) {
return elements;
}
if (
localElementMap.hasOwnProperty(element.id) &&
localElementMap[element.id].version > element.version
) {
elements.push(localElementMap[element.id]);
delete localElementMap[element.id];
} else if (
localElementMap.hasOwnProperty(element.id) &&
localElementMap[element.id].version === element.version &&
localElementMap[element.id].versionNonce !== element.versionNonce
) {
// resolve conflicting edits deterministically by taking the one with the lowest versionNonce
if (localElementMap[element.id].versionNonce < element.versionNonce) {
elements.push(localElementMap[element.id]);
} else {
// it should be highly unlikely that the two versionNonces are the same. if we are
// really worried about this, we can replace the versionNonce with the socket id.
elements.push(element);
}
delete localElementMap[element.id];
} else {
elements.push(element);
delete localElementMap[element.id];
}
return elements;
}, [] as Mutable<typeof elements>)
// add local elements that weren't deleted or on remote
.concat(...Object.values(localElementMap));
// Avoid broadcasting to the rest of the collaborators the scene // Avoid broadcasting to the rest of the collaborators the scene
// we just received! // we just received!
@ -319,10 +373,10 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
}: { init?: boolean; initFromSnapshot?: boolean } = {}, }: { init?: boolean; initFromSnapshot?: boolean } = {},
) => { ) => {
if (init || initFromSnapshot) { if (init || initFromSnapshot) {
this.excalidrawRef.current!.setScrollToCenter(elements); this.excalidrawAPI.setScrollToCenter(elements);
} }
this.excalidrawRef.current!.updateScene({ this.excalidrawAPI.updateScene({
elements, elements,
commitToHistory: !!init, commitToHistory: !!init,
}); });
@ -331,7 +385,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
// when we receive any messages from another peer. This UX can be pretty rough -- if you // when we receive any messages from another peer. This UX can be pretty rough -- if you
// undo, a user makes a change, and then try to redo, your element(s) will be lost. However, // undo, a user makes a change, and then try to redo, your element(s) will be lost. However,
// right now we think this is the right tradeoff. // right now we think this is the right tradeoff.
this.excalidrawRef.current!.history.clear(); this.excalidrawAPI.history.clear();
}; };
setCollaborators(sockets: string[]) { setCollaborators(sockets: string[]) {
@ -347,7 +401,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
} }
} }
this.collaborators = collaborators; this.collaborators = collaborators;
this.excalidrawRef.current!.updateScene({ collaborators }); this.excalidrawAPI.updateScene({ collaborators });
}); });
} }
@ -360,7 +414,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
}; };
public getSceneElementsIncludingDeleted = () => { public getSceneElementsIncludingDeleted = () => {
return this.excalidrawRef.current!.getSceneElementsIncludingDeleted(); return this.excalidrawAPI.getSceneElementsIncludingDeleted();
}; };
onPointerUpdate = (payload: { onPointerUpdate = (payload: {
@ -373,11 +427,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
this.portal.broadcastMouseLocation(payload); this.portal.broadcastMouseLocation(payload);
}; };
broadcastElements = ( broadcastElements = (elements: readonly ExcalidrawElement[]) => {
elements: readonly ExcalidrawElement[],
state: AppState,
) => {
this.excalidrawAppState = state;
if ( if (
getSceneVersion(elements) > getSceneVersion(elements) >
this.getLastBroadcastedOrReceivedSceneVersion() this.getLastBroadcastedOrReceivedSceneVersion()
@ -396,7 +446,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
this.portal.broadcastScene( this.portal.broadcastScene(
SCENE.UPDATE, SCENE.UPDATE,
getSyncableElements( getSyncableElements(
this.excalidrawRef.current!.getSceneElementsIncludingDeleted(), this.excalidrawAPI.getSceneElementsIncludingDeleted(),
), ),
true, true,
); );
@ -425,8 +475,23 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
}); });
}; };
/** PRIVATE. Use `this.getContextValue()` instead. */
private contextValue: CollabAPI | null = null;
/** Getter of context value. Returned object is stable. */
getContextValue = (): CollabAPI => {
this.contextValue = this.contextValue || ({} as CollabAPI);
this.contextValue.isCollaborating = this.state.isCollaborating;
this.contextValue.username = this.state.username;
this.contextValue.onPointerUpdate = this.onPointerUpdate;
this.contextValue.initializeSocketClient = this.initializeSocketClient;
this.contextValue.onCollabButtonClick = this.onCollabButtonClick;
this.contextValue.broadcastElements = this.broadcastElements;
return this.contextValue;
};
render() { render() {
const { children } = this.props;
const { modalIsShown, username, errorMessage, activeRoomLink } = this.state; const { modalIsShown, username, errorMessage, activeRoomLink } = this.state;
return ( return (
@ -450,14 +515,11 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
onClose={() => this.setState({ errorMessage: "" })} onClose={() => this.setState({ errorMessage: "" })}
/> />
)} )}
{children({ <CollabContextProvider
isCollaborating: this.state.isCollaborating, value={{
username: this.state.username, api: this.getContextValue(),
onPointerUpdate: this.onPointerUpdate, }}
initializeSocketClient: this.initializeSocketClient, />
onCollabButtonClick: this.onCollabButtonClick,
broadcastElements: this.broadcastElements,
})}
</> </>
); );
} }

View File

@ -6,23 +6,20 @@ import {
import CollabWrapper from "./CollabWrapper"; import CollabWrapper from "./CollabWrapper";
import { import { getSyncableElements } from "../../packages/excalidraw/index";
getElementMap,
getSyncableElements,
} from "../../packages/excalidraw/index";
import { ExcalidrawElement } from "../../element/types"; import { ExcalidrawElement } from "../../element/types";
import { BROADCAST, SCENE } from "../app_constants"; import { BROADCAST, SCENE } from "../app_constants";
class Portal { class Portal {
app: CollabWrapper; collab: CollabWrapper;
socket: SocketIOClient.Socket | null = null; socket: SocketIOClient.Socket | null = null;
socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized
roomId: string | null = null; roomId: string | null = null;
roomKey: string | null = null; roomKey: string | null = null;
broadcastedElementVersions: Map<string, number> = new Map(); broadcastedElementVersions: Map<string, number> = new Map();
constructor(app: CollabWrapper) { constructor(collab: CollabWrapper) {
this.app = app; this.collab = collab;
} }
open(socket: SocketIOClient.Socket, id: string, key: string) { open(socket: SocketIOClient.Socket, id: string, key: string) {
@ -30,7 +27,7 @@ class Portal {
this.roomId = id; this.roomId = id;
this.roomKey = key; this.roomKey = key;
// Initialize socket listeners (moving from App) // Initialize socket listeners
this.socket.on("init-room", () => { this.socket.on("init-room", () => {
if (this.socket) { if (this.socket) {
this.socket.emit("join-room", this.roomId); this.socket.emit("join-room", this.roomId);
@ -39,12 +36,12 @@ class Portal {
this.socket.on("new-user", async (_socketId: string) => { this.socket.on("new-user", async (_socketId: string) => {
this.broadcastScene( this.broadcastScene(
SCENE.INIT, SCENE.INIT,
getSyncableElements(this.app.getSceneElementsIncludingDeleted()), getSyncableElements(this.collab.getSceneElementsIncludingDeleted()),
/* syncAll */ true, /* syncAll */ true,
); );
}); });
this.socket.on("room-user-change", (clients: string[]) => { this.socket.on("room-user-change", (clients: string[]) => {
this.app.setCollaborators(clients); this.collab.setCollaborators(clients);
}); });
} }
@ -125,10 +122,10 @@ class Portal {
data as SocketUpdateData, data as SocketUpdateData,
); );
if (syncAll && this.app.state.isCollaborating) { if (syncAll && this.collab.state.isCollaborating) {
await Promise.all([ await Promise.all([
broadcastPromise, broadcastPromise,
this.app.saveCollabRoomToFirebase(syncableElements), this.collab.saveCollabRoomToFirebase(syncableElements),
]); ]);
} else { } else {
await broadcastPromise; await broadcastPromise;
@ -146,9 +143,9 @@ class Portal {
socketId: this.socket.id, socketId: this.socket.id,
pointer: payload.pointer, pointer: payload.pointer,
button: payload.button || "up", button: payload.button || "up",
selectedElementIds: selectedElementIds: this.collab.excalidrawAPI.getAppState()
this.app.excalidrawAppState?.selectedElementIds || {}, .selectedElementIds,
username: this.app.state.username, username: this.collab.state.username,
}, },
}; };
return this._broadcastSocketData( return this._broadcastSocketData(
@ -157,62 +154,6 @@ class Portal {
); );
} }
}; };
reconcileElements = (
sceneElements: readonly ExcalidrawElement[],
): readonly ExcalidrawElement[] => {
const currentElements = this.app.getSceneElementsIncludingDeleted();
// create a map of ids so we don't have to iterate
// over the array more than once.
const localElementMap = getElementMap(currentElements);
// Reconcile
return (
sceneElements
.reduce((elements, element) => {
// if the remote element references one that's currently
// edited on local, skip it (it'll be added in the next step)
if (
element.id === this.app.excalidrawAppState?.editingElement?.id ||
element.id === this.app.excalidrawAppState?.resizingElement?.id ||
element.id === this.app.excalidrawAppState?.draggingElement?.id
) {
return elements;
}
if (
localElementMap.hasOwnProperty(element.id) &&
localElementMap[element.id].version > element.version
) {
elements.push(localElementMap[element.id]);
delete localElementMap[element.id];
} else if (
localElementMap.hasOwnProperty(element.id) &&
localElementMap[element.id].version === element.version &&
localElementMap[element.id].versionNonce !== element.versionNonce
) {
// resolve conflicting edits deterministically by taking the one with the lowest versionNonce
if (
localElementMap[element.id].versionNonce < element.versionNonce
) {
elements.push(localElementMap[element.id]);
} else {
// it should be highly unlikely that the two versionNonces are the same. if we are
// really worried about this, we can replace the versionNonce with the socket id.
elements.push(element);
}
delete localElementMap[element.id];
} else {
elements.push(element);
delete localElementMap[element.id];
}
return elements;
}, [] as Mutable<typeof sceneElements>)
// add local elements that weren't deleted or on remote
.concat(...Object.values(localElementMap))
);
};
} }
export default Portal; export default Portal;

View File

@ -1,4 +1,4 @@
@import "../../css/_variables"; @import "../../css/variables.module";
.excalidraw { .excalidraw {
.RoomDialog-linkContainer { .RoomDialog-linkContainer {

View File

@ -1,6 +1,7 @@
import LanguageDetector from "i18next-browser-languagedetector"; import LanguageDetector from "i18next-browser-languagedetector";
import React, { import React, {
useCallback, useCallback,
useContext,
useEffect, useEffect,
useLayoutEffect, useLayoutEffect,
useRef, useRef,
@ -17,12 +18,13 @@ import {
ExcalidrawElement, ExcalidrawElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
} from "../element/types"; } from "../element/types";
import { useCallbackRefState } from "../hooks/useCallbackRefState";
import { Language, t } from "../i18n"; import { Language, t } from "../i18n";
import Excalidraw, { import Excalidraw, {
defaultLang, defaultLang,
languages, languages,
} from "../packages/excalidraw/index"; } from "../packages/excalidraw/index";
import { AppState, ExcalidrawAPIRefValue } from "../types"; import { AppState } from "../types";
import { import {
debounce, debounce,
getVersion, getVersion,
@ -30,7 +32,11 @@ import {
resolvablePromise, resolvablePromise,
} from "../utils"; } from "../utils";
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT } from "./app_constants"; import { SAVE_TO_LOCAL_STORAGE_TIMEOUT } from "./app_constants";
import CollabWrapper, { CollabAPI } from "./collab/CollabWrapper"; import CollabWrapper, {
CollabAPI,
CollabContext,
CollabContextConsumer,
} from "./collab/CollabWrapper";
import { LanguageList } from "./components/LanguageList"; import { LanguageList } from "./components/LanguageList";
import { exportToBackend, getCollaborationLinkData, loadScene } from "./data"; import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
import { loadFromFirebase } from "./data/firebase"; import { loadFromFirebase } from "./data/firebase";
@ -49,15 +55,6 @@ languageDetector.init({
checkWhitelist: false, checkWhitelist: false,
}); });
const excalidrawRef: React.MutableRefObject<
MarkRequired<ExcalidrawAPIRefValue, "ready" | "readyPromise">
> = {
current: {
readyPromise: resolvablePromise(),
ready: false,
},
};
const saveDebounced = debounce( const saveDebounced = debounce(
(elements: readonly ExcalidrawElement[], state: AppState) => { (elements: readonly ExcalidrawElement[], state: AppState) => {
saveToLocalStorage(elements, state); saveToLocalStorage(elements, state);
@ -191,7 +188,7 @@ const initializeScene = async (opts: {
return null; return null;
}; };
const ExcalidrawWrapper = (props: { collab: CollabAPI }) => { const ExcalidrawWrapper = () => {
// dimensions // dimensions
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -226,35 +223,40 @@ const ExcalidrawWrapper = (props: { collab: CollabAPI }) => {
initialStatePromiseRef.current.promise = resolvablePromise<ImportedDataState | null>(); initialStatePromiseRef.current.promise = resolvablePromise<ImportedDataState | null>();
} }
const { collab } = props;
useEffect(() => { useEffect(() => {
// Delayed so that the app has a time to load the latest SW // Delayed so that the app has a time to load the latest SW
setTimeout(() => { setTimeout(() => {
trackEvent("load", "version", getVersion()); trackEvent("load", "version", getVersion());
}, VERSION_TIMEOUT); }, VERSION_TIMEOUT);
}, []);
const [
excalidrawAPI,
excalidrawRefCallback,
] = useCallbackRefState<ExcalidrawImperativeAPI>();
const collabAPI = useContext(CollabContext)?.api;
useEffect(() => {
if (!collabAPI || !excalidrawAPI) {
return;
}
excalidrawRef.current!.readyPromise.then((excalidrawApi) => {
initializeScene({ initializeScene({
resetScene: excalidrawApi.resetScene, resetScene: excalidrawAPI.resetScene,
initializeSocketClient: collab.initializeSocketClient, initializeSocketClient: collabAPI.initializeSocketClient,
}).then((scene) => { }).then((scene) => {
initialStatePromiseRef.current.promise.resolve(scene); initialStatePromiseRef.current.promise.resolve(scene);
}); });
});
const onHashChange = (_: HashChangeEvent) => { const onHashChange = (_: HashChangeEvent) => {
const api = excalidrawRef.current!;
if (!api.ready) {
return;
}
if (window.location.hash.length > 1) { if (window.location.hash.length > 1) {
initializeScene({ initializeScene({
resetScene: api.resetScene, resetScene: excalidrawAPI.resetScene,
initializeSocketClient: collab.initializeSocketClient, initializeSocketClient: collabAPI.initializeSocketClient,
}).then((scene) => { }).then((scene) => {
if (scene) { if (scene) {
api.updateScene(scene); excalidrawAPI.updateScene(scene);
} }
}); });
} }
@ -273,7 +275,7 @@ const ExcalidrawWrapper = (props: { collab: CollabAPI }) => {
window.removeEventListener(EVENT.BLUR, onBlur, false); window.removeEventListener(EVENT.BLUR, onBlur, false);
clearTimeout(titleTimeout); clearTimeout(titleTimeout);
}; };
}, [collab.initializeSocketClient]); }, [collabAPI, excalidrawAPI]);
useEffect(() => { useEffect(() => {
languageDetector.cacheUserLanguage(langCode); languageDetector.cacheUserLanguage(langCode);
@ -284,8 +286,8 @@ const ExcalidrawWrapper = (props: { collab: CollabAPI }) => {
appState: AppState, appState: AppState,
) => { ) => {
saveDebounced(elements, appState); saveDebounced(elements, appState);
if (collab.isCollaborating) { if (collabAPI?.isCollaborating) {
collab.broadcastElements(elements, appState); collabAPI.broadcastElements(elements);
} }
}; };
@ -343,19 +345,20 @@ const ExcalidrawWrapper = (props: { collab: CollabAPI }) => {
return ( return (
<> <>
<Excalidraw <Excalidraw
ref={excalidrawRef} ref={excalidrawRefCallback}
onChange={onChange} onChange={onChange}
width={dimensions.width} width={dimensions.width}
height={dimensions.height} height={dimensions.height}
initialData={initialStatePromiseRef.current.promise} initialData={initialStatePromiseRef.current.promise}
user={{ name: collab.username }} user={{ name: collabAPI?.username }}
onCollabButtonClick={collab.onCollabButtonClick} onCollabButtonClick={collabAPI?.onCollabButtonClick}
isCollaborating={collab.isCollaborating} isCollaborating={collabAPI?.isCollaborating}
onPointerUpdate={collab.onPointerUpdate} onPointerUpdate={collabAPI?.onPointerUpdate}
onExportToBackend={onExportToBackend} onExportToBackend={onExportToBackend}
renderFooter={renderFooter} renderFooter={renderFooter}
langCode={langCode} langCode={langCode}
/> />
{excalidrawAPI && <CollabWrapper excalidrawAPI={excalidrawAPI} />}
{errorMessage && ( {errorMessage && (
<ErrorDialog <ErrorDialog
message={errorMessage} message={errorMessage}
@ -369,13 +372,9 @@ const ExcalidrawWrapper = (props: { collab: CollabAPI }) => {
export default function ExcalidrawApp() { export default function ExcalidrawApp() {
return ( return (
<TopErrorBoundary> <TopErrorBoundary>
<CollabWrapper <CollabContextConsumer>
excalidrawRef={ <ExcalidrawWrapper />
excalidrawRef as React.MutableRefObject<ExcalidrawImperativeAPI> </CollabContextConsumer>
}
>
{(collab) => <ExcalidrawWrapper collab={collab} />}
</CollabWrapper>
</TopErrorBoundary> </TopErrorBoundary>
); );
} }

View File

@ -1,11 +1,10 @@
import { PointerCoords } from "./types"; import { PointerCoords } from "./types";
import { normalizeScroll } from "./scene";
export const getCenter = (pointers: Map<number, PointerCoords>) => { export const getCenter = (pointers: Map<number, PointerCoords>) => {
const allCoords = Array.from(pointers.values()); const allCoords = Array.from(pointers.values());
return { return {
x: normalizeScroll(sum(allCoords, (coords) => coords.x) / allCoords.length), x: sum(allCoords, (coords) => coords.x) / allCoords.length,
y: normalizeScroll(sum(allCoords, (coords) => coords.y) / allCoords.length), y: sum(allCoords, (coords) => coords.y) / allCoords.length,
}; };
}; };

2
src/global.d.ts vendored
View File

@ -85,6 +85,6 @@ type ForwardRef<T, P = any> = Parameters<
// --------------------------------------------------------------------------— // --------------------------------------------------------------------------—
interface Blob { interface Blob {
handle?: import("browser-nativefs").FileSystemHandle; handle?: import("browser-fs-acces").FileSystemHandle;
name?: string; name?: string;
} }

View File

@ -0,0 +1,7 @@
import { useCallback, useState } from "react";
export const useCallbackRefState = <T>() => {
const [refValue, setRefValue] = useState<T | null>(null);
const refCallback = useCallback((value: T | null) => setRefValue(value), []);
return [refValue, refCallback] as const;
};

View File

@ -1,7 +1,18 @@
import React, { useState, useEffect, useRef, useContext } from "react"; import React, { useState, useEffect, useRef, useContext } from "react";
import variables from "./css/variables.module.scss";
const context = React.createContext(false); const context = React.createContext(false);
const getIsMobileMatcher = () => {
return window.matchMedia
? window.matchMedia(variables.isMobileQuery)
: (({
matches: false,
addListener: () => {},
removeListener: () => {},
} as any) as MediaQueryList);
};
export const IsMobileProvider = ({ export const IsMobileProvider = ({
children, children,
}: { }: {
@ -9,16 +20,7 @@ export const IsMobileProvider = ({
}) => { }) => {
const query = useRef<MediaQueryList>(); const query = useRef<MediaQueryList>();
if (!query.current) { if (!query.current) {
query.current = window.matchMedia query.current = getIsMobileMatcher();
? window.matchMedia(
// Keep up to date with _variables.scss
"(max-width: 640px), (max-height: 500px) and (max-width: 1000px)",
)
: (({
matches: false,
addListener: () => {},
removeListener: () => {},
} as any) as MediaQueryList);
} }
const [isMobile, setMobile] = useState(query.current.matches); const [isMobile, setMobile] = useState(query.current.matches);
@ -31,6 +33,8 @@ export const IsMobileProvider = ({
return <context.Provider value={isMobile}>{children}</context.Provider>; return <context.Provider value={isMobile}>{children}</context.Provider>;
}; };
export const isMobile = () => getIsMobileMatcher().matches;
export default function useIsMobile() { export default function useIsMobile() {
return useContext(context); return useContext(context);
} }

View File

@ -1,4 +1,5 @@
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform); export const isDarwin = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
export const isWindows = /^Win/.test(window.navigator.platform);
export const CODES = { export const CODES = {
EQUAL: "Equal", EQUAL: "Equal",
@ -18,6 +19,7 @@ export const CODES = {
F: "KeyF", F: "KeyF",
H: "KeyH", H: "KeyH",
V: "KeyV", V: "KeyV",
X: "KeyX",
Z: "KeyZ", Z: "KeyZ",
} as const; } as const;
@ -48,6 +50,7 @@ export const KEYS = {
T: "t", T: "t",
V: "v", V: "v",
X: "x", X: "x",
Y: "y",
Z: "z", Z: "z",
} as const; } as const;

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "خطأ" "title": "خطأ"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "اختصارات لوحة المفاتيح", "blog": "",
"shapes": "الأشكال", "click": "",
"or": "أو", "curvedArrow": "",
"click": "انقر", "curvedLine": "",
"drag": "اسحب", "documentation": "",
"curvedArrow": "سهم منحنى", "drag": "",
"curvedLine": "خط منحنى", "editor": "",
"editor": "المحرر", "github": "",
"view": "المشهد", "howto": "",
"blog": "اقرأ مدونتنا", "or": "",
"howto": "اتبع دليلنا", "preventBinding": "",
"github": "عثرت على مشكلة؟ إرسال", "shapes": "",
"textNewLine": "إضافة سطر جديد (نص)", "shortcuts": "",
"textFinish": "الانتهاء من تحرير (النص)", "textFinish": "",
"zoomToFit": "تكبير لتلائم جميع العناصر", "textNewLine": "",
"zoomToSelection": "تقريب للمحدد", "title": "",
"preventBinding": "منع ربط السهم" "view": "",
"zoomToFit": "",
"zoomToSelection": ""
}, },
"encrypted": { "encrypted": {
"tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا." "tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا."
@ -232,5 +234,9 @@
"title": "إحصائيات للمهووسين", "title": "إحصائيات للمهووسين",
"total": "المجموع", "total": "المجموع",
"width": "العرض" "width": "العرض"
},
"toast": {
"copyStyles": "",
"copyToClipboardAsPng": ""
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Грешка" "title": "Грешка"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Клавиши за бърз достъп", "blog": "",
"shapes": "Фигури",
"or": "или",
"click": "клик", "click": "клик",
"curvedArrow": "",
"curvedLine": "",
"documentation": "",
"drag": "плъзнете", "drag": "плъзнете",
"curvedArrow": "Извита стрелка",
"curvedLine": "Извита линия",
"editor": "Редактор", "editor": "Редактор",
"github": "",
"howto": "",
"or": "или",
"preventBinding": "",
"shapes": "Фигури",
"shortcuts": "Клавиши за бърз достъп",
"textFinish": "",
"textNewLine": "",
"title": "",
"view": "Преглед", "view": "Преглед",
"blog": "Прочетете нашия блог", "zoomToFit": "",
"howto": "Следвайте нашите ръководства", "zoomToSelection": "Приближи селекцията"
"github": "Намерихте проблем? Изпратете",
"textNewLine": "Добавяне на нов ред (текст)",
"textFinish": "Завършете редактиране (текст)",
"zoomToFit": "Приближи докато се виждат всички елементи",
"zoomToSelection": "Приближи селекцията",
"preventBinding": "Спри прилепяне на стрелките"
}, },
"encrypted": { "encrypted": {
"tooltip": "Вашите рисунки са криптирани от край до край, така че сървърите на Excalidraw няма да могат да ги виждат." "tooltip": "Вашите рисунки са криптирани от край до край, така че сървърите на Excalidraw няма да могат да ги виждат."
@ -232,5 +234,9 @@
"title": "Статистика за хакери", "title": "Статистика за хакери",
"total": "Общо", "total": "Общо",
"width": "Широчина" "width": "Широчина"
},
"toast": {
"copyStyles": "",
"copyToClipboardAsPng": ""
} }
} }

View File

@ -30,17 +30,17 @@
"edges": "Vores", "edges": "Vores",
"sharp": "Agut", "sharp": "Agut",
"round": "Arrodonit", "round": "Arrodonit",
"arrowheads": "Punta de fletxa", "arrowheads": "Puntes de fletxa",
"arrowhead_none": "Cap", "arrowhead_none": "Cap",
"arrowhead_arrow": "Fletxa", "arrowhead_arrow": "Fletxa",
"arrowhead_bar": "Línia", "arrowhead_bar": "Barra",
"arrowhead_dot": "Punt", "arrowhead_dot": "Punt",
"fontSize": "Mida de lletra", "fontSize": "Mida de lletra",
"fontFamily": "Tipus de lletra", "fontFamily": "Tipus de lletra",
"onlySelected": "Només seleccionats", "onlySelected": "Només seleccionats",
"withBackground": "Amb fons", "withBackground": "Amb fons",
"exportEmbedScene": "Incrustar escena al fitxer exportat", "exportEmbedScene": "Incrustar escena al fitxer exportat",
"exportEmbedScene_details": "Les dades de lescena es desaran al fitxer PNG/SVG exportat de manera que es pugui restaurar lescena.\nAugmentarà la mida del fitxer exportat.", "exportEmbedScene_details": "Les dades de lescena es desaran al fitxer PNG/SVG de manera que es pugui restaurar lescena.\nAugmentarà la mida del fitxer exportat.",
"addWatermark": "Afegir \"Fet amb Excalidraw\"", "addWatermark": "Afegir \"Fet amb Excalidraw\"",
"handDrawn": "Dibuixat a mà", "handDrawn": "Dibuixat a mà",
"normal": "Normal", "normal": "Normal",
@ -61,7 +61,7 @@
"architect": "Arquitecte", "architect": "Arquitecte",
"artist": "Artista", "artist": "Artista",
"cartoonist": "Dibuixant", "cartoonist": "Dibuixant",
"fileTitle": "Títol de fitxer", "fileTitle": "Títol del fitxer",
"colorPicker": "Selector de colors", "colorPicker": "Selector de colors",
"canvasBackground": "Fons del llenç", "canvasBackground": "Fons del llenç",
"drawingCanvas": "Llenç de dibuix", "drawingCanvas": "Llenç de dibuix",
@ -127,7 +127,7 @@
"alerts": { "alerts": {
"clearReset": "Tot el llenç s'esborrarà. Estàs segur?", "clearReset": "Tot el llenç s'esborrarà. Estàs segur?",
"couldNotCreateShareableLink": "No s'ha pogut crear un enllaç per compartir.", "couldNotCreateShareableLink": "No s'ha pogut crear un enllaç per compartir.",
"couldNotCreateShareableLinkTooBig": "No sha pogut crear un enllaç compartible: lescena és massa gran", "couldNotCreateShareableLinkTooBig": "No sha pogut crear un enllaç per compartir: lescena és massa gran",
"couldNotLoadInvalidFile": "No s'ha pogut carregar un fitxer no vàlid", "couldNotLoadInvalidFile": "No s'ha pogut carregar un fitxer no vàlid",
"importBackendFailed": "Importació fallida.", "importBackendFailed": "Importació fallida.",
"cannotExportEmptyCanvas": "No es pot exportar un llenç buit.", "cannotExportEmptyCanvas": "No es pot exportar un llenç buit.",
@ -162,7 +162,7 @@
"freeDraw": "Fer clic i arrosegar, deixar anar al punt final", "freeDraw": "Fer clic i arrosegar, deixar anar al punt final",
"text": "Consell: també pots afegir text fent doble clic a qualsevol lloc amb l'eina de selecció", "text": "Consell: també pots afegir text fent doble clic a qualsevol lloc amb l'eina de selecció",
"linearElementMulti": "Fer clic a l'ultim punt, o polsar Escape o Enter per acabar", "linearElementMulti": "Fer clic a l'ultim punt, o polsar Escape o Enter per acabar",
"lockAngle": "Pots restringir langle mantenint premuda MAJÚS", "lockAngle": "Per restringir els angles, mantenir premut el majúscul (SHIFT)",
"resize": "Per restringir les proporcions mentres es canvia la mida, mantenir premut el majúscul (SHIFT); per canviar la mida des del centre, mantenir premut ALT", "resize": "Per restringir les proporcions mentres es canvia la mida, mantenir premut el majúscul (SHIFT); per canviar la mida des del centre, mantenir premut ALT",
"rotate": "Per restringir els angles mentre gira, mantenir premut el majúscul (SHIFT)", "rotate": "Per restringir els angles mentre gira, mantenir premut el majúscul (SHIFT)",
"lineEditor_info": "Fes doble clic o premi Enter per editar punts", "lineEditor_info": "Fes doble clic o premi Enter per editar punts",
@ -171,7 +171,7 @@
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "No es pot mostrar la vista prèvia", "cannotShowPreview": "No es pot mostrar la vista prèvia",
"canvasTooBig": "El llenç pot ser massa gran.", "canvasTooBig": "Pot ser que el llenç sigui massa gran.",
"canvasTooBigTip": "Consell: prova dacostar una mica els elements més allunyats." "canvasTooBigTip": "Consell: prova dacostar una mica els elements més allunyats."
}, },
"errorSplash": { "errorSplash": {
@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Error" "title": "Error"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Dreceres de teclat", "blog": "",
"shapes": "Formes", "click": "",
"or": "o", "curvedArrow": "",
"click": "fer clic", "curvedLine": "",
"drag": "arrosegar", "documentation": "",
"curvedArrow": "Fletxa curva", "drag": "",
"curvedLine": "Línea curva", "editor": "",
"editor": "Editor", "github": "",
"view": "Vista", "howto": "",
"blog": "Llegir el nostre blog", "or": "",
"howto": "Seguir els nostres guies", "preventBinding": "",
"github": "Has trobat un problema? Enviar-ho", "shapes": "",
"textNewLine": "Afegir línea nova (text)", "shortcuts": "",
"textFinish": "Acabar d'editar (text)", "textFinish": "",
"zoomToFit": "Zoom per veure tots els elements", "textNewLine": "",
"zoomToSelection": "Amplia la selecció", "title": "",
"preventBinding": "Prevenir vinculació de la fletxa" "view": "",
"zoomToFit": "",
"zoomToSelection": ""
}, },
"encrypted": { "encrypted": {
"tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors dExcalidraw no els veuran mai." "tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors dExcalidraw no els veuran mai."
@ -232,5 +234,9 @@
"title": "Estadístiques per nerds", "title": "Estadístiques per nerds",
"total": "Total", "total": "Total",
"width": "Amplada" "width": "Amplada"
},
"toast": {
"copyStyles": "",
"copyToClipboardAsPng": ""
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Fehler" "title": "Fehler"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Tastaturkürzel", "blog": "Lies unseren Blog",
"shapes": "Formen",
"or": "oder",
"click": "klicken", "click": "klicken",
"drag": "ziehen",
"curvedArrow": "Gebogener Pfeil", "curvedArrow": "Gebogener Pfeil",
"curvedLine": "Gebogene Linie", "curvedLine": "Gebogene Linie",
"documentation": "Dokumentation",
"drag": "ziehen",
"editor": "Editor", "editor": "Editor",
"view": "Ansicht",
"blog": "Unseren Blog lesen",
"howto": "Folge unseren Anleitungen",
"github": "Ein Problem gefunden? Informiere uns", "github": "Ein Problem gefunden? Informiere uns",
"textNewLine": "Neue Zeile hinzufügen (Text)", "howto": "Folge unseren Anleitungen",
"or": "oder",
"preventBinding": "Pfeil-Bindung verhindern",
"shapes": "Formen",
"shortcuts": "Tastaturkürzel",
"textFinish": "Bearbeiten beenden (Text)", "textFinish": "Bearbeiten beenden (Text)",
"textNewLine": "Neue Zeile hinzufügen (Text)",
"title": "Hilfe",
"view": "Ansicht",
"zoomToFit": "Zoomen um alle Elemente einzupassen", "zoomToFit": "Zoomen um alle Elemente einzupassen",
"zoomToSelection": "Zoomauswahl", "zoomToSelection": "Auf Auswahl zoomen"
"preventBinding": "Pfeil-Bindung verhindern"
}, },
"encrypted": { "encrypted": {
"tooltip": "Da deine Zeichnungen Ende-zu-Ende verschlüsselt werden, sehen auch unsere Excalidraw-Server sie niemals." "tooltip": "Da deine Zeichnungen Ende-zu-Ende verschlüsselt werden, sehen auch unsere Excalidraw-Server sie niemals."
@ -232,5 +234,9 @@
"title": "Statistiken für Nerds", "title": "Statistiken für Nerds",
"total": "Gesamt", "total": "Gesamt",
"width": "Breite" "width": "Breite"
},
"toast": {
"copyStyles": "Formatierung kopiert.",
"copyToClipboardAsPng": "In die Zwischenablage als PNG kopiert."
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Σφάλμα" "title": "Σφάλμα"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Συντομεύσεις πληκτρολογίου", "blog": "Διαβάστε το Blog μας",
"shapes": "Σχήματα",
"or": "ή",
"click": "κλικ", "click": "κλικ",
"drag": "σύρε",
"curvedArrow": "Κυρτό βέλος", "curvedArrow": "Κυρτό βέλος",
"curvedLine": "Κυρτή γραμμή", "curvedLine": "Κυρτή γραμμή",
"documentation": "Εγχειρίδιο",
"drag": "σύρε",
"editor": "Επεξεργαστής", "editor": "Επεξεργαστής",
"view": "Προβολή",
"blog": "Διαβάστε το ιστολόγιο μας",
"howto": "Ακολουθήστε τους οδηγούς μας",
"github": "Βρήκατε πρόβλημα; Υποβάλετε το", "github": "Βρήκατε πρόβλημα; Υποβάλετε το",
"textNewLine": "Προσθήκη νέας γραμμής (κείμενο)", "howto": "Ακολουθήστε τους οδηγούς μας",
"or": "ή",
"preventBinding": "Αποτροπή δέσμευσης βέλων",
"shapes": "Σχήματα",
"shortcuts": "Συντομεύσεις πληκτρολογίου",
"textFinish": "Ολοκλήρωση επεξεργασίας (κείμενο)", "textFinish": "Ολοκλήρωση επεξεργασίας (κείμενο)",
"textNewLine": "Προσθήκη νέας γραμμής (κείμενο)",
"title": "Βοήθεια",
"view": "Προβολή",
"zoomToFit": "Zoom ώστε να χωρέσουν όλα τα στοιχεία", "zoomToFit": "Zoom ώστε να χωρέσουν όλα τα στοιχεία",
"zoomToSelection": "Εστίαση στην επιλογή", "zoomToSelection": "Ζουμ στην επιλογή"
"preventBinding": "Αποτροπή δέσμευσης βέλων"
}, },
"encrypted": { "encrypted": {
"tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα έιναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw." "tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα έιναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw."
@ -232,5 +234,9 @@
"title": "Στατιστικά για σπασίκλες", "title": "Στατιστικά για σπασίκλες",
"total": "Σύνολο ", "total": "Σύνολο ",
"width": "Πλάτος" "width": "Πλάτος"
},
"toast": {
"copyStyles": "Αντιγράφηκαν στυλ.",
"copyToClipboardAsPng": "Αντιγράφτηκε στο πρόχειρο ως PNG."
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Error" "title": "Error"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Atajos del teclado", "blog": "Lee nuestro blog",
"shapes": "Formas", "click": "click",
"or": "o",
"click": "clic",
"drag": "arrastrar",
"curvedArrow": "Flecha curvada", "curvedArrow": "Flecha curvada",
"curvedLine": "Línea curva", "curvedLine": "Línea curva",
"documentation": "Documentación",
"drag": "arrastrar",
"editor": "Editor", "editor": "Editor",
"view": "Vista",
"blog": "Lee nuestro blog",
"howto": "Siga nuestras guías",
"github": "¿Has encontrado un problema? Envíalo", "github": "¿Has encontrado un problema? Envíalo",
"textNewLine": "Añadir nueva línea (texto)", "howto": "Siga nuestras guías",
"or": "o",
"preventBinding": "Evitar yuxtaposición de flechas",
"shapes": "Formas",
"shortcuts": "Atajos del teclado",
"textFinish": "Finalizar edición (texto)", "textFinish": "Finalizar edición (texto)",
"textNewLine": "Añadir nueva línea (texto)",
"title": "Ayuda",
"view": "Vista",
"zoomToFit": "Ajustar la vista para mostrar todos los elementos", "zoomToFit": "Ajustar la vista para mostrar todos los elementos",
"zoomToSelection": "Hacer zoom a la selección", "zoomToSelection": "Hacer zoom a la selección"
"preventBinding": "Evitar yuxtaposición de flechas"
}, },
"encrypted": { "encrypted": {
"tooltip": "Tus dibujos están cifrados de punto a punto, por lo que los servidores de Excalidraw nunca los verán." "tooltip": "Tus dibujos están cifrados de punto a punto, por lo que los servidores de Excalidraw nunca los verán."
@ -232,5 +234,9 @@
"title": "Estadísticas para nerds", "title": "Estadísticas para nerds",
"total": "Total", "total": "Total",
"width": "Ancho" "width": "Ancho"
},
"toast": {
"copyStyles": "Estilos copiados.",
"copyToClipboardAsPng": "Copiado al portapapeles como PNG."
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "خطا" "title": "خطا"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "میانبرهای صفحه کلید", "blog": "بلاگ ما را بخوانید",
"shapes": "شکل‌ها", "click": "",
"or": "یا",
"click": "کلیک",
"drag": "کشیدن",
"curvedArrow": "فلش خمیده", "curvedArrow": "فلش خمیده",
"curvedLine": "منحنی", "curvedLine": "منحنی",
"documentation": "مستندات",
"drag": "",
"editor": "ویرایشگر", "editor": "ویرایشگر",
"view": "نمایش",
"blog": "بلاگ ما را بخوانید",
"howto": "راهنمای ما را دنبال کنید",
"github": "اشکالی می بینید؟ گزارش دهید", "github": "اشکالی می بینید؟ گزارش دهید",
"howto": "راهنمای ما را دنبال کنید",
"or": "یا",
"preventBinding": "مانع شدن از چسبیدن فلش ها",
"shapes": "شکل‌ها",
"shortcuts": "میانبرهای صفحه کلید",
"textFinish": "",
"textNewLine": "یک خط جدید اضافه کنید (متن)", "textNewLine": "یک خط جدید اضافه کنید (متن)",
"textFinish": "پایان ویرایش (متن)", "title": "راهنما",
"view": "مشاهده",
"zoomToFit": "بزرگنمایی برای دیدن تمام آیتم ها", "zoomToFit": "بزرگنمایی برای دیدن تمام آیتم ها",
"zoomToSelection": "بزرگنمایی قسمت انتخاب شده", "zoomToSelection": "بزرگنمایی قسمت انتخاب شده"
"preventBinding": "مانع شدن از چسبیدن فلش ها"
}, },
"encrypted": { "encrypted": {
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند." "tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند."
@ -232,5 +234,9 @@
"title": "آمار برای نردها", "title": "آمار برای نردها",
"total": "مجموع", "total": "مجموع",
"width": "عرض" "width": "عرض"
},
"toast": {
"copyStyles": "کپی سبک.",
"copyToClipboardAsPng": "کپی در حافطه موقت به صورت PNG."
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Virhe" "title": "Virhe"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Pikanäppäimet", "blog": "Lue blogiamme",
"shapes": "Muodot",
"or": "tai",
"click": "klikkaa", "click": "klikkaa",
"drag": "vedä",
"curvedArrow": "Kaareva nuoli", "curvedArrow": "Kaareva nuoli",
"curvedLine": "Kaareva viiva", "curvedLine": "Kaareva viiva",
"editor": "Editori", "documentation": "Käyttöohjeet",
"view": "Näkymä", "drag": "vedä",
"blog": "Lue blogiamme", "editor": "Muokkausohjelma",
"howto": "Seuraa oppaitamme",
"github": "Löysitkö ongelman? Kerro meille", "github": "Löysitkö ongelman? Kerro meille",
"textNewLine": "Lisää uusi rivi (teksti)", "howto": "Seuraa oppaitamme",
"or": "tai",
"preventBinding": "Estä nuolten kiinnitys",
"shapes": "Muodot",
"shortcuts": "Pikanäppäimet",
"textFinish": "Lopeta muokkaus (teksti)", "textFinish": "Lopeta muokkaus (teksti)",
"zoomToFit": "Zoomaa kaikki elementit näkyviin", "textNewLine": "Lisää uusi rivi (teksti)",
"zoomToSelection": "Zoomaa valintaan", "title": "Ohjeet",
"preventBinding": "Estä nuolten sitominen" "view": "Näkymä",
"zoomToFit": "Näytä kaikki elementit",
"zoomToSelection": "Näytä valinta"
}, },
"encrypted": { "encrypted": {
"tooltip": "Piirroksesi ovat päästä päähän salattuja, joten Excalidrawin palvelimet eivät koskaan näe niitä." "tooltip": "Piirroksesi ovat päästä päähän salattuja, joten Excalidrawin palvelimet eivät koskaan näe niitä."
@ -232,5 +234,9 @@
"title": "Nörttien tilastot", "title": "Nörttien tilastot",
"total": "Yhteensä", "total": "Yhteensä",
"width": "Leveys" "width": "Leveys"
},
"toast": {
"copyStyles": "Tyylit kopioitu.",
"copyToClipboardAsPng": "Kopioitu leikepöydälle PNG-tiedostona."
} }
} }

View File

@ -19,7 +19,7 @@
"stroke": "Contour", "stroke": "Contour",
"background": "Arrière-plan", "background": "Arrière-plan",
"fill": "Remplissage", "fill": "Remplissage",
"strokeWidth": "Largeur du contour", "strokeWidth": "Largeur du trait",
"strokeStyle": "Style du trait", "strokeStyle": "Style du trait",
"strokeStyle_solid": "Plein", "strokeStyle_solid": "Plein",
"strokeStyle_dashed": "Tirets", "strokeStyle_dashed": "Tirets",
@ -28,10 +28,10 @@
"opacity": "Opacité", "opacity": "Opacité",
"textAlign": "Alignement du texte", "textAlign": "Alignement du texte",
"edges": "Angles", "edges": "Angles",
"sharp": "Aigu", "sharp": "Pointus",
"round": "Rond", "round": "Arrondis",
"arrowheads": "Extrémités de ligne", "arrowheads": "Extrémités de flèche",
"arrowhead_none": "Aucun", "arrowhead_none": "Aucune",
"arrowhead_arrow": "Flèche", "arrowhead_arrow": "Flèche",
"arrowhead_bar": "Barre", "arrowhead_bar": "Barre",
"arrowhead_dot": "Point", "arrowhead_dot": "Point",
@ -42,7 +42,7 @@
"exportEmbedScene": "Intégrer la scène au fichier exporté", "exportEmbedScene": "Intégrer la scène au fichier exporté",
"exportEmbedScene_details": "Les données de scène seront enregistrées dans le fichier PNG/SVG exporté, afin que la scène puisse être restaurée à partir de celui-ci.\nCela augmentera la taille du fichier exporté.", "exportEmbedScene_details": "Les données de scène seront enregistrées dans le fichier PNG/SVG exporté, afin que la scène puisse être restaurée à partir de celui-ci.\nCela augmentera la taille du fichier exporté.",
"addWatermark": "Ajouter \"Fait avec Excalidraw\"", "addWatermark": "Ajouter \"Fait avec Excalidraw\"",
"handDrawn": "Manuscrite", "handDrawn": "À main levée",
"normal": "Normale", "normal": "Normale",
"code": "Code", "code": "Code",
"small": "Petit", "small": "Petit",
@ -64,7 +64,7 @@
"fileTitle": "Titre du fichier", "fileTitle": "Titre du fichier",
"colorPicker": "Sélecteur de couleur", "colorPicker": "Sélecteur de couleur",
"canvasBackground": "Arrière-plan du canevas", "canvasBackground": "Arrière-plan du canevas",
"drawingCanvas": "Canvas de dessin", "drawingCanvas": "Zone de dessin",
"layers": "Calques", "layers": "Calques",
"actions": "Actions", "actions": "Actions",
"language": "Langue", "language": "Langue",
@ -81,9 +81,9 @@
"addToLibrary": "Ajouter à la bibliothèque", "addToLibrary": "Ajouter à la bibliothèque",
"removeFromLibrary": "Supprimer de la bibliothèque", "removeFromLibrary": "Supprimer de la bibliothèque",
"libraryLoadingMessage": "Chargement de la bibliothèque...", "libraryLoadingMessage": "Chargement de la bibliothèque...",
"libraries": "Explorer les bibliothèques", "libraries": "Parcourir les bibliothèques",
"loadingScene": "Chargement de la scène...", "loadingScene": "Chargement de la scène...",
"align": "Alignement", "align": "Aligner",
"alignTop": "Aligner en haut", "alignTop": "Aligner en haut",
"alignBottom": "Aligner en bas", "alignBottom": "Aligner en bas",
"alignLeft": "Aligner à gauche", "alignLeft": "Aligner à gauche",
@ -99,7 +99,7 @@
"exportToPng": "Exporter en PNG", "exportToPng": "Exporter en PNG",
"exportToSvg": "Exporter en SVG", "exportToSvg": "Exporter en SVG",
"copyToClipboard": "Copier dans le presse-papier", "copyToClipboard": "Copier dans le presse-papier",
"copyPngToClipboard": "Copier le PNG dans le presse-papier", "copyPngToClipboard": "Copier le PNG vers le presse-papier",
"scale": "Échelle", "scale": "Échelle",
"save": "Sauvegarder", "save": "Sauvegarder",
"saveAs": "Enregistrer sous", "saveAs": "Enregistrer sous",
@ -116,12 +116,12 @@
"edit": "Modifier", "edit": "Modifier",
"undo": "Annuler", "undo": "Annuler",
"redo": "Rétablir", "redo": "Rétablir",
"roomDialog": "Démarrer le collaboration en temps réel", "roomDialog": "Démarrer la collaboration en direct",
"createNewRoom": "Créer un nouveau salon", "createNewRoom": "Créer une nouvelle salle",
"fullScreen": "Plein écran", "fullScreen": "Plein écran",
"darkMode": "Mode sombre", "darkMode": "Mode sombre",
"lightMode": "Mode Clair", "lightMode": "Mode clair",
"zenMode": "Mode Zen", "zenMode": "Mode zen",
"exitZenMode": "Quitter le mode zen" "exitZenMode": "Quitter le mode zen"
}, },
"alerts": { "alerts": {
@ -136,8 +136,8 @@
"uploadedSecurly": "Le téléchargement a été sécurisé avec un chiffrement de bout en bout, ce qui signifie que ni Excalidraw ni personne d'autre ne peut en lire le contenu.", "uploadedSecurly": "Le téléchargement a été sécurisé avec un chiffrement de bout en bout, ce qui signifie que ni Excalidraw ni personne d'autre ne peut en lire le contenu.",
"loadSceneOverridePrompt": "Le chargement d'un dessin externe remplacera votre contenu actuel. Souhaitez-vous continuer ?", "loadSceneOverridePrompt": "Le chargement d'un dessin externe remplacera votre contenu actuel. Souhaitez-vous continuer ?",
"errorLoadingLibrary": "Une erreur s'est produite lors du chargement de la bibliothèque tierce.", "errorLoadingLibrary": "Une erreur s'est produite lors du chargement de la bibliothèque tierce.",
"confirmAddLibrary": "Cela va ajouter {{numShapes}} forme(s) à votre bibliothèque. Êtes-vous sûr(e) ?", "confirmAddLibrary": "Cela va ajouter {{numShapes}} forme(s) à votre bibliothèque. Êtes-vous sûr·e ?",
"imageDoesNotContainScene": "L'importation des images n'est pas prise en charge pour le moment.\n\nVoulez-vous importer une scène ? Cette image ne semble pas contenir de données de scène. Avez-vous activé cette option lors de l'exportation ?", "imageDoesNotContainScene": "L'importation d'images n'est pas prise en charge pour le moment.\n\nVouliez-vous importer une scène ? Cette image ne semble pas contenir de données de scène. Avez-vous activé cette option lors de l'exportation ?",
"cannotRestoreFromImage": "Impossible de restaurer la scène depuis ce fichier image" "cannotRestoreFromImage": "Impossible de restaurer la scène depuis ce fichier image"
}, },
"toolBar": { "toolBar": {
@ -160,63 +160,65 @@
"hints": { "hints": {
"linearElement": "Cliquez pour démarrer plusieurs points, faites glisser pour une seule ligne", "linearElement": "Cliquez pour démarrer plusieurs points, faites glisser pour une seule ligne",
"freeDraw": "Cliquez et faites glissez, relâchez quand vous avez terminé", "freeDraw": "Cliquez et faites glissez, relâchez quand vous avez terminé",
"text": "Astuce : vous pouvez également ajouter du texte en double-cliquant n'importe où avec l'outil de sélection", "text": "Astuce : vous pouvez aussi ajouter du texte en double-cliquant n'importe où avec l'outil de sélection",
"linearElementMulti": "Cliquez sur le dernier point ou appuyez sur Échap ou Entrée pour terminer", "linearElementMulti": "Cliquez sur le dernier point ou appuyez sur Échap ou Entrée pour terminer",
"lockAngle": "Vous pouvez contraindre l'angle en maintenant SHIFT", "lockAngle": "Vous pouvez restreindre l'angle en maintenant MAJ",
"resize": "Vous pouvez conserver les proportions en maintenant la touche SHIFT pendant le redimensionnement,\nen maintenant la touche ALT pour redimensionner par rapport au centre", "resize": "Vous pouvez conserver les proportions en maintenant la touche MAJ pendant le redimensionnement,\nmaintenez la touche ALT pour redimensionner par rapport au centre",
"rotate": "Vous pouvez contraindre les angles en maintenant MAJ enfoncé pendant la rotation", "rotate": "Vous pouvez restreindre les angles en maintenant MAJ pendant la rotation",
"lineEditor_info": "Double-cliquez ou appuyez sur Entrée pour éditer les points", "lineEditor_info": "Double-cliquez ou appuyez sur Entrée pour éditer les points",
"lineEditor_pointSelected": "Appuyez sur Supprimer pour supprimer le point, Ctrl ou Cmd+D pour le dupliquer, ou faites-le glisser pour le déplacer", "lineEditor_pointSelected": "Appuyez sur Supprimer pour supprimer le point, Ctrl ou Cmd+D pour le dupliquer, ou faites-le glisser pour le déplacer",
"lineEditor_nothingSelected": "Sélectionnez un point à déplacer ou à supprimer, ou maintenez Alt enfoncé et cliquez pour ajouter de nouveaux points" "lineEditor_nothingSelected": "Sélectionnez un point à déplacer ou supprimer, ou maintenez Alt et cliquez pour ajouter de nouveaux points"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Impossible dafficher laperçu", "cannotShowPreview": "Impossible dafficher laperçu",
"canvasTooBig": "Le canevas est peut-être trop grand.", "canvasTooBig": "Le canevas est peut-être trop grand.",
"canvasTooBigTip": "Conseil : essayez de rapprocher un peu plus les éléments les plus éloignés." "canvasTooBigTip": "Astuce : essayez de rapprocher un peu les éléments les plus éloignés."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "Une erreur est survenue. Essayez ", "headingMain_pre": "Une erreur est survenue. Essayez ",
"headingMain_button": "rechargement de la page.", "headingMain_button": "de recharger la page.",
"clearCanvasMessage": "Si le rechargement ne résout pas l'erreur, essayez ", "clearCanvasMessage": "Si le rechargement ne résout pas l'erreur, essayez ",
"clearCanvasMessage_button": "effacement du canevas.", "clearCanvasMessage_button": "effacement du canevas.",
"clearCanvasCaveat": " Cela entraînera une perte du travail ", "clearCanvasCaveat": " Cela entraînera une perte du travail ",
"trackedToSentry_pre": "L'erreur avec l'identifiant ", "trackedToSentry_pre": "L'erreur avec l'identifiant ",
"trackedToSentry_post": " a été enregistrée dans notre système.", "trackedToSentry_post": " a été enregistrée dans notre système.",
"openIssueMessage_pre": "Nous avons été très prudents de ne pas inclure les informations de votre scène dans l'erreur. Si votre scène n'est pas privée, veuillez envisager de poursuivre sur notre ", "openIssueMessage_pre": "Nous avons fait très attention à ne pas inclure les informations de votre scène dans l'erreur. Si votre scène n'est pas privée, veuillez envisager de poursuivre sur notre ",
"openIssueMessage_button": "outil de suivi des bugs.", "openIssueMessage_button": "outil de suivi des bugs.",
"openIssueMessage_post": " Veuillez inclure les informations ci-dessous en les copiant-collant dans le ticket GitHub.", "openIssueMessage_post": " Veuillez inclure les informations ci-dessous en les copiant-collant dans le ticket GitHub.",
"sceneContent": "Contenu de la scène :" "sceneContent": "Contenu de la scène :"
}, },
"roomDialog": { "roomDialog": {
"desc_intro": "Vous pouvez inviter des personnes dans votre scène actuelle à collaborer avec vous.", "desc_intro": "Vous pouvez inviter des personnes à collaborer avec vous sur votre scène actuelle.",
"desc_privacy": "Ne vous inquiétez pas, la session utilise le chiffrement de bout en bout, donc tout ce que vous dessinez restera privé. Même notre serveur ne sera pas en mesure de voir ce que vous faites.", "desc_privacy": "Pas d'inquiétude, la session utilise le chiffrement de bout en bout, donc tout ce que vous dessinez restera privé. Même notre serveur ne pourra voir ce que vous faites.",
"button_startSession": "Démarrer la session", "button_startSession": "Démarrer la session",
"button_stopSession": "Arrêter la session", "button_stopSession": "Arrêter la session",
"desc_inProgressIntro": "La session de collaboration en direct est maintenant en cours.", "desc_inProgressIntro": "La session de collaboration en direct est maintenant en cours.",
"desc_shareLink": "Partagez ce lien avec ceux avec qui vous souhaitez collaborer :", "desc_shareLink": "Partagez ce lien avec les personnes avec lesquelles vous souhaitez collaborer :",
"desc_exitSession": "Arrêter la session vous déconnectera du salon, mais vous pourrez continuer à travailler avec la scène, localement. Notez que cela n'affectera pas les autres personnes, et ils seront toujours en mesure de collaborer sur leur version." "desc_exitSession": "Arrêter la session vous déconnectera de la salle, mais vous pourrez continuer à travailler avec la scène, localement. Notez que cela n'affectera pas les autres personnes, et ils pourront toujours collaborer sur leur version."
}, },
"errorDialog": { "errorDialog": {
"title": "Erreur" "title": "Erreur"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Raccourcis clavier", "blog": "Lire notre blog",
"shapes": "Formes", "click": "clic",
"or": "ou",
"click": "cliquer",
"drag": "glisser",
"curvedArrow": "Flèche courbée", "curvedArrow": "Flèche courbée",
"curvedLine": "Ligne courbée", "curvedLine": "Ligne courbée",
"documentation": "Documentation",
"drag": "glisser",
"editor": "Éditeur", "editor": "Éditeur",
"view": "Afficher", "github": "Problème trouvé ? Soumettre",
"blog": "Lisez notre blog",
"howto": "Suivez nos guides", "howto": "Suivez nos guides",
"github": "Vous avez trouvé un problème ? Envoyer", "or": "ou",
"textNewLine": "Ajouter une nouvelle ligne (texte)", "preventBinding": "Empêcher la liaison de flèche",
"shapes": "Formes",
"shortcuts": "Raccourcis clavier",
"textFinish": "Terminer l'édition (texte)", "textFinish": "Terminer l'édition (texte)",
"zoomToFit": "Zoomer pour visualiser tous les éléments", "textNewLine": "Ajouter une nouvelle ligne (texte)",
"zoomToSelection": "Zoomer sur la sélection", "title": "Aide",
"preventBinding": "Empêcher la liaison de la flèche" "view": "Affichage",
"zoomToFit": "Zoomer pour voir tous les éléments",
"zoomToSelection": "Zoomer sur la sélection"
}, },
"encrypted": { "encrypted": {
"tooltip": "Vos dessins sont chiffrés de bout en bout, les serveurs d'Excalidraw ne les verront jamais." "tooltip": "Vos dessins sont chiffrés de bout en bout, les serveurs d'Excalidraw ne les verront jamais."
@ -232,5 +234,9 @@
"title": "Stats pour les nerds", "title": "Stats pour les nerds",
"total": "Total", "total": "Total",
"width": "Largeur" "width": "Largeur"
},
"toast": {
"copyStyles": "Styles copiés.",
"copyToClipboardAsPng": "Copié vers le presse-papier en PNG."
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "שגיאה" "title": "שגיאה"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "קיצורי מקלדת", "blog": "",
"shapes": "צורות", "click": "",
"or": "או", "curvedArrow": "",
"click": "לחץ", "curvedLine": "",
"drag": "גרור", "documentation": "",
"curvedArrow": "חץ מעוקל", "drag": "",
"curvedLine": "קו מעוקל", "editor": "",
"editor": "עורך", "github": "",
"view": "תצוגה", "howto": "",
"blog": "קרא את הבלוג שלנו", "or": "",
"howto": "עקוב אחר המדריכים שלנו", "preventBinding": "",
"github": "מצאת בעיה? דווח", "shapes": "",
"textNewLine": "הוסף שורה חדשה (טקסט)", "shortcuts": "",
"textFinish": "סיים עריכה (טקסט)", "textFinish": "",
"zoomToFit": "זום להתאמת כל האלמנטים למסך", "textNewLine": "",
"zoomToSelection": "התמקד בבחירה", "title": "",
"preventBinding": "מנע השתלבות חצים" "view": "",
"zoomToFit": "",
"zoomToSelection": ""
}, },
"encrypted": { "encrypted": {
"tooltip": "הרישומים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם." "tooltip": "הרישומים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם."
@ -232,5 +234,9 @@
"title": "סטטיסטיקות לחנונים", "title": "סטטיסטיקות לחנונים",
"total": "סה״כ", "total": "סה״כ",
"width": "רוחב" "width": "רוחב"
},
"toast": {
"copyStyles": "",
"copyToClipboardAsPng": ""
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "गलती" "title": "गलती"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "कीबोर्ड के शॉर्टकट्स", "blog": "",
"shapes": "आकृतियाँ", "click": "",
"or": "या", "curvedArrow": "",
"click": "क्लिक करें", "curvedLine": "",
"drag": "खींचें", "documentation": "",
"curvedArrow": "घुमावदार तीर", "drag": "",
"curvedLine": "घुमावदार रेखा", "editor": "",
"editor": "संपादक", "github": "",
"view": "दृश्य", "howto": "",
"blog": "हमारा ब्लॉग पढे", "or": "",
"howto": "हमारे गाइड का पालन करें", "preventBinding": "",
"github": "एक मुद्दा मिला? प्रस्तुत करे", "shapes": "",
"textNewLine": "नई पंक्ति (पाठ) जोड़ें", "shortcuts": "",
"textFinish": "संपादन समाप्त करें (पाठ)", "textFinish": "",
"zoomToFit": "सभी तत्वों को फिट करने के लिए ज़ूम करें", "textNewLine": "",
"zoomToSelection": "सिलेक्शन तक ज़ूम करे", "title": "",
"preventBinding": "तीर बंधन रोकें" "view": "",
"zoomToFit": "",
"zoomToSelection": ""
}, },
"encrypted": { "encrypted": {
"tooltip": "आपके चित्र अंत-से-अंत एन्क्रिप्टेड हैं, इसलिए एक्सक्लूसिव्रॉव के सर्वर उन्हें कभी नहीं देखेंगे।" "tooltip": "आपके चित्र अंत-से-अंत एन्क्रिप्टेड हैं, इसलिए एक्सक्लूसिव्रॉव के सर्वर उन्हें कभी नहीं देखेंगे।"
@ -232,5 +234,9 @@
"title": "बेवकूफ के लिए आँकड़े", "title": "बेवकूफ के लिए आँकड़े",
"total": "कुल", "total": "कुल",
"width": "चौड़ाई" "width": "चौड़ाई"
},
"toast": {
"copyStyles": "",
"copyToClipboardAsPng": ""
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Hiba" "title": "Hiba"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Gyorsbillentyűk", "blog": "",
"shapes": "Formák", "click": "",
"or": "vagy", "curvedArrow": "",
"click": "klikk", "curvedLine": "",
"drag": "húzd", "documentation": "",
"curvedArrow": "Ívelt nyíl", "drag": "",
"curvedLine": "Ívelt vonal", "editor": "",
"editor": "Szerkesztő", "github": "",
"view": "Nézet", "howto": "",
"blog": "Olvasd a blogunkat", "or": "",
"howto": "Kövesd az útmutatóinkat", "preventBinding": "",
"github": "Hibát találtál? Küld be", "shapes": "",
"textNewLine": "Új sor hozzáadása (szöveg)", "shortcuts": "",
"textFinish": "Szerkesztés befejezése (szöveg)", "textFinish": "",
"zoomToFit": "Az összes elem látótérbe hozása", "textNewLine": "",
"zoomToSelection": "Kijelölésre nagyítás", "title": "",
"preventBinding": "A nyíl ne ragadjon" "view": "",
"zoomToFit": "",
"zoomToSelection": ""
}, },
"encrypted": { "encrypted": {
"tooltip": "A rajzaidat végpontok közötti titkosítással tároljuk, tehát az Excalidraw szervereiről se tud más belenézni." "tooltip": "A rajzaidat végpontok közötti titkosítással tároljuk, tehát az Excalidraw szervereiről se tud más belenézni."
@ -232,5 +234,9 @@
"title": "Statisztikák", "title": "Statisztikák",
"total": "Összesen", "total": "Összesen",
"width": "Szélesség" "width": "Szélesség"
},
"toast": {
"copyStyles": "",
"copyToClipboardAsPng": ""
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Kesalahan" "title": "Kesalahan"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Pintasan keyboard", "blog": "Baca blog kami",
"shapes": "Bentuk",
"or": "atau",
"click": "klik", "click": "klik",
"drag": "seret",
"curvedArrow": "Panah lengkung", "curvedArrow": "Panah lengkung",
"curvedLine": "Garis lengkung", "curvedLine": "Garis lengkung",
"documentation": "Dokumentasi",
"drag": "seret",
"editor": "Editor", "editor": "Editor",
"view": "Tampilan", "github": "Menemukan masalah? Kirimkan",
"blog": "Baca blog kami",
"howto": "Ikuti panduan kami", "howto": "Ikuti panduan kami",
"github": "Menemukan sebuah masalah? Kirimkan", "or": "atau",
"textNewLine": "Tambahkan baris baru (teks)", "preventBinding": "Cegah pengikatan panah",
"shapes": "Bentuk",
"shortcuts": "Pintasan keyboard",
"textFinish": "Selesai mengedit (teks)", "textFinish": "Selesai mengedit (teks)",
"textNewLine": "Tambahkan baris baru (teks)",
"title": "Bantuan",
"view": "Tampilan",
"zoomToFit": "Perbesar agar sesuai dengan semua elemen", "zoomToFit": "Perbesar agar sesuai dengan semua elemen",
"zoomToSelection": "Perbesar ke seleksi", "zoomToSelection": "Perbesar ke seleksi"
"preventBinding": "Cegah pengikatan panah"
}, },
"encrypted": { "encrypted": {
"tooltip": "Gambar anda terenkripsi end-to-end sehingga server Excalidraw tidak akan pernah dapat melihatnya." "tooltip": "Gambar anda terenkripsi end-to-end sehingga server Excalidraw tidak akan pernah dapat melihatnya."
@ -232,5 +234,9 @@
"title": "Statistik untuk nerd", "title": "Statistik untuk nerd",
"total": "Total", "total": "Total",
"width": "Lebar" "width": "Lebar"
},
"toast": {
"copyStyles": "Gaya tersalin.",
"copyToClipboardAsPng": "Tersalin ke clipboard sebagai PNG."
} }
} }

View File

@ -163,7 +163,7 @@
"text": "Suggerimento: puoi anche aggiungere del testo facendo doppio clic ovunque con lo strumento di selezione", "text": "Suggerimento: puoi anche aggiungere del testo facendo doppio clic ovunque con lo strumento di selezione",
"linearElementMulti": "Clicca sull'ultimo punto o premi Esc o Invio per finire", "linearElementMulti": "Clicca sull'ultimo punto o premi Esc o Invio per finire",
"lockAngle": "Puoi limitare l'angolo tenendo premuto SHIFT", "lockAngle": "Puoi limitare l'angolo tenendo premuto SHIFT",
"resize": "Per vincolare le proporzioni, tenir premuto MAIUSC durante il ridimensionamento;\nper ridimensionare dal centro, tenir premuto ALT", "resize": "Per vincolare le proporzioni, tieni premuto MAIUSC durante il ridimensionamento;\nper ridimensionare dal centro, tieni premuto ALT",
"rotate": "Puoi mantenere gli angoli tenendo premuto SHIFT durante la rotazione", "rotate": "Puoi mantenere gli angoli tenendo premuto SHIFT durante la rotazione",
"lineEditor_info": "Fai doppio click o premi invio per modificare i punti", "lineEditor_info": "Fai doppio click o premi invio per modificare i punti",
"lineEditor_pointSelected": "Premere Elimina per rimuovere il punto, CtrlOrCmd+D per duplicare o trascinare per spostare", "lineEditor_pointSelected": "Premere Elimina per rimuovere il punto, CtrlOrCmd+D per duplicare o trascinare per spostare",
@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Errore" "title": "Errore"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Scorciatoie da tastiera", "blog": "Leggi il nostro blog",
"shapes": "Forme",
"or": "oppure",
"click": "click", "click": "click",
"drag": "trascina",
"curvedArrow": "Freccia curva", "curvedArrow": "Freccia curva",
"curvedLine": "Linea curva", "curvedLine": "Linea curva",
"documentation": "Documentazione",
"drag": "trascina",
"editor": "Editor", "editor": "Editor",
"view": "Vista", "github": "Trovato un problema? Segnalalo",
"blog": "Leggi il nostro blog",
"howto": "Segui le nostre guide", "howto": "Segui le nostre guide",
"github": "Hai trovato un problema? Segnalalo", "or": "oppure",
"preventBinding": "Impedisci legame della freccia",
"shapes": "Forme",
"shortcuts": "Scorciatoie da tastiera",
"textFinish": "Termina la modifica (testo)",
"textNewLine": "Aggiungi nuova riga (testo)", "textNewLine": "Aggiungi nuova riga (testo)",
"textFinish": "Completa la modifica (testo)", "title": "Guida",
"view": "Vista",
"zoomToFit": "Adatta zoom per mostrare tutti gli elementi", "zoomToFit": "Adatta zoom per mostrare tutti gli elementi",
"zoomToSelection": "Zoom alla selezione", "zoomToSelection": "Zoom alla selezione"
"preventBinding": "Prevenire l'associazione freccia"
}, },
"encrypted": { "encrypted": {
"tooltip": "I tuoi disegni sono crittografati end-to-end in modo che i server di Excalidraw non li possano mai vedere." "tooltip": "I tuoi disegni sono crittografati end-to-end in modo che i server di Excalidraw non li possano mai vedere."
@ -232,5 +234,9 @@
"title": "Statistiche per nerd", "title": "Statistiche per nerd",
"total": "Totale", "total": "Totale",
"width": "Larghezza" "width": "Larghezza"
},
"toast": {
"copyStyles": "Stili copiati.",
"copyToClipboardAsPng": "Copiato negli appunti come PNG."
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "エラー" "title": "エラー"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "キーボードショートカット", "blog": "",
"shapes": "図形", "click": "",
"or": "または", "curvedArrow": "",
"click": "クリック", "curvedLine": "",
"drag": "ドラッグ", "documentation": "",
"curvedArrow": "曲がった矢印", "drag": "",
"curvedLine": "曲線", "editor": "",
"editor": "エディタ", "github": "",
"view": "表示", "howto": "",
"blog": "公式ブログを読む", "or": "",
"howto": "ヘルプ・マニュアル", "preventBinding": "",
"github": "不具合報告はこちら", "shapes": "",
"textNewLine": "テキストの改行", "shortcuts": "",
"textFinish": "テキストの編集を終える", "textFinish": "",
"zoomToFit": "すべての図形が収まるよう拡大/縮小", "textNewLine": "",
"zoomToSelection": "", "title": "",
"preventBinding": "矢印を結合しない" "view": "",
"zoomToFit": "",
"zoomToSelection": ""
}, },
"encrypted": { "encrypted": {
"tooltip": "描画内容はエンドツーエンド暗号化が施されており、Excalidrawサーバーが内容を見ることはできません。" "tooltip": "描画内容はエンドツーエンド暗号化が施されており、Excalidrawサーバーが内容を見ることはできません。"
@ -232,5 +234,9 @@
"title": "", "title": "",
"total": "合計", "total": "合計",
"width": "幅" "width": "幅"
},
"toast": {
"copyStyles": "",
"copyToClipboardAsPng": ""
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "오류" "title": "오류"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "키보드 단축키", "blog": "",
"shapes": "모양", "click": "",
"or": "또는", "curvedArrow": "",
"click": "클릭", "curvedLine": "",
"drag": "드래그", "documentation": "",
"curvedArrow": "곡선 화살표", "drag": "",
"curvedLine": "곡선", "editor": "",
"editor": "편집", "github": "",
"view": "보기", "howto": "",
"blog": "블로그 읽어보기", "or": "",
"howto": "가이드 참고하기", "preventBinding": "",
"github": "이슈 제보하기", "shapes": "",
"textNewLine": "줄바꿈 (텍스트)", "shortcuts": "",
"textFinish": "편집 완료 (텍스트)", "textFinish": "",
"zoomToFit": "모든 요소가 보이도록 확대/축소", "textNewLine": "",
"zoomToSelection": "선택 영역으로 확대/축소", "title": "",
"preventBinding": "화살표가 붙지 않게 하기" "view": "",
"zoomToFit": "",
"zoomToSelection": ""
}, },
"encrypted": { "encrypted": {
"tooltip": "그림은 종단 간 암호화되므로 Excalidraw의 서버는 절대로 내용을 알 수 없습니다." "tooltip": "그림은 종단 간 암호화되므로 Excalidraw의 서버는 절대로 내용을 알 수 없습니다."
@ -232,5 +234,9 @@
"title": "덕후들을 위한 통계", "title": "덕후들을 위한 통계",
"total": "합계", "total": "합계",
"width": "너비" "width": "너비"
},
"toast": {
"copyStyles": "",
"copyToClipboardAsPng": ""
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "ချို့ယွင်းချက်" "title": "ချို့ယွင်းချက်"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "ကီးဘုတ်ရှော့ကတ်များ", "blog": "",
"shapes": "ပုံသဏ္ဌာန်", "click": "",
"or": "(သို့)", "curvedArrow": "",
"click": "ကလစ်နှိပ်", "curvedLine": "",
"drag": "တရွတ်ဆွဲ", "documentation": "",
"curvedArrow": "မြှားကွေး", "drag": "",
"curvedLine": "မျဉ်းကွေး", "editor": "",
"editor": "တည်းဖြတ်", "github": "",
"view": "မြင်ကွင်း", "howto": "",
"blog": "ဘလော့ဂ်တွင်လေ့လာပါ", "or": "",
"howto": "အညွှန်း", "preventBinding": "",
"github": "ချို့ယွင်းမှုအတွက်အသိပေးရန်", "shapes": "",
"textNewLine": "စာသားဖြည့်သွင်း", "shortcuts": "",
"textFinish": "စာသားဖြည့်သွင်းပြီး", "textFinish": "",
"zoomToFit": "ကားချပ်အပြည့်ဖေါ်", "textNewLine": "",
"zoomToSelection": "", "title": "",
"preventBinding": "မြှားများမပေါင်းစေရန်" "view": "",
"zoomToFit": "",
"zoomToSelection": ""
}, },
"encrypted": { "encrypted": {
"tooltip": "ရေးဆွဲထားသောပုံများအား နှစ်ဘက်စွန်းတိုင်လျှို့ဝှက်ထားသဖြင့် Excalidraw ၏ဆာဗာများပင်လျှင်မြင်တွေ့ရမည်မဟုတ်ပါ။" "tooltip": "ရေးဆွဲထားသောပုံများအား နှစ်ဘက်စွန်းတိုင်လျှို့ဝှက်ထားသဖြင့် Excalidraw ၏ဆာဗာများပင်လျှင်မြင်တွေ့ရမည်မဟုတ်ပါ။"
@ -232,5 +234,9 @@
"title": "အက္ခရာများအတွက်အချက်အလက်များ", "title": "အက္ခရာများအတွက်အချက်အလက်များ",
"total": "စုစုပေါင်း", "total": "စုစုပေါင်း",
"width": "အကျယ်" "width": "အကျယ်"
},
"toast": {
"copyStyles": "",
"copyToClipboardAsPng": ""
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Feil" "title": "Feil"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Tastatursnarveier", "blog": "Les bloggen vår",
"shapes": "Figurer",
"or": "eller",
"click": "klikk", "click": "klikk",
"drag": "dra",
"curvedArrow": "Buet pil", "curvedArrow": "Buet pil",
"curvedLine": "Buet linje", "curvedLine": "Buet linje",
"editor": "Redigering", "documentation": "Dokumentasjon",
"view": "Visning", "drag": "dra",
"blog": "Les bloggen vår", "editor": "Redigeringsvisning",
"howto": "Følg våre veiledninger",
"github": "Funnet et problem? Send inn", "github": "Funnet et problem? Send inn",
"textNewLine": "Legg til ny linje (tekst)", "howto": "Følg våre veiledninger",
"or": "eller",
"preventBinding": "Forhindre pilbinding",
"shapes": "Former",
"shortcuts": "Tastatursnarveier",
"textFinish": "Fullfør redigering (tekst)", "textFinish": "Fullfør redigering (tekst)",
"zoomToFit": "Zoom for å passe alle elementene", "textNewLine": "Legg til ny linje (tekst)",
"zoomToSelection": "Zoom til utvalg", "title": "Hjelp",
"preventBinding": "Forhindre pilbinding" "view": "Vis",
"zoomToFit": "Zoom for å se alle elementer",
"zoomToSelection": "Zoom til utvalg"
}, },
"encrypted": { "encrypted": {
"tooltip": "Dine tegninger er ende-til-ende-krypterte slik at Excalidraw sine servere aldri vil se dem." "tooltip": "Dine tegninger er ende-til-ende-krypterte slik at Excalidraw sine servere aldri vil se dem."
@ -232,5 +234,9 @@
"title": "Statistikk for nerder", "title": "Statistikk for nerder",
"total": "Totalt", "total": "Totalt",
"width": "Bredde" "width": "Bredde"
},
"toast": {
"copyStyles": "Kopierte stiler.",
"copyToClipboardAsPng": "Kopiert til utklippstavlen som PNG."
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Fout" "title": "Fout"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Sneltoetsen",
"shapes": "Vormen",
"or": "of",
"click": "klik",
"drag": "slepen",
"curvedArrow": "Gebogen pijl",
"curvedLine": "Gebogen lijn",
"editor": "Editor",
"view": "Weergave",
"blog": "Lees onze blog", "blog": "Lees onze blog",
"click": "klik",
"curvedArrow": "Gebogen pijl",
"curvedLine": "Kromme lijn",
"documentation": "Documentatie",
"drag": "slepen",
"editor": "Editor",
"github": "Probleem gevonden? Verzenden",
"howto": "Volg onze handleidingen", "howto": "Volg onze handleidingen",
"github": "Probleem gevonden? Stuur een nieuwe issue", "or": "of",
"textNewLine": "Nieuwe regel toevoegen (tekst)", "preventBinding": "Pijlbinding voorkomen",
"shapes": "Vormen",
"shortcuts": "Sneltoetsen",
"textFinish": "Voltooi bewerken (tekst)", "textFinish": "Voltooi bewerken (tekst)",
"textNewLine": "Nieuwe regel toevoegen (tekst)",
"title": "Help",
"view": "Weergave",
"zoomToFit": "Zoom in op alle elementen", "zoomToFit": "Zoom in op alle elementen",
"zoomToSelection": "Inzoomen op selectie", "zoomToSelection": "Inzoomen op selectie"
"preventBinding": "Pijlbinding voorkomen"
}, },
"encrypted": { "encrypted": {
"tooltip": "Je tekeningen zijn beveiligd met end-to-end encryptie, dus Excalidraw's servers zullen nooit zien wat je tekent." "tooltip": "Je tekeningen zijn beveiligd met end-to-end encryptie, dus Excalidraw's servers zullen nooit zien wat je tekent."
@ -232,5 +234,9 @@
"title": "Statistieken voor nerds", "title": "Statistieken voor nerds",
"total": "Totaal", "total": "Totaal",
"width": "Breedte" "width": "Breedte"
},
"toast": {
"copyStyles": "Stijlen gekopieerd.",
"copyToClipboardAsPng": "Gekopieerd naar klembord als PNG."
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Feil" "title": "Feil"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Tastatursnarvegar", "blog": "",
"shapes": "Figurar", "click": "",
"or": "eller", "curvedArrow": "",
"click": "klikk", "curvedLine": "",
"drag": "drag", "documentation": "",
"curvedArrow": "Boga pil", "drag": "",
"curvedLine": "Boga linje", "editor": "",
"editor": "Redigering", "github": "",
"view": "Vising", "howto": "",
"blog": "Les bloggen vår", "or": "",
"howto": "Følg vegleiinga vår", "preventBinding": "",
"github": "Funne eit problem? Send inn", "shapes": "",
"textNewLine": "Legg til ny linje (tekst)", "shortcuts": "",
"textFinish": "Fullfør redigering (tekst)", "textFinish": "",
"zoomToFit": "Zoom for å sjå alle elementa", "textNewLine": "",
"zoomToSelection": "Zoom til utval", "title": "",
"preventBinding": "Hindre pilkobling" "view": "",
"zoomToFit": "",
"zoomToSelection": ""
}, },
"encrypted": { "encrypted": {
"tooltip": "Teikningane dine er ende-til-ende-krypterte slik at Excalidraw sine serverar aldri får sjå dei." "tooltip": "Teikningane dine er ende-til-ende-krypterte slik at Excalidraw sine serverar aldri får sjå dei."
@ -232,5 +234,9 @@
"title": "Statistikk for nerdar", "title": "Statistikk for nerdar",
"total": "Totalt", "total": "Totalt",
"width": "Breidde" "width": "Breidde"
},
"toast": {
"copyStyles": "",
"copyToClipboardAsPng": ""
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "ਗਲਤੀ" "title": "ਗਲਤੀ"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "ਕੀਬੋਰਡ ਸ਼ਾਰਟਕੱਟ", "blog": "ਸਾਡਾ ਬਲੌਗ ਪੜ੍ਹੋ",
"shapes": "ਆਕ੍ਰਿਤੀਆਂ",
"or": "ਜਾਂ",
"click": "ਕਲਿੱਕ", "click": "ਕਲਿੱਕ",
"drag": "ਘਸੀਟੋ",
"curvedArrow": "ਵਿੰਗਾ ਤੀਰ", "curvedArrow": "ਵਿੰਗਾ ਤੀਰ",
"curvedLine": "ਵਿੰਗੀ ਲਕੀਰ", "curvedLine": "ਵਿੰਗੀ ਲਕੀਰ",
"documentation": "ਕਾਗਜ਼ਾਤ",
"drag": "ਘਸੀਟੋ",
"editor": "ਸੋਧਕ", "editor": "ਸੋਧਕ",
"view": "ਦਿੱਖ",
"blog": "ਸਾਡਾ ਬਲੌਗ ਪੜ੍ਹੋ",
"howto": "ਸਾਡੀਆਂ ਗਾਈਡਾਂ ਦੀ ਪਾਲਣਾ ਕਰੋ",
"github": "ਕੋਈ ਸਮੱਸਿਆ ਲੱਭੀ? ਜਮ੍ਹਾਂ ਕਰਵਾਓ", "github": "ਕੋਈ ਸਮੱਸਿਆ ਲੱਭੀ? ਜਮ੍ਹਾਂ ਕਰਵਾਓ",
"textNewLine": "ਨਵੀਂ ਪੰਕਤੀ ਜੋੜੋ (ਪਾਠ)", "howto": "ਸਾਡੀਆਂ ਗਾਈਡਾਂ ਦੀ ਪਾਲਣਾ ਕਰੋ",
"or": "ਜਾਂ",
"preventBinding": "ਤੀਰ ਬੱਝਣਾ ਰੋਕੋ",
"shapes": "ਆਕ੍ਰਿਤੀਆਂ",
"shortcuts": "ਕੀਬੋਰਡ ਸ਼ਾਰਟਕੱਟ",
"textFinish": "ਸੋਧ ਮੁਕੰਮਲ ਕਰੋ (ਪਾਠ)", "textFinish": "ਸੋਧ ਮੁਕੰਮਲ ਕਰੋ (ਪਾਠ)",
"textNewLine": "ਨਵੀਂ ਪੰਕਤੀ ਜੋੜੋ (ਪਾਠ)",
"title": "ਮਦਦ",
"view": "ਦਿੱਖ",
"zoomToFit": "ਸਾਰੇ ਐਲੀਮੈਂਟਾਂ ਨੂੰ ਫਿੱਟ ਕਰਨ ਲਈ ਜ਼ੂਮ ਕਰੋ", "zoomToFit": "ਸਾਰੇ ਐਲੀਮੈਂਟਾਂ ਨੂੰ ਫਿੱਟ ਕਰਨ ਲਈ ਜ਼ੂਮ ਕਰੋ",
"zoomToSelection": "ਚੋਣ ਤੱਕ ਜ਼ੂਮ ਕਰੋ", "zoomToSelection": "ਚੋਣ ਤੱਕ ਜ਼ੂਮ ਕਰੋ"
"preventBinding": "ਤੀਰ ਬੱਝਣਾ ਰੋਕੋ"
}, },
"encrypted": { "encrypted": {
"tooltip": "ਤੁਹਾਡੀ ਡਰਾਇੰਗਾਂ ਸਿਰੇ-ਤੋਂ-ਸਿਰੇ ਤੱਕ ਇਨਕਰਿਪਟ ਕੀਤੀਆਂ ਹੋਈਆਂ ਹਨ, ਇਸ ਲਈ Excalidraw ਦੇ ਸਰਵਰ ਉਹਨਾਂ ਨੂੰ ਕਦੇ ਵੀ ਨਹੀਂ ਦੇਖਣਗੇ।" "tooltip": "ਤੁਹਾਡੀ ਡਰਾਇੰਗਾਂ ਸਿਰੇ-ਤੋਂ-ਸਿਰੇ ਤੱਕ ਇਨਕਰਿਪਟ ਕੀਤੀਆਂ ਹੋਈਆਂ ਹਨ, ਇਸ ਲਈ Excalidraw ਦੇ ਸਰਵਰ ਉਹਨਾਂ ਨੂੰ ਕਦੇ ਵੀ ਨਹੀਂ ਦੇਖਣਗੇ।"
@ -232,5 +234,9 @@
"title": "ਪੜਾਕੂਆਂ ਲਈ ਅੰਕੜੇ", "title": "ਪੜਾਕੂਆਂ ਲਈ ਅੰਕੜੇ",
"total": "ਕੁੱਲ", "total": "ਕੁੱਲ",
"width": "ਚੌੜਾਈ" "width": "ਚੌੜਾਈ"
},
"toast": {
"copyStyles": "ਕਾਪੀ ਕੀਤੇ ਸਟਾਇਲ।",
"copyToClipboardAsPng": "ਕਲਿੱਪਬੋਰਡ 'ਤੇ PNG ਵਜੋਂ ਕਾਪੀ ਕੀਤਾ।"
} }
} }

View File

@ -1,34 +1,34 @@
{ {
"ar-SA": 100, "ar-SA": 90,
"bg-BG": 100, "bg-BG": 94,
"ca-ES": 100, "ca-ES": 90,
"de-DE": 100, "de-DE": 100,
"el-GR": 100, "el-GR": 100,
"en": 100, "en": 100,
"es-ES": 100, "es-ES": 100,
"fa-IR": 100, "fa-IR": 98,
"fi-FI": 100, "fi-FI": 100,
"fr-FR": 100, "fr-FR": 100,
"he-IL": 100, "he-IL": 90,
"hi-IN": 100, "hi-IN": 90,
"hu-HU": 100, "hu-HU": 90,
"id-ID": 100, "id-ID": 100,
"it-IT": 100, "it-IT": 100,
"ja-JP": 90, "ja-JP": 81,
"ko-KR": 100, "ko-KR": 90,
"my-MM": 93, "my-MM": 83,
"nb-NO": 100, "nb-NO": 100,
"nl-NL": 100, "nl-NL": 100,
"nn-NO": 100, "nn-NO": 90,
"pa-IN": 100, "pa-IN": 100,
"pl-PL": 100, "pl-PL": 90,
"pt-BR": 100, "pt-BR": 100,
"pt-PT": 100, "pt-PT": 100,
"ro-RO": 100, "ro-RO": 100,
"ru-RU": 100, "ru-RU": 100,
"sk-SK": 100, "sk-SK": 100,
"sv-SE": 100, "sv-SE": 100,
"tr-TR": 100, "tr-TR": 90,
"uk-UA": 100, "uk-UA": 100,
"zh-CN": 100, "zh-CN": 100,
"zh-TW": 100 "zh-TW": 100

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Wystąpił błąd" "title": "Wystąpił błąd"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Skróty klawiszowe", "blog": "",
"shapes": "Kształty", "click": "",
"or": "lub", "curvedArrow": "",
"click": "klik", "curvedLine": "",
"drag": "przeciągnij", "documentation": "",
"curvedArrow": "Zakrzywiona strzałka", "drag": "",
"curvedLine": "Zakrzywiona linia", "editor": "",
"editor": "Edytor", "github": "",
"view": "Widok", "howto": "",
"blog": "Przeczytaj naszego bloga", "or": "",
"howto": "Skorzystaj z instrukcji", "preventBinding": "",
"github": "Znalazłeś problem? Zgłoś go", "shapes": "",
"textNewLine": "Dodaj nową linię (tekst)", "shortcuts": "",
"textFinish": "Zakończ edycję (tekst)", "textFinish": "",
"zoomToFit": "Powiększ, aby wyświetlić wszystkie elementy", "textNewLine": "",
"zoomToSelection": "Przybliż zaznaczenie", "title": "",
"preventBinding": "Zablokuj przywiązanie strzałek do obiektu" "view": "",
"zoomToFit": "",
"zoomToSelection": ""
}, },
"encrypted": { "encrypted": {
"tooltip": "Twoje rysunki są zabezpieczone szyfrowaniem end-to-end, tak więc nawet w Excalidraw nie jesteśmy w stanie zobaczyć tego co tworzysz." "tooltip": "Twoje rysunki są zabezpieczone szyfrowaniem end-to-end, tak więc nawet w Excalidraw nie jesteśmy w stanie zobaczyć tego co tworzysz."
@ -232,5 +234,9 @@
"title": "Statystyki dla nerdów", "title": "Statystyki dla nerdów",
"total": "Łącznie", "total": "Łącznie",
"width": "Szerokość" "width": "Szerokość"
},
"toast": {
"copyStyles": "",
"copyToClipboardAsPng": ""
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Erro" "title": "Erro"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Atalhos de teclado", "blog": "Leia o nosso blog",
"shapes": "Formas",
"or": "ou",
"click": "clicar", "click": "clicar",
"drag": "arrastar",
"curvedArrow": "Seta curva", "curvedArrow": "Seta curva",
"curvedLine": "Linha curva", "curvedLine": "Linha curva",
"documentation": "Documentação",
"drag": "arrastar",
"editor": "Editor", "editor": "Editor",
"view": "Visualizar",
"blog": "Leia o nosso blog",
"howto": "Siga os nossos guias",
"github": "Encontrou algum problema? Nos informe", "github": "Encontrou algum problema? Nos informe",
"textNewLine": "Adicionar nova linha (texto)", "howto": "Siga nossos guias",
"or": "ou",
"preventBinding": "Evitar fixação de seta",
"shapes": "Formas",
"shortcuts": "Atalhos de teclado",
"textFinish": "Finalizar edição (texto)", "textFinish": "Finalizar edição (texto)",
"zoomToFit": "Ajustar para caber todos os elementos", "textNewLine": "Adicionar nova linha (texto)",
"zoomToSelection": "Ampliar a seleção", "title": "Ajudar",
"preventBinding": "Prevenir fixação de seta" "view": "Visualizar",
"zoomToFit": "Ampliar para encaixar todos os elementos",
"zoomToSelection": "Ampliar a seleção"
}, },
"encrypted": { "encrypted": {
"tooltip": "Seus desenhos são criptografados de ponta a ponta, então os servidores do Excalidraw nunca os verão." "tooltip": "Seus desenhos são criptografados de ponta a ponta, então os servidores do Excalidraw nunca os verão."
@ -232,5 +234,9 @@
"title": "Estatísticas para nerds", "title": "Estatísticas para nerds",
"total": "Total", "total": "Total",
"width": "Largura" "width": "Largura"
},
"toast": {
"copyStyles": "Estilos copiados.",
"copyToClipboardAsPng": "Copiado para a área de transferência como PNG."
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Erro" "title": "Erro"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Atalhos de teclado", "blog": "Leia o nosso blog",
"shapes": "Formas",
"or": "ou",
"click": "clicar", "click": "clicar",
"drag": "arrastar",
"curvedArrow": "Seta curva", "curvedArrow": "Seta curva",
"curvedLine": "Linha curva", "curvedLine": "Linha curva",
"documentation": "Documentação",
"drag": "arrastar",
"editor": "Editor", "editor": "Editor",
"view": "Visualizar",
"blog": "Leia o nosso blog",
"howto": "Siga os nossos guias",
"github": "Encontrou algum problema? Nos informe", "github": "Encontrou algum problema? Nos informe",
"textNewLine": "Adicionar nova linha (texto)", "howto": "Siga os nossos guias",
"or": "ou",
"preventBinding": "Prevenir fixação de seta",
"shapes": "Formas",
"shortcuts": "Atalhos de teclado",
"textFinish": "Finalizar edição (texto)", "textFinish": "Finalizar edição (texto)",
"textNewLine": "Adicionar nova linha (texto)",
"title": "Ajuda",
"view": "Visualizar",
"zoomToFit": "Ajustar para caber todos os elementos", "zoomToFit": "Ajustar para caber todos os elementos",
"zoomToSelection": "Ampliar a seleção", "zoomToSelection": "Ampliar a seleção"
"preventBinding": "Prevenir fixação de seta"
}, },
"encrypted": { "encrypted": {
"tooltip": "Seus desenhos são criptografados de ponta a ponta, então os servidores do Excalidraw nunca os verão." "tooltip": "Seus desenhos são criptografados de ponta a ponta, então os servidores do Excalidraw nunca os verão."
@ -232,5 +234,9 @@
"title": "Estatísticas para nerds", "title": "Estatísticas para nerds",
"total": "Total", "total": "Total",
"width": "Largura" "width": "Largura"
},
"toast": {
"copyStyles": "Estilos copiados.",
"copyToClipboardAsPng": "Copiado para o clipboard como PNG."
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Eroare" "title": "Eroare"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Comenzi rapide de la tastatură", "blog": "Citește blogul nostru",
"shapes": "Forme",
"or": "sau",
"click": "clic", "click": "clic",
"drag": "glisare",
"curvedArrow": "Săgeată curbată", "curvedArrow": "Săgeată curbată",
"curvedLine": "Linie curbată", "curvedLine": "Linie curbată",
"documentation": "Documentație",
"drag": "glisare",
"editor": "Editor", "editor": "Editor",
"view": "Vizualizare",
"blog": "Citește blogul nostru",
"howto": "Urmărește ghidurile noastre",
"github": "Ai întâmpinat o problemă? Trimite un raport", "github": "Ai întâmpinat o problemă? Trimite un raport",
"textNewLine": "Adaugă o linie nouă (text)", "howto": "Urmărește ghidurile noastre",
"or": "sau",
"preventBinding": "Împiedică legarea săgeții",
"shapes": "Forme",
"shortcuts": "Comenzi rapide de la tastatură",
"textFinish": "Finalizează editarea (text)", "textFinish": "Finalizează editarea (text)",
"zoomToFit": "Apropiere/depărtare pentru a cuprinde totul", "textNewLine": "Adaugă o linie nouă (text)",
"zoomToSelection": "Panoramare la selecție", "title": "Ajutor",
"preventBinding": "Împiedică legarea săgeții" "view": "Vizualizare",
"zoomToFit": "Panoramare pentru a cuprinde totul",
"zoomToSelection": "Panoramare la selecție"
}, },
"encrypted": { "encrypted": {
"tooltip": "Desenele tale sunt criptate integral, astfel că serverele Excalidraw nu le vor vedea niciodată." "tooltip": "Desenele tale sunt criptate integral, astfel că serverele Excalidraw nu le vor vedea niciodată."
@ -232,5 +234,9 @@
"title": "Statistici pentru pasionați", "title": "Statistici pentru pasionați",
"total": "Total", "total": "Total",
"width": "Lățime" "width": "Lățime"
},
"toast": {
"copyStyles": "Stiluri copiate.",
"copyToClipboardAsPng": "Copiat în memoria temporară ca PNG."
} }
} }

View File

@ -41,8 +41,8 @@
"withBackground": "С фоном", "withBackground": "С фоном",
"exportEmbedScene": "Встроить информацию о сцене в экспортируемый файл", "exportEmbedScene": "Встроить информацию о сцене в экспортируемый файл",
"exportEmbedScene_details": "Сцена будет сохранена в PNG/SVG файл так, чтобы всю сцену можно будет восстановить из этого файла. Это увеличит размер файла.", "exportEmbedScene_details": "Сцена будет сохранена в PNG/SVG файл так, чтобы всю сцену можно будет восстановить из этого файла. Это увеличит размер файла.",
"addWatermark": "Добавить \"Сделано с Excalidraw\"", "addWatermark": "Добавить «Создано в Excalidraw»",
"handDrawn": "Нарисованный от руки", "handDrawn": "От руки",
"normal": "Обычный", "normal": "Обычный",
"code": "Код", "code": "Код",
"small": "Малый", "small": "Малый",
@ -64,11 +64,11 @@
"fileTitle": "Название файла", "fileTitle": "Название файла",
"colorPicker": "Выбор цвета", "colorPicker": "Выбор цвета",
"canvasBackground": "Фон холста", "canvasBackground": "Фон холста",
"drawingCanvas": "Холст для рисования", "drawingCanvas": "Полотно",
"layers": "Слои", "layers": "Слои",
"actions": "Действия", "actions": "Действия",
"language": "Язык", "language": "Язык",
"createRoom": "Создать многопользовательскую сессию", "createRoom": "Начать сеанс совместной работы",
"duplicateSelection": "Дубликат", "duplicateSelection": "Дубликат",
"untitled": "Безымянный", "untitled": "Безымянный",
"name": "Имя", "name": "Имя",
@ -189,34 +189,36 @@
}, },
"roomDialog": { "roomDialog": {
"desc_intro": "Вы можете пригласить людей в текущую сцену для совместной работы.", "desc_intro": "Вы можете пригласить людей в текущую сцену для совместной работы.",
"desc_privacy": "Не беспокойтесь, сессия использует сквозное шифрование, поэтому всё что вы нарисуете останется приватным. Ваша информация не будет доступна даже на наших серверах.", "desc_privacy": "Не беспокойтесь — во время сеанса используется сквозное шифрование. Всё, что вы нарисуете, останется конфиденциальным и не будет доступно даже нашему серверу.",
"button_startSession": "Начать сеанс", "button_startSession": "Начать сеанс",
"button_stopSession": "Завершить сеанс", "button_stopSession": "Завершить сеанс",
"desc_inProgressIntro": "Совместная сессия теперь активна.", "desc_inProgressIntro": "Сеанс совместной работы запущен.",
"desc_shareLink": "Поделитесь этой ссылкой со всеми участниками:", "desc_shareLink": "Поделитесь этой ссылкой со всеми участниками:",
"desc_exitSession": "Завершив сеанс, вы выйдете из комнаты, но сможете продолжить работать с документом локально. Это не повлияет на работу других пользователей — они смогут продолжить совместную работу с их версией документа." "desc_exitSession": "Завершив сеанс, вы выйдете из комнаты, но сможете продолжить работать с документом локально. Это не повлияет на работу других пользователей — они смогут продолжить совместную работу с их версией документа."
}, },
"errorDialog": { "errorDialog": {
"title": "Ошибка" "title": "Ошибка"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Сочетания клавиш", "blog": "Прочитайте наш блог",
"shapes": "Фигуры",
"or": "или",
"click": "нажать", "click": "нажать",
"drag": "перетащить",
"curvedArrow": "Изогнутая стрелка", "curvedArrow": "Изогнутая стрелка",
"curvedLine": "Изогнутая линия", "curvedLine": "Изогнутая линия",
"documentation": "Документация",
"drag": "перетащить",
"editor": "Редактор", "editor": "Редактор",
"view": "Просмотр",
"blog": "Прочитайте наш блог",
"howto": "Следуйте нашим инструкциям",
"github": "Нашли проблему? Отправьте", "github": "Нашли проблему? Отправьте",
"textNewLine": "Добавить новую строку (текст)", "howto": "Следуйте нашим инструкциям",
"or": "или",
"preventBinding": "Предотвращать привязку стрелок",
"shapes": "Фигуры",
"shortcuts": "Горячие клавиши",
"textFinish": "Закончить редактирование (текст)", "textFinish": "Закончить редактирование (текст)",
"textNewLine": "Добавить новую строку (текст)",
"title": "Помощь",
"view": "Просмотр",
"zoomToFit": "Отмастштабировать, чтобы поместились все элементы", "zoomToFit": "Отмастштабировать, чтобы поместились все элементы",
"zoomToSelection": "Перейти к выделенному", "zoomToSelection": "Увеличить до выделенного"
"preventBinding": "Предотвратить привязку стрелок"
}, },
"encrypted": { "encrypted": {
"tooltip": "Ваши данные защищены сквозным (End-to-end) шифрованием. Серверы Excalidraw никогда не получат доступ к ним." "tooltip": "Ваши данные защищены сквозным (End-to-end) шифрованием. Серверы Excalidraw никогда не получат доступ к ним."
@ -232,5 +234,9 @@
"title": "Статистика для ботаников", "title": "Статистика для ботаников",
"total": "Всего", "total": "Всего",
"width": "Ширина" "width": "Ширина"
},
"toast": {
"copyStyles": "Скопированы стили.",
"copyToClipboardAsPng": "Скопировано в буфер обмена в формате PNG."
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Chyba" "title": "Chyba"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Klávesové skratky", "blog": "Prečítajte si náš blog",
"shapes": "Tvary",
"or": "alebo",
"click": "kliknutie", "click": "kliknutie",
"drag": "potiahnutie",
"curvedArrow": "Zakrivená šípka", "curvedArrow": "Zakrivená šípka",
"curvedLine": "Zakrivená čiara", "curvedLine": "Zakrivená čiara",
"documentation": "Dokumentácia",
"drag": "potiahnutie",
"editor": "Editovanie", "editor": "Editovanie",
"view": "Zobrazenie",
"blog": "Prečítajte si náš blog",
"howto": "Postupujte podľa naších návodov",
"github": "Objavili ste problém? Nahláste ho", "github": "Objavili ste problém? Nahláste ho",
"textNewLine": "Vložiť nový riadok (text)", "howto": "Postupujte podľa naších návodov",
"or": "alebo",
"preventBinding": "Zakázať pripájanie šípky",
"shapes": "Tvary",
"shortcuts": "Klávesové skratky",
"textFinish": "Ukončenie editovania (text)", "textFinish": "Ukončenie editovania (text)",
"textNewLine": "Vložiť nový riadok (text)",
"title": "Pomocník",
"view": "Zobrazenie",
"zoomToFit": "Priblížiť aby boli zahrnuté všetky prvky", "zoomToFit": "Priblížiť aby boli zahrnuté všetky prvky",
"zoomToSelection": "Priblížiť na výber", "zoomToSelection": "Priblížiť na výber"
"preventBinding": "Zakázať pripájanie šípky"
}, },
"encrypted": { "encrypted": {
"tooltip": "Vaše kresby používajú end-to-end šifrovanie, takže ich Excalidraw server nedokáže prečítať." "tooltip": "Vaše kresby používajú end-to-end šifrovanie, takže ich Excalidraw server nedokáže prečítať."
@ -232,5 +234,9 @@
"title": "Štatistiky", "title": "Štatistiky",
"total": "Celkom", "total": "Celkom",
"width": "Šírka" "width": "Šírka"
},
"toast": {
"copyStyles": "Štýly skopírované.",
"copyToClipboardAsPng": "Skopírované do schránky ako PNG."
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Fel" "title": "Fel"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Tangentbordsgenvägar", "blog": "Läs vår blogg",
"shapes": "Former",
"or": "eller",
"click": "klicka", "click": "klicka",
"drag": "dra",
"curvedArrow": "Böjd pil", "curvedArrow": "Böjd pil",
"curvedLine": "Böjd linje", "curvedLine": "Böjd linje",
"documentation": "Dokumentation",
"drag": "dra",
"editor": "Redigerare", "editor": "Redigerare",
"view": "Visa",
"blog": "Läs vår blogg",
"howto": "Följ våra guider",
"github": "Hittat ett problem? Rapportera", "github": "Hittat ett problem? Rapportera",
"textNewLine": "Lägg till ny rad (text)", "howto": "Följ våra guider",
"or": "eller",
"preventBinding": "Förhindra pilbindning",
"shapes": "Former",
"shortcuts": "Tangentbordsgenvägar",
"textFinish": "Slutför redigering (text)", "textFinish": "Slutför redigering (text)",
"textNewLine": "Lägg till ny rad (text)",
"title": "Hjälp",
"view": "Visa",
"zoomToFit": "Zooma för att rymma alla element", "zoomToFit": "Zooma för att rymma alla element",
"zoomToSelection": "Zooma till markering", "zoomToSelection": "Zooma till markering"
"preventBinding": "Förhindra pilbindning"
}, },
"encrypted": { "encrypted": {
"tooltip": "Dina skisser är krypterade från ände till ände så Excalidraws servrar kommer aldrig att se dem." "tooltip": "Dina skisser är krypterade från ände till ände så Excalidraws servrar kommer aldrig att se dem."
@ -232,5 +234,9 @@
"title": "Statistik för nördar", "title": "Statistik för nördar",
"total": "Totalt", "total": "Totalt",
"width": "Bredd" "width": "Bredd"
},
"toast": {
"copyStyles": "Kopierade stilar.",
"copyToClipboardAsPng": "Kopierat till urklipp som PNG."
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Hata" "title": "Hata"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Klavye kısayolları", "blog": "",
"shapes": "Şekiller", "click": "",
"or": "veya", "curvedArrow": "",
"click": "tıkla", "curvedLine": "",
"drag": "sürükle", "documentation": "",
"curvedArrow": "Eğri ok", "drag": "",
"curvedLine": "Eğri çizgi", "editor": "",
"editor": "Düzenleyici", "github": "",
"view": "Görüntüle", "howto": "",
"blog": "Blog'umuzu okuyun", "or": "",
"howto": "Rehberlerimizi takip edin", "preventBinding": "",
"github": "Bir hata mı buldun? Bildir", "shapes": "",
"textNewLine": "Yeni satır ekle (yazı)", "shortcuts": "",
"textFinish": "(Yazıyı) düzenlemeyi bitir", "textFinish": "",
"zoomToFit": "Tüm öğeleri sığdırmak için yakınlaştır", "textNewLine": "",
"zoomToSelection": "Seçime yaklaş", "title": "",
"preventBinding": "Ok bağlamayı önleyin" "view": "",
"zoomToFit": "",
"zoomToSelection": ""
}, },
"encrypted": { "encrypted": {
"tooltip": "Çizimleriniz uçtan-uca şifrelenmiştir, Excalidraw'ın sunucuları bile onları göremez." "tooltip": "Çizimleriniz uçtan-uca şifrelenmiştir, Excalidraw'ın sunucuları bile onları göremez."
@ -232,5 +234,9 @@
"title": "İnekler için istatistikler", "title": "İnekler için istatistikler",
"total": "Toplam", "total": "Toplam",
"width": "Genişlik" "width": "Genişlik"
},
"toast": {
"copyStyles": "",
"copyToClipboardAsPng": ""
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "Помилка" "title": "Помилка"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "Гарячі клавіші", "blog": "Наш блог",
"shapes": "Фігури",
"or": "або",
"click": "натиснути", "click": "натиснути",
"curvedArrow": "Крива стрілка",
"curvedLine": "Крива лінія",
"documentation": "Документація",
"drag": "перетягнути", "drag": "перетягнути",
"curvedArrow": "Вигнута стрілка",
"curvedLine": "Вигнута лінія",
"editor": "Редактор", "editor": "Редактор",
"view": "Вигляд", "github": "Знайшли помилку? Повідомте",
"blog": "Читайте наш блог",
"howto": "Дотримуйтесь наших інструкцій", "howto": "Дотримуйтесь наших інструкцій",
"github": "Знайшли помилку? Повідомте!", "or": "або",
"textNewLine": "Додати новий рядок (текст)", "preventBinding": "Запобігти зв'язування зі стрілками",
"shapes": "Фігури",
"shortcuts": "Гарячі клавіші",
"textFinish": "Завершити редагування (текст)", "textFinish": "Завершити редагування (текст)",
"zoomToFit": "Збільшити щоб умістити все", "textNewLine": "Додати новий рядок (текст)",
"zoomToSelection": "Перейти до виділеного", "title": "Допомога",
"preventBinding": "Запобігти зв'язування зі стрілками" "view": "Вигляд",
"zoomToFit": "Збільшити щоб умістити всі елементи",
"zoomToSelection": "Наблизити вибране"
}, },
"encrypted": { "encrypted": {
"tooltip": "Ваші креслення захищені наскрізним шифруванням — сервери Excalidraw ніколи їх не побачать." "tooltip": "Ваші креслення захищені наскрізним шифруванням — сервери Excalidraw ніколи їх не побачать."
@ -232,5 +234,9 @@
"title": "Статистика", "title": "Статистика",
"total": "Всього", "total": "Всього",
"width": "Ширина" "width": "Ширина"
},
"toast": {
"copyStyles": "Скопійовані стилі.",
"copyToClipboardAsPng": "Скопійовано в буфер обміну як PNG."
} }
} }

View File

@ -60,10 +60,10 @@
"extraBold": "超粗", "extraBold": "超粗",
"architect": "朴素", "architect": "朴素",
"artist": "艺术", "artist": "艺术",
"cartoonist": "漫画", "cartoonist": "漫画",
"fileTitle": "文件标题", "fileTitle": "文件标题",
"colorPicker": "调色盘", "colorPicker": "调色盘",
"canvasBackground": "Canvas 背景", "canvasBackground": "画布背景",
"drawingCanvas": "绘制 Canvas", "drawingCanvas": "绘制 Canvas",
"layers": "图层", "layers": "图层",
"actions": "操作", "actions": "操作",
@ -128,10 +128,10 @@
"clearReset": "这将会清除整个 画板。您是否要继续?", "clearReset": "这将会清除整个 画板。您是否要继续?",
"couldNotCreateShareableLink": "无法创建共享链接", "couldNotCreateShareableLink": "无法创建共享链接",
"couldNotCreateShareableLinkTooBig": "无法创建可共享链接:画布过大", "couldNotCreateShareableLinkTooBig": "无法创建可共享链接:画布过大",
"couldNotLoadInvalidFile": "无法加载错误文件", "couldNotLoadInvalidFile": "无法加载无效的文件",
"importBackendFailed": "从后端导入失败", "importBackendFailed": "从后端导入失败",
"cannotExportEmptyCanvas": "无法导出空画布。", "cannotExportEmptyCanvas": "无法导出空画布。",
"couldNotCopyToClipboard": "无法复制到剪贴板请尝试使用 Chrome 浏览器。", "couldNotCopyToClipboard": "无法复制到剪贴板请尝试使用 Chrome 浏览器。",
"decryptFailed": "无法解密数据。", "decryptFailed": "无法解密数据。",
"uploadedSecurly": "上传已被端到端加密保护,这意味着 Excalidraw 的服务器和第三方都无法读取内容。", "uploadedSecurly": "上传已被端到端加密保护,这意味着 Excalidraw 的服务器和第三方都无法读取内容。",
"loadSceneOverridePrompt": "加载外部绘图将取代您现有的内容。您想要继续吗?", "loadSceneOverridePrompt": "加载外部绘图将取代您现有的内容。您想要继续吗?",
@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "错误" "title": "错误"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "快捷键列表",
"shapes": "形状",
"or": "或",
"click": "点击",
"drag": "拖动",
"curvedArrow": "曲线(带有箭头)",
"curvedLine": "曲线(无箭头)",
"editor": "编辑器",
"view": "视图",
"blog": "浏览我们的博客", "blog": "浏览我们的博客",
"howto": "跟随我们的指南", "click": "单击",
"github": "发现问题?请提出来", "curvedArrow": "曲线箭头",
"curvedLine": "曲线",
"documentation": "文档",
"drag": "拖动",
"editor": "编辑器",
"github": "提交问题",
"howto": "帮助文档",
"or": "或",
"preventBinding": "禁用箭头吸附",
"shapes": "形状",
"shortcuts": "快捷键列表",
"textFinish": "完成文本编辑",
"textNewLine": "文本换行", "textNewLine": "文本换行",
"textFinish": "完成编辑文本", "title": "帮助",
"view": "视图",
"zoomToFit": "缩放以适应所有元素", "zoomToFit": "缩放以适应所有元素",
"zoomToSelection": "缩放至选择部分", "zoomToSelection": "缩放到选区"
"preventBinding": "防止箭头吸附"
}, },
"encrypted": { "encrypted": {
"tooltip": "您的绘图采用的端到端加密其内容对于Excalidraw服务器是不可见的。" "tooltip": "您的绘图采用的端到端加密其内容对于Excalidraw服务器是不可见的。"
@ -232,5 +234,9 @@
"title": "详细统计信息", "title": "详细统计信息",
"total": "总计", "total": "总计",
"width": "宽度" "width": "宽度"
},
"toast": {
"copyStyles": "复制样式",
"copyToClipboardAsPng": "复制为 PNG 到剪贴板"
} }
} }

View File

@ -199,24 +199,26 @@
"errorDialog": { "errorDialog": {
"title": "錯誤" "title": "錯誤"
}, },
"shortcutsDialog": { "helpDialog": {
"title": "鍵盤快速鍵",
"shapes": "形狀",
"or": "或",
"click": "點擊",
"drag": "拖曳",
"curvedArrow": "箭頭曲線",
"curvedLine": "曲線",
"editor": "編輯器",
"view": "檢視",
"blog": "閱讀部落格", "blog": "閱讀部落格",
"howto": "官方指南", "click": "點擊",
"github": "發現問題?回報 issue", "curvedArrow": "曲箭頭",
"textNewLine": "換行(文字)", "curvedLine": "曲線",
"textFinish": "完成編輯(文字)", "documentation": "文件",
"drag": "拖曳",
"editor": "編輯器",
"github": "發現異常?回報問題",
"howto": "參照我們的說明",
"or": "或",
"preventBinding": "避免箭號連結",
"shapes": "形狀",
"shortcuts": "鍵盤快速鍵",
"textFinish": "完成編輯 (文字)",
"textNewLine": "換行 (文字)",
"title": "說明",
"view": "檢視",
"zoomToFit": "放大至填滿畫面", "zoomToFit": "放大至填滿畫面",
"zoomToSelection": "縮放至選取區", "zoomToSelection": "縮放至選取區"
"preventBinding": "防止箭頭綁定"
}, },
"encrypted": { "encrypted": {
"tooltip": "你的作品已使用 end-to-end 方式加密Excalidraw 的伺服器也無法取得其內容。" "tooltip": "你的作品已使用 end-to-end 方式加密Excalidraw 的伺服器也無法取得其內容。"
@ -232,5 +234,9 @@
"title": "詳細統計", "title": "詳細統計",
"total": "合計", "total": "合計",
"width": "寬度" "width": "寬度"
},
"toast": {
"copyStyles": "已複製樣式",
"copyToClipboardAsPng": "已複製 PNG 至剪貼簿"
} }
} }

View File

@ -12,6 +12,31 @@ The change should be grouped under one of the below section and must contain PR
Please add the latest change on the top under the correct section. Please add the latest change on the top under the correct section.
--> -->
## [Unreleased]
## Excalidraw API
- Expose `getAppState` on `excalidrawRef` [#2834](https://github.com/excalidraw/excalidraw/pull/2834).
## Excalidraw Library
### Features
- Remove `copy`, `cut`, and `paste` actions from contextmenu [#2872](https://github.com/excalidraw/excalidraw/pull/2872)
- Support `Ctrl-Y` shortcut to redo on Windows [#2831](https://github.com/excalidraw/excalidraw/pull/2831).
### Fixes
- Fix remote pointers not accounting for offset [#2855](https://github.com/excalidraw/excalidraw/pull/2855).
## 0.2.1
## Excalidraw API
### Build
- Bundle css files with js [#2819](https://github.com/excalidraw/excalidraw/pull/2819). The host would not need to import css files separately.
## 0.2.0 ## 0.2.0
## Excalidraw API ## Excalidraw API

View File

@ -31,9 +31,6 @@ import React, { useEffect, useState, createRef } from "react";
import Excalidraw from "@excalidraw/excalidraw"; import Excalidraw from "@excalidraw/excalidraw";
import InitialData from "./initialData"; import InitialData from "./initialData";
import "@excalidraw/excalidraw/dist/excalidraw.min.css";
import "@excalidraw/excalidraw/dist/fonts.min.css";
import "./styles.css"; import "./styles.css";
export default function App() { export default function App() {
@ -150,7 +147,7 @@ export default function App() {
<pre> <pre>
import { getSceneVersion } from "@excalidraw/excalidraw"; import { getSceneVersion } from "@excalidraw/excalidraw";
getSceneVersion(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement []</a>) getSceneVersion(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>)
</pre> </pre>
This function returns the current scene version. This function returns the current scene version.
@ -160,7 +157,7 @@ This function returns the current scene version.
**_Signature_** **_Signature_**
<pre> <pre>
getSyncableElements(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement []</a>):<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement []</a> getSyncableElements(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>):<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>
</pre> </pre>
**How to use** **How to use**
@ -176,7 +173,7 @@ This function returns all the deleted elements of the scene.
**_Signature_** **_Signature_**
<pre> <pre>
getElementsMap(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement []</a>): {[id: string]: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement</a>} getElementsMap(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>): {[id: string]: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement</a>}
</pre> </pre>
**How to use** **How to use**
@ -223,7 +220,7 @@ This helps to load Excalidraw with `initialData`. It must be an object or a [pro
| name | type | | name | type |
| --- | --- | | --- | --- |
| elements | [ExcalidrawElement []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78) | | elements | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78) |
| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L37) | | appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L37) |
```json ```json
@ -271,8 +268,9 @@ You can pass a `ref` when you want to access some excalidraw APIs. We expose the
| readyPromise | [resolvablePromise](https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317) | This promise will be resolved with the api once excalidraw has rendered. This will be helpful when you want do some action on the host app once this promise resolves. For this to work you will have to pass ref as shown [here](#readyPromise) | | readyPromise | [resolvablePromise](https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317) | This promise will be resolved with the api once excalidraw has rendered. This will be helpful when you want do some action on the host app once this promise resolves. For this to work you will have to pass ref as shown [here](#readyPromise) |
| updateScene | <pre>(<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L189">sceneData</a>)) => void </pre> | updates the scene with the sceneData | | updateScene | <pre>(<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L189">sceneData</a>)) => void </pre> | updates the scene with the sceneData |
| resetScene | `({ resetLoadingState: boolean }) => void` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. | | resetScene | `({ resetLoadingState: boolean }) => void` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. |
| getSceneElementsIncludingDeleted | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement []</a></pre> | Returns all the elements including the deleted in the scene | | getSceneElementsIncludingDeleted | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a></pre> | Returns all the elements including the deleted in the scene |
| getSceneElements | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement []</a></pre> | Returns all the elements excluding the deleted in the scene | | getSceneElements | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a></pre> | Returns all the elements excluding the deleted in the scene |
| getAppState | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L37">AppState</a></pre> | Returns current appState |
| history | `{ clear: () => void }` | This is the history API. `history.clear()` will clear the history | | history | `{ clear: () => void }` | This is the history API. `history.clear()` will clear the history |
| setScrollToCenter | <pre> (<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>) => void </pre> | sets the elements to center | | setScrollToCenter | <pre> (<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>) => void </pre> | sets the elements to center |
@ -327,7 +325,7 @@ import { defaultLang, languages } from "@excalidraw/excalidraw";
| name | type | | name | type |
| --- | --- | | --- | --- |
| defaultLang | string | | defaultLang | string |
| languages | [Language []](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L8) | | languages | [Language[]](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L8) |
#### `renderFooter` #### `renderFooter`

View File

@ -0,0 +1,6 @@
import Excalidraw from "./index";
import "../../../public/fonts.css";
export default Excalidraw;
export * from "./index";

View File

@ -1,6 +1,6 @@
{ {
"name": "@excalidraw/excalidraw", "name": "@excalidraw/excalidraw",
"version": "0.2.0", "version": "0.2.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -1182,9 +1182,9 @@
} }
}, },
"@types/estree": { "@types/estree": {
"version": "0.0.45", "version": "0.0.46",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.45.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.46.tgz",
"integrity": "sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==", "integrity": "sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg==",
"dev": true "dev": true
}, },
"@types/json-schema": { "@types/json-schema": {
@ -1345,6 +1345,12 @@
"@xtuc/long": "4.2.2" "@xtuc/long": "4.2.2"
} }
}, },
"@webpack-cli/configtest": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.0.0.tgz",
"integrity": "sha512-Un0SdBoN1h4ACnIO7EiCjWuyhNI0Jl96JC+63q6xi4HDUYRZn8Auluea9D+v9NWKc5J4sICVEltdBaVjLX39xw==",
"dev": true
},
"@webpack-cli/info": { "@webpack-cli/info": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.2.1.tgz",
@ -1355,9 +1361,9 @@
} }
}, },
"@webpack-cli/serve": { "@webpack-cli/serve": {
"version": "1.2.1", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.2.2.tgz",
"integrity": "sha512-Zj1z6AyS+vqV6Hfi7ngCjFGdHV5EwZNIHo6QfFTNe9PyW+zBU1zJ9BiOW1pmUEq950RC4+Dym6flyA/61/vhyw==", "integrity": "sha512-03GkWxcgFfm8+WIwcsqJb9agrSDNDDoxaNnexPnCCexP5SCE4IgFd9lNpSy+K2nFqVMpgTFw6SwbmVAVTndVew==",
"dev": true "dev": true
}, },
"@xtuc/ieee754": { "@xtuc/ieee754": {
@ -1379,9 +1385,9 @@
"dev": true "dev": true
}, },
"acorn-walk": { "acorn-walk": {
"version": "8.0.0", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.0.0.tgz", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.0.1.tgz",
"integrity": "sha512-oZRad/3SMOI/pxbbmqyurIx7jHw1wZDcR9G44L8pUVFEomX/0dH89SrM1KaDXuv1NpzAXz6Op/Xu/Qd5XXzdEA==", "integrity": "sha512-zn/7dYtoTVkG4EoMU55QlQU4F+m+T7Kren6Vj3C2DapWPnakG/DL9Ns5aPAPW5Ixd3uxXrV/BoMKKVFIazPcdg==",
"dev": true "dev": true
}, },
"ajv": { "ajv": {
@ -1711,6 +1717,17 @@
"tslib": "^1.9.0" "tslib": "^1.9.0"
} }
}, },
"clone-deep": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
"integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
"dev": true,
"requires": {
"is-plain-object": "^2.0.4",
"kind-of": "^6.0.2",
"shallow-clone": "^3.0.0"
}
},
"color-convert": { "color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@ -2245,6 +2262,15 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true "dev": true
}, },
"is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
"integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
"dev": true,
"requires": {
"isobject": "^3.0.1"
}
},
"is-stream": { "is-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
@ -2263,6 +2289,12 @@
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"dev": true "dev": true
}, },
"isobject": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true
},
"jest-worker": { "jest-worker": {
"version": "26.6.2", "version": "26.6.2",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz",
@ -2324,6 +2356,12 @@
"minimist": "^1.2.5" "minimist": "^1.2.5"
} }
}, },
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"dev": true
},
"klona": { "klona": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz",
@ -2435,9 +2473,9 @@
} }
}, },
"mime": { "mime": {
"version": "2.4.7", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.7.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.0.tgz",
"integrity": "sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==", "integrity": "sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag==",
"dev": true "dev": true
}, },
"mime-db": { "mime-db": {
@ -2462,9 +2500,9 @@
"dev": true "dev": true
}, },
"mini-css-extract-plugin": { "mini-css-extract-plugin": {
"version": "1.3.4", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.4.tgz", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.5.tgz",
"integrity": "sha512-dNjqyeogUd8ucUgw5sxm1ahvSfSUgef7smbmATRSbDm4EmNx5kQA6VdUEhEeCKSjX6CTYjb5vxgMUvRjqP3uHg==", "integrity": "sha512-tvmzcwqJJXau4OQE5vT72pRT18o2zF+tQJp8CWchqvfQnTlflkzS+dANYcRdyPRWUWRkfmeNTKltx0NZI/b5dQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"loader-utils": "^2.0.0", "loader-utils": "^2.0.0",
@ -2918,6 +2956,15 @@
"randombytes": "^2.1.0" "randombytes": "^2.1.0"
} }
}, },
"shallow-clone": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
"integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
"dev": true,
"requires": {
"kind-of": "^6.0.2"
}
},
"shebang-command": { "shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -3257,13 +3304,13 @@
} }
}, },
"webpack": { "webpack": {
"version": "5.15.0", "version": "5.19.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.15.0.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.19.0.tgz",
"integrity": "sha512-y/xG+ONDz78yn3VvP6gAvGr1/gkxOgitvHSXBmquyN8KDtrGEyE3K9WkXOPB7QmfcOBCpO4ELXwNcCYQnEmexA==", "integrity": "sha512-egX19vAQ8fZ4cVYtA9Y941eqJtcZAK68mQq87MMv+GTXKZOc3TpKBBxdGX+HXUYlquPxiluNsJ1VHvwwklW7CQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/eslint-scope": "^3.7.0", "@types/eslint-scope": "^3.7.0",
"@types/estree": "^0.0.45", "@types/estree": "^0.0.46",
"@webassemblyjs/ast": "1.11.0", "@webassemblyjs/ast": "1.11.0",
"@webassemblyjs/wasm-edit": "1.11.0", "@webassemblyjs/wasm-edit": "1.11.0",
"@webassemblyjs/wasm-parser": "1.11.0", "@webassemblyjs/wasm-parser": "1.11.0",
@ -3380,9 +3427,9 @@
} }
}, },
"webpack-bundle-analyzer": { "webpack-bundle-analyzer": {
"version": "4.3.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.3.0.tgz", "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.0.tgz",
"integrity": "sha512-J3TPm54bPARx6QG8z4cKBszahnUglcv70+N+8gUqv2I5KOFHJbzBiLx+pAp606so0X004fxM7hqRu10MLjJifA==", "integrity": "sha512-9DhNa+aXpqdHk8LkLPTBU/dMfl84Y+WE2+KnfI6rSpNRNVKa0VGLjPd2pjFubDeqnWmulFggxmWBxhfJXZnR0g==",
"dev": true, "dev": true,
"requires": { "requires": {
"acorn": "^8.0.4", "acorn": "^8.0.4",
@ -3454,14 +3501,15 @@
} }
}, },
"webpack-cli": { "webpack-cli": {
"version": "4.3.1", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.3.1.tgz", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.4.0.tgz",
"integrity": "sha512-/F4+9QNZM/qKzzL9/06Am8NXIkGV+/NqQ62Dx7DSqudxxpAgBqYn6V7+zp+0Y7JuWksKUbczRY3wMTd+7Uj6OA==", "integrity": "sha512-/Qh07CXfXEkMu5S8wEpjuaw2Zj/CC0hf/qbTDp6N8N7JjdGuaOjZ7kttz+zhuJO/J5m7alQEhNk9lsc4rC6xgQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@discoveryjs/json-ext": "^0.5.0", "@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^1.0.0",
"@webpack-cli/info": "^1.2.1", "@webpack-cli/info": "^1.2.1",
"@webpack-cli/serve": "^1.2.1", "@webpack-cli/serve": "^1.2.2",
"colorette": "^1.2.1", "colorette": "^1.2.1",
"commander": "^6.2.0", "commander": "^6.2.0",
"enquirer": "^2.3.6", "enquirer": "^2.3.6",
@ -3471,7 +3519,7 @@
"interpret": "^2.2.0", "interpret": "^2.2.0",
"rechoir": "^0.7.0", "rechoir": "^0.7.0",
"v8-compile-cache": "^2.2.0", "v8-compile-cache": "^2.2.0",
"webpack-merge": "^4.2.2" "webpack-merge": "^5.7.3"
}, },
"dependencies": { "dependencies": {
"commander": { "commander": {
@ -3483,12 +3531,13 @@
} }
}, },
"webpack-merge": { "webpack-merge": {
"version": "4.2.2", "version": "5.7.3",
"resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.7.3.tgz",
"integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==", "integrity": "sha512-6/JUQv0ELQ1igjGDzHkXbVDRxkfA57Zw7PfiupdLFJYrgFqY5ZP8xxbpp2lU3EPwYx89ht5Z/aDkD40hFCm5AA==",
"dev": true, "dev": true,
"requires": { "requires": {
"lodash": "^4.17.15" "clone-deep": "^4.0.1",
"wildcard": "^2.0.0"
} }
}, },
"webpack-sources": { "webpack-sources": {
@ -3518,10 +3567,16 @@
"isexe": "^2.0.0" "isexe": "^2.0.0"
} }
}, },
"wildcard": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz",
"integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==",
"dev": true
},
"ws": { "ws": {
"version": "7.4.1", "version": "7.4.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz",
"integrity": "sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ==", "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==",
"dev": true "dev": true
}, },
"yallist": { "yallist": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@excalidraw/excalidraw", "name": "@excalidraw/excalidraw",
"version": "0.2.0", "version": "0.2.1",
"main": "dist/excalidraw.min.js", "main": "dist/excalidraw.min.js",
"files": [ "files": [
"dist/*" "dist/*"
@ -54,13 +54,13 @@
"cross-env": "7.0.3", "cross-env": "7.0.3",
"css-loader": "5.0.1", "css-loader": "5.0.1",
"file-loader": "6.2.0", "file-loader": "6.2.0",
"mini-css-extract-plugin": "1.3.4", "mini-css-extract-plugin": "1.3.5",
"sass-loader": "10.1.1", "sass-loader": "10.1.1",
"terser-webpack-plugin": "5.1.1", "terser-webpack-plugin": "5.1.1",
"ts-loader": "8.0.14", "ts-loader": "8.0.14",
"webpack": "5.15.0", "webpack": "5.19.0",
"webpack-bundle-analyzer": "4.3.0", "webpack-bundle-analyzer": "4.4.0",
"webpack-cli": "4.3.1" "webpack-cli": "4.4.0"
}, },
"bugs": "https://github.com/excalidraw/excalidraw/issues", "bugs": "https://github.com/excalidraw/excalidraw/issues",
"repository": "https://github.com/excalidraw/excalidraw", "repository": "https://github.com/excalidraw/excalidraw",

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